Source code for pycif.plugins.modes.response_functions.base_function.base_function
import datetime
import os
import shutil
from logging import debug
from typing import Any, Union
import numpy as np
import pandas as pd
from .....utils.iterators import iter_attributes
from .....utils.path import link
from .....utils.check.errclass import CifFileNotFoundError, CifTypeError, CifValueError
# Aliases for type hinting
Mode = Any
DatetimeLike = Union[datetime.datetime, pd.Timestamp, np.datetime64]
[docs]
def to_datetime(dt: DatetimeLike) -> datetime.datetime:
"""A function to convert various datetime-like objects to standard Python datetime objects.
Parameters:
dt (DatetimeLike): The input datetime-like object to be converted.
Returns:
datetime.datetime: The standard Python datetime object after conversion.
Raises:
ValueError: If the input datetime-like object cannot be converted to a standard datetime object.
TypeError: If the input type is unexpected and cannot be converted to a datetime object.
"""
if isinstance(dt, datetime.datetime):
return dt
elif isinstance(dt, pd.Timestamp):
# Converting from pandas Timestamp to python built-in datetime
if dt is pd.NaT:
raise CifValueError("cannot convert 'NaT' to datetime")
return dt.to_pydatetime()
elif isinstance(dt, np.datetime64):
# Converting from numpy datetime64 to python built-in datetime
if np.isnan(dt):
raise CifValueError("cannot convert 'NaT' to datetime")
return dt.astype('datetime64[s]').tolist()
else:
raise CifTypeError("unexpected type, can not convert to datetime")
[docs]
class BaseFunction:
def __init__(
self,
mode: Mode,
index: int,
component: str,
parameter: str,
date_start: DatetimeLike,
date_end: DatetimeLike,
) -> None:
"""A class representing a base function
Args:
mode (Mode): the mode plugin
index (int): controlvect index
component (str): component name
parameter (str): parameter name
date_start (datetime): start of the base function time window
date_end (datetime): end of the base function time window
"""
if index > 999999:
raise CifValueError("too many response functions")
if date_start == date_end:
raise CifValueError(f"response function {index} has identical start "
f"and end dates: {date_start}")
# References to some of the mode plugin's attributes
self.basedir = mode.basedir
self.datei = mode.datei
self.controlvect = mode.controlvect
self.datavect = mode.datavect
self.inicond = mode.inicond
# References to some of the mode plugin's input arguments
self.run_mode = mode.run_mode
self.inicond_component = mode.inicond_component
self.reload_results = mode.reload_results
self.ignore_tracers = mode.ignore_tracers
self.index = index
self.component = component
self.parameter = parameter
self.date_start = to_datetime(date_start)
self.date_end = to_datetime(date_end)
self.name = f"base_function_{index:06d}"
self.controlvect_filename = "controlvect.pickle"
def __repr__(self) -> str:
s = f"<BaseFunction index={self.index}, component={self.component}, " \
f"parameter={self.parameter}>"
return s
def __str__(self) -> str:
s = f"base function {self.index:06d}: " \
f"{self.component}/{self.parameter}, " \
f"{self.date_start.isoformat()} -> {self.date_end.isoformat()}"
return s
@property
def rundir(self) -> str:
return os.path.join(self.basedir, self.name)
@property
def outdir(self) -> str:
return os.path.join(self.rundir, f"obsoperator/{self.run_mode}_0000/obsvect")
@property
def obsdir(self) -> str:
return os.path.join(self.basedir, f"base_function_obsvect_{self.index:06d}")
@property
def controlvect_path(self) -> str:
return os.path.join(self.rundir, self.controlvect_filename)
@property
def yaml_config_path(self) -> str:
return os.path.join(self.rundir, f"config_{self.name}.yaml")
[docs]
def is_ignored(self) -> bool:
"""True if the base fonction (component, parameter) couple is in the
'ignore_tracer' input argument
"""
return (self.component, self.parameter) in self.ignore_tracers
[docs]
def has_run_succesfully(self) -> bool:
"""True is the base function has run successfully (output directory is
present)"""
return self.reload_results and os.path.isdir(self.outdir)
[docs]
def update_inicond_date(self) -> None:
"""Updates the initial conditions tracers date if those are in the
control vector and the response function start date is different from
the simulation window initial date
"""
if self.datei == self.date_start:
return
for tracer_name, tracer in iter_attributes(self.inicond.parameters):
if not tracer.iscontrol:
continue
if tracer.ndates != 1:
raise CifValueError(
f"initial conditions component '{self.inicond_component}' "
"has multiple dates, response functions excepted only one"
)
debug(
f"updating initial conditions tracer {self.inicond_component}/"
f"{tracer_name} date from {tracer.dates[0].isoformat()} to "
f"{self.date_start.isoformat()}"
)
tracer.dates = np.array([self.date_start])
[docs]
def dump_controlvect(self) -> None:
"""Updates the controlvector and dumps it
"""
self.update_inicond_date()
if self.run_mode == "fwd":
# Setting control vector x
self.controlvect.x = np.zeros(self.controlvect.dim)
self.controlvect.x[self.index] = self.controlvect.xb[self.index]
elif self.run_mode == "tl":
# Setting control vector dx
self.controlvect.dx = np.zeros(self.controlvect.dim)
self.controlvect.dx[self.index] = 1.0
os.makedirs(self.rundir, exist_ok=True)
self.controlvect.dump(self.controlvect_path)
[docs]
def fetch_obsvect(self) -> None:
"""Fetch the response function outputs (observation vector) and
move it to its 'obsvect' directory
"""
if not os.path.isdir(self.obsdir):
if not os.path.isdir(self.outdir):
raise CifFileNotFoundError(
f"Response function {self.index:06d} did not produce the "
f"requiered outputs '{self.outdir}'"
)
link(self.outdir, self.obsdir)
[docs]
def flush(self) -> None:
"""Delete some temporary files to free some disk space
"""
# Removing the control vector dump file
if os.path.isfile(self.controlvect_path):
debug(f"deleting control vector dump '{self.controlvect_path}'")
os.remove(self.controlvect_path)
# Removing the 'chain' directory
chain_dir = os.path.join(
self.rundir, f"obsoperator/{self.run_mode}_0000/chain")
# Some checks to avoid deleting something else
if os.path.isdir(chain_dir) and os.path.isabs(chain_dir):
debug(f"deleting directory '{chain_dir}'")
shutil.rmtree(chain_dir)