Source code for pycif.plugins.models.lmdz_ico.io.inputs.obs

from __future__ import annotations

import datetime
from os import PathLike
from pathlib import Path
from typing import Literal

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

from ......utils.check.errclass import CifValueError
from ......utils.hdf5 import _hdf5_lock


[docs] def make_obs( self, dict_datastores: dict[tuple[str, str], pd.DataFrame], ddi: datetime.datetime, input_type: str, runsubdir: str | PathLike, mode: Literal["fwd", "adj"], ) -> None: """Write observation input file for one sub-period""" # If empty datastore, do nothing if all(isinstance(datastore, dict) for datastore in dict_datastores.values()): return if all(datastore.size == 0 for datastore in dict_datastores.values()): return if self.reset_obs[ddi]: self.reset_obs[ddi] = False self.chunk_indexes[ddi] = { comp: {spec: None for spec in self.chemistry.active_species} for comp in self.output_components } def get_tracer_names(parameter: pd.Series) -> pd.Series: return parameter.str.replace("__sample#", "_").str.lower() # Include only part of the datastore with datastore in active species active_species = [spec.lower() for spec in self.chemistry.active_species] # Initialize datastore to dump datei, _ = self.subsimu_intervals[ddi] ds = [] # Loop over tracers to concatenate datastores to dump for datastore in dict_datastores.values(): tracer_name = get_tracer_names(datastore["metadata"]["parameter"]) mask = tracer_name.isin(active_species) maindata = datastore["maindata"].loc[mask, :] metadata = datastore["metadata"].loc[mask, :] if np.any(metadata["dtstep"].values != 1): raise CifValueError("'dstep' values different from 1 is not supported") # Time coordinate # WARNING: This part is critical, modify with caution time = (metadata["tstep"] * self.dt.total_seconds()).astype("int32") time_units = f"seconds since {datei:%Y-%m-%d %H:00:00}" # Tracer index tracer_name = tracer_name.values tracer_index = np.zeros(tracer_name.shape, dtype=np.int32) for i, spec in enumerate(active_species): tracer_index[tracer_name == spec] = i # Level index # Assumes that stations with no level are in first level # TODO: make it general level = np.asarray(metadata["level"].values) level[~pd.notnull(level)] = 0 level = level.astype("int32") ds_tracer = xr.Dataset( # fmt: off { "time": (["index"], time, { "standard_name": "time", "long_name": "time", "units": time_units, "calendar": "proleptic_gregorian", }), "obs": (["index"], maindata["obs"].values.astype("float32"), { "standard_name": "observation", "long_name": "observation (unused, for reference only)", "units": "ppm", }), "itrac": (["index"], tracer_index + 1 , { "standard_name": "tracer_index", "long_name": "tracer index", }), "ilev" : (["index"], level + 1, { "standard_name": "model_level_index", "long_name": "model level index", }), } # fmt: on ) if mode == "adj": # fmt: off ds_tracer["obs_ad"] = (["index"], maindata["adj_out"].values, { "standard_name": "adjoint_observation", "long_name": "adjoint observation", "units": "ppm", }) # fmt: on if self.grid == "regular": # NOTE: In CIF latitude index is "i" and longitude index is "j" (row-major) # In DISPERSION latitude index is "j" and longitude index is "i" (column-major) # fmt: off ds_tracer["ilon"] = (["index"], metadata["j"].values.astype("int32") + 1, { "standard_name": "model_longitude_index", "long_name": "model longitude index (starts from 1)", }) ds_tracer["ilat"] = (["index"], metadata["i"].values.astype("int32") + 1, { "standard_name": "model_latitude_index", "long_name": "model latitude index (starts from 1)", }) # fmt: on elif self.grid == "dynamico": # fmt: off ds_tracer["icell"] = (["index"], metadata["i"].values.astype("int32") + 1, { "standard_name": "model_cell_index", "long_name": "model cell index (starts from 1)", }) # fmt: on else: raise CifValueError(f"Unknown grid type: '{self.grid}'") ds.append(ds_tracer) # Stop here if ther is no observation to include if ds == []: return # Concatenate trids ds = xr.concat(ds, dim="index") # Concat observations to obs.nc if it already exists obs_file = Path(runsubdir, "obs.nc") if self.iniobs[ddi] and obs_file.exists(): with _hdf5_lock: with xr.open_dataset(obs_file, decode_times=False) as ds_orig: nobs_orig = ds_orig.sizes["index"] ds = xr.concat([ds_orig, ds], dim="index") else: nobs_orig = 0 # Get spatial and temporal location of observations h_vars = ["ilon", "ilat"] if self.grid == "regular" else ["icell"] cols = ["time", "itrac", "ilev"] + h_vars df = ds[cols].to_dataframe() # Get unique obs location to avoid double extraction of parameters groupby_duplicates = df.groupby(cols) group_ids = groupby_duplicates.ngroup() unique_index = group_ids.drop_duplicates() # Aggregate adj_out if mode == "adj": obs_ad = ds["obs_ad"].to_dataframe().groupby(group_ids).sum() obs_ad = obs_ad.values[unique_index] # Truncate to current obs batch group_ids = group_ids.iloc[nobs_orig:] # Get index of observations to extract (unique locations) indices = pd.Series(range(len(unique_index)), index=unique_index) indices = indices.loc[group_ids].values # Save index per tracer itrac = df.drop_duplicates().iloc[indices]["itrac"] - 1 for i, spec in enumerate(self.chemistry.active_species): if (itrac == i).any(): self.chunk_indexes[ddi][input_type][spec] = indices[itrac == i] # Update NetCDF file ds = ds.sel(index=unique_index.index) if mode == "adj": ds["obs_ad"][:] = obs_ad.flatten() with _hdf5_lock: ds.to_netcdf(obs_file) # obs.nc already exists and should be updated if another tracer is added self.iniobs[ddi] = True