Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
44 commits
Select commit Hold shift + click to select a range
fab813d
Draft Wires classes
santisoler Feb 3, 2026
4fe7b77
Add a BlockColumnMatrix custom operator
santisoler Feb 3, 2026
fabbaab
Improve some bits of the Wires and add methods
santisoler Feb 3, 2026
66fbc48
Fix bugs and improve Wires
santisoler Feb 3, 2026
5b825ca
Add support for model_slice to DataMisfit
santisoler Feb 3, 2026
ec3627f
Add notebook trying out gravity inversion with model slice
santisoler Feb 4, 2026
b707be0
Add docstring to Wires
santisoler Feb 4, 2026
2ffc4ab
Add `get_column` method to the `BlockColumnMatrix`
santisoler Feb 4, 2026
a56578f
Support `DataMisfit.hessian_diagonal` when `model_slice` is not None
santisoler Feb 4, 2026
e42130e
Rerun notebook
santisoler Feb 4, 2026
133a6fd
Merge branch 'main' into model-wiring
santisoler Feb 4, 2026
9e25e24
Fix type hint
santisoler Feb 4, 2026
7ce5f65
Improve docstring in DataMisfit
santisoler Feb 4, 2026
da9c49d
Support model_slice in Smallness
santisoler Feb 4, 2026
0915641
Rerun notebook
santisoler Feb 4, 2026
16778c1
Use a slicer matrix in DataMisfit instead of the BlockColumnMatrix
santisoler Feb 5, 2026
5e09873
Make use of the slicer matrix in Smallness
santisoler Feb 5, 2026
b1899e0
Delete the `operators` submodule
santisoler Feb 5, 2026
6f683f8
Implement model_slice in Flatness
santisoler Feb 5, 2026
c24988e
Make _slicer_matrix a property of DataMisfit and regs
santisoler Feb 5, 2026
724eda9
Add a BlockSquareMatrix class
santisoler Feb 10, 2026
3ff0c40
Add expand_array method to Wires
santisoler Feb 10, 2026
3d8ff90
Make use of ModelSlice methods in DataMisfit
santisoler Feb 10, 2026
11fcdf9
Use ModelSlice methods in mesh-based regularizations
santisoler Feb 10, 2026
cbf618b
Fix bug in definition of reference_model
santisoler Feb 10, 2026
90cc096
Rerun notebook
santisoler Feb 10, 2026
d7b832c
Define a support_model_slice decorator
santisoler Feb 10, 2026
c9e7239
Rerun notebook
santisoler Feb 10, 2026
f4f7dbe
Merge branch 'main' into model-wiring-matrix-slicer
santisoler Feb 10, 2026
b2fe79f
Add a HasDiagonal protocol
santisoler Feb 10, 2026
f04ff22
Add a MultiSlice class
santisoler Feb 11, 2026
ddfc411
Add a base class for model slices to avoid repetition
santisoler Feb 11, 2026
0f0bd00
Allow to get MultiSlice from Wires
santisoler Feb 11, 2026
605b6e3
Merge branch 'main' into model-wiring-matrix-slicer
santisoler Feb 11, 2026
0d02ba8
Merge branch 'multi-slicer' into model-wiring-matrix-slicer
santisoler Feb 11, 2026
053b182
Fix style
santisoler Feb 11, 2026
d500b1c
Fix import
santisoler Feb 11, 2026
b263337
Update type hints
santisoler Feb 11, 2026
b0bfa7e
Merge branch 'main' into model-wiring-matrix-slicer
santisoler Feb 12, 2026
e8ffcf6
Use SparseArray type alias in wires.py
santisoler Feb 12, 2026
4b67f24
Remove TODO comment
santisoler Feb 12, 2026
fd3bded
Add more TODO comments
santisoler Feb 12, 2026
ac847de
Fix comment in regularizations
santisoler Feb 12, 2026
27fc395
Merge branch 'main' into model-wiring-matrix-slicer
santisoler Mar 25, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1,082 changes: 1,082 additions & 0 deletions notebooks/50_gravity-inversion-with-model-slice.ipynb

Large diffs are not rendered by default.

2 changes: 2 additions & 0 deletions src/inversion_ideas/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
)
from .regularization import Flatness, Smallness, SparseSmallness, TikhonovZero
from .simulations import wrap_simulation
from .wires import Wires

__all__ = [
"ChiTarget",
Expand All @@ -43,6 +44,7 @@
"SparseSmallness",
"TikhonovZero",
"UpdateSensitivityWeights",
"Wires",
"__version__",
"base",
"conjugate_gradient",
Expand Down
24 changes: 19 additions & 5 deletions src/inversion_ideas/data_misfit.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@

from .base import Objective
from .typing import Model, SparseArray
from .utils import support_model_slice
from .wires import ModelSlice, MultiSlice


class DataMisfit(Objective):
Expand Down Expand Up @@ -78,44 +80,52 @@ def __init__(
uncertainty: npt.NDArray[np.float64],
simulation,
*,
model_slice: ModelSlice | MultiSlice | None = None,
build_hessian=False,
):
# TODO: Check that the data and uncertainties have the size as ndata in the
# simulation.
self.data = data
self.uncertainty = uncertainty
self.simulation = simulation
self.model_slice = model_slice
self.build_hessian = build_hessian
self.set_name("d")

@support_model_slice
def __call__(self, model: Model) -> float:
residual = self.residual(model)
residual = self.simulation(model) - self.data
weights_matrix = self.weights_matrix
return residual.T @ weights_matrix.T @ weights_matrix @ residual

@support_model_slice
def gradient(self, model: Model) -> npt.NDArray[np.float64]:
"""
Gradient vector.
"""
jac = self.simulation.jacobian(model)
weights_matrix = self.weights_matrix
return 2 * jac.T @ (weights_matrix.T @ weights_matrix @ self.residual(model))
residual = self.simulation(model) - self.data
gradient = 2 * jac.T @ (weights_matrix.T @ weights_matrix @ residual)
return gradient

@support_model_slice
def hessian(
self, model: Model
) -> npt.NDArray[np.float64] | SparseArray | LinearOperator:
"""
Hessian matrix.
"""
jac = self.simulation.jacobian(model)
weights_matrix = aslinearoperator(self.weights_matrix)
if not self.build_hessian:
jac = aslinearoperator(jac)
weights_matrix = aslinearoperator(self.weights_matrix)
return 2 * jac.T @ weights_matrix.T @ weights_matrix @ jac

@support_model_slice
def hessian_diagonal(self, model: Model) -> npt.NDArray[np.float64]:
"""
Diagonal of the Hessian.
Approximated diagonal of the Hessian.
"""
jac = self.simulation.jacobian(model)
if isinstance(jac, LinearOperator):
Expand All @@ -132,6 +142,8 @@ def n_params(self):
"""
Number of model parameters.
"""
if self.model_slice is not None:
return self.model_slice.full_size
return self.simulation.n_params

@property
Expand Down Expand Up @@ -166,6 +178,8 @@ def residual(self, model: Model):
where :math:`\mathbf{d}` is the vector with observed data, :math:`\mathcal{F}`
is the forward model, and :math:`\mathbf{m}` is the model vector.
"""
if self.model_slice is not None:
model = self.model_slice.extract(model)
return self.simulation(model) - self.data

@property
Expand All @@ -176,7 +190,7 @@ def weights(self) -> npt.NDArray[np.float64]:
return 1 / self.uncertainty**2

@property
def weights_matrix(self) -> dia_array:
def weights_matrix(self) -> dia_array[np.float64]:
"""
Diagonal matrix with the square root of the regularization weights.
"""
Expand Down
4 changes: 4 additions & 0 deletions src/inversion_ideas/recipes.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
from .preconditioners import JacobiPreconditioner
from .regularization import Flatness, Smallness
from .typing import Model, Preconditioner
from .wires import ModelSlice, MultiSlice


def create_l2_inversion(
Expand Down Expand Up @@ -273,6 +274,7 @@ def create_tikhonov_regularization(
alpha_y: float | None = None,
alpha_z: float | None = None,
reference_model_in_flatness: bool = False,
model_slice: ModelSlice | MultiSlice | None = None,
) -> Combo:
"""
Create a linear combination of Tikhonov (L2) regularization terms.
Expand Down Expand Up @@ -321,13 +323,15 @@ def create_tikhonov_regularization(
active_cells=active_cells,
cell_weights=cell_weights,
reference_model=reference_model,
model_slice=model_slice,
)
if alpha_s is not None:
smallness = alpha_s * smallness

kwargs = {
"active_cells": active_cells,
"cell_weights": cell_weights,
"model_slice": model_slice,
}
if reference_model_in_flatness:
kwargs["reference_model"] = reference_model
Expand Down
61 changes: 43 additions & 18 deletions src/inversion_ideas/regularization/_mesh_based.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@
from .._utils import prod_arrays
from ..base import Objective
from ..typing import Model
from ..utils import support_model_slice
from ..wires import ModelSlice, MultiSlice


class _MeshBasedRegularization(Objective):
Expand All @@ -24,6 +26,12 @@ class _MeshBasedRegularization(Objective):

@property
def n_params(self) -> int:
if (model_slice := getattr(self, "model_slice", None)) is not None:
return model_slice.full_size
return self.n_active

@property
def n_active(self) -> int:
return int(np.sum(self.active_cells))

@property
Expand All @@ -48,7 +56,7 @@ def cell_weights(
"It must be an array or a dictionary."
)
raise TypeError(msg)
if isinstance(value, np.ndarray) and value.size != self.n_params:
if isinstance(value, np.ndarray) and value.size != self.n_active:
msg = (
f"Invalid cell_weights array with '{value.size}' elements. "
f"It must have '{self.n_params}' elements, "
Expand All @@ -57,7 +65,7 @@ def cell_weights(
raise ValueError(msg)
if isinstance(value, dict):
for key, array in value.items():
if array.size != self.n_params:
if array.size != self.n_active:
msg = (
f"Invalid cell_weights array '{key}' with "
f"'{array.size}' elements. "
Expand All @@ -81,13 +89,14 @@ class Smallness(_MeshBasedRegularization):
active_cells : (n_cells) array or None, optional
Array full of bools that indicate the active cells in the mesh. It must have the
same amount of elements as cells in the mesh.
cell_weights : (n_params) array or dict of (n_params) arrays or None, optional
cell_weights : (n_active) array or dict of (n_active) arrays or None, optional
Array with cell weights.
For multiple cell weights, pass a dictionary where keys are strings and values
are the different weights arrays.
If None, no cell weights are going to be used.
reference_model : (n_params) array or None, optional
Array with values for the reference model.
reference_model : (n_active) array or None, optional
Array with values for the reference model. It must have the same number of
elements as active cells in the mesh.

Notes
-----
Expand Down Expand Up @@ -138,28 +147,32 @@ def __init__(
active_cells: npt.NDArray[np.bool] | None = None,
cell_weights: npt.NDArray | dict[str, npt.NDArray] | None = None,
reference_model: Model | None = None,
model_slice: ModelSlice | MultiSlice | None = None,
):
self.mesh = mesh
self.active_cells = (
active_cells
if active_cells is not None
else np.ones(self.mesh.n_cells, dtype=bool)
)
# assign model_slice after active_cells so n_active is correct during __init__
self.model_slice = model_slice

# Assign the cell weights through the setter
self.cell_weights = (
cell_weights
if cell_weights is not None
else np.ones(self.n_params, dtype=np.float64)
else np.ones(self.n_active, dtype=np.float64)
)

self.reference_model = (
reference_model
if reference_model is not None
else np.zeros(self.n_params, dtype=np.float64)
else np.zeros(self.n_active, dtype=np.float64)
)
self.set_name("s")

@support_model_slice
def __call__(self, model: Model) -> float:
"""
Evaluate the regularization on a given model.
Expand All @@ -181,6 +194,7 @@ def __call__(self, model: Model) -> float:
@ model_diff
)

@support_model_slice
def gradient(self, model: Model):
"""
Gradient vector.
Expand All @@ -193,15 +207,17 @@ def gradient(self, model: Model):
model_diff = model - self.reference_model
weights_matrix = self.weights_matrix
cell_volumes_sqrt = self._volumes_sqrt_matrix
return (
gradient = (
2
* cell_volumes_sqrt.T
@ weights_matrix.T
@ weights_matrix
@ cell_volumes_sqrt
@ model_diff
)
return gradient

@support_model_slice
def hessian(self, model: Model): # noqa: ARG002
"""
Hessian matrix.
Expand All @@ -213,13 +229,14 @@ def hessian(self, model: Model): # noqa: ARG002
"""
weights_matrix = self.weights_matrix
cell_volumes_sqrt = self._volumes_sqrt_matrix
return (
hessian = (
2
* cell_volumes_sqrt.T
@ weights_matrix.T
@ weights_matrix
@ cell_volumes_sqrt
)
return hessian

def hessian_diagonal(self, model: Model) -> npt.NDArray[np.float64]:
"""
Expand All @@ -233,7 +250,7 @@ def hessian_diagonal(self, model: Model) -> npt.NDArray[np.float64]:
return self.hessian(model).diagonal()

@property
def weights_matrix(self) -> dia_array:
def weights_matrix(self) -> dia_array[np.float64]:
"""
Diagonal matrix with the square root of regularization weights on cells.
"""
Expand All @@ -247,7 +264,7 @@ def weights_matrix(self) -> dia_array:
return diags_array(np.sqrt(cell_weights))

@property
def _volumes_sqrt_matrix(self) -> dia_array:
def _volumes_sqrt_matrix(self) -> dia_array[np.float64]:
"""
Diagonal matrix with the square root of cell volumes.
"""
Expand All @@ -270,12 +287,12 @@ class Flatness(_MeshBasedRegularization):
active_cells : (n_cells) array or None, optional
Array full of bools that indicate the active cells in the mesh. It must have the
same amount of elements as cells in the mesh.
cell_weights : (n_params) array or dict of (n_params) arrays or None, optional
cell_weights : (n_active) array or dict of (n_params) arrays or None, optional
Array with cell weights.
For multiple cell weights, pass a dictionary where keys are strings and values
are the different weights arrays.
If None, no cell weights are going to be used.
reference_model : (n_params) array or None, optional
reference_model : (n_active) array or None, optional
Array with values for the reference model.

Notes
Expand Down Expand Up @@ -337,6 +354,7 @@ def __init__(
active_cells: npt.NDArray[np.bool] | None = None,
cell_weights: npt.NDArray | dict[str, npt.NDArray] | None = None,
reference_model: Model | None = None,
model_slice: ModelSlice | MultiSlice | None = None,
):
self.mesh = mesh
self.direction = direction
Expand All @@ -345,21 +363,24 @@ def __init__(
if active_cells is not None
else np.ones(self.mesh.n_cells, dtype=bool)
)
# assign model_slice after active_cells so n_active is correct during __init__
self.model_slice = model_slice

# Assign the cell weights through the setter
self.cell_weights = (
cell_weights
if cell_weights is not None
else np.ones(self.n_params, dtype=np.float64)
else np.ones(self.n_active, dtype=np.float64)
)

self.reference_model = (
reference_model
if reference_model is not None
else np.zeros(self.n_params, dtype=np.float64)
else np.zeros(self.n_active, dtype=np.float64)
)
self.set_name(direction)

@support_model_slice
def __call__(self, model: Model) -> float:
"""
Evaluate the regularization on a given model.
Expand All @@ -384,6 +405,7 @@ def __call__(self, model: Model) -> float:
@ model_diff
)

@support_model_slice
def gradient(self, model: Model):
"""
Gradient vector.
Expand All @@ -397,17 +419,19 @@ def gradient(self, model: Model):
weights_matrix = self.weights_matrix
cell_volumes_sqrt = self._volumes_sqrt_matrix
cell_gradient = self._cell_gradient
return (
gradient = (
2
* cell_gradient.T
@ cell_gradient.T
@ cell_volumes_sqrt.T
@ weights_matrix.T
@ weights_matrix
@ cell_volumes_sqrt
@ cell_gradient
@ model_diff
)
return gradient

@support_model_slice
def hessian(self, model: Model): # noqa: ARG002
"""
Hessian matrix.
Expand All @@ -420,7 +444,7 @@ def hessian(self, model: Model): # noqa: ARG002
weights_matrix = self.weights_matrix
cell_gradient = self._cell_gradient
cell_volumes_sqrt = self._volumes_sqrt_matrix
return (
hessian = (
2
* cell_gradient.T
@ cell_volumes_sqrt.T
Expand All @@ -429,6 +453,7 @@ def hessian(self, model: Model): # noqa: ARG002
@ cell_volumes_sqrt
@ cell_gradient
)
return hessian

def hessian_diagonal(self, model: Model) -> npt.NDArray[np.float64]:
"""
Expand Down
Loading
Loading