transcrypto 1.8.0__py3-none-any.whl → 2.0.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,360 @@
1
+ # SPDX-FileCopyrightText: Copyright 2026 Daniel Balparda <balparda@github.com>
2
+ # SPDX-License-Identifier: Apache-2.0
3
+ """Balparda's TransCrypto basic statistics library."""
4
+
5
+ from __future__ import annotations
6
+
7
+ import math
8
+ from collections import abc
9
+
10
+ from transcrypto.utils import base
11
+
12
+ # Lanczos coefficients for g=7, n=9; provides ~15 digit accuracy for gamma
13
+ _LANCZOS_G = 7
14
+ _LANCZOS_COEFF: tuple[float, ...] = (
15
+ 0.99999999999980993,
16
+ 676.5203681218851,
17
+ -1259.1392167224028,
18
+ 771.32342877765313,
19
+ -176.61502916214059,
20
+ 12.507343278686905,
21
+ -0.13857109526572012,
22
+ 9.9843695780195716e-6,
23
+ 1.5056327351493116e-7,
24
+ )
25
+ _TINY: float = 1e-30
26
+ _NSG: abc.Callable[[float], float] = (
27
+ lambda z: _TINY if abs(z) < _TINY else z # numerical stability guard
28
+ )
29
+ _BETA_INCOMPLETE_MAX_ITER: int = 200
30
+ _BETA_INCOMPLETE_TOL: float = 1e-14
31
+ _STUDENT_SMALL: float = 1e-12
32
+
33
+
34
+ def GammaLanczos(z: float, /) -> float:
35
+ """Compute the gamma function Γ(z) using the Lanczos approximation.
36
+
37
+ The Lanczos approximation provides an efficient method to compute
38
+ the gamma function with high accuracy (~15 digits). It uses the
39
+ reflection formula for z < 0.5.
40
+
41
+ Args:
42
+ z (float): Input value. For z ≤ 0 where z is a non-positive integer,
43
+ the function will return ±inf.
44
+
45
+ Returns:
46
+ float: Γ(z), the gamma function evaluated at z.
47
+
48
+ Notes:
49
+ - Uses coefficients optimized for g=7, n=9.
50
+ - For z < 0.5, uses the reflection formula:
51
+ Γ(z) = π / (sin(πz) · Γ(1-z))
52
+
53
+ """
54
+ if z < 0.5: # noqa: PLR2004
55
+ # Reflection formula: Γ(z) = π / (sin(πz) Γ(1-z))
56
+ return math.pi / (math.sin(math.pi * z) * GammaLanczos(1.0 - z))
57
+ z -= 1.0
58
+ x: float = _LANCZOS_COEFF[0]
59
+ for i in range(1, len(_LANCZOS_COEFF)):
60
+ x += _LANCZOS_COEFF[i] / (z + i)
61
+ t: float = z + _LANCZOS_G + 0.5
62
+ tz: float = t ** (z + 0.5)
63
+ return math.sqrt(2.0 * math.pi) * tz * math.exp(-t) * x
64
+
65
+
66
+ def BetaIncompleteCF(a: float, b: float, x: float, /) -> float:
67
+ """Compute continued fraction for the regularized incomplete beta function.
68
+
69
+ Uses the modified Lentz algorithm to evaluate the continued fraction
70
+ expansion of I_x(a, b) efficiently and stably.
71
+
72
+ Args:
73
+ a (float): First shape parameter (> 0).
74
+ b (float): Second shape parameter (> 0).
75
+ x (float): Point at which to evaluate (0 ≤ x ≤ 1).
76
+
77
+ Returns:
78
+ float: The continued fraction value.
79
+
80
+ Notes:
81
+ - Internal helper for `_BetaIncomplete`.
82
+ - Convergence is typically achieved in < 100 iterations for typical inputs.
83
+ - Uses a floor of 1e-30 to prevent division by zero.
84
+
85
+ """
86
+ qab: float = a + b
87
+ qap: float = a + 1.0
88
+ qam: float = a - 1.0
89
+ c: float = 1.0
90
+ d: float = 1.0 / _NSG(1.0 - qab * x / qap)
91
+ h: float = d
92
+ aa: float
93
+ delta: float
94
+ m2: int
95
+ for m in range(1, _BETA_INCOMPLETE_MAX_ITER + 1):
96
+ m2 = 2 * m
97
+ # even step
98
+ aa = m * (b - m) * x / ((qam + m2) * (a + m2))
99
+ c, d = _NSG(1.0 + aa / c), 1.0 / _NSG(1.0 + aa * d)
100
+ h *= d * c
101
+ # odd step
102
+ aa = -(a + m) * (qab + m) * x / ((a + m2) * (qap + m2))
103
+ c, d = _NSG(1.0 + aa / c), 1.0 / _NSG(1.0 + aa * d)
104
+ delta = d * c
105
+ h *= delta
106
+ if abs(delta - 1.0) < _BETA_INCOMPLETE_TOL:
107
+ break
108
+ return h
109
+
110
+
111
+ def BetaIncomplete(a: float, b: float, x: float, /) -> float:
112
+ """Compute the regularized incomplete beta function I_x(a, b).
113
+
114
+ The regularized incomplete beta function is defined as:
115
+ I_x(a, b) = B(x; a, b) / B(a, b)
116
+ where B(x; a, b) is the incomplete beta function and B(a, b) is the
117
+ complete beta function.
118
+
119
+ Args:
120
+ a (float): First shape parameter (> 0).
121
+ b (float): Second shape parameter (> 0).
122
+ x (float): Upper limit of integration (0 ≤ x ≤ 1).
123
+
124
+ Returns:
125
+ float: I_x(a, b), the regularized incomplete beta at x.
126
+
127
+ Raises:
128
+ base.InputError: If x is outside [0, 1].
129
+
130
+ Notes:
131
+ - Uses continued fraction expansion with Lentz algorithm.
132
+ - For numerical stability, uses the symmetry relation when
133
+ x > (a + 1) / (a + b + 2).
134
+
135
+ """
136
+ if x < 0.0 or x > 1.0:
137
+ raise base.InputError(f'x must be in [0, 1], got {x}')
138
+ if x == 0.0:
139
+ return 0.0
140
+ if x == 1.0:
141
+ return 1.0
142
+ log_beta: float = math.lgamma(a) + math.lgamma(b) - math.lgamma(a + b)
143
+ front: float = math.exp(math.log(x) * a + math.log(1.0 - x) * b - log_beta) / a
144
+ if x < (a + 1.0) / (a + b + 2.0):
145
+ return front * BetaIncompleteCF(a, b, x)
146
+ return 1.0 - front * BetaIncompleteCF(b, a, 1.0 - x) * a / b
147
+
148
+
149
+ def StudentTCDF(t_val: float, df: float, /) -> float:
150
+ """Compute the cumulative distribution function (CDF) of Student's t-distribution.
151
+
152
+ The CDF gives the probability P(T ≤ t) where T follows a t-distribution
153
+ with `df` degrees of freedom.
154
+
155
+ Args:
156
+ t_val (float): The t-statistic value.
157
+ df (float): Degrees of freedom (> 0).
158
+
159
+ Returns:
160
+ float: Probability P(T ≤ t_val), in range [0, 1].
161
+
162
+ Notes:
163
+ - Uses the relationship between the t-distribution CDF and the
164
+ regularized incomplete beta function.
165
+ - For t ≥ 0: CDF = 0.5 + 0.5 * (1 - I_x(df/2, 0.5))
166
+ - For t < 0: CDF = 0.5 * I_x(df/2, 0.5)
167
+ - where x = df / (df + t²)
168
+
169
+ """
170
+ x: float = df / (df + t_val * t_val)
171
+ prob: float = 0.5 * BetaIncomplete(df / 2.0, 0.5, x)
172
+ return 0.5 + (0.5 - prob) if t_val >= 0 else prob
173
+
174
+
175
+ def StudentTPPF(q: float, df: float, /) -> float:
176
+ """Compute the percent point function (inverse CDF) of Student's t-distribution.
177
+
178
+ Given a probability q, find the value t such that P(T ≤ t) = q,
179
+ where T follows a t-distribution with `df` degrees of freedom.
180
+
181
+ Args:
182
+ q (float): Probability (0 < q < 1).
183
+ df (float): Degrees of freedom (> 0).
184
+
185
+ Returns:
186
+ float: The t-value such that CDF(t) = q.
187
+
188
+ Raises:
189
+ base.InputError: If q is not in (0, 1).
190
+
191
+ Notes:
192
+ - Uses Newton-Raphson iteration with an initial guess from
193
+ the normal distribution approximation.
194
+ - Converges to ~12 decimal places in typical cases.
195
+
196
+ """
197
+ if not 0.0 < q < 1.0:
198
+ raise base.InputError(f'q must be in (0, 1), got {q}')
199
+ # Special case: q=0.5 is exactly 0 by symmetry
200
+ if q == 0.5: # noqa: PLR2004
201
+ return 0.0
202
+ # Initial guess using inverse normal approximation (Abramowitz & Stegun 26.2.23)
203
+ if q < 0.5: # noqa: PLR2004
204
+ sign: float = -1.0
205
+ p: float = q
206
+ else:
207
+ sign = 1.0
208
+ p = 1.0 - q
209
+ # Protect against log(0) when p is very close to 0
210
+ p = max(p, 1e-300)
211
+ t_approx: float = math.sqrt(-2.0 * math.log(p))
212
+ c0 = 2.515517
213
+ c1 = 0.802853
214
+ c2 = 0.010328
215
+ d1 = 1.432788
216
+ d2 = 0.189269
217
+ d3 = 0.001308
218
+ x0: float = sign * (
219
+ t_approx
220
+ - (c0 + c1 * t_approx + c2 * t_approx**2)
221
+ / (1 + d1 * t_approx + d2 * t_approx**2 + d3 * t_approx**3)
222
+ )
223
+ # Newton-Raphson refinement
224
+ for _ in range(50):
225
+ cdf_val: float = StudentTCDF(x0, df)
226
+ # PDF of Student's t-distribution (computed in log-space to avoid overflow for large df)
227
+ log_pdf: float = (
228
+ math.lgamma((df + 1) / 2)
229
+ - 0.5 * math.log(df * math.pi)
230
+ - math.lgamma(df / 2)
231
+ - ((df + 1) / 2) * math.log(1 + x0**2 / df)
232
+ )
233
+ pdf_val: float = _NSG(math.exp(log_pdf))
234
+ x1: float = x0 - (cdf_val - q) / pdf_val
235
+ if abs(x1 - x0) < _STUDENT_SMALL:
236
+ return x1
237
+ x0 = x1
238
+ return x0 # pragma: no cover - Newton-Raphson always converges for t-distribution
239
+
240
+
241
+ def SampleVariance(data: list[int | float], mean: float, /) -> float:
242
+ """Compute sample variance with Bessel's correction (n-1 denominator).
243
+
244
+ Args:
245
+ data (list[int | float]): Sequence of numeric measurements, with len(data) >= 2.
246
+ mean (float): Pre-computed mean of the data.
247
+
248
+ Returns:
249
+ float: Sample variance s² = Σ(xᵢ - x̄)² / (n - 1).
250
+
251
+ Raises:
252
+ base.InputError: If len(data) < 2.
253
+
254
+ """
255
+ if (data_sz := len(data)) < 2: # noqa: PLR2004
256
+ raise base.InputError(f'sample variance requires at least 2 data points, got {data_sz}')
257
+ return sum((x - mean) ** 2 for x in data) / float(data_sz - 1)
258
+
259
+
260
+ def StandardErrorOfMean(data: list[int | float], /) -> tuple[float, float]:
261
+ """Compute the mean and standard error of the mean (SEM).
262
+
263
+ The SEM is the standard deviation of the sampling distribution of the
264
+ sample mean, computed as s / √n where s is the sample standard deviation.
265
+
266
+ Args:
267
+ data (list[int | float]): Sequence of numeric measurements (n >= 2).
268
+
269
+ Returns:
270
+ tuple[float, float]: (mean, SEM) where:
271
+ - mean: arithmetic mean of the data
272
+ - SEM: standard error of the mean (σ / √n)
273
+
274
+ Notes:
275
+ - Assumes len(data) >= 2; returns (mean, inf) for single element handled by caller.
276
+ - Uses sample standard deviation (Bessel's correction).
277
+
278
+ """ # noqa: RUF002
279
+ n: int = len(data)
280
+ mean: float = sum(data) / n
281
+ variance: float = SampleVariance(data, mean)
282
+ return (mean, math.sqrt(variance / n))
283
+
284
+
285
+ def StudentTInterval(
286
+ confidence: float, df: int, loc: float, scale: float, /
287
+ ) -> tuple[float, float]:
288
+ """Compute a symmetric confidence interval using Student's t-distribution.
289
+
290
+ Args:
291
+ confidence (float): Confidence level (e.g., 0.95 for 95% CI).
292
+ df (int): Degrees of freedom (n - 1 for a sample of size n).
293
+ loc (float): Center of the interval (typically the sample mean).
294
+ scale (float): Scale parameter (typically the SEM).
295
+
296
+ Returns:
297
+ tuple[float, float]: (lower_bound, upper_bound) of the confidence interval.
298
+
299
+ Notes:
300
+ - The interval is symmetric around `loc`:
301
+ [loc - t_crit * scale, loc + t_crit * scale]
302
+ - where t_crit is the critical t-value for the given confidence and df.
303
+
304
+ """
305
+ alpha: float = 1.0 - confidence
306
+ t_crit: float = StudentTPPF(1.0 - alpha / 2.0, df)
307
+ margin: float = t_crit * scale
308
+ return (loc - margin, loc + margin)
309
+
310
+
311
+ def MeasurementStats(
312
+ data: list[int | float], /, *, confidence: float = 0.95
313
+ ) -> tuple[int, float, float, float, tuple[float, float], float]:
314
+ """Compute descriptive statistics for repeated measurements.
315
+
316
+ Given N ≥ 1 measurements, this function computes the sample mean, the
317
+ standard error of the mean (SEM), and the symmetric error estimate for
318
+ the chosen confidence interval using Student's t distribution.
319
+
320
+ Notes:
321
+ • If only one measurement is given, SEM and error are reported as +∞ and
322
+ the confidence interval is (-∞, +∞).
323
+ • This function assumes the underlying distribution is approximately
324
+ normal, or n is large enough for the Central Limit Theorem to apply.
325
+
326
+ Args:
327
+ data (list[int | float]): Sequence of numeric measurements.
328
+ confidence (float, optional): Confidence level for the interval, 0.5 <= confidence < 1;
329
+ defaults to 0.95 (95% confidence interval).
330
+
331
+ Returns:
332
+ tuple:
333
+ - n (int): number of measurements.
334
+ - mean (float): arithmetic mean of the data
335
+ - sem (float): standard error of the mean, sigma / √n
336
+ - error (float): half-width of the confidence interval (mean ± error)
337
+ - ci (tuple[float, float]): lower and upper confidence interval bounds
338
+ - confidence (float): the confidence level used
339
+
340
+ Raises:
341
+ base.InputError: if the input list is empty.
342
+
343
+ """
344
+ # test inputs
345
+ n: int = len(data)
346
+ if not n:
347
+ raise base.InputError('no data')
348
+ if not 0.5 <= confidence < 1.0: # noqa: PLR2004
349
+ raise base.InputError(f'invalid confidence: {confidence=}')
350
+ # solve trivial case
351
+ if n == 1:
352
+ return (n, float(data[0]), math.inf, math.inf, (-math.inf, math.inf), confidence)
353
+ # compute statistics using local implementation (no scipy/numpy dependency)
354
+ mean: float
355
+ sem: float
356
+ mean, sem = StandardErrorOfMean(data)
357
+ ci: tuple[float, float] = StudentTInterval(confidence, n - 1, mean, sem)
358
+ t_crit: float = StudentTPPF((1.0 + confidence) / 2.0, n - 1)
359
+ error: float = t_crit * sem # half-width of the CI
360
+ return (n, mean, sem, error, ci, confidence)
@@ -0,0 +1,175 @@
1
+ # SPDX-FileCopyrightText: Copyright 2026 Daniel Balparda <balparda@github.com>
2
+ # SPDX-License-Identifier: Apache-2.0
3
+ """Balparda's TransCrypto timer library."""
4
+
5
+ from __future__ import annotations
6
+
7
+ import datetime
8
+ import functools
9
+ import logging
10
+ import time
11
+ from collections import abc
12
+ from types import TracebackType
13
+ from typing import Self
14
+
15
+ from transcrypto.utils import base, human
16
+
17
+ # Time utils
18
+
19
+ MIN_TM = int(datetime.datetime(2000, 1, 1, 0, 0, 0, tzinfo=datetime.UTC).timestamp())
20
+ TIME_FORMAT = '%Y/%b/%d-%H:%M:%S-UTC'
21
+ TimeStr: abc.Callable[[int | float | None], str] = lambda tm: (
22
+ time.strftime(TIME_FORMAT, time.gmtime(tm)) if tm else '-'
23
+ )
24
+ Now: abc.Callable[[], int] = lambda: int(time.time())
25
+ StrNow: abc.Callable[[], str] = lambda: TimeStr(Now())
26
+
27
+
28
+ class Timer:
29
+ """An execution timing class that can be used as both a context manager and a decorator.
30
+
31
+ Examples:
32
+ # As a context manager
33
+ with Timer('Block timing'):
34
+ time.sleep(1.2)
35
+
36
+ # As a decorator
37
+ @Timer('Function timing')
38
+ def slow_function():
39
+ time.sleep(0.8)
40
+
41
+ # As a regular object
42
+ tm = Timer('Inline timing')
43
+ tm.Start()
44
+ time.sleep(0.1)
45
+ tm.Stop()
46
+ print(tm)
47
+
48
+ Attributes:
49
+ label (str, optional): Timer label
50
+ emit_log (bool, optional): If True (default) will logging.info() the timer, else will not
51
+ emit_print (bool, optional): If True will print() the timer, else (default) will not
52
+
53
+ """
54
+
55
+ def __init__(
56
+ self,
57
+ label: str = '',
58
+ /,
59
+ *,
60
+ emit_log: bool = True,
61
+ emit_print: abc.Callable[[str], None] | None = None,
62
+ ) -> None:
63
+ """Initialize the Timer.
64
+
65
+ Args:
66
+ label (str, optional): A description or name for the timed block or function
67
+ emit_log (bool, optional): Emit a log message when finished; default is True
68
+ emit_print (Callable[[str], None] | None, optional): Emit a print() message when
69
+ finished using the provided callable; default is None
70
+
71
+ """
72
+ self.emit_log: bool = emit_log
73
+ self.emit_print: abc.Callable[[str], None] | None = emit_print
74
+ self.label: str = label.strip()
75
+ self.start: float | None = None
76
+ self.end: float | None = None
77
+
78
+ @property
79
+ def elapsed(self) -> float:
80
+ """Elapsed time. Will be zero until a measurement is available with start/end.
81
+
82
+ Raises:
83
+ base.Error: negative elapsed time
84
+
85
+ Returns:
86
+ float: elapsed time, in seconds
87
+
88
+ """
89
+ if self.start is None or self.end is None:
90
+ return 0.0
91
+ delta: float = self.end - self.start
92
+ if delta <= 0.0:
93
+ raise base.Error(f'negative/zero delta: {delta}')
94
+ return delta
95
+
96
+ def __str__(self) -> str:
97
+ """Get current timer value.
98
+
99
+ Returns:
100
+ str: human-readable representation of current time value
101
+
102
+ """
103
+ if self.start is None:
104
+ return f'{self.label}: <UNSTARTED>' if self.label else '<UNSTARTED>'
105
+ if self.end is None:
106
+ return (
107
+ f'{self.label}: ' if self.label else ''
108
+ ) + f'<PARTIAL> {human.HumanizedSeconds(time.perf_counter() - self.start)}'
109
+ return (f'{self.label}: ' if self.label else '') + f'{human.HumanizedSeconds(self.elapsed)}'
110
+
111
+ def Start(self) -> None:
112
+ """Start the timer.
113
+
114
+ Raises:
115
+ base.Error: if you try to re-start the timer
116
+
117
+ """
118
+ if self.start is not None:
119
+ raise base.Error('Re-starting timer is forbidden')
120
+ self.start = time.perf_counter()
121
+
122
+ def __enter__(self) -> Self:
123
+ """Start the timer when entering the context.
124
+
125
+ Returns:
126
+ Timer: context object (self)
127
+
128
+ """
129
+ self.Start()
130
+ return self
131
+
132
+ def Stop(self) -> None:
133
+ """Stop the timer and emit logging.info with timer message.
134
+
135
+ Raises:
136
+ base.Error: trying to re-start timer or stop unstarted timer
137
+
138
+ """
139
+ if self.start is None:
140
+ raise base.Error('Stopping an unstarted timer')
141
+ if self.end is not None:
142
+ raise base.Error('Re-stopping timer is forbidden')
143
+ self.end = time.perf_counter()
144
+ message: str = str(self)
145
+ if self.emit_log:
146
+ logging.info(message)
147
+ if self.emit_print is not None:
148
+ self.emit_print(message)
149
+
150
+ def __exit__(
151
+ self,
152
+ unused_exc_type: type[BaseException] | None,
153
+ unused_exc_val: BaseException | None,
154
+ exc_tb: TracebackType | None,
155
+ ) -> None:
156
+ """Stop the timer when exiting the context."""
157
+ self.Stop()
158
+
159
+ def __call__[**F, R](self, func: abc.Callable[F, R]) -> abc.Callable[F, R]:
160
+ """Allow the Timer to be used as a decorator.
161
+
162
+ Args:
163
+ func: The function to time.
164
+
165
+ Returns:
166
+ The wrapped function with timing behavior.
167
+
168
+ """
169
+
170
+ @functools.wraps(func)
171
+ def _Wrapper(*args: F.args, **kwargs: F.kwargs) -> R:
172
+ with self.__class__(self.label, emit_log=self.emit_log, emit_print=self.emit_print):
173
+ return func(*args, **kwargs)
174
+
175
+ return _Wrapper