Source code for pycif.plugins.models.lmdz_ico.ini_periods

from __future__ import annotations

from datetime import datetime
from logging import debug

import numpy as np
import pandas as pd

from ....utils.dates import date_range


[docs] def is_month_start(datetime: pd.Timestamp) -> bool: """Return ``True`` if *datetime* falls exactly at the start of a calendar month (midnight).""" return ( datetime.is_month_start and datetime.hour == 0 and datetime.minute == 0 and datetime.second == 0 )
[docs] def ini_periods(self, **kwargs) -> None: """Compute temporal discretisation for the LMDZ-ico model. Splits the full window into sub-periods capped at one calendar month each, then builds per-period date arrays for time steps, inputs, fluxes, chemistry fields, and meteo mass fluxes. Sets on *self*: * ``subsimu_dates`` — period boundary datetimes. * ``subsimu_intervals`` — dict mapping period start to (start, end) tuple. * ``tstep_dates`` — per-period full-month time-step arrays. * ``input_dates`` — per-period daily initial/end-concentration dates. * ``flux_input_dates`` — per-period flux input dates. * ``chem_input_dates`` — per-period chemistry field dates. * ``meteo_input_dates`` — per-period mass-flux dates. * ``iniobs``, ``reset_obs`` — per-period obs bookkeeping flags. * ``chunk_indexes`` — per-period per-component per-species index cache. * ``runsimu`` — per-period flag controlling whether to run or use cached output. * ``chain`` — per-period flag for adjoint restart chaining. Args: self: LMDZ-ico plugin instance with ``datei``, ``datef``, ``period_freq``, ``dt``, ``flux_freq``, ``chem_freq``, ``mass_fluxes_freq``, and ``chemistry.active_species`` set. **kwargs: unused. """ # Keep old behaviour for month sub-simulations (maybe not needed) if self.period_freq in ("MS", "1MS"): ref_datei = pd.Timestamp(year=self.datei.year, month=self.datei.month, day=1) else: ref_datei = self.datei dates = pd.date_range(ref_datei, self.datef, freq=self.period_freq) subsimu_dates = [] # Simulation sub-periods, cut to a maximum length of 1 month for di, df in zip(dates[:-1], dates[1:]): subsimu_dates.append(di) # Both datetimes are in the same month if di.year == df.year and di.month == df.month: continue next_month = (di + pd.offsets.MonthBegin()).month # df is the first day of the month after di if df.month == next_month and is_month_start(df): continue # There is at least one month between di and df in_between = pd.date_range( di, df, freq="MS", normalize=True, inclusive="neither", ) subsimu_dates.extend(in_between.to_list()) # List of sub-simulation windows subsimu_dates.append(dates[-1]) self.subsimu_dates = pd.to_datetime(subsimu_dates).to_pydatetime() # type: ignore # pylint: disable=no-member if self.subsimu_dates[0] < self.datei: self.subsimu_dates[0] = self.datei if self.subsimu_dates[-1] < self.datef: self.subsimu_dates = np.append(self.subsimu_dates, self.datef) self.subsimu_intervals = { ddi: (ddi, ddf) for ddi, ddf in zip(self.subsimu_dates[:-1], self.subsimu_dates[1:]) } debug_msg = "LMDZ sub-simulation windows:\n" for dk, (di, df) in self.subsimu_intervals.items(): debug_msg += f"- {dk}: {di} - {df}\n" debug(debug_msg[:-1]) def get_input_dates( freq: str | pd.Timedelta, full_month: bool = False, ) -> dict[datetime, np.ndarray]: """Build per-period date arrays at *freq* resolution. Args: freq: date frequency (pandas-compatible string or Timedelta). full_month: if ``True``, span the full calendar month of each period start rather than just the period window. Returns: dict mapping period-start date to its date array. """ dates = {} for ddi, ddf in self.subsimu_intervals.values(): if full_month: ddi_month = pd.Timestamp(year=ddi.year, month=ddi.month, day=1) ddf_month = ddi_month + pd.DateOffset(months=1) dates[ddi] = date_range(ddi_month, ddf_month, period=freq) # type: ignore else: dates[ddi] = date_range(ddi, ddf, period=freq) # type: ignore return dates # List of time steps # WARNING: This part is critical for observations, modify with caution self.tstep_dates = get_input_dates(self.dt, full_month=True) # TODO: make the date inputs flexible (daily so far) self.input_dates = get_input_dates("1D", full_month=False) # Surface fluxes input dates self.flux_input_dates = get_input_dates(self.flux_freq, full_month=False) # Chemical fields input dates (full months) self.chem_input_dates = get_input_dates(self.chem_freq, full_month=True) # Mass fluxes input dates (full months at 3-hourly resolution) # Need to set input dates for meteo plugin here self.meteo_input_dates = get_input_dates(self.mass_fluxes_freq, full_month=True) # Initializes dictionary to keep in memory whether observations were # already dumped for a given period self.iniobs = {ddi: False for ddi in self.subsimu_dates} self.reset_obs = {ddi: True for ddi in self.subsimu_dates} # Initializes dictionary to keep in memory observations index information # Only "concs" component is used there self.chunk_indexes = { ddi: { comp: {spec: None for spec in self.chemistry.active_species} for comp in self.output_components } for ddi in self.subsimu_dates } # Run model or approximation with fake_end self.runsimu = {ddi: True for ddi in self.subsimu_dates} # Keep in memory whether a given period has a successor period # The info is used by the adjoint to fetch or not the restart file self.chain = {ddi: True for ddi in self.subsimu_dates[:-1]} self.chain[self.subsimu_dates[-2]] = False