def check_create(**params): """ Check that the given `params` are valid for creating the variable once the domain is available Args: name (str): a required identifier string unit (str): a string like ("kg/m**3") that defines the physical units of the variable value (float, :class:`~numpy.ndarray`, :class:`~fipy.PhysicalField`): the value to set for the variable. If a :class:`PhysicalField` is supplied, then its (base) unit is used as `unit` and overrides any supplied `unit`. Returns: dict: the params dictionary to be used Raises: ValueError: if no `name` is supplied ValueError: if `unit` is not a valid input for :class:`PhysicalField` Note: Due to limitation in :mod:`fipy` (v3.1.3) that meshes do not accept arrays :class:`PhysicalField` as inputs, the variables defined here are cast into base units since the domain mesh is created in meters. """ name = params.get('name') if name: raise ValueError( 'Create params should not contain name. Will be set from init name.' ) from fipy import PhysicalField value = params.get('value', 0.0) if hasattr(value, 'unit'): unit = value.unit else: unit = params.get('unit') try: p = PhysicalField(value, unit) except: raise ValueError('{!r} is not a valid unit!'.format(unit)) pbase = p.inBaseUnits() params['unit'] = pbase.unit.name() params['value'] = pbase.value return params
class Irradiance(DomainEntity): """ Class that represents a source of irradiance in the model domain. The irradiance is discretized into separate "channels" (see :class:`IrradianceChannel`), representing a range of the light spectrum. This is useful to define channels such as PAR ( photosynthetically active radiation) or NIR (near-infrared), etc. The irradiance has a :attr:`.surface_level` which is modulated as ``cos(time)``, to mimic the cosinusoidal variation of solar radiance during a diel period. The diel period is considered to run from midnight to midnight. The intensity in each channel is then represented as a fraction of the surface level (set at 100). """ def __init__(self, hours_total=24, day_fraction=0.5, channels=None, **kwargs): """ Initialize an irradiance source in the model domain Args: hours_total (int, float, PhysicalField): Number of hours in a diel period day_fraction (float): Fraction (between 0 and 1) of diel period which is illuminated (default: 0.5) channels: See :meth:`.create_channel` **kwargs: passed to superclass """ self.logger = kwargs.get('logger') or logging.getLogger(__name__) self.logger.debug('Init in {}'.format(self.__class__.__name__)) kwargs['logger'] = self.logger super(Irradiance, self).__init__(**kwargs) #: the channels in the irradiance entity self.channels = {} #: the number of hours in a diel period self.hours_total = PhysicalField(hours_total, 'h') if not (1 <= self.hours_total.value <= 48): raise ValueError('Hours total {} should be between (1, 48)'.format( self.hours_total)) # TODO: remove (1, 48) hour constraint on hours_total day_fraction = float(day_fraction) if not (0 < day_fraction < 1): raise ValueError("Day fraction should be between 0 and 1") #: fraction of diel period that is illuminated self.day_fraction = day_fraction #: numer of hours in the illuminated fraction self.hours_day = day_fraction * self.hours_total #: the time within the diel period which is the zenith of radiation self.zenith_time = self.hours_day #: the intensity level at the zenith time self.zenith_level = 100.0 C = 1.0 / numerix.sqrt(2 * numerix.pi) # to scale the cosine distribution from 0 to 1 (at zenith) self._profile = cosine(loc=self.zenith_time, scale=C**2 * self.hours_day) # This profile with loc=zenith means that the day starts at "midnight" and zenith occurs # in the center of the daylength #: a :class:`Variable` for the momentary radiance level at the surface self.surface_irrad = Variable(name='irrad_surface', value=0.0, unit=None) if channels: for chinfo in channels: self.create_channel(**chinfo) self.logger.debug('Created Irradiance: {}'.format(self)) def __repr__(self): return 'Irradiance(total={},{})'.format(self.hours_total, '+'.join(self.channels)) def setup(self, **kwargs): """ With an available `model` instance, setup the defined :attr:`.channels`. """ self.check_domain() model = kwargs.get('model') for channel in self.channels.values(): if not channel.has_domain: channel.domain = self.domain channel.setup(model=model) @property def is_setup(self): """ Returns: bool: True if all the :attr:`.channels` are setup """ return all([c.is_setup for c in self.channels.values()]) def create_channel(self, name, k0=0, k_mods=None, model=None): """ Add a channel with :class:`IrradianceChannel`, such as PAR or NIR Args: name (str): The channel name stored in :attr:`.channels` k0 (int, `PhysicalField`): The base attenuation for this channel through the sediment k_mods (list): ``(var, coeff)`` pairs to add attenuation sources to k0 for the channel model (None, object): instance of the model, if available Returns: The created :class:`IrradianceChannel` instance """ if name in self.channels: raise RuntimeError('Channel {} already created'.format(name)) channel = IrradianceChannel(name=name, k0=k0, k_mods=k_mods) self.channels[name] = channel if self.has_domain: channel.domain = self.domain channel.setup(model=model) return channel def on_time_updated(self, clocktime): """ Update the surface irradiance according to the clock time Args: clocktime (:class:`PhysicalField`): The model clock time """ if isinstance(clocktime, PhysicalField): clocktime_ = clocktime.inBaseUnits( ) % self.hours_total.inBaseUnits() else: clocktime_ = clocktime % self.hours_total.numericValue # logger.debug('clock % hours_total = {} % {} = {}'.format( # clock, self.hours_total, clocktime_)) # logger.debug('Profile level for clock {}: {}'.format( # clock, self._profile.pdf(clocktime_))) surface_value = self.zenith_level * self.hours_day.numericValue / 2.0 * \ self._profile.pdf(clocktime_) self.surface_irrad.value = surface_value self.logger.debug('Updated for time {} surface irradiance: {}'.format( clocktime, self.surface_irrad)) for channel in self.channels.values(): #: TODO: remove explicit calling by using Variable? channel.update_intensities(self.surface_irrad) def snapshot(self, base=False): """ Returns a snapshot of the Irradiance's state with the structure * "channels" * "name" : :meth:`IrradianceChannel.snapshot` of each channel * "metadata" * `"hours_total"`: str(:attr:`.hours_total`) * `"day_fraction"`: :attr:`.day_fraction` * `"zenith_time"`: str(:attr:`.zenith_time`) * `"zenith_level"`: :attr:`.zenith_level` Args: base (bool): Convert to base units? Returns: dict: the state dictionary """ self.logger.debug('Snapshot: {}'.format(self)) self.check_domain() state = dict() meta = state['metadata'] = {} meta['hours_total'] = str(self.hours_total) meta['day_fraction'] = self.day_fraction meta['zenith_time'] = str(self.zenith_time) meta['zenith_level'] = self.zenith_level channels = state['channels'] = {} for ch, chobj in self.channels.items(): channels[ch] = chobj.snapshot(base=base) return state def restore_from(self, state, tidx): """ Restore state of each irradiance channel """ self.logger.debug('Restoring {} from state: {}'.format( self, tuple(state))) self.check_domain() for ch, chobj in self.channels.items(): chobj.restore_from(state['channels'][ch], tidx)