_bary_rational.py 29 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748
  1. # Copyright (c) 2017, The Chancellor, Masters and Scholars of the University
  2. # of Oxford, and the Chebfun Developers. All rights reserved.
  3. #
  4. # Redistribution and use in source and binary forms, with or without
  5. # modification, are permitted provided that the following conditions are met:
  6. # * Redistributions of source code must retain the above copyright
  7. # notice, this list of conditions and the following disclaimer.
  8. # * Redistributions in binary form must reproduce the above copyright
  9. # notice, this list of conditions and the following disclaimer in the
  10. # documentation and/or other materials provided with the distribution.
  11. # * Neither the name of the University of Oxford nor the names of its
  12. # contributors may be used to endorse or promote products derived from
  13. # this software without specific prior written permission.
  14. #
  15. # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
  16. # ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
  17. # WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
  18. # DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR
  19. # ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
  20. # (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
  21. # LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
  22. # ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
  23. # (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
  24. # SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
  25. import warnings
  26. import operator
  27. from types import GenericAlias
  28. import numpy as np
  29. import scipy
  30. __all__ = ["AAA", "FloaterHormannInterpolator"]
  31. class _BarycentricRational:
  32. """Base class for barycentric representation of a rational function."""
  33. # generic type compatibility with scipy-stubs
  34. __class_getitem__ = classmethod(GenericAlias)
  35. def __init__(self, x, y, axis=0, **kwargs):
  36. self._axis = axis
  37. # input validation
  38. z = np.asarray(x)
  39. f = np.asarray(y)
  40. self._input_validation(z, f, **kwargs)
  41. f = np.moveaxis(f, self._axis, 0)
  42. # Remove infinite or NaN function values and repeated entries
  43. to_keep = np.logical_and.reduce(
  44. ((np.isfinite(f)) & (~np.isnan(f))).reshape(f.shape[0], -1),
  45. axis=-1
  46. )
  47. f = f[to_keep, ...]
  48. z = z[to_keep]
  49. z, uni = np.unique(z, return_index=True)
  50. f = f[uni, ...]
  51. self._shape = f.shape[1:]
  52. self._support_points, self._support_values, self.weights = (
  53. self._compute_weights(z, f, **kwargs)
  54. )
  55. # only compute once
  56. self._poles = None
  57. self._residues = None
  58. self._roots = None
  59. def _input_validation(self, x, y, **kwargs):
  60. if x.ndim != 1:
  61. raise ValueError("`x` must be 1-D.")
  62. if not y.ndim >= 1:
  63. raise ValueError("`y` must be at least 1-D.")
  64. if x.size != y.shape[self._axis]:
  65. msg = f"`x` be of size {y.shape[self._axis]} but got size {x.size}."
  66. raise ValueError(msg)
  67. if not np.all(np.isfinite(x)):
  68. raise ValueError("`x` must be finite.")
  69. def _compute_weights(z, f, **kwargs):
  70. raise NotImplementedError
  71. def __call__(self, z):
  72. """Evaluate the rational approximation at given values.
  73. Parameters
  74. ----------
  75. z : array_like
  76. Input values.
  77. """
  78. # evaluate rational function in barycentric form.
  79. z = np.asarray(z)
  80. zv = np.ravel(z)
  81. support_values = self._support_values.reshape(
  82. (self._support_values.shape[0], -1)
  83. )
  84. weights = self.weights[..., np.newaxis]
  85. # Cauchy matrix
  86. # Ignore errors due to inf/inf at support points, these will be fixed later
  87. with np.errstate(invalid="ignore", divide="ignore"):
  88. CC = 1 / np.subtract.outer(zv, self._support_points)
  89. # Vector of values
  90. r = CC @ (weights * support_values) / (CC @ weights)
  91. # Deal with input inf: `r(inf) = lim r(z) = sum(w*f) / sum(w)`
  92. if np.any(np.isinf(zv)):
  93. r[np.isinf(zv)] = (np.sum(weights * support_values)
  94. / np.sum(weights))
  95. # Deal with NaN
  96. ii = np.nonzero(np.isnan(r))[0]
  97. for jj in ii:
  98. if np.isnan(zv[jj]) or not np.any(zv[jj] == self._support_points):
  99. # r(NaN) = NaN is fine.
  100. # The second case may happen if `r(zv[ii]) = 0/0` at some point.
  101. pass
  102. else:
  103. # Clean up values `NaN = inf/inf` at support points.
  104. # Find the corresponding node and set entry to correct value:
  105. r[jj] = support_values[zv[jj] == self._support_points].squeeze()
  106. res = np.reshape(r, z.shape + self._shape)
  107. return np.moveaxis(res, 0, self._axis) if z.ndim > 0 else res
  108. def poles(self):
  109. """Compute the poles of the rational approximation.
  110. Returns
  111. -------
  112. poles : array
  113. Poles of the approximation, repeated according to their multiplicity
  114. but not in any specific order.
  115. """
  116. if self._poles is None:
  117. # Compute poles via generalized eigenvalue problem
  118. m = self.weights.size
  119. B = np.eye(m + 1, dtype=self.weights.dtype)
  120. B[0, 0] = 0
  121. E = np.zeros_like(B, dtype=np.result_type(self.weights,
  122. self._support_points))
  123. E[0, 1:] = self.weights
  124. E[1:, 0] = 1
  125. np.fill_diagonal(E[1:, 1:], self._support_points)
  126. pol = scipy.linalg.eigvals(E, B)
  127. self._poles = pol[np.isfinite(pol)]
  128. return self._poles
  129. def residues(self):
  130. """Compute the residues of the poles of the approximation.
  131. Returns
  132. -------
  133. residues : array
  134. Residues associated with the `poles` of the approximation
  135. """
  136. if self._support_values.ndim > 1:
  137. raise NotImplementedError("Residues not implemented for multi-dimensional"
  138. " data.")
  139. if self._residues is None:
  140. # Compute residues via formula for res of quotient of analytic functions
  141. with np.errstate(divide="ignore", invalid="ignore"):
  142. N = (1/(np.subtract.outer(self.poles(), self._support_points))) @ (
  143. self._support_values * self.weights
  144. )
  145. Ddiff = (
  146. -((1/np.subtract.outer(self.poles(), self._support_points))**2)
  147. @ self.weights
  148. )
  149. self._residues = N / Ddiff
  150. return self._residues
  151. def roots(self):
  152. """Compute the roots of the rational approximation.
  153. Returns
  154. -------
  155. zeros : array
  156. Zeros of the approximation, repeated according to their multiplicity
  157. but not in any specific order.
  158. """
  159. if self._support_values.ndim > 1:
  160. raise NotImplementedError("Roots not implemented for multi-dimensional"
  161. " data.")
  162. if self._roots is None:
  163. # Compute zeros via generalized eigenvalue problem
  164. m = self.weights.size
  165. B = np.eye(m + 1, dtype=self.weights.dtype)
  166. B[0, 0] = 0
  167. E = np.zeros_like(B, dtype=np.result_type(self.weights,
  168. self._support_values,
  169. self._support_points))
  170. E[0, 1:] = self.weights * self._support_values
  171. E[1:, 0] = 1
  172. np.fill_diagonal(E[1:, 1:], self._support_points)
  173. zer = scipy.linalg.eigvals(E, B)
  174. self._roots = zer[np.isfinite(zer)]
  175. return self._roots
  176. class AAA(_BarycentricRational):
  177. r"""
  178. AAA real or complex rational approximation.
  179. As described in [1]_, the AAA algorithm is a greedy algorithm for approximation by
  180. rational functions on a real or complex set of points. The rational approximation is
  181. represented in a barycentric form from which the roots (zeros), poles, and residues
  182. can be computed.
  183. Parameters
  184. ----------
  185. x : 1D array_like, shape (n,)
  186. 1-D array containing values of the independent variable. Values may be real or
  187. complex but must be finite.
  188. y : 1D array_like, shape (n,)
  189. Function values ``f(x)``. Infinite and NaN values of `values` and
  190. corresponding values of `points` will be discarded.
  191. rtol : float, optional
  192. Relative tolerance, defaults to ``eps**0.75``. If a small subset of the entries
  193. in `values` are much larger than the rest the default tolerance may be too
  194. loose. If the tolerance is too tight then the approximation may contain
  195. Froissart doublets or the algorithm may fail to converge entirely.
  196. max_terms : int, optional
  197. Maximum number of terms in the barycentric representation, defaults to ``100``.
  198. Must be greater than or equal to one.
  199. clean_up : bool, optional
  200. Automatic removal of Froissart doublets, defaults to ``True``. See notes for
  201. more details.
  202. clean_up_tol : float, optional
  203. Poles with residues less than this number times the geometric mean
  204. of `values` times the minimum distance to `points` are deemed spurious by the
  205. cleanup procedure, defaults to 1e-13. See notes for more details.
  206. Attributes
  207. ----------
  208. support_points : array
  209. Support points of the approximation. These are a subset of the provided `x` at
  210. which the approximation strictly interpolates `y`.
  211. See notes for more details.
  212. support_values : array
  213. Value of the approximation at the `support_points`.
  214. weights : array
  215. Weights of the barycentric approximation.
  216. errors : array
  217. Error :math:`|f(z) - r(z)|_\infty` over `points` in the successive iterations
  218. of AAA.
  219. Warns
  220. -----
  221. RuntimeWarning
  222. If `rtol` is not achieved in `max_terms` iterations.
  223. See Also
  224. --------
  225. FloaterHormannInterpolator : Floater-Hormann barycentric rational interpolation.
  226. pade : Padé approximation.
  227. Notes
  228. -----
  229. At iteration :math:`m` (at which point there are :math:`m` terms in the both the
  230. numerator and denominator of the approximation), the
  231. rational approximation in the AAA algorithm takes the barycentric form
  232. .. math::
  233. r(z) = n(z)/d(z) =
  234. \frac{\sum_{j=1}^m\ w_j f_j / (z - z_j)}{\sum_{j=1}^m w_j / (z - z_j)},
  235. where :math:`z_1,\dots,z_m` are real or complex support points selected from
  236. `x`, :math:`f_1,\dots,f_m` are the corresponding real or complex data values
  237. from `y`, and :math:`w_1,\dots,w_m` are real or complex weights.
  238. Each iteration of the algorithm has two parts: the greedy selection the next support
  239. point and the computation of the weights. The first part of each iteration is to
  240. select the next support point to be added :math:`z_{m+1}` from the remaining
  241. unselected `x`, such that the nonlinear residual
  242. :math:`|f(z_{m+1}) - n(z_{m+1})/d(z_{m+1})|` is maximised. The algorithm terminates
  243. when this maximum is less than ``rtol * np.linalg.norm(f, ord=np.inf)``. This means
  244. the interpolation property is only satisfied up to a tolerance, except at the
  245. support points where approximation exactly interpolates the supplied data.
  246. In the second part of each iteration, the weights :math:`w_j` are selected to solve
  247. the least-squares problem
  248. .. math::
  249. \text{minimise}_{w_j}|fd - n| \quad \text{subject to} \quad
  250. \sum_{j=1}^{m+1} w_j = 1,
  251. over the unselected elements of `x`.
  252. One of the challenges with working with rational approximations is the presence of
  253. Froissart doublets, which are either poles with vanishingly small residues or
  254. pole-zero pairs that are close enough together to nearly cancel, see [2]_. The
  255. greedy nature of the AAA algorithm means Froissart doublets are rare. However, if
  256. `rtol` is set too tight then the approximation will stagnate and many Froissart
  257. doublets will appear. Froissart doublets can usually be removed by removing support
  258. points and then resolving the least squares problem. The support point :math:`z_j`,
  259. which is the closest support point to the pole :math:`a` with residue
  260. :math:`\alpha`, is removed if the following is satisfied
  261. .. math::
  262. |\alpha| / |z_j - a| < \verb|clean_up_tol| \cdot \tilde{f},
  263. where :math:`\tilde{f}` is the geometric mean of `support_values`.
  264. References
  265. ----------
  266. .. [1] Y. Nakatsukasa, O. Sete, and L. N. Trefethen, "The AAA algorithm for
  267. rational approximation", SIAM J. Sci. Comp. 40 (2018), A1494-A1522.
  268. :doi:`10.1137/16M1106122`
  269. .. [2] J. Gilewicz and M. Pindor, Pade approximants and noise: rational functions,
  270. J. Comp. Appl. Math. 105 (1999), pp. 285-297.
  271. :doi:`10.1016/S0377-0427(02)00674-X`
  272. Examples
  273. --------
  274. Here we reproduce a number of the numerical examples from [1]_ as a demonstration
  275. of the functionality offered by this method.
  276. >>> import numpy as np
  277. >>> import matplotlib.pyplot as plt
  278. >>> from scipy.interpolate import AAA
  279. >>> import warnings
  280. For the first example we approximate the gamma function on ``[-3.5, 4.5]`` by
  281. extrapolating from 100 samples in ``[-1.5, 1.5]``.
  282. >>> from scipy.special import gamma
  283. >>> sample_points = np.linspace(-1.5, 1.5, num=100)
  284. >>> r = AAA(sample_points, gamma(sample_points))
  285. >>> z = np.linspace(-3.5, 4.5, num=1000)
  286. >>> fig, ax = plt.subplots()
  287. >>> ax.plot(z, gamma(z), label="Gamma")
  288. >>> ax.plot(sample_points, gamma(sample_points), label="Sample points")
  289. >>> ax.plot(z, r(z).real, '--', label="AAA approximation")
  290. >>> ax.set(xlabel="z", ylabel="r(z)", ylim=[-8, 8], xlim=[-3.5, 4.5])
  291. >>> ax.legend()
  292. >>> plt.show()
  293. We can also view the poles of the rational approximation and their residues:
  294. >>> order = np.argsort(r.poles())
  295. >>> r.poles()[order]
  296. array([-3.81591039e+00+0.j , -3.00269049e+00+0.j ,
  297. -1.99999988e+00+0.j , -1.00000000e+00+0.j ,
  298. 5.85842812e-17+0.j , 4.77485458e+00-3.06919376j,
  299. 4.77485458e+00+3.06919376j, 5.29095868e+00-0.97373072j,
  300. 5.29095868e+00+0.97373072j])
  301. >>> r.residues()[order]
  302. array([ 0.03658074 +0.j , -0.16915426 -0.j ,
  303. 0.49999915 +0.j , -1. +0.j ,
  304. 1. +0.j , -0.81132013 -2.30193429j,
  305. -0.81132013 +2.30193429j, 0.87326839+10.70148546j,
  306. 0.87326839-10.70148546j])
  307. For the second example, we call `AAA` with a spiral of 1000 points that wind 7.5
  308. times around the origin in the complex plane.
  309. >>> z = np.exp(np.linspace(-0.5, 0.5 + 15j*np.pi, 1000))
  310. >>> r = AAA(z, np.tan(np.pi*z/2), rtol=1e-13)
  311. We see that AAA takes 12 steps to converge with the following errors:
  312. >>> r.errors.size
  313. 12
  314. >>> r.errors
  315. array([2.49261500e+01, 4.28045609e+01, 1.71346935e+01, 8.65055336e-02,
  316. 1.27106444e-02, 9.90889874e-04, 5.86910543e-05, 1.28735561e-06,
  317. 3.57007424e-08, 6.37007837e-10, 1.67103357e-11, 1.17112299e-13])
  318. We can also plot the computed poles:
  319. >>> fig, ax = plt.subplots()
  320. >>> ax.plot(z.real, z.imag, '.', markersize=2, label="Sample points")
  321. >>> ax.plot(r.poles().real, r.poles().imag, '.', markersize=5,
  322. ... label="Computed poles")
  323. >>> ax.set(xlim=[-3.5, 3.5], ylim=[-3.5, 3.5], aspect="equal")
  324. >>> ax.legend()
  325. >>> plt.show()
  326. We now demonstrate the removal of Froissart doublets using the `clean_up` method
  327. using an example from [1]_. Here we approximate the function
  328. :math:`f(z)=\log(2 + z^4)/(1 + 16z^4)` by sampling it at 1000 roots of unity. The
  329. algorithm is run with ``rtol=0`` and ``clean_up=False`` to deliberately cause
  330. Froissart doublets to appear.
  331. >>> z = np.exp(1j*2*np.pi*np.linspace(0,1, num=1000))
  332. >>> def f(z):
  333. ... return np.log(2 + z**4)/(1 - 16*z**4)
  334. >>> with warnings.catch_warnings(): # filter convergence warning due to rtol=0
  335. ... warnings.simplefilter('ignore', RuntimeWarning)
  336. ... r = AAA(z, f(z), rtol=0, max_terms=50, clean_up=False)
  337. >>> mask = np.abs(r.residues()) < 1e-13
  338. >>> fig, axs = plt.subplots(ncols=2)
  339. >>> axs[0].plot(r.poles().real[~mask], r.poles().imag[~mask], '.')
  340. >>> axs[0].plot(r.poles().real[mask], r.poles().imag[mask], 'r.')
  341. Now we call the `clean_up` method to remove Froissart doublets.
  342. >>> with warnings.catch_warnings():
  343. ... warnings.simplefilter('ignore', RuntimeWarning)
  344. ... r.clean_up()
  345. 4 # may vary
  346. >>> mask = np.abs(r.residues()) < 1e-13
  347. >>> axs[1].plot(r.poles().real[~mask], r.poles().imag[~mask], '.')
  348. >>> axs[1].plot(r.poles().real[mask], r.poles().imag[mask], 'r.')
  349. >>> plt.show()
  350. The left image shows the poles prior of the approximation ``clean_up=False`` with
  351. poles with residue less than ``10^-13`` in absolute value shown in red. The right
  352. image then shows the poles after the `clean_up` method has been called.
  353. """
  354. def __init__(self, x, y, *, rtol=None, max_terms=100, clean_up=True,
  355. clean_up_tol=1e-13):
  356. super().__init__(x, y, rtol=rtol, max_terms=max_terms)
  357. if clean_up:
  358. self.clean_up(clean_up_tol)
  359. def _input_validation(self, x, y, rtol=None, max_terms=100, clean_up=True,
  360. clean_up_tol=1e-13):
  361. max_terms = operator.index(max_terms)
  362. if max_terms < 1:
  363. raise ValueError("`max_terms` must be an integer value greater than or "
  364. "equal to one.")
  365. if y.ndim != 1:
  366. raise ValueError("`y` must be 1-D.")
  367. super()._input_validation(x, y)
  368. @property
  369. def support_points(self):
  370. return self._support_points
  371. @property
  372. def support_values(self):
  373. return self._support_values
  374. def _compute_weights(self, z, f, rtol, max_terms):
  375. # Initialization for AAA iteration
  376. M = np.size(z)
  377. mask = np.ones(M, dtype=np.bool_)
  378. dtype = np.result_type(z, f, 1.0)
  379. rtol = np.finfo(dtype).eps**0.75 if rtol is None else rtol
  380. atol = rtol * np.linalg.norm(f, ord=np.inf)
  381. zj = np.empty(max_terms, dtype=dtype)
  382. fj = np.empty(max_terms, dtype=dtype)
  383. # Cauchy matrix
  384. C = np.empty((M, max_terms), dtype=dtype)
  385. # Loewner matrix
  386. A = np.empty((M, max_terms), dtype=dtype)
  387. errors = np.empty(max_terms, dtype=A.real.dtype)
  388. R = np.repeat(np.mean(f), M)
  389. ill_conditioned = False
  390. ill_conditioned_tol = 1/(3*np.finfo(dtype).eps)
  391. # AAA iteration
  392. for m in range(max_terms):
  393. # Introduce next support point
  394. # Select next support point
  395. jj = np.argmax(np.abs(f[mask] - R[mask]))
  396. # Update support points
  397. zj[m] = z[mask][jj]
  398. # Update data values
  399. fj[m] = f[mask][jj]
  400. # Next column of Cauchy matrix
  401. # Ignore errors as we manually interpolate at support points
  402. with np.errstate(divide="ignore", invalid="ignore"):
  403. C[:, m] = 1 / (z - z[mask][jj])
  404. # Update mask
  405. mask[np.nonzero(mask)[0][jj]] = False
  406. # Update Loewner matrix
  407. # Ignore errors as inf values will be masked out in SVD call
  408. with np.errstate(invalid="ignore"):
  409. A[:, m] = (f - fj[m]) * C[:, m]
  410. # Compute weights
  411. rows = mask.sum()
  412. if rows >= m + 1:
  413. # The usual tall-skinny case
  414. if not ill_conditioned:
  415. _, s, V = scipy.linalg.svd(
  416. A[mask, : m + 1], full_matrices=False, check_finite=False,
  417. )
  418. with np.errstate(invalid="ignore", divide="ignore"):
  419. if s[0]/s[-1] > ill_conditioned_tol:
  420. ill_conditioned = True
  421. if ill_conditioned:
  422. col_norm = np.linalg.norm(A[mask, : m + 1], axis=0)
  423. _, s, V = scipy.linalg.svd(
  424. A[mask, : m + 1]/col_norm, full_matrices=False,
  425. check_finite=False,
  426. )
  427. # Treat case of multiple min singular values
  428. mm = s == np.min(s)
  429. # Aim for non-sparse weight vector
  430. wj = (V.conj()[mm, :].sum(axis=0) / np.sqrt(mm.sum())).astype(dtype)
  431. if ill_conditioned:
  432. wj /= col_norm
  433. else:
  434. # Fewer rows than columns
  435. V = scipy.linalg.null_space(A[mask, : m + 1], check_finite=False)
  436. nm = V.shape[-1]
  437. # Aim for non-sparse wt vector
  438. wj = V.sum(axis=-1) / np.sqrt(nm)
  439. # Compute rational approximant
  440. # Omit columns with `wj == 0`
  441. i0 = wj != 0
  442. # Ignore errors as we manually interpolate at support points
  443. with np.errstate(invalid="ignore"):
  444. # Numerator
  445. N = C[:, : m + 1][:, i0] @ (wj[i0] * fj[: m + 1][i0])
  446. # Denominator
  447. D = C[:, : m + 1][:, i0] @ wj[i0]
  448. # Interpolate at support points with `wj !=0`
  449. D_inf = np.isinf(D) | np.isnan(D)
  450. D[D_inf] = 1
  451. N[D_inf] = f[D_inf]
  452. R = N / D
  453. # Check if converged
  454. max_error = np.linalg.norm(f - R, ord=np.inf)
  455. errors[m] = max_error
  456. if max_error <= atol:
  457. break
  458. if m == max_terms - 1:
  459. warnings.warn(f"AAA failed to converge within {max_terms} iterations.",
  460. RuntimeWarning, stacklevel=2)
  461. # Trim off unused array allocation
  462. zj = zj[: m + 1]
  463. fj = fj[: m + 1]
  464. # Remove support points with zero weight
  465. i_non_zero = wj != 0
  466. self.errors = errors[: m + 1]
  467. self._points = z
  468. self._values = f
  469. return zj[i_non_zero], fj[i_non_zero], wj[i_non_zero]
  470. def clean_up(self, cleanup_tol=1e-13):
  471. """Automatic removal of Froissart doublets.
  472. Parameters
  473. ----------
  474. cleanup_tol : float, optional
  475. Poles with residues less than this number times the geometric mean
  476. of `values` times the minimum distance to `points` are deemed spurious by
  477. the cleanup procedure, defaults to 1e-13.
  478. Returns
  479. -------
  480. int
  481. Number of Froissart doublets detected
  482. """
  483. # Find negligible residues
  484. geom_mean_abs_f = scipy.stats.gmean(np.abs(self._values))
  485. Z_distances = np.min(
  486. np.abs(np.subtract.outer(self.poles(), self._points)), axis=1
  487. )
  488. with np.errstate(divide="ignore", invalid="ignore"):
  489. ii = np.nonzero(
  490. np.abs(self.residues()) / Z_distances < cleanup_tol * geom_mean_abs_f
  491. )
  492. ni = ii[0].size
  493. if ni == 0:
  494. return ni
  495. warnings.warn(f"{ni} Froissart doublets detected.", RuntimeWarning,
  496. stacklevel=2)
  497. # For each spurious pole find and remove closest support point
  498. closest_spt_point = np.argmin(
  499. np.abs(np.subtract.outer(self._support_points, self.poles()[ii])), axis=0
  500. )
  501. self._support_points = np.delete(self._support_points, closest_spt_point)
  502. self._support_values = np.delete(self._support_values, closest_spt_point)
  503. # Remove support points z from sample set
  504. mask = np.logical_and.reduce(
  505. np.not_equal.outer(self._points, self._support_points), axis=1
  506. )
  507. f = self._values[mask]
  508. z = self._points[mask]
  509. # recompute weights, we resolve the least squares problem for the remaining
  510. # support points
  511. m = self._support_points.size
  512. # Cauchy matrix
  513. C = 1 / np.subtract.outer(z, self._support_points)
  514. # Loewner matrix
  515. A = f[:, np.newaxis] * C - C * self._support_values
  516. # Solve least-squares problem to obtain weights
  517. _, _, V = scipy.linalg.svd(A, check_finite=False)
  518. self.weights = np.conj(V[m - 1,:])
  519. # reset roots, poles, residues as cached values will be wrong with new weights
  520. self._poles = None
  521. self._residues = None
  522. self._roots = None
  523. return ni
  524. class FloaterHormannInterpolator(_BarycentricRational):
  525. r"""Floater-Hormann barycentric rational interpolator (C∞ smooth on real axis).
  526. As described in [1]_, the method of Floater and Hormann computes weights for a
  527. barycentric rational interpolant with no poles on the real axis.
  528. Parameters
  529. ----------
  530. x : 1D array_like, shape (n,)
  531. 1-D array containing values of the independent variable. Values may be real or
  532. complex but must be finite.
  533. y : array_like, shape (n, ...)
  534. Array containing values of the dependent variable. Infinite and NaN values
  535. of `y` and corresponding values of `x` will be discarded.
  536. d : int, default: 3
  537. Integer satisfying ``0 <= d < n``. Floater-Hormann interpolation blends
  538. ``n - d`` polynomials of degree `d` together; for ``d = n - 1``, this is
  539. equivalent to polynomial interpolation.
  540. axis : int, default: 0
  541. Axis of `y` corresponding to `x`.
  542. Attributes
  543. ----------
  544. weights : array
  545. Weights of the barycentric approximation.
  546. See Also
  547. --------
  548. AAA : Barycentric rational approximation of real and complex functions.
  549. pade : Padé approximation.
  550. Notes
  551. -----
  552. The Floater-Hormann interpolant is a rational function that interpolates the data
  553. with approximation order :math:`O(h^{d+1})`. The rational function blends ``n - d``
  554. polynomials of degree `d` together to produce a rational interpolant that contains
  555. no poles on the real axis, unlike `AAA`. The interpolant is given
  556. by
  557. .. math::
  558. r(x) = \frac{\sum_{i=0}^{n-d} \lambda_i(x) p_i(x)}
  559. {\sum_{i=0}^{n-d} \lambda_i(x)},
  560. where :math:`p_i(x)` is an interpolating polynomial of at most degree `d` through
  561. the points :math:`(x_i,y_i),\dots,(x_{i+d},y_{i+d})`, and :math:`\lambda_i(z)` are
  562. blending functions defined by
  563. .. math::
  564. \lambda_i(x) = \frac{(-1)^i}{(x - x_i)\cdots(x - x_{i+d})}.
  565. When ``d = n - 1`` this reduces to polynomial interpolation.
  566. Due to its stability, the following barycentric representation of the above equation
  567. is used for computation
  568. .. math::
  569. r(z) = \frac{\sum_{k=1}^m\ w_k f_k / (x - x_k)}{\sum_{k=1}^m w_k / (x - x_k)},
  570. where the weights :math:`w_j` are computed as
  571. .. math::
  572. w_k &= (-1)^{k - d} \sum_{i \in J_k} \prod_{j = i, j \neq k}^{i + d}
  573. 1/|x_k - x_j|, \\
  574. J_k &= \{ i \in I: k - d \leq i \leq k\},\\
  575. I &= \{0, 1, \dots, n - d\}.
  576. References
  577. ----------
  578. .. [1] M.S. Floater and K. Hormann, "Barycentric rational interpolation with no
  579. poles and high rates of approximation", Numer. Math. 107, 315 (2007).
  580. :doi:`10.1007/s00211-007-0093-y`
  581. Examples
  582. --------
  583. Here we compare the method against polynomial interpolation for an example where
  584. the polynomial interpolation fails due to Runge's phenomenon.
  585. >>> import numpy as np
  586. >>> from scipy.interpolate import (FloaterHormannInterpolator,
  587. ... BarycentricInterpolator)
  588. >>> def f(x):
  589. ... return 1/(1 + x**2)
  590. >>> x = np.linspace(-5, 5, num=15)
  591. >>> r = FloaterHormannInterpolator(x, f(x))
  592. >>> p = BarycentricInterpolator(x, f(x))
  593. >>> xx = np.linspace(-5, 5, num=1000)
  594. >>> import matplotlib.pyplot as plt
  595. >>> fig, ax = plt.subplots()
  596. >>> ax.plot(xx, f(xx), label="f(x)")
  597. >>> ax.plot(xx, r(xx), "--", label="Floater-Hormann")
  598. >>> ax.plot(xx, p(xx), "--", label="Polynomial")
  599. >>> ax.legend()
  600. >>> plt.show()
  601. """
  602. def __init__(self, points, values, *, d=3, axis=0):
  603. super().__init__(points, values, d=d, axis=axis)
  604. def _input_validation(self, x, y, d):
  605. d = operator.index(d)
  606. if not (0 <= d < len(x)):
  607. raise ValueError("`d` must satisfy 0 <= d < n")
  608. super()._input_validation(x, y)
  609. def _compute_weights(self, z, f, d):
  610. # Floater and Hormann 2007 Eqn. (18) 3 equations later
  611. w = np.zeros_like(z, dtype=np.result_type(z, 1.0))
  612. n = w.size
  613. for k in range(n):
  614. for i in range(max(k-d, 0), min(k+1, n-d)):
  615. w[k] += 1/np.prod(np.abs(np.delete(z[k] - z[i : i + d + 1], k - i)))
  616. w *= (-1.)**(np.arange(n) - d)
  617. return z, f, w