Skip to content

Commit d0712e2

Browse files
Merge pull request #1782 from pybamm-team/spherical-finite-volume
Spherical finite volume
2 parents 7b748f8 + c67c3db commit d0712e2

File tree

11 files changed

+80
-99
lines changed

11 files changed

+80
-99
lines changed

CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,11 @@
11
# [Unreleased](https://github.com/pybamm-team/PyBaMM/)
22

3+
## Features
4+
5+
- Half-cell SPM and SPMe have been implemented ( [#1731](https://github.com/pybamm-team/PyBaMM/pull/1731))
36
## Bug fixes
47

8+
- Fixed finite volume discretization in spherical polar coordinates ([#1782](https://github.com/pybamm-team/PyBaMM/pull/1782))
59
- Fixed `sympy` operators for `Arctan` and `Exponential` ([#1786](https://github.com/pybamm-team/PyBaMM/pull/1786))
610

711
# [v21.10](https://github.com/pybamm-team/PyBaMM/tree/v21.9) - 2021-10-31

examples/notebooks/models/simulating-ORegan-2021-parameter-set.ipynb

Lines changed: 6 additions & 20 deletions
Large diffs are not rendered by default.

pybamm/models/full_battery_models/base_battery_model.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -414,8 +414,8 @@ def default_var_pts(self):
414414
var.x_n: 20,
415415
var.x_s: 20,
416416
var.x_p: 20,
417-
var.r_n: 30,
418-
var.r_p: 30,
417+
var.r_n: 20,
418+
var.r_p: 20,
419419
var.y: 10,
420420
var.z: 10,
421421
var.R_n: 30,

pybamm/spatial_methods/finite_volume.py

Lines changed: 29 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -171,18 +171,12 @@ def divergence(self, symbol, discretised_symbol, boundary_conditions):
171171
# check for particle domain
172172
if submesh.coord_sys == "spherical polar":
173173
second_dim_repeats = self._get_auxiliary_domain_repeats(symbol.domains)
174-
edges = submesh.edges
175174

176-
# create np.array of repeated submesh.nodes
177-
r_numpy = np.kron(np.ones(second_dim_repeats), submesh.nodes)
178-
r_edges_numpy = np.kron(np.ones(second_dim_repeats), edges)
179-
180-
r = pybamm.Vector(r_numpy)
175+
# create np.array of repeated submesh.edges
176+
r_edges_numpy = np.kron(np.ones(second_dim_repeats), submesh.edges)
181177
r_edges = pybamm.Vector(r_edges_numpy)
182178

183-
out = (1 / (r ** 2)) * (
184-
divergence_matrix @ ((r_edges ** 2) * discretised_symbol)
185-
)
179+
out = divergence_matrix @ ((r_edges ** 2) * discretised_symbol)
186180
else:
187181
out = divergence_matrix @ discretised_symbol
188182

@@ -205,7 +199,14 @@ def divergence_matrix(self, domains):
205199
"""
206200
# Create appropriate submesh by combining submeshes in domain
207201
submesh = self.mesh.combine_submeshes(*domains["primary"])
208-
e = 1 / submesh.d_edges
202+
if submesh.coord_sys == "spherical polar":
203+
r_edges_left = submesh.edges[:-1]
204+
r_edges_right = submesh.edges[1:]
205+
d_edges = (r_edges_right ** 3 - r_edges_left ** 3) / 3
206+
else:
207+
d_edges = submesh.d_edges
208+
209+
e = 1 / d_edges
209210

210211
# Create matrix using submesh
211212
n = submesh.npts + 1
@@ -234,17 +235,7 @@ def integral(self, child, discretised_child, integration_dimension):
234235
integration_vector = self.definite_integral_matrix(
235236
child, integration_dimension=integration_dimension
236237
)
237-
238-
# Check for spherical domains
239-
domain = child.domains[integration_dimension]
240-
submesh = self.mesh.combine_submeshes(*domain)
241-
if submesh.coord_sys == "spherical polar":
242-
second_dim_repeats = self._get_auxiliary_domain_repeats(child.domains)
243-
r_numpy = np.kron(np.ones(second_dim_repeats), submesh.nodes)
244-
r = pybamm.Vector(r_numpy)
245-
out = 4 * np.pi * integration_vector @ (discretised_child * r ** 2)
246-
else:
247-
out = integration_vector @ discretised_child
238+
out = integration_vector @ discretised_child
248239

249240
return out
250241

@@ -277,33 +268,40 @@ def definite_integral_matrix(
277268
The finite volume integral matrix for the domain
278269
"""
279270
domains = child.domains
271+
if vector_type != "row" and integration_dimension == "secondary":
272+
raise NotImplementedError(
273+
"Integral in secondary vector only implemented in 'row' form"
274+
)
275+
276+
domain = child.domains[integration_dimension]
277+
submesh = self.mesh.combine_submeshes(*domain)
278+
if submesh.coord_sys == "spherical polar":
279+
r_edges_left = submesh.edges[:-1]
280+
r_edges_right = submesh.edges[1:]
281+
d_edges = 4 * np.pi * (r_edges_right ** 3 - r_edges_left ** 3) / 3
282+
else:
283+
d_edges = submesh.d_edges
284+
280285
if integration_dimension == "primary":
281286
# Create appropriate submesh by combining submeshes in domain
282287
submesh = self.mesh.combine_submeshes(*domains["primary"])
283288

284289
# Create vector of ones for primary domain submesh
285-
vector = submesh.d_edges
286290

287291
if vector_type == "row":
288-
vector = vector[np.newaxis, :]
292+
d_edges = d_edges[np.newaxis, :]
289293
elif vector_type == "column":
290-
vector = vector[:, np.newaxis]
294+
d_edges = d_edges[:, np.newaxis]
291295

292296
# repeat matrix for each node in secondary dimensions
293297
second_dim_repeats = self._get_auxiliary_domain_repeats(domains)
294298
# generate full matrix from the submatrix
295-
matrix = kron(eye(second_dim_repeats), vector)
299+
matrix = kron(eye(second_dim_repeats), d_edges)
296300
elif integration_dimension == "secondary":
297-
if vector_type != "row":
298-
raise NotImplementedError(
299-
"Integral in secondary vector only implemented in 'row' form"
300-
)
301301
# Create appropriate submesh by combining submeshes in domain
302302
primary_submesh = self.mesh.combine_submeshes(*domains["primary"])
303-
secondary_submesh = self.mesh.combine_submeshes(*domains["secondary"])
304303

305304
# Create matrix which integrates in the secondary dimension
306-
d_edges = secondary_submesh.d_edges
307305
# Different number of edges depending on whether child evaluates on edges
308306
# in the primary dimensions
309307
if child.evaluates_on_edges("primary"):

pybamm/util.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -276,6 +276,7 @@ def load_function(filename):
276276
# Strip absolute path to pybamm/input/example.py
277277
if "pybamm" in filename:
278278
root_path = filename[filename.rfind("pybamm") :]
279+
# Commenting not removing these lines in case we get problems later
279280
elif os.getcwd() in filename:
280281
root_path = filename.replace(os.getcwd(), "")
281282
root_path = root_path[1:]

tests/integration/test_models/standard_output_tests.py

Lines changed: 8 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -377,23 +377,19 @@ def test_concentration_limits(self):
377377
def test_conservation(self):
378378
"""Test amount of lithium stored across all particles and in SEI layers is
379379
constant."""
380-
self.c_s_tot = (
380+
c_s_tot = (
381381
self.c_s_n_tot(self.solution.t)
382382
+ self.c_s_p_tot(self.solution.t)
383383
+ self.c_SEI_tot(self.solution.t)
384384
+ self.c_Li_tot(self.solution.t)
385385
)
386-
diff = (self.c_s_tot[1:] - self.c_s_tot[:-1]) / self.c_s_tot[:-1]
387-
if "profile" in self.model.options["particle"]:
388-
np.testing.assert_array_almost_equal(diff, 0, decimal=10)
389-
elif self.model.options["particle size"] == "distribution":
386+
diff = (c_s_tot[1:] - c_s_tot[:-1]) / c_s_tot[:-1]
387+
if self.model.options["particle"] == "quartic profile":
390388
np.testing.assert_array_almost_equal(diff, 0, decimal=10)
389+
# elif self.model.options["particle size"] == "distribution":
390+
# np.testing.assert_array_almost_equal(diff, 0, decimal=10)
391391
elif self.model.options["surface form"] == "differential":
392392
np.testing.assert_array_almost_equal(diff, 0, decimal=10)
393-
elif self.model.options["SEI"] == "ec reaction limited":
394-
np.testing.assert_array_almost_equal(diff, 0, decimal=11)
395-
elif self.model.options["lithium plating"] == "irreversible":
396-
np.testing.assert_array_almost_equal(diff, 0, decimal=13)
397393
else:
398394
np.testing.assert_array_almost_equal(diff, 0, decimal=15)
399395

@@ -779,9 +775,9 @@ def __init__(self, model, param, disc, solution, operating_condition):
779775

780776
def test_degradation_modes(self):
781777
"""Test degradation modes are between 0 and 100%"""
782-
np.testing.assert_array_less(-1e-3, self.LLI(self.t))
783-
np.testing.assert_array_less(-1e-3, self.LAM_ne(self.t))
784-
np.testing.assert_array_less(-1e-3, self.LAM_pe(self.t))
778+
np.testing.assert_array_less(-3e-3, self.LLI(self.t))
779+
np.testing.assert_array_less(-1e-13, self.LAM_ne(self.t))
780+
np.testing.assert_array_less(-1e-13, self.LAM_pe(self.t))
785781
np.testing.assert_array_less(self.LLI(self.t), 100)
786782
np.testing.assert_array_less(self.LAM_ne(self.t), 100)
787783
np.testing.assert_array_less(self.LAM_pe(self.t), 100)

tests/integration/test_models/test_full_battery_models/test_lithium_ion/test_mpm.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -81,8 +81,8 @@ def test_conservation_each_electrode(self):
8181
pos_Li.append(pos)
8282

8383
# compare
84-
np.testing.assert_array_almost_equal(neg_Li[0], neg_Li[1], decimal=14)
85-
np.testing.assert_array_almost_equal(pos_Li[0], pos_Li[1], decimal=14)
84+
np.testing.assert_array_almost_equal(neg_Li[0], neg_Li[1], decimal=13)
85+
np.testing.assert_array_almost_equal(pos_Li[0], pos_Li[1], decimal=13)
8686

8787

8888
if __name__ == "__main__":

tests/integration/test_spatial_methods/test_finite_volume.py

Lines changed: 11 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -126,11 +126,10 @@ def get_error(n):
126126
r_edge = pybamm.SpatialVariableEdge("r_n", domain=["negative particle"])
127127

128128
# Define flux and bcs
129-
N = r_edge ** 2 * pybamm.sin(r_edge)
129+
N = pybamm.sin(r_edge)
130130
div_eqn = pybamm.div(N)
131131
# Define exact solutions
132-
# N = r**3 --> div(N) = 5 * r**2
133-
div_exact = 4 * r * np.sin(r) + r ** 2 * np.cos(r)
132+
div_exact = 2 / r * np.sin(r) + np.cos(r)
134133

135134
# Discretise and evaluate
136135
div_eqn_disc = disc.process_symbol(div_eqn)
@@ -140,7 +139,7 @@ def get_error(n):
140139
return div_approx[:, 0] - div_exact
141140

142141
# Get errors
143-
ns = 10 * 2 ** np.arange(6)
142+
ns = 10 * 2 ** np.arange(1, 7)
144143
errs = {n: get_error(int(n)) for n in ns}
145144
# expect quadratic convergence everywhere
146145
err_norm = np.array([np.linalg.norm(errs[n], np.inf) for n in ns])
@@ -195,11 +194,12 @@ def get_error(m):
195194
r = submesh.nodes
196195
r_edge = pybamm.standard_spatial_vars.r_n_edge
197196

198-
N = r_edge ** 2 * pybamm.sin(r_edge)
197+
# N = r_edge ** 2 * pybamm.sin(r_edge)
198+
N = pybamm.sin(r_edge)
199199
div_eqn = pybamm.div(N)
200200
# Define exact solutions
201-
# N = r**2*sin(r) --> div(N) = 4*r*sin(r) - r**2*cos(r)
202-
div_exact = 4 * r * np.sin(r) + r ** 2 * np.cos(r)
201+
# N = sin(r) --> div(N) = 1/r2 * d/dr(r2*N) = 2/r*sin(r) + cos(r)
202+
div_exact = 2 / r * np.sin(r) + np.cos(r)
203203
div_exact = np.kron(np.ones(mesh["negative electrode"].npts), div_exact)
204204

205205
# Discretise and evaluate
@@ -209,7 +209,7 @@ def get_error(m):
209209
return div_approx[:, 0] - div_exact
210210

211211
# Get errors
212-
ns = 10 * 2 ** np.arange(6)
212+
ns = 10 * 2 ** np.arange(1, 7)
213213
errs = {n: get_error(int(n)) for n in ns}
214214
# expect quadratic convergence everywhere
215215
err_norm = np.array([np.linalg.norm(errs[n], np.inf) for n in ns])
@@ -233,13 +233,11 @@ def get_error(m):
233233
r_edge = pybamm.standard_spatial_vars.r_n_edge
234234
x = pybamm.standard_spatial_vars.x_n
235235

236-
N = pybamm.PrimaryBroadcast(x, "negative particle") * (
237-
r_edge ** 2 * pybamm.sin(r_edge)
238-
)
236+
N = pybamm.PrimaryBroadcast(x, "negative particle") * pybamm.sin(r_edge)
239237
div_eqn = pybamm.div(N)
240238
# Define exact solutions
241239
# N = r**2*sin(r) --> div(N) = 4*r*sin(r) - r**2*cos(r)
242-
div_exact = 4 * r * np.sin(r) + r ** 2 * np.cos(r)
240+
div_exact = 2 / r * np.sin(r) + np.cos(r)
243241
div_exact = np.kron(mesh["negative electrode"].nodes, div_exact)
244242

245243
# Discretise and evaluate
@@ -249,7 +247,7 @@ def get_error(m):
249247
return div_approx[:, 0] - div_exact
250248

251249
# Get errors
252-
ns = 10 * 2 ** np.arange(6)
250+
ns = 10 * 2 ** np.arange(1, 7)
253251
errs = {n: get_error(int(n)) for n in ns}
254252
# expect quadratic convergence everywhere
255253
err_norm = np.array([np.linalg.norm(errs[n], np.inf) for n in ns])

tests/integration/test_spatial_methods/test_spectral_volume.py

Lines changed: 11 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -182,7 +182,7 @@ def get_error(n):
182182
np.testing.assert_array_less(1.99 * np.ones_like(rates), rates)
183183

184184
def test_spherical_div_convergence_quadratic(self):
185-
# test div( r**2 * sin(r) ) == 4*r*sin(r) - r**2*cos(r)
185+
# test div( r**2 * sin(r) ) == 2/r*sin(r) + cos(r)
186186
spatial_methods = {"negative particle": pybamm.SpectralVolume()}
187187

188188
# Function for convergence testing
@@ -195,11 +195,11 @@ def get_error(n):
195195
r_edge = pybamm.SpatialVariableEdge("r_n", domain=["negative particle"])
196196

197197
# Define flux and bcs
198-
N = r_edge ** 2 * pybamm.sin(r_edge)
198+
N = pybamm.sin(r_edge)
199199
div_eqn = pybamm.div(N)
200200
# Define exact solutions
201201
# N = r**3 --> div(N) = 5 * r**2
202-
div_exact = 4 * r * np.sin(r) + r ** 2 * np.cos(r)
202+
div_exact = 2 / r * np.sin(r) + np.cos(r)
203203

204204
# Discretise and evaluate
205205
div_eqn_disc = disc.process_symbol(div_eqn)
@@ -252,7 +252,7 @@ def get_error(n):
252252
np.testing.assert_array_less(0.99 * np.ones_like(rates), rates)
253253

254254
def test_p2d_spherical_convergence_quadratic(self):
255-
# test div( r**2 * sin(r) ) == 4*r*sin(r) - r**2*cos(r)
255+
# test div( r**2 * sin(r) ) == 2/r*sin(r) + cos(r)
256256
spatial_methods = {"negative particle": pybamm.SpectralVolume()}
257257

258258
# Function for convergence testing
@@ -264,11 +264,11 @@ def get_error(m):
264264
r = submesh.nodes
265265
r_edge = pybamm.standard_spatial_vars.r_n_edge
266266

267-
N = r_edge ** 2 * pybamm.sin(r_edge)
267+
N = pybamm.sin(r_edge)
268268
div_eqn = pybamm.div(N)
269269
# Define exact solutions
270-
# N = r**2*sin(r) --> div(N) = 4*r*sin(r) - r**2*cos(r)
271-
div_exact = 4 * r * np.sin(r) + r ** 2 * np.cos(r)
270+
# N = r**2*sin(r) --> div(N) = 2/r*sin(r) + cos(r)
271+
div_exact = 2 / r * np.sin(r) + np.cos(r)
272272
div_exact = np.kron(np.ones(mesh["negative electrode"].npts), div_exact)
273273

274274
# Discretise and evaluate
@@ -286,7 +286,7 @@ def get_error(m):
286286
np.testing.assert_array_less(1.99 * np.ones_like(rates), rates)
287287

288288
def test_p2d_with_x_dep_bcs_spherical_convergence(self):
289-
# test div_r( (r**2 * sin(r)) * x ) == (4*r*sin(r) - r**2*cos(r)) * x
289+
# test div_r( sin(r) * x ) == (2/r*sin(r) + cos(r)) * x
290290
spatial_methods = {
291291
"negative particle": pybamm.SpectralVolume(),
292292
"negative electrode": pybamm.SpectralVolume(),
@@ -302,13 +302,11 @@ def get_error(m):
302302
r_edge = pybamm.standard_spatial_vars.r_n_edge
303303
x = pybamm.standard_spatial_vars.x_n
304304

305-
N = pybamm.PrimaryBroadcast(x, "negative particle") * (
306-
r_edge ** 2 * pybamm.sin(r_edge)
307-
)
305+
N = pybamm.PrimaryBroadcast(x, "negative particle") * (pybamm.sin(r_edge))
308306
div_eqn = pybamm.div(N)
309307
# Define exact solutions
310-
# N = r**2*sin(r) --> div(N) = 4*r*sin(r) - r**2*cos(r)
311-
div_exact = 4 * r * np.sin(r) + r ** 2 * np.cos(r)
308+
# N = sin(r) --> div(N) = 2/r*sin(r) + cos(r)
309+
div_exact = 2 / r * np.sin(r) + np.cos(r)
312310
div_exact = np.kron(mesh["negative electrode"].nodes, div_exact)
313311

314312
# Discretise and evaluate

tests/unit/test_models/test_full_battery_models/test_base_battery_model.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -124,8 +124,8 @@ def test_default_var_pts(self):
124124
var.x_n: 20,
125125
var.x_s: 20,
126126
var.x_p: 20,
127-
var.r_n: 30,
128-
var.r_p: 30,
127+
var.r_n: 20,
128+
var.r_p: 20,
129129
var.y: 10,
130130
var.z: 10,
131131
var.R_n: 30,

0 commit comments

Comments
 (0)