Skip to content

Commit b483d9c

Browse files
Merge pull request #2773 from pybamm-team/issue-2760-step
Issue 2760 step
2 parents 09944fd + 7e2e0ed commit b483d9c

File tree

12 files changed

+92
-77
lines changed

12 files changed

+92
-77
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818

1919
## Breaking changes
2020

21+
- When using `solver.step()`, the first time point in the step is shifted by `pybamm.settings.step_start_offset` (default 1 ns) to avoid having duplicate times in the solution steps from the end of one step and the start of the next. ([#2773](https://github.com/pybamm-team/PyBaMM/pull/2773))
2122
- Renamed "Measured open circuit voltage [V]" to "Surface open-circuit voltage [V]". This variable was calculated from surface particle concentrations, and hence "hid" the overpotential from particle gradients. The new variable "Open-circuit voltage [V]" is calculated from bulk particle concentrations instead. ([#2740](https://github.com/pybamm-team/PyBaMM/pull/2740))
2223
- Renamed all references to "open circuit" to be "open-circuit" instead. ([#2740](https://github.com/pybamm-team/PyBaMM/pull/2740))
2324
- Renamed parameter "1 + dlnf/dlnc" to "Thermodynamic factor". ([#2727](https://github.com/pybamm-team/PyBaMM/pull/2727))

pybamm/settings.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ class Settings(object):
1212
_abs_smoothing = "exact"
1313
max_words_in_line = 4
1414
max_y_value = 1e5
15+
step_start_offset = 1e-9
1516
tolerances = {
1617
"D_e__c_e": 10, # dimensional
1718
"kappa_e__c_e": 10, # dimensional

pybamm/solvers/base_solver.py

Lines changed: 27 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1085,18 +1085,34 @@ def step(
10851085
"Cannot step empty model, use `pybamm.DummySolver` instead"
10861086
)
10871087

1088-
# Make sure dt is positive
1089-
if dt <= 0:
1090-
raise pybamm.SolverError("Step time must be positive")
1088+
# Make sure dt is greater than the offset
1089+
step_start_offset = pybamm.settings.step_start_offset
1090+
if dt <= step_start_offset:
1091+
raise pybamm.SolverError(
1092+
f"Step time must be at least {pybamm.TimerTime(step_start_offset)}"
1093+
)
1094+
1095+
t_start = old_solution.t[-1]
1096+
t_end = t_start + dt
1097+
# Calculate t_eval
1098+
t_eval = np.linspace(t_start, t_end, npts)
1099+
1100+
if t_start == 0:
1101+
t_start_shifted = t_start
1102+
else:
1103+
# offset t_start by t_start_offset (default 1 ns)
1104+
# to avoid repeated times in the solution
1105+
# from having the same time at the end of the previous step and
1106+
# the start of the next step
1107+
t_start_shifted = t_start + step_start_offset
1108+
t_eval[0] = t_start_shifted
10911109

10921110
# Set timer
10931111
timer = pybamm.Timer()
10941112

10951113
# Set up inputs
10961114
model_inputs = self._set_up_model_inputs(model, inputs)
10971115

1098-
t = old_solution.t[-1]
1099-
11001116
first_step_this_model = False
11011117
if model not in self._model_set_up:
11021118
first_step_this_model = True
@@ -1139,16 +1155,17 @@ def step(
11391155
set_up_time = timer.time()
11401156

11411157
# (Re-)calculate consistent initial conditions
1142-
self._set_initial_conditions(model, t, model_inputs, update_rhs=False)
1143-
1144-
# Calculate t_eval
1145-
t_eval = np.linspace(t, t + dt, npts)
1158+
self._set_initial_conditions(
1159+
model, t_start_shifted, model_inputs, update_rhs=False
1160+
)
11461161

11471162
# Check initial conditions don't violate events
11481163
self._check_events_with_initial_conditions(t_eval, model, model_inputs)
11491164

11501165
# Step
1151-
pybamm.logger.verbose("Stepping for {:.0f} < t < {:.0f}".format(t, (t + dt)))
1166+
pybamm.logger.verbose(
1167+
"Stepping for {:.0f} < t < {:.0f}".format(t_start_shifted, t_end)
1168+
)
11521169
timer.reset()
11531170
solution = self._integrate(model, t_eval, model_inputs)
11541171
solution.solve_time = timer.time()

tests/integration/test_experiments.py

Lines changed: 5 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -14,20 +14,21 @@ def test_discharge_rest_charge(self):
1414
"Rest for 1 hour",
1515
"Charge at C/2 for 1 hour",
1616
],
17-
period="0.25 hours",
17+
period="0.5 hours",
1818
)
1919
model = pybamm.lithium_ion.SPM()
2020
sim = pybamm.Simulation(
2121
model, experiment=experiment, solver=pybamm.CasadiSolver()
2222
)
2323
sim.solve()
2424
np.testing.assert_array_almost_equal(
25-
sim._solution["Time [h]"].entries, np.linspace(0, 3, 13)
25+
sim._solution["Time [h]"].entries,
26+
np.array([0, 0.5, 1, 1 + 1e-9, 1.5, 2, 2 + 1e-9, 2.5, 3]),
2627
)
2728
cap = model.default_parameter_values["Nominal cell capacity [A.h]"]
2829
np.testing.assert_array_almost_equal(
2930
sim._solution["Current [A]"].entries,
30-
[cap / 2] * 5 + [0] * 4 + [-cap / 2] * 4,
31+
[cap / 2] * 3 + [0] * 3 + [-cap / 2] * 3,
3132
)
3233

3334
def test_rest_discharge_rest(self):
@@ -57,13 +58,10 @@ def test_gitt(self):
5758
model, experiment=experiment, solver=pybamm.CasadiSolver()
5859
)
5960
sim.solve()
60-
np.testing.assert_array_almost_equal(
61-
sim._solution["Time [h]"].entries, np.arange(0, 20.01, 0.1)
62-
)
6361
cap = model.default_parameter_values["Nominal cell capacity [A.h]"]
6462
np.testing.assert_array_almost_equal(
6563
sim._solution["Current [A]"].entries,
66-
[cap / 20] * 11 + [0] * 10 + ([cap / 20] * 10 + [0] * 10) * 9,
64+
[cap / 20] * 11 + [0] * 11 + ([cap / 20] * 11 + [0] * 11) * 9,
6765
)
6866

6967
def test_infeasible(self):

tests/unit/test_experiments/test_simulation_with_experiment.py

Lines changed: 7 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -176,22 +176,18 @@ def test_run_experiment_cccv_ode(self):
176176
solution = sim.solve(solver=pybamm.CasadiSolver("fast with events"))
177177
solutions.append(solution)
178178

179+
t = solutions[1]["Time [s]"].data
179180
np.testing.assert_array_almost_equal(
180-
solutions[0]["Voltage [V]"].data,
181-
solutions[1]["Voltage [V]"].data,
181+
solutions[0]["Voltage [V]"](t=t),
182+
solutions[1]["Voltage [V]"](t=t),
182183
decimal=1,
183184
)
184185
np.testing.assert_array_almost_equal(
185-
solutions[0]["Current [A]"].data,
186-
solutions[1]["Current [A]"].data,
186+
solutions[0]["Current [A]"](t=t),
187+
solutions[1]["Current [A]"](t=t),
187188
decimal=0,
188189
)
189190

190-
np.testing.assert_array_equal(
191-
solutions[0]["Ambient temperature [C]"].data,
192-
solutions[1]["Ambient temperature [C]"].data,
193-
)
194-
195191
self.assertEqual(solutions[1].termination, "final time")
196192

197193
@unittest.skipIf(not pybamm.have_idaklu(), "idaklu solver is not installed")
@@ -516,12 +512,13 @@ def test_run_experiment_skip_steps(self):
516512
)
517513
sol2 = sim2.solve()
518514
np.testing.assert_array_almost_equal(
519-
sol["Voltage [V]"].data, sol2["Voltage [V]"].data
515+
sol["Voltage [V]"].data, sol2["Voltage [V]"].data, decimal=5
520516
)
521517
for idx1, idx2 in [(1, 0), (2, 1), (4, 2)]:
522518
np.testing.assert_array_almost_equal(
523519
sol.cycles[0].steps[idx1]["Voltage [V]"].data,
524520
sol2.cycles[0].steps[idx2]["Voltage [V]"].data,
521+
decimal=5,
525522
)
526523

527524
def test_all_empty_solution_errors(self):

tests/unit/test_simulation.py

Lines changed: 17 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -148,34 +148,27 @@ def test_step(self):
148148
sim = pybamm.Simulation(model)
149149

150150
sim.step(dt) # 1 step stores first two points
151-
self.assertEqual(sim.solution.t.size, 2)
152151
self.assertEqual(sim.solution.y.full()[0, :].size, 2)
153-
self.assertEqual(sim.solution.t[0], 0)
154-
self.assertEqual(sim.solution.t[1], dt)
152+
np.testing.assert_array_almost_equal(sim.solution.t, np.array([0, dt]))
155153
saved_sol = sim.solution
156154

157155
sim.step(dt) # automatically append the next step
158-
self.assertEqual(sim.solution.t.size, 3)
159-
self.assertEqual(sim.solution.y.full()[0, :].size, 3)
160-
self.assertEqual(sim.solution.t[0], 0)
161-
self.assertEqual(sim.solution.t[1], dt)
162-
self.assertEqual(sim.solution.t[2], 2 * dt)
156+
self.assertEqual(sim.solution.y.full()[0, :].size, 4)
157+
np.testing.assert_array_almost_equal(
158+
sim.solution.t, np.array([0, dt, dt + 1e-9, 2 * dt])
159+
)
163160

164161
sim.step(dt, save=False) # now only store the two end step points
165-
self.assertEqual(sim.solution.t.size, 2)
166162
self.assertEqual(sim.solution.y.full()[0, :].size, 2)
167-
self.assertEqual(sim.solution.t[0], 2 * dt)
168-
self.assertEqual(sim.solution.t[1], 3 * dt)
169-
163+
np.testing.assert_array_almost_equal(
164+
sim.solution.t, np.array([2 * dt + 1e-9, 3 * dt])
165+
)
170166
# Start from saved solution
171-
sim.step(
172-
dt, starting_solution=saved_sol
173-
) # now only store the two end step points
174-
self.assertEqual(sim.solution.t.size, 3)
175-
self.assertEqual(sim.solution.y.full()[0, :].size, 3)
176-
self.assertEqual(sim.solution.t[0], 0)
177-
self.assertEqual(sim.solution.t[1], dt)
178-
self.assertEqual(sim.solution.t[2], 2 * dt)
167+
sim.step(dt, starting_solution=saved_sol)
168+
self.assertEqual(sim.solution.y.full()[0, :].size, 4)
169+
np.testing.assert_array_almost_equal(
170+
sim.solution.t, np.array([0, dt, dt + 1e-9, 2 * dt])
171+
)
179172

180173
def test_solve_with_initial_soc(self):
181174
model = pybamm.lithium_ion.SPM()
@@ -238,11 +231,10 @@ def test_step_with_inputs(self):
238231
sim.step(
239232
dt, inputs={"Current function [A]": 2}
240233
) # automatically append the next step
241-
self.assertEqual(sim.solution.t.size, 3)
242-
self.assertEqual(sim.solution.y.full()[0, :].size, 3)
243-
self.assertEqual(sim.solution.t[0], 0)
244-
self.assertEqual(sim.solution.t[1], dt)
245-
self.assertEqual(sim.solution.t[2], 2 * dt)
234+
self.assertEqual(sim.solution.y.full()[0, :].size, 4)
235+
np.testing.assert_array_almost_equal(
236+
sim.solution.t, np.array([0, dt, dt + 1e-9, 2 * dt])
237+
)
246238
np.testing.assert_array_equal(
247239
sim.solution.all_inputs[1]["Current function [A]"], 2
248240
)

tests/unit/test_solvers/test_base_solver.py

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -67,9 +67,11 @@ def test_nonmonotonic_teval(self):
6767
):
6868
solver.solve(model, np.array([1, 2, 3, 2]))
6969

70-
# Check stepping with negative step size
71-
dt = -1
72-
with self.assertRaisesRegex(pybamm.SolverError, "Step time must be positive"):
70+
# Check stepping with step size too small
71+
dt = 1e-9
72+
with self.assertRaisesRegex(
73+
pybamm.SolverError, "Step time must be at least 1.000 ns"
74+
):
7375
solver.step(None, model, dt)
7476

7577
def test_solution_time_length_fail(self):

tests/unit/test_solvers/test_casadi_solver.py

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -292,7 +292,7 @@ def test_model_step(self):
292292
# Step again (return 5 points)
293293
step_sol_2 = solver.step(step_sol, model, dt, npts=5)
294294
np.testing.assert_array_equal(
295-
step_sol_2.t, np.concatenate([np.array([0]), np.linspace(dt, 2 * dt, 5)])
295+
step_sol_2.t, np.array([0, 1, 1 + 1e-9, 1.25, 1.5, 1.75, 2])
296296
)
297297
np.testing.assert_array_almost_equal(
298298
step_sol_2.y.full()[0], np.exp(0.1 * step_sol_2.t)
@@ -322,16 +322,21 @@ def test_model_step_with_input(self):
322322

323323
# Step again with different inputs
324324
step_sol_2 = solver.step(step_sol, model, dt, npts=5, inputs={"a": -1})
325-
np.testing.assert_array_equal(step_sol_2.t, np.linspace(0, 2 * dt, 9))
325+
np.testing.assert_array_almost_equal(
326+
step_sol_2.t,
327+
np.array([0, 0.025, 0.05, 0.075, 0.1, 0.1 + 1e-9, 0.125, 0.15, 0.175, 0.2]),
328+
)
326329
np.testing.assert_array_equal(
327-
step_sol_2["a"].entries, np.array([0.1, 0.1, 0.1, 0.1, 0.1, -1, -1, -1, -1])
330+
step_sol_2["a"].entries,
331+
np.array([0.1, 0.1, 0.1, 0.1, 0.1, -1, -1, -1, -1, -1]),
328332
)
329333
np.testing.assert_allclose(
330334
step_sol_2.y.full()[0],
331335
np.concatenate(
332336
[
333337
np.exp(0.1 * step_sol_2.t[:5]),
334-
np.exp(0.1 * step_sol_2.t[4]) * np.exp(-(step_sol_2.t[5:] - dt)),
338+
np.exp(0.1 * step_sol_2.t[4])
339+
* np.exp(-(step_sol_2.t[5:] - step_sol_2.t[5])),
335340
]
336341
),
337342
)

tests/unit/test_solvers/test_dummy_solver.py

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -38,9 +38,11 @@ def test_dummy_solver_step(self):
3838
for dt in np.diff(t_eval):
3939
sol = solver.step(sol, model, dt)
4040

41-
np.testing.assert_array_equal(sol.t, t_eval)
42-
np.testing.assert_array_equal(sol.y, np.zeros((1, t_eval.size)))
43-
np.testing.assert_array_equal(sol["v"].data, np.ones(t_eval.size))
41+
# if t_eval is [0,1,2,3]
42+
# sol.t should be [0,1,1+eps,2,2+eps,3] i.e. size 2n-2
43+
np.testing.assert_array_equal(len(sol.t), t_eval.size * 2 - 2)
44+
np.testing.assert_array_equal(sol.y, np.zeros((1, sol.t.size)))
45+
np.testing.assert_array_equal(sol["v"].data, np.ones(sol.t.size))
4446

4547

4648
if __name__ == "__main__":

tests/unit/test_solvers/test_scikits_solvers.py

Lines changed: 12 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -532,14 +532,14 @@ def test_model_step_ode_python(self):
532532
# Step again (return 5 points)
533533
step_sol_2 = solver.step(step_sol, model, dt, npts=5)
534534
np.testing.assert_array_equal(
535-
step_sol_2.t, np.concatenate([np.array([0]), np.linspace(dt, 2 * dt, 5)])
535+
step_sol_2.t, np.array([0, 1, 1 + 1e-9, 1.25, 1.5, 1.75, 2])
536536
)
537537
np.testing.assert_allclose(step_sol_2.y[0], np.exp(-0.1 * step_sol_2.t))
538538

539539
# Check steps give same solution as solve
540540
t_eval = step_sol.t
541541
solution = solver.solve(model, t_eval)
542-
np.testing.assert_allclose(solution.y[0], step_sol.y[0])
542+
np.testing.assert_allclose(solution.y[0], step_sol.y[0], atol=1e-6, rtol=1e-6)
543543

544544
def test_model_step_dae_python(self):
545545
model = pybamm.BaseModel()
@@ -560,22 +560,22 @@ def test_model_step_dae_python(self):
560560
dt = 1
561561
step_sol = solver.step(None, model, dt)
562562
np.testing.assert_array_equal(step_sol.t, [0, dt])
563-
np.testing.assert_allclose(step_sol.y[0], np.exp(0.1 * step_sol.t))
564-
np.testing.assert_allclose(step_sol.y[-1], 2 * np.exp(0.1 * step_sol.t))
563+
np.testing.assert_allclose(step_sol.y[0, :], np.exp(0.1 * step_sol.t))
564+
np.testing.assert_allclose(step_sol.y[-1, :], 2 * np.exp(0.1 * step_sol.t))
565565

566566
# Step again (return 5 points)
567567
step_sol_2 = solver.step(step_sol, model, dt, npts=5)
568568
np.testing.assert_array_equal(
569-
step_sol_2.t, np.concatenate([np.array([0]), np.linspace(dt, 2 * dt, 5)])
569+
step_sol_2.t, np.array([0, 1, 1 + 1e-9, 1.25, 1.5, 1.75, 2])
570570
)
571-
np.testing.assert_allclose(step_sol_2.y[0], np.exp(0.1 * step_sol_2.t))
572-
np.testing.assert_allclose(step_sol_2.y[-1], 2 * np.exp(0.1 * step_sol_2.t))
571+
np.testing.assert_allclose(step_sol_2.y[0, :], np.exp(0.1 * step_sol_2.t))
572+
np.testing.assert_allclose(step_sol_2.y[-1, :], 2 * np.exp(0.1 * step_sol_2.t))
573573

574574
# Check steps give same solution as solve
575575
t_eval = step_sol.t
576576
solution = solver.solve(model, t_eval)
577-
np.testing.assert_allclose(solution.y[0], step_sol.y[0, :])
578-
np.testing.assert_allclose(solution.y[-1], step_sol.y[-1, :])
577+
np.testing.assert_allclose(solution.y[0, :], step_sol.y[0, :])
578+
np.testing.assert_allclose(solution.y[-1, :], step_sol.y[-1, :])
579579

580580
def test_model_solver_ode_events_casadi(self):
581581
# Create model
@@ -598,7 +598,7 @@ def test_model_solver_ode_events_casadi(self):
598598
solution = solver.solve(model, t_eval)
599599
np.testing.assert_allclose(solution.y[0], np.exp(0.1 * solution.t))
600600
np.testing.assert_array_less(solution.y[0:, -1], 1.5)
601-
np.testing.assert_array_less(solution.y[0:, -1], 1.25 + 1e-6)
601+
np.testing.assert_array_less(solution.y[0:, -1], 1.25 + 1e-9)
602602
np.testing.assert_equal(solution.t_event[0], solution.t[-1])
603603
np.testing.assert_array_equal(solution.y_event[:, 0], solution.y[:, -1])
604604

@@ -794,8 +794,8 @@ def test_model_step_nonsmooth_events(self):
794794
)
795795
var1_soln = (step_solution.t % a) ** 2 / 2 + a**2 / 2 * (step_solution.t // a)
796796
var2_soln = 2 * var1_soln
797-
np.testing.assert_array_almost_equal(step_solution.y[0], var1_soln, decimal=5)
798-
np.testing.assert_array_almost_equal(step_solution.y[-1], var2_soln, decimal=5)
797+
np.testing.assert_array_almost_equal(step_solution.y[0], var1_soln, decimal=4)
798+
np.testing.assert_array_almost_equal(step_solution.y[-1], var2_soln, decimal=4)
799799

800800
def test_model_solver_dae_nonsmooth(self):
801801
whole_cell = ["negative electrode", "separator", "positive electrode"]

0 commit comments

Comments
 (0)