Source code for cosapp.drivers.driver

"""
Classes driving simulation on CoSApp :py:class:`~cosapp.systems.system.System`.
"""
from __future__ import annotations
import logging
import time
from typing import Optional, TypeVar

from cosapp.patterns.visitor import Visitor
from cosapp.core.module import Module
from cosapp.recorders.recorder import BaseRecorder
from cosapp.utils.options_dictionary import OptionsDictionary
from cosapp.utils.naming import NameChecker, CommonPorts
from cosapp.utils.helpers import check_arg

logger = logging.getLogger(__name__)

AnyDriver = TypeVar("AnyDriver", bound="Driver")
Recorder = TypeVar("Recorder", bound=BaseRecorder)


[docs] class Driver(Module): """Abstract base class for all systems drivers. Parameters ---------- name : str Name of the driver owner : System, optional :py:class:`~cosapp.systems.system.System` to which this driver belongs; default None **kwargs : Any Keyword arguments used to set driver options Attributes ---------- name : str Name of the driver parent : Driver, optional Top driver containing this driver; default None. owner : System :py:class:`~cosapp.systems.system.System` to which this driver belong. children : OrderedDict[str, Driver] Drivers belonging to this one. options : OptionsDictionary | Options for the current driver: | **verbose** : int, {0, 1} | Verbosity level of the driver; default 0 (i.e. minimal information) solution : List[Tuple[str, float]] List of (name, value) for the iteratives when a solution is reached """ __slots__ = ('_owner', '_recorder', 'options', 'start_time', 'status', 'error_code') _name_check = NameChecker( pattern = r"^[A-Za-z][\w\s@-]*[\w]?$", message = "Driver name must start with a letter, and contain only alphanumerics + {'_', '@', ' ', '-'}", excluded = CommonPorts.names(), ) def __init__( self, name: str, owner: Optional["cosapp.systems.System"] = None, **kwargs ) -> None: """Initialize driver Parameters ---------- - name [str]: Driver name - owner [System, optional]: :py:class:`~cosapp.systems.system.System` to which driver belongs; defaults to `None`. - **kwargs: Optional keywords arguments. """ from cosapp.systems import System super().__init__(name) self._owner: Optional[System] = None self._recorder: Optional[BaseRecorder] = None self.owner = owner self.options = OptionsDictionary() # type: OptionsDictionary # "Driver options dictionary" self.start_time = 0.0 # type: float # unit="s", # desc="Absolute time at which the Driver execution started.", self.status = "" # type: str #desc="Status of the driver." # TODO Fred what are the status? Enum, any str? self.error_code = "0" # type: str # desc="Error code during the execution." # TODO Fred what is the code? ESI? # TODO is tol_target really used in all cases? Is it not redundant with options parameters? # self.target_tol = None # desc='Targeted maximal relative tolerance for numerical solution.') # TODO is current_tol updated in all cases to a relevant value? # self.current_tol = np.nan # desc='Maximal current relative tolerance of the numerical solution.') self.options.declare( "verbose", default=0, dtype=int, lower=0, upper=1, desc="Verbosity level of the driver", ) for key in self.options: try: self.options[key] = kwargs.pop(key) except KeyError: continue
[docs] def accept(self, visitor: Visitor) -> None: """Specifies course of action when visited by `visitor`""" visitor.visit_driver(self)
def __repr__(self) -> str: context = "alone" if self.owner is None else f"on System {self.owner.name!r}" return f"{self.name} ({context}) - {self.__class__.__name__}" @property def owner(self): """System: System owning the driver and its children.""" return self._owner @owner.setter def owner(self, system: Optional["cosapp.systems.System"]) -> None: self._set_owner(system) def _set_owner(self, system: Optional["cosapp.systems.System"]) -> bool: """Owner setter as a protected method, to be used by derived classes. This prevents from calling base class `owner.setter`, which can be tricky. Returns ------- changed [bool]: `True` if owner has changed, `False` otherwise. """ from cosapp.systems import System if system is not None: check_arg(system, 'owner', System) changed = system is not self._owner self._owner: Optional[System] = system if self._recorder is not None: self._recorder.watched_object = system for child in self.children.values(): child.owner = system return changed
[docs] def check_owner_attr(self, item: str) -> None: if item not in self.owner: raise AttributeError(f"{item!r} not found in System {self.owner.name!r}")
@property def recorder(self) -> Optional[BaseRecorder]: """BaseRecorder or None : Recorder attached to this `Driver`.""" return self._recorder
[docs] def is_standalone(self) -> bool: """Is this Driver able to solve a system? Returns ------- bool Ability to solve a system or not. """ for driver in self.children.values(): if driver.is_standalone(): return True return False
def _set_children_active_status(self, active_status : bool) -> None: self._active = active_status for child in self.children.values(): child._set_children_active_status(active_status)
[docs] def setup_run(self) -> None: """Set execution order and start the recorder.""" if self.owner is None: raise AttributeError(f"Driver {self.name!r} has no owner system.")
def _precompute(self) -> None: """Set execution order and start the recorder.""" self.start_time = time.time() if self.owner.parent is None and self.parent is None: logger.info(" " + "-" * 60) logger.info(f" # Starting driver {self.name!r} on {self.owner.name!r}") if self._recorder is not None: self._recorder.start() def _postcompute(self) -> None: """Actions performed after the `Module.compute` call.""" if self._recorder is not None: self._recorder.exit() # TODO Fred Better in clean_run ? if self.owner.parent is None and self.parent is None: logger.info( " # Ending driver {!r} on {!r} in {} seconds\n".format( self.name, self.owner.name, round(time.time() - self.start_time, 3) ) )
[docs] def add_child(self, child: AnyDriver, execution_index: Optional[int]=None, desc="") -> AnyDriver: """Add a child `Driver` to the current `Driver`. When adding a child `Driver`, it is possible to specified its position in the execution order. Parameters ---------- - child: Driver `Driver` to add to the current `Driver` - execution_index: int, optional Index of the execution order list at which the `Module` should be inserted; default latest. - desc [str, optional]: Sub-driver description in the context of its parent driver. Returns ------- `child` Notes ----- The added child will have its owner set to that of current driver. """ check_arg(child, 'child', Driver) child.owner = self.owner return super().add_child(child, execution_index, desc)
[docs] def add_driver(self, child: AnyDriver, execution_index: Optional[int]=None, desc="") -> AnyDriver: """Alias for :py:meth:`~cosapp.drivers.driver.Driver.add_child`.""" return self.add_child(child, execution_index, desc)
[docs] def add_recorder(self, recorder: Recorder) -> Recorder: check_arg(recorder, 'recorder', BaseRecorder) self._recorder = recorder self._recorder.watched_object = self.owner self._recorder._owner = self return self.recorder