Skip to content

Commit c442841

Browse files
authored
Issue133 fitting weights (#140)
1 parent 9dcf459 commit c442841

File tree

10 files changed

+126
-91
lines changed

10 files changed

+126
-91
lines changed

src/easyscience/fitting/minimizers/minimizer_base.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -64,7 +64,7 @@ def fit(
6464
self,
6565
x: np.ndarray,
6666
y: np.ndarray,
67-
weights: Optional[np.ndarray] = None,
67+
weights: np.ndarray,
6868
model: Optional[Callable] = None,
6969
parameters: Optional[Parameter] = None,
7070
method: Optional[str] = None,

src/easyscience/fitting/minimizers/minimizer_bumps.py

Lines changed: 15 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -70,7 +70,7 @@ def fit(
7070
self,
7171
x: np.ndarray,
7272
y: np.ndarray,
73-
weights: Optional[np.ndarray] = None,
73+
weights: np.ndarray,
7474
model: Optional[Callable] = None,
7575
parameters: Optional[Parameter] = None,
7676
method: Optional[str] = None,
@@ -87,7 +87,7 @@ def fit(
8787
:type x: np.ndarray
8888
:param y: measured points
8989
:type y: np.ndarray
90-
:param weights: Weights for supplied measured points * Not really optional*
90+
:param weights: Weights for supplied measured points
9191
:type weights: np.ndarray
9292
:param model: Optional Model which is being fitted to
9393
:type model: lmModel
@@ -101,8 +101,19 @@ def fit(
101101
"""
102102
method_dict = self._get_method_kwargs(method)
103103

104-
if weights is None:
105-
weights = np.sqrt(np.abs(y))
104+
x, y, weights = np.asarray(x), np.asarray(y), np.asarray(weights)
105+
106+
if y.shape != x.shape:
107+
raise ValueError('x and y must have the same shape.')
108+
109+
if weights.shape != x.shape:
110+
raise ValueError('Weights must have the same shape as x and y.')
111+
112+
if not np.isfinite(weights).all():
113+
raise ValueError('Weights cannot be NaN or infinite.')
114+
115+
if (weights <= 0).any():
116+
raise ValueError('Weights must be strictly positive and non-zero.')
106117

107118
if engine_kwargs is None:
108119
engine_kwargs = {}

src/easyscience/fitting/minimizers/minimizer_dfo.py

Lines changed: 15 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -59,7 +59,7 @@ def fit(
5959
self,
6060
x: np.ndarray,
6161
y: np.ndarray,
62-
weights: Optional[np.ndarray] = None,
62+
weights: np.ndarray,
6363
model: Optional[Callable] = None,
6464
parameters: Optional[List[Parameter]] = None,
6565
method: str = None,
@@ -74,7 +74,7 @@ def fit(
7474
:type x: np.ndarray
7575
:param y: measured points
7676
:type y: np.ndarray
77-
:param weights: Weights for supplied measured points * Not really optional*
77+
:param weights: Weights for supplied measured points
7878
:type weights: np.ndarray
7979
:param model: Optional Model which is being fitted to
8080
:type model: lmModel
@@ -86,8 +86,19 @@ def fit(
8686
:return: Fit results
8787
:rtype: ModelResult
8888
"""
89-
if weights is None:
90-
weights = np.sqrt(np.abs(y))
89+
x, y, weights = np.asarray(x), np.asarray(y), np.asarray(weights)
90+
91+
if y.shape != x.shape:
92+
raise ValueError('x and y must have the same shape.')
93+
94+
if weights.shape != x.shape:
95+
raise ValueError('Weights must have the same shape as x and y.')
96+
97+
if not np.isfinite(weights).all():
98+
raise ValueError('Weights cannot be NaN or infinite.')
99+
100+
if (weights <= 0).any():
101+
raise ValueError('Weights must be strictly positive and non-zero.')
91102

92103
if model is None:
93104
model_function = self._make_model(parameters=parameters)

src/easyscience/fitting/minimizers/minimizer_lmfit.py

Lines changed: 14 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -81,7 +81,7 @@ def fit(
8181
self,
8282
x: np.ndarray,
8383
y: np.ndarray,
84-
weights: Optional[np.ndarray] = None,
84+
weights: np.ndarray = None,
8585
model: Optional[LMModel] = None,
8686
parameters: Optional[LMParameters] = None,
8787
method: Optional[str] = None,
@@ -112,8 +112,19 @@ def fit(
112112
:return: Fit results
113113
:rtype: ModelResult
114114
"""
115-
if weights is None:
116-
weights = 1 / np.sqrt(np.abs(y))
115+
x, y, weights = np.asarray(x), np.asarray(y), np.asarray(weights)
116+
117+
if y.shape != x.shape:
118+
raise ValueError('x and y must have the same shape.')
119+
120+
if weights.shape != x.shape:
121+
raise ValueError('Weights must have the same shape as x and y.')
122+
123+
if not np.isfinite(weights).all():
124+
raise ValueError('Weights cannot be NaN or infinite.')
125+
126+
if (weights <= 0).any():
127+
raise ValueError('Weights must be strictly positive and non-zero.')
117128

118129
if engine_kwargs is None:
119130
engine_kwargs = {}

tests/integration_tests/fitting/test_fitter.py

Lines changed: 25 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -67,20 +67,20 @@ def check_fit_results(result, sp_sin, ref_sin, x, **kwargs):
6767
assert np.all(result.x == x)
6868
for item1, item2 in zip(sp_sin._kwargs.values(), ref_sin._kwargs.values()):
6969
# assert item.error > 0 % This does not work as some methods don't calculate error
70-
assert item1.error == pytest.approx(0, abs=1e-1)
70+
assert item1.error == pytest.approx(0, abs=2.1e-1)
7171
assert item1.value == pytest.approx(item2.value, abs=5e-3)
7272
y_calc_ref = ref_sin(x)
7373
assert result.y_calc == pytest.approx(y_calc_ref, abs=1e-2)
7474
assert result.residual == pytest.approx(sp_sin(x) - y_calc_ref, abs=1e-2)
7575

7676

77-
@pytest.mark.parametrize("with_errors", [False, True])
7877
@pytest.mark.parametrize("fit_engine", [None, AvailableMinimizers.LMFit, AvailableMinimizers.Bumps, AvailableMinimizers.DFO])
79-
def test_basic_fit(fit_engine: AvailableMinimizers, with_errors):
78+
def test_basic_fit(fit_engine: AvailableMinimizers):
8079
ref_sin = AbsSin(0.2, np.pi)
8180
sp_sin = AbsSin(0.354, 3.05)
8281

8382
x = np.linspace(0, 5, 200)
83+
weights = np.ones_like(x)
8484
y = ref_sin(x)
8585

8686
sp_sin.offset.fixed = False
@@ -92,11 +92,8 @@ def test_basic_fit(fit_engine: AvailableMinimizers, with_errors):
9292
f.switch_minimizer(fit_engine)
9393
except AttributeError:
9494
pytest.skip(msg=f"{fit_engine} is not installed")
95-
args = [x, y]
96-
kwargs = {}
97-
if with_errors:
98-
kwargs["weights"] = 1 / np.sqrt(y)
99-
result = f.fit(*args, **kwargs)
95+
96+
result = f.fit(x=x, y=y, weights=weights)
10097

10198
if fit_engine is not None:
10299
assert result.minimizer_engine.package == fit_engine.name.lower() # Special case where minimizer matches package
@@ -110,6 +107,7 @@ def test_fit_result(fit_engine):
110107
sp_sin = AbsSin(0.354, 3.05)
111108

112109
x = np.linspace(0, 5, 200)
110+
weights = np.ones_like(x)
113111
y = ref_sin(x)
114112

115113
sp_sin.offset.fixed = False
@@ -132,7 +130,7 @@ def test_fit_result(fit_engine):
132130
except AttributeError:
133131
pytest.skip(msg=f"{fit_engine} is not installed")
134132

135-
result = f.fit(x, y)
133+
result = f.fit(x, y, weights=weights)
136134
check_fit_results(result, sp_sin, ref_sin, x, sp_ref1=sp_ref1, sp_ref2=sp_ref2)
137135

138136

@@ -142,6 +140,7 @@ def test_basic_max_evaluations(fit_engine):
142140
sp_sin = AbsSin(0.354, 3.05)
143141

144142
x = np.linspace(0, 5, 200)
143+
weights = np.ones_like(x)
145144
y = ref_sin(x)
146145

147146
sp_sin.offset.fixed = False
@@ -153,11 +152,9 @@ def test_basic_max_evaluations(fit_engine):
153152
f.switch_minimizer(fit_engine)
154153
except AttributeError:
155154
pytest.skip(msg=f"{fit_engine} is not installed")
156-
args = [x, y]
157-
kwargs = {}
158155
f.max_evaluations = 3
159156
try:
160-
result = f.fit(*args, **kwargs)
157+
result = f.fit(x=x, y=y, weights=weights)
161158
# Result should not be the same as the reference
162159
assert sp_sin.phase.value != pytest.approx(ref_sin.phase.value, rel=1e-3)
163160
assert sp_sin.offset.value != pytest.approx(ref_sin.offset.value, rel=1e-3)
@@ -172,6 +169,7 @@ def test_basic_tolerance(fit_engine, tolerance):
172169
sp_sin = AbsSin(0.354, 3.05)
173170

174171
x = np.linspace(0, 5, 200)
172+
weights = np.ones_like(x)
175173
y = ref_sin(x)
176174

177175
sp_sin.offset.fixed = False
@@ -183,10 +181,8 @@ def test_basic_tolerance(fit_engine, tolerance):
183181
f.switch_minimizer(fit_engine)
184182
except AttributeError:
185183
pytest.skip(msg=f"{fit_engine} is not installed")
186-
args = [x, y]
187-
kwargs = {}
188184
f.tolerance = tolerance
189-
result = f.fit(*args, **kwargs)
185+
result = f.fit(x=x, y=y, weights=weights)
190186
# Result should not be the same as the reference
191187
assert sp_sin.phase.value != pytest.approx(ref_sin.phase.value, rel=1e-3)
192188
assert sp_sin.offset.value != pytest.approx(ref_sin.offset.value, rel=1e-3)
@@ -198,14 +194,15 @@ def test_lmfit_methods(fit_method):
198194
sp_sin = AbsSin(0.354, 3.05)
199195

200196
x = np.linspace(0, 5, 200)
197+
weights = np.ones_like(x)
201198
y = ref_sin(x)
202199

203200
sp_sin.offset.fixed = False
204201
sp_sin.phase.fixed = False
205202

206203
f = Fitter(sp_sin, sp_sin)
207204
assert fit_method in f._minimizer.supported_methods()
208-
result = f.fit(x, y, method=fit_method)
205+
result = f.fit(x, y, weights=weights, method=fit_method)
209206
check_fit_results(result, sp_sin, ref_sin, x)
210207

211208

@@ -216,6 +213,7 @@ def test_bumps_methods(fit_method):
216213
sp_sin = AbsSin(0.354, 3.05)
217214

218215
x = np.linspace(0, 5, 200)
216+
weights = np.ones_like(x)
219217
y = ref_sin(x)
220218

221219
sp_sin.offset.fixed = False
@@ -224,7 +222,7 @@ def test_bumps_methods(fit_method):
224222
f = Fitter(sp_sin, sp_sin)
225223
f.switch_minimizer("Bumps")
226224
assert fit_method in f._minimizer.supported_methods()
227-
result = f.fit(x, y, method=fit_method)
225+
result = f.fit(x, y, weights=weights, method=fit_method)
228226
check_fit_results(result, sp_sin, ref_sin, x)
229227

230228

@@ -234,6 +232,7 @@ def test_dependent_parameter(fit_engine):
234232
sp_sin = AbsSin(1, 0.5)
235233

236234
x = np.linspace(0, 5, 200)
235+
weights = np.ones_like(x)
237236
y = ref_sin(x)
238237

239238
f = Fitter(sp_sin, sp_sin)
@@ -246,31 +245,27 @@ def test_dependent_parameter(fit_engine):
246245
except AttributeError:
247246
pytest.skip(msg=f"{fit_engine} is not installed")
248247

249-
result = f.fit(x, y)
248+
result = f.fit(x, y, weights=weights)
250249
check_fit_results(result, sp_sin, ref_sin, x)
251250

252-
@pytest.mark.parametrize("with_errors", [False, True])
253251
@pytest.mark.parametrize("fit_engine", [None, AvailableMinimizers.LMFit, AvailableMinimizers.Bumps, AvailableMinimizers.DFO])
254-
def test_2D_vectorized(fit_engine, with_errors):
252+
def test_2D_vectorized(fit_engine):
255253
x = np.linspace(0, 5, 200)
256254
mm = AbsSin2D(0.3, 1.6)
257255
m2 = AbsSin2D(
258256
0.1, 1.8
259257
) # The fit is quite sensitive to the initial values :-(
260258
X, Y = np.meshgrid(x, x)
261259
XY = np.stack((X, Y), axis=2)
260+
weights = np.ones_like(mm(XY))
262261
ff = Fitter(m2, m2)
263262
if fit_engine is not None:
264263
try:
265264
ff.switch_minimizer(fit_engine)
266265
except AttributeError:
267266
pytest.skip(msg=f"{fit_engine} is not installed")
268267
try:
269-
args = [XY, mm(XY)]
270-
kwargs = {"vectorized": True}
271-
if with_errors:
272-
kwargs["weights"] = 1 / np.sqrt(args[1])
273-
result = ff.fit(*args, **kwargs)
268+
result = ff.fit(x=XY, y=mm(XY), weights=weights, vectorized=True)
274269
except FitError as e:
275270
if "Unable to allocate" in str(e):
276271
pytest.skip(msg="MemoryError - Matrix too large")
@@ -285,28 +280,24 @@ def test_2D_vectorized(fit_engine, with_errors):
285280
assert result.residual == pytest.approx(mm(XY) - y_calc_ref, abs=1e-2)
286281

287282

288-
@pytest.mark.parametrize("with_errors", [False, True])
289283
@pytest.mark.parametrize("fit_engine", [None, AvailableMinimizers.LMFit, AvailableMinimizers.Bumps, AvailableMinimizers.DFO])
290-
def test_2D_non_vectorized(fit_engine, with_errors):
284+
def test_2D_non_vectorized(fit_engine):
291285
x = np.linspace(0, 5, 200)
292286
mm = AbsSin2DL(0.3, 1.6)
293287
m2 = AbsSin2DL(
294288
0.1, 1.8
295289
) # The fit is quite sensitive to the initial values :-(
296290
X, Y = np.meshgrid(x, x)
297291
XY = np.stack((X, Y), axis=2)
292+
weights = np.ones_like(mm(XY.reshape(-1, 2)))
298293
ff = Fitter(m2, m2)
299294
if fit_engine is not None:
300295
try:
301296
ff.switch_minimizer(fit_engine)
302297
except AttributeError:
303298
pytest.skip(msg=f"{fit_engine} is not installed")
304299
try:
305-
args = [XY, mm(XY.reshape(-1, 2))]
306-
kwargs = {"vectorized": False}
307-
if with_errors:
308-
kwargs["weights"] = 1 / np.sqrt(args[1])
309-
result = ff.fit(*args, **kwargs)
300+
result = ff.fit(x=XY, y=mm(XY.reshape(-1, 2)), weights=weights, vectorized=False)
310301
except FitError as e:
311302
if "Unable to allocate" in str(e):
312303
pytest.skip(msg="MemoryError - Matrix too large")
@@ -329,6 +320,7 @@ def test_fixed_parameter_does_not_change(fit_engine):
329320
sp_sin = AbsSin(0.354, 3.05)
330321

331322
x = np.linspace(0, 5, 200)
323+
weights = np.ones_like(x)
332324
y = ref_sin(x)
333325

334326
# Fix the offset, only phase should be optimized
@@ -345,7 +337,7 @@ def test_fixed_parameter_does_not_change(fit_engine):
345337
except AttributeError:
346338
pytest.skip(msg=f"{fit_engine} is not installed")
347339

348-
result = f.fit(x, y)
340+
result = f.fit(x=x, y=y, weights=weights)
349341

350342
# EXPECT
351343
# Offset should remain unchanged

0 commit comments

Comments
 (0)