Skip to content

Commit d2e417c

Browse files
committed
add comparison with scipy.odr.odr
1 parent 9390e0a commit d2e417c

File tree

1 file changed

+135
-82
lines changed

1 file changed

+135
-82
lines changed

tests/test_odr_fit.py

Lines changed: 135 additions & 82 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33

44
import numpy as np
55
import pytest
6+
from scipy.odr import odr as odrscipy
67
from scipy.optimize import curve_fit
78

89
from odrpack import OdrStop, odr_fit
@@ -11,31 +12,36 @@
1112
SEED = 1234567890
1213

1314

14-
def add_noise(array, noise, seed):
15+
def add_noise(array, noise, seed=SEED):
1516
"""Adds random noise to an array."""
1617
rng = np.random.default_rng(seed)
1718
return array*(1 + noise*rng.uniform(-1, 1, size=array.shape))
1819

1920

21+
def flipargs(f):
22+
"""Flips the order of the arguments of a function."""
23+
return lambda x, beta: f(beta, x)
24+
25+
2026
@pytest.fixture
2127
def case1():
22-
# m=1, q=1
28+
"Made up test case with m=1, q=1"
2329
def f(x: np.ndarray, beta: np.ndarray) -> np.ndarray:
2430
return beta[0] + beta[1] * x + beta[2] * x**2 + beta[3] * x**3
2531

2632
beta_star = np.array([1, -2., 0.1, -0.1])
2733
x = np.linspace(-10., 10., 21)
2834
y = f(x, beta_star)
2935

30-
x = add_noise(x, 5e-2, SEED)
31-
y = add_noise(y, 10e-2, SEED)
36+
x = add_noise(x, 5e-2)
37+
y = add_noise(y, 10e-2)
3238

3339
return {'xdata': x, 'ydata': y, 'f': f, 'beta0': np.zeros_like(beta_star)}
3440

3541

3642
@pytest.fixture
3743
def case2():
38-
# m=2, q=1
44+
"Made up test with m=2, q=1"
3945
def f(x: np.ndarray, beta: np.ndarray) -> np.ndarray:
4046
return (beta[0] * x[0, :])**3 + x[1, :]**beta[1]
4147

@@ -44,15 +50,15 @@ def f(x: np.ndarray, beta: np.ndarray) -> np.ndarray:
4450
x = np.vstack((x1, 10+x1/2))
4551
y = f(x, beta_star)
4652

47-
x = add_noise(x, 5e-2, SEED)
48-
y = add_noise(y, 10e-2, SEED)
53+
x = add_noise(x, 5e-2)
54+
y = add_noise(y, 10e-2)
4955

50-
return {'xdata': x, 'ydata': y, 'f': f, 'beta0': np.array([1., 1.])}
56+
return {'xdata': x, 'ydata': y, 'f': f, 'beta0': np.ones_like(beta_star)}
5157

5258

5359
@pytest.fixture
5460
def case3():
55-
# m=3, q=2
61+
"Made up test case with m=3, q=2"
5662
def f(x: np.ndarray, beta: np.ndarray) -> np.ndarray:
5763
y = np.zeros((2, x.shape[-1]))
5864
y[0, :] = (beta[0] * x[0, :])**3 + x[1, :]**beta[1] + np.exp(x[2, :]/2)
@@ -64,10 +70,79 @@ def f(x: np.ndarray, beta: np.ndarray) -> np.ndarray:
6470
x = np.vstack((x1, np.exp(x1), x1**2))
6571
y = f(x, beta_star)
6672

67-
x = add_noise(x, 5e-2, SEED)
68-
y = add_noise(y, 10e-2, SEED)
73+
x = add_noise(x, 5e-2)
74+
y = add_noise(y, 10e-2)
75+
76+
return {'xdata': x, 'ydata': y, 'f': f, 'beta0': np.full_like(beta_star, 5.)}
77+
78+
79+
@pytest.fixture
80+
def example2():
81+
"odrpack's example2"
82+
x = np.array([[0.50, -0.12],
83+
[1.20, -0.60],
84+
[1.60, -1.00],
85+
[1.86, -1.40],
86+
[2.12, -2.54],
87+
[2.36, -3.36],
88+
[2.44, -4.00],
89+
[2.36, -4.75],
90+
[2.06, -5.25],
91+
[1.74, -5.64],
92+
[1.34, -5.97],
93+
[0.90, -6.32],
94+
[-0.28, -6.44],
95+
[-0.78, -6.44],
96+
[-1.36, -6.41],
97+
[-1.90, -6.25],
98+
[-2.50, -5.88],
99+
[-2.88, -5.50],
100+
[-3.18, -5.24],
101+
[-3.44, -4.86]]).T
102+
y = np.full(x.shape[-1], 0.0)
103+
beta0 = np.array([-1.0, -3.0, 0.09, 0.02, 0.08])
104+
105+
beta_ref = np.array([-9.99380462E-01,
106+
-2.93104890E+00,
107+
8.75730642E-02,
108+
1.62299601E-02,
109+
7.97538109E-02])
110+
111+
def f(x: np.ndarray, beta: np.ndarray) -> np.ndarray:
112+
v, h = x
113+
return beta[2]*(v-beta[0])**2 + 2*beta[3]*(v-beta[0])*(h-beta[1]) \
114+
+ beta[4]*(h-beta[1])**2 - 1
115+
116+
return {'xdata': x, 'ydata': y, 'f': f, 'beta0': beta0,
117+
'beta_ref': beta_ref}
118+
119+
120+
@pytest.fixture
121+
def example5():
122+
"odrpack's example5"
123+
x = np.array([0.982, 1.998, 4.978, 6.01])
124+
y = np.array([2.7, 7.4, 148.0, 403.0])
125+
beta0 = np.array([2., 0.5])
126+
bounds = (np.array([0., 0.]), np.array([10., 0.9]))
127+
128+
beta_ref = np.array([1.63337602, 0.9])
129+
delta_ref = np.array([-0.36886137, -0.31273038, 0.029287, 0.11031505])
130+
131+
def f(x: np.ndarray, beta: np.ndarray) -> np.ndarray:
132+
return beta[0] * np.exp(beta[1]*x)
133+
134+
def jac_beta(x: np.ndarray, beta: np.ndarray) -> np.ndarray:
135+
jac = np.zeros((beta.size, x.size))
136+
jac[0, :] = np.exp(beta[1]*x)
137+
jac[1, :] = beta[0]*x*np.exp(beta[1]*x)
138+
return jac
139+
140+
def jac_x(x: np.ndarray, beta: np.ndarray) -> np.ndarray:
141+
return beta[0] * beta[1] * np.exp(beta[1]*x)
69142

70-
return {'xdata': x, 'ydata': y, 'f': f, 'beta0': np.array([5., 5., 5.])}
143+
return {'f': f, 'xdata': x, 'ydata': y, 'beta0': beta0, 'bounds': bounds,
144+
'jac_beta': jac_beta, 'jac_x': jac_x, 'beta_ref': beta_ref,
145+
'delta_ref': delta_ref}
71146

72147

73148
def test_base_cases(case1, case2, case3):
@@ -246,7 +321,7 @@ def test_weight_x(case1, case3):
246321
sol = odr_fit(**case1, weight_x=1e10)
247322
assert np.allclose(sol.delta, np.zeros_like(sol.delta))
248323

249-
# weight_x (n,) and m==1
324+
# weight_x (n,) and m=1
250325
weight_x = np.ones_like(case1['xdata'])
251326
fix = (4, 7)
252327
weight_x[fix,] = 1e10
@@ -456,46 +531,35 @@ def test_rptfile_and_errfile(case1):
456531
os.remove(errfile)
457532

458533

459-
def test_jacobians():
460-
461-
# model and data are from odrpack's example5
462-
xdata = np.array([0.982, 1.998, 4.978, 6.01])
463-
ydata = np.array([2.7, 7.4, 148.0, 403.0])
464-
beta0 = np.array([2., 0.5])
465-
bounds = (np.array([0., 0.]), np.array([10., 0.9]))
466-
467-
def f(x: np.ndarray, beta: np.ndarray) -> np.ndarray:
468-
return beta[0] * np.exp(beta[1]*x)
469-
470-
def jac_beta(x: np.ndarray, beta: np.ndarray) -> np.ndarray:
471-
jac = np.zeros((beta.size, x.size))
472-
jac[0, :] = np.exp(beta[1]*x)
473-
jac[1, :] = beta[0]*x*np.exp(beta[1]*x)
474-
return jac
534+
def test_jacobians(example5):
475535

476-
def jac_x(x: np.ndarray, beta: np.ndarray) -> np.ndarray:
477-
return beta[0] * beta[1] * np.exp(beta[1]*x)
478-
479-
beta_ref = np.array([1.63337602, 0.9])
480-
delta_ref = np.array([-0.36886137, -0.31273038, 0.029287, 0.11031505])
536+
xdata = example5['xdata']
537+
ydata = example5['ydata']
538+
beta0 = example5['beta0']
539+
bounds = example5['bounds']
540+
f = example5['f']
541+
jac_beta = example5['jac_beta']
542+
jac_x = example5['jac_x']
543+
beta_ref = example5['beta_ref']
544+
delta_ref = example5['delta_ref']
481545

482546
# ODR without jacobian
483547
for diff_scheme in ['forward', 'central']:
484548
sol = odr_fit(f, xdata, ydata, beta0, bounds=bounds,
485549
diff_scheme=diff_scheme)
486-
assert np.allclose(sol.beta, beta_ref, rtol=1e-4)
487-
assert np.allclose(sol.delta, delta_ref, rtol=1e-3)
550+
assert np.allclose(sol.beta, beta_ref, rtol=1e-6)
551+
assert np.allclose(sol.delta, delta_ref, rtol=1e-5)
488552

489553
# ODR with jacobian
490554
sol = odr_fit(f, xdata, ydata, beta0, bounds=bounds,
491555
jac_beta=jac_beta, jac_x=jac_x)
492-
assert np.allclose(sol.beta, beta_ref, rtol=1e-4)
493-
assert np.allclose(sol.delta, delta_ref, rtol=1e-3)
556+
assert np.allclose(sol.beta, beta_ref, rtol=1e-6)
557+
assert np.allclose(sol.delta, delta_ref, rtol=1e-6)
494558

495559
# OLS with jacobian
496560
sol1 = odr_fit(f, xdata, ydata, beta0, weight_x=1e100)
497561
sol2 = odr_fit(f, xdata, ydata, beta0, jac_beta=jac_beta, task='OLS')
498-
assert np.allclose(sol2.beta, sol1.beta, rtol=1e-4)
562+
assert np.allclose(sol2.beta, sol1.beta)
499563
assert np.allclose(sol2.delta, np.zeros_like(xdata))
500564

501565
# invalid f shape
@@ -525,49 +589,14 @@ def jac_x(x: np.ndarray, beta: np.ndarray) -> np.ndarray:
525589
_ = odr_fit(f, xdata, ydata, beta0, diff_scheme='invalid')
526590

527591

528-
def test_implicit_model():
592+
def test_implicit_model(example2):
529593

530-
# model and data are from odrpack's example2
531-
beta0 = np.array([-1.0, -3.0, 0.09, 0.02, 0.08])
532-
x = [[0.50, -0.12],
533-
[1.20, -0.60],
534-
[1.60, -1.00],
535-
[1.86, -1.40],
536-
[2.12, -2.54],
537-
[2.36, -3.36],
538-
[2.44, -4.00],
539-
[2.36, -4.75],
540-
[2.06, -5.25],
541-
[1.74, -5.64],
542-
[1.34, -5.97],
543-
[0.90, -6.32],
544-
[-0.28, -6.44],
545-
[-0.78, -6.44],
546-
[-1.36, -6.41],
547-
[-1.90, -6.25],
548-
[-2.50, -5.88],
549-
[-2.88, -5.50],
550-
[-3.18, -5.24],
551-
[-3.44, -4.86]]
552-
xdata = np.array(x).T
553-
ydata = np.full(xdata.shape[-1], 0.0)
594+
sol = odr_fit(example2['f'], example2['xdata'], example2['ydata'],
595+
example2['beta0'], task='implicit-ODR')
596+
assert np.allclose(sol.beta, example2['beta_ref'])
554597

555-
def f(x: np.ndarray, beta: np.ndarray) -> np.ndarray:
556-
v, h = x
557-
return beta[2]*(v-beta[0])**2 + 2*beta[3]*(v-beta[0])*(h-beta[1]) \
558-
+ beta[4]*(h-beta[1])**2 - 1
559598

560-
beta_ref = np.array([-9.99380462E-01,
561-
-2.93104890E+00,
562-
8.75730642E-02,
563-
1.62299601E-02,
564-
7.97538109E-02])
565-
566-
sol = odr_fit(f, xdata, ydata, beta0, task='implicit-ODR')
567-
assert np.allclose(sol.beta, beta_ref)
568-
569-
570-
def test_ols(case1):
599+
def test_OLS(case1):
571600

572601
sol1 = odr_fit(**case1, task='OLS')
573602
sol2 = odr_fit(**case1, weight_x=1e100)
@@ -577,7 +606,6 @@ def test_ols(case1):
577606

578607
def test_exception_odrstop():
579608

580-
# model and data are from odrpack's example5
581609
xdata = np.array([1.0, 2.0, 3.0, 4.0])
582610
ydata = np.array([1.0, 2.0, 3.0, 4.0])
583611
beta0 = np.array([1.0, 1.0])
@@ -593,10 +621,35 @@ def f(x: np.ndarray, beta: np.ndarray) -> np.ndarray:
593621
assert sol.info == 51000
594622

595623

596-
def test_compare_scipy(case1):
624+
def test_compare_scipy(case1, case2, case3, example2):
597625

626+
# case1 // scipy.optimize.curve_fit
598627
sol1 = odr_fit(**case1, task='OLS')
599628
sol2 = curve_fit(lambda x, *b: case1['f'](x, np.array(b)),
600629
case1['xdata'], case1['ydata'], case1['beta0'])
601-
print(sol2)
602630
assert np.allclose(sol1.beta, sol2[0])
631+
632+
# case1,2,3 // scipy.odr.odr
633+
for case in [case3]:
634+
for subcase in range(2):
635+
if subcase == 0:
636+
wd = np.random.uniform(0.1, 1.0)
637+
we = np.random.uniform(0.1, 1.0)
638+
rtol = 1e-4
639+
else:
640+
wd = np.random.rand(*case['xdata'].shape)
641+
we = np.random.rand(*case['ydata'].shape)
642+
rtol = 1e-3 # very tough problem!
643+
644+
sol1 = odr_fit(**case, weight_x=wd, weight_y=we)
645+
sol2 = odrscipy(flipargs(case['f']),
646+
case['beta0'], case['ydata'], case['xdata'],
647+
wd=wd, we=we, full_output=True)
648+
649+
assert np.allclose(sol1.beta, sol2[0], rtol=1e-5)
650+
651+
assert np.all(np.max(we*abs(sol1.eps - sol2[3]['eps']), -1) /
652+
(np.max(case['ydata'], -1) - np.min(case['ydata'], -1)) < rtol)
653+
654+
assert np.all(np.max(wd*abs(sol1.delta - sol2[3]['delta']), -1) /
655+
(np.max(case['xdata'], -1) - np.min(case['xdata'], -1)) < rtol)

0 commit comments

Comments
 (0)