Skip to content

Commit 52e269b

Browse files
committed
Issue a warning if exiting without convergence
1 parent fc7a954 commit 52e269b

File tree

2 files changed

+53
-23
lines changed

2 files changed

+53
-23
lines changed

harmonica/_euler_methods.py

Lines changed: 13 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,8 @@
88
Classes for Euler-based source location estimation.
99
"""
1010

11+
import warnings
12+
1113
import numpy as np
1214
import scipy as sp
1315
import verde.base as vdb
@@ -391,7 +393,7 @@ def _fit(self, coordinates, data, weights):
391393
data_misfit = np.linalg.norm(residuals * data_weights)
392394
merit = data_misfit + self.euler_misfit_balance * euler_misfit
393395
for _ in range(self.max_iterations):
394-
parameter_step, data_step, cofactor_new = self._newton_step(
396+
parameter_step, data_step, cofactor = self._newton_step(
395397
coordinates,
396398
data_observed,
397399
data_predicted,
@@ -404,25 +406,21 @@ def _fit(self, coordinates, data, weights):
404406
# Update metrics
405407
euler = self._eulers_equation(coordinates, data_predicted, parameters)
406408
residuals = data_observed - data_predicted
407-
new_euler_misfit = np.linalg.norm(euler)
408-
new_data_misfit = np.linalg.norm(residuals * data_weights)
409-
new_merit = new_data_misfit + self.euler_misfit_balance * new_euler_misfit
410-
# Check if was increasing the merit function, which means this solution is
411-
# worse than the last.
412-
if new_merit > merit:
413-
# If it's bad, walk back the last step
414-
data_predicted -= data_step
415-
parameters -= parameter_step
416-
break
417-
cofactor = cofactor_new
418-
# Update tracked metrics
419-
euler_misfit = new_euler_misfit
420-
data_misfit = new_data_misfit
409+
euler_misfit = np.linalg.norm(euler)
410+
data_misfit = np.linalg.norm(residuals * data_weights)
411+
new_merit = data_misfit + self.euler_misfit_balance * euler_misfit
421412
merit_change = abs((merit - new_merit) / merit)
422413
merit = new_merit
423414
# Check for convergence
424415
if merit_change < self.tol:
425416
break
417+
else:
418+
message = (
419+
"Euler Inversion exited because maximum number of iterations was reached"
420+
" and not because the algorithm converged. Consider increasing the"
421+
" maximum number of iterations."
422+
)
423+
warnings.warn(message, stacklevel=2)
426424
# Save output attributes
427425
self.location_ = parameters[:3]
428426
self.base_level_ = parameters[3]

harmonica/tests/test_euler_methods.py

Lines changed: 40 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -59,8 +59,7 @@ def test_euler_inversion_with_numeric_derivatives(euler):
5959
npt.assert_allclose(euler.location_[:2], dipole_coordinates[:2], rtol=0.01)
6060
npt.assert_allclose(euler.location_[2], dipole_coordinates[2], rtol=0.05)
6161
npt.assert_allclose(euler.base_level_, true_base_level, rtol=0.01)
62-
if hasattr(euler, "structural_index_"):
63-
assert euler.structural_index_ == 3
62+
assert euler.structural_index_ == 3
6463

6564

6665
@pytest.mark.parametrize(
@@ -107,8 +106,45 @@ def test_euler_inversion_with_analytic_derivatives(euler):
107106
npt.assert_allclose(euler.location_[:2], masses_coordinates[:2], rtol=0.01)
108107
npt.assert_allclose(euler.location_[2], masses_coordinates[2], rtol=0.05)
109108
npt.assert_allclose(euler.base_level_, base_level, rtol=0.01)
110-
if hasattr(euler, "structural_index_"):
111-
assert euler.structural_index_ == 2
109+
assert euler.structural_index_ == 2
110+
111+
112+
113+
@pytest.mark.parametrize(
114+
"euler",
115+
[
116+
EulerInversion(structural_index=3, max_iterations=1),
117+
EulerInversion(max_iterations=1),
118+
],
119+
ids=("fixed_SI", "estimate_SI"),
120+
)
121+
def test_euler_inversion_convergence_warning(euler):
122+
"Check that a warning is raised when the method exits without convergence"
123+
# Add dipole source
124+
dipole_coordinates = (10e3, 15e3, -10e3)
125+
inc, dec = -40, 15
126+
dipole_moments = magnetic_angles_to_vec(1.0e14, inc, dec)
127+
region = [-100e3, 100e3, -80e3, 80e3]
128+
coordinates = vd.grid_coordinates(region, spacing=500, extra_coords=500)
129+
b = dipole_magnetic(coordinates, dipole_coordinates, dipole_moments, field="b")
130+
# Add a fixed base level
131+
true_base_level = 200 # nT
132+
anomaly = total_field_anomaly(b, inc, dec) + true_base_level
133+
anomaly += np.random.default_rng(42).normal(0, 20, anomaly.shape)
134+
grid = vd.make_xarray_grid(
135+
coordinates, anomaly, data_names="tfa", extra_coords_names="upward"
136+
)
137+
grid["d_east"] = derivative_easting(grid.tfa)
138+
grid["d_north"] = derivative_northing(grid.tfa)
139+
grid["d_up"] = derivative_upward(grid.tfa)
140+
table = vd.grid_to_table(grid)
141+
coordinates = (table.easting, table.northing, table.upward)
142+
with pytest.warns(UserWarning, match="Euler Inversion exited"):
143+
euler.fit(
144+
(table.easting, table.northing, table.upward),
145+
(table.tfa, table.d_east, table.d_north, table.d_up),
146+
)
147+
112148

113149

114150
def test_euler_deconvolution_with_numeric_derivatives():
@@ -139,8 +175,6 @@ def test_euler_deconvolution_with_numeric_derivatives():
139175
)
140176
npt.assert_allclose(euler.location_, dipole_coordinates, rtol=0.01)
141177
npt.assert_allclose(euler.base_level_, true_base_level, rtol=0.01)
142-
if hasattr(euler, "structural_index_"):
143-
npt.assert_allclose(euler.structural_index_, 3, rtol=0.01)
144178

145179

146180
def test_euler_deconvolution_with_analytic_derivatives():
@@ -172,5 +206,3 @@ def test_euler_deconvolution_with_analytic_derivatives():
172206
)
173207
npt.assert_allclose(euler.location_, masses_coordinates, atol=1.0e-3, rtol=1.0e-3)
174208
npt.assert_allclose(euler.base_level_, 0.0, atol=1.0e-3, rtol=1.0e-3)
175-
if hasattr(euler, "structural_index_"):
176-
npt.assert_allclose(euler.structural_index_, 2, rtol=0.01)

0 commit comments

Comments
 (0)