bezierv 0.1.0__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.
- bezierv/__init__.py +0 -0
- bezierv/algorithms/__init__.py +0 -0
- bezierv/algorithms/conv_bezierv.py +113 -0
- bezierv/algorithms/nelder_mead.py +157 -0
- bezierv/algorithms/non_linear.py +167 -0
- bezierv/algorithms/non_linear_solver.py +186 -0
- bezierv/algorithms/proj_grad.py +132 -0
- bezierv/algorithms/proj_subgrad.py +203 -0
- bezierv/algorithms/utils.py +67 -0
- bezierv/classes/__init__.py +0 -0
- bezierv/classes/bezierv.py +579 -0
- bezierv/classes/convolver.py +74 -0
- bezierv/classes/distfit.py +216 -0
- bezierv/tests/__init__.py +0 -0
- bezierv/tests/conf_test.py +0 -0
- bezierv/tests/test_algorithms/test_conv_bezierv.py +0 -0
- bezierv/tests/test_algorithms/test_nelder_mead.py +54 -0
- bezierv/tests/test_algorithms/test_proj_grad.py +63 -0
- bezierv/tests/test_algorithms/test_proj_subgrad.py +65 -0
- bezierv/tests/test_algorithms/test_utils.py +42 -0
- bezierv/tests/test_classes/conftest.py +42 -0
- bezierv/tests/test_classes/test_bezierv.py +0 -0
- bezierv/tests/test_classes/test_convolver.py +36 -0
- bezierv/tests/test_classes/test_distfit.py +34 -0
- bezierv-0.1.0.dist-info/METADATA +32 -0
- bezierv-0.1.0.dist-info/RECORD +29 -0
- bezierv-0.1.0.dist-info/WHEEL +5 -0
- bezierv-0.1.0.dist-info/licenses/LICENSE +21 -0
- bezierv-0.1.0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,579 @@
|
|
1
|
+
import numpy as np
|
2
|
+
import math
|
3
|
+
import matplotlib.pyplot as plt
|
4
|
+
|
5
|
+
from scipy.optimize import brentq, bisect
|
6
|
+
from scipy.integrate import quad
|
7
|
+
from statsmodels.distributions.empirical_distribution import ECDF
|
8
|
+
|
9
|
+
from numpy import tanh, arctanh
|
10
|
+
|
11
|
+
class Bezierv:
|
12
|
+
def __init__(self,
|
13
|
+
n: int,
|
14
|
+
controls_x=None,
|
15
|
+
controls_z=None):
|
16
|
+
"""
|
17
|
+
Initialize a Bezierv instance representing a Bezier random variable.
|
18
|
+
|
19
|
+
This constructor initializes the Bezier curve with a given number of control points `n` and
|
20
|
+
optionally with provided control points for the x and z coordinates. If control points are not
|
21
|
+
provided, they are initialized as zero arrays. It also sets up auxiliary arrays for differences
|
22
|
+
(deltas) and binomial coefficients, and initializes moment attributes to NaN.
|
23
|
+
|
24
|
+
Parameters
|
25
|
+
----------
|
26
|
+
n : int
|
27
|
+
The number of control points of the Bezier random variable.
|
28
|
+
controls_x : array-like, optional
|
29
|
+
The x-coordinates of the control points (length n+1). If None, a zero array is created.
|
30
|
+
controls_z : array-like, optional
|
31
|
+
The z-coordinates of the control points (length n+1). If None, a zero array is created.
|
32
|
+
|
33
|
+
Attributes
|
34
|
+
----------
|
35
|
+
n : int
|
36
|
+
The number of control points of the Bezier random variables.
|
37
|
+
deltas_x : np.array
|
38
|
+
Differences between consecutive x control points.
|
39
|
+
deltas_z : np.array
|
40
|
+
Differences between consecutive z control points.
|
41
|
+
comb : np.array
|
42
|
+
Array of binomial coefficients (n).
|
43
|
+
comb_minus : np.array
|
44
|
+
Array of binomial coefficients (n - 1).
|
45
|
+
support : tuple
|
46
|
+
The support of the Bezier random variable (initialized as (-inf, inf)).
|
47
|
+
controls_x : np.array
|
48
|
+
x-coordinates of the control points.
|
49
|
+
controls_z : np.array
|
50
|
+
z-coordinates of the control points.
|
51
|
+
mean : float
|
52
|
+
The mean of the curve (initialized as np.inf).
|
53
|
+
var : float
|
54
|
+
The variance of the curve (initialized as np.inf).
|
55
|
+
skew : float
|
56
|
+
The skewness of the curve (initialized as np.inf).
|
57
|
+
kurt : float
|
58
|
+
The kurtosis of the curve (initialized as np.inf).
|
59
|
+
"""
|
60
|
+
self.n = n
|
61
|
+
self.deltas_x = np.zeros(n)
|
62
|
+
self.deltas_z = np.zeros(n)
|
63
|
+
self.comb = np.zeros(n + 1)
|
64
|
+
self.comb_minus = np.zeros(n)
|
65
|
+
self.support = (-np.inf, np.inf)
|
66
|
+
|
67
|
+
if controls_x is None and controls_z is None:
|
68
|
+
self.controls_x = np.zeros(n + 1)
|
69
|
+
self.controls_z = np.zeros(n + 1)
|
70
|
+
elif controls_x is not None and controls_z is not None:
|
71
|
+
controls_x = np.asarray(controls_x, dtype=float)
|
72
|
+
controls_z = np.asarray(controls_z, dtype=float)
|
73
|
+
self._validate_lengths(controls_x, controls_z)
|
74
|
+
self._validate_ordering(controls_x, controls_z)
|
75
|
+
self.controls_x = controls_x
|
76
|
+
self.controls_z = controls_z
|
77
|
+
self.support = (controls_x[0], controls_x[-1])
|
78
|
+
else:
|
79
|
+
raise ValueError('Either all or none of the parameters controls_x and controls_y must be provided')
|
80
|
+
|
81
|
+
self.mean = np.inf
|
82
|
+
self.variance = np.inf
|
83
|
+
self.skewness = np.inf
|
84
|
+
self.kurtosis = np.inf
|
85
|
+
|
86
|
+
self.combinations()
|
87
|
+
self.deltas()
|
88
|
+
|
89
|
+
def update_bezierv(self,
|
90
|
+
controls_x: np.array,
|
91
|
+
controls_z: np.array):
|
92
|
+
"""
|
93
|
+
Update the control points for the Bezier curve, bounds and recalculate deltas.
|
94
|
+
|
95
|
+
Parameters
|
96
|
+
----------
|
97
|
+
controls_x : array-like
|
98
|
+
The new x-coordinates of the control points.
|
99
|
+
controls_z : array-like
|
100
|
+
The new z-coordinates of the control points.
|
101
|
+
|
102
|
+
Returns
|
103
|
+
-------
|
104
|
+
None
|
105
|
+
"""
|
106
|
+
controls_x = np.asarray(controls_x, dtype=float)
|
107
|
+
controls_z = np.asarray(controls_z, dtype=float)
|
108
|
+
self._validate_lengths(controls_x, controls_z)
|
109
|
+
self._validate_ordering(controls_x, controls_z)
|
110
|
+
|
111
|
+
self.controls_x = controls_x
|
112
|
+
self.controls_z = controls_z
|
113
|
+
self.support = (controls_x[0], controls_x[-1])
|
114
|
+
|
115
|
+
self.deltas()
|
116
|
+
|
117
|
+
def combinations(self):
|
118
|
+
"""
|
119
|
+
Compute and store binomial coefficients.
|
120
|
+
|
121
|
+
Parameters
|
122
|
+
----------
|
123
|
+
None
|
124
|
+
|
125
|
+
Returns
|
126
|
+
-------
|
127
|
+
None
|
128
|
+
"""
|
129
|
+
n = self.n
|
130
|
+
for i in range(0, n + 1):
|
131
|
+
self.comb[i] = math.comb(n, i)
|
132
|
+
if i < n:
|
133
|
+
self.comb_minus[i] = math.comb(n - 1, i)
|
134
|
+
|
135
|
+
def deltas(self):
|
136
|
+
"""
|
137
|
+
Compute the differences between consecutive control points.
|
138
|
+
|
139
|
+
Parameters
|
140
|
+
----------
|
141
|
+
None
|
142
|
+
|
143
|
+
Returns
|
144
|
+
-------
|
145
|
+
None
|
146
|
+
"""
|
147
|
+
n = self.n
|
148
|
+
for i in range(n):
|
149
|
+
self.deltas_x[i] = self.controls_x[i + 1] - self.controls_x[i]
|
150
|
+
self.deltas_z[i] = self.controls_z[i + 1] - self.controls_z[i]
|
151
|
+
|
152
|
+
def bernstein(self, t, i, combinations, n):
|
153
|
+
"""
|
154
|
+
Compute the Bernstein basis polynomial value.
|
155
|
+
|
156
|
+
Parameters
|
157
|
+
----------
|
158
|
+
t : float
|
159
|
+
The parameter value (in the interval [0, 1]).
|
160
|
+
i : int
|
161
|
+
The index of the Bernstein basis polynomial.
|
162
|
+
combinations : array-like
|
163
|
+
An array of binomial coefficients to use in the computation.
|
164
|
+
n : int
|
165
|
+
The degree for the Bernstein polynomial.
|
166
|
+
|
167
|
+
Returns
|
168
|
+
-------
|
169
|
+
float
|
170
|
+
The value of the Bernstein basis polynomial at t.
|
171
|
+
"""
|
172
|
+
return combinations[i] * t**i * (1 - t)**(n - i)
|
173
|
+
|
174
|
+
def poly_x(self, t, controls_x = None):
|
175
|
+
"""
|
176
|
+
Evaluate the x-coordinate at a given t value.
|
177
|
+
|
178
|
+
Parameters
|
179
|
+
----------
|
180
|
+
t : float
|
181
|
+
The parameter value at which to evaluate (in [0, 1]).
|
182
|
+
controls_x : np.array, optional
|
183
|
+
An array of control points for the x-coordinate. Defaults to self.controls_x.
|
184
|
+
|
185
|
+
Returns
|
186
|
+
-------
|
187
|
+
float
|
188
|
+
The evaluated x-coordinate at t.
|
189
|
+
"""
|
190
|
+
if controls_x is None:
|
191
|
+
self._ensure_initialized()
|
192
|
+
controls_x = self.controls_x
|
193
|
+
n = self.n
|
194
|
+
p_x = 0
|
195
|
+
for i in range(n + 1):
|
196
|
+
p_x += self.bernstein(t, i, self.comb, self.n) * controls_x[i]
|
197
|
+
return p_x
|
198
|
+
|
199
|
+
def poly_z(self, t, controls_z = None):
|
200
|
+
"""
|
201
|
+
Evaluate the z-coordinate at a given t value.
|
202
|
+
|
203
|
+
Parameters
|
204
|
+
----------
|
205
|
+
t : float
|
206
|
+
The parameter value at which to evaluate the curve (typically in [0, 1]).
|
207
|
+
controls_z : array-like, optional
|
208
|
+
An array of control points for the z-coordinate. Defaults to self.controls_z.
|
209
|
+
|
210
|
+
Returns
|
211
|
+
-------
|
212
|
+
float
|
213
|
+
The evaluated z-coordinate at t.
|
214
|
+
"""
|
215
|
+
if controls_z is None:
|
216
|
+
self._ensure_initialized()
|
217
|
+
controls_z = self.controls_z
|
218
|
+
n = self.n
|
219
|
+
p_z = 0
|
220
|
+
for i in range(n + 1):
|
221
|
+
p_z += self.bernstein(t, i, self.comb, self.n) * controls_z[i]
|
222
|
+
return p_z
|
223
|
+
|
224
|
+
def root_find(self, x, method='brentq'):
|
225
|
+
"""
|
226
|
+
Find t such that the Bezier curve's x-coordinate equals a given value.
|
227
|
+
|
228
|
+
This method solves for the root of the equation poly_x(t) - x = 0 using a specified root-finding
|
229
|
+
algorithm. The search is performed in the interval [0, 1].
|
230
|
+
|
231
|
+
Parameters
|
232
|
+
----------
|
233
|
+
x : float
|
234
|
+
The x-coordinate for which to find the corresponding parameter t.
|
235
|
+
method : {'brentq', 'bisect'}, optional
|
236
|
+
The root-finding method to use. Default is 'brentq'.
|
237
|
+
|
238
|
+
Returns
|
239
|
+
-------
|
240
|
+
float
|
241
|
+
The parameter t in the interval [0, 1] such that poly_x(t) is approximately equal to x.
|
242
|
+
"""
|
243
|
+
self._ensure_initialized()
|
244
|
+
def poly_x_zero(t, x):
|
245
|
+
return self.poly_x(t) - x
|
246
|
+
if method == 'brentq':
|
247
|
+
t = brentq(poly_x_zero, 0, 1, args=(x,))
|
248
|
+
elif method == 'bisect':
|
249
|
+
t = bisect(poly_x_zero, 0, 1, args=(x,))
|
250
|
+
return t
|
251
|
+
|
252
|
+
def eval_t(self, t):
|
253
|
+
"""
|
254
|
+
Evaluate the CDF of the Bezier random variable at a given parameter value t.
|
255
|
+
|
256
|
+
Parameters
|
257
|
+
----------
|
258
|
+
t : float
|
259
|
+
The parameter value at which to evaluate the curve (typically in [0, 1]).
|
260
|
+
|
261
|
+
Returns
|
262
|
+
-------
|
263
|
+
tuple of floats
|
264
|
+
A tuple (p_x, p_z) where p_x is the evaluated x-coordinate and p_z is the evaluated z-coordinate.
|
265
|
+
"""
|
266
|
+
self._ensure_initialized()
|
267
|
+
n = self.n
|
268
|
+
p_x = 0
|
269
|
+
p_z = 0
|
270
|
+
for i in range(n + 1):
|
271
|
+
p_x += self.comb[i] * t**i * (1 - t)**(n - i) * self.controls_x[i]
|
272
|
+
p_z += self.comb[i] * t**i * (1 - t)**(n - i) * self.controls_z[i]
|
273
|
+
return p_x, p_z
|
274
|
+
|
275
|
+
def eval_x(self, x):
|
276
|
+
"""
|
277
|
+
Evaluate the CDF of the Bezier random variable at a given x-coordinate.
|
278
|
+
|
279
|
+
Parameters
|
280
|
+
----------
|
281
|
+
x : float
|
282
|
+
The x-coordinate at which to evaluate the Bezier curve.
|
283
|
+
|
284
|
+
Returns
|
285
|
+
-------
|
286
|
+
tuple of floats
|
287
|
+
A tuple (p_x, p_z) where p_x is the x-coordinate and p_z is the z-coordinate of the curve at x.
|
288
|
+
"""
|
289
|
+
self._ensure_initialized()
|
290
|
+
t = self.root_find(x)
|
291
|
+
return self.eval_t(t)
|
292
|
+
|
293
|
+
def cdf_x(self, x):
|
294
|
+
"""
|
295
|
+
Compute the cumulative distribution function (CDF) at a given x-coordinate.
|
296
|
+
|
297
|
+
Parameters
|
298
|
+
----------
|
299
|
+
x : float
|
300
|
+
The x-coordinate at which to evaluate the CDF.
|
301
|
+
|
302
|
+
Returns
|
303
|
+
-------
|
304
|
+
float
|
305
|
+
The CDF value at the given x-coordinate.
|
306
|
+
"""
|
307
|
+
self._ensure_initialized()
|
308
|
+
if x < self.controls_x[0]:
|
309
|
+
return 0
|
310
|
+
if x > self.controls_x[-1]:
|
311
|
+
return 1
|
312
|
+
_, p_z = self.eval_x(x)
|
313
|
+
return p_z
|
314
|
+
|
315
|
+
def quantile(self, alpha, method='brentq'):
|
316
|
+
"""
|
317
|
+
Compute the quantile function (inverse CDF) for a given probability level alpha.
|
318
|
+
|
319
|
+
Parameters
|
320
|
+
----------
|
321
|
+
alpha : float
|
322
|
+
The probability level for which to compute the quantile (in [0, 1]).
|
323
|
+
|
324
|
+
Returns
|
325
|
+
-------
|
326
|
+
float
|
327
|
+
The quantile value corresponding to the given alpha.
|
328
|
+
"""
|
329
|
+
self._ensure_initialized()
|
330
|
+
def cdf_t(t, alpha):
|
331
|
+
return self.poly_z(t) - alpha
|
332
|
+
|
333
|
+
if method == 'brentq':
|
334
|
+
t = brentq(cdf_t, 0, 1, args=(alpha,))
|
335
|
+
elif method == 'bisect':
|
336
|
+
t = bisect(cdf_t, 0, 1, args=(alpha,))
|
337
|
+
return self.poly_x(t)
|
338
|
+
|
339
|
+
def pdf_t(self, t):
|
340
|
+
"""
|
341
|
+
Compute the probability density function (PDF) of the Bezier random variable with respect to t.
|
342
|
+
|
343
|
+
Parameters
|
344
|
+
----------
|
345
|
+
t : float
|
346
|
+
The value at which to compute the PDF (in [0, 1]).
|
347
|
+
|
348
|
+
Returns
|
349
|
+
-------
|
350
|
+
float
|
351
|
+
The computed PDF value at t.
|
352
|
+
"""
|
353
|
+
self._ensure_initialized()
|
354
|
+
n = self.n
|
355
|
+
pdf_num_z = 0
|
356
|
+
pdf_denom_x = 0
|
357
|
+
for i in range(n):
|
358
|
+
pdf_num_z += self.bernstein(t, i, self.comb_minus, n - 1) * self.deltas_z[i]
|
359
|
+
pdf_denom_x += self.bernstein(t, i, self.comb_minus, n - 1) * self.deltas_x[i]
|
360
|
+
return pdf_num_z/pdf_denom_x
|
361
|
+
|
362
|
+
def pdf_x(self, x):
|
363
|
+
"""
|
364
|
+
Compute the probability density function (PDF) of the Bezier random variable at a given x.
|
365
|
+
|
366
|
+
Parameters
|
367
|
+
----------
|
368
|
+
x : float
|
369
|
+
The x-coordinate at which to evaluate the PDF.
|
370
|
+
|
371
|
+
Returns
|
372
|
+
-------
|
373
|
+
float
|
374
|
+
The computed PDF value at x.
|
375
|
+
"""
|
376
|
+
self._ensure_initialized()
|
377
|
+
t = self.root_find(x)
|
378
|
+
return self.pdf_t(t)
|
379
|
+
|
380
|
+
def pdf_numerator_t(self, t):
|
381
|
+
"""
|
382
|
+
Compute the numerator part of the PDF for the Bezier random variable with respect to t.
|
383
|
+
|
384
|
+
Parameters
|
385
|
+
----------
|
386
|
+
t : float
|
387
|
+
The value at which to compute the PDF numerator (in [0, 1]).
|
388
|
+
|
389
|
+
Returns
|
390
|
+
-------
|
391
|
+
float
|
392
|
+
The numerator of the PDF at t.
|
393
|
+
"""
|
394
|
+
self._ensure_initialized()
|
395
|
+
pdf_num_z = 0
|
396
|
+
for i in range(self.n):
|
397
|
+
pdf_num_z += self.bernstein(t, i, self.comb_minus, self.n - 1) * self.deltas_z[i]
|
398
|
+
return pdf_num_z
|
399
|
+
|
400
|
+
def get_mean(self, closed_form: bool=True):
|
401
|
+
"""
|
402
|
+
Compute and return the mean of the distribution.
|
403
|
+
|
404
|
+
Parameters
|
405
|
+
----------
|
406
|
+
closed_form : bool, optional
|
407
|
+
If True, use a closed-form solution for the mean. If False, compute it numerically (default is True).
|
408
|
+
|
409
|
+
Returns
|
410
|
+
-------
|
411
|
+
float
|
412
|
+
The mean value of the distribution.
|
413
|
+
"""
|
414
|
+
self._ensure_initialized()
|
415
|
+
if self.mean == np.inf:
|
416
|
+
if closed_form:
|
417
|
+
total = 0.0
|
418
|
+
for ell in range(self.n + 1):
|
419
|
+
inner_sum = 0.0
|
420
|
+
for i in range(self.n):
|
421
|
+
denom = math.comb(2 * self.n - 1, ell + i)
|
422
|
+
inner_sum += (self.comb_minus[i] / denom) * self.deltas_z[i]
|
423
|
+
total += self.comb[ell] * self.controls_x[ell] * inner_sum
|
424
|
+
self.mean = 0.5 * total
|
425
|
+
else:
|
426
|
+
a, b = self.bounds
|
427
|
+
self.mean, _ = quad(lambda x: x * self.pdf_x(x), a, b)
|
428
|
+
return self.mean
|
429
|
+
|
430
|
+
def get_variance(self):
|
431
|
+
self._ensure_initialized()
|
432
|
+
a, b = self.bounds
|
433
|
+
E_x2, _ = quad(lambda x: (x)**2 * self.pdf_x(x), a, b)
|
434
|
+
if self.mean == np.inf:
|
435
|
+
self.variance = E_x2 - self.get_mean()**2
|
436
|
+
else:
|
437
|
+
self.variance = E_x2 - self.mean**2
|
438
|
+
return self.variance
|
439
|
+
|
440
|
+
def random(self,
|
441
|
+
n_sims: int,
|
442
|
+
*,
|
443
|
+
rng: np.random.Generator | int | None = None):
|
444
|
+
"""
|
445
|
+
Generate random samples from the Bezier random variable.
|
446
|
+
|
447
|
+
This method generates `n_sims` random samples from the Bezier random variable by evaluating
|
448
|
+
the inverse CDF at uniformly distributed random values in the interval [0, 1].
|
449
|
+
|
450
|
+
Parameters
|
451
|
+
----------
|
452
|
+
n_sims : int
|
453
|
+
The number of random samples to generate.
|
454
|
+
rng : numpy.random.Generator | int | None, optional
|
455
|
+
Pseudorandom-number generator state. If *None* (default), a new
|
456
|
+
``numpy.random.Generator`` is created with fresh OS entropy. Any
|
457
|
+
value accepted by :func:`numpy.random.default_rng` (e.g. a seed
|
458
|
+
integer or :class:`~numpy.random.SeedSequence`) is also allowed.
|
459
|
+
|
460
|
+
|
461
|
+
Returns
|
462
|
+
-------
|
463
|
+
np.array
|
464
|
+
An array of shape (n_sims,) containing the generated random samples from the Bezier random variable.
|
465
|
+
"""
|
466
|
+
self._ensure_initialized()
|
467
|
+
rng = np.random.default_rng(rng)
|
468
|
+
u = rng.uniform(0, 1, n_sims)
|
469
|
+
samples = np.zeros(n_sims)
|
470
|
+
for i in range(n_sims):
|
471
|
+
samples[i] = self.quantile(u[i])
|
472
|
+
return samples
|
473
|
+
|
474
|
+
|
475
|
+
def plot_cdf(self, data=None, num_points=100, ax=None):
|
476
|
+
"""
|
477
|
+
Plot the cumulative distribution function (CDF) of the Bezier random variable alongside
|
478
|
+
the empirical CDF (if data is provided).
|
479
|
+
|
480
|
+
Parameters
|
481
|
+
----------
|
482
|
+
data : array-like, optional
|
483
|
+
The data points at which to evaluate and plot the CDF. If None, a linspace is used.
|
484
|
+
num_points : int, optional
|
485
|
+
The number of points to use in the linspace when data is not provided (default is 100).
|
486
|
+
ax : matplotlib.axes.Axes, optional
|
487
|
+
The axes on which to plot the CDF. If None, the current axes are used.
|
488
|
+
|
489
|
+
Returns
|
490
|
+
-------
|
491
|
+
None
|
492
|
+
"""
|
493
|
+
self._ensure_initialized()
|
494
|
+
data_bool = True
|
495
|
+
if data is None:
|
496
|
+
data_bool = False
|
497
|
+
data = np.linspace(np.min(self.controls_x), np.max(self.controls_x), num_points)
|
498
|
+
|
499
|
+
data = np.sort(data)
|
500
|
+
x_bezier = np.zeros(len(data))
|
501
|
+
cdf_x_bezier = np.zeros(len(data))
|
502
|
+
|
503
|
+
for i in range(len(data)):
|
504
|
+
p_x, p_z = self.eval_x(data[i])
|
505
|
+
x_bezier[i] = p_x
|
506
|
+
cdf_x_bezier[i] = p_z
|
507
|
+
|
508
|
+
show = False
|
509
|
+
if ax is None:
|
510
|
+
ax = plt.gca()
|
511
|
+
show = True
|
512
|
+
|
513
|
+
if data_bool:
|
514
|
+
ecdf_fn = ECDF(data)
|
515
|
+
ax.plot(data, ecdf_fn(data), label='Empirical cdf', linestyle='--', color='black')
|
516
|
+
|
517
|
+
ax.plot(x_bezier, cdf_x_bezier, label='Bezier cdf', linestyle='--')
|
518
|
+
ax.scatter(self.controls_x, self.controls_z, label='Control Points', color='red')
|
519
|
+
ax.legend()
|
520
|
+
if show:
|
521
|
+
plt.show()
|
522
|
+
|
523
|
+
def plot_pdf(self, data=None, num_points=100, ax=None):
|
524
|
+
"""
|
525
|
+
Plot the probability density function (PDF) of the Bezier random variable.
|
526
|
+
|
527
|
+
Parameters
|
528
|
+
----------
|
529
|
+
data : array-like, optional
|
530
|
+
The data points at which to evaluate and plot the PDF. If None, a linspace is used.
|
531
|
+
num_points : int, optional
|
532
|
+
The number of points to use in the linspace when data is not provided (default is 100).
|
533
|
+
ax : matplotlib.axes.Axes, optional
|
534
|
+
The axes on which to plot the PDF. If None, the current axes are used.
|
535
|
+
|
536
|
+
Returns
|
537
|
+
-------
|
538
|
+
None
|
539
|
+
"""
|
540
|
+
self._ensure_initialized()
|
541
|
+
if data is None:
|
542
|
+
data = np.linspace(np.min(self.controls_x), np.max(self.controls_x), num_points)
|
543
|
+
|
544
|
+
x_bezier = np.zeros(len(data))
|
545
|
+
pdf_x_bezier = np.zeros(len(data))
|
546
|
+
|
547
|
+
show = False
|
548
|
+
if ax is None:
|
549
|
+
ax = plt.gca()
|
550
|
+
show = True
|
551
|
+
|
552
|
+
for i in range(len(data)):
|
553
|
+
p_x, _ = self.eval_x(data[i])
|
554
|
+
x_bezier[i] = p_x
|
555
|
+
pdf_x_bezier[i] = self.pdf_x(data[i])
|
556
|
+
|
557
|
+
ax.plot(x_bezier, pdf_x_bezier, label='Bezier pdf', linestyle='-')
|
558
|
+
ax.legend()
|
559
|
+
if show:
|
560
|
+
plt.show()
|
561
|
+
|
562
|
+
def _validate_lengths(self, controls_x, controls_z):
|
563
|
+
if len(controls_x) != len(controls_z):
|
564
|
+
raise ValueError("controls_x and controls_z must have the same length.")
|
565
|
+
if len(controls_x) != self.n + 1:
|
566
|
+
raise ValueError(f"controls arrays must have length n+1 (= {self.n + 1}).")
|
567
|
+
|
568
|
+
def _validate_ordering(self, controls_x, controls_z):
|
569
|
+
if np.any(np.diff(controls_x) < 0):
|
570
|
+
raise ValueError("controls_x must be nondecreasing.")
|
571
|
+
if np.any(np.diff(controls_z) < 0):
|
572
|
+
raise ValueError("controls_z must be nondecreasing.")
|
573
|
+
|
574
|
+
def _ensure_initialized(self):
|
575
|
+
if np.allclose(self.controls_x, 0) and np.allclose(self.controls_z, 0):
|
576
|
+
raise RuntimeError(
|
577
|
+
"Bezier controls are all zeros (placeholder). "
|
578
|
+
"Provide valid controls in the constructor or call update_bezierv()."
|
579
|
+
)
|
@@ -0,0 +1,74 @@
|
|
1
|
+
import numpy as np
|
2
|
+
from scipy.integrate import quad
|
3
|
+
from bezierv.classes.distfit import DistFit
|
4
|
+
from bezierv.classes.bezierv import Bezierv
|
5
|
+
|
6
|
+
class Convolver:
|
7
|
+
def __init__(self, list_bezierv: list[Bezierv]):
|
8
|
+
"""
|
9
|
+
Initialize a ConvBezier instance for convolving Bezier curves.
|
10
|
+
|
11
|
+
This constructor sets up the convolution object by storing the provided Bezierv
|
12
|
+
random variables, and creates a new Bezierv instance to hold the convolution
|
13
|
+
result. It also initializes the number of data points to be used in the numerical
|
14
|
+
convolution process.
|
15
|
+
|
16
|
+
Parameters
|
17
|
+
----------
|
18
|
+
list_bezierv : list[Bezierv]
|
19
|
+
A list of Bezierv instances representing the Bezier random variables to be convolved.
|
20
|
+
"""
|
21
|
+
for bez in list_bezierv:
|
22
|
+
bez._validate_lengths(bez.controls_x, bez.controls_z)
|
23
|
+
bez._validate_ordering(bez.controls_x, bez.controls_z)
|
24
|
+
bez._ensure_initialized()
|
25
|
+
|
26
|
+
self.list_bezierv = list_bezierv
|
27
|
+
|
28
|
+
|
29
|
+
def convolve(self,
|
30
|
+
n_sims: int = 1000,
|
31
|
+
*,
|
32
|
+
rng: np.random.Generator | int | None = None,
|
33
|
+
**kwargs) -> Bezierv:
|
34
|
+
"""
|
35
|
+
Convolve the Bezier RVs via Monte Carlo and fit a Bezierv to the sum.
|
36
|
+
|
37
|
+
Parameters
|
38
|
+
----------
|
39
|
+
n_sims : int
|
40
|
+
Number of Monte Carlo samples.
|
41
|
+
rng : numpy.random.Generator | int | None, optional
|
42
|
+
Shared PRNG stream for *all* sampling.
|
43
|
+
**kwargs :
|
44
|
+
Init options for DistFit(...):
|
45
|
+
n, init_x, init_z, init_t, emp_cdf_data, method_init_x
|
46
|
+
Fit options for DistFit.fit(...):
|
47
|
+
method, step_size_PG, max_iter_PG, threshold_PG,
|
48
|
+
step_size_PS, max_iter_PS, solver_NL, max_iter_NM
|
49
|
+
"""
|
50
|
+
rng = np.random.default_rng(rng)
|
51
|
+
|
52
|
+
bezierv_sum = np.zeros(n_sims)
|
53
|
+
for bz in self.list_bezierv:
|
54
|
+
samples = bz.random(n_sims, rng=rng)
|
55
|
+
bezierv_sum += samples
|
56
|
+
|
57
|
+
init_keys = {
|
58
|
+
"n", "init_x", "init_z", "init_t", "emp_cdf_data", "method_init_x"
|
59
|
+
}
|
60
|
+
fit_keys = {
|
61
|
+
"method", "step_size_PG", "max_iter_PG", "threshold_PG",
|
62
|
+
"step_size_PS", "max_iter_PS", "solver_NL", "max_iter_NM"
|
63
|
+
}
|
64
|
+
|
65
|
+
init_kwargs = {k: v for k, v in kwargs.items() if k in init_keys}
|
66
|
+
fit_kwargs = {k: v for k, v in kwargs.items() if k in fit_keys}
|
67
|
+
|
68
|
+
unknown = set(kwargs).difference(init_keys | fit_keys)
|
69
|
+
if unknown:
|
70
|
+
raise TypeError(f"Unknown keyword(s) for convolve: {sorted(unknown)}")
|
71
|
+
|
72
|
+
fitter = DistFit(bezierv_sum, **init_kwargs)
|
73
|
+
bezierv_result, _ = fitter.fit(**fit_kwargs)
|
74
|
+
return bezierv_result
|