Skip to content

ModelOutput

Container returned by ParcelModel.run(mode="full"). Holds the full state trajectory and post-solve diagnostics, with format-conversion methods for pandas, polars, xarray, NetCDF, CSV, and Parquet.

ModelOutput dataclass

ModelOutput(
    time: ndarray,
    state: ndarray,
    aerosols: list,
    summary: dict,
    V: float | AbstractUpdraft,
    T0: float,
    S0: float,
    P0: float,
    accom: float,
)

Output from a single ParcelModel run.

Attributes:

Name Type Description
time np.ndarray, shape ``(n_time,)``

Simulation time (s).

state np.ndarray, shape ``(n_time, 7 + nr)``

Full state trajectory. The first seven columns are the bulk parcel variables (see pyrcel.constants.STATE_VARS); the remaining columns are per-bin wet radii (m) ordered as in aerosols.

aerosols list[AerosolSpecies]

Aerosol modes, in the same order as the radius columns in state.

summary dict

Post-solve diagnostics: S_max, t_smax, T_smax, z_smax, per_species (list of per-mode dicts), total_act_frac.

V float | AbstractUpdraft

Updraft used for the run (stored for dataset metadata).

T0, S0, P0, accom float

Initial conditions (stored for dataset metadata).

Nd property

Nd: float

Total activated droplet number concentration (m⁻³) at the last trajectory step.

Evaluated by comparing wet radii against per-bin critical radii at summary["nd_t_eval"] (the final output time, which is terminate_depth m above S_max when terminate=True). This is a hard-threshold diagnostic; for the differentiable analog see issue #67.

nd_frac property

nd_frac: float

Total activated fraction at the last trajectory step (see Nd).

to_pandas

to_pandas() -> tuple[pd.DataFrame, dict[str, pd.DataFrame]]

Return (parcel_df, {species: aerosol_df}) as pandas DataFrames.

parcel_df has columns ["z", "P", "T", "wv", "wc", "wi", "S"] indexed by simulation time. Each aerosol_df has columns ["r000", "r001", …] (wet radii in metres) with the same time index.

Source code in pyrcel/model_output.py
def to_pandas(self) -> tuple[pd.DataFrame, dict[str, pd.DataFrame]]:
    """Return ``(parcel_df, {species: aerosol_df})`` as pandas DataFrames.

    ``parcel_df`` has columns ``["z", "P", "T", "wv", "wc", "wi", "S"]``
    indexed by simulation time.  Each ``aerosol_df`` has columns
    ``["r000", "r001", …]`` (wet radii in metres) with the same time index.
    """
    import pandas as pd

    idx = pd.Index(self.time, name="time")
    parcel = pd.DataFrame(
        {var: self.state[:, i] for i, var in enumerate(c.STATE_VARS)},
        index=idx,
    )
    aerosol: dict[str, pd.DataFrame] = {}
    offset = c.N_STATE_VARS
    for aer in self.aerosols:
        cols = {f"r{j:03d}": self.state[:, offset + j] for j in range(aer.nr)}
        aerosol[aer.species] = pd.DataFrame(cols, index=idx)
        offset += aer.nr
    return parcel, aerosol

to_polars

to_polars() -> tuple[pl.DataFrame, dict[str, pl.DataFrame]]

Return (parcel_df, {species: aerosol_df}) as polars DataFrames.

Columns and layout mirror to_pandas; the time index becomes an explicit "time" column (polars does not have a named index).

Source code in pyrcel/model_output.py
def to_polars(self) -> tuple[pl.DataFrame, dict[str, pl.DataFrame]]:
    """Return ``(parcel_df, {species: aerosol_df})`` as polars DataFrames.

    Columns and layout mirror [to_pandas][pyrcel.model_output.ModelOutput.to_pandas]; the time
    index becomes an
    explicit ``"time"`` column (polars does not have a named index).
    """
    import polars as pl

    parcel = pl.DataFrame(
        {"time": self.time} | {var: self.state[:, i] for i, var in enumerate(c.STATE_VARS)}
    )
    aerosol: dict[str, pl.DataFrame] = {}
    offset = c.N_STATE_VARS
    for aer in self.aerosols:
        cols = {"time": self.time} | {
            f"r{j:03d}": self.state[:, offset + j] for j in range(aer.nr)
        }
        aerosol[aer.species] = pl.DataFrame(cols)
        offset += aer.nr
    return parcel, aerosol

to_xarray

to_xarray() -> xr.Dataset

Return a CF-flavoured xarray.Dataset.

Mirrors the variable layout of the legacy NetCDF writer: a time coordinate, per-species <species>_bins coordinates with dry radii / kappa / number concentration, wet-radius histories <species>_size, parcel thermodynamic profiles, and the post-solve summary as scalar variables / global attributes.

Source code in pyrcel/model_output.py
def to_xarray(self) -> xr.Dataset:
    """Return a CF-flavoured `xarray.Dataset`.

    Mirrors the variable layout of the legacy NetCDF writer: a ``time``
    coordinate, per-species ``<species>_bins`` coordinates with dry radii /
    kappa / number concentration, wet-radius histories ``<species>_size``,
    parcel thermodynamic profiles, and the post-solve summary as scalar
    variables / global attributes.
    """
    import xarray as xr

    from . import __version__ as ver

    ds = xr.Dataset(attrs={"Conventions": "CF-1.0", "source": f"pyrcel v{ver} (JAX/diffrax)"})
    ds.coords["time"] = (
        "time",
        self.time,
        {"units": "seconds", "long_name": "simulation time"},
    )

    offset = c.N_STATE_VARS
    for aer in self.aerosols:
        nr = aer.nr
        sp = aer.species
        bins = f"{sp}_bins"
        ds.coords[bins] = (
            bins,
            np.arange(1, nr + 1, dtype=np.int32),
            {"long_name": f"{sp} size bin number"},
        )
        ds[f"{sp}_rdry"] = (
            (bins,),
            np.asarray(aer.r_drys) * 1e6,
            {"units": "micron", "long_name": f"{sp} bin dry radii"},
        )
        ds[f"{sp}_kappas"] = (
            (bins,),
            np.full(nr, aer.kappa),
            {"long_name": f"{sp} bin kappa-kohler hygroscopicity"},
        )
        ds[f"{sp}_Nis"] = (
            (bins,),
            np.asarray(aer.Nis) * 1e-6,
            {"units": "cm-3", "long_name": f"{sp} bin number concentration"},
        )
        ds[f"{sp}_size"] = (
            ("time", bins),
            self.state[:, offset : offset + nr] * 1e6,
            {"units": "micron", "long_name": f"{sp} bin wet radii"},
        )
        offset += nr

    rho = rho_air(self.T, self.P, self.S + 1.0)
    profiles = {
        "S": (self.S * 100.0, {"units": "%", "long_name": "Supersaturation"}),
        "T": (self.T, {"units": "K", "long_name": "Temperature"}),
        "P": (self.P, {"units": "Pa", "long_name": "Pressure"}),
        "wv": (self.wv, {"units": "kg/kg", "long_name": "Water vapor mixing ratio"}),
        "wc": (self.wc, {"units": "kg/kg", "long_name": "Liquid water mixing ratio"}),
        "wi": (self.wi, {"units": "kg/kg", "long_name": "Ice water mixing ratio"}),
        "height": (self.heights, {"units": "meters", "long_name": "Parcel height above start"}),
        "rho": (np.asarray(rho), {"units": "kg/m3", "long_name": "Air density"}),
        "wtot": (
            self.wv + self.wc,
            {"units": "kg/kg", "long_name": "Total water mixing ratio"},
        ),
    }
    for name, (data, attrs) in profiles.items():
        ds[name] = (("time",), np.asarray(data), attrs)

    s = self.summary
    ds["S_max"] = (
        (),
        s["S_max"] * 100.0,
        {"units": "%", "long_name": "Maximum supersaturation"},
    )
    ds["t_smax"] = (
        (),
        s["t_smax"],
        {"units": "seconds", "long_name": "Time of maximum supersaturation"},
    )
    for p in s["per_species"]:
        ds[f"{p['species']}_eq_act_frac"] = (
            (),
            p["eq_act_frac"],
            {"long_name": f"{p['species']} equilibrium activated fraction"},
        )
        ds[f"{p['species']}_Nd"] = (
            (),
            p["Nd"],
            {"units": "m-3", "long_name": f"{p['species']} activated droplet number"},
        )
    ds["Nd"] = (
        (),
        s["total_Nd"],
        {"units": "m-3", "long_name": "Total activated droplet number concentration"},
    )
    ds["nd_t_eval"] = (
        (),
        s["nd_t_eval"],
        {"units": "s", "long_name": "Time at which Nd snapshot was evaluated"},
    )

    if isinstance(self.V, ConstantV):
        v_attr = float(self.V.V)
    elif isinstance(self.V, AbstractUpdraft):
        v_attr = "V(t)"
    else:
        v_attr = float(self.V)
    ds.attrs.update(
        {
            "V": v_attr,
            "T0": self.T0,
            "S0": self.S0,
            "P0": self.P0,
            "accom": self.accom,
            "total_act_frac": s["total_act_frac"],
        }
    )
    return ds

to_netcdf

to_netcdf(path: str | Path) -> str

Write to a NetCDF4 file and return the path.

Source code in pyrcel/model_output.py
def to_netcdf(self, path: str | Path) -> str:
    """Write to a NetCDF4 file and return the path."""
    path = str(path)
    self.to_xarray().to_netcdf(path)
    return path

to_csv

to_csv(path: str | Path) -> str

Write the flat parcel trajectory (all state columns + radius bins) to CSV.

Columns: time, then all state variables, then per-bin wet radii prefixed by species (e.g. sulfate_r000).

Source code in pyrcel/model_output.py
def to_csv(self, path: str | Path) -> str:
    """Write the flat parcel trajectory (all state columns + radius bins) to CSV.

    Columns: ``time``, then all state variables, then per-bin wet radii
    prefixed by species (e.g. ``sulfate_r000``).
    """
    parcel, aerosol = self.to_pandas()
    # Flatten aerosol DFs with species-prefixed column names
    import pandas as pd

    parts = [parcel.reset_index()]
    for species, df in aerosol.items():
        parts.append(
            df.reset_index(drop=True).rename(
                columns={col: f"{species}_{col}" for col in df.columns}
            )
        )
    flat = pd.concat(parts, axis=1)
    flat.to_csv(path, index=False)
    return str(path)

to_parquet

to_parquet(path: str | Path) -> str

Write the flat parcel trajectory (all state columns + radius bins) to Parquet.

Source code in pyrcel/model_output.py
def to_parquet(self, path: str | Path) -> str:
    """Write the flat parcel trajectory (all state columns + radius bins) to Parquet."""
    parcel_pl, aerosol_pl = self.to_polars()
    import polars as pl

    parts = [parcel_pl]
    for species, df in aerosol_pl.items():
        parts.append(
            df.drop("time").rename(
                {col: f"{species}_{col}" for col in df.columns if col != "time"}
            )
        )
    flat = pl.concat(parts, how="horizontal")
    flat.write_parquet(path)
    return str(path)