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.
@@ -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