From eeaa25ecaf77cd622df6ac8ddd0dfa700d23611a Mon Sep 17 00:00:00 2001 From: Max Zhang Date: Mon, 27 Apr 2026 10:18:37 -0400 Subject: [PATCH 01/17] v0.21: add Slider._to_1d_chebyshev private helper Private helper that builds a 1-D ChebyshevApproximation from a 1-D Slider by evaluating at Chebyshev nodes. Foundation for v0.21 roots/minimize/maximize methods on ChebyshevSlider. --- src/pychebyshev/slider.py | 38 ++++++++++++++++++++++++++++++ tests/test_calculus_completion.py | 39 +++++++++++++++++++++++++++++++ 2 files changed, 77 insertions(+) diff --git a/src/pychebyshev/slider.py b/src/pychebyshev/slider.py index fafd152..4529aaf 100644 --- a/src/pychebyshev/slider.py +++ b/src/pychebyshev/slider.py @@ -1135,6 +1135,44 @@ def integrate(self, dims=None, bounds=None): obj._derivative_id_to_orders = [] return obj + def _to_1d_chebyshev(self, sliced_1d): + """Build a 1-D ChebyshevApproximation from a 1-D Slider. + + Evaluates *sliced_1d* at the Type-I Chebyshev nodes of its + surviving dimension and constructs a ``ChebyshevApproximation`` + via ``from_values``. The result is bit-identical equivalent to + a direct ``ChebyshevApproximation`` build of the same function + over the same nodes. + + Parameters + ---------- + sliced_1d : ChebyshevSlider + A 1-D Slider (``num_dimensions == 1``). + + Returns + ------- + ChebyshevApproximation + 1-D Chebyshev approximation matching *sliced_1d*'s values. + """ + from pychebyshev.barycentric import ChebyshevApproximation + + n = sliced_1d.n_nodes[0] + a, b = sliced_1d.domain[0] + # Type-I Chebyshev nodes in [a, b], ascending order + k = np.arange(n) + t = -np.cos((2.0 * k + 1.0) * np.pi / (2.0 * n)) + cheb_nodes = 0.5 * (a + b) + 0.5 * (b - a) * t + + values = np.array([ + float(sliced_1d.eval([float(x)], derivative_order=[0])) for x in cheb_nodes + ]) + return ChebyshevApproximation.from_values( + values, + num_dimensions=1, + domain=[(float(a), float(b))], + n_nodes=[int(n)], + ) + def _check_slider_compatible(self, other): """Validate that two sliders can be combined arithmetically.""" from pychebyshev._algebra import _check_compatible diff --git a/tests/test_calculus_completion.py b/tests/test_calculus_completion.py index 9fabfe6..ef5ffcc 100644 --- a/tests/test_calculus_completion.py +++ b/tests/test_calculus_completion.py @@ -639,3 +639,42 @@ def f(x, _): slider.build(verbose=False) with pytest.raises(ValueError): slider.integrate(dims=[0], bounds=[(-2.0, 2.0)]) + + +# ====================================================================== +# v0.21 — Slider/TT roots, minimize, maximize +# ====================================================================== + +class TestSliderTo1DChebyshev: + """Slider._to_1d_chebyshev: build a 1-D ChebyshevApproximation from + a 1-D Slider via eval at Chebyshev nodes.""" + + def test_to_1d_chebyshev_recovers_function(self): + """1-D Slider built from f(x) = x^3, _to_1d_chebyshev returns + a 1-D Approximation with the same values.""" + def f(x, _): return x[0] ** 3 + slider = ChebyshevSlider( + f, num_dimensions=1, domain=[(-1, 1)], n_nodes=[7], + partition=[[0]], pivot_point=[0.0], + ) + slider.build(verbose=False) + cheb_1d = slider._to_1d_chebyshev(slider) + # Compare on a fine grid + for x in np.linspace(-0.9, 0.9, 11): + expected = x ** 3 + got = float(cheb_1d.eval([float(x)], derivative_order=[0])) + assert abs(got - expected) < 1e-10, f"x={x}: got {got}, expected {expected}" + + def test_to_1d_chebyshev_preserves_domain_and_n_nodes(self): + """The 1-D Approximation has the same domain and n_nodes as input.""" + def f(x, _): return x[0] + slider = ChebyshevSlider( + f, num_dimensions=1, domain=[(-2.5, 3.5)], n_nodes=[9], + partition=[[0]], pivot_point=[0.0], + ) + slider.build(verbose=False) + cheb_1d = slider._to_1d_chebyshev(slider) + assert cheb_1d.num_dimensions == 1 + assert cheb_1d.domain[0][0] == -2.5 + assert cheb_1d.domain[0][1] == 3.5 + assert cheb_1d.n_nodes[0] == 9 From 12f630ccea9c5a9990dab67152615181104a5e3b Mon Sep 17 00:00:00 2001 From: Max Zhang Date: Mon, 27 Apr 2026 10:23:34 -0400 Subject: [PATCH 02/17] =?UTF-8?q?v0.21:=20address=20Task=201=20review=20?= =?UTF-8?q?=E2=80=94=20use=20=5Fmake=5Fnodes=5Ffor=5Fdim=20helper?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Replace hand-rolled Chebyshev-node formula with the canonical _make_nodes_for_dim helper from _extrude_slice (single source of truth). - Drop redundant deferred import of ChebyshevApproximation (already imported at module level; no circular-import concern). - Add dimensionality assertion to catch caller misuse. --- src/pychebyshev/slider.py | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/src/pychebyshev/slider.py b/src/pychebyshev/slider.py index 4529aaf..9b64c7a 100644 --- a/src/pychebyshev/slider.py +++ b/src/pychebyshev/slider.py @@ -1154,14 +1154,16 @@ def _to_1d_chebyshev(self, sliced_1d): ChebyshevApproximation 1-D Chebyshev approximation matching *sliced_1d*'s values. """ - from pychebyshev.barycentric import ChebyshevApproximation + from pychebyshev._extrude_slice import _make_nodes_for_dim + + assert sliced_1d.num_dimensions == 1, ( + f"_to_1d_chebyshev expects a 1-D Slider, got {sliced_1d.num_dimensions}-D" + ) n = sliced_1d.n_nodes[0] a, b = sliced_1d.domain[0] # Type-I Chebyshev nodes in [a, b], ascending order - k = np.arange(n) - t = -np.cos((2.0 * k + 1.0) * np.pi / (2.0 * n)) - cheb_nodes = 0.5 * (a + b) + 0.5 * (b - a) * t + cheb_nodes = _make_nodes_for_dim(a, b, n) values = np.array([ float(sliced_1d.eval([float(x)], derivative_order=[0])) for x in cheb_nodes From 693a3bbbbc5535e2f8a4cafaabc46fb71b062b4b Mon Sep 17 00:00:00 2001 From: Max Zhang Date: Mon, 27 Apr 2026 10:27:51 -0400 Subject: [PATCH 03/17] v0.21: add ChebyshevSlider.roots() Mirrors ChebyshevApproximation.roots() signature exactly: 1-D direct, multi-D via fixed= dict. Implementation reduces to 1-D via slice() and delegates to a 1-D ChebyshevApproximation built via _to_1d_chebyshev(). Co-Authored-By: Claude Sonnet 4.6 --- src/pychebyshev/slider.py | 48 +++++++++++++++++ tests/test_calculus_completion.py | 85 +++++++++++++++++++++++++++++++ 2 files changed, 133 insertions(+) diff --git a/src/pychebyshev/slider.py b/src/pychebyshev/slider.py index 9b64c7a..9358fc2 100644 --- a/src/pychebyshev/slider.py +++ b/src/pychebyshev/slider.py @@ -1175,6 +1175,54 @@ def _to_1d_chebyshev(self, sliced_1d): n_nodes=[int(n)], ) + def roots(self, dim=None, fixed=None): + """Find all roots of the slider along a specified dimension. + + Reduces to a 1-D problem by slicing all other dimensions to + their fixed values, then delegates to + ``ChebyshevApproximation.roots()`` (which uses the colleague + matrix eigenvalue method, Good 1961). + + Parameters + ---------- + dim : int or None + Dimension along which to find roots. For 1-D sliders, + defaults to 0. + fixed : dict or None + For multi-D sliders, ``{dim_index: value}`` for **all** + dimensions except *dim*. + + Returns + ------- + ndarray + Sorted real root locations in the physical domain. + + Raises + ------ + RuntimeError + If ``build()`` has not been called. + ValueError + If *dim* / *fixed* validation fails or values are out of domain. + + References + ---------- + Good (1961), "The colleague matrix", Quarterly J. Math. 12(1):61–68. + Trefethen (2013), "Approximation Theory and Approximation Practice", + SIAM, Chapter 18. + """ + if not self._built: + raise RuntimeError("Call build() first") + + from pychebyshev._calculus import _validate_calculus_args + + dim, slice_params = _validate_calculus_args( + self.num_dimensions, dim, fixed, self.domain + ) + + sliced = self.slice(slice_params) if slice_params else self + cheb_1d = self._to_1d_chebyshev(sliced) + return cheb_1d.roots() + def _check_slider_compatible(self, other): """Validate that two sliders can be combined arithmetically.""" from pychebyshev._algebra import _check_compatible diff --git a/tests/test_calculus_completion.py b/tests/test_calculus_completion.py index ef5ffcc..27d2ae5 100644 --- a/tests/test_calculus_completion.py +++ b/tests/test_calculus_completion.py @@ -678,3 +678,88 @@ def f(x, _): return x[0] assert cheb_1d.domain[0][0] == -2.5 assert cheb_1d.domain[0][1] == 3.5 assert cheb_1d.n_nodes[0] == 9 + + +class TestSliderRoots: + """Tests for ChebyshevSlider.roots() — mirrors test_calculus.py::TestRootsApprox.""" + + def test_roots_quadratic_1d(self): + """1-D Slider: roots of x^2 - 0.25 on [-1,1] are {-0.5, 0.5}.""" + 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() + assert len(roots) == 2, f"Expected 2 roots, got {len(roots)}: {roots}" + for r, e in zip(roots, [-0.5, 0.5]): + assert abs(r - e) < 1e-10, f"Root {r} != {e}" + + def test_roots_no_roots_1d(self): + """1-D Slider: exp(x) on [0,1] has no roots.""" + def f(x, _): return math.exp(x[0]) + slider = ChebyshevSlider( + f, num_dimensions=1, domain=[(0, 1)], n_nodes=[10], + partition=[[0]], pivot_point=[0.5], + ) + slider.build(verbose=False) + roots = slider.roots() + assert len(roots) == 0, f"Expected no roots, got {roots}" + + def test_roots_2d_fixed(self): + """2-D Slider: f(x,y) = x - y, with y fixed at 0.3, root at x=0.3.""" + def f(x, _): return x[0] - x[1] + slider = ChebyshevSlider( + f, num_dimensions=2, domain=[(-1, 1), (-1, 1)], n_nodes=[5, 5], + partition=[[0], [1]], pivot_point=[0.0, 0.0], + ) + slider.build(verbose=False) + roots = slider.roots(dim=0, fixed={1: 0.3}) + assert len(roots) == 1, f"Expected 1 root, got {len(roots)}: {roots}" + assert abs(roots[0] - 0.3) < 1e-10, f"Root {roots[0]} != 0.3" + + def test_roots_3d_fixed(self): + """3-D Slider: f(x,y,z) = x*y - z, with y=2, z=0.5, root at x=0.25.""" + def f(x, _): return x[0] * x[1] - x[2] + slider = ChebyshevSlider( + f, num_dimensions=3, + domain=[(-1, 1), (1, 3), (-1, 1)], n_nodes=[7, 7, 7], + partition=[[0], [1], [2]], pivot_point=[0.0, 2.0, 0.0], + ) + slider.build(verbose=False) + roots = slider.roots(dim=0, fixed={1: 2.0, 2: 0.5}) + assert len(roots) == 1, f"Expected 1 root, got {len(roots)}: {roots}" + assert abs(roots[0] - 0.25) < 1e-8 + + def test_roots_missing_fixed_raises(self): + """Multi-D without full fixed dict raises ValueError.""" + def f(x, _): return x[0] + x[1] + slider = ChebyshevSlider( + f, num_dimensions=2, domain=[(-1, 1), (-1, 1)], n_nodes=[5, 5], + partition=[[0], [1]], pivot_point=[0.0, 0.0], + ) + slider.build(verbose=False) + with pytest.raises(ValueError): + slider.roots(dim=0) + + def test_roots_fixed_out_of_domain_raises(self): + """Fixed value outside domain raises ValueError.""" + def f(x, _): return x[0] + x[1] + slider = ChebyshevSlider( + f, num_dimensions=2, domain=[(-1, 1), (-1, 1)], n_nodes=[5, 5], + partition=[[0], [1]], pivot_point=[0.0, 0.0], + ) + slider.build(verbose=False) + with pytest.raises(ValueError): + slider.roots(dim=0, fixed={1: 5.0}) + + def test_roots_before_build_raises(self): + """roots() before build() raises RuntimeError.""" + def f(x, _): return x[0] + slider = ChebyshevSlider( + f, num_dimensions=1, domain=[(-1, 1)], n_nodes=[5], + partition=[[0]], pivot_point=[0.0], + ) + with pytest.raises(RuntimeError, match="build"): + slider.roots() From 71dcbc379f893bb63c13ded75042d564ed9a3d22 Mon Sep 17 00:00:00 2001 From: Max Zhang Date: Mon, 27 Apr 2026 10:34:23 -0400 Subject: [PATCH 04/17] v0.21: add ChebyshevSlider.minimize() Same delegation pattern as roots(): validate -> slice -> 1-D Approximation -> delegate to its minimize(). --- src/pychebyshev/slider.py | 40 +++++++++++++++++ tests/test_calculus_completion.py | 74 +++++++++++++++++++++++++++++++ 2 files changed, 114 insertions(+) diff --git a/src/pychebyshev/slider.py b/src/pychebyshev/slider.py index 9358fc2..7949cc1 100644 --- a/src/pychebyshev/slider.py +++ b/src/pychebyshev/slider.py @@ -1223,6 +1223,46 @@ def roots(self, dim=None, fixed=None): cheb_1d = self._to_1d_chebyshev(sliced) return cheb_1d.roots() + def minimize(self, dim=None, fixed=None): + """Find the minimum value of the slider along a dimension. + + Computes derivative roots to locate critical points, then + evaluates at all critical points and domain endpoints. For + multi-D sliders, all dimensions except the target must be + fixed. + + Parameters + ---------- + dim : int or None + Dimension along which to minimize. Defaults to 0 for 1-D. + fixed : dict or None + For multi-D, ``{dim_index: value}`` for all other dims. + + Returns + ------- + (value, location) : (float, float) + The minimum value and its coordinate in the target dimension. + + Raises + ------ + RuntimeError + If ``build()`` has not been called. + ValueError + If *dim* / *fixed* validation fails. + """ + if not self._built: + raise RuntimeError("Call build() first") + + from pychebyshev._calculus import _validate_calculus_args + + dim, slice_params = _validate_calculus_args( + self.num_dimensions, dim, fixed, self.domain + ) + + sliced = self.slice(slice_params) if slice_params else self + cheb_1d = self._to_1d_chebyshev(sliced) + return cheb_1d.minimize() + def _check_slider_compatible(self, other): """Validate that two sliders can be combined arithmetically.""" from pychebyshev._algebra import _check_compatible diff --git a/tests/test_calculus_completion.py b/tests/test_calculus_completion.py index 27d2ae5..599f53a 100644 --- a/tests/test_calculus_completion.py +++ b/tests/test_calculus_completion.py @@ -763,3 +763,77 @@ def f(x, _): return x[0] ) with pytest.raises(RuntimeError, match="build"): slider.roots() + + +# ============================================================================ +# TestSliderMinimize +# ============================================================================ + +class TestSliderMinimize: + """Tests for ChebyshevSlider.minimize() — mirrors test_calculus.py::TestMinMaxApprox.""" + + def test_minimize_quadratic_1d(self): + """1-D Slider: min of (x-0.3)^2 + 1 is 1.0 at x=0.3.""" + def f(x, _): return (x[0] - 0.3) ** 2 + 1.0 + slider = ChebyshevSlider( + f, num_dimensions=1, domain=[(-1, 1)], n_nodes=[10], + partition=[[0]], pivot_point=[0.0], + ) + slider.build(verbose=False) + val, loc = slider.minimize() + assert abs(val - 1.0) < 1e-10, f"min value {val} != 1.0" + assert abs(loc - 0.3) < 1e-10, f"min loc {loc} != 0.3" + + def test_minimize_constant(self): + """Constant 5 — min is 5, location is at left endpoint.""" + def f(x, _): return 5.0 + slider = ChebyshevSlider( + f, num_dimensions=1, domain=[(-2, 4)], n_nodes=[5], + partition=[[0]], pivot_point=[0.0], + ) + slider.build(verbose=False) + val, loc = slider.minimize() + assert abs(val - 5.0) < 1e-12 + + def test_minimize_2d_fixed(self): + """2-D Slider: min of (x-0.5)^2 + y at y=0 is 0 at x=0.5.""" + def f(x, _): return (x[0] - 0.5) ** 2 + x[1] + slider = ChebyshevSlider( + f, num_dimensions=2, domain=[(-1, 1), (-1, 1)], n_nodes=[8, 8], + partition=[[0], [1]], pivot_point=[0.0, 0.0], + ) + slider.build(verbose=False) + val, loc = slider.minimize(dim=0, fixed={1: 0.0}) + assert abs(val - 0.0) < 1e-9 + assert abs(loc - 0.5) < 1e-9 + + def test_minimize_endpoint(self): + """Linear x on [0,1] — min is 0 at x=0.""" + def f(x, _): return x[0] + slider = ChebyshevSlider( + f, num_dimensions=1, domain=[(0, 1)], n_nodes=[5], + partition=[[0]], pivot_point=[0.5], + ) + slider.build(verbose=False) + val, loc = slider.minimize() + assert abs(val - 0.0) < 1e-10 + assert abs(loc - 0.0) < 1e-10 + + def test_minimize_missing_fixed_raises(self): + def f(x, _): return x[0] + x[1] + slider = ChebyshevSlider( + f, num_dimensions=2, domain=[(-1, 1), (-1, 1)], n_nodes=[5, 5], + partition=[[0], [1]], pivot_point=[0.0, 0.0], + ) + slider.build(verbose=False) + with pytest.raises(ValueError): + slider.minimize(dim=0) + + def test_minimize_before_build_raises(self): + def f(x, _): return x[0] + slider = ChebyshevSlider( + f, num_dimensions=1, domain=[(-1, 1)], n_nodes=[5], + partition=[[0]], pivot_point=[0.0], + ) + with pytest.raises(RuntimeError, match="build"): + slider.minimize() From 0ef5f697596cab43c6412ac2dc96dbee4960db01 Mon Sep 17 00:00:00 2001 From: Max Zhang Date: Mon, 27 Apr 2026 10:35:58 -0400 Subject: [PATCH 05/17] v0.21: add ChebyshevSlider.maximize() Co-Authored-By: Claude Sonnet 4.6 --- src/pychebyshev/slider.py | 19 ++++++++ tests/test_calculus_completion.py | 74 +++++++++++++++++++++++++++++++ 2 files changed, 93 insertions(+) diff --git a/src/pychebyshev/slider.py b/src/pychebyshev/slider.py index 7949cc1..6848243 100644 --- a/src/pychebyshev/slider.py +++ b/src/pychebyshev/slider.py @@ -1263,6 +1263,25 @@ def minimize(self, dim=None, fixed=None): cheb_1d = self._to_1d_chebyshev(sliced) return cheb_1d.minimize() + def maximize(self, dim=None, fixed=None): + """Find the maximum value of the slider along a dimension. + + See ``minimize()`` for parameter details. Returns + ``(max_value, max_location)`` instead. + """ + if not self._built: + raise RuntimeError("Call build() first") + + from pychebyshev._calculus import _validate_calculus_args + + dim, slice_params = _validate_calculus_args( + self.num_dimensions, dim, fixed, self.domain + ) + + sliced = self.slice(slice_params) if slice_params else self + cheb_1d = self._to_1d_chebyshev(sliced) + return cheb_1d.maximize() + def _check_slider_compatible(self, other): """Validate that two sliders can be combined arithmetically.""" from pychebyshev._algebra import _check_compatible diff --git a/tests/test_calculus_completion.py b/tests/test_calculus_completion.py index 599f53a..d38b31f 100644 --- a/tests/test_calculus_completion.py +++ b/tests/test_calculus_completion.py @@ -837,3 +837,77 @@ def f(x, _): return x[0] ) with pytest.raises(RuntimeError, match="build"): slider.minimize() + + +# ============================================================================ +# TestSliderMaximize +# ============================================================================ + +class TestSliderMaximize: + """Tests for ChebyshevSlider.maximize().""" + + def test_maximize_quadratic_1d(self): + """1-D Slider: max of -(x-0.3)^2 + 5 is 5 at x=0.3.""" + def f(x, _): return -(x[0] - 0.3) ** 2 + 5.0 + slider = ChebyshevSlider( + f, num_dimensions=1, domain=[(-1, 1)], n_nodes=[10], + partition=[[0]], pivot_point=[0.0], + ) + slider.build(verbose=False) + val, loc = slider.maximize() + assert abs(val - 5.0) < 1e-10 + assert abs(loc - 0.3) < 1e-10 + + def test_maximize_constant(self): + """Constant 7 — max is 7.""" + def f(x, _): return 7.0 + slider = ChebyshevSlider( + f, num_dimensions=1, domain=[(-2, 4)], n_nodes=[5], + partition=[[0]], pivot_point=[0.0], + ) + slider.build(verbose=False) + val, _ = slider.maximize() + assert abs(val - 7.0) < 1e-12 + + def test_maximize_2d_fixed(self): + """2-D Slider: max of -(x-0.5)^2 + y at y=1 is 1 at x=0.5.""" + def f(x, _): return -(x[0] - 0.5) ** 2 + x[1] + slider = ChebyshevSlider( + f, num_dimensions=2, domain=[(-1, 1), (-1, 1)], n_nodes=[8, 8], + partition=[[0], [1]], pivot_point=[0.0, 0.0], + ) + slider.build(verbose=False) + val, loc = slider.maximize(dim=0, fixed={1: 1.0}) + assert abs(val - 1.0) < 1e-9 + assert abs(loc - 0.5) < 1e-9 + + def test_maximize_endpoint(self): + """Linear x on [0,1] — max is 1 at x=1.""" + def f(x, _): return x[0] + slider = ChebyshevSlider( + f, num_dimensions=1, domain=[(0, 1)], n_nodes=[5], + partition=[[0]], pivot_point=[0.5], + ) + slider.build(verbose=False) + val, loc = slider.maximize() + assert abs(val - 1.0) < 1e-10 + assert abs(loc - 1.0) < 1e-10 + + def test_maximize_missing_fixed_raises(self): + def f(x, _): return x[0] + x[1] + slider = ChebyshevSlider( + f, num_dimensions=2, domain=[(-1, 1), (-1, 1)], n_nodes=[5, 5], + partition=[[0], [1]], pivot_point=[0.0, 0.0], + ) + slider.build(verbose=False) + with pytest.raises(ValueError): + slider.maximize(dim=0) + + def test_maximize_before_build_raises(self): + def f(x, _): return x[0] + slider = ChebyshevSlider( + f, num_dimensions=1, domain=[(-1, 1)], n_nodes=[5], + partition=[[0]], pivot_point=[0.0], + ) + with pytest.raises(RuntimeError, match="build"): + slider.maximize() From 4bdf7a293399f0350ee4bf3736f8957519b27f59 Mon Sep 17 00:00:00 2001 From: Max Zhang Date: Mon, 27 Apr 2026 10:39:34 -0400 Subject: [PATCH 06/17] v0.21: cross-feature tests for Slider roots/min/max Verifies composition with extrude, slice, algebra, coupled partitions, and pickle round-trip. Co-Authored-By: Claude Sonnet 4.6 --- tests/test_calculus_completion.py | 85 +++++++++++++++++++++++++++++++ 1 file changed, 85 insertions(+) diff --git a/tests/test_calculus_completion.py b/tests/test_calculus_completion.py index d38b31f..993603a 100644 --- a/tests/test_calculus_completion.py +++ b/tests/test_calculus_completion.py @@ -911,3 +911,88 @@ def f(x, _): return x[0] ) with pytest.raises(RuntimeError, match="build"): slider.maximize() + + +# ============================================================================ +# TestSliderCalculusCrossFeature +# ============================================================================ + +class TestSliderCalculusCrossFeature: + """Cross-feature integration tests for Slider roots/min/max.""" + + def test_roots_after_extrude(self): + """Extrude a 1-D slider to 2-D, roots along original dim still work.""" + def f(x, _): return x[0] ** 2 - 0.25 + slider_1d = ChebyshevSlider( + f, num_dimensions=1, domain=[(-1, 1)], n_nodes=[10], + partition=[[0]], pivot_point=[0.0], + ) + slider_1d.build(verbose=False) + slider_2d = slider_1d.extrude((1, (-1.0, 1.0), 5)) + # After extrude, dim 0 still has the quadratic structure. + roots = slider_2d.roots(dim=0, fixed={1: 0.5}) + assert len(roots) == 2 + for r, e in zip(roots, [-0.5, 0.5]): + assert abs(r - e) < 1e-9 + + def test_minimize_after_slice(self): + """Slice a 3-D slider down to 2-D, then minimize along surviving dim.""" + def f(x, _): return (x[0] - 0.2) ** 2 + x[1] ** 2 + x[2] + slider_3d = ChebyshevSlider( + f, num_dimensions=3, domain=[(-1, 1)] * 3, n_nodes=[7, 7, 7], + partition=[[0], [1], [2]], pivot_point=[0.0, 0.0, 0.0], + ) + slider_3d.build(verbose=False) + slider_2d = slider_3d.slice([(2, 0.5)]) + val, loc = slider_2d.minimize(dim=0, fixed={1: 0.0}) + # f(0.2, 0, 0.5) = 0 + 0 + 0.5 = 0.5 + assert abs(val - 0.5) < 1e-9 + assert abs(loc - 0.2) < 1e-9 + + def test_maximize_after_algebra(self): + """Slider built via algebra (no .function) — maximize still works.""" + def f(x, _): return x[0] + def g(x, _): return -x[0] + s_f = ChebyshevSlider( + f, num_dimensions=1, domain=[[-1, 1]], n_nodes=[5], + partition=[[0]], pivot_point=[0.0], + ) + s_g = ChebyshevSlider( + g, num_dimensions=1, domain=[[-1, 1]], n_nodes=[5], + partition=[[0]], pivot_point=[0.0], + ) + s_f.build(verbose=False) + s_g.build(verbose=False) + # h(x) = f(x) + 2*g(x) = x - 2x = -x; max on [-1,1] is 1 at x=-1 + h = s_f + 2.0 * s_g + val, loc = h.maximize() + assert abs(val - 1.0) < 1e-10 + assert abs(loc - (-1.0)) < 1e-10 + + def test_roots_2d_coupled_partition(self): + """Slider with 2-D coupled group [[0,1]] — roots in dim 0 with dim 1 fixed.""" + def f(x, _): return x[0] * x[1] - 0.1 + slider = ChebyshevSlider( + f, num_dimensions=2, domain=[(-1, 1), (0.5, 1.5)], n_nodes=[6, 6], + partition=[[0, 1]], pivot_point=[0.0, 1.0], + ) + slider.build(verbose=False) + # f(x, 0.5) = 0.5*x - 0.1 = 0 at x = 0.2 + roots = slider.roots(dim=0, fixed={1: 0.5}) + assert len(roots) == 1 + assert abs(roots[0] - 0.2) < 1e-9 + + def test_save_load_round_trip_preserves_roots(self): + """Pickle round-trip preserves roots().""" + import pickle + 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_before = slider.roots() + data = pickle.dumps(slider) + slider_loaded = pickle.loads(data) + roots_after = slider_loaded.roots() + np.testing.assert_array_almost_equal(roots_before, roots_after, decimal=12) From 10df9bc04faa351693d59e17c663b1c27d3537c7 Mon Sep 17 00:00:00 2001 From: Max Zhang Date: Mon, 27 Apr 2026 10:42:02 -0400 Subject: [PATCH 07/17] v0.21: add ChebyshevTT._to_1d_chebyshev private helper Builds a 1-D ChebyshevApproximation from a 1-D TT via to_dense(). Foundation for v0.21 roots/minimize/maximize on ChebyshevTT. --- src/pychebyshev/tensor_train.py | 33 +++++++++++++++++++++++++++++++ tests/test_calculus_completion.py | 32 ++++++++++++++++++++++++++++++ 2 files changed, 65 insertions(+) diff --git a/src/pychebyshev/tensor_train.py b/src/pychebyshev/tensor_train.py index 1c062e7..a242b22 100644 --- a/src/pychebyshev/tensor_train.py +++ b/src/pychebyshev/tensor_train.py @@ -1687,6 +1687,39 @@ def integrate( result_tt._dim_order = new_dim_order return result_tt + def _to_1d_chebyshev(self, sliced_1d): + """Build a 1-D ChebyshevApproximation from a 1-D ChebyshevTT. + + Uses ``to_dense()`` to extract the values vector, then constructs + a ChebyshevApproximation via ``from_values``. ``to_dense()`` + already applies the inverse permutation so values are in user + frame. + + Parameters + ---------- + sliced_1d : ChebyshevTT + A 1-D TT (``num_dimensions == 1``). + + Returns + ------- + ChebyshevApproximation + 1-D Chebyshev approximation matching *sliced_1d*'s values. + """ + from pychebyshev.barycentric import ChebyshevApproximation + + assert sliced_1d.num_dimensions == 1, ( + f"_to_1d_chebyshev expects a 1-D TT, got {sliced_1d.num_dimensions}-D" + ) + + values = np.asarray(sliced_1d.to_dense(), dtype=float).reshape(-1) + a, b = sliced_1d.domain[0] + return ChebyshevApproximation.from_values( + values, + num_dimensions=1, + domain=[(float(a), float(b))], + n_nodes=[int(sliced_1d.n_nodes[0])], + ) + def to_dense(self) -> np.ndarray: """Materialize the TT chain into a full N-D tensor of values. diff --git a/tests/test_calculus_completion.py b/tests/test_calculus_completion.py index 993603a..b571719 100644 --- a/tests/test_calculus_completion.py +++ b/tests/test_calculus_completion.py @@ -996,3 +996,35 @@ def f(x, _): return x[0] ** 2 - 0.25 slider_loaded = pickle.loads(data) roots_after = slider_loaded.roots() np.testing.assert_array_almost_equal(roots_before, roots_after, decimal=12) + + +class TestTTTo1DChebyshev: + """TT._to_1d_chebyshev: build a 1-D ChebyshevApproximation from + a 1-D TT via to_dense().""" + + def test_to_1d_chebyshev_recovers_function(self): + """1-D TT built from f(x) = x^3, _to_1d_chebyshev returns + a 1-D Approximation with the same values.""" + def f(x, _): return x[0] ** 3 + tt = ChebyshevTT( + f, num_dimensions=1, domain=[(-1, 1)], n_nodes=[7], + ) + tt.build(verbose=False) + cheb_1d = tt._to_1d_chebyshev(tt) + for x in np.linspace(-0.9, 0.9, 11): + expected = x ** 3 + got = float(cheb_1d.eval([float(x)], derivative_order=[0])) + assert abs(got - expected) < 1e-9, f"x={x}: got {got}, expected {expected}" + + def test_to_1d_chebyshev_preserves_domain_and_n_nodes(self): + """The 1-D Approximation has the same domain and n_nodes as input.""" + def f(x, _): return x[0] + tt = ChebyshevTT( + f, num_dimensions=1, domain=[(-2.5, 3.5)], n_nodes=[9], + ) + tt.build(verbose=False) + cheb_1d = tt._to_1d_chebyshev(tt) + assert cheb_1d.num_dimensions == 1 + assert abs(cheb_1d.domain[0][0] - (-2.5)) < 1e-12 + assert abs(cheb_1d.domain[0][1] - 3.5) < 1e-12 + assert cheb_1d.n_nodes[0] == 9 From 49bdb41075160e742e102f04b31822aebc34730d Mon Sep 17 00:00:00 2001 From: Max Zhang Date: Mon, 27 Apr 2026 10:44:12 -0400 Subject: [PATCH 08/17] v0.21: add ChebyshevTT.roots() Same delegation pattern as Slider: validate -> slice -> 1-D Approximation -> delegate to its roots(). User-frame dim/fixed translate to storage frame transparently via slice()/to_dense(). Co-Authored-By: Claude Sonnet 4.6 --- src/pychebyshev/tensor_train.py | 43 +++++++++++++++++++++ tests/test_calculus_completion.py | 63 +++++++++++++++++++++++++++++++ 2 files changed, 106 insertions(+) diff --git a/src/pychebyshev/tensor_train.py b/src/pychebyshev/tensor_train.py index a242b22..1f693a5 100644 --- a/src/pychebyshev/tensor_train.py +++ b/src/pychebyshev/tensor_train.py @@ -1720,6 +1720,49 @@ def _to_1d_chebyshev(self, sliced_1d): n_nodes=[int(sliced_1d.n_nodes[0])], ) + def roots(self, dim=None, fixed=None): + """Find all roots of the TT-approximated function along *dim*. + + Reduces to a 1-D problem by slicing all other dimensions to + their fixed values, then delegates to + ``ChebyshevApproximation.roots()``. The user-frame dim/fixed + keys translate to storage frame transparently inside + ``self.slice()`` and ``self.to_dense()`` per v0.20.1 frame + discipline. + + Parameters + ---------- + dim : int or None + User-frame dimension index. Defaults to 0 for 1-D. + fixed : dict or None + ``{dim_index: value}`` for all dimensions except *dim*. + User-frame indices. + + Returns + ------- + ndarray + Sorted real root locations in the physical domain. + + Raises + ------ + RuntimeError + If ``build()`` has not been called. + ValueError + If validation fails or values are out of domain. + """ + if not self._built: + raise RuntimeError("Call build() first") + + from pychebyshev._calculus import _validate_calculus_args + + dim, slice_params = _validate_calculus_args( + self.num_dimensions, dim, fixed, self.domain + ) + + sliced = self.slice(slice_params) if slice_params else self + cheb_1d = self._to_1d_chebyshev(sliced) + return cheb_1d.roots() + def to_dense(self) -> np.ndarray: """Materialize the TT chain into a full N-D tensor of values. diff --git a/tests/test_calculus_completion.py b/tests/test_calculus_completion.py index b571719..d2ad242 100644 --- a/tests/test_calculus_completion.py +++ b/tests/test_calculus_completion.py @@ -1028,3 +1028,66 @@ def f(x, _): return x[0] assert abs(cheb_1d.domain[0][0] - (-2.5)) < 1e-12 assert abs(cheb_1d.domain[0][1] - 3.5) < 1e-12 assert cheb_1d.n_nodes[0] == 9 + + +class TestTTRoots: + """Tests for ChebyshevTT.roots().""" + + def test_roots_quadratic_1d(self): + """1-D TT: roots of x^2 - 0.25 on [-1,1] are {-0.5, 0.5}.""" + 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() + assert len(roots) == 2 + for r, e in zip(roots, [-0.5, 0.5]): + assert abs(r - e) < 1e-9 + + def test_roots_no_roots_1d(self): + """1-D TT: exp(x) on [0,1] has no roots.""" + def f(x, _): return math.exp(x[0]) + tt = ChebyshevTT(f, num_dimensions=1, domain=[(0, 1)], n_nodes=[10]) + tt.build(verbose=False) + roots = tt.roots() + assert len(roots) == 0 + + def test_roots_2d_fixed(self): + """2-D TT: f(x,y) = x - y, with y fixed at 0.3, root at x=0.3.""" + def f(x, _): return x[0] - x[1] + tt = ChebyshevTT(f, num_dimensions=2, domain=[(-1, 1), (-1, 1)], n_nodes=[5, 5]) + tt.build(verbose=False) + roots = tt.roots(dim=0, fixed={1: 0.3}) + assert len(roots) == 1 + assert abs(roots[0] - 0.3) < 1e-9 + + def test_roots_3d_fixed(self): + """3-D TT: f(x,y,z) = x*y - z, with y=2, z=0.5, root at x=0.25.""" + def f(x, _): return x[0] * x[1] - x[2] + tt = ChebyshevTT( + f, num_dimensions=3, + domain=[(-1, 1), (1, 3), (-1, 1)], n_nodes=[7, 7, 7], + ) + tt.build(verbose=False) + roots = tt.roots(dim=0, fixed={1: 2.0, 2: 0.5}) + assert len(roots) == 1 + assert abs(roots[0] - 0.25) < 1e-8 + + def test_roots_missing_fixed_raises(self): + def f(x, _): return x[0] + x[1] + tt = ChebyshevTT(f, num_dimensions=2, domain=[(-1, 1), (-1, 1)], n_nodes=[5, 5]) + tt.build(verbose=False) + with pytest.raises(ValueError): + tt.roots(dim=0) + + def test_roots_fixed_out_of_domain_raises(self): + def f(x, _): return x[0] + x[1] + tt = ChebyshevTT(f, num_dimensions=2, domain=[(-1, 1), (-1, 1)], n_nodes=[5, 5]) + tt.build(verbose=False) + with pytest.raises(ValueError): + tt.roots(dim=0, fixed={1: 5.0}) + + def test_roots_before_build_raises(self): + def f(x, _): return x[0] + tt = ChebyshevTT(f, num_dimensions=1, domain=[(-1, 1)], n_nodes=[5]) + with pytest.raises(RuntimeError, match="build"): + tt.roots() From d8af996d965e91544b8da2b43050a3f595ca9b9e Mon Sep 17 00:00:00 2001 From: Max Zhang Date: Mon, 27 Apr 2026 10:46:12 -0400 Subject: [PATCH 09/17] v0.21: add ChebyshevTT.minimize() Structural mirror of ChebyshevTT.roots(): slices to 1-D via _validate_calculus_args + self.slice(), then delegates to ChebyshevApproximation.minimize(). Six tests in TestTTMinimize. Co-Authored-By: Claude Sonnet 4.6 --- src/pychebyshev/tensor_train.py | 82 +++++++++++++++++++++++++++++++ tests/test_calculus_completion.py | 53 ++++++++++++++++++++ 2 files changed, 135 insertions(+) diff --git a/src/pychebyshev/tensor_train.py b/src/pychebyshev/tensor_train.py index 1f693a5..c6f0a59 100644 --- a/src/pychebyshev/tensor_train.py +++ b/src/pychebyshev/tensor_train.py @@ -1763,6 +1763,88 @@ def roots(self, dim=None, fixed=None): cheb_1d = self._to_1d_chebyshev(sliced) return cheb_1d.roots() + def minimize(self, dim=None, fixed=None): + """Find the minimum value of the TT along a dimension. + + Reduces to a 1-D problem by slicing all other dimensions to + their fixed values, then delegates to + ``ChebyshevApproximation.minimize()``. See ``roots()`` for + parameter details. + + Parameters + ---------- + dim : int or None + User-frame dimension index. Defaults to 0 for 1-D. + fixed : dict or None + ``{dim_index: value}`` for all dimensions except *dim*. + User-frame indices. + + Returns + ------- + (value, location) : (float, float) + The minimum value and its location in the physical domain. + + Raises + ------ + RuntimeError + If ``build()`` has not been called. + ValueError + If validation fails or values are out of domain. + """ + if not self._built: + raise RuntimeError("Call build() first") + + from pychebyshev._calculus import _validate_calculus_args + + dim, slice_params = _validate_calculus_args( + self.num_dimensions, dim, fixed, self.domain + ) + + sliced = self.slice(slice_params) if slice_params else self + cheb_1d = self._to_1d_chebyshev(sliced) + return cheb_1d.minimize() + + def maximize(self, dim=None, fixed=None): + """Find the maximum value of the TT along a dimension. + + Reduces to a 1-D problem by slicing all other dimensions to + their fixed values, then delegates to + ``ChebyshevApproximation.maximize()``. See ``minimize()`` for + parameter details. + + Parameters + ---------- + dim : int or None + User-frame dimension index. Defaults to 0 for 1-D. + fixed : dict or None + ``{dim_index: value}`` for all dimensions except *dim*. + User-frame indices. + + Returns + ------- + (value, location) : (float, float) + The maximum value and its location in the physical domain. + + Raises + ------ + RuntimeError + If ``build()`` has not been called. + ValueError + If validation fails or values are out of domain. + """ + if not self._built: + raise RuntimeError("Call build() first") + + from pychebyshev._calculus import _validate_calculus_args + + dim, slice_params = _validate_calculus_args( + self.num_dimensions, dim, fixed, self.domain + ) + + sliced = self.slice(slice_params) if slice_params else self + cheb_1d = self._to_1d_chebyshev(sliced) + return cheb_1d.maximize() + def to_dense(self) -> np.ndarray: """Materialize the TT chain into a full N-D tensor of values. diff --git a/tests/test_calculus_completion.py b/tests/test_calculus_completion.py index d2ad242..e91feb8 100644 --- a/tests/test_calculus_completion.py +++ b/tests/test_calculus_completion.py @@ -1091,3 +1091,56 @@ def f(x, _): return x[0] tt = ChebyshevTT(f, num_dimensions=1, domain=[(-1, 1)], n_nodes=[5]) with pytest.raises(RuntimeError, match="build"): tt.roots() + + +# ============================================================================ +# TestTTMinimize +# ============================================================================ + +class TestTTMinimize: + """Tests for ChebyshevTT.minimize().""" + + def test_minimize_quadratic_1d(self): + 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(verbose=False) + val, loc = tt.minimize() + assert abs(val - 1.0) < 1e-10 + assert abs(loc - 0.3) < 1e-10 + + def test_minimize_constant(self): + def f(x, _): return 5.0 + tt = ChebyshevTT(f, num_dimensions=1, domain=[(-2, 4)], n_nodes=[5]) + tt.build(verbose=False) + val, _ = tt.minimize() + assert abs(val - 5.0) < 1e-10 + + def test_minimize_2d_fixed(self): + def f(x, _): return (x[0] - 0.5) ** 2 + x[1] + tt = ChebyshevTT(f, num_dimensions=2, domain=[(-1, 1), (-1, 1)], n_nodes=[8, 8]) + tt.build(verbose=False) + val, loc = tt.minimize(dim=0, fixed={1: 0.0}) + assert abs(val - 0.0) < 1e-9 + assert abs(loc - 0.5) < 1e-9 + + def test_minimize_endpoint(self): + """Linear x on [0,1] — min at x=0.""" + def f(x, _): return x[0] + tt = ChebyshevTT(f, num_dimensions=1, domain=[(0, 1)], n_nodes=[5]) + tt.build(verbose=False) + val, loc = tt.minimize() + assert abs(val - 0.0) < 1e-9 + assert abs(loc - 0.0) < 1e-9 + + def test_minimize_missing_fixed_raises(self): + def f(x, _): return x[0] + x[1] + tt = ChebyshevTT(f, num_dimensions=2, domain=[(-1, 1), (-1, 1)], n_nodes=[5, 5]) + tt.build(verbose=False) + with pytest.raises(ValueError): + tt.minimize(dim=0) + + def test_minimize_before_build_raises(self): + def f(x, _): return x[0] + tt = ChebyshevTT(f, num_dimensions=1, domain=[(-1, 1)], n_nodes=[5]) + with pytest.raises(RuntimeError, match="build"): + tt.minimize() From 15d26656d5f94da0a7ec519223f384f37e0b9ca0 Mon Sep 17 00:00:00 2001 From: Max Zhang Date: Mon, 27 Apr 2026 10:46:35 -0400 Subject: [PATCH 10/17] v0.21: add ChebyshevTT.maximize() Structural mirror of ChebyshevTT.minimize(): slices to 1-D via _validate_calculus_args + self.slice(), then delegates to ChebyshevApproximation.maximize(). Six tests in TestTTMaximize. Co-Authored-By: Claude Sonnet 4.6 --- tests/test_calculus_completion.py | 52 +++++++++++++++++++++++++++++++ 1 file changed, 52 insertions(+) diff --git a/tests/test_calculus_completion.py b/tests/test_calculus_completion.py index e91feb8..b35f15f 100644 --- a/tests/test_calculus_completion.py +++ b/tests/test_calculus_completion.py @@ -1144,3 +1144,55 @@ def f(x, _): return x[0] tt = ChebyshevTT(f, num_dimensions=1, domain=[(-1, 1)], n_nodes=[5]) with pytest.raises(RuntimeError, match="build"): tt.minimize() + + +# ============================================================================ +# TestTTMaximize +# ============================================================================ + +class TestTTMaximize: + """Tests for ChebyshevTT.maximize().""" + + def test_maximize_quadratic_1d(self): + def f(x, _): return -(x[0] - 0.3) ** 2 + 5.0 + tt = ChebyshevTT(f, num_dimensions=1, domain=[(-1, 1)], n_nodes=[10]) + tt.build(verbose=False) + val, loc = tt.maximize() + assert abs(val - 5.0) < 1e-10 + assert abs(loc - 0.3) < 1e-10 + + def test_maximize_constant(self): + def f(x, _): return 7.0 + tt = ChebyshevTT(f, num_dimensions=1, domain=[(-2, 4)], n_nodes=[5]) + tt.build(verbose=False) + val, _ = tt.maximize() + assert abs(val - 7.0) < 1e-10 + + def test_maximize_2d_fixed(self): + def f(x, _): return -(x[0] - 0.5) ** 2 + x[1] + tt = ChebyshevTT(f, num_dimensions=2, domain=[(-1, 1), (-1, 1)], n_nodes=[8, 8]) + tt.build(verbose=False) + val, loc = tt.maximize(dim=0, fixed={1: 1.0}) + assert abs(val - 1.0) < 1e-9 + assert abs(loc - 0.5) < 1e-9 + + def test_maximize_endpoint(self): + def f(x, _): return x[0] + tt = ChebyshevTT(f, num_dimensions=1, domain=[(0, 1)], n_nodes=[5]) + tt.build(verbose=False) + val, loc = tt.maximize() + assert abs(val - 1.0) < 1e-9 + assert abs(loc - 1.0) < 1e-9 + + def test_maximize_missing_fixed_raises(self): + def f(x, _): return x[0] + x[1] + tt = ChebyshevTT(f, num_dimensions=2, domain=[(-1, 1), (-1, 1)], n_nodes=[5, 5]) + tt.build(verbose=False) + with pytest.raises(ValueError): + tt.maximize(dim=0) + + def test_maximize_before_build_raises(self): + def f(x, _): return x[0] + tt = ChebyshevTT(f, num_dimensions=1, domain=[(-1, 1)], n_nodes=[5]) + with pytest.raises(RuntimeError, match="build"): + tt.maximize() From a92dd2b999b920aa08215b57e5b8bd89176ad1a4 Mon Sep 17 00:00:00 2001 From: Max Zhang Date: Mon, 27 Apr 2026 10:49:52 -0400 Subject: [PATCH 11/17] v0.21: cross-feature tests for TT roots/min/max Verifies frame discipline under with_auto_order/reorder, composition with extrude/slice/algebra, and pickle round-trip. Co-Authored-By: Claude Sonnet 4.6 --- tests/test_calculus_completion.py | 85 +++++++++++++++++++++++++++++++ 1 file changed, 85 insertions(+) diff --git a/tests/test_calculus_completion.py b/tests/test_calculus_completion.py index b35f15f..fb61902 100644 --- a/tests/test_calculus_completion.py +++ b/tests/test_calculus_completion.py @@ -1196,3 +1196,88 @@ def f(x, _): return x[0] tt = ChebyshevTT(f, num_dimensions=1, domain=[(-1, 1)], n_nodes=[5]) with pytest.raises(RuntimeError, match="build"): tt.maximize() + + +# ============================================================================ +# TestTTCalculusCrossFeature +# ============================================================================ + +class TestTTCalculusCrossFeature: + """Cross-feature integration tests for TT roots/min/max.""" + + def test_roots_after_with_auto_order(self): + """with_auto_order builds a TT with potentially non-identity _dim_order; + roots in user-frame must still find the correct values.""" + # Function with known root structure along dim 0 + def f(x, _): return (x[0] - 0.4) * (1.0 + 0.1 * x[1] + 0.2 * x[2]) + tt = ChebyshevTT.with_auto_order( + f, num_dimensions=3, domain=[(-1, 1)] * 3, n_nodes=[10, 10, 10], + n_trials=3, + ) + # Whatever the storage order, user-frame dim=0 should yield root at 0.4 + roots = tt.roots(dim=0, fixed={1: 0.0, 2: 0.0}) + assert len(roots) == 1 + assert abs(roots[0] - 0.4) < 1e-8 + + def test_minimize_after_reorder(self): + """Explicit reorder([2, 0, 1]) preserves user-frame minimize results.""" + 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) + tt_reordered = tt.reorder([2, 0, 1]) + val, loc = tt_reordered.minimize(dim=0, fixed={1: 0.0, 2: 0.0}) + # f(0.2, 0, 0) = 0 + assert abs(val - 0.0) < 1e-7 + assert abs(loc - 0.2) < 1e-7 + + def test_maximize_after_extrude(self): + """Extrude a 1-D TT to 2-D; max along original dim still works.""" + def f(x, _): return -(x[0] - 0.3) ** 2 + 5.0 + tt_1d = ChebyshevTT(f, num_dimensions=1, domain=[(-1, 1)], n_nodes=[10]) + tt_1d.build(verbose=False) + tt_2d = tt_1d.extrude([(1, (-1.0, 1.0), 5)]) + val, loc = tt_2d.maximize(dim=0, fixed={1: 0.5}) + assert abs(val - 5.0) < 1e-9 + assert abs(loc - 0.3) < 1e-9 + + def test_roots_after_slice(self): + """Slice a 3-D TT to 2-D, then roots along surviving dim.""" + def f(x, _): return x[0] * x[1] - 0.1 + 0.0 * x[2] + tt = ChebyshevTT( + f, num_dimensions=3, domain=[(-1, 1), (0.5, 1.5), (-1, 1)], + n_nodes=[7, 7, 7], + ) + tt.build(verbose=False) + tt_2d = tt.slice([(2, 0.0)]) + # f(x, 0.5, 0) = 0.5*x - 0.1 = 0 at x = 0.2 + roots = tt_2d.roots(dim=0, fixed={1: 0.5}) + assert len(roots) == 1 + assert abs(roots[0] - 0.2) < 1e-8 + + def test_minimize_after_algebra(self): + """TT built via algebra (e.g. tt + 2*other) — minimize still works.""" + def f(x, _): return x[0] + def g(x, _): return -x[0] + tf = ChebyshevTT(f, num_dimensions=1, domain=[(-1, 1)], n_nodes=[5]) + tg = ChebyshevTT(g, num_dimensions=1, domain=[(-1, 1)], n_nodes=[5]) + tf.build(verbose=False) + tg.build(verbose=False) + # h(x) = f(x) + 2*g(x) = x - 2x = -x; min on [-1,1] is -1 at x=1 + h = tf + 2.0 * tg + val, loc = h.minimize() + assert abs(val - (-1.0)) < 1e-10 + assert abs(loc - 1.0) < 1e-10 + + def test_save_load_round_trip_preserves_roots(self): + """Pickle round-trip preserves TT.roots().""" + import pickle + 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_before = tt.roots() + data = pickle.dumps(tt) + tt_loaded = pickle.loads(data) + roots_after = tt_loaded.roots() + np.testing.assert_array_almost_equal(roots_before, roots_after, decimal=10) From 534fe949fefa0c18a8c391301df8f2106392ff36 Mon Sep 17 00:00:00 2001 From: Max Zhang Date: Mon, 27 Apr 2026 10:51:06 -0400 Subject: [PATCH 12/17] v0.21: cross-class consistency tests for new calculus methods Verifies Slider and TT roots/min/max agree with ChebyshevApproximation on the same function to ~1e-9. --- tests/test_calculus_completion.py | 61 +++++++++++++++++++++++++++++++ 1 file changed, 61 insertions(+) diff --git a/tests/test_calculus_completion.py b/tests/test_calculus_completion.py index fb61902..c2af0e1 100644 --- a/tests/test_calculus_completion.py +++ b/tests/test_calculus_completion.py @@ -1281,3 +1281,64 @@ def f(x, _): return x[0] ** 2 - 0.25 tt_loaded = pickle.loads(data) roots_after = tt_loaded.roots() np.testing.assert_array_almost_equal(roots_before, roots_after, decimal=10) + + +# ============================================================================ +# TestCrossClassCalculusConsistency +# ============================================================================ + +class TestCrossClassCalculusConsistency: + """Slider/TT roots/min/max must match ChebyshevApproximation on the + same function to ~1e-9.""" + + def test_slider_matches_approx_roots(self): + """Same quadratic, same nodes — Slider.roots == Approx.roots.""" + def f(x, _): return x[0] ** 2 - 0.25 + cheb = ChebyshevApproximation(f, 1, [(-1, 1)], [10]) + slider = ChebyshevSlider( + f, num_dimensions=1, domain=[(-1, 1)], n_nodes=[10], + partition=[[0]], pivot_point=[0.0], + ) + cheb.build(verbose=False) + slider.build(verbose=False) + np.testing.assert_array_almost_equal( + sorted(cheb.roots()), sorted(slider.roots()), decimal=10 + ) + + def test_tt_matches_approx_roots(self): + """Same quadratic — TT.roots == Approx.roots.""" + def f(x, _): return x[0] ** 2 - 0.25 + cheb = ChebyshevApproximation(f, 1, [(-1, 1)], [10]) + tt = ChebyshevTT(f, num_dimensions=1, domain=[(-1, 1)], n_nodes=[10]) + cheb.build(verbose=False) + tt.build(verbose=False) + np.testing.assert_array_almost_equal( + sorted(cheb.roots()), sorted(tt.roots()), decimal=9 + ) + + def test_slider_matches_approx_minmax_2d(self): + """2-D function — Slider min/max == Approx min/max with same fixed.""" + 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], + ) + cheb.build(verbose=False) + slider.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}) + assert abs(v_c - v_s) < 1e-9 + assert abs(l_c - l_s) < 1e-9 + + def test_tt_matches_approx_minmax_3d(self): + """3-D — TT min/max == Approx min/max with same fixed.""" + def f(x, _): return (x[0] - 0.3) ** 2 + (x[1] + 0.1) ** 2 + x[2] + cheb = ChebyshevApproximation(f, 3, [(-1, 1)] * 3, [7, 7, 7]) + tt = ChebyshevTT(f, num_dimensions=3, domain=[(-1, 1)] * 3, n_nodes=[7, 7, 7]) + cheb.build(verbose=False) + tt.build(verbose=False) + v_c, l_c = cheb.maximize(dim=0, fixed={1: 0.0, 2: 0.5}) + v_t, l_t = tt.maximize(dim=0, fixed={1: 0.0, 2: 0.5}) + assert abs(v_c - v_t) < 1e-7 + assert abs(l_c - l_t) < 1e-7 From e3a48881cd22be35a8cd4f00cf54c53479becc4e Mon Sep 17 00:00:00 2001 From: Max Zhang Date: Mon, 27 Apr 2026 10:54:01 -0400 Subject: [PATCH 13/17] v0.21: add comparison/demo script Self-contained demo of new Slider/TT calculus methods against analytical answers. No MoCaX equivalent -- beyond-MoCaX feature. Co-Authored-By: Claude Sonnet 4.6 --- compare_v021_slider_tt_calculus.py | 119 +++++++++++++++++++++++++++++ 1 file changed, 119 insertions(+) create mode 100644 compare_v021_slider_tt_calculus.py diff --git a/compare_v021_slider_tt_calculus.py b/compare_v021_slider_tt_calculus.py new file mode 100644 index 0000000..4db97d5 --- /dev/null +++ b/compare_v021_slider_tt_calculus.py @@ -0,0 +1,119 @@ +"""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_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_cross_class_consistency() + print(f"\nTotal: {time.time() - t0:.2f}s") + + +if __name__ == "__main__": + main() From caec3ef3ab31ec739ae3ee8a76723c23b88b9608 Mon Sep 17 00:00:00 2001 From: Max Zhang Date: Mon, 27 Apr 2026 10:55:34 -0400 Subject: [PATCH 14/17] =?UTF-8?q?v0.21:=20docs=20=E2=80=94=20extend=20calc?= =?UTF-8?q?ulus=20user=20guide=20with=20Slider/TT=20examples?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Sonnet 4.6 --- docs/user-guide/calculus.md | 88 +++++++++++++++++++++++++++++++++++++ 1 file changed, 88 insertions(+) diff --git a/docs/user-guide/calculus.md b/docs/user-guide/calculus.md index 95691f0..630e085 100644 --- a/docs/user-guide/calculus.md +++ b/docs/user-guide/calculus.md @@ -507,3 +507,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`. From 48fb6447992590560787730ce91e801b627abb23 Mon Sep 17 00:00:00 2001 From: Max Zhang Date: Mon, 27 Apr 2026 10:59:35 -0400 Subject: [PATCH 15/17] v0.21.0: bump version + release housekeeping MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Bump to 0.21.0. Drop the 'deferred to v0.21' note from v0.17. Add v0.21 architecture line. Update test count (1055 → 1112). Closes the calculus parity gap promised since v0.17 — all four public classes now support integrate + roots + min/max. Co-Authored-By: Claude Sonnet 4.6 --- CHANGELOG.md | 23 +++++++++++++++++++++++ CLAUDE.md | 16 +++++++++++----- pyproject.toml | 2 +- src/pychebyshev/_version.py | 2 +- 4 files changed, 36 insertions(+), 7 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index ee0a0da..02e2e35 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 diff --git a/CLAUDE.md b/CLAUDE.md index 30e20e5..454801a 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -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 @@ -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()`. @@ -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) @@ -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: ~64 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. diff --git a/pyproject.toml b/pyproject.toml index ce781d9..c51f9bf 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -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"} diff --git a/src/pychebyshev/_version.py b/src/pychebyshev/_version.py index ac82404..6a726d8 100644 --- a/src/pychebyshev/_version.py +++ b/src/pychebyshev/_version.py @@ -1 +1 @@ -__version__ = "0.20.1" +__version__ = "0.21.0" From ba048f4f50b4088eda65f14a27451cdf8be5772d Mon Sep 17 00:00:00 2001 From: Max Zhang Date: Mon, 27 Apr 2026 11:11:40 -0400 Subject: [PATCH 16/17] =?UTF-8?q?v0.21:=20address=20final=20review=20?= =?UTF-8?q?=E2=80=94=20drop=20pre-v0.21=20docs=20contradictions?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The Task 13 docs addition appended a v0.21 section but didn't update the upfront 'Supported Classes' table or the 'Limitations' bullet that both still claimed Slider/TT lacked roots/min/max. Both now reflect v0.21 reality. --- docs/user-guide/calculus.md | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/docs/user-guide/calculus.md b/docs/user-guide/calculus.md index 630e085..a7a41c2 100644 --- a/docs/user-guide/calculus.md +++ b/docs/user-guide/calculus.md @@ -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 @@ -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. From 949f915f1d480673b4cb86006321a1cd67747b73 Mon Sep 17 00:00:00 2001 From: Max Zhang Date: Mon, 27 Apr 2026 11:24:02 -0400 Subject: [PATCH 17/17] =?UTF-8?q?v0.21:=20address=20/review=20polish=20?= =?UTF-8?q?=E2=80=94=20demo=20+=20count=20corrections?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Add demo_3d_reorder_transparency() to compare_v021_slider_tt_calculus.py: uses explicit reorder([2,0,1]) to force non-identity _dim_order, then verifies user-frame minimize() is transparent. The existing with_auto_order() demo happens to pick canonical [0,1,2] for the chosen function so the new sub-demo visibly exercises the non-identity path. - Fix CLAUDE.md test count: v0.21 additions are 57 tests, not 64. The latent _algebra._check_compatible tuple-vs-list domain bug observed during Task 5 is filed as issue #22 (pre-existing, not introduced by v0.21). --- CLAUDE.md | 2 +- compare_v021_slider_tt_calculus.py | 28 ++++++++++++++++++++++++++++ 2 files changed, 29 insertions(+), 1 deletion(-) diff --git a/CLAUDE.md b/CLAUDE.md index 454801a..439624c 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -132,7 +132,7 @@ Not part of the library. Compare Chebyshev barycentric against alternative metho - `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: ~64 tests + 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()`, diff --git a/compare_v021_slider_tt_calculus.py b/compare_v021_slider_tt_calculus.py index 4db97d5..d11c417 100644 --- a/compare_v021_slider_tt_calculus.py +++ b/compare_v021_slider_tt_calculus.py @@ -83,6 +83,33 @@ def f(x, _): return (x[0] - 0.4) * (1.0 + 0.1 * x[1] + 0.2 * x[2]) _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 @@ -111,6 +138,7 @@ def main() -> None: 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")