test__numdiff.py 31 KB

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