Skip to content

Commit 1d6be14

Browse files
authored
Merge pull request #16 from 0xC000005/v0.19-build-diag
v0.19.0: Build & Diagnostics — parallel build, progress bars, plot helpers
2 parents 42745d7 + c4e1b1b commit 1d6be14

17 files changed

Lines changed: 1179 additions & 28 deletions

CHANGELOG.md

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,19 @@ All notable changes to PyChebyshev will be documented in this file.
55
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
66
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
77

8+
## [0.19.0] - 2026-04-26
9+
10+
### Added — Build & Diagnostics
11+
12+
- `n_workers=` keyword-only constructor kwarg on `ChebyshevApproximation` and `ChebyshevSpline` for parallel function evaluation at build time via `concurrent.futures.ProcessPoolExecutor`. `None` (default) = sequential, `-1` = `os.cpu_count()`, positive int = pool size. Function and `additional_data` must be picklable.
13+
- `verbose=2` opt-in tqdm progress bars on `ChebyshevSpline.build()`, `ChebyshevSlider.build()`, and `ChebyshevTT.build()` (TT-Cross sweeps). Existing `verbose=True/False` behavior unchanged.
14+
- `ChebyshevApproximation.plot_convergence(target_error=None, max_n=64, ax=None)` — builds at increasing N, plots error decay on log-y axis with optional target line.
15+
- `plot_1d(ax=None, n_points=200, fixed=None)`, `plot_2d_surface(...)`, `plot_2d_contour(...)` instance methods on all four classes. Use `fixed=` to constrain dimensions when the source has more dims than the plot needs.
16+
- New optional dependency group `pychebyshev[viz]` (matplotlib + tqdm). Plot methods raise `ImportError` with install hint when used without the group; tqdm fallback emits a warning.
17+
- New private helpers: `_progress.py`, `_parallel.py`, `_viz.py`.
18+
19+
**Beyond MoCaX:** MoCaX has neither parallel build nor visualization helpers.
20+
821
## [0.18.0] - 2026-04-26
922

1023
### Added — TT Feature Parity

CLAUDE.md

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ PyChebyshev is a pip-installable Python library for multi-dimensional Chebyshev
1212
# Setup
1313
uv sync
1414

15-
# Run tests (~949 tests, ~110s due to 5D Black-Scholes builds)
15+
# Run tests (~989 tests, ~110s due to 5D Black-Scholes builds)
1616
uv run pytest tests/ -v
1717

1818
# Run a single test
@@ -67,6 +67,11 @@ The installable package. Public classes: `ChebyshevApproximation`, `ChebyshevSpl
6767
(`+`, `-`, `*` scalar, in-place variants, `__neg__`), and `to_dense()`.
6868
After v0.18, ChebyshevTT has full surface parity with
6969
ChebyshevApproximation for non-calculus features.
70+
- v0.19 adds parallel build (`n_workers=` on Approximation/Spline),
71+
tqdm progress bars (`verbose=2` on Spline/Slider/TT),
72+
`plot_convergence()` (Approximation), and `plot_1d`/`plot_2d_surface`/`plot_2d_contour`
73+
instance methods on all four classes. New optional dep group
74+
`pychebyshev[viz]` (matplotlib + tqdm).
7075

7176
### Benchmark Scripts (project root)
7277

@@ -87,6 +92,7 @@ Not part of the library. Compare Chebyshev barycentric against alternative metho
8792
- `compare_v016_polish.py` — PyChebyshev v0.16 polish surface vs MoCaX 4.3.1 cosmetic API (requires `mocaxpy`; gracefully skips MoCaX side if not installed)
8893
- `compare_calculus_completion.py` — PyChebyshev v0.17 Slider/TT integrate vs MoCaX 4.3.1 (no equivalent — beyond-MoCaX feature)
8994
- `compare_v018_tt_parity.py` — PyChebyshev v0.18 TT surface (extrude/slice/algebra/from_values/to_dense) vs MoCaX 4.3.1
95+
- `compare_v019_build_diagnostics.py` — PyChebyshev v0.19 build optimization (parallel eval, progress bars, visualization) — no MoCaX equivalent
9096

9197
### Tests (`tests/`)
9298

@@ -117,6 +123,9 @@ Not part of the library. Compare Chebyshev barycentric against alternative metho
117123
- `test_v018_tt_parity.py`~52 tests: `ChebyshevTT.nodes()`, `from_values()`,
118124
`extrude()`, `slice()`, algebra (`+`, `-`, `*` scalar, in-place, `__neg__`),
119125
`to_dense()`; cross-feature and round-trip checks.
126+
- `test_v019_build_diagnostics.py`~40 tests: parallel build via `n_workers=`,
127+
tqdm progress bars (`verbose=2`), `plot_convergence()`, `plot_1d()`,
128+
`plot_2d_surface()`, `plot_2d_contour()`; cross-feature integration.
120129

121130
### CI/CD (`.github/workflows/`)
122131

compare_v019_build_diagnostics.py

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
"""Demonstrate v0.19 parallel build + progress bars + viz helpers.
2+
3+
MoCaX has neither parallel build nor visualization helpers — this is a
4+
beyond-MoCaX feature.
5+
"""
6+
import math
7+
import time
8+
9+
from pychebyshev import ChebyshevApproximation
10+
11+
12+
def slow_f(x, _):
13+
"""Simulate a slow function (~1 ms per call)."""
14+
time.sleep(0.001)
15+
return math.sin(x[0]) * math.cos(x[1])
16+
17+
18+
def main():
19+
domain = [[-1.0, 1.0], [-1.0, 1.0]]
20+
n = 12
21+
22+
# Sequential build
23+
t0 = time.time()
24+
cheb_seq = ChebyshevApproximation(slow_f, 2, domain, [n, n])
25+
cheb_seq.build(verbose=False)
26+
t_seq = time.time() - t0
27+
28+
# Parallel build (4 workers)
29+
t0 = time.time()
30+
cheb_par = ChebyshevApproximation(slow_f, 2, domain, [n, n], n_workers=4)
31+
cheb_par.build(verbose=False)
32+
t_par = time.time() - t0
33+
34+
print(f"Sequential build: {t_seq:.2f}s")
35+
print(f"Parallel build (4 workers): {t_par:.2f}s")
36+
if t_par > 0:
37+
print(f"Speedup: {t_seq / t_par:.2f}x")
38+
39+
# Plot convergence
40+
try:
41+
import matplotlib
42+
matplotlib.use("Agg")
43+
import matplotlib.pyplot as plt
44+
ax = cheb_par.plot_convergence(target_error=1e-6, max_n=20)
45+
plt.savefig("convergence.png", dpi=80)
46+
plt.close()
47+
print("Convergence plot saved to convergence.png")
48+
except ImportError:
49+
print("(matplotlib not installed; skipping plot)")
50+
51+
# Plot 2D surface
52+
try:
53+
import matplotlib.pyplot as plt
54+
ax = cheb_par.plot_2d_surface(n_points=30)
55+
plt.savefig("surface.png", dpi=80)
56+
plt.close()
57+
print("2D surface plot saved to surface.png")
58+
except ImportError:
59+
pass
60+
61+
62+
if __name__ == "__main__":
63+
main()

docs/roadmap.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -161,7 +161,7 @@ non-calculus surface.
161161

162162
**Beyond MoCaX:** richer TT primitives than MoCaXExtend exposes.
163163

164-
## v0.19 — Build & Diagnostics :material-clock-outline:
164+
## v0.19 — Build & Diagnostics :material-check:
165165

166166
Ergonomics for users with slow `f` or large grids.
167167

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
# Build & Diagnostics (v0.19)
2+
3+
Three opt-in features to improve the experience of building large interpolants
4+
with slow functions.
5+
6+
## Parallel build (Approximation, Spline)
7+
8+
Pass `n_workers=` to the constructor to evaluate `f` in parallel via
9+
`concurrent.futures.ProcessPoolExecutor`:
10+
11+
```python
12+
cheb = ChebyshevApproximation(
13+
expensive_f, 3, [[-1, 1]] * 3, [10, 10, 10],
14+
n_workers=4, # 4 worker processes
15+
)
16+
cheb.build()
17+
18+
# Other accepted forms:
19+
cheb = ChebyshevApproximation(..., n_workers=None) # sequential (default)
20+
cheb = ChebyshevApproximation(..., n_workers=-1) # use os.cpu_count()
21+
```
22+
23+
Constraints:
24+
25+
- `expensive_f` and `additional_data` must be picklable. Top-level functions
26+
and dicts of basic types work; closures over local variables don't.
27+
- For functions that return in microseconds, the pool overhead can exceed
28+
parallelism gains — leave `n_workers=None` for cheap functions.
29+
30+
For `ChebyshevSpline`, `n_workers` propagates to every piece's underlying
31+
`ChebyshevApproximation` build.
32+
33+
## Progress bars (`verbose=2`)
34+
35+
```python
36+
cheb.build(verbose=2) # opt-in tqdm progress bar
37+
```
38+
39+
Available on all four classes. Existing `verbose=True/False` continues to
40+
control text prints; `verbose=2` *adds* a tqdm progress bar.
41+
42+
If `tqdm` isn't installed, the build emits a one-time warning and proceeds
43+
without a bar. Install via `pip install pychebyshev[viz]`.
44+
45+
## Plot helpers
46+
47+
```python
48+
cheb.plot_1d() # 1-D source: plots f(x)
49+
cheb.plot_1d(fixed={1: 0.5, 2: 0.0}) # multi-D: pin all but one
50+
cheb.plot_2d_surface(n_points=50) # 3-D surface plot
51+
cheb.plot_2d_contour(n_levels=20) # filled contour
52+
cheb.plot_convergence(target_error=1e-6, max_n=64) # Approximation only
53+
```
54+
55+
All four classes support `plot_1d`, `plot_2d_surface`, `plot_2d_contour`.
56+
`plot_convergence` is `ChebyshevApproximation`-only (Spline/Slider/TT have
57+
different convergence regimes).
58+
59+
All return a matplotlib Axes — use `ax=` to compose with your own figures.
60+
61+
Requires `pip install pychebyshev[viz]`.

mkdocs.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,7 @@ nav:
6868
- Error-Driven Construction: user-guide/error-driven-construction.md
6969
- Special Points: user-guide/special-points.md
7070
- Ergonomics: user-guide/ergonomics.md
71+
- Build & Diagnostics: user-guide/build-diagnostics.md
7172
- API Reference: api/reference.md
7273
- Benchmarks: benchmarks.md
7374
- Roadmap: roadmap.md

pyproject.toml

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
44

55
[project]
66
name = "pychebyshev"
7-
version = "0.18.0"
7+
version = "0.19.0"
88
description = "Fast multi-dimensional Chebyshev tensor interpolation with analytical derivatives"
99
readme = "README.md"
1010
license = {text = "MIT"}
@@ -34,6 +34,7 @@ dependencies = [
3434
]
3535

3636
[project.optional-dependencies]
37+
viz = ["matplotlib>=3.5", "tqdm>=4.60"]
3738
jit = ["numba>=0.60"] # Deprecated: vectorized_eval() via BLAS is ~150x faster than JIT fast_eval()
3839
dev = [
3940
"blackscholes>=0.2.0",

src/pychebyshev/_parallel.py

Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
"""Parallel function evaluation at build time via ProcessPoolExecutor."""
2+
from __future__ import annotations
3+
4+
import os
5+
from concurrent.futures import ProcessPoolExecutor
6+
7+
import numpy as np
8+
9+
10+
def _normalize_n_workers(n_workers):
11+
"""Validate and normalize n_workers ctor arg.
12+
13+
Returns
14+
-------
15+
int | None
16+
None for sequential; positive int for parallel pool size.
17+
18+
Raises
19+
------
20+
ValueError
21+
If n_workers is 0, < -1, or not an int.
22+
"""
23+
if n_workers is None:
24+
return None
25+
if not isinstance(n_workers, int) or isinstance(n_workers, bool):
26+
raise ValueError(f"n_workers must be int or None, got {type(n_workers).__name__}")
27+
if n_workers == 0:
28+
raise ValueError("n_workers must be >= 1, -1 for cpu_count, or None for sequential")
29+
if n_workers == -1:
30+
return os.cpu_count() or 1
31+
if n_workers < -1:
32+
raise ValueError(f"n_workers={n_workers} not allowed (use -1, 1, or positive int)")
33+
return n_workers
34+
35+
36+
def _evaluate_in_parallel(function, points, additional_data, n_workers):
37+
"""Evaluate ``function(point, additional_data)`` at every point.
38+
39+
Parameters
40+
----------
41+
function : callable
42+
Picklable callable taking (point, additional_data) -> scalar.
43+
points : iterable of points
44+
Iterable of point lists or arrays.
45+
additional_data : object
46+
Picklable second-arg context.
47+
n_workers : int | None
48+
Effective worker count (already normalized via _normalize_n_workers).
49+
50+
Returns
51+
-------
52+
np.ndarray
53+
Shape (N,) float64 array of results.
54+
"""
55+
points_list = [list(p) for p in points]
56+
if n_workers is None or n_workers == 1:
57+
return np.array(
58+
[float(function(p, additional_data)) for p in points_list],
59+
dtype=np.float64,
60+
)
61+
worker = _Worker(function, additional_data)
62+
with ProcessPoolExecutor(max_workers=n_workers) as pool:
63+
results = list(pool.map(worker, points_list))
64+
return np.array(results, dtype=np.float64)
65+
66+
67+
class _Worker:
68+
"""Picklable wrapper that calls ``function(point, additional_data)``."""
69+
70+
def __init__(self, function, additional_data):
71+
self.function = function
72+
self.additional_data = additional_data
73+
74+
def __call__(self, point):
75+
return float(self.function(point, self.additional_data))

src/pychebyshev/_progress.py

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
"""Optional tqdm-based progress bars. Activated by ``verbose=2``."""
2+
from __future__ import annotations
3+
4+
import warnings
5+
6+
try:
7+
from tqdm import tqdm # type: ignore[import-untyped]
8+
_HAS_TQDM = True
9+
except ImportError:
10+
_HAS_TQDM = False
11+
tqdm = None # type: ignore[assignment]
12+
13+
14+
def _maybe_progress(iterable, *, desc: str, verbose):
15+
"""Wrap ``iterable`` in a tqdm progress bar when ``verbose == 2`` and
16+
tqdm is available; otherwise return the iterable unchanged.
17+
18+
A one-time warning is emitted if ``verbose == 2`` but tqdm is missing,
19+
pointing the user at the ``pychebyshev[viz]`` install extra.
20+
"""
21+
if verbose != 2:
22+
return iterable
23+
if not _HAS_TQDM:
24+
warnings.warn(
25+
"verbose=2 requires tqdm; install with `pip install pychebyshev[viz]`",
26+
stacklevel=2,
27+
)
28+
return iterable
29+
return tqdm(iterable, desc=desc, leave=False)

src/pychebyshev/_version.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
__version__ = "0.18.0"
1+
__version__ = "0.19.0"

0 commit comments

Comments
 (0)