|
| 1 | +# chromosim/numerics/build_matrices.py |
| 2 | +""" |
| 3 | +Finite-volume derivative matrices in z for 1D packed-column models. |
| 4 | +
|
| 5 | +This is a direct Python/NumPy/SciPy port of the MATLAB build_matrices.m: |
| 6 | +
|
| 7 | +- First derivative (D1): backward/upwind stencil, inlet row is zeroed |
| 8 | + because the inlet boundary condition is handled explicitly in the ODE. |
| 9 | +- Second derivative (D2): central stencil with: |
| 10 | + * inlet ghost-cell treatment: D2[0,0] = -2/dz^2 (Dirichlet-type) |
| 11 | + * Danckwerts-style outlet: D2[-1,-1] = 1/dz^2 |
| 12 | +
|
| 13 | +Returns the same 'par' object/dict with .D1 and .D2 as CSR sparse matrices. |
| 14 | +""" |
| 15 | + |
| 16 | +from __future__ import annotations |
| 17 | +import numpy as np |
| 18 | +from scipy.sparse import diags |
| 19 | + |
| 20 | +def _get(par, key): |
| 21 | + """Support both dicts and objects with attributes.""" |
| 22 | + try: |
| 23 | + return getattr(par, key) |
| 24 | + except AttributeError: |
| 25 | + return par[key] |
| 26 | + |
| 27 | +def _set(par, key, value): |
| 28 | + """Support both dicts and objects with attributes.""" |
| 29 | + try: |
| 30 | + setattr(par, key, value) |
| 31 | + except AttributeError: |
| 32 | + par[key] = value |
| 33 | + |
| 34 | +def build_matrices(par): |
| 35 | + """ |
| 36 | + Build derivative matrices and attach them to `par`. |
| 37 | +
|
| 38 | + Parameters |
| 39 | + ---------- |
| 40 | + par : dict or object with attributes |
| 41 | + Required fields: |
| 42 | + - N : int number of axial cells |
| 43 | + - dz : float cell length (m) |
| 44 | +
|
| 45 | + Side effects |
| 46 | + ------------ |
| 47 | + Sets: |
| 48 | + par.D1 : (N x N) scipy.sparse.csr_matrix |
| 49 | + par.D2 : (N x N) scipy.sparse.csr_matrix |
| 50 | +
|
| 51 | + Returns |
| 52 | + ------- |
| 53 | + par : same object/dict, for chaining. |
| 54 | + """ |
| 55 | + N = int(_get(par, "N")) |
| 56 | + dz = float(_get(par, "dz")) |
| 57 | + |
| 58 | + e = np.ones(N) |
| 59 | + |
| 60 | + # 1) First derivative: backward/upwind |
| 61 | + D1 = diags([-e, e], offsets=[-1, 0], shape=(N, N), format="lil") / dz |
| 62 | + D1[0, :] = 0.0 # inlet row is handled via the boundary condition |
| 63 | + D1 = D1.tocsr() |
| 64 | + |
| 65 | + # 2) Second derivative: central |
| 66 | + D2 = diags([e, -2*e, e], offsets=[-1, 0, 1], shape=(N, N), format="lil") / dz**2 |
| 67 | + D2[0, 0] = -2.0 / dz**2 # inlet ghost cell (Dirichlet) |
| 68 | + D2[-1, -1] = 1.0 / dz**2 # Danckwerts-type outlet |
| 69 | + D2 = D2.tocsr() |
| 70 | + |
| 71 | + _set(par, "D1", D1) |
| 72 | + _set(par, "D2", D2) |
| 73 | + return par |
| 74 | + |
| 75 | +if __name__ == "__main__": |
| 76 | + # Minimal smoke test |
| 77 | + from types import SimpleNamespace |
| 78 | + par = SimpleNamespace(N=5, dz=0.01) |
| 79 | + par = build_matrices(par) |
| 80 | + print("D1:\n", par.D1.toarray()) |
| 81 | + print("D2:\n", par.D2.toarray()) |
0 commit comments