Source code for pydox.calibration.facade

from copy import deepcopy
from typing import Self, Optional
from collections import OrderedDict

import numpy as np
import pydox as do
from pydox._config.utils import list_methods
from pydox.calibration.methods.in_air import plots as in_air_plots
from pydox.commodities import ConfigsDict, PlotParams
from pydox.calibration.spec import Workflow
from pydox.calibration.method import Method
from pydox.calibration.methods.in_air.spec import MethodInAir
from pydox.calibration.methods.climatology import MethodClimatology


[docs] def Calibration( method: Optional[str] = None, *args, **kwargs ) -> MethodInAir | MethodClimatology: """Create a single methodology calibration Parameters ---------- method: Optional[str] Name of the method implementation to create. By default, creates a method defined with the ``calibration_methods.default`` setting. Returns ------- :class:`pydox.calibration.MethodInAir`, :class:`pydox.calibration.MethodClimatology` Notes ----- With this design, we cannot implement ``Calibration.from_config()`` """ if method is None: method = do.get_params("calibration_methods.default") if method not in list_methods(): raise ValueError("lorem ipsum") if method == "in_air": return MethodInAir(*args, **kwargs) elif method == "climatology": return MethodClimatology(*args, **kwargs) else: raise NotImplementedError
[docs] class CalibrationSet(Workflow): """Collection of methodology implementations Notes ----- - Support more than one calibration method - Handle sequential or parallel execution of configuration sets Examples -------- .. code-block:: python from pydox import Calibration, CalibrationSet s = CalibrationSet() s.commit(Calibration('in_air').set_params('calibration_methods.in_air', carryover=[False, True], dataset=['ncep', 'era5'])) s.commit(Calibration('climatology').set_params('calibration_methods.climatology.max_pressure', [25., 50.])) s.n_configs s.configs s.fit(a_float) s.fitted s.coefs s.set_best_fit s.best_fit s.plot """
[docs] def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) self._methods: OrderedDict[int, Method] = ( OrderedDict() ) # internal placeholder for all methods to be registered (which are 'Method' children instances)
def __repr__(self): summary: list[str] = super().__repr__().split("\n") summary[0] = f"<pydox.Workflow.CalibrationSet> {self.name}" return "\n".join(summary)
[docs] def commit(self, o: MethodInAir | MethodClimatology) -> Self: """This method looks like the real added difference compared to a 'Workflow' for a single method ('Method')""" ii = len(self._methods) self._methods.update({ii: deepcopy(o)}) return self
def uid(self, icfg: int = None) -> str: """UID for this CalibrationSet or a specific configuration""" if icfg is None: return self._uid() else: this_icfg: int = 0 for im, m in self._methods.items(): for idc, dc in m.configs.items(): if this_icfg == icfg: return m.uid(idc) this_icfg += 1 raise ValueError(f"Invalid configuration id {icfg}") def _flatten_configs(self) -> ConfigsDict: # This method implementation *imposes* how to 'iterate' over methods and their ordered placeholders (eg: coefs, fit_data, ...). # To keep any CalibrationSet ordered placeholder consistent, we need to iterate: 1st on method, then on configurations. configs: ConfigsDict = OrderedDict() icfg: int = 0 for im, m in self._methods.items(): for idc, dc in m.configs.items(): configs[icfg] = dc icfg += 1 return configs def is_cumulative(self) -> bool: """Check if this instance can perform a cumulative fit An instance is eligible if: - There is at least 2 methods - There is only one configuration for each method """ if self.n_configs < 2: raise ValueError( f"Fit with cumulative gain requires at least 2 configurations" ) for im, this_method in self._methods.items(): if this_method.n_configs != 1: raise ValueError( "Fit with cumulative gain requires a single configuration for each methods" ) # Return False if methods are not different ? # if len(np.unique([m.rcgroup for m in self._methods.values()])) == 1: # return False return True def load_input_data( self, argofloat_obj, *args, **kwargs, ) -> Self: icfg: int = 0 for im, this_method in self._methods.items(): this_method.load_input_data(argofloat_obj, *args, **kwargs) for idc, dc in this_method.configs.items(): self._input_data[icfg] = this_method.input_data[idc] icfg += 1
[docs] def fit( self, argofloat_obj, cumulative: Optional[bool] = False, **kwargs, ) -> Self: self._fitted_float["WMO"] = argofloat_obj.WMO if not cumulative: icfg: int = 0 for im, this_method in self._methods.items(): this_method.fit(argofloat_obj, **kwargs) # Gather detailed results: for idc, dc in this_method.configs.items(): self._input_data[icfg] = this_method.input_data[idc] self._coefs[icfg] = this_method.coefs[idc] self._fit_data[icfg] = this_method._fit_data[idc] self._fitted_float["CYCLE_NUMBER"][icfg] = ( this_method._fitted_float["CYCLE_NUMBER"][idc] ) icfg += 1 elif ( self.is_cumulative() ): # (cumulative was set to True, so we check eligibility immediately) icfg: int = 0 for im, this_method in self._methods.items(): this_method.fit(argofloat_obj, **kwargs) idc = 0 # We can safely use the 1st value because all methods have a single configuration (see self.is_cumulative()). # Gather more detailed results in dedicated placeholders of the instance: self._input_data[icfg] = this_method.input_data[idc] self._coefs[icfg] = this_method.coefs[idc] self._fit_data[icfg] = this_method.fit_data[idc] self._fitted_float["CYCLE_NUMBER"][icfg] = this_method._fitted_float[ "CYCLE_NUMBER" ][idc] # Update next method configuration initial conditions with this estimate: if im + 1 < len(self._methods): coefs = this_method.coefs[idc] self._methods[im + 1].set_params( "calibration_parameters.initial_guess.gain", coefs.gain.value, ) # So it is the fit method responsibility to use the initial value accordingly icfg += 1 # Update fitted status: self._fitted = all( [m.fitted for m in self._methods.values()] ) # is the set fitted when all methods are fitted, or at least one ? ############### Plot # Create a parameter obj for plots ppar = PlotParams( watermark=self.name, dpi=self.get_params("plots.dpi"), level=self.get_params("plots.level"), uid=self.uid(), ) if np.all(np.unique([m.rcgroup for m in self._methods.values()]) == "in_air"): # If all methods are in-air, we can safely generate these plots: if "hue" in self.get_params("plots.configs_layout"): in_air_plots.plot_fit_results_hue( self.input_data, self.coefs, ppar=ppar ) if "subplot" in self.get_params("plots.configs_layout"): in_air_plots.plot_fit_results_subplot( self.input_data, self.coefs, ppar=ppar ) if "figure" in self.get_params("plots.configs_layout"): in_air_plots.plot_fit_results_figure( self.input_data, self.coefs, ppar=ppar ) return self