Source code for ethz_snow.operatingConditions

"""Implement OperatingConditions class.

This module contains the OperatingConditions class used to
store information regarding the operating conditions
in freezing processes.
"""
import numpy as np

from typing import Optional, Iterable, Union


[docs]class OperatingConditions: """A class to handle a single Stochastic Nucleation of Water simulation. More information regarding the equations and their derivation can be found in XXX, Deck et al. (2021). Parameters: cnt (float): Controlled nucleation time. controlledNucleation (bool): Controlled nucleation on/off. cooling (dict): A dictionary describing the cooling profile. holding (dict): A dictionary describing the holding step. t_tot (float): The total process time. """ def __init__( self, t_tot: float = 2e4, cooling: dict = {"rate": 0.5 / 60, "start": 20, "end": -50}, holding: Optional[Union[Iterable[dict], dict]] = None, cnTemp: Union[float, int] = None, ): """Construct an OperatingConditions object. Args: t_tot (float, optional): The total process time. Defaults to 2e4. cooling (dict, optional): A dictionary describing the cooling profile. Defaults to {"rate": 0.5 / 60, "start": 20, "end": -50}. holding (Optional[Union[Iterable[dict], dict]], optional): A dictionary or list of dictionaries describing the holding step(s). Defaults to None (no holding). cnTemp (Union[float, int], optional): At what temperature controlled nucleation is triggered. Nucleation triggers when temp<=cnTemp. If that occurs during a holding phase nucleation will trigger _at the end_ of the phase. """ self.t_tot = t_tot if not all([key in cooling.keys() for key in ["rate", "start", "end"]]): raise ValueError("Cooling dictionary does not contain all required keys.") self.cooling = cooling self.holding = holding self.cnTemp = cnTemp @property def holding(self) -> Iterable[dict]: """Get holding property.""" return self._holding @holding.setter def holding(self, value: Union[dict, list, tuple]): """Set holding property.""" if isinstance(value, dict): value = [value] elif isinstance(value, (list, tuple)): # bring holding steps into right order (descending) value = sorted(value, key=lambda hdict: hdict["temp"], reverse=True) elif value is None: pass else: raise TypeError("holding must be a dict or Iterable of dict.") if value is not None: for val in value: if not all([key in val.keys() for key in ["duration", "temp"]]): raise ValueError( "Holding dictionary does not contain all required keys." ) self._holding = value @property def cnt(self) -> float: """Return the time when controlled nucleation should trigger. Raises: NotImplementedError: If holding is not defined don't know how to calculate cnt. Returns: float: The time of controlled nucleation (inf if no controlled nucleation applied). """ # time it takes to holding if self.holding is None: raise NotImplementedError( "Holding profile is not defined." + "Cannot calculate controlled nucleation time." ) if self.cnTemp is not None: T_vec = self.tempProfile(1) t_vec = np.arange(0, len(T_vec)) I_endHold = np.argmax(T_vec[::-1] >= self.cnTemp) t_endHold = t_vec[::-1][I_endHold] else: t_endHold = np.inf return t_endHold
[docs] def tempProfile(self, dt: float) -> np.ndarray: """Return temperature profile. Compute temperature profile with or without holding step. Args: dt (float): The time step size. Returns: np.ndarray: The temperature profile. """ # total number of steps n = int(np.ceil(self.t_tot / dt)) + 1 T_start = self.cooling["start"] cr = self.cooling["rate"] if self.holding is not None: hdicts = self.holding + [ {"temp": self.cooling["end"], "duration": self.t_tot} ] else: hdicts = [{"temp": self.cooling["end"], "duration": self.t_tot}] T_vec = np.array([]) for hdict in hdicts: T_hold = hdict["temp"] t_hold = (T_start - T_hold) / cr duration_hold = hdict["duration"] # ramp from start to hold temp T_vec_toHold = self._simpleCool( Tstart=T_start, Tend=T_hold, coolingRate=cr, dt=dt, ) # append holding period T_vec_holding = [T_hold] * int(np.ceil((duration_hold - t_hold % dt) / dt)) T_start = T_hold T_vec = np.concatenate([T_vec, T_vec_toHold, T_vec_holding]) T_vec = T_vec[:n] return T_vec
def _simpleCool( self, Tstart: float, Tend: float, coolingRate: float, dt: float, t_tot: Optional[float] = None, ) -> np.ndarray: """Return cooling profile for linear step. Args: Tstart (float): Start temperature. Tend (float): End temperature. coolingRate (float): Cooling rate. dt (float): Time step. t_tot (Optional[float], optional): Total process time. Defaults to None. Returns: np.ndarray: The temperature profile. """ t_end = (Tstart - Tend) / coolingRate t_vec = np.arange(0, t_end, dt) T_profile = Tstart - t_vec * coolingRate if t_tot is not None: assert ( t_tot >= t_end ), "Final temp can't be reached with cooling rate, holding/total time." add_n = int(np.ceil((t_tot - t_vec[-1]) / dt)) T_profile = np.append(T_profile, [Tend] * add_n) return T_profile def __repr__(self) -> str: """Return string representation of the OperatingConditions class. Returns: str: The OperatingConditions class string representation giving some basic info. """ holdPluralBool = (self.holding is not None) and (len(self.holding) > 1) holdPlural = f"Hold{'s' if (holdPluralBool) else ''}: " holdStr = holdPlural + " AND ".join( [f"{hdict['duration']} @ {hdict['temp']}" for hdict in self.holding] ) return ( f"OperatingConditions([t_tot: {self.t_tot}, " + f"Cooling: {self.cooling['start']} to {self.cooling['end']} " + f"with rate {self.cooling['rate']:4.2f}, " + holdStr + ", " + f"Controlled Nucleation: {'ON' if self.cnTemp else 'OFF'}" )