"""
======================================================
Cyclic Stationarity (:mod:`CADETProcess.stationarity`)
======================================================
.. currentmodule:: CADETProcess.stationarity
Module to evaluate cyclic stationarity of succeeding cycles.
.. autosummary::
:toctree: generated/
RelativeArea
NRMSE
StationarityEvaluator
""" # noqa
import copy
from typing import Any, Optional
import numpy as np
from addict import Dict
from CADETProcess import log
from CADETProcess.comparison import NRMSE as _NRMSE
from CADETProcess.comparison import Comparator
from CADETProcess.comparison import RelativeArea as _RelativeArea
from CADETProcess.dataStructure import Structure, UnsignedFloat
from CADETProcess.processModel import Inlet
from CADETProcess.simulationResults import SimulationResults
from CADETProcess.solution import SolutionIO
__all__ = ["MassBalance", "NRMSE", "RelativeArea", "StationarityEvaluator"]
class CriterionBase(Structure):
threshold = UnsignedFloat(default=1e-3)
def __str__(self) -> str:
return self.__class__.__name__
class MassBalance(CriterionBase):
"""Class to evaluate mass balance as stationarity critereon."""
pass
[docs]
class NRMSE(CriterionBase):
"""Class to evaluate NRMSE as stationarity critereon."""
pass
[docs]
class RelativeArea(CriterionBase):
"""Class to evaluate difference in relative area as stationarity critereon."""
pass
[docs]
class StationarityEvaluator(Comparator):
"""Class for checking two succeding chromatograms for stationarity."""
valid_criteria = ["MassBalance", "NRMSE", "RelativeArea"]
def __init__(
self,
criteria: Optional[list[CriterionBase]] = None,
log_level: str = "WARNING",
*args: Any,
**kwargs: Any,
) -> None:
"""
Initialize the stationarity evaluator.
Parameters
----------
criteria : List[CriterionBase], optional
List of criteria for stationarity evaluation, by default None
log_level : str, optional
The logging level, by default 'WARNING'
args : list
Additional arguments.
kwargs : dict
Additional keyword arguments.
"""
super().__init__(*args, **kwargs)
self.logger = log.get_logger("StationarityEvaluator", level=log_level)
self._criteria = []
@property
def criteria(self) -> list[CriterionBase]:
"""list: List of criteria."""
return self._criteria
[docs]
def add_criterion(self, criterion: CriterionBase) -> None:
"""
Add a criterion to the list of criteria.
Parameters
----------
criterion : CriterionBase
Criterion to add to the list of criteria.
"""
if not isinstance(criterion, CriterionBase):
raise TypeError("Expected CriterionBase.")
self._criteria.append(criterion)
[docs]
def assert_stationarity(self, simulation_results: SimulationResults) -> bool:
"""
Check stationarity of two succeeding cycles.
Parameters
----------
simulation_results : SimulationResults
Results of current cycle.
Returns
-------
bool
True if stationarity is reached. False otherwise.
Raises
------
TypeError
If simulation_results is not a SimulationResults object.
"""
self._metrics = []
criteria = Dict()
if not isinstance(simulation_results, SimulationResults):
raise TypeError("Expcected SimulationResults")
process = simulation_results.process
flow_sheet = process.flow_sheet
stationarity = True
# System Mass Balance
for c in self.criteria:
if not isinstance(c, MassBalance):
continue
m_feed = process.m_feed
results_outlets = [
simulation_results.solution_cycles[unit.name].outlet[-1]
for unit in flow_sheet.outlets
]
m_out = np.sum([
outlet_solution.create_fraction().mass
for outlet_solution in results_outlets
], axis=0)
with np.errstate(divide="ignore", invalid="ignore"):
diff = abs(m_feed - m_out) / m_feed
diff = np.where(np.isfinite(diff), diff, 0.0)
if not np.all(diff <= c.threshold):
s = False
stationarity = s
else:
s = True
criteria[str(c)]["threshold"] = c.threshold
criteria[str(c)]["stationarity"] = s
# Per unit comparison
_criterion_to_metric = {NRMSE: _NRMSE, RelativeArea: _RelativeArea}
def _assert_unit_io(
solution_previous: SolutionIO,
solution_this: SolutionIO,
unit: str,
side: str,
criteria: dict,
c: object,
) -> bool:
"""Assert stationarity for a given inlet/outlet of a unit."""
ref = copy.deepcopy(solution_previous)
metric = _criterion_to_metric[type(c)](ref)
diff = metric.evaluate(solution_this)
criteria[str(c)][unit][side]["metric"] = diff
s = np.all(diff <= c.threshold)
criteria[str(c)][unit][side]["stationarity"] = s
return s
for unit, solution in simulation_results.solution_cycles.items():
if isinstance(flow_sheet[unit], Inlet):
continue
for c in self.criteria:
if isinstance(c, MassBalance):
continue
if not _assert_unit_io(
solution.inlet[-2],
solution.inlet[-1],
unit,
"inlet",
criteria,
c,
):
stationarity = False
if not _assert_unit_io(
solution.outlet[-2],
solution.outlet[-1],
unit,
"outlet",
criteria,
c,
):
stationarity = False
self.logger.debug(f"Stationrity criteria: {criteria}")
return stationarity