| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411 |
- # Copyright (c) 2017, The Chancellor, Masters and Scholars of the University
- # of Oxford, and the Chebfun Developers. All rights reserved.
- #
- # Redistribution and use in source and binary forms, with or without
- # modification, are permitted provided that the following conditions are met:
- # * Redistributions of source code must retain the above copyright
- # notice, this list of conditions and the following disclaimer.
- # * Redistributions in binary form must reproduce the above copyright
- # notice, this list of conditions and the following disclaimer in the
- # documentation and/or other materials provided with the distribution.
- # * Neither the name of the University of Oxford nor the names of its
- # contributors may be used to endorse or promote products derived from
- # this software without specific prior written permission.
- #
- # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
- # ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
- # WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
- # DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR
- # ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
- # (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
- # LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
- # ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
- # (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
- # SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
- from math import factorial
- import numpy as np
- from numpy.testing import assert_allclose, assert_equal, assert_array_less
- import pytest
- import scipy
- from scipy.interpolate import AAA, FloaterHormannInterpolator, BarycentricInterpolator
- TOL = 1e4 * np.finfo(np.float64).eps
- UNIT_INTERVAL = np.linspace(-1, 1, num=1000)
- PTS = np.logspace(-15, 0, base=10, num=500)
- PTS = np.concatenate([-PTS[::-1], [0], PTS])
- @pytest.mark.parametrize("method", [AAA, FloaterHormannInterpolator])
- @pytest.mark.parametrize("dtype", [np.float32, np.float64, np.complex64, np.complex128])
- def test_dtype_preservation(method, dtype):
- rtol = np.finfo(dtype).eps ** 0.75 * 100
- if method is FloaterHormannInterpolator:
- rtol *= 100
- rng = np.random.default_rng(59846294526092468)
- z = np.linspace(-1, 1, dtype=dtype)
- r = method(z, np.sin(z))
- z2 = rng.uniform(-1, 1, size=100).astype(dtype)
- assert_allclose(r(z2), np.sin(z2), rtol=rtol)
- assert r(z2).dtype == dtype
- if method is AAA:
- assert r.support_points.dtype == dtype
- assert r.support_values.dtype == dtype
- assert r.errors.dtype == z.real.dtype
- assert r.weights.dtype == dtype
- assert r.poles().dtype == np.result_type(dtype, 1j)
- assert r.residues().dtype == np.result_type(dtype, 1j)
- assert r.roots().dtype == np.result_type(dtype, 1j)
- @pytest.mark.parametrize("method", [AAA, FloaterHormannInterpolator])
- @pytest.mark.parametrize("dtype", [np.int16, np.int32, np.int64])
- def test_integer_promotion(method, dtype):
- z = np.arange(10, dtype=dtype)
- r = method(z, z)
- assert r.weights.dtype == np.result_type(dtype, 1.0)
- if method is AAA:
- assert r.support_points.dtype == np.result_type(dtype, 1.0)
- assert r.support_values.dtype == np.result_type(dtype, 1.0)
- assert r.errors.dtype == np.result_type(dtype, 1.0)
- assert r.poles().dtype == np.result_type(dtype, 1j)
- assert r.residues().dtype == np.result_type(dtype, 1j)
- assert r.roots().dtype == np.result_type(dtype, 1j)
- assert r(z).dtype == np.result_type(dtype, 1.0)
- class TestAAA:
- def test_input_validation(self):
- with pytest.raises(ValueError, match="`x` be of size 2 but got size 1."):
- AAA([0], [1, 1])
- with pytest.raises(ValueError, match="1-D"):
- AAA([[0], [0]], [[1], [1]])
- with pytest.raises(ValueError, match="finite"):
- AAA([np.inf], [1])
- with pytest.raises(TypeError):
- AAA([1], [1], max_terms=1.0)
- with pytest.raises(ValueError, match="greater"):
- AAA([1], [1], max_terms=-1)
- def test_convergence_error(self):
- with pytest.warns(RuntimeWarning, match="AAA failed"):
- AAA(UNIT_INTERVAL, np.exp(UNIT_INTERVAL), max_terms=1)
- # The following tests are based on:
- # https://github.com/chebfun/chebfun/blob/master/tests/chebfun/test_aaa.m
- def test_exp(self):
- f = np.exp(UNIT_INTERVAL)
- r = AAA(UNIT_INTERVAL, f)
- assert_allclose(r(UNIT_INTERVAL), f, atol=TOL)
- assert_equal(r(np.nan), np.nan)
- assert np.isfinite(r(np.inf))
- m1 = r.support_points.size
- r = AAA(UNIT_INTERVAL, f, rtol=1e-3)
- assert r.support_points.size < m1
- def test_tan(self):
- f = np.tan(np.pi * UNIT_INTERVAL)
- r = AAA(UNIT_INTERVAL, f)
- assert_allclose(r(UNIT_INTERVAL), f, atol=10 * TOL, rtol=1.4e-7)
- assert_allclose(np.min(np.abs(r.roots())), 0, atol=3e-10)
- assert_allclose(np.min(np.abs(r.poles() - 0.5)), 0, atol=TOL)
- # Test for spurious poles (poles with tiny residue are likely spurious)
- assert np.min(np.abs(r.residues())) > 1e-13
- def test_short_cases(self):
- # Computed using Chebfun:
- # >> format long
- # >> [r, pol, res, zer, zj, fj, wj, errvec] = aaa([1 2], [0 1])
- z = np.array([0, 1])
- f = np.array([1, 2])
- r = AAA(z, f, rtol=1e-13)
- assert_allclose(r(z), f, atol=TOL)
- assert_allclose(r.poles(), 0.5)
- assert_allclose(r.residues(), 0.25)
- assert_allclose(r.roots(), 1/3)
- assert_equal(r.support_points, z)
- assert_equal(r.support_values, f)
- assert_allclose(r.weights, [0.707106781186547, 0.707106781186547])
- assert_equal(r.errors, [1, 0])
- # >> format long
- # >> [r, pol, res, zer, zj, fj, wj, errvec] = aaa([1 0 0], [0 1 2])
- z = np.array([0, 1, 2])
- f = np.array([1, 0, 0])
- r = AAA(z, f, rtol=1e-13)
- assert_allclose(r(z), f, atol=TOL)
- assert_allclose(np.sort(r.poles()),
- np.sort([1.577350269189626, 0.422649730810374]))
- assert_allclose(np.sort(r.residues()),
- np.sort([-0.070441621801729, -0.262891711531604]))
- assert_allclose(np.sort(r.roots()), np.sort([2, 1]))
- assert_equal(r.support_points, z)
- assert_equal(r.support_values, f)
- assert_allclose(r.weights, [0.577350269189626, 0.577350269189626,
- 0.577350269189626])
- assert_equal(r.errors, [1, 1, 0])
- def test_scale_invariance(self):
- z = np.linspace(0.3, 1.5)
- f = np.exp(z) / (1 + 1j)
- r1 = AAA(z, f)
- r2 = AAA(z, (2**311 * f).astype(np.complex128))
- r3 = AAA(z, (2**-311 * f).astype(np.complex128))
- assert_equal(r1(0.2j), 2**-311 * r2(0.2j))
- assert_equal(r1(1.4), 2**311 * r3(1.4))
- def test_log_func(self):
- rng = np.random.default_rng(1749382759832758297)
- z = rng.standard_normal(10000) + 3j * rng.standard_normal(10000)
- def f(z):
- return np.log(5 - z) / (1 + z**2)
- r = AAA(z, f(z))
- assert_allclose(r(0), f(0), atol=TOL)
- def test_infinite_data(self):
- z = np.linspace(-1, 1)
- r = AAA(z, scipy.special.gamma(z))
- assert_allclose(r(0.63), scipy.special.gamma(0.63), atol=1e-15)
- def test_nan(self):
- x = np.linspace(0, 20)
- with np.errstate(invalid="ignore"):
- f = np.sin(x) / x
- r = AAA(x, f)
- assert_allclose(r(2), np.sin(2) / 2, atol=1e-15)
- def test_residues(self):
- x = np.linspace(-1.337, 2, num=537)
- r = AAA(x, np.exp(x) / x)
- ii = np.flatnonzero(np.abs(r.poles()) < 1e-8)
- assert_allclose(r.residues()[ii], 1, atol=1e-15)
- r = AAA(x, (1 + 1j) * scipy.special.gamma(x))
- ii = np.flatnonzero(abs(r.poles() - (-1)) < 1e-8)
- assert_allclose(r.residues()[ii], -1 - 1j, atol=1e-15)
- # The following tests are based on:
- # https://github.com/complexvariables/RationalFunctionApproximation.jl/blob/main/test/interval.jl
- @pytest.mark.parametrize("func,atol,rtol",
- [(lambda x: np.abs(x + 0.5 + 0.01j), 5e-13, 1e-7),
- (lambda x: np.sin(1/(1.05 - x)), 2e-13, 1e-7),
- (lambda x: np.exp(-1/(x**2)), 3.5e-11, 0),
- (lambda x: np.exp(-100*x**2), 2e-12, 0),
- (lambda x: np.exp(-10/(1.2 - x)), 1e-14, 0),
- (lambda x: 1/(1+np.exp(100*(x + 0.5))), 2e-13, 1e-7),
- (lambda x: np.abs(x - 0.95), 1e-6, 1e-7)])
- def test_basic_functions(self, func, atol, rtol):
- with np.errstate(divide="ignore"):
- f = func(PTS)
- assert_allclose(AAA(UNIT_INTERVAL, func(UNIT_INTERVAL))(PTS),
- f, atol=atol, rtol=rtol)
- def test_poles_zeros_residues(self):
- def f(z):
- return (z+1) * (z+2) / ((z+3) * (z+4))
- r = AAA(UNIT_INTERVAL, f(UNIT_INTERVAL))
- assert_allclose(np.sum(r.poles() + r.roots()), -10, atol=1e-12)
- def f(z):
- return 2/(3 + z) + 5/(z - 2j)
- r = AAA(UNIT_INTERVAL, f(UNIT_INTERVAL))
- assert_allclose(r.residues().prod(), 10, atol=1e-8)
- r = AAA(UNIT_INTERVAL, np.sin(10*np.pi*UNIT_INTERVAL))
- assert_allclose(np.sort(np.abs(r.roots()))[18], 0.9, atol=1e-12)
- def f(z):
- return (z - (3 + 3j))/(z + 2)
- r = AAA(UNIT_INTERVAL, f(UNIT_INTERVAL))
- assert_allclose(r.poles()[0]*r.roots()[0], -6-6j, atol=1e-12)
- @pytest.mark.parametrize("func",
- [lambda z: np.zeros_like(z), lambda z: z, lambda z: 1j*z,
- lambda z: z**2 + z, lambda z: z**3 + z,
- lambda z: 1/(1.1 + z), lambda z: 1/(1 + 1j*z),
- lambda z: 1/(3 + z + z**2), lambda z: 1/(1.01 + z**3)])
- def test_polynomials_and_reciprocals(self, func):
- assert_allclose(AAA(UNIT_INTERVAL, func(UNIT_INTERVAL))(PTS),
- func(PTS), atol=2e-13)
- # The following tests are taken from:
- # https://github.com/macd/BaryRational.jl/blob/main/test/test_aaa.jl
- def test_spiral(self):
- z = np.exp(np.linspace(-0.5, 0.5 + 15j*np.pi, num=1000))
- r = AAA(z, np.tan(np.pi*z/2))
- assert_allclose(np.sort(np.abs(r.poles()))[:4], [1, 1, 3, 3], rtol=9e-7)
- def test_spiral_cleanup(self):
- z = np.exp(np.linspace(-0.5, 0.5 + 15j*np.pi, num=1000))
- # here we set `rtol=0` to force froissart doublets, without cleanup there
- # are many spurious poles
- with pytest.warns(RuntimeWarning):
- r = AAA(z, np.tan(np.pi*z/2), rtol=0, max_terms=60, clean_up=False)
- n_spurious = np.sum(np.abs(r.residues()) < 1e-14)
- with pytest.warns(RuntimeWarning):
- assert r.clean_up() >= 1
- # check there are less potentially spurious poles than before
- assert np.sum(np.abs(r.residues()) < 1e-14) < n_spurious
- # check accuracy
- assert_allclose(r(z), np.tan(np.pi*z/2), atol=6e-12, rtol=3e-12)
- def test_diag_scaling(self):
- # fails without diag scaling
- z = np.logspace(-15, 0, 300)
- f = np.sqrt(z)
- r = AAA(z, f)
- zz = np.logspace(-15, 0, 500)
- assert_allclose(r(zz), np.sqrt(zz), rtol=9e-6)
- class BatchFloaterHormann:
- # FloaterHormann class with reference batch behaviour
- def __init__(self, x, y, axis):
- y = np.moveaxis(y, axis, -1)
- self._batch_shape = y.shape[:-1]
- self._interps = [FloaterHormannInterpolator(x, yi,)
- for yi in y.reshape(-1, y.shape[-1])]
- self._axis = axis
- def __call__(self, x):
- y = [interp(x) for interp in self._interps]
- y = np.reshape(y, self._batch_shape + x.shape)
- return np.moveaxis(y, -1, self._axis) if x.shape else y
- class TestFloaterHormann:
- def runge(self, z):
- return 1/(1 + z**2)
- def scale(self, n, d):
- return (-1)**(np.arange(n) + d) * factorial(d)
- def test_iv(self):
- with pytest.raises(ValueError, match="`x`"):
- FloaterHormannInterpolator([[0]], [0], d=0)
- with pytest.raises(ValueError, match="`y`"):
- FloaterHormannInterpolator([0], 0, d=0)
- with pytest.raises(ValueError, match="`x` be of size 2 but got size 1."):
- FloaterHormannInterpolator([0], [[1, 1], [1, 1]], d=0)
- with pytest.raises(ValueError, match="finite"):
- FloaterHormannInterpolator([np.inf], [1], d=0)
- with pytest.raises(ValueError, match="`d`"):
- FloaterHormannInterpolator([0], [0], d=-1)
- with pytest.raises(ValueError, match="`d`"):
- FloaterHormannInterpolator([0], [0], d=10)
- with pytest.raises(TypeError):
- FloaterHormannInterpolator([0], [0], d=0.0)
- # reference values from Floater and Hormann 2007 page 8.
- @pytest.mark.parametrize("d,expected", [
- (0, [1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1]),
- (1, [1, 2, 2, 2, 2, 2, 2, 2, 2, 2, 1]),
- (2, [1, 3, 4, 4, 4, 4, 4, 4, 4, 3, 1]),
- (3, [1, 4, 7, 8, 8, 8, 8, 8, 7, 4, 1]),
- (4, [1, 5, 11, 15, 16, 16, 16, 15, 11, 5, 1])
- ])
- def test_uniform_grid(self, d, expected):
- # Check against explicit results on an uniform grid
- x = np.arange(11)
- r = FloaterHormannInterpolator(x, 0.0*x, d=d)
- assert_allclose(r.weights.ravel()*self.scale(x.size, d), expected,
- rtol=1e-15, atol=1e-15)
- @pytest.mark.parametrize("d", range(10))
- def test_runge(self, d):
- x = np.linspace(0, 1, 51)
- rng = np.random.default_rng(802754237598370893)
- xx = rng.uniform(0, 1, size=1000)
- y = self.runge(x)
- h = x[1] - x[0]
- r = FloaterHormannInterpolator(x, y, d=d)
- tol = 10*h**(d+1)
- assert_allclose(r(xx), self.runge(xx), atol=1e-10, rtol=tol)
- # check interpolation property
- assert_equal(r(x), self.runge(x))
- def test_complex(self):
- x = np.linspace(-1, 1)
- z = x + x*1j
- r = FloaterHormannInterpolator(z, np.sin(z), d=12)
- xx = np.linspace(-1, 1, num=1000)
- zz = xx + xx*1j
- assert_allclose(r(zz), np.sin(zz), rtol=1e-12)
- def test_polyinterp(self):
- # check that when d=n-1 FH gives a polynomial interpolant
- x = np.linspace(0, 1, 11)
- xx = np.linspace(0, 1, 1001)
- y = np.sin(x)
- r = FloaterHormannInterpolator(x, y, d=x.size-1)
- p = BarycentricInterpolator(x, y)
- assert_allclose(r(xx), p(xx), rtol=1e-12, atol=1e-12)
- @pytest.mark.parametrize("y_shape", [(2,), (2, 3, 1), (1, 5, 6, 4)])
- @pytest.mark.parametrize("xx_shape", [(100), (10, 10)])
- def test_trailing_dim(self, y_shape, xx_shape):
- x = np.linspace(0, 1)
- y = np.broadcast_to(
- np.expand_dims(np.sin(x), tuple(range(1, len(y_shape) + 1))),
- x.shape + y_shape
- )
- r = FloaterHormannInterpolator(x, y)
- rng = np.random.default_rng(897138947238097528091759187597)
- xx = rng.random(xx_shape)
- yy = np.broadcast_to(
- np.expand_dims(np.sin(xx), tuple(range(xx.ndim, len(y_shape) + xx.ndim))),
- xx.shape + y_shape
- )
- rr = r(xx)
- assert rr.shape == xx.shape + y_shape
- assert_allclose(rr, yy, rtol=1e-6)
- def test_zeros(self):
- x = np.linspace(0, 10, num=100)
- r = FloaterHormannInterpolator(x, np.sin(np.pi*x))
- err = np.abs(np.subtract.outer(r.roots(), np.arange(11))).min(axis=0)
- assert_array_less(err, 1e-5)
- def test_no_poles(self):
- x = np.linspace(-1, 1)
- r = FloaterHormannInterpolator(x, 1/x**2)
- p = r.poles()
- mask = (p.real >= -1) & (p.real <= 1) & (np.abs(p.imag) < 1.e-12)
- assert np.sum(mask) == 0
- @pytest.mark.parametrize('eval_shape', [(), (1,), (3,)])
- @pytest.mark.parametrize('axis', [-1, 0, 1])
- def test_batch(self, eval_shape, axis):
- rng = np.random.default_rng(4329872134985134)
- n = 10
- shape = (2, 3, 4, n)
- domain = (0, 10)
- x = np.linspace(*domain, n)
- y = np.moveaxis(rng.random(shape), -1, axis)
- res = FloaterHormannInterpolator(x, y, axis=axis)
- ref = BatchFloaterHormann(x, y, axis=axis)
- x = rng.uniform(*domain, size=eval_shape)
- assert_allclose(res(x), ref(x))
- pytest.raises(NotImplementedError, res.roots)
- pytest.raises(NotImplementedError, res.residues)
|