예제 #1
0
    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
예제 #2
0
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)