test_least_squares.py 28 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735
  1. from __future__ import division
  2. from itertools import product
  3. import numpy as np
  4. from numpy.linalg import norm
  5. from numpy.testing import (assert_, assert_allclose,
  6. assert_equal)
  7. from pytest import raises as assert_raises
  8. from scipy._lib._numpy_compat import suppress_warnings
  9. from scipy.sparse import issparse, lil_matrix
  10. from scipy.sparse.linalg import aslinearoperator
  11. from scipy.optimize import least_squares
  12. from scipy.optimize._lsq.least_squares import IMPLEMENTED_LOSSES
  13. from scipy.optimize._lsq.common import EPS, make_strictly_feasible
  14. def fun_trivial(x, a=0):
  15. return (x - a)**2 + 5.0
  16. def jac_trivial(x, a=0.0):
  17. return 2 * (x - a)
  18. def fun_2d_trivial(x):
  19. return np.array([x[0], x[1]])
  20. def jac_2d_trivial(x):
  21. return np.identity(2)
  22. def fun_rosenbrock(x):
  23. return np.array([10 * (x[1] - x[0]**2), (1 - x[0])])
  24. def jac_rosenbrock(x):
  25. return np.array([
  26. [-20 * x[0], 10],
  27. [-1, 0]
  28. ])
  29. def jac_rosenbrock_bad_dim(x):
  30. return np.array([
  31. [-20 * x[0], 10],
  32. [-1, 0],
  33. [0.0, 0.0]
  34. ])
  35. def fun_rosenbrock_cropped(x):
  36. return fun_rosenbrock(x)[0]
  37. def jac_rosenbrock_cropped(x):
  38. return jac_rosenbrock(x)[0]
  39. # When x is 1-d array, return is 2-d array.
  40. def fun_wrong_dimensions(x):
  41. return np.array([x, x**2, x**3])
  42. def jac_wrong_dimensions(x, a=0.0):
  43. return np.atleast_3d(jac_trivial(x, a=a))
  44. def fun_bvp(x):
  45. n = int(np.sqrt(x.shape[0]))
  46. u = np.zeros((n + 2, n + 2))
  47. x = x.reshape((n, n))
  48. u[1:-1, 1:-1] = x
  49. y = u[:-2, 1:-1] + u[2:, 1:-1] + u[1:-1, :-2] + u[1:-1, 2:] - 4 * x + x**3
  50. return y.ravel()
  51. class BroydenTridiagonal(object):
  52. def __init__(self, n=100, mode='sparse'):
  53. np.random.seed(0)
  54. self.n = n
  55. self.x0 = -np.ones(n)
  56. self.lb = np.linspace(-2, -1.5, n)
  57. self.ub = np.linspace(-0.8, 0.0, n)
  58. self.lb += 0.1 * np.random.randn(n)
  59. self.ub += 0.1 * np.random.randn(n)
  60. self.x0 += 0.1 * np.random.randn(n)
  61. self.x0 = make_strictly_feasible(self.x0, self.lb, self.ub)
  62. if mode == 'sparse':
  63. self.sparsity = lil_matrix((n, n), dtype=int)
  64. i = np.arange(n)
  65. self.sparsity[i, i] = 1
  66. i = np.arange(1, n)
  67. self.sparsity[i, i - 1] = 1
  68. i = np.arange(n - 1)
  69. self.sparsity[i, i + 1] = 1
  70. self.jac = self._jac
  71. elif mode == 'operator':
  72. self.jac = lambda x: aslinearoperator(self._jac(x))
  73. elif mode == 'dense':
  74. self.sparsity = None
  75. self.jac = lambda x: self._jac(x).toarray()
  76. else:
  77. assert_(False)
  78. def fun(self, x):
  79. f = (3 - x) * x + 1
  80. f[1:] -= x[:-1]
  81. f[:-1] -= 2 * x[1:]
  82. return f
  83. def _jac(self, x):
  84. J = lil_matrix((self.n, self.n))
  85. i = np.arange(self.n)
  86. J[i, i] = 3 - 2 * x
  87. i = np.arange(1, self.n)
  88. J[i, i - 1] = -1
  89. i = np.arange(self.n - 1)
  90. J[i, i + 1] = -2
  91. return J
  92. class ExponentialFittingProblem(object):
  93. """Provide data and function for exponential fitting in the form
  94. y = a + exp(b * x) + noise."""
  95. def __init__(self, a, b, noise, n_outliers=1, x_range=(-1, 1),
  96. n_points=11, random_seed=None):
  97. np.random.seed(random_seed)
  98. self.m = n_points
  99. self.n = 2
  100. self.p0 = np.zeros(2)
  101. self.x = np.linspace(x_range[0], x_range[1], n_points)
  102. self.y = a + np.exp(b * self.x)
  103. self.y += noise * np.random.randn(self.m)
  104. outliers = np.random.randint(0, self.m, n_outliers)
  105. self.y[outliers] += 50 * noise * np.random.rand(n_outliers)
  106. self.p_opt = np.array([a, b])
  107. def fun(self, p):
  108. return p[0] + np.exp(p[1] * self.x) - self.y
  109. def jac(self, p):
  110. J = np.empty((self.m, self.n))
  111. J[:, 0] = 1
  112. J[:, 1] = self.x * np.exp(p[1] * self.x)
  113. return J
  114. def cubic_soft_l1(z):
  115. rho = np.empty((3, z.size))
  116. t = 1 + z
  117. rho[0] = 3 * (t**(1/3) - 1)
  118. rho[1] = t ** (-2/3)
  119. rho[2] = -2/3 * t**(-5/3)
  120. return rho
  121. LOSSES = list(IMPLEMENTED_LOSSES.keys()) + [cubic_soft_l1]
  122. class BaseMixin(object):
  123. def test_basic(self):
  124. # Test that the basic calling sequence works.
  125. res = least_squares(fun_trivial, 2., method=self.method)
  126. assert_allclose(res.x, 0, atol=1e-4)
  127. assert_allclose(res.fun, fun_trivial(res.x))
  128. def test_args_kwargs(self):
  129. # Test that args and kwargs are passed correctly to the functions.
  130. a = 3.0
  131. for jac in ['2-point', '3-point', 'cs', jac_trivial]:
  132. with suppress_warnings() as sup:
  133. sup.filter(UserWarning,
  134. "jac='(3-point|cs)' works equivalently to '2-point' for method='lm'")
  135. res = least_squares(fun_trivial, 2.0, jac, args=(a,),
  136. method=self.method)
  137. res1 = least_squares(fun_trivial, 2.0, jac, kwargs={'a': a},
  138. method=self.method)
  139. assert_allclose(res.x, a, rtol=1e-4)
  140. assert_allclose(res1.x, a, rtol=1e-4)
  141. assert_raises(TypeError, least_squares, fun_trivial, 2.0,
  142. args=(3, 4,), method=self.method)
  143. assert_raises(TypeError, least_squares, fun_trivial, 2.0,
  144. kwargs={'kaboom': 3}, method=self.method)
  145. def test_jac_options(self):
  146. for jac in ['2-point', '3-point', 'cs', jac_trivial]:
  147. with suppress_warnings() as sup:
  148. sup.filter(UserWarning,
  149. "jac='(3-point|cs)' works equivalently to '2-point' for method='lm'")
  150. res = least_squares(fun_trivial, 2.0, jac, method=self.method)
  151. assert_allclose(res.x, 0, atol=1e-4)
  152. assert_raises(ValueError, least_squares, fun_trivial, 2.0, jac='oops',
  153. method=self.method)
  154. def test_nfev_options(self):
  155. for max_nfev in [None, 20]:
  156. res = least_squares(fun_trivial, 2.0, max_nfev=max_nfev,
  157. method=self.method)
  158. assert_allclose(res.x, 0, atol=1e-4)
  159. def test_x_scale_options(self):
  160. for x_scale in [1.0, np.array([0.5]), 'jac']:
  161. res = least_squares(fun_trivial, 2.0, x_scale=x_scale)
  162. assert_allclose(res.x, 0)
  163. assert_raises(ValueError, least_squares, fun_trivial,
  164. 2.0, x_scale='auto', method=self.method)
  165. assert_raises(ValueError, least_squares, fun_trivial,
  166. 2.0, x_scale=-1.0, method=self.method)
  167. assert_raises(ValueError, least_squares, fun_trivial,
  168. 2.0, x_scale=None, method=self.method)
  169. assert_raises(ValueError, least_squares, fun_trivial,
  170. 2.0, x_scale=1.0+2.0j, method=self.method)
  171. def test_diff_step(self):
  172. # res1 and res2 should be equivalent.
  173. # res2 and res3 should be different.
  174. res1 = least_squares(fun_trivial, 2.0, diff_step=1e-1,
  175. method=self.method)
  176. res2 = least_squares(fun_trivial, 2.0, diff_step=-1e-1,
  177. method=self.method)
  178. res3 = least_squares(fun_trivial, 2.0,
  179. diff_step=None, method=self.method)
  180. assert_allclose(res1.x, 0, atol=1e-4)
  181. assert_allclose(res2.x, 0, atol=1e-4)
  182. assert_allclose(res3.x, 0, atol=1e-4)
  183. assert_equal(res1.x, res2.x)
  184. assert_equal(res1.nfev, res2.nfev)
  185. assert_(res2.nfev != res3.nfev)
  186. def test_incorrect_options_usage(self):
  187. assert_raises(TypeError, least_squares, fun_trivial, 2.0,
  188. method=self.method, options={'no_such_option': 100})
  189. assert_raises(TypeError, least_squares, fun_trivial, 2.0,
  190. method=self.method, options={'max_nfev': 100})
  191. def test_full_result(self):
  192. # MINPACK doesn't work very well with factor=100 on this problem,
  193. # thus using low 'atol'.
  194. res = least_squares(fun_trivial, 2.0, method=self.method)
  195. assert_allclose(res.x, 0, atol=1e-4)
  196. assert_allclose(res.cost, 12.5)
  197. assert_allclose(res.fun, 5)
  198. assert_allclose(res.jac, 0, atol=1e-4)
  199. assert_allclose(res.grad, 0, atol=1e-2)
  200. assert_allclose(res.optimality, 0, atol=1e-2)
  201. assert_equal(res.active_mask, 0)
  202. if self.method == 'lm':
  203. assert_(res.nfev < 30)
  204. assert_(res.njev is None)
  205. else:
  206. assert_(res.nfev < 10)
  207. assert_(res.njev < 10)
  208. assert_(res.status > 0)
  209. assert_(res.success)
  210. def test_full_result_single_fev(self):
  211. # MINPACK checks the number of nfev after the iteration,
  212. # so it's hard to tell what he is going to compute.
  213. if self.method == 'lm':
  214. return
  215. res = least_squares(fun_trivial, 2.0, method=self.method,
  216. max_nfev=1)
  217. assert_equal(res.x, np.array([2]))
  218. assert_equal(res.cost, 40.5)
  219. assert_equal(res.fun, np.array([9]))
  220. assert_equal(res.jac, np.array([[4]]))
  221. assert_equal(res.grad, np.array([36]))
  222. assert_equal(res.optimality, 36)
  223. assert_equal(res.active_mask, np.array([0]))
  224. assert_equal(res.nfev, 1)
  225. assert_equal(res.njev, 1)
  226. assert_equal(res.status, 0)
  227. assert_equal(res.success, 0)
  228. def test_rosenbrock(self):
  229. x0 = [-2, 1]
  230. x_opt = [1, 1]
  231. for jac, x_scale, tr_solver in product(
  232. ['2-point', '3-point', 'cs', jac_rosenbrock],
  233. [1.0, np.array([1.0, 0.2]), 'jac'],
  234. ['exact', 'lsmr']):
  235. with suppress_warnings() as sup:
  236. sup.filter(UserWarning,
  237. "jac='(3-point|cs)' works equivalently to '2-point' for method='lm'")
  238. res = least_squares(fun_rosenbrock, x0, jac, x_scale=x_scale,
  239. tr_solver=tr_solver, method=self.method)
  240. assert_allclose(res.x, x_opt)
  241. def test_rosenbrock_cropped(self):
  242. x0 = [-2, 1]
  243. if self.method == 'lm':
  244. assert_raises(ValueError, least_squares, fun_rosenbrock_cropped,
  245. x0, method='lm')
  246. else:
  247. for jac, x_scale, tr_solver in product(
  248. ['2-point', '3-point', 'cs', jac_rosenbrock_cropped],
  249. [1.0, np.array([1.0, 0.2]), 'jac'],
  250. ['exact', 'lsmr']):
  251. res = least_squares(
  252. fun_rosenbrock_cropped, x0, jac, x_scale=x_scale,
  253. tr_solver=tr_solver, method=self.method)
  254. assert_allclose(res.cost, 0, atol=1e-14)
  255. def test_fun_wrong_dimensions(self):
  256. assert_raises(ValueError, least_squares, fun_wrong_dimensions,
  257. 2.0, method=self.method)
  258. def test_jac_wrong_dimensions(self):
  259. assert_raises(ValueError, least_squares, fun_trivial,
  260. 2.0, jac_wrong_dimensions, method=self.method)
  261. def test_fun_and_jac_inconsistent_dimensions(self):
  262. x0 = [1, 2]
  263. assert_raises(ValueError, least_squares, fun_rosenbrock, x0,
  264. jac_rosenbrock_bad_dim, method=self.method)
  265. def test_x0_multidimensional(self):
  266. x0 = np.ones(4).reshape(2, 2)
  267. assert_raises(ValueError, least_squares, fun_trivial, x0,
  268. method=self.method)
  269. def test_x0_complex_scalar(self):
  270. x0 = 2.0 + 0.0*1j
  271. assert_raises(ValueError, least_squares, fun_trivial, x0,
  272. method=self.method)
  273. def test_x0_complex_array(self):
  274. x0 = [1.0, 2.0 + 0.0*1j]
  275. assert_raises(ValueError, least_squares, fun_trivial, x0,
  276. method=self.method)
  277. def test_bvp(self):
  278. # This test was introduced with fix #5556. It turned out that
  279. # dogbox solver had a bug with trust-region radius update, which
  280. # could block its progress and create an infinite loop. And this
  281. # discrete boundary value problem is the one which triggers it.
  282. n = 10
  283. x0 = np.ones(n**2)
  284. if self.method == 'lm':
  285. max_nfev = 5000 # To account for Jacobian estimation.
  286. else:
  287. max_nfev = 100
  288. res = least_squares(fun_bvp, x0, ftol=1e-2, method=self.method,
  289. max_nfev=max_nfev)
  290. assert_(res.nfev < max_nfev)
  291. assert_(res.cost < 0.5)
  292. class BoundsMixin(object):
  293. def test_inconsistent(self):
  294. assert_raises(ValueError, least_squares, fun_trivial, 2.0,
  295. bounds=(10.0, 0.0), method=self.method)
  296. def test_infeasible(self):
  297. assert_raises(ValueError, least_squares, fun_trivial, 2.0,
  298. bounds=(3., 4), method=self.method)
  299. def test_wrong_number(self):
  300. assert_raises(ValueError, least_squares, fun_trivial, 2.,
  301. bounds=(1., 2, 3), method=self.method)
  302. def test_inconsistent_shape(self):
  303. assert_raises(ValueError, least_squares, fun_trivial, 2.0,
  304. bounds=(1.0, [2.0, 3.0]), method=self.method)
  305. # 1-D array wont't be broadcasted
  306. assert_raises(ValueError, least_squares, fun_rosenbrock, [1.0, 2.0],
  307. bounds=([0.0], [3.0, 4.0]), method=self.method)
  308. def test_in_bounds(self):
  309. for jac in ['2-point', '3-point', 'cs', jac_trivial]:
  310. res = least_squares(fun_trivial, 2.0, jac=jac,
  311. bounds=(-1.0, 3.0), method=self.method)
  312. assert_allclose(res.x, 0.0, atol=1e-4)
  313. assert_equal(res.active_mask, [0])
  314. assert_(-1 <= res.x <= 3)
  315. res = least_squares(fun_trivial, 2.0, jac=jac,
  316. bounds=(0.5, 3.0), method=self.method)
  317. assert_allclose(res.x, 0.5, atol=1e-4)
  318. assert_equal(res.active_mask, [-1])
  319. assert_(0.5 <= res.x <= 3)
  320. def test_bounds_shape(self):
  321. for jac in ['2-point', '3-point', 'cs', jac_2d_trivial]:
  322. x0 = [1.0, 1.0]
  323. res = least_squares(fun_2d_trivial, x0, jac=jac)
  324. assert_allclose(res.x, [0.0, 0.0])
  325. res = least_squares(fun_2d_trivial, x0, jac=jac,
  326. bounds=(0.5, [2.0, 2.0]), method=self.method)
  327. assert_allclose(res.x, [0.5, 0.5])
  328. res = least_squares(fun_2d_trivial, x0, jac=jac,
  329. bounds=([0.3, 0.2], 3.0), method=self.method)
  330. assert_allclose(res.x, [0.3, 0.2])
  331. res = least_squares(
  332. fun_2d_trivial, x0, jac=jac, bounds=([-1, 0.5], [1.0, 3.0]),
  333. method=self.method)
  334. assert_allclose(res.x, [0.0, 0.5], atol=1e-5)
  335. def test_rosenbrock_bounds(self):
  336. x0_1 = np.array([-2.0, 1.0])
  337. x0_2 = np.array([2.0, 2.0])
  338. x0_3 = np.array([-2.0, 2.0])
  339. x0_4 = np.array([0.0, 2.0])
  340. x0_5 = np.array([-1.2, 1.0])
  341. problems = [
  342. (x0_1, ([-np.inf, -1.5], np.inf)),
  343. (x0_2, ([-np.inf, 1.5], np.inf)),
  344. (x0_3, ([-np.inf, 1.5], np.inf)),
  345. (x0_4, ([-np.inf, 1.5], [1.0, np.inf])),
  346. (x0_2, ([1.0, 1.5], [3.0, 3.0])),
  347. (x0_5, ([-50.0, 0.0], [0.5, 100]))
  348. ]
  349. for x0, bounds in problems:
  350. for jac, x_scale, tr_solver in product(
  351. ['2-point', '3-point', 'cs', jac_rosenbrock],
  352. [1.0, [1.0, 0.5], 'jac'],
  353. ['exact', 'lsmr']):
  354. res = least_squares(fun_rosenbrock, x0, jac, bounds,
  355. x_scale=x_scale, tr_solver=tr_solver,
  356. method=self.method)
  357. assert_allclose(res.optimality, 0.0, atol=1e-5)
  358. class SparseMixin(object):
  359. def test_exact_tr_solver(self):
  360. p = BroydenTridiagonal()
  361. assert_raises(ValueError, least_squares, p.fun, p.x0, p.jac,
  362. tr_solver='exact', method=self.method)
  363. assert_raises(ValueError, least_squares, p.fun, p.x0,
  364. tr_solver='exact', jac_sparsity=p.sparsity,
  365. method=self.method)
  366. def test_equivalence(self):
  367. sparse = BroydenTridiagonal(mode='sparse')
  368. dense = BroydenTridiagonal(mode='dense')
  369. res_sparse = least_squares(
  370. sparse.fun, sparse.x0, jac=sparse.jac,
  371. method=self.method)
  372. res_dense = least_squares(
  373. dense.fun, dense.x0, jac=sparse.jac,
  374. method=self.method)
  375. assert_equal(res_sparse.nfev, res_dense.nfev)
  376. assert_allclose(res_sparse.x, res_dense.x, atol=1e-20)
  377. assert_allclose(res_sparse.cost, 0, atol=1e-20)
  378. assert_allclose(res_dense.cost, 0, atol=1e-20)
  379. def test_tr_options(self):
  380. p = BroydenTridiagonal()
  381. res = least_squares(p.fun, p.x0, p.jac, method=self.method,
  382. tr_options={'btol': 1e-10})
  383. assert_allclose(res.cost, 0, atol=1e-20)
  384. def test_wrong_parameters(self):
  385. p = BroydenTridiagonal()
  386. assert_raises(ValueError, least_squares, p.fun, p.x0, p.jac,
  387. tr_solver='best', method=self.method)
  388. assert_raises(TypeError, least_squares, p.fun, p.x0, p.jac,
  389. tr_solver='lsmr', tr_options={'tol': 1e-10})
  390. def test_solver_selection(self):
  391. sparse = BroydenTridiagonal(mode='sparse')
  392. dense = BroydenTridiagonal(mode='dense')
  393. res_sparse = least_squares(sparse.fun, sparse.x0, jac=sparse.jac,
  394. method=self.method)
  395. res_dense = least_squares(dense.fun, dense.x0, jac=dense.jac,
  396. method=self.method)
  397. assert_allclose(res_sparse.cost, 0, atol=1e-20)
  398. assert_allclose(res_dense.cost, 0, atol=1e-20)
  399. assert_(issparse(res_sparse.jac))
  400. assert_(isinstance(res_dense.jac, np.ndarray))
  401. def test_numerical_jac(self):
  402. p = BroydenTridiagonal()
  403. for jac in ['2-point', '3-point', 'cs']:
  404. res_dense = least_squares(p.fun, p.x0, jac, method=self.method)
  405. res_sparse = least_squares(
  406. p.fun, p.x0, jac,method=self.method,
  407. jac_sparsity=p.sparsity)
  408. assert_equal(res_dense.nfev, res_sparse.nfev)
  409. assert_allclose(res_dense.x, res_sparse.x, atol=1e-20)
  410. assert_allclose(res_dense.cost, 0, atol=1e-20)
  411. assert_allclose(res_sparse.cost, 0, atol=1e-20)
  412. def test_with_bounds(self):
  413. p = BroydenTridiagonal()
  414. for jac, jac_sparsity in product(
  415. [p.jac, '2-point', '3-point', 'cs'], [None, p.sparsity]):
  416. res_1 = least_squares(
  417. p.fun, p.x0, jac, bounds=(p.lb, np.inf),
  418. method=self.method,jac_sparsity=jac_sparsity)
  419. res_2 = least_squares(
  420. p.fun, p.x0, jac, bounds=(-np.inf, p.ub),
  421. method=self.method, jac_sparsity=jac_sparsity)
  422. res_3 = least_squares(
  423. p.fun, p.x0, jac, bounds=(p.lb, p.ub),
  424. method=self.method, jac_sparsity=jac_sparsity)
  425. assert_allclose(res_1.optimality, 0, atol=1e-10)
  426. assert_allclose(res_2.optimality, 0, atol=1e-10)
  427. assert_allclose(res_3.optimality, 0, atol=1e-10)
  428. def test_wrong_jac_sparsity(self):
  429. p = BroydenTridiagonal()
  430. sparsity = p.sparsity[:-1]
  431. assert_raises(ValueError, least_squares, p.fun, p.x0,
  432. jac_sparsity=sparsity, method=self.method)
  433. def test_linear_operator(self):
  434. p = BroydenTridiagonal(mode='operator')
  435. res = least_squares(p.fun, p.x0, p.jac, method=self.method)
  436. assert_allclose(res.cost, 0.0, atol=1e-20)
  437. assert_raises(ValueError, least_squares, p.fun, p.x0, p.jac,
  438. method=self.method, tr_solver='exact')
  439. def test_x_scale_jac_scale(self):
  440. p = BroydenTridiagonal()
  441. res = least_squares(p.fun, p.x0, p.jac, method=self.method,
  442. x_scale='jac')
  443. assert_allclose(res.cost, 0.0, atol=1e-20)
  444. p = BroydenTridiagonal(mode='operator')
  445. assert_raises(ValueError, least_squares, p.fun, p.x0, p.jac,
  446. method=self.method, x_scale='jac')
  447. class LossFunctionMixin(object):
  448. def test_options(self):
  449. for loss in LOSSES:
  450. res = least_squares(fun_trivial, 2.0, loss=loss,
  451. method=self.method)
  452. assert_allclose(res.x, 0, atol=1e-15)
  453. assert_raises(ValueError, least_squares, fun_trivial, 2.0,
  454. loss='hinge', method=self.method)
  455. def test_fun(self):
  456. # Test that res.fun is actual residuals, and not modified by loss
  457. # function stuff.
  458. for loss in LOSSES:
  459. res = least_squares(fun_trivial, 2.0, loss=loss,
  460. method=self.method)
  461. assert_equal(res.fun, fun_trivial(res.x))
  462. def test_grad(self):
  463. # Test that res.grad is true gradient of loss function at the
  464. # solution. Use max_nfev = 1, to avoid reaching minimum.
  465. x = np.array([2.0]) # res.x will be this.
  466. res = least_squares(fun_trivial, x, jac_trivial, loss='linear',
  467. max_nfev=1, method=self.method)
  468. assert_equal(res.grad, 2 * x * (x**2 + 5))
  469. res = least_squares(fun_trivial, x, jac_trivial, loss='huber',
  470. max_nfev=1, method=self.method)
  471. assert_equal(res.grad, 2 * x)
  472. res = least_squares(fun_trivial, x, jac_trivial, loss='soft_l1',
  473. max_nfev=1, method=self.method)
  474. assert_allclose(res.grad,
  475. 2 * x * (x**2 + 5) / (1 + (x**2 + 5)**2)**0.5)
  476. res = least_squares(fun_trivial, x, jac_trivial, loss='cauchy',
  477. max_nfev=1, method=self.method)
  478. assert_allclose(res.grad, 2 * x * (x**2 + 5) / (1 + (x**2 + 5)**2))
  479. res = least_squares(fun_trivial, x, jac_trivial, loss='arctan',
  480. max_nfev=1, method=self.method)
  481. assert_allclose(res.grad, 2 * x * (x**2 + 5) / (1 + (x**2 + 5)**4))
  482. res = least_squares(fun_trivial, x, jac_trivial, loss=cubic_soft_l1,
  483. max_nfev=1, method=self.method)
  484. assert_allclose(res.grad,
  485. 2 * x * (x**2 + 5) / (1 + (x**2 + 5)**2)**(2/3))
  486. def test_jac(self):
  487. # Test that res.jac.T.dot(res.jac) gives Gauss-Newton approximation
  488. # of Hessian. This approximation is computed by doubly differentiating
  489. # the cost function and dropping the part containing second derivative
  490. # of f. For a scalar function it is computed as
  491. # H = (rho' + 2 * rho'' * f**2) * f'**2, if the expression inside the
  492. # brackets is less than EPS it is replaced by EPS. Here we check
  493. # against the root of H.
  494. x = 2.0 # res.x will be this.
  495. f = x**2 + 5 # res.fun will be this.
  496. res = least_squares(fun_trivial, x, jac_trivial, loss='linear',
  497. max_nfev=1, method=self.method)
  498. assert_equal(res.jac, 2 * x)
  499. # For `huber` loss the Jacobian correction is identically zero
  500. # in outlier region, in such cases it is modified to be equal EPS**0.5.
  501. res = least_squares(fun_trivial, x, jac_trivial, loss='huber',
  502. max_nfev=1, method=self.method)
  503. assert_equal(res.jac, 2 * x * EPS**0.5)
  504. # Now let's apply `loss_scale` to turn the residual into an inlier.
  505. # The loss function becomes linear.
  506. res = least_squares(fun_trivial, x, jac_trivial, loss='huber',
  507. f_scale=10, max_nfev=1)
  508. assert_equal(res.jac, 2 * x)
  509. # 'soft_l1' always gives a positive scaling.
  510. res = least_squares(fun_trivial, x, jac_trivial, loss='soft_l1',
  511. max_nfev=1, method=self.method)
  512. assert_allclose(res.jac, 2 * x * (1 + f**2)**-0.75)
  513. # For 'cauchy' the correction term turns out to be negative, and it
  514. # replaced by EPS**0.5.
  515. res = least_squares(fun_trivial, x, jac_trivial, loss='cauchy',
  516. max_nfev=1, method=self.method)
  517. assert_allclose(res.jac, 2 * x * EPS**0.5)
  518. # Now use scaling to turn the residual to inlier.
  519. res = least_squares(fun_trivial, x, jac_trivial, loss='cauchy',
  520. f_scale=10, max_nfev=1, method=self.method)
  521. fs = f / 10
  522. assert_allclose(res.jac, 2 * x * (1 - fs**2)**0.5 / (1 + fs**2))
  523. # 'arctan' gives an outlier.
  524. res = least_squares(fun_trivial, x, jac_trivial, loss='arctan',
  525. max_nfev=1, method=self.method)
  526. assert_allclose(res.jac, 2 * x * EPS**0.5)
  527. # Turn to inlier.
  528. res = least_squares(fun_trivial, x, jac_trivial, loss='arctan',
  529. f_scale=20.0, max_nfev=1, method=self.method)
  530. fs = f / 20
  531. assert_allclose(res.jac, 2 * x * (1 - 3 * fs**4)**0.5 / (1 + fs**4))
  532. # cubic_soft_l1 will give an outlier.
  533. res = least_squares(fun_trivial, x, jac_trivial, loss=cubic_soft_l1,
  534. max_nfev=1)
  535. assert_allclose(res.jac, 2 * x * EPS**0.5)
  536. # Turn to inlier.
  537. res = least_squares(fun_trivial, x, jac_trivial,
  538. loss=cubic_soft_l1, f_scale=6, max_nfev=1)
  539. fs = f / 6
  540. assert_allclose(res.jac,
  541. 2 * x * (1 - fs**2 / 3)**0.5 * (1 + fs**2)**(-5/6))
  542. def test_robustness(self):
  543. for noise in [0.1, 1.0]:
  544. p = ExponentialFittingProblem(1, 0.1, noise, random_seed=0)
  545. for jac in ['2-point', '3-point', 'cs', p.jac]:
  546. res_lsq = least_squares(p.fun, p.p0, jac=jac,
  547. method=self.method)
  548. assert_allclose(res_lsq.optimality, 0, atol=1e-2)
  549. for loss in LOSSES:
  550. if loss == 'linear':
  551. continue
  552. res_robust = least_squares(
  553. p.fun, p.p0, jac=jac, loss=loss, f_scale=noise,
  554. method=self.method)
  555. assert_allclose(res_robust.optimality, 0, atol=1e-2)
  556. assert_(norm(res_robust.x - p.p_opt) <
  557. norm(res_lsq.x - p.p_opt))
  558. class TestDogbox(BaseMixin, BoundsMixin, SparseMixin, LossFunctionMixin):
  559. method = 'dogbox'
  560. class TestTRF(BaseMixin, BoundsMixin, SparseMixin, LossFunctionMixin):
  561. method = 'trf'
  562. def test_lsmr_regularization(self):
  563. p = BroydenTridiagonal()
  564. for regularize in [True, False]:
  565. res = least_squares(p.fun, p.x0, p.jac, method='trf',
  566. tr_options={'regularize': regularize})
  567. assert_allclose(res.cost, 0, atol=1e-20)
  568. class TestLM(BaseMixin):
  569. method = 'lm'
  570. def test_bounds_not_supported(self):
  571. assert_raises(ValueError, least_squares, fun_trivial,
  572. 2.0, bounds=(-3.0, 3.0), method='lm')
  573. def test_m_less_n_not_supported(self):
  574. x0 = [-2, 1]
  575. assert_raises(ValueError, least_squares, fun_rosenbrock_cropped, x0,
  576. method='lm')
  577. def test_sparse_not_supported(self):
  578. p = BroydenTridiagonal()
  579. assert_raises(ValueError, least_squares, p.fun, p.x0, p.jac,
  580. method='lm')
  581. def test_jac_sparsity_not_supported(self):
  582. assert_raises(ValueError, least_squares, fun_trivial, 2.0,
  583. jac_sparsity=[1], method='lm')
  584. def test_LinearOperator_not_supported(self):
  585. p = BroydenTridiagonal(mode="operator")
  586. assert_raises(ValueError, least_squares, p.fun, p.x0, p.jac,
  587. method='lm')
  588. def test_loss(self):
  589. res = least_squares(fun_trivial, 2.0, loss='linear', method='lm')
  590. assert_allclose(res.x, 0.0, atol=1e-4)
  591. assert_raises(ValueError, least_squares, fun_trivial, 2.0,
  592. method='lm', loss='huber')
  593. def test_basic():
  594. # test that 'method' arg is really optional
  595. res = least_squares(fun_trivial, 2.0)
  596. assert_allclose(res.x, 0, atol=1e-10)