88import numpy as np
99from numpy .polynomial .laguerre import Laguerre
1010
11- from polykin .utils .types import FloatArray , FloatArrayLike , FloatOrArrayLike
11+ from polykin .utils .types import (FloatArray , FloatArrayLike , FloatVector ,
12+ FloatVectorLike )
1213
1314__all__ = [
1415 'convolve_moments' ,
1819]
1920
2021
21- def convolve_moments (q0 : float ,
22- q1 : float ,
23- q2 : float ,
24- r0 : float ,
25- r1 : float ,
26- r2 : float
27- ) -> tuple [float , float , float ]:
28- r"""Compute the first three moments of the convolution of two distributions.
22+ def convolve_moments (
23+ q : FloatVectorLike ,
24+ r : FloatVectorLike ,
25+ ) -> FloatVector :
26+ r"""Compute the moments of the convolution of two distributions.
2927
30- If $P = Q * R$ is the convolution of $Q$ and $R$, defined as:
28+ If $P = Q * R$ is the convolution of distributions $Q$ and $R$, defined as:
3129
32- $$ P_n = \sum_{i =0}^{n} Q_{n-i }R_{i } $$
30+ $$ P_n = \sum_{m =0}^{n} Q_{n-m }R_{m } $$
3331
34- then the first three moments of $P$ are related to the moments of $Q$ and
35- $R$ by:
32+ and $p_i$, $q_i$ and $r_i$ denote the $i$-th moments of $P$, $ Q$ and $R$,
33+ respectively,
3634
3735 \begin{aligned}
38- p_0 &= q_0 r_0 \\
39- p_1 &= q_1 r_0 + q_0 r_1 \\
40- p_2 &= q_2 r_0 + 2 q_1 r_1 + q_0 r_2
36+ p_i & = \sum_{n=0}^{\infty} n^i P_n \\
37+ q_i & = \sum_{n=0}^{\infty} n^i Q_n \\
38+ r_i & = \sum_{n=0}^{\infty} n^i R_n
4139 \end{aligned}
4240
43- where $p_i$, $q_i$ and $r_i$ denote the $i$-th moments of $P$, $Q$ and $R$,
44- respectively.
41+ then the moments of $P$ are related to the moments of $Q$ and $R$ by:
42+
43+ $$ p_i = \sum_{j=0}^{i} \binom{i}{j} q_j r_{i-j} $$
4544
4645 Parameters
4746 ----------
48- q0 : float
49- 0-th moment of $Q$.
50- q1 : float
51- 1-st moment of $Q$.
52- q2 : float
53- 2-nd moment of $Q$.
54- r0 : float
55- 0-th moment of $R$.
56- r1 : float
57- 1-st moment of $R$.
58- r2 : float
59- 2-nd moment of $R$.
47+ q : FloatVectorLike (N)
48+ Moments of $Q$, denoted $(q_0, q_1, \ldots)$.
49+ r : FloatVectorLike (N)
50+ Moments of $R$, denoted $(r_0, r_1, \ldots)$.
6051
6152 Returns
6253 -------
63- tuple[float, float, float]
64- 0-th, 1-st and 2-nd moments of $P=Q*R$.
54+ FloatVector (N)
55+ Moments of $P=Q*R$, denoted $(p_0, p_1, \ldots) $.
6556
6657 Examples
6758 --------
6859 >>> from polykin.distributions import convolve_moments
69- >>> convolve_moments(1. , 1e2, 2e4, 1. , 50., 5e4)
70- (1.0, 150.0, 80000.0 )
60+ >>> convolve_moments([1.0 , 1e2, 2e4], [1.0 , 50.0 , 5e4] )
61+ array([1.0e+00, 1.5e+02, 8.0e+04] )
7162 """
72- p0 = q0 * r0
73- p1 = q1 * r0 + q0 * r1
74- p2 = q2 * r0 + 2 * q1 * r1 + q0 * r2
75- return p0 , p1 , p2
63+ if len (q ) != len (r ):
64+ raise ValueError ("`q` and `r` must have the same length." )
65+
66+ p = np .zeros (len (q ))
67+ for i in range (len (q )):
68+ for j in range (i + 1 ):
69+ p [i ] += comb (i , j )* q [j ]* r [i - j ]
70+
71+ return p
7672
7773
78- def convolve_moments_self (q0 : float ,
79- q1 : float ,
80- q2 : float ,
81- order : int = 1
82- ) -> tuple [float , float , float ]:
83- r"""Compute the first three moments of the k-th order convolution of a
84- distribution with itself.
74+ def convolve_moments_self (
75+ q : FloatVectorLike ,
76+ order : int
77+ ) -> FloatVector :
78+ r"""Compute the moments of the k-th order convolution of a distribution
79+ with itself.
8580
86- If $P^k $ is the $k$-th order convolution of $Q$ with itself, defined as:
81+ If $P^{(k)} $ is the $k$-th order convolution of $Q$ with itself, defined as:
8782
8883 \begin{aligned}
89- P^1_n &= Q*Q = \sum_{i =0}^{n} Q_{n-i } Q_{i } \\
90- P^2_n &= (Q*Q)*Q = \sum_{i =0}^{n} Q_{n-i } P^1_{i } \\
91- P^3_n &= ((Q*Q)*Q)*Q = \sum_{i =0}^{n} Q_{n-i } P^2_{i } \\
84+ P^{(1)}_n &= Q*Q = \sum_{m =0}^{n} Q_{n-m } Q_{m } \\
85+ P^{(2)}_n &= (Q*Q)*Q = \sum_{m =0}^{n} Q_{n-m } P^{(1)}_{m } \\
86+ P^{(3)}_n &= ((Q*Q)*Q)*Q = \sum_{m =0}^{n} Q_{n-m } P^{(2)}_{m } \\
9287 ...
9388 \end{aligned}
9489
95- then the first three moments of $P^k $ are related to the moments of $Q$ by:
90+ then the moments of $P^{(k)} $ are related to the moments of $Q$ by:
9691
9792 \begin{aligned}
9893 p_0 &= q_0^{k+1} \\
9994 p_1 &= (k+1) q_0^k q_1 \\
100- p_2 &= (k+1) q_0^{k-1} (k q_1^2 +q_0 q_2)
95+ p_2 &= (k+1) q_0^{k-1} (k q_1^2 +q_0 q_2) \\
96+ \ldots
10197 \end{aligned}
10298
103- where $p_i$ and $q_i$ denote the $i$-th moments of $P^k $ and $Q$,
99+ where $p_i$ and $q_i$ denote the $i$-th moments of $P^{(k)} $ and $Q$,
104100 respectively.
105101
106102 Parameters
107103 ----------
108- q0 : float
109- 0-th moment of $Q$.
110- q1 : float
111- 1-st moment of $Q$.
112- q2 : float
113- 2-nd moment of $Q$.
104+ q : FloatVectorLike (N)
105+ Moments of $Q$, denoted $(q_0, q_1, \ldots)$.
106+ order : int
107+ Order of the convolution.
114108
115109 Returns
116110 -------
117- tuple[float, float, float]
118- 0-th, 1-st and 2-nd moments of $P^k =(Q*Q)*...$.
111+ FloatVector (N)
112+ Moments of $P^{(k)} =(Q*Q)*...$, denoted $(p_0, p_1, \ldots) $.
119113
120114 Examples
121115 --------
122116 >>> from polykin.distributions import convolve_moments_self
123- >>> convolve_moments_self(1. , 1e2, 2e4, 2)
124- (1.0, 300.0, 120000.0 )
117+ >>> convolve_moments_self([1e0 , 1e2, 2e4] , 2)
118+ array([1.0e+00, 3.0e+02, 1.2e+05] )
125119 """
126- p0 = q0 ** (order + 1 )
127- p1 = (order + 1 ) * q0 ** order * q1
128- p2 = (order + 1 ) * q0 ** (order - 1 ) * (order * q1 ** 2 + q0 * q2 )
129- return p0 , p1 , p2
120+
121+ q = np .asarray (q , dtype = float )
122+
123+ if order == 0 :
124+ return q .copy ()
125+
126+ N = len (q )
127+ if N <= 3 :
128+ # Use closed-form expressions for the first three moments
129+ p = np .empty (N )
130+ if N > 0 :
131+ p [0 ] = q [0 ]** (order + 1 )
132+ if N > 1 :
133+ p [1 ] = (order + 1 ) * q [0 ]** order * q [1 ]
134+ if N > 2 :
135+ p [2 ] = (order + 1 ) * q [0 ]** (order - 1 ) * (order * q [1 ]** 2 + q [0 ]* q [2 ])
136+ else :
137+ p = q .copy ()
138+ for _ in range (order ):
139+ p = convolve_moments (q , p )
140+
141+ return p
130142
131143
132144def convert_polymer_standards (
133- M1 : FloatOrArrayLike ,
145+ M1 : float | FloatArrayLike ,
134146 K1 : float ,
135147 K2 : float ,
136148 a1 : float ,
@@ -159,7 +171,7 @@ def convert_polymer_standards(
159171
160172 Parameters
161173 ----------
162- M1 : FloatOrArrayLike
174+ M1 : float | FloatArrayLike
163175 Molar mass in standard 1.
164176 K1 : float
165177 Mark-Houwink coefficient for standard 1.
@@ -177,30 +189,30 @@ def convert_polymer_standards(
177189
178190 Examples
179191 --------
180- A sample of PMMA was mesured to have a molar mass of 100 kg /mol in PS
192+ A sample of PMMA was mesured to have a molar mass of 1e5 g /mol in PS
181193 equivalent weight. What is the sample molar mass in actual PMMA weight?
182194
183195 >>> from polykin.distributions import convert_polymer_standards
184196 >>> a1 = 0.77 # PS in THF
185197 >>> K1 = 6.82e-3 # PS in THF
186198 >>> a2 = 0.69 # PMMA in THF
187199 >>> K2 = 1.28e-2 # PMMA in THF
188- >>> M2 = convert_polymer_standards(100 , K1, K2, a1, a2)
189- >>> print(f"{M2:.2f} kg /mol")
190- 85.68 kg /mol
200+ >>> M2 = convert_polymer_standards(1e5 , K1, K2, a1, a2)
201+ >>> print(f"{M2:.2e} g /mol")
202+ 1.19e+05 g /mol
191203 """
192- M1 = np .asarray (M1 , dtype = np . float64 )
204+ M1 = np .asarray (M1 , dtype = float )
193205 return (K1 / K2 )** (1 / (1 + a2 )) * M1 ** ((1 + a1 )/ (1 + a2 ))
194206
195207
196208def reconstruct_Laguerre (
197- moments : FloatArrayLike ,
209+ moments : FloatVectorLike ,
198210) -> Callable [[FloatArrayLike ], FloatArray ]:
199- r"""Reconstruct a differential number distribution from its first `k`
211+ r"""Reconstruct a differential number distribution from a finite set of
200212 moments using a Laguerre-series approximation.
201213
202- According to Bamford and Tompa, a number distribution can be expressed as
203- an (infinite) expansion in Laguerre polynomials:
214+ According to Bamford and Tompa, a number distribution $P(n)$ can be expressed
215+ as an (infinite) expansion in Laguerre polynomials:
204216
205217 $$ P(n) = \frac{e^{-\rho}}{(DP_n)^2}
206218 \sum_{m=0}^{\infty} \gamma_m L_m(\rho) $$
@@ -231,8 +243,8 @@ def reconstruct_Laguerre(
231243
232244 Parameters
233245 ----------
234- moments : FloatArrayLike
235- First `k` raw moments of the number distribution (`λ₀`, `λ₁`, ..., `λ_k`) .
246+ moments : FloatVectorLike
247+ Moments of $P$, denoted $(\lambda_0, \lambda_1, \ldots)$ .
236248
237249 Returns
238250 -------
0 commit comments