bs-python-utils 1.1.1__tar.gz → 1.1.3__tar.gz
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-1.1.1 → bs_python_utils-1.1.3}/PKG-INFO +5 -1
- {bs_python_utils-1.1.1 → bs_python_utils-1.1.3}/README.md +4 -0
- {bs_python_utils-1.1.1 → bs_python_utils-1.1.3}/bs_python_utils/__init__.py +2 -1
- bs_python_utils-1.1.3/bs_python_utils/bs_derivs.py +1 -0
- bs_python_utils-1.1.3/bs_python_utils/core/Timer.py +176 -0
- bs_python_utils-1.1.3/bs_python_utils/numerical/bs_derivs.py +49 -0
- {bs_python_utils-1.1.1 → bs_python_utils-1.1.3}/bs_python_utils.egg-info/PKG-INFO +5 -1
- {bs_python_utils-1.1.1 → bs_python_utils-1.1.3}/bs_python_utils.egg-info/SOURCES.txt +5 -1
- {bs_python_utils-1.1.1 → bs_python_utils-1.1.3}/pyproject.toml +1 -1
- bs_python_utils-1.1.3/tests/test_bs_derivs.py +365 -0
- bs_python_utils-1.1.3/tests/test_timer.py +225 -0
- bs_python_utils-1.1.1/bs_python_utils/core/Timer.py +0 -81
- {bs_python_utils-1.1.1 → bs_python_utils-1.1.3}/LICENSE +0 -0
- {bs_python_utils-1.1.1 → bs_python_utils-1.1.3}/bs_python_utils/GaussHermiteSparseGrids/GHsparseGrid1prec13.txt +0 -0
- {bs_python_utils-1.1.1 → bs_python_utils-1.1.3}/bs_python_utils/GaussHermiteSparseGrids/GHsparseGrid1prec17.txt +0 -0
- {bs_python_utils-1.1.1 → bs_python_utils-1.1.3}/bs_python_utils/GaussHermiteSparseGrids/GHsparseGrid1prec9.txt +0 -0
- {bs_python_utils-1.1.1 → bs_python_utils-1.1.3}/bs_python_utils/GaussHermiteSparseGrids/GHsparseGrid2prec13.txt +0 -0
- {bs_python_utils-1.1.1 → bs_python_utils-1.1.3}/bs_python_utils/GaussHermiteSparseGrids/GHsparseGrid2prec17.txt +0 -0
- {bs_python_utils-1.1.1 → bs_python_utils-1.1.3}/bs_python_utils/GaussHermiteSparseGrids/GHsparseGrid2prec9.txt +0 -0
- {bs_python_utils-1.1.1 → bs_python_utils-1.1.3}/bs_python_utils/GaussHermiteSparseGrids/GHsparseGrid3prec13.txt +0 -0
- {bs_python_utils-1.1.1 → bs_python_utils-1.1.3}/bs_python_utils/GaussHermiteSparseGrids/GHsparseGrid3prec17.txt +0 -0
- {bs_python_utils-1.1.1 → bs_python_utils-1.1.3}/bs_python_utils/GaussHermiteSparseGrids/GHsparseGrid3prec9.txt +0 -0
- {bs_python_utils-1.1.1 → bs_python_utils-1.1.3}/bs_python_utils/GaussHermiteSparseGrids/GHsparseGrid4prec13.txt +0 -0
- {bs_python_utils-1.1.1 → bs_python_utils-1.1.3}/bs_python_utils/GaussHermiteSparseGrids/GHsparseGrid4prec17.txt +0 -0
- {bs_python_utils-1.1.1 → bs_python_utils-1.1.3}/bs_python_utils/GaussHermiteSparseGrids/GHsparseGrid4prec9.txt +0 -0
- {bs_python_utils-1.1.1 → bs_python_utils-1.1.3}/bs_python_utils/GaussHermiteSparseGrids/GHsparseGrid5prec13.txt +0 -0
- {bs_python_utils-1.1.1 → bs_python_utils-1.1.3}/bs_python_utils/GaussHermiteSparseGrids/GHsparseGrid5prec17.txt +0 -0
- {bs_python_utils-1.1.1 → bs_python_utils-1.1.3}/bs_python_utils/GaussHermiteSparseGrids/GHsparseGrid5prec9.txt +0 -0
- {bs_python_utils-1.1.1 → bs_python_utils-1.1.3}/bs_python_utils/Timer.py +0 -0
- {bs_python_utils-1.1.1 → bs_python_utils-1.1.3}/bs_python_utils/bivariate_quantiles.py +0 -0
- {bs_python_utils-1.1.1 → bs_python_utils-1.1.3}/bs_python_utils/bs_altair.py +0 -0
- {bs_python_utils-1.1.1 → bs_python_utils-1.1.3}/bs_python_utils/bs_logging.py +0 -0
- {bs_python_utils-1.1.1 → bs_python_utils-1.1.3}/bs_python_utils/bs_mathstr.py +0 -0
- {bs_python_utils-1.1.1 → bs_python_utils-1.1.3}/bs_python_utils/bs_mem.py +0 -0
- {bs_python_utils-1.1.1 → bs_python_utils-1.1.3}/bs_python_utils/bs_opt.py +0 -0
- {bs_python_utils-1.1.1 → bs_python_utils-1.1.3}/bs_python_utils/bs_plots.py +0 -0
- {bs_python_utils-1.1.1 → bs_python_utils-1.1.3}/bs_python_utils/bs_seaborn.py +0 -0
- {bs_python_utils-1.1.1 → bs_python_utils-1.1.3}/bs_python_utils/bs_sparse_gaussian.py +0 -0
- {bs_python_utils-1.1.1 → bs_python_utils-1.1.3}/bs_python_utils/bsmplutils.py +0 -0
- {bs_python_utils-1.1.1 → bs_python_utils-1.1.3}/bs_python_utils/bsnputils.py +0 -0
- {bs_python_utils-1.1.1 → bs_python_utils-1.1.3}/bs_python_utils/bssputils.py +0 -0
- {bs_python_utils-1.1.1 → bs_python_utils-1.1.3}/bs_python_utils/bsstats.py +0 -0
- {bs_python_utils-1.1.1 → bs_python_utils-1.1.3}/bs_python_utils/bsutils.py +0 -0
- {bs_python_utils-1.1.1 → bs_python_utils-1.1.3}/bs_python_utils/chebyshev.py +0 -0
- {bs_python_utils-1.1.1 → bs_python_utils-1.1.3}/bs_python_utils/core/__init__.py +0 -0
- {bs_python_utils-1.1.1 → bs_python_utils-1.1.3}/bs_python_utils/core/bs_logging.py +0 -0
- {bs_python_utils-1.1.1 → bs_python_utils-1.1.3}/bs_python_utils/core/bs_mathstr.py +0 -0
- {bs_python_utils-1.1.1 → bs_python_utils-1.1.3}/bs_python_utils/core/bs_mem.py +0 -0
- {bs_python_utils-1.1.1 → bs_python_utils-1.1.3}/bs_python_utils/core/bsutils.py +0 -0
- {bs_python_utils-1.1.1 → bs_python_utils-1.1.3}/bs_python_utils/data_anal/__init__.py +0 -0
- {bs_python_utils-1.1.1 → bs_python_utils-1.1.3}/bs_python_utils/data_anal/pandas_utils.py +0 -0
- {bs_python_utils-1.1.1 → bs_python_utils-1.1.3}/bs_python_utils/data_anal/sklearn_utils.py +0 -0
- {bs_python_utils-1.1.1 → bs_python_utils-1.1.3}/bs_python_utils/distance_covariances.py +0 -0
- {bs_python_utils-1.1.1 → bs_python_utils-1.1.3}/bs_python_utils/examples/examples_altair.py +0 -0
- {bs_python_utils-1.1.1 → bs_python_utils-1.1.3}/bs_python_utils/examples/examples_distance_covariances.py +0 -0
- {bs_python_utils-1.1.1 → bs_python_utils-1.1.3}/bs_python_utils/examples/examples_mem.py +0 -0
- {bs_python_utils-1.1.1 → bs_python_utils-1.1.3}/bs_python_utils/examples/examples_mpl.py +0 -0
- {bs_python_utils-1.1.1 → bs_python_utils-1.1.3}/bs_python_utils/examples/examples_opt.py +0 -0
- {bs_python_utils-1.1.1 → bs_python_utils-1.1.3}/bs_python_utils/examples/examples_seaborn.py +0 -0
- {bs_python_utils-1.1.1 → bs_python_utils-1.1.3}/bs_python_utils/examples/examples_sklearn.py +0 -0
- {bs_python_utils-1.1.1 → bs_python_utils-1.1.3}/bs_python_utils/examples/examples_sparse_gaussian.py +0 -0
- {bs_python_utils-1.1.1 → bs_python_utils-1.1.3}/bs_python_utils/numerical/__init__.py +0 -0
- {bs_python_utils-1.1.1 → bs_python_utils-1.1.3}/bs_python_utils/numerical/bs_sparse_gaussian.py +0 -0
- {bs_python_utils-1.1.1 → bs_python_utils-1.1.3}/bs_python_utils/numerical/bsnputils.py +0 -0
- {bs_python_utils-1.1.1 → bs_python_utils-1.1.3}/bs_python_utils/numerical/bssputils.py +0 -0
- {bs_python_utils-1.1.1 → bs_python_utils-1.1.3}/bs_python_utils/numerical/chebyshev.py +0 -0
- {bs_python_utils-1.1.1 → bs_python_utils-1.1.3}/bs_python_utils/opt/__init__.py +0 -0
- {bs_python_utils-1.1.1 → bs_python_utils-1.1.3}/bs_python_utils/opt/bs_opt.py +0 -0
- {bs_python_utils-1.1.1 → bs_python_utils-1.1.3}/bs_python_utils/pandas_utils.py +0 -0
- {bs_python_utils-1.1.1 → bs_python_utils-1.1.3}/bs_python_utils/sklearn_utils.py +0 -0
- {bs_python_utils-1.1.1 → bs_python_utils-1.1.3}/bs_python_utils/stats/__init__.py +0 -0
- {bs_python_utils-1.1.1 → bs_python_utils-1.1.3}/bs_python_utils/stats/bivariate_quantiles.py +0 -0
- {bs_python_utils-1.1.1 → bs_python_utils-1.1.3}/bs_python_utils/stats/bsstats.py +0 -0
- {bs_python_utils-1.1.1 → bs_python_utils-1.1.3}/bs_python_utils/stats/distance_covariances.py +0 -0
- {bs_python_utils-1.1.1 → bs_python_utils-1.1.3}/bs_python_utils/streamlit_utils.py +0 -0
- {bs_python_utils-1.1.1 → bs_python_utils-1.1.3}/bs_python_utils/viz/__init__.py +0 -0
- {bs_python_utils-1.1.1 → bs_python_utils-1.1.3}/bs_python_utils/viz/bs_altair.py +0 -0
- {bs_python_utils-1.1.1 → bs_python_utils-1.1.3}/bs_python_utils/viz/bs_plots.py +0 -0
- {bs_python_utils-1.1.1 → bs_python_utils-1.1.3}/bs_python_utils/viz/bs_seaborn.py +0 -0
- {bs_python_utils-1.1.1 → bs_python_utils-1.1.3}/bs_python_utils/viz/bsmplutils.py +0 -0
- {bs_python_utils-1.1.1 → bs_python_utils-1.1.3}/bs_python_utils/viz/streamlit_utils.py +0 -0
- {bs_python_utils-1.1.1 → bs_python_utils-1.1.3}/bs_python_utils.egg-info/dependency_links.txt +0 -0
- {bs_python_utils-1.1.1 → bs_python_utils-1.1.3}/bs_python_utils.egg-info/requires.txt +0 -0
- {bs_python_utils-1.1.1 → bs_python_utils-1.1.3}/bs_python_utils.egg-info/top_level.txt +0 -0
- {bs_python_utils-1.1.1 → bs_python_utils-1.1.3}/setup.cfg +0 -0
- {bs_python_utils-1.1.1 → bs_python_utils-1.1.3}/tests/test_bivariate_quantiles.py +0 -0
- {bs_python_utils-1.1.1 → bs_python_utils-1.1.3}/tests/test_bs_opt.py +0 -0
- {bs_python_utils-1.1.1 → bs_python_utils-1.1.3}/tests/test_bs_sparse_gaussian.py +0 -0
- {bs_python_utils-1.1.1 → bs_python_utils-1.1.3}/tests/test_bsnputils.py +0 -0
- {bs_python_utils-1.1.1 → bs_python_utils-1.1.3}/tests/test_bssputils.py +0 -0
- {bs_python_utils-1.1.1 → bs_python_utils-1.1.3}/tests/test_bsstats.py +0 -0
- {bs_python_utils-1.1.1 → bs_python_utils-1.1.3}/tests/test_bsutils.py +0 -0
- {bs_python_utils-1.1.1 → bs_python_utils-1.1.3}/tests/test_chebyshev.py +0 -0
- {bs_python_utils-1.1.1 → bs_python_utils-1.1.3}/tests/test_nptri.py +0 -0
- {bs_python_utils-1.1.1 → bs_python_utils-1.1.3}/tests/test_random_draws.py +0 -0
- {bs_python_utils-1.1.1 → bs_python_utils-1.1.3}/tests/test_sklearn_utils.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: bs-python-utils
|
|
3
|
-
Version: 1.1.
|
|
3
|
+
Version: 1.1.3
|
|
4
4
|
Summary: Utilities programs for my Python code
|
|
5
5
|
Requires-Python: >=3.13
|
|
6
6
|
Description-Content-Type: text/markdown
|
|
@@ -50,6 +50,10 @@ Dynamic: license-file
|
|
|
50
50
|
|
|
51
51
|
### Release notes
|
|
52
52
|
|
|
53
|
+
#### 1.1.2 and 1.1.3(September 11, 2026)
|
|
54
|
+
- Improved the `Timer` class.
|
|
55
|
+
- added `bs_derivs` with Richardson derivatives.
|
|
56
|
+
|
|
53
57
|
#### 1.1.1 (September 04, 2026)
|
|
54
58
|
Added simulation-based bivariate ranks.
|
|
55
59
|
|
|
@@ -6,7 +6,8 @@ programming.
|
|
|
6
6
|
* `bs_logging` has customized logging
|
|
7
7
|
* `bs_mathstr` has Unicode for math strings
|
|
8
8
|
* `bs_mem` reports memory usage
|
|
9
|
-
* `Timer` has a `Timer`
|
|
9
|
+
* `Timer` has a `Timer` context manager to time code execution.
|
|
10
|
+
* `bs_derivs` has Richardson derivatives
|
|
10
11
|
* `bsnputils` has Numpy functions
|
|
11
12
|
* `bssputils` has Scipy functions
|
|
12
13
|
* `sklearn_utils` has Sklearn functions
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
from bs_python_utils.numerical.bs_derivs import * # noqa: F401, F403
|
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Utilities to time code:
|
|
3
|
+
|
|
4
|
+
* a `Timer` class that can be used as a context manager
|
|
5
|
+
* a `timeit` decorator for functions.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import time
|
|
9
|
+
from collections.abc import Callable, Iterable
|
|
10
|
+
from dataclasses import dataclass, field
|
|
11
|
+
from functools import wraps
|
|
12
|
+
from typing import Any, ClassVar, Self
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def timeit(func: Callable) -> Callable:
|
|
16
|
+
"""Decorator to measure and print execution time of a function.
|
|
17
|
+
|
|
18
|
+
Wraps a function to measure its execution time using perf_counter()
|
|
19
|
+
for high precision, and prints the elapsed time to stdout.
|
|
20
|
+
|
|
21
|
+
Args:
|
|
22
|
+
func: The function to be timed.
|
|
23
|
+
|
|
24
|
+
Returns:
|
|
25
|
+
A wrapped function that prints the execution time when called.
|
|
26
|
+
|
|
27
|
+
Example:
|
|
28
|
+
>>> @timeit
|
|
29
|
+
... def slow_function():
|
|
30
|
+
... time.sleep(0.1) # doctest: +SKIP
|
|
31
|
+
>>> slow_function() # doctest: +SKIP
|
|
32
|
+
slow_function executed in 0.100 seconds
|
|
33
|
+
"""
|
|
34
|
+
|
|
35
|
+
@wraps(func)
|
|
36
|
+
def wrapper(*args: Iterable, **kwargs: dict) -> Any:
|
|
37
|
+
start = time.perf_counter()
|
|
38
|
+
result = func(*args, **kwargs)
|
|
39
|
+
end = time.perf_counter()
|
|
40
|
+
print(f"{func.__name__} executed in {end - start:.3f} seconds")
|
|
41
|
+
return result
|
|
42
|
+
|
|
43
|
+
return wrapper
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
class TimerError(Exception):
|
|
47
|
+
"""A custom exception used to report errors in use of Timer class"""
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
@dataclass
|
|
51
|
+
class Timer:
|
|
52
|
+
"""A flexible timer class for measuring execution time.
|
|
53
|
+
|
|
54
|
+
This class can be used in three ways:
|
|
55
|
+
|
|
56
|
+
1. **As a context manager**: automatically starts and stops timing
|
|
57
|
+
2. **With manual calls**: call start() and stop() explicitly
|
|
58
|
+
3. **As a named timer**: accumulate timing across multiple calls using the `name` attribute
|
|
59
|
+
|
|
60
|
+
Attributes:
|
|
61
|
+
name: Optional name for the timer. If provided, cumulative elapsed time is stored
|
|
62
|
+
in the class-level `timers` dict. Use None for one-off timings.
|
|
63
|
+
text: Format string for displaying elapsed time. Defaults to "Elapsed time: {:0.4f} seconds".
|
|
64
|
+
The string is formatted with a single float representing elapsed seconds.
|
|
65
|
+
logger: Callable to handle output. Defaults to print(). Set to None to suppress output.
|
|
66
|
+
timers: Class-level dictionary storing cumulative times for all named timers.
|
|
67
|
+
|
|
68
|
+
Raises:
|
|
69
|
+
TimerError: If stop() is called without start(), or start() is called on a running timer.
|
|
70
|
+
|
|
71
|
+
Example:
|
|
72
|
+
>>> # As a context manager (recommended for simple use cases)
|
|
73
|
+
>>> with Timer():
|
|
74
|
+
... time.sleep(0.1) # doctest: +SKIP
|
|
75
|
+
Elapsed time: 0.1000 seconds
|
|
76
|
+
|
|
77
|
+
>>> # With a custom message
|
|
78
|
+
>>> with Timer(text="My code took {:0.2f}s"):
|
|
79
|
+
... time.sleep(0.05) # doctest: +SKIP
|
|
80
|
+
My code took 0.05s
|
|
81
|
+
|
|
82
|
+
>>> # Suppress output
|
|
83
|
+
>>> with Timer(logger=None):
|
|
84
|
+
... time.sleep(0.1) # doctest: +SKIP
|
|
85
|
+
|
|
86
|
+
>>> # Suppress output and capture elapsed time
|
|
87
|
+
>>> with Timer(logger=None) as t:
|
|
88
|
+
... time.sleep(0.1) # doctest: +SKIP
|
|
89
|
+
>>> elapsed = t.stop()
|
|
90
|
+
|
|
91
|
+
>>> # Manual start/stop
|
|
92
|
+
>>> timer = Timer()
|
|
93
|
+
>>> timer.start() # doctest: +SKIP
|
|
94
|
+
>>> time.sleep(0.1) # doctest: +SKIP
|
|
95
|
+
>>> elapsed = timer.stop() # doctest: +SKIP
|
|
96
|
+
Elapsed time: 0.1000 seconds
|
|
97
|
+
|
|
98
|
+
>>> # Named timer for cumulative timing
|
|
99
|
+
>>> Timer(name="db_queries").start() # doctest: +SKIP
|
|
100
|
+
>>> time.sleep(0.05) # doctest: +SKIP
|
|
101
|
+
>>> Timer(name="db_queries").stop() # doctest: +SKIP
|
|
102
|
+
Elapsed time: 0.0500 seconds
|
|
103
|
+
>>> Timer.timers # doctest: +SKIP
|
|
104
|
+
{'db_queries': 0.05}
|
|
105
|
+
"""
|
|
106
|
+
|
|
107
|
+
timers: ClassVar = {}
|
|
108
|
+
name: Any = None
|
|
109
|
+
text: Any = "Elapsed time: {:0.4f} seconds"
|
|
110
|
+
logger: Any = print
|
|
111
|
+
_start_time: Any = field(default=None, init=False, repr=False)
|
|
112
|
+
|
|
113
|
+
def __post_init__(self):
|
|
114
|
+
"""Initialize the timer and register it if named.
|
|
115
|
+
|
|
116
|
+
For named timers, creates an entry in the class-level `timers` dict
|
|
117
|
+
to accumulate elapsed time across multiple start/stop cycles.
|
|
118
|
+
"""
|
|
119
|
+
if self.name is not None:
|
|
120
|
+
self.timers.setdefault(self.name, 0)
|
|
121
|
+
|
|
122
|
+
def start(self):
|
|
123
|
+
"""Start the timer.
|
|
124
|
+
|
|
125
|
+
Records the current time using perf_counter() for high precision.
|
|
126
|
+
|
|
127
|
+
Raises:
|
|
128
|
+
TimerError: If the timer is already running.
|
|
129
|
+
"""
|
|
130
|
+
if self._start_time is not None:
|
|
131
|
+
raise TimerError("Timer is running. Use .stop() to stop it")
|
|
132
|
+
|
|
133
|
+
self._start_time = time.perf_counter()
|
|
134
|
+
|
|
135
|
+
def stop(self) -> float:
|
|
136
|
+
"""Stop the timer and report elapsed time.
|
|
137
|
+
|
|
138
|
+
Calculates elapsed time since start() was called, sends output to the logger
|
|
139
|
+
if configured, and accumulates the time if this is a named timer.
|
|
140
|
+
|
|
141
|
+
Returns:
|
|
142
|
+
Elapsed time in seconds as a float.
|
|
143
|
+
|
|
144
|
+
Raises:
|
|
145
|
+
TimerError: If the timer is not running.
|
|
146
|
+
"""
|
|
147
|
+
if self._start_time is None:
|
|
148
|
+
raise TimerError("Timer is not running. Use .start() to start it")
|
|
149
|
+
|
|
150
|
+
# Calculate elapsed time
|
|
151
|
+
elapsed_time: float = time.perf_counter() - self._start_time
|
|
152
|
+
self._start_time = None
|
|
153
|
+
|
|
154
|
+
# Report elapsed time
|
|
155
|
+
if self.logger:
|
|
156
|
+
self.logger(self.text.format(elapsed_time))
|
|
157
|
+
if self.name:
|
|
158
|
+
self.timers[self.name] += elapsed_time
|
|
159
|
+
|
|
160
|
+
return elapsed_time
|
|
161
|
+
|
|
162
|
+
def __enter__(self) -> Self:
|
|
163
|
+
"""Enter context manager: start the timer.
|
|
164
|
+
|
|
165
|
+
Returns:
|
|
166
|
+
self: The Timer instance.
|
|
167
|
+
"""
|
|
168
|
+
self.start()
|
|
169
|
+
return self
|
|
170
|
+
|
|
171
|
+
def __exit__(self, *exc_info):
|
|
172
|
+
"""Exit context manager: stop the timer.
|
|
173
|
+
|
|
174
|
+
Stops the timer regardless of whether an exception occurred in the with block.
|
|
175
|
+
"""
|
|
176
|
+
self.stop()
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
from collections.abc import Callable
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
def richardson_derivative(
|
|
5
|
+
f: Callable[[float, list], float], x: float, args: list, h: float
|
|
6
|
+
) -> float:
|
|
7
|
+
"""Compute the first derivative of $f(x, args)$ at $x$ via Richardson extrapolation.
|
|
8
|
+
|
|
9
|
+
Uses a 5-point central-difference stencil (step `h` and `2h`) that is
|
|
10
|
+
fourth-order accurate.
|
|
11
|
+
|
|
12
|
+
Args:
|
|
13
|
+
f: The function to differentiate, called as `f(x, args)`.
|
|
14
|
+
x: The point at which to differentiate.
|
|
15
|
+
args: Other arguments passed through to `f`.
|
|
16
|
+
h: The step size.
|
|
17
|
+
|
|
18
|
+
Returns:
|
|
19
|
+
An approximation of the first derivative of `f` at `x`.
|
|
20
|
+
"""
|
|
21
|
+
f1 = f(x + h, args)
|
|
22
|
+
f2 = f(x - h, args)
|
|
23
|
+
f3 = f(x + 2.0 * h, args)
|
|
24
|
+
f4 = f(x - 2.0 * h, args)
|
|
25
|
+
return (8.0 * (f1 - f2) - (f3 - f4)) / (12.0 * h)
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def richardson_second_derivative(
|
|
29
|
+
f: Callable[[float, list], float], x: float, args: list, h: float
|
|
30
|
+
) -> float:
|
|
31
|
+
"""Compute the second derivative of $f(x, args)$ at $x$ via Richardson extrapolation.
|
|
32
|
+
|
|
33
|
+
Applies `richardson_derivative` to the (Richardson-extrapolated) first
|
|
34
|
+
derivative of `f`.
|
|
35
|
+
|
|
36
|
+
Args:
|
|
37
|
+
f: The function to differentiate, called as `f(x, args)`.
|
|
38
|
+
x: The point at which to differentiate.
|
|
39
|
+
args: Other arguments passed through to `f`.
|
|
40
|
+
h: The step size.
|
|
41
|
+
|
|
42
|
+
Returns:
|
|
43
|
+
An approximation of the second derivative of `f` at `x`.
|
|
44
|
+
"""
|
|
45
|
+
|
|
46
|
+
def fprime(y, a):
|
|
47
|
+
return richardson_derivative(f, y, a, h)
|
|
48
|
+
|
|
49
|
+
return richardson_derivative(fprime, x, args, h)
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: bs-python-utils
|
|
3
|
-
Version: 1.1.
|
|
3
|
+
Version: 1.1.3
|
|
4
4
|
Summary: Utilities programs for my Python code
|
|
5
5
|
Requires-Python: >=3.13
|
|
6
6
|
Description-Content-Type: text/markdown
|
|
@@ -50,6 +50,10 @@ Dynamic: license-file
|
|
|
50
50
|
|
|
51
51
|
### Release notes
|
|
52
52
|
|
|
53
|
+
#### 1.1.2 and 1.1.3(September 11, 2026)
|
|
54
|
+
- Improved the `Timer` class.
|
|
55
|
+
- added `bs_derivs` with Richardson derivatives.
|
|
56
|
+
|
|
53
57
|
#### 1.1.1 (September 04, 2026)
|
|
54
58
|
Added simulation-based bivariate ranks.
|
|
55
59
|
|
|
@@ -5,6 +5,7 @@ bs_python_utils/Timer.py
|
|
|
5
5
|
bs_python_utils/__init__.py
|
|
6
6
|
bs_python_utils/bivariate_quantiles.py
|
|
7
7
|
bs_python_utils/bs_altair.py
|
|
8
|
+
bs_python_utils/bs_derivs.py
|
|
8
9
|
bs_python_utils/bs_logging.py
|
|
9
10
|
bs_python_utils/bs_mathstr.py
|
|
10
11
|
bs_python_utils/bs_mem.py
|
|
@@ -60,6 +61,7 @@ bs_python_utils/examples/examples_seaborn.py
|
|
|
60
61
|
bs_python_utils/examples/examples_sklearn.py
|
|
61
62
|
bs_python_utils/examples/examples_sparse_gaussian.py
|
|
62
63
|
bs_python_utils/numerical/__init__.py
|
|
64
|
+
bs_python_utils/numerical/bs_derivs.py
|
|
63
65
|
bs_python_utils/numerical/bs_sparse_gaussian.py
|
|
64
66
|
bs_python_utils/numerical/bsnputils.py
|
|
65
67
|
bs_python_utils/numerical/bssputils.py
|
|
@@ -77,6 +79,7 @@ bs_python_utils/viz/bs_seaborn.py
|
|
|
77
79
|
bs_python_utils/viz/bsmplutils.py
|
|
78
80
|
bs_python_utils/viz/streamlit_utils.py
|
|
79
81
|
tests/test_bivariate_quantiles.py
|
|
82
|
+
tests/test_bs_derivs.py
|
|
80
83
|
tests/test_bs_opt.py
|
|
81
84
|
tests/test_bs_sparse_gaussian.py
|
|
82
85
|
tests/test_bsnputils.py
|
|
@@ -86,4 +89,5 @@ tests/test_bsutils.py
|
|
|
86
89
|
tests/test_chebyshev.py
|
|
87
90
|
tests/test_nptri.py
|
|
88
91
|
tests/test_random_draws.py
|
|
89
|
-
tests/test_sklearn_utils.py
|
|
92
|
+
tests/test_sklearn_utils.py
|
|
93
|
+
tests/test_timer.py
|
|
@@ -0,0 +1,365 @@
|
|
|
1
|
+
import math
|
|
2
|
+
from math import cos, exp, pi, sin
|
|
3
|
+
|
|
4
|
+
from bs_python_utils.numerical.bs_derivs import (
|
|
5
|
+
richardson_derivative,
|
|
6
|
+
richardson_second_derivative,
|
|
7
|
+
)
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class TestRichardsonDerivative:
|
|
11
|
+
"""Tests for richardson_derivative function."""
|
|
12
|
+
|
|
13
|
+
def test_linear_function(self):
|
|
14
|
+
"""First derivative of ax + b is a."""
|
|
15
|
+
|
|
16
|
+
# f(x) = 2x + 3, f'(x) = 2
|
|
17
|
+
def f(x, args):
|
|
18
|
+
return 2 * x + 3
|
|
19
|
+
|
|
20
|
+
x = 1.0
|
|
21
|
+
h = 0.01
|
|
22
|
+
result = richardson_derivative(f, x, [], h)
|
|
23
|
+
assert math.isclose(result, 2.0, rel_tol=1e-10)
|
|
24
|
+
|
|
25
|
+
def test_quadratic_function(self):
|
|
26
|
+
"""First derivative of x^2 is 2x."""
|
|
27
|
+
|
|
28
|
+
# f(x) = x^2, f'(x) = 2x
|
|
29
|
+
def f(x, args):
|
|
30
|
+
return x**2
|
|
31
|
+
|
|
32
|
+
x = 3.0
|
|
33
|
+
h = 0.01
|
|
34
|
+
result = richardson_derivative(f, x, [], h)
|
|
35
|
+
expected = 2 * x
|
|
36
|
+
assert math.isclose(result, expected, rel_tol=1e-8)
|
|
37
|
+
|
|
38
|
+
def test_cubic_function(self):
|
|
39
|
+
"""First derivative of x^3 is 3x^2."""
|
|
40
|
+
|
|
41
|
+
# f(x) = x^3, f'(x) = 3x^2
|
|
42
|
+
def f(x, args):
|
|
43
|
+
return x**3
|
|
44
|
+
|
|
45
|
+
x = 2.0
|
|
46
|
+
h = 0.01
|
|
47
|
+
result = richardson_derivative(f, x, [], h)
|
|
48
|
+
expected = 3 * x**2
|
|
49
|
+
assert math.isclose(result, expected, rel_tol=1e-8)
|
|
50
|
+
|
|
51
|
+
def test_quartic_function(self):
|
|
52
|
+
"""First derivative of x^4 is 4x^3."""
|
|
53
|
+
|
|
54
|
+
# f(x) = x^4, f'(x) = 4x^3
|
|
55
|
+
def f(x, args):
|
|
56
|
+
return x**4
|
|
57
|
+
|
|
58
|
+
x = 1.5
|
|
59
|
+
h = 0.01
|
|
60
|
+
result = richardson_derivative(f, x, [], h)
|
|
61
|
+
expected = 4 * x**3
|
|
62
|
+
assert math.isclose(result, expected, rel_tol=1e-6)
|
|
63
|
+
|
|
64
|
+
def test_exponential_function(self):
|
|
65
|
+
"""First derivative of e^x is e^x."""
|
|
66
|
+
|
|
67
|
+
# f(x) = e^x, f'(x) = e^x
|
|
68
|
+
def f(x, args):
|
|
69
|
+
return exp(x)
|
|
70
|
+
|
|
71
|
+
x = 0.5
|
|
72
|
+
h = 0.01
|
|
73
|
+
result = richardson_derivative(f, x, [], h)
|
|
74
|
+
expected = exp(x)
|
|
75
|
+
assert math.isclose(result, expected, rel_tol=1e-8)
|
|
76
|
+
|
|
77
|
+
def test_sine_function(self):
|
|
78
|
+
"""First derivative of sin(x) is cos(x)."""
|
|
79
|
+
|
|
80
|
+
# f(x) = sin(x), f'(x) = cos(x)
|
|
81
|
+
def f(x, args):
|
|
82
|
+
return sin(x)
|
|
83
|
+
|
|
84
|
+
x = pi / 4
|
|
85
|
+
h = 0.01
|
|
86
|
+
result = richardson_derivative(f, x, [], h)
|
|
87
|
+
expected = cos(x)
|
|
88
|
+
assert math.isclose(result, expected, rel_tol=1e-8)
|
|
89
|
+
|
|
90
|
+
def test_cosine_function(self):
|
|
91
|
+
"""First derivative of cos(x) is -sin(x)."""
|
|
92
|
+
|
|
93
|
+
# f(x) = cos(x), f'(x) = -sin(x)
|
|
94
|
+
def f(x, args):
|
|
95
|
+
return cos(x)
|
|
96
|
+
|
|
97
|
+
x = pi / 3
|
|
98
|
+
h = 0.01
|
|
99
|
+
result = richardson_derivative(f, x, [], h)
|
|
100
|
+
expected = -sin(x)
|
|
101
|
+
assert math.isclose(result, expected, rel_tol=1e-8)
|
|
102
|
+
|
|
103
|
+
def test_with_args_parameter(self):
|
|
104
|
+
"""Test that args are passed through correctly."""
|
|
105
|
+
|
|
106
|
+
# f(x, args) = a*x^2 + b*x + c where args = [a, b, c]
|
|
107
|
+
def f(x, args):
|
|
108
|
+
a, b, c = args
|
|
109
|
+
return a * x**2 + b * x + c
|
|
110
|
+
|
|
111
|
+
x = 2.0
|
|
112
|
+
h = 0.01
|
|
113
|
+
args = [3.0, 2.0, 1.0]
|
|
114
|
+
result = richardson_derivative(f, x, args, h)
|
|
115
|
+
expected = 2 * args[0] * x + args[1] # f'(x) = 2ax + b
|
|
116
|
+
assert math.isclose(result, expected, rel_tol=1e-8)
|
|
117
|
+
|
|
118
|
+
def test_zero_at_critical_point(self):
|
|
119
|
+
"""Derivative should be near zero at critical points."""
|
|
120
|
+
|
|
121
|
+
# f(x) = x^2, critical point at x = 0
|
|
122
|
+
def f(x, args):
|
|
123
|
+
return x**2
|
|
124
|
+
|
|
125
|
+
x = 0.0
|
|
126
|
+
h = 0.01
|
|
127
|
+
result = richardson_derivative(f, x, [], h)
|
|
128
|
+
assert abs(result) < 1e-10
|
|
129
|
+
|
|
130
|
+
def test_different_step_sizes(self):
|
|
131
|
+
"""Results should be accurate with different step sizes."""
|
|
132
|
+
|
|
133
|
+
# f(x) = sin(x), f'(x) = cos(x)
|
|
134
|
+
def f(x, args):
|
|
135
|
+
return sin(x)
|
|
136
|
+
|
|
137
|
+
x = pi / 6
|
|
138
|
+
expected = cos(x)
|
|
139
|
+
|
|
140
|
+
for h in [0.1, 0.01, 0.001]:
|
|
141
|
+
result = richardson_derivative(f, x, [], h)
|
|
142
|
+
# Smaller h should generally be more accurate
|
|
143
|
+
assert math.isclose(result, expected, rel_tol=1e-5)
|
|
144
|
+
|
|
145
|
+
def test_negative_x(self):
|
|
146
|
+
"""Should work correctly for negative x values."""
|
|
147
|
+
|
|
148
|
+
# f(x) = x^3, f'(x) = 3x^2
|
|
149
|
+
def f(x, args):
|
|
150
|
+
return x**3
|
|
151
|
+
|
|
152
|
+
x = -2.5
|
|
153
|
+
h = 0.01
|
|
154
|
+
result = richardson_derivative(f, x, [], h)
|
|
155
|
+
expected = 3 * x**2
|
|
156
|
+
assert math.isclose(result, expected, rel_tol=1e-8)
|
|
157
|
+
|
|
158
|
+
def test_large_x(self):
|
|
159
|
+
"""Should work correctly for large x values."""
|
|
160
|
+
|
|
161
|
+
# f(x) = x^2, f'(x) = 2x
|
|
162
|
+
def f(x, args):
|
|
163
|
+
return x**2
|
|
164
|
+
|
|
165
|
+
x = 100.0
|
|
166
|
+
h = 0.1
|
|
167
|
+
result = richardson_derivative(f, x, [], h)
|
|
168
|
+
expected = 2 * x
|
|
169
|
+
assert math.isclose(result, expected, rel_tol=1e-8)
|
|
170
|
+
|
|
171
|
+
def test_small_x(self):
|
|
172
|
+
"""Should work correctly for small x values close to zero."""
|
|
173
|
+
|
|
174
|
+
# f(x) = x^2, f'(x) = 2x
|
|
175
|
+
def f(x, args):
|
|
176
|
+
return x**2
|
|
177
|
+
|
|
178
|
+
x = 0.001
|
|
179
|
+
h = 0.00001
|
|
180
|
+
result = richardson_derivative(f, x, [], h)
|
|
181
|
+
expected = 2 * x
|
|
182
|
+
assert math.isclose(result, expected, rel_tol=1e-6)
|
|
183
|
+
|
|
184
|
+
def test_fourth_order_accuracy(self):
|
|
185
|
+
"""Richardson extrapolation should achieve fourth-order accuracy."""
|
|
186
|
+
|
|
187
|
+
# For a polynomial of degree 4 or less, the method should be exact
|
|
188
|
+
# f(x) = x^4 - 2x^3 + x^2, f'(x) = 4x^3 - 6x^2 + 2x
|
|
189
|
+
def f(x, args):
|
|
190
|
+
return x**4 - 2 * x**3 + x**2
|
|
191
|
+
|
|
192
|
+
x = 1.5
|
|
193
|
+
h = 0.01
|
|
194
|
+
result = richardson_derivative(f, x, [], h)
|
|
195
|
+
expected = 4 * x**3 - 6 * x**2 + 2 * x
|
|
196
|
+
# Fourth-order method should be very accurate for polynomials
|
|
197
|
+
assert math.isclose(result, expected, rel_tol=1e-10)
|
|
198
|
+
|
|
199
|
+
|
|
200
|
+
class TestRichardsonSecondDerivative:
|
|
201
|
+
"""Tests for richardson_second_derivative function."""
|
|
202
|
+
|
|
203
|
+
def test_quadratic_function(self):
|
|
204
|
+
"""Second derivative of x^2 is 2."""
|
|
205
|
+
|
|
206
|
+
# f(x) = x^2, f''(x) = 2
|
|
207
|
+
def f(x, args):
|
|
208
|
+
return x**2
|
|
209
|
+
|
|
210
|
+
x = 3.0
|
|
211
|
+
h = 0.01
|
|
212
|
+
result = richardson_second_derivative(f, x, [], h)
|
|
213
|
+
assert math.isclose(result, 2.0, rel_tol=1e-6)
|
|
214
|
+
|
|
215
|
+
def test_cubic_function(self):
|
|
216
|
+
"""Second derivative of x^3 is 6x."""
|
|
217
|
+
|
|
218
|
+
# f(x) = x^3, f''(x) = 6x
|
|
219
|
+
def f(x, args):
|
|
220
|
+
return x**3
|
|
221
|
+
|
|
222
|
+
x = 2.0
|
|
223
|
+
h = 0.01
|
|
224
|
+
result = richardson_second_derivative(f, x, [], h)
|
|
225
|
+
expected = 6 * x
|
|
226
|
+
assert math.isclose(result, expected, rel_tol=1e-6)
|
|
227
|
+
|
|
228
|
+
def test_quartic_function(self):
|
|
229
|
+
"""Second derivative of x^4 is 12x^2."""
|
|
230
|
+
|
|
231
|
+
# f(x) = x^4, f''(x) = 12x^2
|
|
232
|
+
def f(x, args):
|
|
233
|
+
return x**4
|
|
234
|
+
|
|
235
|
+
x = 1.5
|
|
236
|
+
h = 0.01
|
|
237
|
+
result = richardson_second_derivative(f, x, [], h)
|
|
238
|
+
expected = 12 * x**2
|
|
239
|
+
assert math.isclose(result, expected, rel_tol=1e-5)
|
|
240
|
+
|
|
241
|
+
def test_exponential_function(self):
|
|
242
|
+
"""Second derivative of e^x is e^x."""
|
|
243
|
+
|
|
244
|
+
# f(x) = e^x, f''(x) = e^x
|
|
245
|
+
def f(x, args):
|
|
246
|
+
return exp(x)
|
|
247
|
+
|
|
248
|
+
x = 0.5
|
|
249
|
+
h = 0.01
|
|
250
|
+
result = richardson_second_derivative(f, x, [], h)
|
|
251
|
+
expected = exp(x)
|
|
252
|
+
assert math.isclose(result, expected, rel_tol=1e-6)
|
|
253
|
+
|
|
254
|
+
def test_sine_function(self):
|
|
255
|
+
"""Second derivative of sin(x) is -sin(x)."""
|
|
256
|
+
|
|
257
|
+
# f(x) = sin(x), f''(x) = -sin(x)
|
|
258
|
+
def f(x, args):
|
|
259
|
+
return sin(x)
|
|
260
|
+
|
|
261
|
+
x = pi / 4
|
|
262
|
+
h = 0.01
|
|
263
|
+
result = richardson_second_derivative(f, x, [], h)
|
|
264
|
+
expected = -sin(x)
|
|
265
|
+
assert math.isclose(result, expected, rel_tol=1e-5)
|
|
266
|
+
|
|
267
|
+
def test_cosine_function(self):
|
|
268
|
+
"""Second derivative of cos(x) is -cos(x)."""
|
|
269
|
+
|
|
270
|
+
# f(x) = cos(x), f''(x) = -cos(x)
|
|
271
|
+
def f(x, args):
|
|
272
|
+
return cos(x)
|
|
273
|
+
|
|
274
|
+
x = pi / 3
|
|
275
|
+
h = 0.01
|
|
276
|
+
result = richardson_second_derivative(f, x, [], h)
|
|
277
|
+
expected = -cos(x)
|
|
278
|
+
assert math.isclose(result, expected, rel_tol=1e-5)
|
|
279
|
+
|
|
280
|
+
def test_with_args_parameter(self):
|
|
281
|
+
"""Test that args are passed through correctly."""
|
|
282
|
+
|
|
283
|
+
# f(x, args) = a*x^2 + b*x + c where args = [a, b, c]
|
|
284
|
+
def f(x, args):
|
|
285
|
+
a, b, c = args
|
|
286
|
+
return a * x**2 + b * x + c
|
|
287
|
+
|
|
288
|
+
x = 2.0
|
|
289
|
+
h = 0.01
|
|
290
|
+
args = [3.0, 2.0, 1.0]
|
|
291
|
+
result = richardson_second_derivative(f, x, args, h)
|
|
292
|
+
expected = 2 * args[0] # f''(x) = 2a
|
|
293
|
+
assert math.isclose(result, expected, rel_tol=1e-8)
|
|
294
|
+
|
|
295
|
+
def test_inflection_point(self):
|
|
296
|
+
"""Second derivative should change sign at inflection point."""
|
|
297
|
+
|
|
298
|
+
# f(x) = x^3, inflection point at x = 0
|
|
299
|
+
def f(x, args):
|
|
300
|
+
return x**3
|
|
301
|
+
|
|
302
|
+
h = 0.01
|
|
303
|
+
# Before inflection point
|
|
304
|
+
result_neg = richardson_second_derivative(f, -1.0, [], h)
|
|
305
|
+
# After inflection point
|
|
306
|
+
result_pos = richardson_second_derivative(f, 1.0, [], h)
|
|
307
|
+
|
|
308
|
+
assert result_neg < 0
|
|
309
|
+
assert result_pos > 0
|
|
310
|
+
|
|
311
|
+
def test_negative_x(self):
|
|
312
|
+
"""Should work correctly for negative x values."""
|
|
313
|
+
|
|
314
|
+
# f(x) = x^4, f''(x) = 12x^2
|
|
315
|
+
def f(x, args):
|
|
316
|
+
return x**4
|
|
317
|
+
|
|
318
|
+
x = -1.5
|
|
319
|
+
h = 0.01
|
|
320
|
+
result = richardson_second_derivative(f, x, [], h)
|
|
321
|
+
expected = 12 * x**2
|
|
322
|
+
assert math.isclose(result, expected, rel_tol=1e-5)
|
|
323
|
+
|
|
324
|
+
def test_large_x(self):
|
|
325
|
+
"""Should work correctly for large x values."""
|
|
326
|
+
|
|
327
|
+
# f(x) = x^2, f''(x) = 2
|
|
328
|
+
def f(x, args):
|
|
329
|
+
return x**2
|
|
330
|
+
|
|
331
|
+
x = 100.0
|
|
332
|
+
h = 0.1
|
|
333
|
+
result = richardson_second_derivative(f, x, [], h)
|
|
334
|
+
assert math.isclose(result, 2.0, rel_tol=1e-5)
|
|
335
|
+
|
|
336
|
+
def test_composition_with_derivatives(self):
|
|
337
|
+
"""Verify f''(x) by comparing with numerical first derivative of f'(x)."""
|
|
338
|
+
|
|
339
|
+
# f(x) = sin(x)
|
|
340
|
+
# Compute f'' using the second derivative function
|
|
341
|
+
def f(x, args):
|
|
342
|
+
return sin(x)
|
|
343
|
+
|
|
344
|
+
x = pi / 6
|
|
345
|
+
h = 0.01
|
|
346
|
+
|
|
347
|
+
second_deriv = richardson_second_derivative(f, x, [], h)
|
|
348
|
+
expected = -sin(x)
|
|
349
|
+
|
|
350
|
+
assert math.isclose(second_deriv, expected, rel_tol=1e-5)
|
|
351
|
+
|
|
352
|
+
def test_quartic_polynomial_exact(self):
|
|
353
|
+
"""For degree 4 polynomial, second derivative should be very accurate."""
|
|
354
|
+
|
|
355
|
+
# f(x) = x^4 - 2x^3 + x^2 + x - 1
|
|
356
|
+
# f'(x) = 4x^3 - 6x^2 + 2x + 1
|
|
357
|
+
# f''(x) = 12x^2 - 12x + 2
|
|
358
|
+
def f(x, args):
|
|
359
|
+
return x**4 - 2 * x**3 + x**2 + x - 1
|
|
360
|
+
|
|
361
|
+
x = 1.5
|
|
362
|
+
h = 0.01
|
|
363
|
+
result = richardson_second_derivative(f, x, [], h)
|
|
364
|
+
expected = 12 * x**2 - 12 * x + 2
|
|
365
|
+
assert math.isclose(result, expected, rel_tol=1e-8)
|