Source code for pycif.plugins.obsoperators.standard.transforms.utils.dump_debug

import os
import pathlib

import numpy as np
import pandas as pd
import xarray as xr

from ......utils import path
from ......utils.datastores.dump import dump_datastore
from ......utils.check.errclass import CifTypeError


def _dataset_meta(ds):
    """Return a human-readable metadata string for an :class:`xr.Dataset`.

    Reports, for the ``spec`` variable and — when present — the ``incr``
    variable:

    * dimension names and sizes,
    * min and max values (NaN-safe),
    * whether any NaN values are present.

    Args:
        ds (xr.Dataset): dataset to summarise.

    Returns:
        str: multi-line metadata string, one block per variable.
    """
    lines = []
    for varname in ("spec", "incr"):
        if varname not in ds:
            continue
        da = ds[varname]
        lines.append(f"[{varname}]")
        dims = dict(zip(da.dims, da.shape))
        lines.append(f"  dims   : {dims}")
        try:
            vals = da.values.astype(float)
            lines.append(f"  min    : {float(np.nanmin(vals)):.6g}")
            lines.append(f"  max    : {float(np.nanmax(vals)):.6g}")
            lines.append(f"  has_nan: {bool(np.isnan(vals).any())}")
        except Exception as exc:
            lines.append(f"  stats  : (could not compute: {exc})")
    return "\n".join(lines)


def _dataframe_meta(df):
    """Return a human-readable metadata string for a :class:`pd.DataFrame`.

    Reports the number of rows and, for each of the ``maindata``, ``spec``,
    and ``incr`` columns that exist in the dataframe:

    * min and max values,
    * whether any NaN values are present.

    Args:
        df (pd.DataFrame): dataframe to summarise.

    Returns:
        str: multi-line metadata string, one block per column.
    """
    lines = [f"length : {len(df)}"]
    for colname in ("spec", "incr"):
        if colname not in df.columns:
            continue
        col = df[("maindata", colname)]
        lines.append(f"[{colname}]")
        try:
            lines.append(f"  min    : {float(col.min()):.6g}")
            lines.append(f"  max    : {float(col.max()):.6g}")
        except (TypeError, ValueError):
            lines.append(f"  min    : {col.min()}")
            lines.append(f"  max    : {col.max()}")
        lines.append(f"  has_nan: {bool(col.isna().any())}")
    return "\n".join(lines)


[docs] def dump_debug( transform, transf_mapper, tmp_datastore, runsubdir, ddi, entry="outputs", transform_onlyinit=False, dump_metadata_only=False): """Dump inputs or outputs of a transform step for post-run inspection. For each tracer ID (``trid``) in ``tmp_datastore[entry]``, writes debug files under:: <runsubdir>/../transform_debug/<transform>/<ddi>/<component>/<parameter>/ Two modes are supported: **Full dump** (``dump_metadata_only=False``) Writes the complete datastore to disk as NetCDF files (:class:`xr.Dataset`) or pyCIF datastore files (:class:`pd.DataFrame`). One file is produced per tracer ID and date. This mode is accurate but slow and disk-intensive. **Metadata-only dump** (``dump_metadata_only=True``) Writes lightweight plain-text files instead of full data files. Dramatically reduces wall-time overhead and disk usage while still capturing enough information to trace data flow and detect anomalies. * For :class:`xr.Dataset` values — records the dimension names and sizes, min/max values, and NaN presence for the ``spec`` variable (and ``incr`` when it exists in the dataset). * For :class:`pd.DataFrame` values — records the row count and, for each of the ``maindata``, ``spec``, and ``incr`` columns that are present, the min/max values and NaN presence. Args: transform (str): name of the transform being debugged. transf_mapper (dict): mapper entry for the transform (not directly used here; kept for API consistency). tmp_datastore (dict): the datastore for the current transform step, keyed by ``"inputs"`` and ``"outputs"``. runsubdir (str): the per-period run sub-directory. Debug files are written to ``<runsubdir>/../transform_debug/…``. ddi (datetime.datetime): the current simulation date (used both for directory naming and for per-date filename formatting). entry (str, optional): which side of the datastore to dump — ``"inputs"`` or ``"outputs"``. Defaults to ``"outputs"``. transform_onlyinit (bool, optional): when ``True``, the transform ran in dry-run / init-only mode. Currently unused inside this function but kept for API consistency with the call sites in :func:`~.do_transforms.do_transforms`. Defaults to ``False``. dump_metadata_only (bool, optional): when ``True``, write lightweight metadata text files instead of full NetCDF/datastore files. Defaults to ``False``. Raises: TypeError: if a datastore entry is neither an :class:`xr.Dataset` nor a :class:`dict` of :class:`xr.Dataset` / :class:`pd.DataFrame`. """ for trid in tmp_datastore[entry]: debug_dir = ( f"{runsubdir}/../transform_debug/{transform}" f"/{ddi.strftime('%Y-%m-%d_%H:%M')}/{trid[0]}/{trid[1]}" ) path.init_dir(debug_dir) # ------------------------------------------------------------------ # # Branch 1: the datastore entry is itself an xr.Dataset # ------------------------------------------------------------------ # if isinstance(tmp_datastore[entry][trid], xr.Dataset): if dump_metadata_only: meta_file = f"{debug_dir}/dataarray_meta_{entry}.txt" with open(meta_file, "w") as fh: fh.write(_dataset_meta(tmp_datastore[entry][trid])) else: debug_file = f"{debug_dir}/dataarray_debug_{entry}.nc" if os.path.isfile(debug_file): pathlib.Path(debug_file).unlink() xr.Dataset(tmp_datastore[entry][trid]).to_netcdf( debug_file, mode="w") continue # ------------------------------------------------------------------ # # Branch 2: the datastore entry is a dict of per-date values # ------------------------------------------------------------------ # if not isinstance(tmp_datastore[entry][trid], dict): raise CifTypeError( f"dump_debug got an unexpected type for tmp_datastore: " f"{type(tmp_datastore[entry][trid])}" ) for d in tmp_datastore[entry][trid]: value = tmp_datastore[entry][trid][d] if isinstance(value, pd.DataFrame): if dump_metadata_only: meta_file = d.strftime( f"{debug_dir}/monitor_meta_%Y%m%d%H%M_{entry}.txt") with open(meta_file, "w") as fh: fh.write(_dataframe_meta(value)) else: debug_file = d.strftime( f"{debug_dir}/monitor_debug_%Y%m%d%H%M_{entry}.nc") if os.path.isfile(debug_file): pathlib.Path(debug_file).unlink() dump_datastore( value, debug_file, col2dump=value.columns, dump_default=False, mode='w' ) elif isinstance(value, xr.Dataset): if dump_metadata_only: meta_file = d.strftime( f"{debug_dir}/dataarray_meta_%Y%m%d%H%M_{entry}.txt") with open(meta_file, "w") as fh: fh.write(_dataset_meta(value)) else: debug_file = d.strftime( f"{debug_dir}/dataarray_debug_%Y%m%d%H%M_{entry}.nc") if os.path.isfile(debug_file): pathlib.Path(debug_file).unlink() xr.Dataset(value).to_netcdf(debug_file, mode="w")