test_differentiate.py 27 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690
  1. import math
  2. import pytest
  3. import numpy as np
  4. import scipy._lib._elementwise_iterative_method as eim
  5. import scipy._lib.array_api_extra as xpx
  6. from scipy._lib._array_api_no_0d import xp_assert_close, xp_assert_equal, xp_assert_less
  7. from scipy._lib._array_api import is_numpy, is_torch, make_xp_test_case
  8. from scipy import stats, optimize, special
  9. from scipy.differentiate import derivative, jacobian, hessian
  10. from scipy.differentiate._differentiate import _EERRORINCREASE
  11. @make_xp_test_case(derivative)
  12. class TestDerivative:
  13. def f(self, x):
  14. return special.ndtr(x)
  15. @pytest.mark.parametrize('x', [0.6, np.linspace(-0.05, 1.05, 10)])
  16. def test_basic(self, x, xp):
  17. # Invert distribution CDF and compare against distribution `ppf`
  18. default_dtype = xp.asarray(1.).dtype
  19. res = derivative(self.f, xp.asarray(x, dtype=default_dtype))
  20. ref = xp.asarray(stats.norm().pdf(x), dtype=default_dtype)
  21. xp_assert_close(res.df, ref)
  22. # This would be nice, but doesn't always work out. `error` is an
  23. # estimate, not a bound.
  24. if not is_torch(xp):
  25. xp_assert_less(xp.abs(res.df - ref), res.error)
  26. @pytest.mark.parametrize('case', stats._distr_params.distcont)
  27. def test_accuracy(self, case):
  28. distname, params = case
  29. dist = getattr(stats, distname)(*params)
  30. x = dist.median() + 0.1
  31. res = derivative(dist.cdf, x)
  32. ref = dist.pdf(x)
  33. xp_assert_close(res.df, ref, atol=1e-10)
  34. @pytest.mark.parametrize('order', [1, 6])
  35. @pytest.mark.parametrize('shape', [tuple(), (12,), (3, 4), (3, 2, 2)])
  36. def test_vectorization(self, order, shape, xp):
  37. # Test for correct functionality, output shapes, and dtypes for various
  38. # input shapes.
  39. x = np.linspace(-0.05, 1.05, 12).reshape(shape) if shape else 0.6
  40. n = np.size(x)
  41. state = {}
  42. @np.vectorize
  43. def _derivative_single(x):
  44. return derivative(self.f, x, order=order)
  45. def f(x, *args, **kwargs):
  46. state['nit'] += 1
  47. state['feval'] += 1 if (x.size == n or x.ndim <=1) else x.shape[-1]
  48. return self.f(x, *args, **kwargs)
  49. state['nit'] = -1
  50. state['feval'] = 0
  51. res = derivative(f, xp.asarray(x, dtype=xp.float64), order=order)
  52. refs = _derivative_single(x).ravel()
  53. ref_x = [ref.x for ref in refs]
  54. xp_assert_close(xp.reshape(res.x, (-1,)), xp.asarray(ref_x))
  55. ref_df = [ref.df for ref in refs]
  56. xp_assert_close(xp.reshape(res.df, (-1,)), xp.asarray(ref_df))
  57. ref_error = [ref.error for ref in refs]
  58. xp_assert_close(xp.reshape(res.error, (-1,)), xp.asarray(ref_error),
  59. atol=1e-12)
  60. ref_success = [bool(ref.success) for ref in refs]
  61. xp_assert_equal(xp.reshape(res.success, (-1,)), xp.asarray(ref_success))
  62. ref_flag = [np.int32(ref.status) for ref in refs]
  63. xp_assert_equal(xp.reshape(res.status, (-1,)), xp.asarray(ref_flag))
  64. ref_nfev = [np.int32(ref.nfev) for ref in refs]
  65. xp_assert_equal(xp.reshape(res.nfev, (-1,)), xp.asarray(ref_nfev))
  66. if is_numpy(xp): # can't expect other backends to be exactly the same
  67. assert xp.max(res.nfev) == state['feval']
  68. ref_nit = [np.int32(ref.nit) for ref in refs]
  69. xp_assert_equal(xp.reshape(res.nit, (-1,)), xp.asarray(ref_nit))
  70. if is_numpy(xp): # can't expect other backends to be exactly the same
  71. assert xp.max(res.nit) == state['nit']
  72. def test_flags(self, xp):
  73. # Test cases that should produce different status flags; show that all
  74. # can be produced simultaneously.
  75. rng = np.random.default_rng(5651219684984213)
  76. def f(xs, js):
  77. f.nit += 1
  78. funcs = [lambda x: x - 2.5, # converges
  79. lambda x: xp.exp(x)*rng.random(), # error increases
  80. lambda x: xp.exp(x), # reaches maxiter due to order=2
  81. lambda x: xp.full_like(x, xp.nan)] # stops due to NaN
  82. res = [funcs[int(j)](x) for x, j in zip(xs, xp.reshape(js, (-1,)))]
  83. return xp.stack(res)
  84. f.nit = 0
  85. args = (xp.arange(4, dtype=xp.int64),)
  86. res = derivative(f, xp.ones(4, dtype=xp.float64),
  87. tolerances=dict(rtol=1e-14),
  88. order=2, args=args)
  89. ref_flags = xp.asarray([eim._ECONVERGED,
  90. _EERRORINCREASE,
  91. eim._ECONVERR,
  92. eim._EVALUEERR], dtype=xp.int32)
  93. xp_assert_equal(res.status, ref_flags)
  94. def test_flags_preserve_shape(self, xp):
  95. # Same test as above but using `preserve_shape` option to simplify.
  96. rng = np.random.default_rng(5651219684984213)
  97. def f(x):
  98. out = [x - 2.5, # converges
  99. xp.exp(x)*rng.random(), # error increases
  100. xp.exp(x), # reaches maxiter due to order=2
  101. xp.full_like(x, xp.nan)] # stops due to NaN
  102. return xp.stack(out)
  103. res = derivative(f, xp.asarray(1, dtype=xp.float64),
  104. tolerances=dict(rtol=1e-14),
  105. order=2, preserve_shape=True)
  106. ref_flags = xp.asarray([eim._ECONVERGED,
  107. _EERRORINCREASE,
  108. eim._ECONVERR,
  109. eim._EVALUEERR], dtype=xp.int32)
  110. xp_assert_equal(res.status, ref_flags)
  111. def test_preserve_shape(self, xp):
  112. # Test `preserve_shape` option
  113. def f(x):
  114. out = [x, xp.sin(3*x), x+xp.sin(10*x), xp.sin(20*x)*(x-1)**2]
  115. return xp.stack(out)
  116. x = xp.asarray(0.)
  117. ref = xp.asarray([xp.asarray(1), 3*xp.cos(3*x), 1+10*xp.cos(10*x),
  118. 20*xp.cos(20*x)*(x-1)**2 + 2*xp.sin(20*x)*(x-1)])
  119. res = derivative(f, x, preserve_shape=True)
  120. xp_assert_close(res.df, ref)
  121. def test_convergence(self, xp):
  122. # Test that the convergence tolerances behave as expected
  123. x = xp.asarray(1., dtype=xp.float64)
  124. f = special.ndtr
  125. ref = float(stats.norm.pdf(1.))
  126. tolerances0 = dict(atol=0, rtol=0)
  127. tolerances = tolerances0.copy()
  128. tolerances['atol'] = 1e-3
  129. res1 = derivative(f, x, tolerances=tolerances, order=4)
  130. assert abs(res1.df - ref) < 1e-3
  131. tolerances['atol'] = 1e-6
  132. res2 = derivative(f, x, tolerances=tolerances, order=4)
  133. assert abs(res2.df - ref) < 1e-6
  134. assert abs(res2.df - ref) < abs(res1.df - ref)
  135. tolerances = tolerances0.copy()
  136. tolerances['rtol'] = 1e-3
  137. res1 = derivative(f, x, tolerances=tolerances, order=4)
  138. assert abs(res1.df - ref) < 1e-3 * ref
  139. tolerances['rtol'] = 1e-6
  140. res2 = derivative(f, x, tolerances=tolerances, order=4)
  141. assert abs(res2.df - ref) < 1e-6 * ref
  142. assert abs(res2.df - ref) < abs(res1.df - ref)
  143. def test_step_parameters(self, xp):
  144. # Test that step factors have the expected effect on accuracy
  145. x = xp.asarray(1., dtype=xp.float64)
  146. f = special.ndtr
  147. ref = float(stats.norm.pdf(1.))
  148. res1 = derivative(f, x, initial_step=0.5, maxiter=1)
  149. res2 = derivative(f, x, initial_step=0.05, maxiter=1)
  150. assert abs(res2.df - ref) < abs(res1.df - ref)
  151. res1 = derivative(f, x, step_factor=2, maxiter=1)
  152. res2 = derivative(f, x, step_factor=20, maxiter=1)
  153. assert abs(res2.df - ref) < abs(res1.df - ref)
  154. # `step_factor` can be less than 1: `initial_step` is the minimum step
  155. kwargs = dict(order=4, maxiter=1, step_direction=0)
  156. res = derivative(f, x, initial_step=0.5, step_factor=0.5, **kwargs)
  157. ref = derivative(f, x, initial_step=1, step_factor=2, **kwargs)
  158. xp_assert_close(res.df, ref.df, rtol=5e-15)
  159. # This is a similar test for one-sided difference
  160. kwargs = dict(order=2, maxiter=1, step_direction=1)
  161. res = derivative(f, x, initial_step=1, step_factor=2, **kwargs)
  162. ref = derivative(f, x, initial_step=1/np.sqrt(2), step_factor=0.5, **kwargs)
  163. xp_assert_close(res.df, ref.df, rtol=5e-15)
  164. kwargs['step_direction'] = -1
  165. res = derivative(f, x, initial_step=1, step_factor=2, **kwargs)
  166. ref = derivative(f, x, initial_step=1/np.sqrt(2), step_factor=0.5, **kwargs)
  167. xp_assert_close(res.df, ref.df, rtol=5e-15)
  168. def test_step_direction(self, xp):
  169. # test that `step_direction` works as expected
  170. def f(x):
  171. y = xp.exp(x)
  172. y = xpx.at(y)[(x < 0) + (x > 2)].set(xp.nan)
  173. return y
  174. x = xp.linspace(0, 2, 10)
  175. step_direction = xp.zeros_like(x)
  176. step_direction = xpx.at(step_direction)[x < 0.6].set(1)
  177. step_direction = xpx.at(step_direction)[x > 1.4].set(-1)
  178. res = derivative(f, x, step_direction=step_direction)
  179. xp_assert_close(res.df, xp.exp(x))
  180. assert xp.all(res.success)
  181. def test_vectorized_step_direction_args(self, xp):
  182. # test that `step_direction` and `args` are vectorized properly
  183. def f(x, p):
  184. return x ** p
  185. def df(x, p):
  186. return p * x ** (p - 1)
  187. x = xp.reshape(xp.asarray([1, 2, 3, 4]), (-1, 1, 1))
  188. hdir = xp.reshape(xp.asarray([-1, 0, 1]), (1, -1, 1))
  189. p = xp.reshape(xp.asarray([2, 3]), (1, 1, -1))
  190. res = derivative(f, x, step_direction=hdir, args=(p,))
  191. ref = xp.broadcast_to(df(x, p), res.df.shape)
  192. ref = xp.asarray(ref, dtype=xp.asarray(1.).dtype)
  193. xp_assert_close(res.df, ref)
  194. def test_initial_step(self, xp):
  195. # Test that `initial_step` works as expected and is vectorized
  196. def f(x):
  197. return xp.exp(x)
  198. x = xp.asarray(0., dtype=xp.float64)
  199. step_direction = xp.asarray([-1, 0, 1])
  200. h0 = xp.reshape(xp.logspace(-3, 0, 10), (-1, 1))
  201. res = derivative(f, x, initial_step=h0, order=2, maxiter=1,
  202. step_direction=step_direction)
  203. err = xp.abs(res.df - f(x))
  204. # error should be smaller for smaller step sizes
  205. assert xp.all(err[:-1, ...] < err[1:, ...])
  206. # results of vectorized call should match results with
  207. # initial_step taken one at a time
  208. for i in range(h0.shape[0]):
  209. ref = derivative(f, x, initial_step=h0[i, 0], order=2, maxiter=1,
  210. step_direction=step_direction)
  211. xp_assert_close(res.df[i, :], ref.df, rtol=1e-14)
  212. def test_maxiter_callback(self, xp):
  213. # Test behavior of `maxiter` parameter and `callback` interface
  214. x = xp.asarray(0.612814, dtype=xp.float64)
  215. maxiter = 3
  216. def f(x):
  217. res = special.ndtr(x)
  218. return res
  219. default_order = 8
  220. res = derivative(f, x, maxiter=maxiter, tolerances=dict(rtol=1e-15))
  221. assert not xp.any(res.success)
  222. assert xp.all(res.nfev == default_order + 1 + (maxiter - 1)*2)
  223. assert xp.all(res.nit == maxiter)
  224. def callback(res):
  225. callback.iter += 1
  226. callback.res = res
  227. assert hasattr(res, 'x')
  228. assert float(res.df) not in callback.dfs
  229. callback.dfs.add(float(res.df))
  230. assert res.status == eim._EINPROGRESS
  231. if callback.iter == maxiter:
  232. raise StopIteration
  233. callback.iter = -1 # callback called once before first iteration
  234. callback.res = None
  235. callback.dfs = set()
  236. res2 = derivative(f, x, callback=callback, tolerances=dict(rtol=1e-15))
  237. # terminating with callback is identical to terminating due to maxiter
  238. # (except for `status`)
  239. for key in res.keys():
  240. if key == 'status':
  241. assert res[key] == eim._ECONVERR
  242. assert res2[key] == eim._ECALLBACK
  243. elif key == 'error':
  244. # switched from equality check to accommodate
  245. # macosx-x86_64/Accelerate
  246. xp_assert_close(res2[key], res[key], atol=1e-14)
  247. xp_assert_close(callback.res[key], res[key], atol=1e-14)
  248. else:
  249. assert res2[key] == callback.res[key] == res[key]
  250. @pytest.mark.parametrize("hdir", (-1, 0, 1))
  251. @pytest.mark.parametrize("x", (0.65, [0.65, 0.7]))
  252. @pytest.mark.parametrize("dtype", ('float32', 'float64'))
  253. def test_dtype(self, hdir, x, dtype, xp):
  254. # Test that dtypes are preserved
  255. dtype = getattr(xp, dtype)
  256. x = xp.asarray(x, dtype=dtype)
  257. def f(x):
  258. assert x.dtype == dtype
  259. return xp.exp(x)
  260. def callback(res):
  261. assert res.x.dtype == dtype
  262. assert res.df.dtype == dtype
  263. assert res.error.dtype == dtype
  264. res = derivative(f, x, order=4, step_direction=hdir, callback=callback)
  265. assert res.x.dtype == dtype
  266. assert res.df.dtype == dtype
  267. assert res.error.dtype == dtype
  268. eps = xp.finfo(dtype).eps
  269. # not sure why torch is less accurate here; might be worth investigating
  270. rtol = eps**0.5 * 50 if is_torch(xp) else eps**0.5
  271. xp_assert_close(res.df, xp.exp(res.x), rtol=rtol)
  272. def test_input_validation(self, xp):
  273. # Test input validation for appropriate error messages
  274. one = xp.asarray(1)
  275. message = '`f` must be callable.'
  276. with pytest.raises(ValueError, match=message):
  277. derivative(None, one)
  278. message = 'Abscissae and function output must be real numbers.'
  279. with pytest.raises(ValueError, match=message):
  280. derivative(lambda x: x, xp.asarray(-4+1j))
  281. message = "When `preserve_shape=False`, the shape of the array..."
  282. with pytest.raises(ValueError, match=message):
  283. derivative(lambda x: [1, 2, 3], xp.asarray([-2, -3]))
  284. message = 'Tolerances and step parameters must be non-negative...'
  285. with pytest.raises(ValueError, match=message):
  286. derivative(lambda x: x, one, tolerances=dict(atol=-1))
  287. with pytest.raises(ValueError, match=message):
  288. derivative(lambda x: x, one, tolerances=dict(rtol='ekki'))
  289. with pytest.raises(ValueError, match=message):
  290. derivative(lambda x: x, one, step_factor=object())
  291. message = '`maxiter` must be a positive integer.'
  292. with pytest.raises(ValueError, match=message):
  293. derivative(lambda x: x, one, maxiter=1.5)
  294. with pytest.raises(ValueError, match=message):
  295. derivative(lambda x: x, one, maxiter=0)
  296. message = '`order` must be a positive integer'
  297. with pytest.raises(ValueError, match=message):
  298. derivative(lambda x: x, one, order=1.5)
  299. with pytest.raises(ValueError, match=message):
  300. derivative(lambda x: x, one, order=0)
  301. message = '`preserve_shape` must be True or False.'
  302. with pytest.raises(ValueError, match=message):
  303. derivative(lambda x: x, one, preserve_shape='herring')
  304. message = '`callback` must be callable.'
  305. with pytest.raises(ValueError, match=message):
  306. derivative(lambda x: x, one, callback='shrubbery')
  307. def test_special_cases(self, xp):
  308. # Test edge cases and other special cases
  309. # Test that integers are not passed to `f`
  310. # (otherwise this would overflow)
  311. def f(x):
  312. assert xp.isdtype(x.dtype, 'real floating')
  313. return x ** 99 - 1
  314. if not is_torch(xp): # torch defaults to float32
  315. res = derivative(f, xp.asarray(7), tolerances=dict(rtol=1e-10))
  316. assert res.success
  317. xp_assert_close(res.df, xp.asarray(99*7.**98))
  318. # Test invalid step size and direction
  319. res = derivative(xp.exp, xp.asarray(1), step_direction=xp.nan)
  320. xp_assert_equal(res.df, xp.asarray(xp.nan))
  321. xp_assert_equal(res.status, xp.asarray(-3, dtype=xp.int32))
  322. res = derivative(xp.exp, xp.asarray(1), initial_step=0)
  323. xp_assert_equal(res.df, xp.asarray(xp.nan))
  324. xp_assert_equal(res.status, xp.asarray(-3, dtype=xp.int32))
  325. # Test that if success is achieved in the correct number
  326. # of iterations if function is a polynomial. Ideally, all polynomials
  327. # of order 0-2 would get exact result with 0 refinement iterations,
  328. # all polynomials of order 3-4 would be differentiated exactly after
  329. # 1 iteration, etc. However, it seems that `derivative` needs an
  330. # extra iteration to detect convergence based on the error estimate.
  331. for n in range(6):
  332. x = xp.asarray(1.5, dtype=xp.float64)
  333. def f(x):
  334. return 2*x**n
  335. ref = 2*n*x**(n-1)
  336. res = derivative(f, x, maxiter=1, order=max(1, n))
  337. xp_assert_close(res.df, ref, rtol=1e-15)
  338. xp_assert_equal(res.error, xp.asarray(xp.nan, dtype=xp.float64))
  339. res = derivative(f, x, order=max(1, n))
  340. assert res.success
  341. assert res.nit == 2
  342. xp_assert_close(res.df, ref, rtol=1e-15)
  343. # Test scalar `args` (not in tuple)
  344. def f(x, c):
  345. return c*x - 1
  346. res = derivative(f, xp.asarray(2), args=xp.asarray(3))
  347. xp_assert_close(res.df, xp.asarray(3.))
  348. # no need to run a test on multiple backends if it's xfailed
  349. @pytest.mark.skip_xp_backends(np_only=True)
  350. @pytest.mark.xfail
  351. @pytest.mark.parametrize("case", ( # function, evaluation point
  352. (lambda x: (x - 1) ** 3, 1),
  353. (lambda x: np.where(x > 1, (x - 1) ** 5, (x - 1) ** 3), 1)
  354. ))
  355. def test_saddle_gh18811(self, case, xp):
  356. # With default settings, `derivative` will not always converge when
  357. # the true derivative is exactly zero. This tests that specifying a
  358. # (tight) `atol` alleviates the problem. See discussion in gh-18811.
  359. atol = 1e-16
  360. res = derivative(*case, step_direction=[-1, 0, 1], atol=atol)
  361. assert np.all(res.success)
  362. xp_assert_close(res.df, 0, atol=atol)
  363. class JacobianHessianTest:
  364. def test_iv(self, xp):
  365. jh_func = self.jh_func.__func__
  366. # Test input validation
  367. message = "Argument `x` must be at least 1-D."
  368. with pytest.raises(ValueError, match=message):
  369. jh_func(xp.sin, 1, tolerances=dict(atol=-1))
  370. # Confirm that other parameters are being passed to `derivative`,
  371. # which raises an appropriate error message.
  372. x = xp.ones(3)
  373. func = optimize.rosen
  374. message = 'Tolerances and step parameters must be non-negative scalars.'
  375. with pytest.raises(ValueError, match=message):
  376. jh_func(func, x, tolerances=dict(atol=-1))
  377. with pytest.raises(ValueError, match=message):
  378. jh_func(func, x, tolerances=dict(rtol=-1))
  379. with pytest.raises(ValueError, match=message):
  380. jh_func(func, x, step_factor=-1)
  381. message = '`order` must be a positive integer.'
  382. with pytest.raises(ValueError, match=message):
  383. jh_func(func, x, order=-1)
  384. message = '`maxiter` must be a positive integer.'
  385. with pytest.raises(ValueError, match=message):
  386. jh_func(func, x, maxiter=-1)
  387. @make_xp_test_case(jacobian)
  388. class TestJacobian(JacobianHessianTest):
  389. jh_func = jacobian
  390. # Example functions and Jacobians from Wikipedia:
  391. # https://en.wikipedia.org/wiki/Jacobian_matrix_and_determinant#Examples
  392. def f1(z, xp):
  393. x, y = z
  394. return xp.stack([x ** 2 * y, 5 * x + xp.sin(y)])
  395. def df1(z):
  396. x, y = z
  397. return [[2 * x * y, x ** 2], [np.full_like(x, 5), np.cos(y)]]
  398. f1.mn = 2, 2 # type: ignore[attr-defined]
  399. f1.ref = df1 # type: ignore[attr-defined]
  400. def f2(z, xp):
  401. r, phi = z
  402. return xp.stack([r * xp.cos(phi), r * xp.sin(phi)])
  403. def df2(z):
  404. r, phi = z
  405. return [[np.cos(phi), -r * np.sin(phi)],
  406. [np.sin(phi), r * np.cos(phi)]]
  407. f2.mn = 2, 2 # type: ignore[attr-defined]
  408. f2.ref = df2 # type: ignore[attr-defined]
  409. def f3(z, xp):
  410. r, phi, th = z
  411. return xp.stack([r * xp.sin(phi) * xp.cos(th), r * xp.sin(phi) * xp.sin(th),
  412. r * xp.cos(phi)])
  413. def df3(z):
  414. r, phi, th = z
  415. return [[np.sin(phi) * np.cos(th), r * np.cos(phi) * np.cos(th),
  416. -r * np.sin(phi) * np.sin(th)],
  417. [np.sin(phi) * np.sin(th), r * np.cos(phi) * np.sin(th),
  418. r * np.sin(phi) * np.cos(th)],
  419. [np.cos(phi), -r * np.sin(phi), np.zeros_like(r)]]
  420. f3.mn = 3, 3 # type: ignore[attr-defined]
  421. f3.ref = df3 # type: ignore[attr-defined]
  422. def f4(x, xp):
  423. x1, x2, x3 = x
  424. return xp.stack([x1, 5 * x3, 4 * x2 ** 2 - 2 * x3, x3 * xp.sin(x1)])
  425. def df4(x):
  426. x1, x2, x3 = x
  427. one = np.ones_like(x1)
  428. return [[one, 0 * one, 0 * one],
  429. [0 * one, 0 * one, 5 * one],
  430. [0 * one, 8 * x2, -2 * one],
  431. [x3 * np.cos(x1), 0 * one, np.sin(x1)]]
  432. f4.mn = 3, 4 # type: ignore[attr-defined]
  433. f4.ref = df4 # type: ignore[attr-defined]
  434. def f5(x, xp):
  435. x1, x2, x3 = x
  436. return xp.stack([5 * x2, 4 * x1 ** 2 - 2 * xp.sin(x2 * x3), x2 * x3])
  437. def df5(x):
  438. x1, x2, x3 = x
  439. one = np.ones_like(x1)
  440. return [[0 * one, 5 * one, 0 * one],
  441. [8 * x1, -2 * x3 * np.cos(x2 * x3), -2 * x2 * np.cos(x2 * x3)],
  442. [0 * one, x3, x2]]
  443. f5.mn = 3, 3 # type: ignore[attr-defined]
  444. f5.ref = df5 # type: ignore[attr-defined]
  445. def rosen(x, _): return optimize.rosen(x)
  446. rosen.mn = 5, 1 # type: ignore[attr-defined]
  447. rosen.ref = optimize.rosen_der # type: ignore[attr-defined]
  448. @pytest.mark.parametrize('dtype', ('float32', 'float64'))
  449. @pytest.mark.parametrize('size', [(), (6,), (2, 3)])
  450. @pytest.mark.parametrize('func', [f1, f2, f3, f4, f5, rosen])
  451. def test_examples(self, dtype, size, func, xp):
  452. atol = 1e-10 if dtype == 'float64' else 1.99e-3
  453. dtype = getattr(xp, dtype)
  454. rng = np.random.default_rng(458912319542)
  455. m, n = func.mn
  456. x = rng.random(size=(m,) + size)
  457. res = jacobian(lambda x: func(x , xp), xp.asarray(x, dtype=dtype))
  458. # convert list of arrays to single array before converting to xp array
  459. ref = xp.asarray(np.asarray(func.ref(x)), dtype=dtype)
  460. xp_assert_close(res.df, ref, atol=atol)
  461. def test_attrs(self, xp):
  462. # Test attributes of result object
  463. z = xp.asarray([0.5, 0.25])
  464. # case in which some elements of the Jacobian are harder
  465. # to calculate than others
  466. def df1(z):
  467. x, y = z
  468. return xp.stack([xp.cos(0.5*x) * xp.cos(y), xp.sin(2*x) * y**2])
  469. def df1_0xy(x, y):
  470. return xp.cos(0.5*x) * xp.cos(y)
  471. def df1_1xy(x, y):
  472. return xp.sin(2*x) * y**2
  473. res = jacobian(df1, z, initial_step=10)
  474. # FIXME https://github.com/scipy/scipy/pull/22320#discussion_r1914898175
  475. if not is_torch(xp):
  476. assert xpx.nunique(res.nit) == 4
  477. assert xpx.nunique(res.nfev) == 4
  478. res00 = jacobian(lambda x: df1_0xy(x, z[1]), z[0:1], initial_step=10)
  479. res01 = jacobian(lambda y: df1_0xy(z[0], y), z[1:2], initial_step=10)
  480. res10 = jacobian(lambda x: df1_1xy(x, z[1]), z[0:1], initial_step=10)
  481. res11 = jacobian(lambda y: df1_1xy(z[0], y), z[1:2], initial_step=10)
  482. ref = optimize.OptimizeResult()
  483. for attr in ['success', 'status', 'df', 'nit', 'nfev']:
  484. ref_attr = xp.asarray([[getattr(res00, attr), getattr(res01, attr)],
  485. [getattr(res10, attr), getattr(res11, attr)]])
  486. ref[attr] = xp.squeeze(
  487. ref_attr,
  488. axis=tuple(ax for ax, size in enumerate(ref_attr.shape) if size == 1)
  489. )
  490. rtol = 1.5e-5 if res[attr].dtype == xp.float32 else 1.5e-14
  491. xp_assert_close(res[attr], ref[attr], rtol=rtol)
  492. def test_step_direction_size(self, xp):
  493. # Check that `step_direction` and `initial_step` can be used to ensure that
  494. # the usable domain of a function is respected.
  495. rng = np.random.default_rng(23892589425245)
  496. b = rng.random(3)
  497. eps = 1e-7 # torch needs wiggle room?
  498. def f(x):
  499. x = xpx.at(x)[0, x[0] < b[0]].set(xp.nan)
  500. x = xpx.at(x)[0, x[0] > b[0] + 0.25].set(xp.nan)
  501. x = xpx.at(x)[1, x[1] > b[1]].set(xp.nan)
  502. x = xpx.at(x)[1, x[1] < b[1] - 0.1-eps].set(xp.nan)
  503. return TestJacobian.f5(x, xp)
  504. dir = [1, -1, 0]
  505. h0 = [0.25, 0.1, 0.5]
  506. atol = {'atol': 1e-8}
  507. res = jacobian(f, xp.asarray(b, dtype=xp.float64), initial_step=h0,
  508. step_direction=dir, tolerances=atol)
  509. ref = xp.asarray(TestJacobian.df5(b), dtype=xp.float64)
  510. xp_assert_close(res.df, ref, atol=1e-8)
  511. assert xp.all(xp.isfinite(ref))
  512. @make_xp_test_case(hessian)
  513. class TestHessian(JacobianHessianTest):
  514. jh_func = hessian
  515. @pytest.mark.parametrize('shape', [(), (4,), (2, 4)])
  516. def test_example(self, shape, xp):
  517. rng = np.random.default_rng(458912319542)
  518. m = 3
  519. x = xp.asarray(rng.random((m,) + shape), dtype=xp.float64)
  520. res = hessian(optimize.rosen, x)
  521. if shape:
  522. x = xp.reshape(x, (m, -1))
  523. ref = xp.stack([optimize.rosen_hess(xi) for xi in x.T])
  524. ref = xp.moveaxis(ref, 0, -1)
  525. ref = xp.reshape(ref, (m, m,) + shape)
  526. else:
  527. ref = optimize.rosen_hess(x)
  528. xp_assert_close(res.ddf, ref, atol=1e-8)
  529. # # Removed symmetry enforcement; consider adding back in as a feature
  530. # # check symmetry
  531. # for key in ['ddf', 'error', 'nfev', 'success', 'status']:
  532. # assert_equal(res[key], np.swapaxes(res[key], 0, 1))
  533. def test_float32(self, xp):
  534. rng = np.random.default_rng(458912319542)
  535. x = xp.asarray(rng.random(3), dtype=xp.float32)
  536. res = hessian(optimize.rosen, x)
  537. ref = optimize.rosen_hess(x)
  538. mask = (ref != 0)
  539. xp_assert_close(res.ddf[mask], ref[mask])
  540. atol = 1e-2 * xp.abs(xp.min(ref[mask]))
  541. xp_assert_close(res.ddf[~mask], ref[~mask], atol=atol)
  542. def test_nfev(self, xp):
  543. z = xp.asarray([0.5, 0.25])
  544. def f1(z):
  545. x, y = xp.broadcast_arrays(*z)
  546. f1.nfev = f1.nfev + (math.prod(x.shape[2:]) if x.ndim > 2 else 1)
  547. return xp.sin(x) * y ** 3
  548. f1.nfev = 0
  549. res = hessian(f1, z, initial_step=10)
  550. f1.nfev = 0
  551. res00 = hessian(lambda x: f1([x[0], z[1]]), z[0:1], initial_step=10)
  552. assert res.nfev[0, 0] == f1.nfev == res00.nfev[0, 0]
  553. f1.nfev = 0
  554. res11 = hessian(lambda y: f1([z[0], y[0]]), z[1:2], initial_step=10)
  555. assert res.nfev[1, 1] == f1.nfev == res11.nfev[0, 0]
  556. # Removed symmetry enforcement; consider adding back in as a feature
  557. # assert_equal(res.nfev, res.nfev.T) # check symmetry
  558. # assert np.unique(res.nfev).size == 3
  559. @pytest.mark.skip_xp_backends(np_only=True,
  560. reason='Python list input uses NumPy backend')
  561. def test_small_rtol_warning(self, xp):
  562. message = 'The specified `rtol=1e-15`, but...'
  563. with pytest.warns(RuntimeWarning, match=message):
  564. hessian(xp.sin, [1.], tolerances=dict(rtol=1e-15))