Skip to article frontmatterSkip to article content
Site not loading correctly?

This may be due to an incorrect BASE_URL configuration. See the MyST Documentation for reference.

4 Methods

Delft University of Technology
Updated: 18 Oct 2026

The governing equations are continuous in space and time. This chapter states how they are turned into a finite system that can be integrated: the spatial discretisation, the treatment of the pellet centre, the choice of scheme, and the integrator.

The design principle throughout is that the right-hand side should read as a transcription of the governing equations, with no branches in it. Every approximation is an object passed in, not a conditional inside the derivative.

1. Method of Lines

The Method of Lines (MOL) discretises space and leaves time continuous. The partial differential equations become a large system of ordinary differential equations in tt, one per grid node, which is then handed to a stiff integrator.

Two spatial coordinates are discretised:

The state vector is therefore the bulk concentrations at every axial cell, the intra-particle concentrations at every radial node of every cell, and the surface coverages. With the default closures the axial problem stays hyperbolic and the pellet carries NrN_r states per cell.

Why MOL rather than a fully implicit space-time scheme. MOL lets the spatial discretisation and the time integration be reasoned about separately, and it means the stiff integrator sees an explicit right-hand side it can evaluate and differentiate itself. The alternative — assembling a large sparse implicit system by hand — buys nothing here and makes the physics harder to read off the code.

2. The radial operator and the centre node

The radial operator is written in flux form,

1rs∂∂r(rs∂c∂r)\frac{1}{r^s}\frac{\partial}{\partial r}\left(r^s \frac{\partial c}{\partial r}\right)

discretised across cell faces at ri±1/2r_{i\pm1/2}. Flux form rather than the algebraically equivalent ∂2c∂r2+sr∂c∂r\frac{\partial^2 c}{\partial r^2} + \frac{s}{r}\frac{\partial c}{\partial r} because flux form has one stencil instead of two, so the two terms cannot drift out of the discrete conservation that the continuous operator satisfies.

The centre is where this gets interesting

The operator is singular at r=0r = 0, and the limit has to be taken before discretising. Doing it properly:

1rs∂∂r(rsf′)  ⟶  f′′+srf′\frac{1}{r^s}\frac{\partial}{\partial r}\left(r^s f'\right) \;\longrightarrow\; f'' + \frac{s}{r}f'

As r→0r \to 0 with symmetry f′(0)=0f'(0) = 0, the second term’s 0/00/0 resolves against the first via l’Hôpital to (s−1)f′′(0)(s-1)f''(0), giving a total of (1+s)f′′(0)(1+s)f''(0). The second derivative at the centre comes from the ghost node at r=−drr = -dr, and symmetry says its value equals the node at +dr+dr:

f′′(0)=c1−2c0+c−1dr2=2(c1−c0)dr2f''(0) = \frac{c_1 - 2c_0 + c_{-1}}{dr^2} = \frac{2(c_1 - c_0)}{dr^2}

so the full centre operator is

2(1+s)(c1−c0)dr2  ⟶  6(c1−c0)dr2for s=2\frac{2(1+s)(c_1 - c_0)}{dr^2} \;\longrightarrow\; \frac{6(c_1 - c_0)}{dr^2} \quad \text{for } s = 2

The coefficient 2(1+s)2(1+s) is the one to carry, not the 6. Writing 6 hardcoded gives the right answer for spheres and silently the wrong one for cylinders (4) and slabs (2). Keeping the ss in the expression costs nothing and makes the code correct for all three geometries, which is why it is derived rather than tabulated — and why there is a test asserting the coefficient is exactly 2(1+s)2(1+s).

3. The axial scheme

Backward-difference upwind is the default. Central differences would be second order and would ring on the square-wave inlet this model is meant to handle, producing concentrations below zero — and a stiff chemical source amplifies a negative concentration into a visible artefact immediately. Monotonicity beats order here.

TVD2 is available for smooth transients, where the extra order is worth the limiter. It is second order and monotone on a step, both of which are tested rather than asserted.

4. Volume weights

Node positions, face positions, and volume weights come from a single RadialGrid object, built once and handed to the operator. This is deliberate: a bug in the weights cannot silently disagree with a bug in the stencil if they come from the same object. The weights are exact for the volume element and sum to one — including for even node counts, which is a case that is easy to get wrong.

5. The integrator

The system is stiff, and the stiffness is physical rather than numerical: surface chemistry relaxes on a far shorter timescale than convective transport. An explicit integrator would be limited by the fastest surface timescale regardless of how slowly the bulk evolves.

The system is therefore integrated with an implicit multi-step / implicit Runge–Kutta solver from scipy.integrate, which can take steps set by accuracy rather than by the fastest mode. The Jacobian sparsity pattern is supplied: it is block-tridiagonal in the axial direction, which the integrator uses to avoid dense factorisation. That pattern is itself tested.

6. Evaluation order

The right-hand side evaluates in a fixed order — bulk, shell, particle, chemistry, then assembly — and the order matters. Each stage consumes the previous stage’s output, so reordering them silently changes the scheme from explicit to something else. It is stated in the code rather than left implicit for exactly that reason.

7. What is verified, and how

The test suite does not assert that the code runs; it asserts that the discretisation is correct. Representative checks:

The last two are the ones that catch real bugs: a mutating right-hand side breaks an integrator in ways that look like physics, and a mismatched prefactor produces a model that conserves nothing while still running.