1414import numpy as np
1515import verde as vd
1616
17- import magali as mg
18-
1917from ._synthetic import dipole_bz
2018from ._units import (
2119 coordinates_micrometer_to_meter ,
2220 meter_to_micrometer ,
2321 tesla_to_nanotesla ,
2422)
23+ from ._utils import gradient
2524from ._validation import check_fit_input
2625
2726
@@ -113,7 +112,7 @@ def jacobian(self, coordinates):
113112
114113def _jacobian_linear (x , y , z , xc , yc , zc , result ):
115114 """
116- Jit -compiled version of the Jacobian matrix calculation for the linear inversion.
115+ JIT -compiled Jacobian calculation for linear inversion.
117116 """
118117 factor = choclo .constants .VACUUM_MAGNETIC_PERMEABILITY / (4 * np .pi )
119118 n_data = x .size
@@ -129,7 +128,7 @@ def _jacobian_linear(x, y, z, xc, yc, zc, result):
129128
130129class NonlinearMagneticDipoleBz :
131130 """
132- Estimate the position and magnetic dipole moment vector from Bz measurements .
131+ Estimate dipole position and moment from Bz data .
133132
134133 Uses the Bz component of the magnetic field to estimate both the position
135134 and the moment of a magnetic dipole through a nonlinear inversion based on
@@ -140,17 +139,19 @@ class NonlinearMagneticDipoleBz:
140139 Parameters
141140 ----------
142141 initial_location : tuple of float
143- Initial guess for the coordinates (x, y, z) of the dipole location, in µm.
142+ Initial guess for the coordinates (x, y, z) of the dipole location, in
143+ µm.
144144 max_iter : int
145145 Maximum number of iterations for both the outer and inner loops of the
146146 nonlinear inversion.
147147 tol : float
148- Convergence tolerance for the relative change in misfit between iterations.
148+ Convergence tolerance for the relative change in misfit between
149+ iterations.
149150 alpha_init : float
150151 Initial damping parameter for the Levenberg-Marquardt algorithm.
151152 alpha_scale : float
152- Multiplicative factor used to increase or decrease the damping parameter
153- during the optimization.
153+ Multiplicative factor used to increase or decrease the damping
154+ parameter during the optimization.
154155
155156 Attributes
156157 ----------
@@ -170,7 +171,7 @@ def __init__(
170171 self ,
171172 initial_location ,
172173 max_iter = 100 ,
173- tol = 1e-4 ,
174+ tol = 1e-2 ,
174175 alpha_init = 1 ,
175176 alpha_scale = 10.0 ,
176177 ):
@@ -196,8 +197,8 @@ def predict(self, coordinates):
196197 Returns
197198 -------
198199 predicted : array
199- Array with the predicted Bz values (in nT) at the observation points.
200- Has the same shape as the input coordinate arrays.
200+ Array with the predicted Bz values (in nT) at the observation
201+ points. Has the same shape as the input coordinate arrays.
201202
202203 Raises
203204 ------
@@ -266,36 +267,41 @@ def fit(self, coordinates, data):
266267
267268 Performs nonlinear inversion using the Levenberg-Marquardt method to
268269 estimate both the dipole location and its magnetic moment. The method
269- alternates between a nonlinear update of the dipole location (inner loop)
270- and a linear least-squares estimate of the dipole moment (outer loop).
271- The Jacobian matrix with respect to the location is computed numerically
272- using JIT-accelerated code.
270+ alternates between a nonlinear update of the dipole location (inner
271+ loop) and a linear least-squares estimate of the dipole moment (outer
272+ loop). The Jacobian matrix with respect to the location is computed
273+ numerically using JIT-accelerated code.
273274
274275 The Jacobian matrix used in the nonlinear step contains partial
275276 derivatives of the Bz field with respect to the dipole location:
276- :math:`\frac{\partial B_z}{\partial x_0}`, :math:`\frac{\partial B_z}{\partial y_0}`,
277- and :math:`\frac{\partial B_z}{\partial z_0}`. These are computed assuming a fixed
278- moment vector.
277+ :math:`\frac{\partial B_z}{\partial x_0}`,
278+ :math:`\frac{\partial B_z}{\partial y_0}`, and
279+ :math:`\frac{\partial B_z}{\partial z_0}`. These are computed assuming
280+ a fixed moment vector.
279281
280282 At each inner iteration:
281283
282- - The forward field is computed using the trial location and fixed moment.
284+ - The forward field is computed using the trial location and fixed
285+ moment.
283286 - A trial update is accepted if it reduces the data misfit.
284287 - The damping parameter (alpha) is adapted based on success/failure.
285288
286289 At each outer iteration:
287290
288- - A new linear estimate of the dipole moment is computed for the current location.
291+ - A new linear estimate of the dipole moment is computed for the
292+ current location.
289293 - Convergence is assessed based on relative reduction in residual norm.
290294
291295 Parameters
292296 ----------
293297 coordinates : tuple of array-like
294298 Arrays with the x, y, z coordinates of the observation points.
295- The arrays can have any shape as long as they all have the same shape.
299+ The arrays can have any shape as long as they all have the same
300+ shape.
296301 data : array-like
297- Observed Bz component of the magnetic field (in nT) at the observation
298- points. Must have the same shape as the coordinate arrays.
302+ Observed Bz component of the magnetic field (in nT) at the
303+ observation points. Must have the same shape as the coordinate
304+ arrays.
299305
300306 Returns
301307 -------
@@ -308,11 +314,12 @@ def fit(self, coordinates, data):
308314 Internally uses:
309315
310316 - :func:`jacobian_nonlinear_jit`: JIT-compiled function that fills the
311- Jacobian matrix :math:`\frac{\partial B_z}{\partial (x_0, y_0, z_0)}` for a fixed moment.
312- - :func:`dipole_bz`: forward model for the Bz field of a dipole at given
313- coordinates.
314- - :class:`MagneticMomentBz`: linear inversion for estimating moment given a
315- fixed location.
317+ Jacobian matrix :math:`\frac{\partial B_z}{\partial (x_0, y_0, z_0)}`
318+ for a fixed moment.
319+ - :func:`dipole_bz`: forward model for the Bz field of a dipole at
320+ given coordinates.
321+ - :class:`MagneticMomentBz`: linear inversion for estimating moment
322+ given a fixed location.
316323 """
317324 coordinates , data = check_fit_input (coordinates , data )
318325 coordinates_m = tuple (
@@ -324,18 +331,29 @@ def fit(self, coordinates, data):
324331 moment = linear_model .dipole_moment_
325332 residual = data - dipole_bz (coordinates , location , moment )
326333 misfit = [np .linalg .norm (residual )]
327- alpha = self .alpha_init
328334 jacobian = np .empty ((data .size , 3 ))
329- identity = np .identity (3 )
330335 for _ in range (self .max_iter ):
331336 location_misfit = [misfit [- 1 ]]
332337 for _ in range (self .max_iter ):
333338 jacobian = self .jacobian (coordinates_m , location , moment , jacobian )
334339 hessian = jacobian .T @ jacobian
340+ # Make alpha proportional to the curvature scale
341+ scaling_factor = 1e-20
342+ alpha = scaling_factor * max (np .median (np .diag (hessian )), 1e-30 )
335343 gradient = jacobian .T @ residual
336344 took_a_step = False
337345 for _ in range (50 ):
338- delta = np .linalg .solve (hessian + alpha * identity , gradient )
346+ # build damping matrix proportional to diag(H)
347+ diagH = np .diag (np .diag (hessian ))
348+ damping = alpha * diagH
349+ # small floor to avoid zero diagonal
350+ damping += 1e-20 * np .eye (3 )
351+ delta = np .linalg .solve (hessian + damping , gradient )
352+ max_step_m = 1e-6 # 10 µm
353+ step_norm = np .linalg .norm (delta )
354+ if step_norm > max_step_m :
355+ delta = delta * (max_step_m / step_norm )
356+
339357 trial_location = location + meter_to_micrometer (delta )
340358 trial_predicted = dipole_bz (
341359 coordinates ,
@@ -380,14 +398,16 @@ def fit(self, coordinates, data):
380398
381399def _jacobian_nonlinear (x , y , z , xc , yc , zc , mx , my , mz , result ):
382400 """
383- Jit -compiled version of the Jacobian matrix calculation for the nonlinear inversion.
401+ JIT -compiled Jacobian for nonlinear inversion.
384402 """
385403 factor = choclo .constants .VACUUM_MAGNETIC_PERMEABILITY / (4 * np .pi )
386404 for i in numba .prange (x .size ):
387405 dx = x [i ] - xc
388406 dy = y [i ] - yc
389407 dz = z [i ] - zc
390408 r2 = dx ** 2 + dy ** 2 + dz ** 2
409+ # prevent huge derivatives if (xc,yc,zc) gets very near an observation
410+ r2 = max (r2 , 1e-18 )
391411 r5 = r2 ** (5 / 2 )
392412 r7 = r2 ** (7 / 2 )
393413 # ∂bz / ∂xc
@@ -477,7 +497,7 @@ def iterative_nonlinear_inversion(
477497 for box in bounding_boxes :
478498 anomaly = data_updated .sel (x = slice (* box [:2 ]), y = slice (* box [2 :]))
479499
480- dx , dy , dz , tga = mg . gradient (anomaly )
500+ dx , dy , dz , tga = gradient (anomaly )
481501 anomaly ["dx" ], anomaly ["dy" ], anomaly ["dz" ], anomaly ["tga" ] = dx , dy , dz , tga
482502
483503 table = vd .grid_to_table (anomaly )
@@ -488,20 +508,20 @@ def iterative_nonlinear_inversion(
488508 bz_corrected = table .bz .values - euler .base_level_
489509 coordinates = (table .x .values , table .y .values , table .z .values )
490510
491- model_nl = mg . NonlinearMagneticDipoleBz (
511+ model_nl = NonlinearMagneticDipoleBz (
492512 initial_location = euler .location_ , max_iter = 1000
493513 )
494514 model_nl .fit (coordinates , bz_corrected )
495515
496- locations_ .append (euler .location_ )
516+ locations_ .append (model_nl .location_ )
497517 dipole_moments_ .append (model_nl .dipole_moment_ )
498518 r2_values .append (model_nl .r2_ )
499519
500- modeled_bz = mg . dipole_bz (
520+ modeled_bz = dipole_bz (
501521 global_coordinates , model_nl .location_ , model_nl .dipole_moment_
502522 )
503- for x_val , y_val , bz_val in zip (table .x . values , table .y . values , modeled_bz ):
504- data_updated .loc [{"x" : x_val , "y" : y_val }] -= bz_val
523+ for x , y , bz in zip (table .x , table .y , modeled_bz ):
524+ data_updated .loc [{"x" : x , "y" : y }] -= bz
505525
506526 data_updated = (
507527 hm .upward_continuation (data_updated , height_difference )
@@ -510,7 +530,7 @@ def iterative_nonlinear_inversion(
510530 .assign_coords (z = data_updated .z + height_difference )
511531 .rename ("bz" )
512532 )
513- dx , dy , dz , tga = mg . gradient (data_updated )
533+ dx , dy , dz , tga = gradient (data_updated )
514534 data_updated ["dx" ] = dx
515535 data_updated ["dy" ] = dy
516536 data_updated ["dz" ] = dz
0 commit comments