Skip to content

Commit ce35ad7

Browse files
authored
Add implementation of sparse inversion (#28)
Add a new `SparseSmallness` regularization that makes use of Lawson's equation to approximate an Lp norm with an L2 norm. Add a new `Irls` directive that implements the IRLS algorithm (Fournier et al., 2019): it updates the IRLS threshold in sparse regularizations and cools down the beta multiplier based on the current value of the data misfit. Add a new `create_sparse_inversion` constructor function. Add two notebooks that run gravity sparse inversions.
1 parent 0bfc4a6 commit ce35ad7

File tree

9 files changed

+4222
-27
lines changed

9 files changed

+4222
-27
lines changed

notebooks/30_gravity-sparse-inversion.ipynb

Lines changed: 1823 additions & 0 deletions
Large diffs are not rendered by default.

notebooks/31_gravity-sparse-inversion-constructor.ipynb

Lines changed: 1748 additions & 0 deletions
Large diffs are not rendered by default.

src/inversion_ideas/__init__.py

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,15 +5,19 @@
55
from . import base, typing, utils
66
from ._version import __version__
77
from .conditions import ChiTarget, CustomCondition, ModelChanged, ObjectiveChanged
8-
from .constructors import create_inversion
8+
from .constructors import create_inversion, create_sparse_inversion
99
from .data_misfit import DataMisfit
10-
from .directives import MultiplierCooler, UpdateSensitivityWeights
10+
from .directives import (
11+
Irls,
12+
MultiplierCooler,
13+
UpdateSensitivityWeights,
14+
)
1115
from .errors import ConvergenceWarning
1216
from .inversion import Inversion
1317
from .inversion_log import InversionLog, InversionLogRich
1418
from .minimize import GaussNewtonConjugateGradient, conjugate_gradient
1519
from .preconditioners import JacobiPreconditioner, get_jacobi_preconditioner
16-
from .regularization import Flatness, Smallness, TikhonovZero
20+
from .regularization import Flatness, Smallness, SparseSmallness, TikhonovZero
1721
from .simulations import wrap_simulation
1822

1923
__all__ = [
@@ -26,17 +30,20 @@
2630
"Inversion",
2731
"InversionLog",
2832
"InversionLogRich",
33+
"Irls",
2934
"JacobiPreconditioner",
3035
"ModelChanged",
3136
"MultiplierCooler",
3237
"ObjectiveChanged",
3338
"Smallness",
39+
"SparseSmallness",
3440
"TikhonovZero",
3541
"UpdateSensitivityWeights",
3642
"__version__",
3743
"base",
3844
"conjugate_gradient",
3945
"create_inversion",
46+
"create_sparse_inversion",
4047
"get_jacobi_preconditioner",
4148
"typing",
4249
"utils",

src/inversion_ideas/conditions.py

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,9 @@ class ChiTarget(Condition):
8181
"""
8282

8383
def __init__(self, data_misfit: DataMisfit, chi_target=1.0):
84+
if not hasattr(data_misfit, "chi_factor"):
85+
msg = "Invalid `data_misfit`: missing `chi_factor` method."
86+
raise TypeError(msg)
8487
self.data_misfit = data_misfit
8588
self.chi_target = chi_target
8689

@@ -250,6 +253,18 @@ def info(self, model) -> Tree:
250253
tree.add(f"atol = {self.atol:.2e}")
251254
return tree
252255

256+
def ratio(self, model) -> float:
257+
"""
258+
Ratio ``|φ(m) - φ(m_prev)|/|φ(m_prev)|``.
259+
"""
260+
if not hasattr(self, "previous"):
261+
return np.nan
262+
diff = abs(self.objective_function(model) - self.previous)
263+
previous = abs(self.previous)
264+
if previous == 0.0:
265+
return np.inf
266+
return diff / previous
267+
253268
def initialize(self):
254269
"""
255270
Initialize condition and clean ``previous`` attribute.

src/inversion_ideas/constructors.py

Lines changed: 146 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,9 +8,11 @@
88
import numpy.typing as npt
99

1010
from .base import Minimizer, Objective
11-
from .conditions import ChiTarget
12-
from .directives import MultiplierCooler
11+
from .conditions import ChiTarget, ObjectiveChanged
12+
from .data_misfit import DataMisfit
13+
from .directives import Irls, MultiplierCooler
1314
from .inversion import Inversion
15+
from .inversion_log import Column
1416
from .preconditioners import JacobiPreconditioner
1517
from .typing import Model, Preconditioner
1618

@@ -43,7 +45,7 @@ def create_inversion(
4345
Model norm :math:`\phi_m`.
4446
starting_beta : float
4547
Starting value for the trade-off parameter :math:`\beta`.
46-
initial_model : npt.NDArray[np.float64]
48+
initial_model : (n_params) array
4749
Initial model to use in the inversion.
4850
minimizer : Minimizer
4951
Instance of :class:`Minimizer` used to minimize the objective function during
@@ -116,3 +118,144 @@ def create_inversion(
116118
minimizer_kwargs=minimizer_kwargs,
117119
)
118120
return inversion
121+
122+
123+
def create_sparse_inversion(
124+
data_misfit: DataMisfit,
125+
model_norm: Objective,
126+
*,
127+
starting_beta: float,
128+
initial_model: Model,
129+
minimizer: Minimizer | Callable[[Objective, Model], Model],
130+
beta_cooling_factor: float = 2.0,
131+
data_misfit_rtol=1e-1,
132+
chi_l2_target: float = 1.0,
133+
model_norm_rtol: float = 1e-3,
134+
max_iterations: int | None = None,
135+
cache_models: bool = True,
136+
preconditioner: Preconditioner | Callable[[Model], Preconditioner] | None = None,
137+
) -> Inversion:
138+
r"""
139+
Create sparse norm inversion of the form: :math:`\phi_d + \beta \phi_m`.
140+
141+
Build an inversion where :math:`\phi_m` is a sparse norm regularization.
142+
An IRLS algorithm will be applied, split in two stages.
143+
The inversion will stop when the following inequality holds:
144+
145+
.. math::
146+
147+
\frac{|\phi_m^{(k)} - \phi_m^{(k-1)}|}{|\phi_m^{(k-1)}|} < \eta_{\phi_m}
148+
149+
where :math:`\eta_{\phi_m}` is the ``model_norm_rtol``.
150+
151+
Parameters
152+
----------
153+
data_misfit : Objective
154+
Data misfit term :math:`\phi_d`.
155+
model_norm : Objective
156+
Model norm :math:`\phi_m`. It can be a single objective function term or a combo
157+
containing multiple ones. At least one of them should be a sparse regularization
158+
term.
159+
starting_beta : float
160+
Starting value for the trade-off parameter :math:`\beta`.
161+
initial_model : (n_params) array
162+
Initial model to use in the inversion.
163+
minimizer : Minimizer
164+
Instance of :class:`Minimizer` used to minimize the objective function during
165+
the inversion.
166+
beta_cooling_factor : float, optional
167+
Cooling factor for the trade-off parameter :math:`\beta`. Every
168+
``beta_cooling_rate`` iterations, the :math:`\beta` will be _cooled down_ by
169+
dividing it by the ``beta_cooling_factor``.
170+
data_misfit_rtol : float, optional
171+
Tolerance for the data misfit. This value is used to determine whether to cool
172+
down the IRLS threshold or beta. See eq. 21 in Fournier and Oldenburg (2019).
173+
chi_l2_target : float, optional
174+
Chi factor target for the stage one (the L2 inversion). Once this chi target is
175+
reached, the second stage starts.
176+
model_norm_rtol : float, optional
177+
Tolerance for the model norm. This value is used to determine if the inversion
178+
should stop. See eq. 22 in Fournier and Oldenburg (2019).
179+
max_iterations : int, optional
180+
Max amount of iterations that will be performed. If ``None``, then there will be
181+
no limit on the total amount of iterations.
182+
cache_models : bool, optional
183+
Whether to cache models after each iteration in the inversion.
184+
preconditioner : {"jacobi"} or 2d array or sparray or LinearOperator or callable or None, optional
185+
Preconditioner that will be passed to the ``minimizer`` on every call during the
186+
inversion. The preconditioner can be a predefined 2d array, a sparse array or
187+
a LinearOperator. Alternatively, it can be a callable that takes the ``model``
188+
as argument and returns a preconditioner matrix (same types listed before). If
189+
``"jacobi"``, a default Jacobi preconditioner that will get updated on every
190+
iteration will be defined for the inversion. If None, no preconditioner will be
191+
passed.
192+
193+
Returns
194+
-------
195+
Inversion
196+
"""
197+
# Define objective function
198+
regularization = starting_beta * model_norm
199+
objective_function = data_misfit + regularization
200+
201+
# Define IRLS directive
202+
directives = [
203+
Irls(
204+
regularization,
205+
data_misfit=data_misfit,
206+
chi_l2_target=chi_l2_target,
207+
beta_cooling_factor=beta_cooling_factor,
208+
data_misfit_rtol=data_misfit_rtol,
209+
)
210+
]
211+
212+
# Stopping criteria
213+
smallness_not_changing = ObjectiveChanged(model_norm, rtol=model_norm_rtol)
214+
215+
# Preconditioner
216+
minimizer_kwargs = {}
217+
if preconditioner is not None:
218+
if isinstance(preconditioner, str):
219+
if preconditioner == "jacobi":
220+
preconditioner = JacobiPreconditioner(objective_function)
221+
else:
222+
msg = f"Invalid preconditioner '{preconditioner}'."
223+
raise ValueError(msg)
224+
minimizer_kwargs["preconditioner"] = preconditioner
225+
226+
# Define inversion
227+
inversion = Inversion(
228+
objective_function,
229+
initial_model,
230+
minimizer,
231+
directives=directives,
232+
stopping_criteria=smallness_not_changing,
233+
cache_models=cache_models,
234+
max_iterations=max_iterations,
235+
log=True,
236+
minimizer_kwargs=minimizer_kwargs,
237+
)
238+
239+
# Add extra columns to log
240+
if inversion.log is not None:
241+
# TODO: fix this in case that model norm is a combo
242+
inversion.log.add_column(
243+
"IRLS", lambda _, __: "active" if model_norm.irls else "inactive"
244+
)
245+
inversion.log.add_column(
246+
"IRLS threshold",
247+
Column(
248+
title="ε",
249+
callable=lambda _, __: model_norm.threshold,
250+
fmt=None,
251+
),
252+
)
253+
inversion.log.add_column(
254+
"model_norm_relative_diff",
255+
Column(
256+
title=r"|φm_(k) - φm_(k-1)|/|φm_(k-1)|",
257+
callable=lambda _, model: smallness_not_changing.ratio(model),
258+
fmt=None,
259+
),
260+
)
261+
return inversion

0 commit comments

Comments
 (0)