test__numdiff.py 34 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885
  1. import math
  2. from itertools import product
  3. import numpy as np
  4. from numpy.testing import assert_allclose, assert_equal, assert_
  5. import pytest
  6. from pytest import raises as assert_raises
  7. from scipy._lib._util import MapWrapper, _ScalarFunctionWrapper
  8. from scipy.sparse import csr_array, csc_array, lil_array
  9. from scipy.optimize._numdiff import (
  10. _adjust_scheme_to_bounds, approx_derivative, check_derivative,
  11. group_columns, _eps_for_method, _compute_absolute_step)
  12. from scipy.optimize import rosen
  13. def test_group_columns():
  14. structure = [
  15. [1, 1, 0, 0, 0, 0],
  16. [1, 1, 1, 0, 0, 0],
  17. [0, 1, 1, 1, 0, 0],
  18. [0, 0, 1, 1, 1, 0],
  19. [0, 0, 0, 1, 1, 1],
  20. [0, 0, 0, 0, 1, 1],
  21. [0, 0, 0, 0, 0, 0]
  22. ]
  23. for transform in [np.asarray, csr_array, csc_array, lil_array]:
  24. A = transform(structure)
  25. order = np.arange(6)
  26. groups_true = np.array([0, 1, 2, 0, 1, 2])
  27. groups = group_columns(A, order)
  28. assert_equal(groups, groups_true)
  29. order = [1, 2, 4, 3, 5, 0]
  30. groups_true = np.array([2, 0, 1, 2, 0, 1])
  31. groups = group_columns(A, order)
  32. assert_equal(groups, groups_true)
  33. # Test repeatability.
  34. groups_1 = group_columns(A)
  35. groups_2 = group_columns(A)
  36. assert_equal(groups_1, groups_2)
  37. def test_correct_fp_eps():
  38. # check that relative step size is correct for FP size
  39. EPS = np.finfo(np.float64).eps
  40. relative_step = {"2-point": EPS**0.5,
  41. "3-point": EPS**(1/3),
  42. "cs": EPS**0.5}
  43. for method in ['2-point', '3-point', 'cs']:
  44. assert_allclose(
  45. _eps_for_method(np.float64, np.float64, method),
  46. relative_step[method])
  47. assert_allclose(
  48. _eps_for_method(np.complex128, np.complex128, method),
  49. relative_step[method]
  50. )
  51. # check another FP size
  52. EPS = np.finfo(np.float32).eps
  53. relative_step = {"2-point": EPS**0.5,
  54. "3-point": EPS**(1/3),
  55. "cs": EPS**0.5}
  56. for method in ['2-point', '3-point', 'cs']:
  57. assert_allclose(
  58. _eps_for_method(np.float64, np.float32, method),
  59. relative_step[method]
  60. )
  61. assert_allclose(
  62. _eps_for_method(np.float32, np.float64, method),
  63. relative_step[method]
  64. )
  65. assert_allclose(
  66. _eps_for_method(np.float32, np.float32, method),
  67. relative_step[method]
  68. )
  69. class TestAdjustSchemeToBounds:
  70. def test_no_bounds(self):
  71. x0 = np.zeros(3)
  72. h = np.full(3, 1e-2)
  73. inf_lower = np.empty_like(x0)
  74. inf_upper = np.empty_like(x0)
  75. inf_lower.fill(-np.inf)
  76. inf_upper.fill(np.inf)
  77. h_adjusted, one_sided = _adjust_scheme_to_bounds(
  78. x0, h, 1, '1-sided', inf_lower, inf_upper)
  79. assert_allclose(h_adjusted, h)
  80. assert_(np.all(one_sided))
  81. h_adjusted, one_sided = _adjust_scheme_to_bounds(
  82. x0, h, 2, '1-sided', inf_lower, inf_upper)
  83. assert_allclose(h_adjusted, h)
  84. assert_(np.all(one_sided))
  85. h_adjusted, one_sided = _adjust_scheme_to_bounds(
  86. x0, h, 1, '2-sided', inf_lower, inf_upper)
  87. assert_allclose(h_adjusted, h)
  88. assert_(np.all(~one_sided))
  89. h_adjusted, one_sided = _adjust_scheme_to_bounds(
  90. x0, h, 2, '2-sided', inf_lower, inf_upper)
  91. assert_allclose(h_adjusted, h)
  92. assert_(np.all(~one_sided))
  93. def test_with_bound(self):
  94. x0 = np.array([0.0, 0.85, -0.85])
  95. lb = -np.ones(3)
  96. ub = np.ones(3)
  97. h = np.array([1, 1, -1]) * 1e-1
  98. h_adjusted, _ = _adjust_scheme_to_bounds(x0, h, 1, '1-sided', lb, ub)
  99. assert_allclose(h_adjusted, h)
  100. h_adjusted, _ = _adjust_scheme_to_bounds(x0, h, 2, '1-sided', lb, ub)
  101. assert_allclose(h_adjusted, np.array([1, -1, 1]) * 1e-1)
  102. h_adjusted, one_sided = _adjust_scheme_to_bounds(
  103. x0, h, 1, '2-sided', lb, ub)
  104. assert_allclose(h_adjusted, np.abs(h))
  105. assert_(np.all(~one_sided))
  106. h_adjusted, one_sided = _adjust_scheme_to_bounds(
  107. x0, h, 2, '2-sided', lb, ub)
  108. assert_allclose(h_adjusted, np.array([1, -1, 1]) * 1e-1)
  109. assert_equal(one_sided, np.array([False, True, True]))
  110. def test_tight_bounds(self):
  111. lb = np.array([-0.03, -0.03])
  112. ub = np.array([0.05, 0.05])
  113. x0 = np.array([0.0, 0.03])
  114. h = np.array([-0.1, -0.1])
  115. h_adjusted, _ = _adjust_scheme_to_bounds(x0, h, 1, '1-sided', lb, ub)
  116. assert_allclose(h_adjusted, np.array([0.05, -0.06]))
  117. h_adjusted, _ = _adjust_scheme_to_bounds(x0, h, 2, '1-sided', lb, ub)
  118. assert_allclose(h_adjusted, np.array([0.025, -0.03]))
  119. h_adjusted, one_sided = _adjust_scheme_to_bounds(
  120. x0, h, 1, '2-sided', lb, ub)
  121. assert_allclose(h_adjusted, np.array([0.03, -0.03]))
  122. assert_equal(one_sided, np.array([False, True]))
  123. h_adjusted, one_sided = _adjust_scheme_to_bounds(
  124. x0, h, 2, '2-sided', lb, ub)
  125. assert_allclose(h_adjusted, np.array([0.015, -0.015]))
  126. assert_equal(one_sided, np.array([False, True]))
  127. class TestApproxDerivativesDense:
  128. def fun_scalar_scalar(self, x):
  129. return np.sinh(x)
  130. def jac_scalar_scalar(self, x):
  131. return np.cosh(x)
  132. def fun_scalar_vector(self, x):
  133. return np.array([x[0]**2, np.tan(x[0]), np.exp(x[0])])
  134. def jac_scalar_vector(self, x):
  135. return np.array(
  136. [2 * x[0], np.cos(x[0]) ** -2, np.exp(x[0])]).reshape(-1, 1)
  137. def fun_vector_scalar(self, x):
  138. return np.sin(x[0] * x[1]) * np.log(x[0])
  139. def wrong_dimensions_fun(self, x):
  140. return np.array([x**2, np.tan(x), np.exp(x)])
  141. def jac_vector_scalar(self, x):
  142. return np.array([
  143. x[1] * np.cos(x[0] * x[1]) * np.log(x[0]) +
  144. np.sin(x[0] * x[1]) / x[0],
  145. x[0] * np.cos(x[0] * x[1]) * np.log(x[0])
  146. ])
  147. def fun_vector_vector(self, x):
  148. return np.array([
  149. x[0] * np.sin(x[1]),
  150. x[1] * np.cos(x[0]),
  151. x[0] ** 3 * x[1] ** -0.5
  152. ])
  153. def fun_vector_vector_with_arg(self, x, arg):
  154. """Used to test passing custom arguments with check_derivative()"""
  155. assert arg == 42
  156. return np.array([
  157. x[0] * np.sin(x[1]),
  158. x[1] * np.cos(x[0]),
  159. x[0] ** 3 * x[1] ** -0.5
  160. ])
  161. def jac_vector_vector(self, x):
  162. return np.array([
  163. [np.sin(x[1]), x[0] * np.cos(x[1])],
  164. [-x[1] * np.sin(x[0]), np.cos(x[0])],
  165. [3 * x[0] ** 2 * x[1] ** -0.5, -0.5 * x[0] ** 3 * x[1] ** -1.5]
  166. ])
  167. def jac_vector_vector_with_arg(self, x, arg):
  168. """Used to test passing custom arguments with check_derivative()"""
  169. assert arg == 42
  170. return np.array([
  171. [np.sin(x[1]), x[0] * np.cos(x[1])],
  172. [-x[1] * np.sin(x[0]), np.cos(x[0])],
  173. [3 * x[0] ** 2 * x[1] ** -0.5, -0.5 * x[0] ** 3 * x[1] ** -1.5]
  174. ])
  175. def fun_parametrized(self, x, c0, c1=1.0):
  176. return np.array([np.exp(c0 * x[0]), np.exp(c1 * x[1])])
  177. def jac_parametrized(self, x, c0, c1=0.1):
  178. return np.array([
  179. [c0 * np.exp(c0 * x[0]), 0],
  180. [0, c1 * np.exp(c1 * x[1])]
  181. ])
  182. def fun_with_nan(self, x):
  183. return x if np.abs(x) <= 1e-8 else np.nan
  184. def jac_with_nan(self, x):
  185. return 1.0 if np.abs(x) <= 1e-8 else np.nan
  186. def fun_zero_jacobian(self, x):
  187. return np.array([x[0] * x[1], np.cos(x[0] * x[1])])
  188. def jac_zero_jacobian(self, x):
  189. return np.array([
  190. [x[1], x[0]],
  191. [-x[1] * np.sin(x[0] * x[1]), -x[0] * np.sin(x[0] * x[1])]
  192. ])
  193. def jac_non_numpy(self, x):
  194. # x can be a scalar or an array [val].
  195. # Cast to true scalar before handing over to math.exp
  196. xp = np.asarray(x).item()
  197. return math.exp(xp)
  198. def test_scalar_scalar(self):
  199. x0 = 1.0
  200. jac_diff_2 = approx_derivative(self.fun_scalar_scalar, x0,
  201. method='2-point')
  202. jac_diff_3 = approx_derivative(self.fun_scalar_scalar, x0)
  203. jac_diff_4 = approx_derivative(self.fun_scalar_scalar, x0,
  204. method='cs')
  205. jac_true = self.jac_scalar_scalar(x0)
  206. assert_allclose(jac_diff_2, jac_true, rtol=1e-6)
  207. assert_allclose(jac_diff_3, jac_true, rtol=1e-9)
  208. assert_allclose(jac_diff_4, jac_true, rtol=1e-12)
  209. def test_scalar_scalar_abs_step(self):
  210. # can approx_derivative use abs_step?
  211. x0 = 1.0
  212. jac_diff_2 = approx_derivative(self.fun_scalar_scalar, x0,
  213. method='2-point', abs_step=1.49e-8)
  214. jac_diff_3 = approx_derivative(self.fun_scalar_scalar, x0,
  215. abs_step=1.49e-8)
  216. jac_diff_4 = approx_derivative(self.fun_scalar_scalar, x0,
  217. method='cs', abs_step=1.49e-8)
  218. jac_true = self.jac_scalar_scalar(x0)
  219. assert_allclose(jac_diff_2, jac_true, rtol=1e-6)
  220. assert_allclose(jac_diff_3, jac_true, rtol=1e-9)
  221. assert_allclose(jac_diff_4, jac_true, rtol=1e-12)
  222. def test_scalar_vector(self):
  223. x0 = 0.5
  224. with MapWrapper(2) as mapper:
  225. jac_diff_2 = approx_derivative(self.fun_scalar_vector, x0,
  226. method='2-point', workers=mapper)
  227. jac_diff_3 = approx_derivative(self.fun_scalar_vector, x0, workers=map)
  228. jac_diff_4 = approx_derivative(self.fun_scalar_vector, x0,
  229. method='cs', workers=None)
  230. jac_true = self.jac_scalar_vector(np.atleast_1d(x0))
  231. assert_allclose(jac_diff_2, jac_true, rtol=1e-6)
  232. assert_allclose(jac_diff_3, jac_true, rtol=1e-9)
  233. assert_allclose(jac_diff_4, jac_true, rtol=1e-12)
  234. @pytest.mark.fail_slow(5.0)
  235. def test_workers_evaluations_and_nfev(self):
  236. # check that nfev consumed by approx_derivative is tracked properly
  237. # and that parallel evaluation is same as series
  238. x0 = [0.5, 1.5, 2.0]
  239. with MapWrapper(2) as mapper:
  240. md2, mdct2 = approx_derivative(rosen, x0,
  241. method='2-point', workers=mapper,
  242. full_output=True)
  243. md3, mdct3 = approx_derivative(rosen, x0,
  244. workers=mapper, full_output=True)
  245. # supply a number for workers. This is not normally recommended
  246. # for upstream workers as setting up processes incurs a large overhead
  247. md4, mdct4 = approx_derivative(rosen, x0,
  248. method='cs', workers=2,
  249. full_output=True)
  250. sfr = _ScalarFunctionWrapper(rosen)
  251. d2, dct2 = approx_derivative(sfr, x0, method='2-point', full_output=True)
  252. assert_equal(dct2['nfev'], sfr.nfev)
  253. sfr.nfev = 0
  254. d3, dct3 = approx_derivative(sfr, x0, full_output=True)
  255. assert_equal(dct3['nfev'], sfr.nfev)
  256. sfr.nfev = 0
  257. d4, dct4 = approx_derivative(sfr, x0, method='cs', full_output=True)
  258. assert_equal(dct4['nfev'], sfr.nfev)
  259. assert_equal(mdct2['nfev'], dct2['nfev'])
  260. assert_equal(mdct3['nfev'], dct3['nfev'])
  261. assert_equal(mdct4['nfev'], dct4['nfev'])
  262. # also check that gradients are equivalent
  263. assert_equal(md2, d2)
  264. assert_equal(md3, d3)
  265. assert_equal(md4, d4)
  266. def test_vector_scalar(self):
  267. x0 = np.array([100.0, -0.5])
  268. jac_diff_2 = approx_derivative(self.fun_vector_scalar, x0,
  269. method='2-point')
  270. jac_diff_3 = approx_derivative(self.fun_vector_scalar, x0)
  271. jac_diff_4 = approx_derivative(self.fun_vector_scalar, x0,
  272. method='cs')
  273. jac_true = self.jac_vector_scalar(x0)
  274. assert_allclose(jac_diff_2, jac_true, rtol=1e-6)
  275. assert_allclose(jac_diff_3, jac_true, rtol=1e-7)
  276. assert_allclose(jac_diff_4, jac_true, rtol=1e-12)
  277. def test_vector_scalar_abs_step(self):
  278. # can approx_derivative use abs_step?
  279. x0 = np.array([100.0, -0.5])
  280. jac_diff_2 = approx_derivative(self.fun_vector_scalar, x0,
  281. method='2-point', abs_step=1.49e-8)
  282. jac_diff_3 = approx_derivative(self.fun_vector_scalar, x0,
  283. abs_step=1.49e-8, rel_step=np.inf)
  284. jac_diff_4 = approx_derivative(self.fun_vector_scalar, x0,
  285. method='cs', abs_step=1.49e-8)
  286. jac_true = self.jac_vector_scalar(x0)
  287. assert_allclose(jac_diff_2, jac_true, rtol=1e-6)
  288. assert_allclose(jac_diff_3, jac_true, rtol=3e-9)
  289. assert_allclose(jac_diff_4, jac_true, rtol=1e-12)
  290. def test_vector_vector(self):
  291. x0 = np.array([-100.0, 0.2])
  292. jac_diff_2 = approx_derivative(self.fun_vector_vector, x0,
  293. method='2-point')
  294. jac_diff_3 = approx_derivative(self.fun_vector_vector, x0)
  295. with MapWrapper(2) as mapper:
  296. jac_diff_4 = approx_derivative(self.fun_vector_vector, x0,
  297. method='cs', workers=mapper)
  298. jac_true = self.jac_vector_vector(x0)
  299. assert_allclose(jac_diff_2, jac_true, rtol=1e-5)
  300. assert_allclose(jac_diff_3, jac_true, rtol=1e-6)
  301. assert_allclose(jac_diff_4, jac_true, rtol=1e-12)
  302. def test_wrong_dimensions(self):
  303. x0 = 1.0
  304. assert_raises(RuntimeError, approx_derivative,
  305. self.wrong_dimensions_fun, x0)
  306. f0 = self.wrong_dimensions_fun(np.atleast_1d(x0))
  307. assert_raises(ValueError, approx_derivative,
  308. self.wrong_dimensions_fun, x0, f0=f0)
  309. def test_custom_rel_step(self):
  310. x0 = np.array([-0.1, 0.1])
  311. jac_diff_2 = approx_derivative(self.fun_vector_vector, x0,
  312. method='2-point', rel_step=1e-4)
  313. jac_diff_3 = approx_derivative(self.fun_vector_vector, x0,
  314. rel_step=1e-4)
  315. jac_true = self.jac_vector_vector(x0)
  316. assert_allclose(jac_diff_2, jac_true, rtol=1e-2)
  317. assert_allclose(jac_diff_3, jac_true, rtol=1e-4)
  318. def test_options(self):
  319. x0 = np.array([1.0, 1.0])
  320. c0 = -1.0
  321. c1 = 1.0
  322. lb = 0.0
  323. ub = 2.0
  324. f0 = self.fun_parametrized(x0, c0, c1=c1)
  325. rel_step = np.array([-1e-6, 1e-7])
  326. jac_true = self.jac_parametrized(x0, c0, c1)
  327. jac_diff_2 = approx_derivative(
  328. self.fun_parametrized, x0, method='2-point', rel_step=rel_step,
  329. f0=f0, args=(c0,), kwargs=dict(c1=c1), bounds=(lb, ub))
  330. jac_diff_3 = approx_derivative(
  331. self.fun_parametrized, x0, rel_step=rel_step,
  332. f0=f0, args=(c0,), kwargs=dict(c1=c1), bounds=(lb, ub))
  333. assert_allclose(jac_diff_2, jac_true, rtol=1e-6)
  334. assert_allclose(jac_diff_3, jac_true, rtol=1e-9)
  335. def test_with_bounds_2_point(self):
  336. lb = -np.ones(2)
  337. ub = np.ones(2)
  338. x0 = np.array([-2.0, 0.2])
  339. assert_raises(ValueError, approx_derivative,
  340. self.fun_vector_vector, x0, bounds=(lb, ub))
  341. x0 = np.array([-1.0, 1.0])
  342. jac_diff = approx_derivative(self.fun_vector_vector, x0,
  343. method='2-point', bounds=(lb, ub))
  344. jac_true = self.jac_vector_vector(x0)
  345. assert_allclose(jac_diff, jac_true, rtol=1e-6)
  346. def test_with_bounds_3_point(self):
  347. lb = np.array([1.0, 1.0])
  348. ub = np.array([2.0, 2.0])
  349. x0 = np.array([1.0, 2.0])
  350. jac_true = self.jac_vector_vector(x0)
  351. jac_diff = approx_derivative(self.fun_vector_vector, x0)
  352. assert_allclose(jac_diff, jac_true, rtol=1e-9)
  353. jac_diff = approx_derivative(self.fun_vector_vector, x0,
  354. bounds=(lb, np.inf))
  355. assert_allclose(jac_diff, jac_true, rtol=1e-9)
  356. jac_diff = approx_derivative(self.fun_vector_vector, x0,
  357. bounds=(-np.inf, ub))
  358. assert_allclose(jac_diff, jac_true, rtol=1e-9)
  359. jac_diff = approx_derivative(self.fun_vector_vector, x0,
  360. bounds=(lb, ub))
  361. assert_allclose(jac_diff, jac_true, rtol=1e-9)
  362. def test_tight_bounds(self):
  363. x0 = np.array([10.0, 10.0])
  364. lb = x0 - 3e-9
  365. ub = x0 + 2e-9
  366. jac_true = self.jac_vector_vector(x0)
  367. jac_diff = approx_derivative(
  368. self.fun_vector_vector, x0, method='2-point', bounds=(lb, ub))
  369. assert_allclose(jac_diff, jac_true, rtol=1e-6)
  370. jac_diff = approx_derivative(
  371. self.fun_vector_vector, x0, method='2-point',
  372. rel_step=1e-6, bounds=(lb, ub))
  373. assert_allclose(jac_diff, jac_true, rtol=1e-6)
  374. jac_diff = approx_derivative(
  375. self.fun_vector_vector, x0, bounds=(lb, ub))
  376. assert_allclose(jac_diff, jac_true, rtol=1e-6)
  377. jac_diff = approx_derivative(
  378. self.fun_vector_vector, x0, rel_step=1e-6, bounds=(lb, ub))
  379. assert_allclose(jac_true, jac_diff, rtol=1e-6)
  380. def test_bound_switches(self):
  381. lb = -1e-8
  382. ub = 1e-8
  383. x0 = 0.0
  384. jac_true = self.jac_with_nan(x0)
  385. jac_diff_2 = approx_derivative(
  386. self.fun_with_nan, x0, method='2-point', rel_step=1e-6,
  387. bounds=(lb, ub))
  388. jac_diff_3 = approx_derivative(
  389. self.fun_with_nan, x0, rel_step=1e-6, bounds=(lb, ub))
  390. assert_allclose(jac_diff_2, jac_true, rtol=1e-6)
  391. assert_allclose(jac_diff_3, jac_true, rtol=1e-9)
  392. x0 = 1e-8
  393. jac_true = self.jac_with_nan(x0)
  394. jac_diff_2 = approx_derivative(
  395. self.fun_with_nan, x0, method='2-point', rel_step=1e-6,
  396. bounds=(lb, ub))
  397. jac_diff_3 = approx_derivative(
  398. self.fun_with_nan, x0, rel_step=1e-6, bounds=(lb, ub))
  399. assert_allclose(jac_diff_2, jac_true, rtol=1e-6)
  400. assert_allclose(jac_diff_3, jac_true, rtol=1e-9)
  401. def test_non_numpy(self):
  402. x0 = 1.0
  403. jac_true = self.jac_non_numpy(x0)
  404. jac_diff_2 = approx_derivative(self.jac_non_numpy, x0,
  405. method='2-point')
  406. jac_diff_3 = approx_derivative(self.jac_non_numpy, x0)
  407. assert_allclose(jac_diff_2, jac_true, rtol=1e-6)
  408. assert_allclose(jac_diff_3, jac_true, rtol=1e-8)
  409. # math.exp cannot handle complex arguments, hence this raises
  410. assert_raises(TypeError, approx_derivative, self.jac_non_numpy, x0,
  411. **dict(method='cs'))
  412. def test_fp(self):
  413. # checks that approx_derivative works for FP size other than 64.
  414. # Example is derived from the minimal working example in gh12991.
  415. rng = np.random.default_rng(1)
  416. def func(p, x):
  417. return p[0] + p[1] * x
  418. def err(p, x, y):
  419. return func(p, x) - y
  420. x = np.linspace(0, 1, 100, dtype=np.float64)
  421. y = rng.random(size=100, dtype=np.float64)
  422. p0 = np.array([-1.0, -1.0])
  423. jac_fp64 = approx_derivative(err, p0, method='2-point', args=(x, y))
  424. # parameter vector is float32, func output is float64
  425. jac_fp = approx_derivative(err, p0.astype(np.float32),
  426. method='2-point', args=(x, y))
  427. assert err(p0, x, y).dtype == np.float64
  428. assert_allclose(jac_fp, jac_fp64, atol=1e-3)
  429. # parameter vector is float64, func output is float32
  430. def err_fp32(p):
  431. assert p.dtype == np.float32
  432. return err(p, x, y).astype(np.float32)
  433. jac_fp = approx_derivative(err_fp32, p0.astype(np.float32),
  434. method='2-point')
  435. assert_allclose(jac_fp, jac_fp64, atol=1e-3)
  436. # check upper bound of error on the derivative for 2-point
  437. def f(x):
  438. return np.sin(x)
  439. def g(x):
  440. return np.cos(x)
  441. def hess(x):
  442. return -np.sin(x)
  443. def calc_atol(h, x0, f, hess, EPS):
  444. # truncation error
  445. t0 = h / 2 * max(np.abs(hess(x0)), np.abs(hess(x0 + h)))
  446. # roundoff error. There may be a divisor (>1) missing from
  447. # the following line, so this contribution is possibly
  448. # overestimated
  449. t1 = EPS / h * max(np.abs(f(x0)), np.abs(f(x0 + h)))
  450. return t0 + t1
  451. for dtype in [np.float16, np.float32, np.float64]:
  452. EPS = np.finfo(dtype).eps
  453. x0 = np.array(1.0).astype(dtype)
  454. h = _compute_absolute_step(None, x0, f(x0), '2-point')
  455. atol = calc_atol(h, x0, f, hess, EPS)
  456. err = approx_derivative(f, x0, method='2-point',
  457. abs_step=h) - g(x0)
  458. assert abs(err) < atol
  459. def test_check_derivative(self):
  460. x0 = np.array([-10.0, 10])
  461. accuracy = check_derivative(self.fun_vector_vector,
  462. self.jac_vector_vector, x0)
  463. assert_(accuracy < 1e-9)
  464. accuracy = check_derivative(self.fun_vector_vector,
  465. self.jac_vector_vector, x0)
  466. assert_(accuracy < 1e-6)
  467. x0 = np.array([0.0, 0.0])
  468. accuracy = check_derivative(self.fun_zero_jacobian,
  469. self.jac_zero_jacobian, x0)
  470. assert_(accuracy == 0)
  471. accuracy = check_derivative(self.fun_zero_jacobian,
  472. self.jac_zero_jacobian, x0)
  473. assert_(accuracy == 0)
  474. def test_check_derivative_with_kwargs(self):
  475. x0 = np.array([-10.0, 10])
  476. accuracy = check_derivative(self.fun_vector_vector_with_arg,
  477. self.jac_vector_vector_with_arg,
  478. x0,
  479. kwargs={'arg': 42})
  480. assert_(accuracy < 1e-9)
  481. class TestApproxDerivativeSparse:
  482. # Example from Numerical Optimization 2nd edition, p. 198.
  483. def setup_method(self):
  484. self.rng = np.random.default_rng(121091202)
  485. self.n = 50
  486. self.lb = -0.1 * (1 + np.arange(self.n))
  487. self.ub = 0.1 * (1 + np.arange(self.n))
  488. self.x0 = np.empty(self.n)
  489. self.x0[::2] = (1 - 1e-7) * self.lb[::2]
  490. self.x0[1::2] = (1 - 1e-7) * self.ub[1::2]
  491. self.J_true = self.jac(self.x0)
  492. def fun(self, x):
  493. e = x[1:]**3 - x[:-1]**2
  494. return np.hstack((0, 3 * e)) + np.hstack((2 * e, 0))
  495. def jac(self, x):
  496. n = x.size
  497. J = np.zeros((n, n))
  498. J[0, 0] = -4 * x[0]
  499. J[0, 1] = 6 * x[1]**2
  500. for i in range(1, n - 1):
  501. J[i, i - 1] = -6 * x[i-1]
  502. J[i, i] = 9 * x[i]**2 - 4 * x[i]
  503. J[i, i + 1] = 6 * x[i+1]**2
  504. J[-1, -1] = 9 * x[-1]**2
  505. J[-1, -2] = -6 * x[-2]
  506. return J
  507. def structure(self, n):
  508. A = np.zeros((n, n), dtype=int)
  509. A[0, 0] = 1
  510. A[0, 1] = 1
  511. for i in range(1, n - 1):
  512. A[i, i - 1: i + 2] = 1
  513. A[-1, -1] = 1
  514. A[-1, -2] = 1
  515. return A
  516. @pytest.mark.fail_slow(5)
  517. def test_all(self):
  518. A = self.structure(self.n)
  519. order = np.arange(self.n)
  520. groups_1 = group_columns(A, order)
  521. self.rng.shuffle(order)
  522. groups_2 = group_columns(A, order)
  523. with MapWrapper(2) as mapper:
  524. for method, groups, l, u, mf in product(
  525. ['2-point', '3-point', 'cs'], [groups_1, groups_2],
  526. [-np.inf, self.lb], [np.inf, self.ub], [map, mapper]):
  527. J = approx_derivative(self.fun, self.x0, method=method,
  528. bounds=(l, u), sparsity=(A, groups),
  529. workers=mf)
  530. assert_(isinstance(J, csr_array))
  531. assert_allclose(J.toarray(), self.J_true, rtol=1e-6)
  532. rel_step = np.full_like(self.x0, 1e-8)
  533. rel_step[::2] *= -1
  534. J = approx_derivative(self.fun, self.x0, method=method,
  535. rel_step=rel_step, sparsity=(A, groups),
  536. workers=mf)
  537. assert_allclose(J.toarray(), self.J_true, rtol=1e-5)
  538. def test_no_precomputed_groups(self):
  539. A = self.structure(self.n)
  540. J = approx_derivative(self.fun, self.x0, sparsity=A)
  541. assert_allclose(J.toarray(), self.J_true, rtol=1e-6)
  542. def test_equivalence(self):
  543. structure = np.ones((self.n, self.n), dtype=int)
  544. groups = np.arange(self.n)
  545. for method in ['2-point', '3-point', 'cs']:
  546. J_dense = approx_derivative(self.fun, self.x0, method=method)
  547. J_sparse = approx_derivative(
  548. self.fun, self.x0, sparsity=(structure, groups), method=method)
  549. assert_allclose(J_dense, J_sparse.toarray(),
  550. rtol=5e-16, atol=7e-15)
  551. def test_check_derivative(self):
  552. def jac(x):
  553. return csr_array(self.jac(x))
  554. accuracy = check_derivative(self.fun, jac, self.x0,
  555. bounds=(self.lb, self.ub))
  556. assert_(accuracy < 1e-9)
  557. accuracy = check_derivative(self.fun, jac, self.x0,
  558. bounds=(self.lb, self.ub))
  559. assert_(accuracy < 1e-9)
  560. class TestApproxDerivativeLinearOperator:
  561. def fun_scalar_scalar(self, x):
  562. return np.sinh(x)
  563. def jac_scalar_scalar(self, x):
  564. return np.cosh(x)
  565. def fun_scalar_vector(self, x):
  566. return np.array([x[0]**2, np.tan(x[0]), np.exp(x[0])])
  567. def jac_scalar_vector(self, x):
  568. return np.array(
  569. [2 * x[0], np.cos(x[0]) ** -2, np.exp(x[0])]).reshape(-1, 1)
  570. def fun_vector_scalar(self, x):
  571. return np.sin(x[0] * x[1]) * np.log(x[0])
  572. def jac_vector_scalar(self, x):
  573. return np.array([
  574. x[1] * np.cos(x[0] * x[1]) * np.log(x[0]) +
  575. np.sin(x[0] * x[1]) / x[0],
  576. x[0] * np.cos(x[0] * x[1]) * np.log(x[0])
  577. ])
  578. def fun_vector_vector(self, x):
  579. return np.array([
  580. x[0] * np.sin(x[1]),
  581. x[1] * np.cos(x[0]),
  582. x[0] ** 3 * x[1] ** -0.5
  583. ])
  584. def jac_vector_vector(self, x):
  585. return np.array([
  586. [np.sin(x[1]), x[0] * np.cos(x[1])],
  587. [-x[1] * np.sin(x[0]), np.cos(x[0])],
  588. [3 * x[0] ** 2 * x[1] ** -0.5, -0.5 * x[0] ** 3 * x[1] ** -1.5]
  589. ])
  590. def test_scalar_scalar(self):
  591. x0 = 1.0
  592. jac_diff_2 = approx_derivative(self.fun_scalar_scalar, x0,
  593. method='2-point',
  594. as_linear_operator=True)
  595. jac_diff_3 = approx_derivative(self.fun_scalar_scalar, x0,
  596. as_linear_operator=True)
  597. jac_diff_4 = approx_derivative(self.fun_scalar_scalar, x0,
  598. method='cs',
  599. as_linear_operator=True)
  600. jac_true = self.jac_scalar_scalar(x0)
  601. rng = np.random.default_rng(11290049580398)
  602. for i in range(10):
  603. p = rng.uniform(-10, 10, size=(1,))
  604. assert_allclose(jac_diff_2.dot(p), jac_true*p,
  605. rtol=1e-5)
  606. assert_allclose(jac_diff_3.dot(p), jac_true*p,
  607. rtol=5e-6)
  608. assert_allclose(jac_diff_4.dot(p), jac_true*p,
  609. rtol=5e-6)
  610. def test_scalar_vector(self):
  611. x0 = 0.5
  612. jac_diff_2 = approx_derivative(self.fun_scalar_vector, x0,
  613. method='2-point',
  614. as_linear_operator=True)
  615. jac_diff_3 = approx_derivative(self.fun_scalar_vector, x0,
  616. as_linear_operator=True)
  617. jac_diff_4 = approx_derivative(self.fun_scalar_vector, x0,
  618. method='cs',
  619. as_linear_operator=True)
  620. jac_true = self.jac_scalar_vector(np.atleast_1d(x0))
  621. rng = np.random.default_rng(11290049580398)
  622. for i in range(10):
  623. p = rng.uniform(-10, 10, size=(1,))
  624. assert_allclose(jac_diff_2.dot(p), jac_true.dot(p),
  625. rtol=1e-5)
  626. assert_allclose(jac_diff_3.dot(p), jac_true.dot(p),
  627. rtol=5e-6)
  628. assert_allclose(jac_diff_4.dot(p), jac_true.dot(p),
  629. rtol=5e-6)
  630. def test_vector_scalar(self):
  631. x0 = np.array([100.0, -0.5])
  632. jac_diff_2 = approx_derivative(self.fun_vector_scalar, x0,
  633. method='2-point',
  634. as_linear_operator=True)
  635. jac_diff_3 = approx_derivative(self.fun_vector_scalar, x0,
  636. as_linear_operator=True)
  637. jac_diff_4 = approx_derivative(self.fun_vector_scalar, x0,
  638. method='cs',
  639. as_linear_operator=True)
  640. jac_true = self.jac_vector_scalar(x0)
  641. rng = np.random.default_rng(11290049580398)
  642. for i in range(10):
  643. p = rng.uniform(-10, 10, size=x0.shape)
  644. assert_allclose(jac_diff_2.dot(p), np.atleast_1d(jac_true.dot(p)),
  645. rtol=1e-5)
  646. assert_allclose(jac_diff_3.dot(p), np.atleast_1d(jac_true.dot(p)),
  647. rtol=5e-6)
  648. assert_allclose(jac_diff_4.dot(p), np.atleast_1d(jac_true.dot(p)),
  649. rtol=1e-7)
  650. def test_vector_vector(self):
  651. x0 = np.array([-100.0, 0.2])
  652. jac_diff_2 = approx_derivative(self.fun_vector_vector, x0,
  653. method='2-point',
  654. as_linear_operator=True)
  655. jac_diff_3 = approx_derivative(self.fun_vector_vector, x0,
  656. as_linear_operator=True)
  657. jac_diff_4 = approx_derivative(self.fun_vector_vector, x0,
  658. method='cs',
  659. as_linear_operator=True)
  660. jac_true = self.jac_vector_vector(x0)
  661. rng = np.random.default_rng(11290049580398)
  662. for i in range(10):
  663. p = rng.uniform(-10, 10, size=x0.shape)
  664. assert_allclose(jac_diff_2.dot(p), jac_true.dot(p), rtol=1e-5)
  665. assert_allclose(jac_diff_3.dot(p), jac_true.dot(p), rtol=1e-6)
  666. assert_allclose(jac_diff_4.dot(p), jac_true.dot(p), rtol=1e-7)
  667. def test_exception(self):
  668. x0 = np.array([-100.0, 0.2])
  669. assert_raises(ValueError, approx_derivative,
  670. self.fun_vector_vector, x0,
  671. method='2-point', bounds=(1, np.inf))
  672. def test_absolute_step_sign():
  673. # test for gh12487
  674. # if an absolute step is specified for 2-point differences make sure that
  675. # the side corresponds to the step. i.e. if step is positive then forward
  676. # differences should be used, if step is negative then backwards
  677. # differences should be used.
  678. # function has double discontinuity at x = [-1, -1]
  679. # first component is \/, second component is /\
  680. def f(x):
  681. return -np.abs(x[0] + 1) + np.abs(x[1] + 1)
  682. # check that the forward difference is used
  683. grad = approx_derivative(f, [-1, -1], method='2-point', abs_step=1e-8)
  684. assert_allclose(grad, [-1.0, 1.0])
  685. # check that the backwards difference is used
  686. grad = approx_derivative(f, [-1, -1], method='2-point', abs_step=-1e-8)
  687. assert_allclose(grad, [1.0, -1.0])
  688. # check that the forwards difference is used with a step for both
  689. # parameters
  690. grad = approx_derivative(
  691. f, [-1, -1], method='2-point', abs_step=[1e-8, 1e-8]
  692. )
  693. assert_allclose(grad, [-1.0, 1.0])
  694. # check that we can mix forward/backwards steps.
  695. grad = approx_derivative(
  696. f, [-1, -1], method='2-point', abs_step=[1e-8, -1e-8]
  697. )
  698. assert_allclose(grad, [-1.0, -1.0])
  699. grad = approx_derivative(
  700. f, [-1, -1], method='2-point', abs_step=[-1e-8, 1e-8]
  701. )
  702. assert_allclose(grad, [1.0, 1.0])
  703. # the forward step should reverse to a backwards step if it runs into a
  704. # bound
  705. # This is kind of tested in TestAdjustSchemeToBounds, but only for a lower level
  706. # function.
  707. grad = approx_derivative(
  708. f, [-1, -1], method='2-point', abs_step=1e-8,
  709. bounds=(-np.inf, -1)
  710. )
  711. assert_allclose(grad, [1.0, -1.0])
  712. grad = approx_derivative(
  713. f, [-1, -1], method='2-point', abs_step=-1e-8, bounds=(-1, np.inf)
  714. )
  715. assert_allclose(grad, [-1.0, 1.0])
  716. def test__compute_absolute_step():
  717. # tests calculation of absolute step from rel_step
  718. methods = ['2-point', '3-point', 'cs']
  719. x0 = np.array([1e-5, 0, 1, 1e5])
  720. EPS = np.finfo(np.float64).eps
  721. relative_step = {
  722. "2-point": EPS**0.5,
  723. "3-point": EPS**(1/3),
  724. "cs": EPS**0.5
  725. }
  726. f0 = np.array(1.0)
  727. for method in methods:
  728. rel_step = relative_step[method]
  729. correct_step = np.array([rel_step,
  730. rel_step * 1.,
  731. rel_step * 1.,
  732. rel_step * np.abs(x0[3])])
  733. abs_step = _compute_absolute_step(None, x0, f0, method)
  734. assert_allclose(abs_step, correct_step)
  735. sign_x0 = (-x0 >= 0).astype(float) * 2 - 1
  736. abs_step = _compute_absolute_step(None, -x0, f0, method)
  737. assert_allclose(abs_step, sign_x0 * correct_step)
  738. # if a relative step is provided it should be used
  739. rel_step = np.array([0.1, 1, 10, 100])
  740. correct_step = np.array([rel_step[0] * x0[0],
  741. relative_step['2-point'],
  742. rel_step[2] * 1.,
  743. rel_step[3] * np.abs(x0[3])])
  744. abs_step = _compute_absolute_step(rel_step, x0, f0, '2-point')
  745. assert_allclose(abs_step, correct_step)
  746. sign_x0 = (-x0 >= 0).astype(float) * 2 - 1
  747. abs_step = _compute_absolute_step(rel_step, -x0, f0, '2-point')
  748. assert_allclose(abs_step, sign_x0 * correct_step)