RadCluster
Graph-based cluster dynamics for irradiated materials
Loading...
Searching...
No Matches
bin_moment.py
Go to the documentation of this file.
1"""
2core/reductions/bin_moment.py — Logarithmic size-bin moment reduction.
3
4Host-independent. Implements the "reconstruct -> walk -> project"
5reduction of Ghoniem (2026), Section 5.4: a per-size cluster ladder of
6length N is replaced by a discrete prefix plus O(log N) logarithmically
7spaced bins, each carrying P in {1,2,3} moments
8
9 mu_k^(0) = sum_{n in B_k} c_n
10 mu_k^(1) = sum_{n in B_k} n c_n
11 mu_k^(2) = sum_{n in B_k} n^2 c_n
12
13evolved by the three-step procedure executed at every RHS call:
14
15 R reconstruct a per-size distribution c_n from the tracked moments
16 under the chosen intra-bin closure (piecewise-constant / linear
17 dual-basis / log-normal);
18 E evaluate the *exact* per-size master-equation rates with a
19 :class:`GraphWalker` — no bin-averaged rate constants;
20 P project the per-size rates onto the tracked moments,
21 d mu_k^(p)/dt = sum_{n in B_k} n^p (dc_n/dt).
22
23Because step E uses the exact per-size rates, every conservation
24property of the master equation is inherited automatically — no
25inter-bin flux has to be made upwind-consistent (Section 5.4).
26
27The pure bin-partition and closure mathematics is reused from the
28reviewed, validated ``bin_moment_rates`` module (those functions are
29host-independent); this class composes them with a graph walk.
30"""
31from __future__ import annotations
32
33from typing import List, Tuple
34
35import numpy as np
36
37from ...bin_moment_rates import ( # reviewed, host-independent bin mathematics
38 build_bins,
39 moments_from_distribution,
40 n_moments_for_shape,
41 reconstruct_distribution,
42)
43
44
46 """A logarithmic bin-moment reduction of one cluster size axis.
47
48 Parameters
49 ----------
50 n_max : int
51 Largest cluster size on the full per-size ladder.
52 n_discrete : int
53 Sizes 1..n_discrete are kept discrete (one ODE per size); sizes
54 above are aggregated into geometric bins.
55 n_bins : int
56 Target number of geometric bins above the discrete prefix.
57 shape_function : {'constant', 'linear', 'lognormal'}
58 The intra-bin closure; selects P = 1, 2 or 3 moments per bin.
59 """
60
61 def __init__(self, n_max: int, n_discrete: int, n_bins: int,
62 shape_function: str = "linear"):
63 self.n_max = int(n_max)
64 self.n_discrete = int(n_discrete)
65 self.shape_function = str(shape_function).lower()
66 self.moments_per_bin = n_moments_for_shape(self.shape_function)
67
68 if n_bins > 0 and self.n_discrete < self.n_max:
69 r = (float(self.n_max) / max(self.n_discrete, 1)) ** (1.0 / n_bins)
70 self.r = max(r, 1.01)
71 self.bins, self.edges = build_bins(
72 self.n_max, n1=self.n_discrete + 1, r=self.r)
73 else:
74 self.bins, self.edges = [], np.array([self.n_max + 1])
75 self.r = 2.0
76 self.n_bins = len(self.bins)
77
78 # ── layout ────────────────────────────────────────────────────────────
79 @property
80 def length(self) -> int:
81 """Number of ODE entries: discrete prefix + P moments per bin."""
82 return self.n_discrete + self.moments_per_bin * self.n_bins
83
84 # ── step R: moments -> per-size distribution ──────────────────────────
85 def reconstruct(self, reduced: np.ndarray) -> np.ndarray:
86 """Reconstruct the full per-size distribution c_n (length n_max)."""
87 P = self.moments_per_bin
88 c = np.zeros(self.n_max)
89 c[:self.n_discrete] = np.maximum(reduced[:self.n_discrete], 0.0)
90 if self.n_bins:
91 blk = reduced[self.n_discrete:self.n_discrete + P * self.n_bins]
92 mu0 = np.maximum(blk[0::P][:self.n_bins], 0.0)
93 mu1 = blk[1::P][:self.n_bins] if P >= 2 else None
94 mu2 = blk[2::P][:self.n_bins] if P >= 3 else None
95 c_bin = reconstruct_distribution(
96 self.shape_function, mu0, mu1, mu2, self.bins, self.n_max)
97 c[self.n_discrete:] = c_bin[self.n_discrete:]
98 return c
99
100 # ── step P: per-size rates -> moment rates ────────────────────────────
101 def project(self, dc: np.ndarray) -> np.ndarray:
102 """Project per-size rates dc_n/dt onto the tracked moment rates."""
103 P = self.moments_per_bin
104 out = np.zeros(self.length)
105 out[:self.n_discrete] = dc[:self.n_discrete]
106 for k, (nlo, nhi) in enumerate(self.bins):
107 ns = np.arange(nlo, nhi)
108 idx = ns - 1
109 valid = (idx >= 0) & (idx < self.n_max)
110 dcv = dc[idx[valid]]
111 nv = ns[valid].astype(float)
112 base = self.n_discrete + P * k
113 out[base] = dcv.sum() # d mu0/dt
114 if P >= 2:
115 out[base + 1] = float(nv @ dcv) # d mu1/dt
116 if P >= 3:
117 out[base + 2] = float((nv * nv) @ dcv) # d mu2/dt
118 return out
119
120 # ── moments of an initial per-size distribution ───────────────────────
121 def moments_of(self, c: np.ndarray) -> np.ndarray:
122 """Pack a per-size distribution into the reduced moment vector."""
123 P = self.moments_per_bin
124 reduced = np.zeros(self.length)
125 reduced[:self.n_discrete] = c[:self.n_discrete]
126 if self.n_bins:
127 mu0, mu1, mu2 = moments_from_distribution(c, self.bins, n_mom=P)
128 for k in range(self.n_bins):
129 base = self.n_discrete + P * k
130 reduced[base] = mu0[k]
131 if P >= 2:
132 reduced[base + 1] = mu1[k]
133 if P >= 3:
134 reduced[base + 2] = mu2[k]
135 return reduced
136
137 def __repr__(self) -> str:
138 return (f"<BinMomentReduction n_max={self.n_max} "
139 f"n_discrete={self.n_discrete} n_bins={self.n_bins} "
140 f"closure={self.shape_function!r} P={self.moments_per_bin} "
141 f"len={self.length}>")
__init__(self, int n_max, int n_discrete, int n_bins, str shape_function="linear")
Definition bin_moment.py:62
np.ndarray reconstruct(self, np.ndarray reduced)
Definition bin_moment.py:85