Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
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
4 changes: 2 additions & 2 deletions src/pyissm/inversion/__init__.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
"""
Inversion module for pyISSM.
Inversion subpackage for pyISSM.

This module contains tools for performing inversions, including sensitivity analyses and convergence diagnostics.
This subpackage contains tools for performing inversions, including sensitivity analyses and convergence diagnostics.
"""

from . import metrics, plot, sensitivity
47 changes: 47 additions & 0 deletions src/pyissm/inversion/metrics.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@ def cost_function_values(md):
"""
Extract final inversion cost-function values.

Extracts the final cost-function values for each cost function and the total cost from the model results.

Parameters
----------
md : :class: `pyissm.Model`
Expand All @@ -23,6 +25,12 @@ def cost_function_values(md):
-------
:class:`dict`
Dictionary containing final cost-function values for each cost function and total cost.

Example
-------
: code-block:: python

>>> final_costs = pyissm.inversion.metrics.cost_function_values(md)
"""

# Get cost function history from model results
Expand All @@ -48,6 +56,9 @@ def cost_function_ratios(md):
"""
Compute ratios between all inversion cost functions.

These ratios can provide insight into the relative contributions of different cost functions
to the total cost and help identify which cost functions are dominating the inversion results.

Parameters
----------
md : :class:`pyissm.Model`
Expand All @@ -57,6 +68,12 @@ def cost_function_ratios(md):
-------
:class:`dict`
Dictionary of pairwise cost-function ratios.

Example
-------
: code-block:: python

>>> cost_ratios = pyissm.inversion.metrics.cost_function_ratios(md)
"""

# Get individual cost values
Expand Down Expand Up @@ -97,6 +114,9 @@ def extract_convergence_history(md):
"""
Extract convergence history of cost functions.

Extracts the history of cost-function values across iterations from the modelresults and
organizes it into a pandas DataFrame for easier analysis and visualization of convergence behavior.

Parameters
----------
md : :class:`pyissm.Model`
Expand All @@ -105,6 +125,12 @@ def extract_convergence_history(md):
-------
:class:`pandas.DataFrame`
DataFrame containing cost function history.

Example
-------
: code-block:: python

>>> convergence_history = pyissm.inversion.metrics.extract_convergence_history(md)
"""

# Get cost function history from model results
Expand Down Expand Up @@ -134,6 +160,10 @@ def velocity_residual_area_metrics(md):
"""
Compute positive/negative residual area metrics.

Calculates the total area of elements with positive and negative velocity residuals,
as well as their fractions relative to the total area. This provides insight into the
spatial distribution of model-data misfit in terms of velocity residuals.

Parameters
----------
md : :class:`pyissm.Model`
Expand All @@ -143,6 +173,12 @@ def velocity_residual_area_metrics(md):
-------
:class:`dict`
Dictionary containing the computed metrics.

Example
-------
: code-block:: python

>>> area_metrics = pyissm.inversion.metrics.velocity_residual_area_metrics(md)
"""

# Compute velocity residuals at vertices
Expand Down Expand Up @@ -205,18 +241,29 @@ def field_smoothness_metrics(md,
field = None):
"""
Compute field smoothness metrics.

Calculates the area-weighted mean and RMSE of the gradient magnitude (slope) of a specified field.
This provides insight into the spatial variability and smoothness of the control parameter field,
which can be important for understanding inversion results and regularization effects.

Parameters
----------
md : :class:`pyissm.Model`
ISSM model object containing mesh and (optionally) solution data.

field : :class:`numpy.ndarray`
Field values, defined on vertices. If None, will attempt to extract from model results based on control parameter.

Returns
-------
:class:`dict`
Dictionary containing smoothness metrics.

Example
-------
: code-block:: python

>>> smoothness_metrics = pyissm.inversion.metrics.field_smoothness_metrics(md)
"""

# Extract field from model if not provided
Expand Down
49 changes: 44 additions & 5 deletions src/pyissm/inversion/plot.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,9 @@
"""
Inversion plotting tools for pyISSM.

This module contains various functions for plotting inversion diagnostics for ISSM models.
"""

import numpy as np
import matplotlib.pyplot as plt
import textwrap
Expand Down Expand Up @@ -48,6 +54,14 @@ def plot_convergence_history(convergence_history,
-----
This function does not validate metric names. A missing metric column will
raise a pandas ``KeyError`` during plotting.

Example
-------
: code-block:: python

>>> convergence_history = pyissm.inversion.metrics.extract_convergence_history(md)
>>> fig, ax = plot_convergence_history(convergence_history)

"""

## Is an ax passed?
Expand Down Expand Up @@ -150,6 +164,14 @@ def plot_sensitivity_heatmap(diagnostics,
-----
The heatmap is generated via :meth:`pandas.DataFrame.pivot_table` using
``y`` as the index, ``x`` as the columns, and ``value`` as cell values.

Example
-------
: code-block:: python

>>> manifest = pd.read_csv('./sensitivity_runs/manifest.csv')
>>> diagnostics = pyissm.inversion.sensitivity.compute_sensitivity_diagnostics(manifest, output_dir = './sensitivity_runs')
>>> fig, ax = pyissm.inversion.plot.plot_sensitivity_heatmap(diagnostics, x = 'cf101', y = 'cf103', value = 'vel_rmse')
"""

## Is an ax passed?
Expand Down Expand Up @@ -210,16 +232,25 @@ def plot_run_summary(md,
----------
md : :class:`pyissm.Model`
ISSM model object containing inversion results.

output_pdf : :class:`str`
Path to output PDF file.

diagnostics : :class:`dict`, optional
Diagnostics configuration. Default is None.
Model diagnostics. Default is None. Computed internally if not provided.

plot_kwargs : :class:`dict`, optional
Plot keyword arguments. Default is None.

Returns
-------
None
None. Saves the generated report to the specified PDF file.

Example
-------
: code-block:: python

>>> plot.plot_run_summary(md, output_pdf = f'./sensitivity_runs/run_001_1_1/run_001_1_summary.pdf')
"""

# Compute velocity residuals for diagnostics and plotting
Expand Down Expand Up @@ -486,9 +517,9 @@ def plot_lcurve(diagnostics,
Column containing regularization coefficient values.

If None, automatically inferred from regularization term:
cost_501 -> cf501
cost_502 -> cf502
cost_503 -> cf503
- cost_501 -> cf501
- cost_502 -> cf502
- cost_503 -> cf503

misfit_cols : list of :class:`str`, optional
Cost-function columns contributing to model-data misfit.
Expand Down Expand Up @@ -533,6 +564,14 @@ def plot_lcurve(diagnostics,

ax : :class:`matplotlib.axes.Axes`
The axes containing the plot. If an axes was passed in, this is the same object; if no axes was passed, this is the newly created axes.

Example
-------
: code-block:: python

>>> manifest = pd.read_csv('./sensitivity_runs/manifest.csv')
>>> diagnostics = pyissm.inversion.sensitivity.compute_sensitivity_diagnostics(manifest, output_dir = './sensitivity_runs')
>>> fig, ax = plot_lcurve(diagnostics)
"""

## Is an ax passed?
Expand Down
84 changes: 71 additions & 13 deletions src/pyissm/inversion/sensitivity.py
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,15 @@ def build_parameter_grid(coefficients, initial_run_id = 1):
- run_id
- cfXXX columns for each cost function
- run_name

Example
-------
: code-block:: python

>>> coefficients = {101: [1, 10, 100, 1000],
... 103: [1, 10, 100, 1000]}
>>> param_grid = pyissm.inversion.sensitivity.build_parameter_grid(coefficients)
>>> param_grid = pyissm.inversion.sensitivity.build_parameter_grid(coefficients, initial_run_id = 101)
"""

# Sort cost functions for consistent ordering
Expand Down Expand Up @@ -107,9 +116,8 @@ def assign_cost_functions(md,

global_mask : :class:`numpy.ndarray` of :class:`bool`, optional
Global boolean mask applied to all cost functions.

True -> coefficient active
False -> coefficient set to zero
- True -> coefficient active
- False -> coefficient set to zero

coeff_masks : :class:`dict`, optional
Dictionary mapping cost-function IDs to boolean masks.
Expand All @@ -124,6 +132,20 @@ def assign_cost_functions(md,
-------
md : :class:`pyissm.Model`
Updated ISSM model.

Example
-------
: code-block:: python

>>> coefficients = {101: 1000,
... 103: 25,
... 502: 1e-19}
>>> global_mask = md.inversion.vel_obs > 0
>>> md = pyissm.inversion.sensitivity.assign_cost_functions(md, coefficients, global_mask = global_mask)

>>> coeff_masks = {101: md.inversion.vel_obs > 0,
... 103: md.mask.ice_levelset > 0}
>>> md = pyissm.inversion.sensitivity.assign_cost_functions(md, coefficients, coeff_masks = coeff_masks)
"""

# Sort cost functions for consistent ordering
Expand Down Expand Up @@ -209,8 +231,8 @@ def parameter_sensitivity(md,

**NOTE:**
This is intentionally separate from: md.cluster.executionpath:
- md.cluster.executionpath controls where ISSM stages runtime files for execution.
- output_dir controls where pyISSM stores organized experiment outputs.
- `md.cluster.executionpath` controls where ISSM stages runtime files for execution.
- `output_dir` controls where pyISSM stores organized experiment outputs.

run : :class:`bool`, default = True
Submit inversion runs.
Expand All @@ -220,9 +242,8 @@ def parameter_sensitivity(md,

global_mask : :class:`numpy.ndarray` of :class:`bool`, optional
Global boolean mask applied to all cost functions. Passed to :func:`assign_cost_functions`.

True -> coefficient active
False -> coefficient set to zero
- True -> coefficient active
- False -> coefficient set to zero

coeff_masks : :class:`dict`, optional
Dictionary mapping cost-function IDs to boolean masks. Passed to :func:`assign_cost_functions`.
Expand All @@ -236,6 +257,15 @@ def parameter_sensitivity(md,
-------
:class:`pandas.DataFrame`
Experiment manifest.

Example
-------
: code-block:: python

>>> coefficients = {101: [1, 10, 100, 1000],
... 103: [1, 10, 100, 1000]}
>>> parameter_grid = pyissm.inversion.sensitivity.build_parameter_grid(coefficients)
>>> manifest = pyissm.inversion.sensitivity.parameter_sensitivity(md, parameter_grid, output_dir = './sensitivity_runs', run = True, load_only = False)
"""

# Prevent ambiguous execution mode
Expand Down Expand Up @@ -365,7 +395,7 @@ def parameter_sensitivity(md,

def load_parameter_sensitivity_run(manifest_row):
"""
Load a parameter sensitivity run.
Load a parameter sensitivity run from a manifest row.

Parameters
----------
Expand All @@ -375,6 +405,13 @@ def load_parameter_sensitivity_run(manifest_row):
-------
md : :class:`pyissm.Model`
Loaded ISSM model for the specified run.

Example
-------
: code-block:: python

>>> manifest = pd.read_csv('./sensitivity_runs/manifest.csv')
>>> md = pyissm.inversion.sensitivity.load_parameter_sensitivity_run(manifest.iloc[0])
"""

# Extract run directory and name from manifest row
Expand All @@ -397,17 +434,31 @@ def compute_sensitivity_diagnostics(manifest,
output_dir = None,
plot_run_summaries = True):
"""
Compute diagnostics for parameter sensitivity runs.
Compute diagnostics for parameter sensitivity runs from a manifest.

Parameters
----------
manifest : :class:`pandas.DataFrame`
Output from :func:`pyissm.inversion.sensitivity.parameter_sensitivity`.

output_dir : :class:`str` or :class:`pathlib.Path`, optional
Directory used to store computed diagnostics. If None, diagnostics are not saved to disk.

plot_run_summaries : :class:`bool`, default = True
Whether to generate summary PDF reports for each run, including diagnostic plots and convergence history. If True, summary PDFs are saved in each run's directory.

Returns
-------
:class:`pandas.DataFrame`
Diagnostics table.

Example
-------
: code-block:: python

>>> manifest = pd.read_csv('./sensitivity_runs/manifest.csv')
>>> diagnostics = pyissm.inversion.sensitivity.compute_sensitivity_diagnostics(manifest, output_dir = './sensitivity_runs/diagnostics')

"""

# Copy manifest so coefficient metadata is preserved
Expand Down Expand Up @@ -496,9 +547,8 @@ def normalize_diagnostics(diagnostics,

higher_is_better : :class:`dict`, optional
Dictionary mapping column name -> bool.

True = higher values are better
False = lower values are better
- True = higher values are better
- False = lower values are better

Example
-------
Expand All @@ -515,6 +565,14 @@ def normalize_diagnostics(diagnostics,
-------
:class:`pandas.DataFrame`
Copy of dataframe with normalized columns added.

Example
-------
: code-block:: python

>>> manifest = pd.read_csv('./sensitivity_runs/manifest.csv')
>>> diagnostics = pyissm.inversion.sensitivity.compute_sensitivity_diagnostics(manifest, output_dir = './sensitivity_runs/diagnostics')
>>> diagnostics_norm = pyissm.inversion.sensitivity.normalize_diagnostics(diagnostics, columns = ['vel_rmse', 'thickness_rmse'])
"""

# Create a copy of the diagnostics DataFrame to avoid modifying the original
Expand Down
Loading