bs-python-utils 0.0.1__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- bs_python_utils/Timer.py +76 -0
- bs_python_utils/__init__.py +0 -0
- bs_python_utils/bs_altair.py +927 -0
- bs_python_utils/bs_logging.py +100 -0
- bs_python_utils/bs_mathstr.py +130 -0
- bs_python_utils/bs_mem.py +148 -0
- bs_python_utils/bs_opt.py +518 -0
- bs_python_utils/bs_plots.py +2 -0
- bs_python_utils/bs_seaborn.py +174 -0
- bs_python_utils/bs_sparse_gaussian.py +46 -0
- bs_python_utils/bsmplutils.py +34 -0
- bs_python_utils/bsnputils.py +957 -0
- bs_python_utils/bssputils.py +79 -0
- bs_python_utils/bsstats.py +463 -0
- bs_python_utils/bsutils.py +363 -0
- bs_python_utils/distance_covariances.py +258 -0
- bs_python_utils/example_opt.py +71 -0
- bs_python_utils/examples_altair.py +195 -0
- bs_python_utils/examples_distance_covariances.py +32 -0
- bs_python_utils/examples_mem.py +25 -0
- bs_python_utils/examples_seaborn.py +37 -0
- bs_python_utils/examples_sklearn.py +33 -0
- bs_python_utils/pandas_utils.py +239 -0
- bs_python_utils/sklearn_utils.py +74 -0
- bs_python_utils-0.0.1.dist-info/LICENSE +21 -0
- bs_python_utils-0.0.1.dist-info/METADATA +71 -0
- bs_python_utils-0.0.1.dist-info/RECORD +28 -0
- bs_python_utils-0.0.1.dist-info/WHEEL +4 -0
|
@@ -0,0 +1,957 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Contains various `numpy` utility programs.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
import sys
|
|
6
|
+
from math import cos, exp, floor, log, pi, sqrt
|
|
7
|
+
from typing import Any, Callable, Iterable, cast
|
|
8
|
+
|
|
9
|
+
import numpy as np
|
|
10
|
+
from numpy.polynomial import Polynomial
|
|
11
|
+
|
|
12
|
+
from bs_python_utils.bsutils import bs_error_abort, print_stars
|
|
13
|
+
|
|
14
|
+
# some useful types
|
|
15
|
+
TwoArrays = tuple[np.ndarray, np.ndarray]
|
|
16
|
+
ThreeArrays = tuple[np.ndarray, np.ndarray, np.ndarray]
|
|
17
|
+
FourArrays = tuple[np.ndarray, np.ndarray, np.ndarray, np.ndarray]
|
|
18
|
+
SixArrays = tuple[
|
|
19
|
+
np.ndarray, np.ndarray, np.ndarray, np.ndarray, np.ndarray, np.ndarray
|
|
20
|
+
]
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
# Numpy parallel RNG
|
|
24
|
+
def generate_RNG_streams(
|
|
25
|
+
nsim: int, initial_seed: int = 13091962
|
|
26
|
+
) -> list[np.random.Generator]:
|
|
27
|
+
"""
|
|
28
|
+
return `nsim` RNGs
|
|
29
|
+
|
|
30
|
+
Args:
|
|
31
|
+
nsim: number of RNGs we want
|
|
32
|
+
initial_seed: any large integer
|
|
33
|
+
|
|
34
|
+
Returns:
|
|
35
|
+
`nsim` streams
|
|
36
|
+
|
|
37
|
+
Example:
|
|
38
|
+
streams = generate_RNG_streams(10, 575856896)
|
|
39
|
+
x = streams[i].normal(scale=s, size=(nmarkets, nproducts))
|
|
40
|
+
"""
|
|
41
|
+
ss = np.random.SeedSequence(initial_seed)
|
|
42
|
+
# Spawn off child SeedSequences to pass to child processes.
|
|
43
|
+
child_seeds = ss.spawn(nsim)
|
|
44
|
+
streams = [np.random.default_rng(s) for s in child_seeds]
|
|
45
|
+
return streams
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def ecdf(x: np.ndarray) -> np.ndarray:
|
|
49
|
+
"""Evaluate the empirical cdf at each point in sample
|
|
50
|
+
|
|
51
|
+
Args:
|
|
52
|
+
x: 1-dim array `(nobs)`
|
|
53
|
+
|
|
54
|
+
Returns:
|
|
55
|
+
A 1-dim array `(nobs)` with the values of the empirical cdf at `x`, from 1/`nobs` to 1
|
|
56
|
+
|
|
57
|
+
"""
|
|
58
|
+
if x.ndim != 1:
|
|
59
|
+
print_stars(f"ecdf: x should have 1 dimension, not {x.ndim}")
|
|
60
|
+
sys.exit()
|
|
61
|
+
nx = x.size
|
|
62
|
+
order_x = np.argsort(x)
|
|
63
|
+
ecdf_val = np.zeros(nx)
|
|
64
|
+
for i_order, n_order in enumerate(order_x):
|
|
65
|
+
ecdf_val[n_order] = (i_order + 1.0) / nx
|
|
66
|
+
return ecdf_val
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def inv_ecdf(v: np.ndarray, q: np.ndarray | float) -> np.ndarray | float:
|
|
70
|
+
"""Evaluate the empirical `q`-quantiles of the sample `v`
|
|
71
|
+
in a way that is consistent with `ecdf`.
|
|
72
|
+
|
|
73
|
+
Args:
|
|
74
|
+
v: 1-dim array `(nobs)` of the data points
|
|
75
|
+
q: 1-dim array `(nobs)` of quantiles or float
|
|
76
|
+
|
|
77
|
+
Returns:
|
|
78
|
+
A 1-dim array `(nobs)` with the values of the `q`-quantiles of `v`, or just the one quantile
|
|
79
|
+
|
|
80
|
+
"""
|
|
81
|
+
if v.ndim != 1:
|
|
82
|
+
bs_error_abort(f"v should have 1 dimension, not {v.ndim}")
|
|
83
|
+
nv = v.size
|
|
84
|
+
sorted_v = np.zeros(nv + 2)
|
|
85
|
+
sorted_v[1 : (nv + 1)] = np.sort(v)
|
|
86
|
+
sorted_v[0] = 2.0 * sorted_v[1] - sorted_v[2] # added to extend for q < 1/nv
|
|
87
|
+
sorted_v[nv + 1] = sorted_v[nv] # added to extend for q = 1
|
|
88
|
+
if isinstance(q, float):
|
|
89
|
+
q = np.array([q])
|
|
90
|
+
q_floor = floor(nv * q)
|
|
91
|
+
val_q = sorted_v[q_floor] + (nv * q - q_floor) * (
|
|
92
|
+
sorted_v[q_floor + 1] - sorted_v[q_floor]
|
|
93
|
+
)
|
|
94
|
+
return cast(float, val_q)
|
|
95
|
+
elif isinstance(q, np.ndarray):
|
|
96
|
+
q_floor = np.floor(nv * q).astype(int)
|
|
97
|
+
vals_q = sorted_v[q_floor] + (nv * q - q_floor) * (
|
|
98
|
+
sorted_v[q_floor + 1] - sorted_v[q_floor]
|
|
99
|
+
)
|
|
100
|
+
return cast(np.ndarray, vals_q)
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
def nprepeat_col(v: np.ndarray, n: int) -> np.ndarray:
|
|
104
|
+
"""
|
|
105
|
+
create a matrix with `n` columns equal to `v`
|
|
106
|
+
|
|
107
|
+
Args:
|
|
108
|
+
v: a 1-dim array of size `m`
|
|
109
|
+
n: the number of columns requested
|
|
110
|
+
|
|
111
|
+
Returns:
|
|
112
|
+
a 2-dim array of shape `(m, n)`
|
|
113
|
+
"""
|
|
114
|
+
return np.repeat(v[:, np.newaxis], n, axis=1)
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
def nprepeat_row(v: np.ndarray, m: int) -> np.ndarray:
|
|
118
|
+
"""
|
|
119
|
+
create a matrix with `m` rows equal to `v`
|
|
120
|
+
|
|
121
|
+
Args:
|
|
122
|
+
v: a 1-dim array of size `n`
|
|
123
|
+
m: the number of rows requested
|
|
124
|
+
|
|
125
|
+
Returns:
|
|
126
|
+
a 2-dim array of shape `(m, n)`
|
|
127
|
+
"""
|
|
128
|
+
return np.repeat(v[np.newaxis, :], m, axis=0)
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
def npmaxabs(arr: np.ndarray) -> float:
|
|
132
|
+
"""
|
|
133
|
+
maximum absolute value in an array
|
|
134
|
+
|
|
135
|
+
Args:
|
|
136
|
+
arr: any Numpy array
|
|
137
|
+
|
|
138
|
+
Returns:
|
|
139
|
+
the largest element in absolute value
|
|
140
|
+
"""
|
|
141
|
+
return cast(float, np.max(np.abs(arr)))
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
def rice_stderr(
|
|
145
|
+
y: np.ndarray, x: np.ndarray, is_sorted: bool = False
|
|
146
|
+
) -> np.ndarray | float:
|
|
147
|
+
"""
|
|
148
|
+
computes the Rice local estimators of the standard error of y | x
|
|
149
|
+
|
|
150
|
+
Args:
|
|
151
|
+
y: vector of y-values
|
|
152
|
+
x: vector of x-values
|
|
153
|
+
is_sorted: set it to `True` if `x` is in increasing order
|
|
154
|
+
|
|
155
|
+
Returns:
|
|
156
|
+
an array of the same size with the stderr(y | x)
|
|
157
|
+
"""
|
|
158
|
+
n = check_vector(x)
|
|
159
|
+
ny = check_vector(y)
|
|
160
|
+
if ny != n:
|
|
161
|
+
bs_error_abort("x and y should have the same size")
|
|
162
|
+
|
|
163
|
+
if not is_sorted:
|
|
164
|
+
# need to sort by increasing value of x
|
|
165
|
+
order_x = np.argsort(x)
|
|
166
|
+
ys = y[order_x]
|
|
167
|
+
else:
|
|
168
|
+
ys = y
|
|
169
|
+
|
|
170
|
+
variance_estimator = np.zeros(n)
|
|
171
|
+
|
|
172
|
+
# we average over neighbors
|
|
173
|
+
n_neighbors = int(sqrt(float(n)) / 2.0)
|
|
174
|
+
facd = 1.0 / (2.0 * n_neighbors)
|
|
175
|
+
n_neighbors2 = n_neighbors // 2
|
|
176
|
+
|
|
177
|
+
# for the first observations
|
|
178
|
+
yleft = ys[:n_neighbors2]
|
|
179
|
+
dy = yleft[1:] - yleft[:-1]
|
|
180
|
+
variance_estimator[:n_neighbors2] = np.sum(dy * dy) * facd
|
|
181
|
+
|
|
182
|
+
# for the middle of the sample
|
|
183
|
+
minus_nn2 = n - n_neighbors2
|
|
184
|
+
for ix in range(n_neighbors2, minus_nn2):
|
|
185
|
+
ix_neighbors = slice(ix - n_neighbors2, ix + n_neighbors2)
|
|
186
|
+
yx = ys[ix_neighbors]
|
|
187
|
+
dy = yx[1:] - yx[:-1]
|
|
188
|
+
variance_estimator[ix] = np.sum(dy * dy) * facd
|
|
189
|
+
|
|
190
|
+
# and for the last observations
|
|
191
|
+
yright = ys[minus_nn2:]
|
|
192
|
+
dy = yright[1:] - yright[:-1]
|
|
193
|
+
variance_estimator[minus_nn2:] = np.sum(dy * dy) * facd
|
|
194
|
+
|
|
195
|
+
stderr_estimator = np.sqrt(variance_estimator)
|
|
196
|
+
|
|
197
|
+
return stderr_estimator
|
|
198
|
+
|
|
199
|
+
|
|
200
|
+
def nplog(
|
|
201
|
+
arr: np.ndarray,
|
|
202
|
+
eps: float = 1e-30,
|
|
203
|
+
deriv: int = 0,
|
|
204
|
+
verbose: bool = False,
|
|
205
|
+
) -> np.ndarray | TwoArrays | ThreeArrays:
|
|
206
|
+
"""
|
|
207
|
+
`C^2` extension of `\\ln(a)` below `eps`, perhaps with derivatives
|
|
208
|
+
|
|
209
|
+
Args:
|
|
210
|
+
arr: any Numpy array
|
|
211
|
+
eps: lower bound
|
|
212
|
+
deriv: if 1, compute derivative, if 2, second derivative
|
|
213
|
+
verbose: prints debugging info
|
|
214
|
+
|
|
215
|
+
Returns:
|
|
216
|
+
`\\ln(a)` `C^2`-extended below `eps`, perhaps with derivatives
|
|
217
|
+
"""
|
|
218
|
+
if deriv not in [0, 1, 2]:
|
|
219
|
+
bs_error_abort(f"deriv can only be 0, 1, or 2; not {deriv}")
|
|
220
|
+
if np.min(arr) > eps:
|
|
221
|
+
if deriv == 0:
|
|
222
|
+
return cast(np.ndarray, np.log(arr))
|
|
223
|
+
elif deriv == 1:
|
|
224
|
+
return cast(TwoArrays, (np.log(arr), 1.0 / arr))
|
|
225
|
+
# deriv == 2
|
|
226
|
+
return cast(ThreeArrays, (np.log(arr), 1.0 / arr, -1.0 / (arr * arr)))
|
|
227
|
+
else:
|
|
228
|
+
logarreps = np.log(np.maximum(arr, eps))
|
|
229
|
+
darr = 1.0 - arr / eps
|
|
230
|
+
logarr_smaller = log(eps) - darr * (1.0 + darr / 2.0)
|
|
231
|
+
if verbose:
|
|
232
|
+
n_small_args = np.sum(arr < eps)
|
|
233
|
+
if n_small_args > 0:
|
|
234
|
+
finals = "s" if n_small_args > 1 else ""
|
|
235
|
+
print(
|
|
236
|
+
f"nplog: {n_small_args} argument{finals} smaller than {eps}: mini ="
|
|
237
|
+
f" {np.min(arr)}"
|
|
238
|
+
)
|
|
239
|
+
logeps = np.where(arr > eps, logarreps, logarr_smaller)
|
|
240
|
+
if deriv == 0:
|
|
241
|
+
return logeps
|
|
242
|
+
arreps = np.maximum(arr, eps)
|
|
243
|
+
der_logarreps = 1.0 / arreps
|
|
244
|
+
der_logarr_smaller = (1.0 + darr) / eps
|
|
245
|
+
dlogeps = np.where(arr > eps, der_logarreps, der_logarr_smaller)
|
|
246
|
+
if deriv == 1:
|
|
247
|
+
return cast(TwoArrays, (logeps, dlogeps))
|
|
248
|
+
# deriv == 2
|
|
249
|
+
der2_logarreps = -1.0 / (arreps * arreps)
|
|
250
|
+
der2_logarr_smaller = np.full(arr.shape, -1.0 / (eps * eps))
|
|
251
|
+
d2logeps = np.where(arr > eps, der2_logarreps, der2_logarr_smaller)
|
|
252
|
+
return cast(ThreeArrays, (logeps, dlogeps, d2logeps))
|
|
253
|
+
|
|
254
|
+
|
|
255
|
+
def npexp(
|
|
256
|
+
arr: np.ndarray,
|
|
257
|
+
bigx: float = 50.0,
|
|
258
|
+
lowx: float = -50.0,
|
|
259
|
+
deriv: int = 0,
|
|
260
|
+
verbose: bool = False,
|
|
261
|
+
) -> np.ndarray | TwoArrays | ThreeArrays:
|
|
262
|
+
"""
|
|
263
|
+
`C^2` extension of `\\exp(a)` above `bigx` and below `lowx`,
|
|
264
|
+
perhaps with derivatives
|
|
265
|
+
|
|
266
|
+
Args:
|
|
267
|
+
arr: any Numpy array
|
|
268
|
+
bigx: upper bound
|
|
269
|
+
lowx: lower bound
|
|
270
|
+
deriv: if 1, compute derivative, if 2, second derivative
|
|
271
|
+
verbose: prints debugging info
|
|
272
|
+
|
|
273
|
+
|
|
274
|
+
Returns:
|
|
275
|
+
`\\exp(a)` `C^2`-extended above `bigx` and below `lowx`,
|
|
276
|
+
perhaps with derivatives
|
|
277
|
+
"""
|
|
278
|
+
if deriv not in [0, 1, 2]:
|
|
279
|
+
bs_error_abort(f"deriv can only be 0, 1, or 2; not {deriv}")
|
|
280
|
+
min_arr, max_arr = np.min(arr), np.max(arr)
|
|
281
|
+
if max_arr <= bigx and min_arr >= lowx:
|
|
282
|
+
exparr = np.exp(arr)
|
|
283
|
+
if deriv == 0:
|
|
284
|
+
return cast(np.ndarray, exparr)
|
|
285
|
+
elif deriv == 1:
|
|
286
|
+
return cast(TwoArrays, (exparr, exparr))
|
|
287
|
+
# deriv == 2
|
|
288
|
+
return cast(ThreeArrays, (exparr, exparr, exparr))
|
|
289
|
+
else: # some large and/or small arguments
|
|
290
|
+
exparr = np.exp(np.maximum(np.minimum(arr, bigx), lowx))
|
|
291
|
+
print(f"{exparr=}")
|
|
292
|
+
ebigx = exp(bigx)
|
|
293
|
+
elowx = exp(lowx)
|
|
294
|
+
darrb = arr - bigx
|
|
295
|
+
darrl = lowx - arr
|
|
296
|
+
exparr_larger = ebigx * (1.0 + darrb * (1.0 + 0.5 * darrb))
|
|
297
|
+
exparr_smaller = elowx * (1.0 - darrl * (1.0 - 0.5 * darrl))
|
|
298
|
+
if verbose:
|
|
299
|
+
n_large_args = np.sum(arr > bigx)
|
|
300
|
+
finals = "s" if n_large_args > 1 else ""
|
|
301
|
+
print(
|
|
302
|
+
f"npexp: {n_large_args} argument{finals} larger than {bigx}:\n"
|
|
303
|
+
f"maxi = {np.max(arr)}"
|
|
304
|
+
)
|
|
305
|
+
n_small_args = np.sum(arr < lowx)
|
|
306
|
+
finals = "s" if n_small_args > 1 else ""
|
|
307
|
+
print(
|
|
308
|
+
f"npexp: {n_small_args} argument{finals} smaller than {lowx}:\n"
|
|
309
|
+
f"mini = {np.min(arr)}"
|
|
310
|
+
)
|
|
311
|
+
expval = exparr
|
|
312
|
+
print(expval)
|
|
313
|
+
expval = np.where(arr > bigx, exparr_larger, expval)
|
|
314
|
+
expval = np.where(arr < lowx, exparr_smaller, expval)
|
|
315
|
+
if deriv == 0:
|
|
316
|
+
return cast(np.ndarray, expval)
|
|
317
|
+
dexpval = exparr
|
|
318
|
+
dexparr_larger = ebigx * (1.0 + darrb)
|
|
319
|
+
dexparr_smaller = elowx * (1.0 - darrl)
|
|
320
|
+
dexpval = np.where(arr > bigx, dexparr_larger, dexpval)
|
|
321
|
+
dexpval = np.where(arr < lowx, dexparr_smaller, dexpval)
|
|
322
|
+
if deriv == 1:
|
|
323
|
+
return cast(TwoArrays, (expval, dexpval))
|
|
324
|
+
# deriv == 2
|
|
325
|
+
d2expval = exparr
|
|
326
|
+
return cast(ThreeArrays, (expval, dexpval, d2expval))
|
|
327
|
+
|
|
328
|
+
|
|
329
|
+
def _nppow_arrays(
|
|
330
|
+
a: np.ndarray, b: np.ndarray, deriv: int
|
|
331
|
+
) -> np.ndarray | ThreeArrays | SixArrays:
|
|
332
|
+
"""implements nppow when a and b are conformal arrays"""
|
|
333
|
+
avec = a.ravel()
|
|
334
|
+
bvec = b.ravel()
|
|
335
|
+
a_pow_b = avec**bvec
|
|
336
|
+
a_pow_br = a_pow_b.reshape(a.shape)
|
|
337
|
+
if deriv == 0:
|
|
338
|
+
return cast(np.ndarray, a_pow_br)
|
|
339
|
+
der_wrt_a = a_pow_b * bvec / avec
|
|
340
|
+
log_avec = nplog(avec)
|
|
341
|
+
der_wrt_b = a_pow_b * log_avec
|
|
342
|
+
derivs1 = (der_wrt_a.reshape(a.shape), der_wrt_b.reshape(a.shape))
|
|
343
|
+
if deriv == 1:
|
|
344
|
+
return cast(ThreeArrays, (a_pow_br, *derivs1))
|
|
345
|
+
# deriv == 2
|
|
346
|
+
a_pow_b1 = a_pow_b / avec
|
|
347
|
+
b1 = bvec - 1.0
|
|
348
|
+
der2_wrt_aa = bvec * b1 * a_pow_b1 / avec
|
|
349
|
+
der2_wrt_ab = a_pow_b1 * (1.0 + bvec * log_avec)
|
|
350
|
+
der2_wrt_bb = a_pow_b * log_avec * log_avec
|
|
351
|
+
derivs2 = (
|
|
352
|
+
der2_wrt_aa.reshape(a.shape),
|
|
353
|
+
der2_wrt_ab.reshape(a.shape),
|
|
354
|
+
der2_wrt_bb.reshape(a.shape),
|
|
355
|
+
)
|
|
356
|
+
return cast(SixArrays, (a_pow_br, *derivs1, *derivs2))
|
|
357
|
+
|
|
358
|
+
|
|
359
|
+
def nppow(
|
|
360
|
+
a: np.ndarray, b: int | float | np.ndarray, deriv: int = 0
|
|
361
|
+
) -> np.ndarray | ThreeArrays | SixArrays:
|
|
362
|
+
"""
|
|
363
|
+
evaluates a**b element-by-element, perhaps with derivatives
|
|
364
|
+
|
|
365
|
+
Args:
|
|
366
|
+
a: an array
|
|
367
|
+
b: if an array, should have the same shape as `a`
|
|
368
|
+
deriv: if 1, compute derivative, if 2, second derivative
|
|
369
|
+
|
|
370
|
+
Returns:
|
|
371
|
+
an array of the same shape as `a`
|
|
372
|
+
"""
|
|
373
|
+
if isinstance(b, float):
|
|
374
|
+
mina = np.min(a)
|
|
375
|
+
if mina < 0.0:
|
|
376
|
+
bs_error_abort("All elements of a must be positive!")
|
|
377
|
+
|
|
378
|
+
if isinstance(b, (int, float)):
|
|
379
|
+
a_pow_b = a**b
|
|
380
|
+
if deriv == 0:
|
|
381
|
+
return a_pow_b
|
|
382
|
+
log_a = np.log(a)
|
|
383
|
+
derivs1 = (b * a_pow_b / a, a_pow_b * log_a)
|
|
384
|
+
if deriv == 1:
|
|
385
|
+
return cast(ThreeArrays, (a_pow_b, *derivs1))
|
|
386
|
+
b1 = b - 1.0
|
|
387
|
+
a_pow_b1 = a_pow_b / a
|
|
388
|
+
# deriv == 2
|
|
389
|
+
derivs2 = (
|
|
390
|
+
b * b1 * a_pow_b1 / a,
|
|
391
|
+
a_pow_b1 * (1.0 + b * log_a),
|
|
392
|
+
a_pow_b * log_a * log_a,
|
|
393
|
+
)
|
|
394
|
+
return cast(SixArrays, (a_pow_b, *derivs1, *derivs2))
|
|
395
|
+
else:
|
|
396
|
+
if a.shape != b.shape:
|
|
397
|
+
bs_error_abort("b is not a number or an array of the same shape as a!")
|
|
398
|
+
return _nppow_arrays(a, b, deriv)
|
|
399
|
+
|
|
400
|
+
|
|
401
|
+
def nppad_beg_zeros(v: np.ndarray, n: int) -> np.ndarray:
|
|
402
|
+
"""
|
|
403
|
+
pad the beginning of a 1-dim array with zeros to increase its size to `n`, if needed
|
|
404
|
+
|
|
405
|
+
Args:
|
|
406
|
+
v: 1-dim array of size `(nv)`
|
|
407
|
+
n: size requested
|
|
408
|
+
|
|
409
|
+
Returns:
|
|
410
|
+
padded array if `nv` < `n`, otherwise `v`
|
|
411
|
+
"""
|
|
412
|
+
nv = check_vector(v)
|
|
413
|
+
if nv < n:
|
|
414
|
+
return np.pad(v, (n - nv, 0))
|
|
415
|
+
else:
|
|
416
|
+
return v
|
|
417
|
+
|
|
418
|
+
|
|
419
|
+
def nppad_end_zeros(v: np.ndarray, n: int) -> np.ndarray:
|
|
420
|
+
"""
|
|
421
|
+
pad the end of a 1-dim array with zeros to increase its size to `n`, if needed
|
|
422
|
+
|
|
423
|
+
Args:
|
|
424
|
+
v: 1-dim array of size `(nv)`
|
|
425
|
+
n: size requested
|
|
426
|
+
|
|
427
|
+
Returns:
|
|
428
|
+
padded array if `nv` < `n`, else `v`
|
|
429
|
+
"""
|
|
430
|
+
nv = check_vector(v)
|
|
431
|
+
if nv < n:
|
|
432
|
+
return np.pad(v, (0, n - nv))
|
|
433
|
+
else:
|
|
434
|
+
return v
|
|
435
|
+
|
|
436
|
+
|
|
437
|
+
def nppad2_end_zeros(mat: np.ndarray, m: int, n: int) -> np.ndarray:
|
|
438
|
+
"""
|
|
439
|
+
pad the ends of a 2-dim array with zeros to increase its size to `(m,n)`, if needed
|
|
440
|
+
|
|
441
|
+
Args:
|
|
442
|
+
mat: 2-dim array
|
|
443
|
+
m: number of rows requested
|
|
444
|
+
n: number of columns requested
|
|
445
|
+
|
|
446
|
+
Returns:
|
|
447
|
+
padded array, where needed
|
|
448
|
+
"""
|
|
449
|
+
nrows, ncols = check_matrix(mat)
|
|
450
|
+
max_rows = max(m, nrows)
|
|
451
|
+
max_cols = max(n, ncols)
|
|
452
|
+
if nrows < max_rows and ncols < max_cols: # pad both dimensions
|
|
453
|
+
pmat = np.zeros((m, n))
|
|
454
|
+
pmat[:nrows, :ncols] = mat
|
|
455
|
+
return pmat
|
|
456
|
+
elif nrows < max_rows: # pad rows
|
|
457
|
+
pmat = np.zeros((m, ncols))
|
|
458
|
+
pmat[:nrows, :] = mat
|
|
459
|
+
return pmat
|
|
460
|
+
elif ncols < max_cols: # pad columns
|
|
461
|
+
pmat = np.zeros((nrows, n))
|
|
462
|
+
pmat[:, :ncols] = mat
|
|
463
|
+
return pmat
|
|
464
|
+
else: # no need for padding
|
|
465
|
+
return mat
|
|
466
|
+
|
|
467
|
+
|
|
468
|
+
def bsgrid(v: np.ndarray, w: np.ndarray) -> np.ndarray:
|
|
469
|
+
"""
|
|
470
|
+
make a two-dimensional matrix of all pairs of elements of the vectors `v` and `w`
|
|
471
|
+
|
|
472
|
+
Args:
|
|
473
|
+
v: basis vector, size m
|
|
474
|
+
w: basis vector, size n
|
|
475
|
+
|
|
476
|
+
Returns:
|
|
477
|
+
an array of shape `(m n, 2)`
|
|
478
|
+
"""
|
|
479
|
+
m = check_vector(v)
|
|
480
|
+
n = check_vector(w)
|
|
481
|
+
m, n = v.size, w.size
|
|
482
|
+
v1 = np.repeat(v, n)
|
|
483
|
+
v2 = np.tile(w, m)
|
|
484
|
+
return np.column_stack((v1, v2))
|
|
485
|
+
|
|
486
|
+
"""
|
|
487
|
+
This is a Python function that tests whether a given input is a vector and returns its size if
|
|
488
|
+
successful.
|
|
489
|
+
|
|
490
|
+
:param v: `v` is a numpy array that is expected to be a vector
|
|
491
|
+
:type v: np.ndarray
|
|
492
|
+
:param fun_name: `fun_name` is an optional parameter that represents the name of the calling
|
|
493
|
+
function. If provided, it will be used in error messages to indicate which function caused the
|
|
494
|
+
error. If not provided, the error message will not include the function name
|
|
495
|
+
:type fun_name: Optional[str]
|
|
496
|
+
"""
|
|
497
|
+
|
|
498
|
+
|
|
499
|
+
def check_vector(v: Any, fun_name: str = None) -> int:
|
|
500
|
+
"""
|
|
501
|
+
test that `v` is a vector; aborts otherwise
|
|
502
|
+
|
|
503
|
+
Args:
|
|
504
|
+
v: a vector, we hope
|
|
505
|
+
fun_name: name of the calling function
|
|
506
|
+
|
|
507
|
+
Returns:
|
|
508
|
+
the size if successful
|
|
509
|
+
"""
|
|
510
|
+
fun_str = ["" if fun_name is None else fun_name + ":"]
|
|
511
|
+
if not isinstance(v, np.ndarray):
|
|
512
|
+
bs_error_abort(f"{fun_str} v should be a Numpy array")
|
|
513
|
+
v = cast(np.ndarray, v)
|
|
514
|
+
ndims_v = v.ndim
|
|
515
|
+
if ndims_v != 1:
|
|
516
|
+
bs_error_abort(f"{fun_str} v should have one dimension, not {ndims_v}")
|
|
517
|
+
return cast(int, v.size)
|
|
518
|
+
|
|
519
|
+
|
|
520
|
+
def check_matrix(x: Any, fun_name: str = None) -> tuple[int, int]:
|
|
521
|
+
"""
|
|
522
|
+
test that `x` is a matrix; aborts otherwise
|
|
523
|
+
|
|
524
|
+
Args:
|
|
525
|
+
x: a matrix, we hope
|
|
526
|
+
fun_name: name of the calling function
|
|
527
|
+
|
|
528
|
+
Returns:
|
|
529
|
+
the shape if successful
|
|
530
|
+
"""
|
|
531
|
+
fun_str = ["" if fun_name is None else fun_name + ":"]
|
|
532
|
+
if not isinstance(x, np.ndarray):
|
|
533
|
+
bs_error_abort(f"{fun_str} Xx should be a Numpy array")
|
|
534
|
+
x = cast(np.ndarray, x)
|
|
535
|
+
ndims_x = x.ndim
|
|
536
|
+
if ndims_x != 2:
|
|
537
|
+
bs_error_abort(f"{fun_str} x should have two dimensions, not {ndims_x}")
|
|
538
|
+
return cast(tuple[int, int], x.shape)
|
|
539
|
+
|
|
540
|
+
|
|
541
|
+
def check_vector_or_matrix(x: Any, fun_name: str = None) -> int:
|
|
542
|
+
"""
|
|
543
|
+
test that `x` is a vector or a matrix; aborts otherwise
|
|
544
|
+
|
|
545
|
+
Args:
|
|
546
|
+
x: a vector or matrix, we hope
|
|
547
|
+
fun_name: name of the calling function
|
|
548
|
+
|
|
549
|
+
Returns:
|
|
550
|
+
the number of dimensions of `x` (1 or 2)
|
|
551
|
+
"""
|
|
552
|
+
fun_str = ["" if fun_name is None else fun_name + ":"]
|
|
553
|
+
if not isinstance(x, np.ndarray):
|
|
554
|
+
bs_error_abort(f"{fun_str} X should be a Numpy array")
|
|
555
|
+
x = cast(np.ndarray, x)
|
|
556
|
+
ndims_x = x.ndim
|
|
557
|
+
if ndims_x != 1 and ndims_x != 2:
|
|
558
|
+
bs_error_abort(f"{fun_str} x should have at most two dimensions, not {ndims_x}")
|
|
559
|
+
return cast(int, ndims_x)
|
|
560
|
+
|
|
561
|
+
|
|
562
|
+
def bs_sqrt_pdmatrix(m: np.ndarray) -> np.ndarray:
|
|
563
|
+
"""
|
|
564
|
+
square root of a positive definite matrix
|
|
565
|
+
|
|
566
|
+
Args:
|
|
567
|
+
m: a positive definite matrix
|
|
568
|
+
|
|
569
|
+
Returns:
|
|
570
|
+
the square root of the matrix
|
|
571
|
+
"""
|
|
572
|
+
_ = check_square(m, "bs_sqrt_pdmatrix")
|
|
573
|
+
eigval, eigvec = np.linalg.eigh(m)
|
|
574
|
+
eigval = np.maximum(eigval, 0.0)
|
|
575
|
+
eigval_sqrt = np.sqrt(eigval)
|
|
576
|
+
eigval_sqrt_diag = np.diag(eigval_sqrt)
|
|
577
|
+
res = eigvec @ eigval_sqrt_diag @ eigvec.T
|
|
578
|
+
return cast(np.ndarray, res)
|
|
579
|
+
|
|
580
|
+
|
|
581
|
+
def check_square(A: Any, fun_name: str | None) -> int:
|
|
582
|
+
"""
|
|
583
|
+
test that an object used in `fun_name` is a square matrix
|
|
584
|
+
|
|
585
|
+
Args:
|
|
586
|
+
A: square matrix, we hope
|
|
587
|
+
fun_name: the name of the calling function
|
|
588
|
+
|
|
589
|
+
Returns:
|
|
590
|
+
the number of rows and columns of `A`
|
|
591
|
+
"""
|
|
592
|
+
fun_str = ["" if fun_name is None else fun_name + ":"]
|
|
593
|
+
if not isinstance(A, np.ndarray):
|
|
594
|
+
bs_error_abort(f"{fun_str} A should be a Numpy array")
|
|
595
|
+
A = cast(np.ndarray, A)
|
|
596
|
+
if A.ndim == 2:
|
|
597
|
+
n, nv = A.shape
|
|
598
|
+
if nv != n:
|
|
599
|
+
bs_error_abort(f"{fun_str} The matrix A should be square, not {A.shape}")
|
|
600
|
+
else:
|
|
601
|
+
bs_error_abort(f"{fun_name} A should have two dimensions, not {A.ndim}")
|
|
602
|
+
return cast(int, n)
|
|
603
|
+
|
|
604
|
+
|
|
605
|
+
def check_tensor(x: Any, n_dims: int, fun_name: str | None) -> tuple[int, ...]:
|
|
606
|
+
"""
|
|
607
|
+
test that `x` is an `n_dims` dimensional array; aborts otherwise
|
|
608
|
+
|
|
609
|
+
Args:
|
|
610
|
+
x: an `n_dims` dimensional array, we hope
|
|
611
|
+
fun_name: name of the calling function
|
|
612
|
+
|
|
613
|
+
Returns:
|
|
614
|
+
the shape if successful
|
|
615
|
+
"""
|
|
616
|
+
fun_str = ["" if fun_name is None else fun_name + ":"]
|
|
617
|
+
if not isinstance(x, np.ndarray):
|
|
618
|
+
bs_error_abort(f"{fun_str} x should be a Numpy array")
|
|
619
|
+
x = cast(np.ndarray, x)
|
|
620
|
+
ndims_x = x.ndim
|
|
621
|
+
if ndims_x != n_dims:
|
|
622
|
+
bs_error_abort(f"{fun_str} x should have {n_dims} dimensions, not {ndims_x}")
|
|
623
|
+
return (0,) # for mypy
|
|
624
|
+
return cast(tuple[int, ...], x.shape)
|
|
625
|
+
|
|
626
|
+
|
|
627
|
+
def make_lexico_grid(arr: np.ndarray) -> np.ndarray:
|
|
628
|
+
"""
|
|
629
|
+
make a lexicographic grid
|
|
630
|
+
|
|
631
|
+
Args:
|
|
632
|
+
arr: `nr`-vector or `(nr,nc)` matrix; `nc` must be 1, 2 or 3
|
|
633
|
+
|
|
634
|
+
Returns:
|
|
635
|
+
`arr` if it is a vector; otherwise a matrix `({nr}^{nc}, {nc})`
|
|
636
|
+
for `nc=2` it is like `bsgrid`
|
|
637
|
+
"""
|
|
638
|
+
ndims_arr = check_vector_or_matrix(arr, "make_lexico_grid`")
|
|
639
|
+
if ndims_arr == 1:
|
|
640
|
+
return arr
|
|
641
|
+
else:
|
|
642
|
+
nr, nc = arr.shape
|
|
643
|
+
if nc == 2:
|
|
644
|
+
n0 = np.repeat(arr[:, 0], nr)
|
|
645
|
+
n1 = np.tile(arr[:, 1], nr)
|
|
646
|
+
return np.column_stack((n0, n1))
|
|
647
|
+
elif nc == 3:
|
|
648
|
+
nr2 = nr * nr
|
|
649
|
+
n0 = np.repeat(arr[:, 0], nr2)
|
|
650
|
+
n1 = np.repeat(np.tile(arr[:, 1], nr), nr)
|
|
651
|
+
n2 = np.tile(arr[:, 2], nr2)
|
|
652
|
+
return np.column_stack((n0, n1, n2))
|
|
653
|
+
else:
|
|
654
|
+
bs_error_abort(
|
|
655
|
+
f"at this stage, the number of columns must be 3 or less, not {nc}..."
|
|
656
|
+
)
|
|
657
|
+
return arr # for mypy
|
|
658
|
+
|
|
659
|
+
|
|
660
|
+
class BivariatePolynomial:
|
|
661
|
+
"""
|
|
662
|
+
a class for bivariate polynomials as a list of `Polynomial` objects
|
|
663
|
+
|
|
664
|
+
minimal interface:
|
|
665
|
+
|
|
666
|
+
* construct from matrix of coefficients
|
|
667
|
+
* add, subtract, multiply (with constant and with :class:`BivariatePolynomial`)
|
|
668
|
+
* evaluate p(x, y) when x, y are at most vectors (and have the same shape if both vectors)
|
|
669
|
+
"""
|
|
670
|
+
|
|
671
|
+
def __init__(self, coeffs: np.ndarray):
|
|
672
|
+
"""
|
|
673
|
+
constructor for :class:`BivariatePolynomial`
|
|
674
|
+
|
|
675
|
+
coeffs: a two-dimensional array `(deg1+1, deg2+2)`
|
|
676
|
+
"""
|
|
677
|
+
self.deg1, self.deg2 = coeffs.shape[0] - 1, coeffs.shape[1] - 1
|
|
678
|
+
self.coef = coeffs
|
|
679
|
+
self.listpol2 = []
|
|
680
|
+
for k in range(self.deg1 + 1):
|
|
681
|
+
self.listpol2.append(Polynomial(coeffs[k, :]))
|
|
682
|
+
|
|
683
|
+
def __add__(self, bivpol):
|
|
684
|
+
if isinstance(bivpol, (int, float)):
|
|
685
|
+
coeffs = self.coef.copy()
|
|
686
|
+
coeffs[0, 0] += bivpol
|
|
687
|
+
return BivariatePolynomial(coeffs)
|
|
688
|
+
degbp1, degbp2 = bivpol.deg1, bivpol.deg2
|
|
689
|
+
max_deg1 = max(degbp1, self.deg1)
|
|
690
|
+
max_deg2 = max(degbp2, self.deg2)
|
|
691
|
+
coeffs_new = nppad2_end_zeros(self.coef, max_deg1 + 1, max_deg2 + 1)
|
|
692
|
+
coeffsbp_new = nppad2_end_zeros(bivpol.coef, max_deg1 + 1, max_deg2 + 1)
|
|
693
|
+
return BivariatePolynomial(coeffs_new + coeffsbp_new)
|
|
694
|
+
|
|
695
|
+
def __repr__(self):
|
|
696
|
+
return f"BivariatePolynomial({self.deg1!r}, {self.deg2!r})"
|
|
697
|
+
|
|
698
|
+
def __iadd__(self, bivpol):
|
|
699
|
+
return self.__add__(bivpol)
|
|
700
|
+
|
|
701
|
+
def __radd__(self, bivpol):
|
|
702
|
+
return self.__add__(bivpol)
|
|
703
|
+
|
|
704
|
+
def __sub__(self, bivpol):
|
|
705
|
+
if isinstance(bivpol, (int, float)):
|
|
706
|
+
coeffs = self.coef.copy()
|
|
707
|
+
coeffs[0, 0] -= bivpol
|
|
708
|
+
return BivariatePolynomial(coeffs)
|
|
709
|
+
degbp1, degbp2 = bivpol.deg1, bivpol.deg2
|
|
710
|
+
max_deg1 = max(degbp1, self.deg1)
|
|
711
|
+
max_deg2 = max(degbp2, self.deg2)
|
|
712
|
+
coeffs_new = nppad2_end_zeros(self.coef, max_deg1 + 1, max_deg2 + 1)
|
|
713
|
+
coeffsbp_new = nppad2_end_zeros(bivpol.coef, max_deg1 + 1, max_deg2 + 1)
|
|
714
|
+
return BivariatePolynomial(coeffs_new - coeffsbp_new)
|
|
715
|
+
|
|
716
|
+
def __mul__(self, bivpol):
|
|
717
|
+
if isinstance(bivpol, (int, float)):
|
|
718
|
+
return BivariatePolynomial(bivpol * self.coef)
|
|
719
|
+
deg1, degbp1 = self.deg1, bivpol.deg1
|
|
720
|
+
deg2, degbp2 = self.deg2, bivpol.deg2
|
|
721
|
+
degmul1 = deg1 + degbp1
|
|
722
|
+
degmul2 = deg2 + degbp2
|
|
723
|
+
lp2, blp2 = self.listpol2, bivpol.listpol2
|
|
724
|
+
|
|
725
|
+
coeffs_mul = np.zeros((degmul1 + 1, degmul2 + 1))
|
|
726
|
+
for m in range(degmul1 + 1):
|
|
727
|
+
minm = max(0, m - degbp1)
|
|
728
|
+
maxm = min(m, self.deg1)
|
|
729
|
+
pm = Polynomial(0)
|
|
730
|
+
for i in range(minm, maxm + 1):
|
|
731
|
+
pm += lp2[i] * blp2[m - i]
|
|
732
|
+
coeffs_mul[m, :] += pm.coef
|
|
733
|
+
|
|
734
|
+
bp_mul = BivariatePolynomial(coeffs_mul)
|
|
735
|
+
return bp_mul
|
|
736
|
+
|
|
737
|
+
def __rmul__(self, bivpol):
|
|
738
|
+
return self.__mul__(bivpol)
|
|
739
|
+
|
|
740
|
+
def __call__(self, x1, x2):
|
|
741
|
+
x1fac = 1.0
|
|
742
|
+
val = 0.0
|
|
743
|
+
for p in self.listpol2:
|
|
744
|
+
val += p(x2) * x1fac
|
|
745
|
+
x1fac *= x1
|
|
746
|
+
return val
|
|
747
|
+
|
|
748
|
+
|
|
749
|
+
def outer_bivar(pol1: Polynomial, pol2: Polynomial) -> BivariatePolynomial:
|
|
750
|
+
"""
|
|
751
|
+
make a `BivariatePolynomial` from the product of two `Polynomial` objects
|
|
752
|
+
|
|
753
|
+
Args:
|
|
754
|
+
pol1: Polynomial in the first variable
|
|
755
|
+
pol2: Polynomial in the second variable
|
|
756
|
+
|
|
757
|
+
Returns:
|
|
758
|
+
a `BivariatePolynomial` = `pol1 * pol2`
|
|
759
|
+
"""
|
|
760
|
+
p1 = pol1.coef
|
|
761
|
+
p2 = pol2.coef
|
|
762
|
+
prod_coef = np.outer(p1, p2)
|
|
763
|
+
return BivariatePolynomial(prod_coef)
|
|
764
|
+
|
|
765
|
+
|
|
766
|
+
def npxlogx(
|
|
767
|
+
arr: np.ndarray,
|
|
768
|
+
eps: float = 1e-30,
|
|
769
|
+
deriv: int = 0,
|
|
770
|
+
verbose: bool = False,
|
|
771
|
+
) -> np.ndarray | TwoArrays | ThreeArrays:
|
|
772
|
+
"""
|
|
773
|
+
C^2` extension of `a\\ln(a)` below `eps`, perhaps with derivatives
|
|
774
|
+
|
|
775
|
+
Args:
|
|
776
|
+
arr: a Numpy array
|
|
777
|
+
eps: lower bound
|
|
778
|
+
deriv: if 1, compute derivative, if 2, second derivative
|
|
779
|
+
verbose: prints debugging info
|
|
780
|
+
|
|
781
|
+
Returns:
|
|
782
|
+
`a\\ln(a)` `C^2`-extended below `eps`, perhaps with derivatives
|
|
783
|
+
"""
|
|
784
|
+
if deriv not in [0, 1, 2]:
|
|
785
|
+
bs_error_abort(f"deriv must be 0, 1, or 2; not {deriv}")
|
|
786
|
+
if np.min(arr) > eps:
|
|
787
|
+
return cast(np.ndarray, arr * np.log(arr))
|
|
788
|
+
else:
|
|
789
|
+
logeps = log(eps)
|
|
790
|
+
logarreps = np.log(np.maximum(arr, eps))
|
|
791
|
+
xlogarreps = arr * logarreps
|
|
792
|
+
xlogarr_smaller = arr * (arr / eps + logeps - 1.0)
|
|
793
|
+
if verbose:
|
|
794
|
+
n_small_args = np.sum(arr < eps)
|
|
795
|
+
if n_small_args > 0:
|
|
796
|
+
finals = "s" if n_small_args > 1 else ""
|
|
797
|
+
print(
|
|
798
|
+
f"npxlogx: {n_small_args} argument{finals} smaller than {eps}: mini"
|
|
799
|
+
f" = {np.min(arr)}"
|
|
800
|
+
)
|
|
801
|
+
xlogval = np.where(arr > eps, xlogarreps, xlogarr_smaller)
|
|
802
|
+
if deriv == 0:
|
|
803
|
+
return xlogval
|
|
804
|
+
dxlogarreps = 1.0 + logarreps
|
|
805
|
+
dxlogarr_smaller = logeps + arr / eps
|
|
806
|
+
dxlogval = np.where(arr > eps, dxlogarreps, dxlogarr_smaller)
|
|
807
|
+
if deriv == 1:
|
|
808
|
+
return cast(TwoArrays, (xlogval, dxlogval))
|
|
809
|
+
# deriv == 2
|
|
810
|
+
d2xlogval = 1.0 / np.maximum(arr, eps)
|
|
811
|
+
return cast(ThreeArrays, (xlogval, dxlogval, d2xlogval))
|
|
812
|
+
|
|
813
|
+
|
|
814
|
+
def gauher(n: int) -> TwoArrays:
|
|
815
|
+
"""
|
|
816
|
+
nodes and weights for Gauss-Hermite integration
|
|
817
|
+
|
|
818
|
+
Args:
|
|
819
|
+
n: number of nodes
|
|
820
|
+
|
|
821
|
+
Returns:
|
|
822
|
+
array of `n` nodes, array of `n` weights
|
|
823
|
+
"""
|
|
824
|
+
EPS = 1.0e-14
|
|
825
|
+
PIM4 = 0.7511255444649425
|
|
826
|
+
MAXIT = 10
|
|
827
|
+
|
|
828
|
+
x = np.zeros(n)
|
|
829
|
+
w = np.zeros(n)
|
|
830
|
+
|
|
831
|
+
m = (n + 1) // 2
|
|
832
|
+
|
|
833
|
+
for i in range(m):
|
|
834
|
+
if i == 0:
|
|
835
|
+
n2 = 2.0 * n + 1.0
|
|
836
|
+
z = sqrt(n2) - 1.85575 * (n2**-0.16667)
|
|
837
|
+
elif i == 1:
|
|
838
|
+
z -= 1.14 * (n**0.426) / z
|
|
839
|
+
elif i == 2:
|
|
840
|
+
z = 1.86 * z - 0.86 * x[0]
|
|
841
|
+
elif i == 3:
|
|
842
|
+
z = 1.91 * z - 0.91 * x[1]
|
|
843
|
+
else:
|
|
844
|
+
z = 2.0 * z - x[i - 2]
|
|
845
|
+
for _n_iter in range(MAXIT):
|
|
846
|
+
p1 = PIM4
|
|
847
|
+
p2 = 0.0
|
|
848
|
+
for j in range(n):
|
|
849
|
+
p3 = p2
|
|
850
|
+
p2 = p1
|
|
851
|
+
p1 = z * sqrt(2.0 / (j + 1)) * p2 - sqrt(j / (j + 1)) * p3
|
|
852
|
+
pp = sqrt(2 * n) * p2
|
|
853
|
+
z1 = z
|
|
854
|
+
z = z1 - p1 / pp
|
|
855
|
+
if abs(z - z1) <= EPS:
|
|
856
|
+
break
|
|
857
|
+
if _n_iter >= MAXIT:
|
|
858
|
+
bs_error_abort(f"too many iterations: {_n_iter}")
|
|
859
|
+
x[i] = z
|
|
860
|
+
x[n - 1 - i] = -z
|
|
861
|
+
w[i] = 2.0 / (pp * pp)
|
|
862
|
+
w[n - 1 - i] = w[i]
|
|
863
|
+
|
|
864
|
+
# need to reverse order for x (w is symmetric)
|
|
865
|
+
return cast(TwoArrays, (x[::-1], w))
|
|
866
|
+
|
|
867
|
+
|
|
868
|
+
def gauleg(n: int) -> TwoArrays:
|
|
869
|
+
"""
|
|
870
|
+
nodes and weights for Gauss-Legendre integration `\\int_{-1}^1 f(x)dx`
|
|
871
|
+
|
|
872
|
+
Args:
|
|
873
|
+
n: number of nodes
|
|
874
|
+
|
|
875
|
+
Returns:
|
|
876
|
+
array of `n` nodes, array of `n` weights
|
|
877
|
+
"""
|
|
878
|
+
x = np.zeros(n)
|
|
879
|
+
w = np.zeros(n)
|
|
880
|
+
EPS = 3e-11
|
|
881
|
+
m = (n + 1) // 2
|
|
882
|
+
for i in range(1, m + 1):
|
|
883
|
+
z = cos(pi * (i - 0.25) / (n + 0.5))
|
|
884
|
+
z1 = np.inf
|
|
885
|
+
while abs(z - z1) > EPS:
|
|
886
|
+
p1 = 1.0
|
|
887
|
+
p2 = 0.0
|
|
888
|
+
for j in range(1, n + 1):
|
|
889
|
+
p3 = p2
|
|
890
|
+
p2 = p1
|
|
891
|
+
p1 = ((2.0 * j - 1.0) * z * p2 - (j - 1.0) * p3) / j
|
|
892
|
+
pp = n * (z * p1 - p2) / (z * z - 1.0)
|
|
893
|
+
z1 = z
|
|
894
|
+
z = z1 - p1 / pp
|
|
895
|
+
x[i - 1] = -z
|
|
896
|
+
x[n - i] = z
|
|
897
|
+
w[i - 1] = 2.0 / ((1.0 - z * z) * pp * pp)
|
|
898
|
+
w[n - i] = w[i - 1]
|
|
899
|
+
|
|
900
|
+
return cast(TwoArrays, (x, w))
|
|
901
|
+
|
|
902
|
+
|
|
903
|
+
def gaussian_expectation(
|
|
904
|
+
f: Callable,
|
|
905
|
+
x: np.ndarray | None,
|
|
906
|
+
w: np.ndarray | None,
|
|
907
|
+
n: int = 16,
|
|
908
|
+
vectorized: bool = False,
|
|
909
|
+
pars: Iterable = None,
|
|
910
|
+
) -> np.ndarray | float:
|
|
911
|
+
"""
|
|
912
|
+
computes the expectation of a function of an `N(0,1)` random variable
|
|
913
|
+
using Gauss-Hermite with n nodes
|
|
914
|
+
the nodes and weights can be provided, if available
|
|
915
|
+
|
|
916
|
+
Args:
|
|
917
|
+
f: a scalar or array function of a scalar or array variable and possibly other parameters
|
|
918
|
+
vectorized: if True, the function accepts an array as argument
|
|
919
|
+
pars: parameters for `f`, if any
|
|
920
|
+
n: number of nodes
|
|
921
|
+
x: locations of the nodes
|
|
922
|
+
w: their weights
|
|
923
|
+
|
|
924
|
+
Returns:
|
|
925
|
+
the expectation of `f(N(0,1))`
|
|
926
|
+
"""
|
|
927
|
+
if x is None:
|
|
928
|
+
nodes, weights = gauher(n)
|
|
929
|
+
nodes *= sqrt(2.0)
|
|
930
|
+
weights /= sqrt(pi)
|
|
931
|
+
n_nodes = n
|
|
932
|
+
elif w is None:
|
|
933
|
+
bs_error_abort("x is None but w is not")
|
|
934
|
+
elif w.size != x.size:
|
|
935
|
+
bs_error_abort("x has {x.size} elements and w has {w.size}")
|
|
936
|
+
else:
|
|
937
|
+
nodes = x * sqrt(2.0)
|
|
938
|
+
weights = w / sqrt(pi)
|
|
939
|
+
n_nodes = nodes.size
|
|
940
|
+
if pars is None:
|
|
941
|
+
if vectorized:
|
|
942
|
+
integral_vec = f(nodes) @ weights
|
|
943
|
+
else:
|
|
944
|
+
# to ensure integral_val has the same shape as f
|
|
945
|
+
integral_val = weights[0] * f(nodes[0])
|
|
946
|
+
for i in range(1, n_nodes):
|
|
947
|
+
integral_val += weights[i] * f(nodes[i])
|
|
948
|
+
else:
|
|
949
|
+
if vectorized:
|
|
950
|
+
integral_vec = f(nodes, pars) @ weights
|
|
951
|
+
else:
|
|
952
|
+
# to ensure integral_val has the same shape as f
|
|
953
|
+
integral_val = weights[0] * f(nodes[0], pars)
|
|
954
|
+
for i in range(1, n_nodes):
|
|
955
|
+
integral_val += weights[i] * f(nodes[i], pars)
|
|
956
|
+
|
|
957
|
+
return cast(np.ndarray, integral_vec) if vectorized else cast(float, integral_val)
|