Source code for pyrt.controller

"""The :code:`controller` module holds miscellaneous classes responsible for
creating variables that control how DISORT runs.
"""
from warnings import warn


[docs]class ComputationalParameters: """A data structure for holding the size of computational parameters. ComputationalParameters holds the number of model layers, streams, moments, and angles. It also performs basic checks that these values are plausible. Objects of this class are meant to be be used as inputs to other classes. """ def __init__(self, n_layers: int, n_moments: int, n_streams: int, n_azimuth: int, n_polar: int, n_user_levels: int) -> None: """ Parameters ---------- n_layers The number of layers to use in the model. n_moments The number of polynomial moments to use in the model. This number should be greater than or equal to :code:`n_streams`. n_streams The number of streams (i.e. the number of computational polar angles) to use in the model. This number should be even and at least 2. n_azimuth The number of azimuthal angles where DISORT should return radiant quantities. n_polar The number of user-specified polar angles where DISORT should return radiant quantities. n_user_levels The number of user levels to use in the model. Raises ------ TypeError Raised if any of the inputs cannot be converted to an int. ValueError Raised if any of the inputs are not positive finite. Warnings -------- UserWarning Raised if n_streams is not even or if n_streams is greater than n_moments. """ self.__n_layers = self.__make_n_layers(n_layers) self.__n_moments = self.__make_n_moments(n_moments) self.__n_streams = self.__make_n_streams(n_streams) self.__n_azimuth = self.__make_n_azimuth(n_azimuth) self.__n_polar = self.__make_n_polar(n_polar) self.__n_user_levels = self.__make_n_user_levels(n_user_levels) self.__warn_if_n_streams_is_not_even() self.__warn_if_n_streams_is_greater_than_n_moments() def __make_n_layers(self, n_layers: int) -> int: n_layers = self.__make_parameter(n_layers, 'n_layers') self.__raise_value_error_if_parameter_is_not_positive( n_layers, 'n_layers') return n_layers def __make_n_moments(self, n_moments: int) -> int: n_moments = self.__make_parameter(n_moments, 'n_moments') self.__raise_value_error_if_parameter_is_not_positive( n_moments, 'n_moments') return n_moments def __make_n_streams(self, n_streams: int) -> int: n_streams = self.__make_parameter(n_streams, 'n_streams') self.__raise_value_error_if_parameter_is_not_positive( n_streams, 'n_streams') return n_streams def __make_n_azimuth(self, n_azimuth: int) -> int: n_azimuth = self.__make_parameter(n_azimuth, 'n_azimuth') self.__raise_value_error_if_parameter_is_not_positive( n_azimuth, 'n_azimuth') return n_azimuth def __make_n_polar(self, n_polar: int) -> int: n_polar = self.__make_parameter(n_polar, 'n_polar') self.__raise_value_error_if_parameter_is_not_positive( n_polar, 'n_polar') return n_polar def __make_n_user_levels(self, n_user_levels: int) -> int: n_user_levels = self.__make_parameter(n_user_levels, 'n_user_levels') self.__raise_value_error_if_parameter_is_not_positive( n_user_levels, 'n_user_levels') return n_user_levels @staticmethod def __make_parameter(param: int, name: str) -> int: try: return int(param) except TypeError as te: raise TypeError(f'Cannot convert {name} to an int.') from te except ValueError as ve: raise ValueError(f'Cannot convert {name} to an int.') from ve except OverflowError as oe: raise ValueError(f'{name} must be finite.') from oe @staticmethod def __raise_value_error_if_parameter_is_not_positive( param: int, name: str) -> None: if param < 1: raise ValueError(f'{name} must be positive.') def __warn_if_n_streams_is_not_even(self) -> None: if self.__n_streams % 2 != 0: warn('n_streams should be even.') def __warn_if_n_streams_is_greater_than_n_moments(self) -> None: if self.__n_streams > self.__n_moments: warn('n_streams should not be greater than n_moments.') @property def n_layers(self) -> int: """Get the input number of layers. Notes ----- In DISORT, this variable is named :code:`MAXCLY` (though in the :code:`disort` package, this variable is optional). In problems without scattering, this variable is not used. """ return self.__n_layers @property def n_moments(self) -> int: """Get the input number of moments. Notes ----- In DISORT, this variable is named :code:`MAXMOM` (though in the :code:`disort` package, this variable is optional). """ return self.__n_moments @property def n_streams(self) -> int: """Get the input number of streams. Notes ----- In DISORT, this variable is named :code:`MAXCMU` (though in the :code:`disort` package, this variable is optional). In general, the more streams used the more accurate DISORT's computations will be. """ return self.__n_streams @property def n_azimuth(self) -> int: """Get the input number of azimuthal angles. Notes ----- In DISORT, this variable is named :code:`MAXPHI` (though in the :code:`disort` package, this variable is optional). """ return self.__n_azimuth @property def n_polar(self) -> int: """Get the input number of user_specified polar angles. Notes ----- In DISORT, this variable is named :code:`MAXUMU` (though in the :code:`disort` package, this variable is optional). Only used by DISORT if :py:attr:`~output.OutputBehavior.user_angles` is set to ``True``. """ return self.__n_polar @property def n_user_levels(self) -> int: """Get the input number of user levels. Notes ----- In DISORT, this variable is named :code:`MAXULV` (though in the :code:`disort` package, this variable is optional). See Also -------- :class:`~output.UserLevel` """ return self.__n_user_levels def __str__(self) -> str: return f'ComputationalParameters: \n' \ f' n_layers = {self.__n_layers} \n' \ f' n_moments = {self.__n_moments} \n' \ f' n_streams = {self.__n_streams} \n' \ f' n_azimuth = {self.__n_azimuth} \n' \ f' n_polar = {self.__n_polar} \n' \ f' n_user_levels = {self.__n_user_levels}'
# TODO: fix user_angles docstring. It's a mess
[docs]class ModelBehavior: """A data structure for holding the DISORT control variables. ModelBehavior holds the control flags that dictate DISORT's behavior. It also performs basic checks that the input control options are plausible. """ def __init__(self, accuracy: float = 0.0, delta_m_plus: bool = True, do_pseudo_sphere: bool = False, header: str = '', print_variables: list[bool] = None, radius: float = 6371.0) -> None: """ Parameters ---------- accuracy The convergence criterion for azimuthal (Fourier cosine) series. Will stop when the following occurs twice: largest term being added is less than 'accuracy' times total series sum (twice because there are cases where terms are anomalously small but azimuthal series has not converged). Should be between 0 and 0.01 to avoid risk of serious non-convergence. Has no effect on problems lacking a beam source, since azimuthal series has only one term in that case. delta_m_plus Denote whether to use the delta-M+ method of Lin et al. (2018). do_pseudo_sphere Denote whether to use a pseudo-spherical correction. header Use a 127- (or less) character header for prints, embedded in the DISORT banner. Input headers greater than 127 characters will be truncated. Setting :code:`header=''` will eliminate both the banner and the header, and this is the only way to do so (:code:`header` is not controlled by any of the :code:`print_variables` flags); :code:`header` can be used to mark the progress of a calculation in which DISORT is called many times, while leaving all other printing turned off. print_variables Make a list of variables that control what DISORT prints. The 5 booleans control whether each of the following is printed: 1. Input variables (except ``PMOM``) 2. Fluxes 3. Intensities at user levels and angles 4. Planar transmissivity and planar albedo as a function of solar zenith angle (only used if :py:attr:`~output.OutputBehavior.incidence_beam_conditions` is set to ``True``) 5. PMOM for each layer (but only if 1. == True and only for layers with scattering) Default is :code:`None`, which makes :code:`[False, False, False, False, False]`. radius The planetary radius. This is presumably only used if :code:`do_pseudo_sphere` is set to :code:`True`, although there is no documentation on this Raises ------ TypeError Raised if any inputs cannot be cast to the correct type. ValueError Raised if any inputs cannot be cast to the correct type or if print_variables does not have 5 elements. Warnings -------- UserWarning Raised if accuracy is not between 0 and 0.01. """ self.__accuracy = self.__make_accuracy(accuracy) self.__delta_m_plus = self.__make_delta_m_plus(delta_m_plus) self.__do_pseudo_sphere = self.__make_do_pseudo_sphere(do_pseudo_sphere) self.__header = self.__make_header(header) self.__print_variables = self.__make_print_variables(print_variables) self.__radius = self.__make_radius(radius) self.__warn_if_accuracy_is_outside_valid_range() def __make_accuracy(self, accuracy: float) -> float: return self.__cast_variable_to_float(accuracy, 'accuracy') def __make_delta_m_plus(self, delta_m_plus: bool) -> bool: return self.__cast_variable_to_bool(delta_m_plus, 'delta_m_plus') def __make_do_pseudo_sphere(self, do_pseudo_sphere: bool) -> bool: return self.__cast_variable_to_bool( do_pseudo_sphere, 'do_pseudo_sphere') # TODO: this does more than one thing @staticmethod def __make_header(header) -> str: try: header = str(header) if len(header) >= 127: header = header[:126] return header except TypeError as te: raise TypeError('header cannot be cast into a string.') from te # TODO: this does more than one thing @staticmethod def __make_print_variables(pvar) -> list[bool]: if pvar is None: return [False, False, False, False, False] else: try: prnt = [bool(f) for f in pvar] if len(prnt) != 5: raise ValueError('print_variables should have 5 elements.') return prnt except TypeError as te: raise TypeError('print_variables should be a list of bools') \ from te def __make_radius(self, radius: float) -> float: radius = self.__cast_variable_to_float(radius, 'radius') self.__raise_value_error_if_radius_is_not_positive(radius) return radius @staticmethod def __cast_variable_to_float(variable: float, name: str) -> float: try: return float(variable) except TypeError as te: raise TypeError(f'{name} cannot be cast into a float.') from te except ValueError as ve: raise ValueError(f'{name} cannot be cast into a float.') from ve @staticmethod def __cast_variable_to_bool(variable: bool, name: str) -> bool: try: return bool(variable) except TypeError as te: raise TypeError(f'{name} cannot be cast into a boolean.') from te @staticmethod def __raise_value_error_if_radius_is_not_positive(radius) -> None: if radius <= 0: raise ValueError('radius must be positive.') def __warn_if_accuracy_is_outside_valid_range(self) -> None: if not 0 <= self.__accuracy <= 0.01: warn('accuracy is expected to be between 0 and 0.01.') @property def accuracy(self) -> float: """Get the input accuracy. Notes ----- In DISORT, this variable is named :code:`ACCUR`. """ return self.__accuracy @property def delta_m_plus(self) -> bool: """Get whether to use delta-M+ scaling. Notes ----- In DISORT, this variable is named :code:`DELTAMPLUS`. """ return self.__delta_m_plus @property def do_pseudo_sphere(self) -> bool: """Get whether to perform a pseudo-spherical correction. Notes ----- In DISORT, this variable is named :code:`DO_PSEUDO_SPHERE`. """ return self.__do_pseudo_sphere @property def header(self) -> str: """Get the characters that will appear in the DISORT banner. Notes ----- In DISORT, this variable is named :code:`HEADER`. """ return self.__header @property def print_variables(self) -> list[bool]: """Get the variables to print. Notes ----- In DISORT, this variable is named :code:`PRNT`. """ return self.__print_variables @property def radius(self) -> float: """Get the planetary radius. Notes ----- In DISORT, this variable is named :code:`EARTH_RADIUS`. """ return self.__radius def __str__(self) -> str: return f'ModelBehavior: \n' \ f' accuracy = {self.__accuracy} \n' \ f' delta_m_plus = {self.__delta_m_plus} \n' \ f' do_pseudo_sphere = {self.__do_pseudo_sphere} \n' \ f' header = {self.__header} \n' \ f' print_variables = {self.__print_variables} \n' \ f' radius = {self.__radius}'