def test_on_time_updated(self): expr = mock.MagicMock(Expression) expr.expr.return_value = EXPR = object() pe = ProcessEvent(expr) CONDVAL = mock.MagicMock() pe.condition = mock.Mock(return_value=CONDVAL) CONDVAL.__invert__.return_value = NEG = object() PREVVAL = PhysicalField(2, 's') pe._prev_clock = PREV = mock.MagicMock(return_value=PREVVAL) clock = mock.MagicMock() DIFF_VAL = PhysicalField(5, 's') clock.__sub__ = mock.Mock(return_value=DIFF_VAL) EVENTVAL = PhysicalField(9, 's') pe.event_time = mock.MagicMock(return_value=EVENTVAL) pe.event_time.value = mock.MagicMock() pe.event_time.__iter__.return_value = [1, 2] pe.on_time_updated(clock) clock.copy().__sub__.assert_called_once() pe.condition.assert_called_once() pe.event_time.setValue.assert_called_once() CONDVAL.__invert__.assert_called_once() pe.event_time.value.__setitem__.assert_called_once_with(NEG, 0.0)
def update_irradiance(self): """ Update the set of data paths for irradiance intensities in :attr:`.irradiance_intensities`. """ self.logger.debug('Updating irradiances') irradiances = set() irradiance = self.store[self.ENTRY_ENV].get(self.ENTRY_IRRADIANCE) if not irradiance: self.logger.debug('No irradiance info found') else: # load irradiance cycle info # irradiance might be hdf.Dataset or a dict linfo = dict(getattr(irradiance, 'attrs', irradiance.get('metadata'))) self.diel_period = PhysicalField(linfo['hours_total']) self.diel_zenith = PhysicalField(linfo['zenith_time']) for chname in irradiance['channels']: data_path = self.PATH_IRRADIANCE_CHANNELS + '/'.join([ '', # for a leading slash in the subpath chname, 'intensity' ]) irradiances.add(data_path) self.logger.debug('Added irradiance intensity: {}'.format(data_path)) self.irradiance_intensities = irradiances self.logger.debug('Updated irradiance intensities: {}'.format(self.irradiance_intensities))
def finalize(self): """ Call this to setup the equation. Once this is called, no more terms can be added. This does:: self.obj = self.term_transient == sum(self.RHS_terms) Raises: RuntimeError: if no :attr:`.term_transient` defined or no RHS terms defined """ if self.finalized: self.logger.warning('Equation already finalized') return if self.term_transient is None: raise RuntimeError('Cannot finalize equation without transient term!') RHS_terms = self.RHS_terms if not RHS_terms: raise RuntimeError('Cannot finalize equation without right-hand side terms') if self.source_exprs: self.sources_total = sum(self.source_exprs.values()) #: the additive sum of all the sources else: self.sources_total = PhysicalField(np.zeros_like(self.var), self.var.unit.name() + '/s') self.obj = self.term_transient == sum(self.RHS_terms) self.update_tracked_budget(PhysicalField(0.0, 's')) self.finalized = True self.logger.info('Final equation: {}'.format(self.obj))
def test_constrain_with_unit(self, varunit, conunit): create = dict(value=3., unit=varunit, hasOld=True) name = 'MyVar' conval = PhysicalField(5, conunit) constraints = dict(top=conval, ) v = ModelVariable(name=name, create=create, constraints=constraints) domain = SedimentDBLDomain() v.set_domain(domain) try: varval = conval.inUnitsOf(varunit) except TypeError: # incompatible units varval = None if varval is None: with pytest.raises(TypeError): v.setup() else: v.setup() v.var.updateOld() assert v.var.faceValue[0] == conval
def unit_constructor(loader, node): value = loader.construct_scalar(node) if isinstance(value, str): ret = PhysicalField(str(value)) elif isinstance(value, (tuple, list)): ret = PhysicalField(*value) else: raise ValueError( 'Unknown input for PhysicalField: {} (type: {})'.format( value, type(value))) return ret
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
def simtime_lims(self, vals): if vals is None: lmin = PhysicalField(0.1, 's') # lmax = (self.simtime_total / 25.0).inUnitsOf('s').floor() lmax = PhysicalField(120, 's') else: lmin, lmax = [PhysicalField(float(_), 's') for _ in vals] if not (0 < lmin < lmax): raise ValueError( 'simtime_lims ({}, {}) are not positive and in order'.format( lmin, lmax)) self._simtime_lims = (lmin, lmax) self.logger.debug('simtime_lims set: {}'.format(self._simtime_lims))
def resume_existing_simulation(self, data_outpath=None): if not self.resume: self.logger.info( 'resume={}, so will not resume from existing file'.format( self.resume)) return data_outpath = data_outpath or self.data_outpath if not os.path.exists(data_outpath): self.logger.debug('Outpath does not exist, cannot resume...') return from fipy import PhysicalField import h5py as hdf # open the store and read out the time info with hdf.File(data_outpath, 'r') as store: tds = store['/time/data'] nt = len(tds) target_time = tds[self.resume] latest_time = tds[-1] time_unit = tds.attrs['unit'] target_time = PhysicalField(target_time, time_unit) latest_time = PhysicalField(latest_time, time_unit) click.secho( 'Model resume set: rewind from latest {} ({}) to {} ({})?'.format( latest_time, nt, target_time, self.resume), fg='red') if self.confirm: click.confirm( 'Rewinding model clock can lead to data loss! Continue?', default=False, abort=True) try: with hdf.File(data_outpath, 'a') as store: self.model.restore_from(store, time_idx=self.resume) click.secho('Model restore successful. Clock = {}'.format( self.model.clock), fg='green') self.simulation.simtime_step = 1 # set a small simtime to start except: click.secho( 'Simulation could not be restored from given data file!', fg='red') raise # click.Abort()
def test_init_physical(self): cell_size = PhysicalField('20 mum') sediment_length = PhysicalField('1. cm') dbl_length = PhysicalField('1.0 mm') domain = SedimentDBLDomain(cell_size=cell_size, sediment_length=sediment_length, dbl_length=dbl_length) # internally converted to mm assert domain.cell_size.unit.name() == 'mm' assert domain.cell_size == cell_size assert domain.sediment_length == sediment_length assert domain.DBL_length == dbl_length assert 'sed_mask' in domain
def test_init_float(self): # interpreted as millimeters cell_size = 0.3 sediment_length = 30 dbl_length = 3 domain = SedimentDBLDomain(cell_size=cell_size, sediment_length=sediment_length, dbl_length=dbl_length) assert domain.cell_size.unit.name() == 'mm' assert domain.cell_size == PhysicalField(cell_size, 'mm') assert domain.sediment_length == PhysicalField(sediment_length, 'mm') assert domain.DBL_length == PhysicalField(dbl_length, 'mm')
def test_simtime_lims(self): sim = Simulation() # check the default values sim.simtime_lims = None sMin, sMax = sim.simtime_lims assert sMin == PhysicalField(0.1, 's') assert sMax == PhysicalField(120, 's') with pytest.raises(ValueError): sim.simtime_lims = (0.1, 0.001) sim.simtime_lims = (2, 3) sMin, sMax = sim.simtime_lims assert sMin == PhysicalField(2, 's') assert sMax == PhysicalField(3, 's')
def read_data_from(self, path, tidx=None): """ Get the data from the path in the snapshot Args: path (str): Input used for :meth:`.get_node` tidx (None): This is ignored, since a model snapshot only contains one timepoint Returns: a :class:`PhysicalField` of the data with units at the node """ self.logger.debug('Getting data from {}'.format(path)) node = self.get_node(path) try: data, meta = node['data'] except KeyError: try: data, meta = node['data_static'] except KeyError: self.logger.error( 'Could not find "data" or "data_static" in {}'.format( path)) raise ValueError( 'Path {} does not exist in snapshot'.format(path)) unit = meta['unit'] # if tidx is None: # return PhysicalField(np.atleast_1d(data), unit) # else: # return PhysicalField(np.atleast_1d(data[tidx]), unit) return PhysicalField(np.atleast_1d(data), unit)
def process(self, num, state): """ Write the progress information to the progress bar This includes info about residual, duration (dt), and number of sweeps in the timestep and the global progress through the model clock. """ time, tdict = state['time']['data'] # curr = PhysicalField(time, tdict['unit']).inUnitsOf(simtime_total.unit) curr = PhysicalField(time, tdict['unit']).inUnitsOf(self._clock_unit) dt = curr - self._prev_t dt_unitless = round(float((curr - self._prev_t).value), 4) # print(curr, self._prev_t, dt, dt_unitless) # clock_info = '{0:.2f}/{1:.2f} {2}'.format( # float(curr.value), # float(simtime_total.value), # simtime_total.unit.name(), # ) residual = state['metrics']['residual']['data'][0] sweeps = state['metrics']['num_sweeps']['data'][0] self._pbar.set_postfix( # clock=clock_info, dt=self.srepr(dt.inUnitsOf('s'), prec=4), res='{:.2g} / {:.2g}'.format(residual, self.sim.max_residual), sweeps='{:02d}/{}'.format(sweeps, self.sim.max_sweeps,) ) self._pbar.update(dt_unitless) self._prev_t = curr
def test_restore_from(self, model): varpath = 'domain.var1' VAL = [1.5] # make a single value array in PhysicalField UNIT = 'mol/l' model.get_object.return_value = var = mock.MagicMock(CellVariable) var.__getitem__.return_value = var0 = mock.MagicMock(CellVariable) model.domain.depths = depths = mock.MagicMock(CellVariable) depths.__getitem__.return_value = depth0 = mock.Mock() unitMock = var0 * depth0 unitMock.inBaseUnits.return_value.unit = UNIT with mock.patch('fipy.tools.numerix.trapz') as trapzMock: trapzMock.return_value = 0.0 eqn = ModelEquation(model=model, varpath=varpath, coeff=5) trackedMock = mock.MagicMock(dict) trackedMock.__getitem__.return_value = (VAL, dict(unit=UNIT)) PV = PhysicalField(VAL, UNIT) FIELDS = eqn.Tracked._fields eqn.restore_from(dict(tracked_budget=trackedMock), tidx=None) assert eqn.tracked == (PV, ) * len(FIELDS)
def restore_var(input, tidx): """ This is the inverse operation of :func:`snapshot_var`. It takes the output of that function and returns a PhysicalField quantity Returns: :class:`PhysicalField` """ if tidx is None: tidx = slice(None, None) if isinstance(input, hdf.Group): value = input['data'] unitstr = value.attrs['unit'] elif isinstance(input, (tuple, list)): value, mdict = input unitstr = mdict['unit'] else: raise ValueError('Unknown type {} to restore data from'.format( type(input))) return PhysicalField(value[tidx], unit=unitstr)
def test_evolution(self): sim = Simulation() sim._started = True with pytest.raises(RuntimeError): for ret in sim.evolution(): pass sim._started = False model = mock.MagicMock(MicroBenthosModel) clock = mock.MagicMock(ModelClock) clock.return_value = PhysicalField(0, 'h') model.clock = clock model.full_eqn = feqn = mock.Mock() feqn.sweep.return_value = RES = sim.max_residual / 2 sim.model = model evolution = sim.evolution() step, state = next(evolution) model.update_vars.call_count == 2 # once before while loop starts and once for the first yield clock.copy.assert_called_once() assert step == 1 assert isinstance(state, dict) step, state = next(evolution) clock.increment_time.assert_called_once()
def setup_model(self): self.logger.debug('Setting model data: {}'.format(self.model)) if self.model.store is None: self.logger.debug('Cannot setup model, since store is empty') return if self.fig is None: self._create_figure(**self._fig_kwds) depth_unit = 'mm' self.depths = D = np.array( self.model.depths.inUnitsOf(depth_unit).value) self.axMicrobes.set_ylabel(f'Depth ({depth_unit})') for ax in self.axes_depth: ax.axhspan(min(D), 0, color='aquamarine', alpha=0.4, zorder=0) ax.axhspan(0, max(D), color='xkcd:brown', alpha=0.4, zorder=0) self._init_artist_styles() self.create_artists() self.update_legends() self.logger.propagate = False self._clock = PhysicalField(0, 's') self.update_artists(tidx=0) self.fig.tight_layout() #rect=self._depth_rect, pad=1.02)
def __init__(self, name, k0=PhysicalField(0, '1/cm'), k_mods=None, **kwargs): """ A scalar irradiance channel. This creates variables for the channel intensities, for the attenuation values. Args: name (str): The channel name k0 (float, PhysicalField): The base attenuation for this channel through the sediment with units of (1/cm) k_mods (None, list): ``(var, coeff)`` pairs that modify `k0` based on the value of the variable pointed at by `var` (example: `"microbes.cyano.biomass"`) and multiplied with a `coeff`. `coeff` must have the units such that `var * coeff` has the units of `k0`. Raises: ValueError: if the units of `k0` are not compatible with 1/cm """ self.logger = kwargs.get('logger') or logging.getLogger(__name__) self.logger.debug('Init in {}'.format(self.__class__.__name__)) kwargs['logger'] = self.logger super(IrradianceChannel, self).__init__(**kwargs) self.name = name #: CellVariable to hold the intensities of the irradiance channel through the domain self.intensities = None try: #: the base attenuation through the sediment self.k0 = PhysicalField(k0, '1/cm') except TypeError: raise ValueError('Invalid value for k0: {}'.format(k0)) #: variable that represents the net attenuation self.k_var = None #: list of modulations for the attenuation in :attr:`.k0` self.k_mods = k_mods or [] self._mods_added = {} self.logger.debug('Created irradiance channel {}'.format(self))
def test_seed_linear_from_constraints(self, params, constraints, error): seed = dict(profile='linear') if params: seed['params'] = params unit = 'km/kg' N = 10 v = ModelVariable( name='mvar', create=dict(value=3.2, unit=unit), seed=seed, constraints=constraints, ) v.domain = mock.Mock(SedimentDBLDomain) v.domain.create_var.return_value = PhysicalField([34] * N, unit) v.domain.mesh = mock.Mock() v.domain.idx_surface = mock.Mock() v.constrain = mock.Mock() if error: with pytest.raises(error): v.setup() else: v.setup() if params is None: params = {} if constraints is None: constraints = {} startval = PhysicalField( params.get('start', constraints.get('top')), unit).inUnitsOf(unit).value stopval = PhysicalField( params.get('stop', constraints.get('bottom')), unit).inUnitsOf(unit).value expected = PhysicalField(numerix.linspace(startval, stopval, N), unit) assert numerix.array_equal(v.var, expected)
def constrain(self, loc, value): """ Constrain the variable at the given location to the given value Args: loc (str): One of ``("top", "bottom", "dbl", "sediment")`` value (float, :class:`PhysicalField`): a numeric value for the constraint Returns: None Raises: TypeError: if the units for `value` are incompatible with :attr:`.var` units ValueError: for improper values for `loc` ValueError: if value is not a 0-dimension array RuntimeError: if variable doesn't exist """ if self.var is None: raise RuntimeError('Variable {} does not exist!'.format(self.name)) self.logger.debug("Setting constraint for {!r}: {} = {}".format( self.var, loc, value)) loc, grad_type = self._parse_constraint_loc(loc) if loc in ('top', 'bottom'): mask = self._LOCs[loc] else: mask = numerix.zeros(self.var.shape, dtype=bool) try: L = self._LOCs[loc] self.logger.debug('Constraint mask loc: {}'.format(L)) mask[L] = 1 except KeyError: raise ValueError('loc={} not in {}'.format( loc, tuple(self._LOCs.keys()))) if isinstance(value, PhysicalField): value = value.inUnitsOf(self.var.unit) else: value = PhysicalField(value, self.var.unit) self.logger.info('Constraining {} (grad={}) at {} = {}'.format( self.var, grad_type, loc, value)) if grad_type: var_entity = getattr(self.var, grad_type) else: var_entity = self.var var_entity.constrain(value, mask)
def test_dump_unit(): inp = "40 mol/l" P = PhysicalField(inp) # PhysicalField with str input coerces value to float expected = "!unit '{} {}'".format(P.value, P.unit.name()) s = yaml.dump(P).strip() assert s == expected
def test_load_unit(): val = "35 mol/l" s = """ km: !unit {} """.format(val) val_ = yaml.unsafe_load(s)['km'] assert val_ == PhysicalField(val)
def _check_with_unit_name(self, field, value): """ Checks that the string can be used as units """ self.logger.debug('Validating unit_name: {}'.format(value)) try: PhysicalField(1, value) except TypeError: self._error(field, 'Must be str of physical units')
def write_frame(self, state): """ Save a frame into the output directory for the current state """ time, tdict = state['time']['data'] clock = int(PhysicalField(time, tdict['unit']).numericValue) fname = 'frame_{:010d}.png'.format(clock) path = os.path.join(self.frames_outdir, fname) self.plot.fig.savefig(path) self.logger.debug('Wrote frame: {}'.format(fname))
def test_update_time(self): # update to clock time e = Entity() with pytest.raises(TypeError): e.on_time_updated() e.on_time_updated(2) e.on_time_updated(3.5) e.on_time_updated(PhysicalField('35 s')) # should this raise an error? No, because this is only reacts to the model clock e.on_time_updated(PF('5 kg'))
def var_quantity(self): """ Calculate the integral quantity of the variable in the domain Returns: PhysicalField: depth integrated amount """ # self.logger.debug('Calculating actual var quantity') q = np.trapz(self.var.numericValue, self.model.domain.depths.numericValue) unit = (self.var[0] * self.model.domain.depths[0]).inBaseUnits().unit return PhysicalField(q, unit)
def test_create_var(self, value, unit, hasOld): # creation casts it into base units create = dict(value=value, unit=unit, hasOld=hasOld) name = 'myVar' v = ModelVariable(name=name, create=create) domain = SedimentDBLDomain() v.set_domain(domain) v.setup() assert v.var is domain[name] assert v.var.name == v.name assert v.var.shape == domain.mesh.shape assert (v.var() == PhysicalField(value, unit).inBaseUnits()).all()
def test_simtime_days(self): sim = Simulation(simtime_days=3) sTot = sim.simtime_total model = mock.MagicMock(MicroBenthosModel) clock = mock.MagicMock(ModelClock) model.clock = clock model.full_eqn = mock.Mock() model.get_object.return_value = I = mock.Mock() I.hours_total = 5 sim.model = model model.get_object.assert_called_once_with('env.irradiance') assert sim.simtime_total == PhysicalField(5 * 3, 'h')
def test_simtime_total(self): sim = Simulation() with pytest.raises(ValueError): sim.simtime_total = 0 with pytest.raises(ValueError): sim.simtime_total = -3 V = 3 sim.simtime_total = V assert sim.simtime_total == PhysicalField(V, 'h') with pytest.raises(ValueError): sim.simtime_total = sim.simtime_step / 2.0
def read_data_from(self, path, tidx=None): """ Read out the data in `path` (a :class:`h5py:Dataset`) into a :class:`PhysicalField` If `tidx` is `None`, then no slicing of the dataset is done """ path = path.replace('.', '/') if not path.endswith('/data'): path += '/data' ds = self.store[path] try: ds.id.refresh() except AttributeError: pass self.logger.debug('Found {}: {}'.format(path, ds)) ds_unit = str(ds.attrs['unit']) if tidx is None: return PhysicalField(ds, ds_unit) else: return PhysicalField(ds[tidx], ds_unit)