Source code for PyHydroGeophysX.analysis.sensitivity

"""Sensitivity and resolution analysis utilities."""

from typing import Any, Dict, Optional, Tuple

import numpy as np


def _as_matrix(weights, size: Optional[int] = None) -> np.ndarray:
    arr = np.asarray(weights)
    if arr.ndim == 1:
        return np.diag(arr)
    if arr.ndim == 2:
        return arr
    if size is None:
        raise ValueError("Cannot infer matrix size from scalar weights without `size`.")
    return np.eye(size) * float(arr)


[docs]def compute_resolution_matrix(J, Wd, Wm, lam): """ Compute model resolution matrix: R = (J^T Wd Wd J + lam Wm^T Wm)^(-1) J^T Wd Wd J """ J = np.asarray(J, dtype=float) Wd_mat = _as_matrix(Wd, J.shape[0]) Wm_mat = _as_matrix(Wm, J.shape[1]) jt_wdwd_j = J.T @ Wd_mat @ Wd_mat @ J reg_term = float(lam) * (Wm_mat.T @ Wm_mat) lhs = jt_wdwd_j + reg_term lhs_inv = np.linalg.pinv(lhs) return lhs_inv @ jt_wdwd_j
[docs]def compute_depth_of_investigation( inv_class, data: Any, mesh: Any, scale_low: float = 0.8, scale_high: float = 1.2, ) -> Tuple[np.ndarray, Dict[str, np.ndarray]]: """ Estimate DOI using two inversions with different reference/initial models. This follows the Oldenburg-Li style idea of quantifying model sensitivity to reference-model choice. """ if hasattr(mesh, "cellCount"): n_cells = int(mesh.cellCount()) else: raise ValueError("mesh must expose cellCount() for DOI estimation.") base = np.ones(n_cells, dtype=float) result_low = inv_class.run(initial_model=base * scale_low) result_high = inv_class.run(initial_model=base * scale_high) model_low = np.asarray(result_low.final_model, dtype=float).ravel() model_high = np.asarray(result_high.final_model, dtype=float).ravel() if model_low.size != model_high.size: raise ValueError("DOI models are incompatible in size.") doi = np.abs(model_high - model_low) / (np.abs(model_high) + np.abs(model_low) + 1e-12) doi = np.clip(doi, 0.0, 1.0) return doi, {"model_low": model_low, "model_high": model_high}
[docs]def compute_cumulative_sensitivity(J): """Compute cumulative sensitivity as absolute column sums of the Jacobian.""" J = np.asarray(J, dtype=float) if J.ndim != 2: raise ValueError("J must be a 2D matrix.") return np.sum(np.abs(J), axis=0)
[docs]def plot_sensitivity_map(sensitivity, mesh, ax=None): """Plot a sensitivity map on a mesh (or as a simple line plot fallback).""" import matplotlib.pyplot as plt sens = np.asarray(sensitivity, dtype=float).ravel() if ax is None: fig, ax = plt.subplots(figsize=(8, 4)) else: fig = ax.figure # Try mesh-based plotting first when PyGIMLi mesh plotting is available. mesh_plot_ok = False try: import pygimli as pg if mesh is not None and hasattr(mesh, "cellCount") and mesh.cellCount() == sens.size: pg.show(mesh, data=sens, ax=ax, cMap="viridis", label="Sensitivity") ax.set_title("Sensitivity Map") mesh_plot_ok = True except Exception: mesh_plot_ok = False if not mesh_plot_ok: ax.plot(np.arange(sens.size), sens, "k-") ax.set_xlabel("Cell Index") ax.set_ylabel("Sensitivity") ax.set_title("Cumulative Sensitivity") ax.grid(True, alpha=0.3) return fig, ax