Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 23 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,29 @@ All notable changes to PyChebyshev will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## [0.21.0] - 2026-04-27

### Added
- `ChebyshevSlider.roots(dim, fixed)` — find all roots along `dim` with other dims fixed
- `ChebyshevSlider.minimize(dim, fixed)` — find global minimum along `dim`
- `ChebyshevSlider.maximize(dim, fixed)` — find global maximum along `dim`
- `ChebyshevTT.roots(dim, fixed)` — same, for ChebyshevTT (user-frame `dim`, transparent under `_dim_order`)
- `ChebyshevTT.minimize(dim, fixed)` — same, for ChebyshevTT
- `ChebyshevTT.maximize(dim, fixed)` — same, for ChebyshevTT

### Notes
- Closes the calculus parity gap promised since v0.17. All four public
classes (`ChebyshevApproximation`, `ChebyshevSpline`, `ChebyshevSlider`,
`ChebyshevTT`) now support the full calculus surface: `integrate`,
`roots`, `minimize`, `maximize`.
- Implementation reuses the existing 1-D primitives in `_calculus.py`
(`_roots_1d`, `_optimize_1d`) — no new math.
- Mirrors v0.9 `ChebyshevApproximation`/`ChebyshevSpline` semantics:
multi-D requires `fixed={d: v, ...}` for all dims except target.
- Under `ChebyshevTT.with_auto_order()` / `reorder()` (v0.20+), the
user-frame `dim` and `fixed` keys translate to storage frame
transparently via `slice()` and `to_dense()`.

## [0.20.1] - 2026-04-27

### Fixed — TT `_dim_order` Full Threading
Expand Down
16 changes: 11 additions & 5 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ PyChebyshev is a pip-installable Python library for multi-dimensional Chebyshev
# Setup
uv sync

# Run tests (~1055 tests, ~115s due to 5D Black-Scholes builds)
# Run tests (~1112 tests, ~115s due to 5D Black-Scholes builds)
uv run pytest tests/ -v

# Run a single test
Expand Down Expand Up @@ -61,7 +61,7 @@ The installable package. Public classes: `ChebyshevApproximation`, `ChebyshevSpl
typed helpers (constructors accept both forms).
- v0.17 adds `integrate()` on `ChebyshevSlider` and `ChebyshevTT` (full +
partial integration). After v0.17, every PyChebyshev class supports
integration. Roots/min/max on Slider/TT remain deferred to v0.21.
integration.
- v0.18 adds TT feature parity: `ChebyshevTT.nodes()` static,
`from_values()` classmethod, `extrude()`, `slice()`, algebra
(`+`, `-`, `*` scalar, in-place variants, `__neg__`), and `to_dense()`.
Expand All @@ -81,6 +81,9 @@ The installable package. Public classes: `ChebyshevApproximation`, `ChebyshevSpl
fully thread `_dim_order`. New public `ChebyshevTT.reorder(new_order)`
method (TT-swap via adjacent SVDs) is the explicit alignment escape
hatch for binary algebra between TTs of different orders.
- v0.21 adds `ChebyshevSlider.roots()/minimize()/maximize()` and
`ChebyshevTT.roots()/minimize()/maximize()`. After v0.21, all four
classes support the full calculus surface (integrate + roots + min/max).

### Benchmark Scripts (project root)

Expand Down Expand Up @@ -126,9 +129,12 @@ Not part of the library. Compare Chebyshev barycentric against alternative metho
`get_evaluation_points`, `get_num_evaluation_points`), `peek_format_version`,
`is_dimensionality_allowed`, `defer_build` + `set_original_function_values`,
`Domain`/`Ns`/`SpecialPoints` typed helpers.
- `test_calculus_completion.py` — ~37 tests: `ChebyshevSlider.integrate()` (full
and partial), `ChebyshevTT.integrate()` (full and partial), cross-class
consistency checks, bounds validation.
- `test_calculus_completion.py` — ~101 tests: `ChebyshevSlider.integrate/roots/minimize/maximize`,
`ChebyshevTT.integrate/roots/minimize/maximize` (full and partial,
user-frame dim/fixed transparent under `_dim_order`), cross-class
consistency checks, bounds validation. v0.21 additions: 57 tests
across 9 new test classes covering Slider/TT roots/min/max parity
with Approximation/Spline.
- `test_v018_tt_parity.py` — ~52 tests: `ChebyshevTT.nodes()`, `from_values()`,
`extrude()`, `slice()`, algebra (`+`, `-`, `*` scalar, in-place, `__neg__`),
`to_dense()`; cross-feature and round-trip checks.
Expand Down
147 changes: 147 additions & 0 deletions compare_v021_slider_tt_calculus.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,147 @@
"""v0.21 demo: Slider/TT roots, minimize, maximize.

PyChebyshev v0.21 closes the calculus parity gap promised since v0.17.
Before v0.21:
- ChebyshevApproximation: integrate, roots, minimize, maximize
- ChebyshevSpline: integrate, roots, minimize, maximize
- ChebyshevSlider: integrate only
- ChebyshevTT: integrate only

After v0.21: all four classes have the full calculus surface.

This script demonstrates:
- 1-D Slider/TT roots/min/max against analytical answers
- 2-D and 3-D Slider/TT roots/min/max with fixed= dict
- Cross-class consistency: Slider/TT/Approximation agree on the same function
- Composition with extrude/slice/algebra/with_auto_order

No MoCaX baseline -- these are beyond-MoCaX features.
"""

from __future__ import annotations

import math
import time

import numpy as np

from pychebyshev import (
ChebyshevApproximation,
ChebyshevSlider,
ChebyshevTT,
)


def _check(label: str, got: float, expected: float, tol: float = 1e-9) -> None:
err = abs(got - expected)
status = "OK " if err < tol else "FAIL"
print(f" [{status}] {label:50s} got {got:+.10e} expected {expected:+.10e} err {err:.2e}")


def demo_1d_slider() -> None:
print("\n=== 1-D ChebyshevSlider: roots/min/max of x^2 - 0.25 on [-1, 1] ===")
def f(x, _): return x[0] ** 2 - 0.25
slider = ChebyshevSlider(
f, num_dimensions=1, domain=[(-1, 1)], n_nodes=[10],
partition=[[0]], pivot_point=[0.0],
)
slider.build(verbose=False)
roots = slider.roots()
_check("roots[0]", float(roots[0]), -0.5)
_check("roots[1]", float(roots[1]), 0.5)
val, loc = slider.minimize()
_check("min value", val, -0.25)
_check("min loc", loc, 0.0)
val, loc = slider.maximize()
_check("max value", val, 0.75)


def demo_1d_tt() -> None:
print("\n=== 1-D ChebyshevTT: roots/min/max of x^2 - 0.25 on [-1, 1] ===")
def f(x, _): return x[0] ** 2 - 0.25
tt = ChebyshevTT(f, num_dimensions=1, domain=[(-1, 1)], n_nodes=[10])
tt.build(verbose=False)
roots = tt.roots()
_check("roots[0]", float(roots[0]), -0.5)
_check("roots[1]", float(roots[1]), 0.5)
val, loc = tt.minimize()
_check("min value", val, -0.25)
val, loc = tt.maximize()
_check("max value", val, 0.75)


def demo_3d_with_auto_order() -> None:
print("\n=== 3-D TT with_auto_order: roots/min must respect user-frame ===")
def f(x, _): return (x[0] - 0.4) * (1.0 + 0.1 * x[1] + 0.2 * x[2])
# with_auto_order is a classmethod constructor that builds and reorders
tt_reordered = ChebyshevTT.with_auto_order(
f, num_dimensions=3, domain=[(-1, 1)] * 3, n_nodes=[10, 10, 10],
n_trials=3,
)
print(f" TT _dim_order after with_auto_order: {tt_reordered._dim_order}")
roots = tt_reordered.roots(dim=0, fixed={1: 0.0, 2: 0.0})
_check("roots[0]", float(roots[0]), 0.4)


def demo_3d_reorder_transparency() -> None:
"""Force non-identity _dim_order via explicit reorder() and verify
user-frame roots/minimize results are unchanged."""
print("\n=== 3-D TT reorder([2, 0, 1]): user-frame transparency under non-identity storage ===")
def f(x, _): return (x[0] - 0.2) ** 2 + x[1] ** 2 + x[2] ** 2

tt = ChebyshevTT(
f, num_dimensions=3, domain=[(-1, 1)] * 3, n_nodes=[8, 8, 8],
)
tt.build(verbose=False)
print(f" Canonical TT _dim_order: {tt._dim_order}")

# Reorder forces a non-identity storage layout
tt_permuted = tt.reorder([2, 0, 1])
print(f" After reorder([2,0,1]): _dim_order = {tt_permuted._dim_order}")

# Minimize in user-frame dim 0 must give same answer in both
v_canonical, l_canonical = tt.minimize(dim=0, fixed={1: 0.0, 2: 0.0})
v_permuted, l_permuted = tt_permuted.minimize(dim=0, fixed={1: 0.0, 2: 0.0})
_check("canonical min value", v_canonical, 0.0, tol=1e-8)
_check("canonical min loc", l_canonical, 0.2, tol=1e-8)
_check("permuted min value", v_permuted, 0.0, tol=1e-8)
_check("permuted min loc", l_permuted, 0.2, tol=1e-8)
_check("canonical vs permuted value", abs(v_canonical - v_permuted), 0.0, tol=1e-12)
_check("canonical vs permuted loc", abs(l_canonical - l_permuted), 0.0, tol=1e-12)


def demo_cross_class_consistency() -> None:
print("\n=== Cross-class consistency: Slider/TT/Approx agree on same function ===")
def f(x, _): return (x[0] - 0.2) ** 2 + (x[1] + 0.1) ** 2

cheb = ChebyshevApproximation(f, 2, [(-1, 1), (-1, 1)], [9, 9])
slider = ChebyshevSlider(
f, num_dimensions=2, domain=[(-1, 1), (-1, 1)], n_nodes=[9, 9],
partition=[[0], [1]], pivot_point=[0.0, 0.0],
)
tt = ChebyshevTT(f, num_dimensions=2, domain=[(-1, 1), (-1, 1)], n_nodes=[9, 9])
for x in (cheb, slider, tt):
x.build(verbose=False)

v_c, l_c = cheb.minimize(dim=0, fixed={1: -0.1})
v_s, l_s = slider.minimize(dim=0, fixed={1: -0.1})
v_t, l_t = tt.minimize(dim=0, fixed={1: -0.1})
print(f" Approx.minimize -> ({v_c:+.10e}, {l_c:+.10e})")
print(f" Slider.minimize -> ({v_s:+.10e}, {l_s:+.10e})")
print(f" TT.minimize -> ({v_t:+.10e}, {l_t:+.10e})")
print(f" diff |Slider-Approx| = {abs(v_s-v_c):.2e}, |TT-Approx| = {abs(v_t-v_c):.2e}")


def main() -> None:
print(f"PyChebyshev v0.21 calculus parity demo")
t0 = time.time()
demo_1d_slider()
demo_1d_tt()
demo_3d_with_auto_order()
demo_3d_reorder_transparency()
demo_cross_class_consistency()
print(f"\nTotal: {time.time() - t0:.2f}s")


if __name__ == "__main__":
main()
100 changes: 93 additions & 7 deletions docs/user-guide/calculus.md
Original file line number Diff line number Diff line change
Expand Up @@ -36,8 +36,8 @@ all from a single pre-built proxy, without calling the pricing engine again.
|-------|:---:|:---:|:---:|
| `ChebyshevApproximation` | Yes | Yes | Yes |
| `ChebyshevSpline` | Yes | Yes | Yes |
| `ChebyshevSlider` | Yes (v0.17) | No | No |
| `ChebyshevTT` | Yes (v0.17) | No | No |
| `ChebyshevSlider` | Yes (v0.17) | Yes (v0.21) | Yes (v0.21) |
| `ChebyshevTT` | Yes (v0.17) | Yes (v0.21) | Yes (v0.21) |

## Integration

Expand Down Expand Up @@ -484,12 +484,10 @@ matrices, and barycentric evaluation.

## Limitations

- **`roots()`, `minimize()`, and `maximize()` are not yet supported on
`ChebyshevSlider` or `ChebyshevTT`.** These operations require
1-D polynomial coefficient extraction; generalising to the sliding
decomposition or TT format is deferred.
- **Multi-D rootfinding** (2D Bezout resultants) is not implemented. Only
1-D slices are supported via the `dim` + `fixed` interface.
1-D slices are supported via the `dim` + `fixed` interface. This applies
to all four classes — `ChebyshevApproximation`, `ChebyshevSpline`,
`ChebyshevSlider`, and `ChebyshevTT`.
- **Result has `function=None`** -- partial integration results cannot call
`build()` again, since there is no underlying function reference.

Expand All @@ -507,3 +505,91 @@ matrices, and barycentric evaluation.

4. Berrut, J.-P. & Trefethen, L.N. (2004), "Barycentric Lagrange
Interpolation", *SIAM Review* 46(3):501--517.

## Slider and TT calculus (v0.21+)

Starting in v0.21, `ChebyshevSlider` and `ChebyshevTT` support the same
calculus surface as `ChebyshevApproximation` and `ChebyshevSpline`:
`integrate`, `roots`, `minimize`, `maximize`. The signatures are
identical.

### Roots

```python
from pychebyshev import ChebyshevTT

def f(x, _): return x[0] ** 2 - 0.25
tt = ChebyshevTT(f, num_dimensions=1, domain=[(-1, 1)], n_nodes=[10])
tt.build()

print(tt.roots()) # [-0.5, 0.5]
```

For multi-D TT/Slider, fix all but one dimension:

```python
def g(x, _): return x[0] - x[1]
tt = ChebyshevTT(g, num_dimensions=2, domain=[(-1, 1), (-1, 1)], n_nodes=[5, 5])
tt.build()

print(tt.roots(dim=0, fixed={1: 0.3})) # [0.3]
```

### Minimize / maximize

```python
def f(x, _): return (x[0] - 0.3) ** 2 + 1.0
tt = ChebyshevTT(f, num_dimensions=1, domain=[(-1, 1)], n_nodes=[10])
tt.build()

val, loc = tt.minimize() # (1.0, 0.3)
val, loc = tt.maximize() # endpoint maximum
```

### Frame transparency under `with_auto_order`/`reorder`

`ChebyshevTT.roots()`, `minimize()`, and `maximize()` accept user-frame
`dim` and `fixed` keys. After `with_auto_order()` or `reorder()`
permute the internal storage layout, the user-frame interface is
unchanged — same call, same answer:

```python
import numpy as np
from pychebyshev import ChebyshevTT

def f(x, _): return (x[0] - 0.4) * (1.0 + 0.1*x[1] + 0.2*x[2])

tt = ChebyshevTT(f, num_dimensions=3, domain=[(-1, 1)]*3, n_nodes=[10, 10, 10])
tt.build()
tt_optimized = ChebyshevTT.with_auto_order(
f, num_dimensions=3, domain=[(-1, 1)]*3, n_nodes=[10, 10, 10],
)
# Same user-frame call, same result regardless of internal _dim_order
roots_a = tt.roots(dim=0, fixed={1: 0.0, 2: 0.0})
roots_b = tt_optimized.roots(dim=0, fixed={1: 0.0, 2: 0.0})
np.testing.assert_array_almost_equal(roots_a, roots_b)
```

### Slider example

```python
from pychebyshev import ChebyshevSlider

def f(x, _): return x[0] ** 2 - 0.25
slider = ChebyshevSlider(
f, num_dimensions=1, domain=[(-1, 1)], n_nodes=[10],
partition=[[0]], pivot_point=[0.0],
)
slider.build()
print(slider.roots()) # [-0.5, 0.5]
val, loc = slider.minimize() # (-0.25, 0.0)
val, loc = slider.maximize() # (0.75, ±1)
```

### Implementation note

Both classes implement these methods by reducing to a 1-D problem
(via `slice()`), constructing a 1-D `ChebyshevApproximation`, and
delegating to its `roots()`/`minimize()`/`maximize()`. No new math —
all spectral algorithms reuse the v0.9 primitives in
`pychebyshev._calculus`.
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ build-backend = "hatchling.build"

[project]
name = "pychebyshev"
version = "0.20.1"
version = "0.21.0"
description = "Fast multi-dimensional Chebyshev tensor interpolation with analytical derivatives"
readme = "README.md"
license = {text = "MIT"}
Expand Down
2 changes: 1 addition & 1 deletion src/pychebyshev/_version.py
Original file line number Diff line number Diff line change
@@ -1 +1 @@
__version__ = "0.20.1"
__version__ = "0.21.0"
Loading
Loading