Skip to content

Commit 49aaff0

Browse files
Merge pull request #1014 from pybamm-team/issue-1005-backward-indefinite-integral
Issue 1005 backward indefinite integral
2 parents a45f180 + 14bf307 commit 49aaff0

File tree

11 files changed

+323
-78
lines changed

11 files changed

+323
-78
lines changed

CHANGELOG.md

+1
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
## Features
44

5+
- Added `BackwardIndefiniteIntegral` symbol ([#1014](https://github.com/pybamm-team/PyBaMM/pull/1014))
56
- Added SEI film resistance as an option ([#994](https://github.com/pybamm-team/PyBaMM/pull/994))
67
- Added tab, edge, and surface cooling ([#965](https://github.com/pybamm-team/PyBaMM/pull/965))
78
- Added functionality to solver to automatically discretise a 0D model ([#947](https://github.com/pybamm-team/PyBaMM/pull/947))

pybamm/discretisations/discretisation.py

+7-1
Original file line numberDiff line numberDiff line change
@@ -836,7 +836,13 @@ def _process_symbol(self, symbol):
836836
return child_spatial_method.boundary_mass_matrix(child, self.bcs)
837837

838838
elif isinstance(symbol, pybamm.IndefiniteIntegral):
839-
return child_spatial_method.indefinite_integral(child, disc_child)
839+
return child_spatial_method.indefinite_integral(
840+
child, disc_child, "forward"
841+
)
842+
elif isinstance(symbol, pybamm.BackwardIndefiniteIntegral):
843+
return child_spatial_method.indefinite_integral(
844+
child, disc_child, "backward"
845+
)
840846

841847
elif isinstance(symbol, pybamm.Integral):
842848
out = child_spatial_method.integral(child, disc_child)

pybamm/expression_tree/unary_operators.py

+64-12
Original file line numberDiff line numberDiff line change
@@ -510,14 +510,8 @@ def evaluates_on_edges(self):
510510
return False
511511

512512

513-
class IndefiniteIntegral(Integral):
514-
"""A node in the expression tree representing an indefinite integral operator
515-
516-
.. math::
517-
I = \\int_{x_\text{min}}^{x}\\!f(u)\\,du
518-
519-
where :math:`u\\in\\text{domain}` which can represent either a
520-
spatial or temporal variable.
513+
class BaseIndefiniteIntegral(Integral):
514+
"""Base class for indefinite integrals (forward or backward).
521515
522516
Parameters
523517
----------
@@ -540,15 +534,73 @@ def __init__(self, child, integration_variable):
540534
super().__init__(child, integration_variable)
541535
# overwrite domains with child domains
542536
self.copy_domains(child)
537+
538+
def _evaluate_for_shape(self):
539+
return self.children[0].evaluate_for_shape()
540+
541+
def evaluates_on_edges(self):
542+
# If child evaluates on edges, indefinite integral doesn't
543+
# If child doesn't evaluate on edges, indefinite integral does
544+
return not self.child.evaluates_on_edges()
545+
546+
547+
class IndefiniteIntegral(BaseIndefiniteIntegral):
548+
"""A node in the expression tree representing an indefinite integral operator
549+
550+
.. math::
551+
I = \\int_{x_\text{min}}^{x}\\!f(u)\\,du
552+
553+
where :math:`u\\in\\text{domain}` which can represent either a
554+
spatial or temporal variable.
555+
556+
Parameters
557+
----------
558+
function : :class:`pybamm.Symbol`
559+
The function to be integrated (will become self.children[0])
560+
integration_variable : :class:`pybamm.IndependentVariable`
561+
The variable over which to integrate
562+
563+
**Extends:** :class:`BaseIndefiniteIntegral`
564+
"""
565+
566+
def __init__(self, child, integration_variable):
567+
super().__init__(child, integration_variable)
543568
# Overwrite the name
544569
self.name = "{} integrated w.r.t {}".format(
545-
child.name, integration_variable.name
570+
child.name, self.integration_variable[0].name
546571
)
547572
if isinstance(integration_variable, pybamm.SpatialVariable):
548-
self.name += " on {}".format(integration_variable.domain)
573+
self.name += " on {}".format(self.integration_variable[0].domain)
549574

550-
def _evaluate_for_shape(self):
551-
return self.children[0].evaluate_for_shape()
575+
576+
class BackwardIndefiniteIntegral(BaseIndefiniteIntegral):
577+
"""A node in the expression tree representing a backward indefinite integral
578+
operator
579+
580+
.. math::
581+
I = \\int_{x}^{x_\text{max}}\\!f(u)\\,du
582+
583+
where :math:`u\\in\\text{domain}` which can represent either a
584+
spatial or temporal variable.
585+
586+
Parameters
587+
----------
588+
function : :class:`pybamm.Symbol`
589+
The function to be integrated (will become self.children[0])
590+
integration_variable : :class:`pybamm.IndependentVariable`
591+
The variable over which to integrate
592+
593+
**Extends:** :class:`BaseIndefiniteIntegral`
594+
"""
595+
596+
def __init__(self, child, integration_variable):
597+
super().__init__(child, integration_variable)
598+
# Overwrite the name
599+
self.name = "{} integrated backward w.r.t {}".format(
600+
child.name, self.integration_variable[0].name
601+
)
602+
if isinstance(integration_variable, pybamm.SpatialVariable):
603+
self.name += " on {}".format(self.integration_variable[0].domain)
552604

553605

554606
class DefiniteIntegralVector(SpatialOperator):

pybamm/spatial_methods/finite_volume.py

+104-52
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55

66
from scipy.sparse import (
77
diags,
8+
spdiags,
89
eye,
910
kron,
1011
csr_matrix,
@@ -248,6 +249,8 @@ def integral(self, child, discretised_child):
248249
else:
249250
out = integration_vector @ discretised_child
250251

252+
out.copy_domains(child)
253+
251254
return out
252255

253256
def definite_integral_matrix(self, domain, vector_type="row"):
@@ -296,15 +299,19 @@ def definite_integral_matrix(self, domain, vector_type="row"):
296299
matrix = csr_matrix(kron(eye(second_dim_len), vector))
297300
return pybamm.Matrix(matrix)
298301

299-
def indefinite_integral(self, child, discretised_child):
302+
def indefinite_integral(self, child, discretised_child, direction):
300303
"""Implementation of the indefinite integral operator. """
301304

302305
# Different integral matrix depending on whether the integrand evaluates on
303306
# edges or nodes
304307
if child.evaluates_on_edges():
305-
integration_matrix = self.indefinite_integral_matrix_edges(child.domain)
308+
integration_matrix = self.indefinite_integral_matrix_edges(
309+
child.domain, direction
310+
)
306311
else:
307-
integration_matrix = self.indefinite_integral_matrix_nodes(child.domain)
312+
integration_matrix = self.indefinite_integral_matrix_nodes(
313+
child.domain, direction
314+
)
308315

309316
# Don't need to check for spherical domains as spherical polars
310317
# only change the diveregence (childs here have grad and no div)
@@ -314,10 +321,28 @@ def indefinite_integral(self, child, discretised_child):
314321

315322
return out
316323

317-
def indefinite_integral_matrix_edges(self, domain):
324+
def indefinite_integral_matrix_edges(self, domain, direction):
318325
"""
319326
Matrix for finite-volume implementation of the indefinite integral where the
320-
integrand is evaluated on mesh edges
327+
integrand is evaluated on mesh edges (shape (n+1, 1)).
328+
The integral will then be evaluated on mesh nodes (shape (n, 1)).
329+
330+
Parameters
331+
----------
332+
domain : list
333+
The domain(s) of integration
334+
direction : str
335+
The direction of integration (forward or backward). See notes.
336+
337+
Returns
338+
-------
339+
:class:`pybamm.Matrix`
340+
The finite volume integral matrix for the domain
341+
342+
Notes
343+
-----
344+
345+
**Forward integral**
321346
322347
.. math::
323348
F(x) = \\int_0^x\\!f(u)\\,du
@@ -335,16 +360,81 @@ def indefinite_integral_matrix_edges(self, domain):
335360
Hence we must have
336361
337362
- :math:`F_0 = du_{1/2} * f_{1/2} / 2`
338-
- :math:`F_{i+1} = F_i + du * f_{i+1/2}`
363+
- :math:`F_{i+1} = F_i + du_{i+1/2} * f_{i+1/2}`
364+
365+
Note that :math:`f_{-1/2}` and :math:`f_{end+1/2}` are included in the discrete
366+
integrand vector `f`, so we add a column of zeros at each end of the
367+
indefinite integral matrix to ignore these.
368+
369+
**Backward integral**
370+
371+
.. math::
372+
F(x) = \\int_x^end\\!f(u)\\,du
339373
340-
Note that :math:`f_{-1/2}` and :math:`f_{n+1/2}` are included in the discrete
374+
The indefinite integral must satisfy the following conditions:
375+
376+
- :math:`F(end) = 0`
377+
- :math:`f(x) = -\\frac{dF}{dx}`
378+
379+
or, in discrete form,
380+
381+
- `BoundaryValue(F, "right") = 0`, i.e. :math:`3*F_{end} - F_{end-1} = 0`
382+
- :math:`f_{i+1/2} = -(F_{i+1} - F_i) / dx_{i+1/2}`
383+
384+
Hence we must have
385+
386+
- :math:`F_{end} = du_{end+1/2} * f_{end-1/2} / 2`
387+
- :math:`F_{i-1} = F_i + du_{i-1/2} * f_{i-1/2}`
388+
389+
Note that :math:`f_{-1/2}` and :math:`f_{end+1/2}` are included in the discrete
341390
integrand vector `f`, so we add a column of zeros at each end of the
342391
indefinite integral matrix to ignore these.
392+
"""
393+
394+
# Create appropriate submesh by combining submeshes in domain
395+
submesh_list = self.mesh.combine_submeshes(*domain)
396+
submesh = submesh_list[0]
397+
n = submesh.npts
398+
sec_pts = len(submesh_list)
399+
400+
du_n = submesh.d_nodes
401+
if direction == "forward":
402+
du_entries = [du_n] * (n - 1)
403+
offset = -np.arange(1, n, 1)
404+
main_integral_matrix = spdiags(du_entries, offset, n, n - 1)
405+
bc_offset_matrix = lil_matrix((n, n - 1))
406+
bc_offset_matrix[:, 0] = du_n[0] / 2
407+
elif direction == "backward":
408+
du_entries = [du_n] * (n + 1)
409+
offset = np.arange(n, -1, -1)
410+
main_integral_matrix = spdiags(du_entries, offset, n, n - 1)
411+
bc_offset_matrix = lil_matrix((n, n - 1))
412+
bc_offset_matrix[:, -1] = du_n[-1] / 2
413+
sub_matrix = main_integral_matrix + bc_offset_matrix
414+
# add a column of zeros at each end
415+
zero_col = csr_matrix((n, 1))
416+
sub_matrix = hstack([zero_col, sub_matrix, zero_col])
417+
# Convert to csr_matrix so that we can take the index (row-slicing), which is
418+
# not supported by the default kron format
419+
# Note that this makes column-slicing inefficient, but this should not be an
420+
# issue
421+
matrix = csr_matrix(kron(eye(sec_pts), sub_matrix))
422+
423+
return pybamm.Matrix(matrix)
424+
425+
def indefinite_integral_matrix_nodes(self, domain, direction):
426+
"""
427+
Matrix for finite-volume implementation of the (backward) indefinite integral
428+
where the integrand is evaluated on mesh nodes (shape (n, 1)).
429+
The integral will then be evaluated on mesh edges (shape (n+1, 1)).
430+
This is just a straightforward (backward) cumulative sum of the integrand
343431
344432
Parameters
345433
----------
346434
domain : list
347435
The domain(s) of integration
436+
direction : str
437+
The direction of integration (forward or backward)
348438
349439
Returns
350440
-------
@@ -358,16 +448,13 @@ def indefinite_integral_matrix_edges(self, domain):
358448
n = submesh.npts
359449
sec_pts = len(submesh_list)
360450

361-
du_n = submesh.d_nodes
362-
du_entries = [du_n] * (n - 1)
363-
offset = -np.arange(1, n, 1)
364-
main_integral_matrix = diags(du_entries, offset, shape=(n, n - 1))
365-
bc_offset_matrix = lil_matrix((n, n - 1))
366-
bc_offset_matrix[:, 0] = du_n[0] / 2
367-
sub_matrix = main_integral_matrix + bc_offset_matrix
368-
# add a column of zeros at each end
369-
zero_col = csr_matrix((n, 1))
370-
sub_matrix = hstack([zero_col, sub_matrix, zero_col])
451+
du_n = submesh.d_edges
452+
du_entries = [du_n] * n
453+
if direction == "forward":
454+
offset = -np.arange(1, n + 1, 1) # from -1 down to -n
455+
elif direction == "backward":
456+
offset = np.arange(n - 1, -1, -1) # from n-1 down to 0
457+
sub_matrix = spdiags(du_entries, offset, n + 1, n)
371458
# Convert to csr_matrix so that we can take the index (row-slicing), which is
372459
# not supported by the default kron format
373460
# Note that this makes column-slicing inefficient, but this should not be an
@@ -473,41 +560,6 @@ def internal_neumann_condition(
473560

474561
return dy / dx
475562

476-
def indefinite_integral_matrix_nodes(self, domain):
477-
"""
478-
Matrix for finite-volume implementation of the indefinite integral where the
479-
integrand is evaluated on mesh nodes.
480-
This is just a straightforward cumulative sum of the integrand
481-
482-
Parameters
483-
----------
484-
domain : list
485-
The domain(s) of integration
486-
487-
Returns
488-
-------
489-
:class:`pybamm.Matrix`
490-
The finite volume integral matrix for the domain
491-
"""
492-
493-
# Create appropriate submesh by combining submeshes in domain
494-
submesh_list = self.mesh.combine_submeshes(*domain)
495-
submesh = submesh_list[0]
496-
n = submesh.npts
497-
sec_pts = len(submesh_list)
498-
499-
du_n = submesh.d_edges
500-
du_entries = [du_n] * (n)
501-
offset = -np.arange(1, n + 1, 1)
502-
sub_matrix = diags(du_entries, offset, shape=(n + 1, n))
503-
# Convert to csr_matrix so that we can take the index (row-slicing), which is
504-
# not supported by the default kron format
505-
# Note that this makes column-slicing inefficient, but this should not be an
506-
# issue
507-
matrix = csr_matrix(kron(eye(sec_pts), sub_matrix))
508-
509-
return pybamm.Matrix(matrix)
510-
511563
def add_ghost_nodes(self, symbol, discretised_symbol, bcs):
512564
"""
513565
Add ghost nodes to a symbol.

pybamm/spatial_methods/spatial_method.py

+3-1
Original file line numberDiff line numberDiff line change
@@ -242,7 +242,7 @@ def integral(self, child, discretised_child):
242242
"""
243243
raise NotImplementedError
244244

245-
def indefinite_integral(self, child, discretised_child):
245+
def indefinite_integral(self, child, discretised_child, direction):
246246
"""
247247
Implements the indefinite integral for a spatial method.
248248
@@ -252,6 +252,8 @@ def indefinite_integral(self, child, discretised_child):
252252
The symbol to which is being integrated
253253
discretised_child: :class:`pybamm.Symbol`
254254
The discretised symbol of the correct size
255+
direction : str
256+
The direction of integration
255257
256258
Returns
257259
-------

pybamm/spatial_methods/zero_dimensional_method.py

+8-3
Original file line numberDiff line numberDiff line change
@@ -37,11 +37,16 @@ def mass_matrix(self, symbol, boundary_conditions):
3737
"""
3838
return pybamm.Matrix(np.ones((1, 1)))
3939

40-
def indefinite_integral(self, child, discretised_child):
40+
def indefinite_integral(self, child, discretised_child, direction):
4141
"""
42-
Calculates the zero-dimensional indefinite integral, i.e. the identity operator
42+
Calculates the zero-dimensional indefinite integral.
43+
If 'direction' is forward, this is the identity operator.
44+
If 'direction' is backward, this is the negation operator.
4345
"""
44-
return discretised_child
46+
if direction == "forward":
47+
return discretised_child
48+
elif direction == "backward":
49+
return -discretised_child
4550

4651
def integral(self, child, discretised_child):
4752
"""

tests/unit/test_expression_tree/test_operations/test_copy.py

+3
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ def test_symbol_new_copy(self):
1212
a = pybamm.Scalar(0)
1313
b = pybamm.Scalar(1)
1414
v_n = pybamm.Variable("v", "negative electrode")
15+
x_n = pybamm.standard_spatial_vars.x_n
1516
v_s = pybamm.Variable("v", "separator")
1617
vec = pybamm.Vector(np.array([1, 2, 3, 4, 5]))
1718
mesh = get_mesh_for_testing()
@@ -29,6 +30,8 @@ def test_symbol_new_copy(self):
2930
pybamm.grad(v_n),
3031
pybamm.div(pybamm.grad(v_n)),
3132
pybamm.Integral(a, pybamm.t),
33+
pybamm.IndefiniteIntegral(v_n, x_n),
34+
pybamm.BackwardIndefiniteIntegral(v_n, x_n),
3235
pybamm.BoundaryValue(v_n, "right"),
3336
pybamm.BoundaryGradient(v_n, "right"),
3437
pybamm.PrimaryBroadcast(a, "domain"),

0 commit comments

Comments
 (0)