Source code for parcels.particle

from ctypes import c_void_p
from operator import attrgetter

import numpy as np

from parcels.field import Field
from parcels.tools.statuscodes import StateCode, OperationCode
from parcels.tools.loggers import logger

__all__ = ['ScipyParticle', 'JITParticle', 'Variable']

indicators_64bit = [np.float64, np.uint64, np.int64, c_void_p]


[docs]class Variable(object): """Descriptor class that delegates data access to particle data :param name: Variable name as used within kernels :param dtype: Data type (numpy.dtype) of the variable :param initial: Initial value of the variable. Note that this can also be a Field object, which will then be sampled at the location of the particle :param to_write: Boolean or 'once'. Controls whether Variable is written to NetCDF file. If to_write = 'once', the variable will be written as a time-independent 1D array :type to_write: (bool, 'once', optional) """ def __init__(self, name, dtype=np.float32, initial=0, to_write=True): self.name = name self.dtype = dtype self.initial = initial self.to_write = to_write def __get__(self, instance, cls): if instance is None: return self if issubclass(cls, JITParticle): return instance._cptr.__getitem__(self.name) else: return getattr(instance, "_%s" % self.name, self.initial) def __set__(self, instance, value): if isinstance(instance, JITParticle): instance._cptr.__setitem__(self.name, value) else: setattr(instance, "_%s" % self.name, value) def __repr__(self): return "PVar<%s|%s>" % (self.name, self.dtype)
[docs] def is64bit(self): """Check whether variable is 64-bit""" return True if self.dtype in indicators_64bit else False
class ParticleType(object): """Class encapsulating the type information for custom particles :param user_vars: Optional list of (name, dtype) tuples for custom variables """ def __init__(self, pclass): if not isinstance(pclass, type): raise TypeError("Class object required to derive ParticleType") if not issubclass(pclass, ScipyParticle): raise TypeError("Class object does not inherit from parcels.ScipyParticle") self.name = pclass.__name__ self.uses_jit = issubclass(pclass, JITParticle) # Pick Variable objects out of __dict__. self.variables = [v for v in pclass.__dict__.values() if isinstance(v, Variable)] for cls in pclass.__bases__: if issubclass(cls, ScipyParticle): # Add inherited particle variables ptype = cls.getPType() for v in self.variables: if v.name in [v.name for v in ptype.variables]: raise AttributeError( "Custom Variable name '%s' is not allowed, as it is also a built-in variable" % v.name) if v.name == 'z': raise AttributeError( "Custom Variable name 'z' is not allowed, as it is used for depth in ParticleFile") self.variables = ptype.variables + self.variables # Sort variables with all the 64-bit first so that they are aligned for the JIT cptr self.variables = [v for v in self.variables if v.is64bit()] + \ [v for v in self.variables if not v.is64bit()] def __repr__(self): return "PType<%s>::%s" % (self.name, self.variables) @property def _cache_key(self): return "-".join(["%s:%s" % (v.name, v.dtype) for v in self.variables]) @property def dtype(self): """Numpy.dtype object that defines the C struct""" type_list = [(v.name, v.dtype) for v in self.variables] for v in self.variables: if v.dtype not in self.supported_dtypes: raise RuntimeError(str(v.dtype) + " variables are not implemented in JIT mode") if self.size % 8 > 0: # Add padding to be 64-bit aligned type_list += [('pad', np.float32)] return np.dtype(type_list) @property def size(self): """Size of the underlying particle struct in bytes""" return sum([8 if v.is64bit() else 4 for v in self.variables]) @property def supported_dtypes(self): """List of all supported numpy dtypes. All others are not supported""" # Developer note: other dtypes (mostly 2-byte ones) are not supported now # because implementing and aligning them in cgen.GenerableStruct is a # major headache. Perhaps in a later stage return [np.int32, np.int64, np.float32, np.double, np.float64, c_void_p] class _Particle(object): """Private base class for all particle types""" lastID = 0 # class-level variable keeping track of last Particle ID used def __init__(self): ptype = self.getPType() # Explicit initialisation of all particle variables for v in ptype.variables: if isinstance(v.initial, attrgetter): initial = v.initial(self) elif isinstance(v.initial, Field): lon = self.getInitialValue(ptype, name='lon') lat = self.getInitialValue(ptype, name='lat') depth = self.getInitialValue(ptype, name='depth') time = self.getInitialValue(ptype, name='time') if time is None: raise RuntimeError('Cannot initialise a Variable with a Field if no time provided. ' 'Add a "time=" to ParticleSet construction') v.initial.fieldset.computeTimeChunk(time, 0) initial = v.initial[time, depth, lat, lon] logger.warning_once("Particle initialisation from field can be very slow as it is computed in scipy mode.") else: initial = v.initial # Enforce type of initial value if v.dtype != c_void_p: setattr(self, v.name, v.dtype(initial)) # Placeholder for explicit error handling self.exception = None @classmethod def getPType(cls): return ParticleType(cls) @classmethod def getInitialValue(cls, ptype, name): return next((v.initial for v in ptype.variables if v.name is name), None) @classmethod def setLastID(cls, offset): _Particle.lastID = offset
[docs]class ScipyParticle(_Particle): """Class encapsulating the basic attributes of a particle, to be executed in SciPy mode :param lon: Initial longitude of particle :param lat: Initial latitude of particle :param depth: Initial depth of particle :param fieldset: :mod:`parcels.fieldset.FieldSet` object to track this particle on :param time: Current time of the particle Additional Variables can be added via the :Class Variable: objects """ lon = Variable('lon', dtype=np.float32) lat = Variable('lat', dtype=np.float32) depth = Variable('depth', dtype=np.float32) time = Variable('time', dtype=np.float64) xi = Variable('xi', dtype=np.int32, to_write=False) yi = Variable('yi', dtype=np.int32, to_write=False) zi = Variable('zi', dtype=np.int32, to_write=False) ti = Variable('ti', dtype=np.int32, to_write=False, initial=-1) id = Variable('id', dtype=np.int64) fileid = Variable('fileid', dtype=np.int32, initial=-1, to_write=False) dt = Variable('dt', dtype=np.float64, to_write=False) state = Variable('state', dtype=np.int32, initial=StateCode.Evaluate, to_write=False) next_dt = Variable('_next_dt', dtype=np.float64, initial=np.nan, to_write=False) def __init__(self, lon, lat, pid, fieldset, depth=0., time=0., cptr=None): # Enforce default values through Variable descriptor type(self).lon.initial = lon type(self).lat.initial = lat type(self).depth.initial = depth type(self).time.initial = time type(self).id.initial = pid _Particle.lastID = max(_Particle.lastID, pid) type(self).fileid.initial = -1 type(self).dt.initial = None type(self).next_dt.initial = np.nan for index in ['xi', 'yi', 'zi', 'ti']: if index != 'ti': setattr(self, index, np.zeros((fieldset.gridset.size), dtype=np.int32)) else: setattr(self, index, -1*np.ones((fieldset.gridset.size), dtype=np.int32)) super(ScipyParticle, self).__init__() def __repr__(self): time_string = 'not_yet_set' if self.time is None or np.isnan(self.time) else "{:f}".format(self.time) str = "P[%d](lon=%f, lat=%f, depth=%f, " % (self.id, self.lon, self.lat, self.depth) for var in vars(type(self)): if type(getattr(type(self), var)) is Variable and getattr(type(self), var).to_write is True: str += "%s=%f, " % (var, getattr(self, var)) return str + "time=%s)" % time_string def delete(self): self.state = OperationCode.Delete def set_state(self, state): self.state = state @classmethod def set_lonlatdepth_dtype(cls, dtype): cls.lon.dtype = dtype cls.lat.dtype = dtype cls.depth.dtype = dtype def update_next_dt(self, next_dt=None): if next_dt is None: if self._next_dt is not None: self.dt = self._next_dt self._next_dt = None else: self._next_dt = next_dt
[docs]class JITParticle(ScipyParticle): """Particle class for JIT-based (Just-In-Time) Particle objects :param lon: Initial longitude of particle :param lat: Initial latitude of particle :param fieldset: :mod:`parcels.fieldset.FieldSet` object to track this particle on :param dt: Execution timestep for this particle :param time: Current time of the particle Additional Variables can be added via the :Class Variable: objects Users should use JITParticles for faster advection computation. """ def __init__(self, *args, **kwargs): self._cptr = kwargs.pop('cptr', None) if self._cptr is None: # Allocate data for a single particle ptype = self.getPType() self._cptr = np.empty(1, dtype=ptype.dtype)[0] super(JITParticle, self).__init__(*args, **kwargs) for index in ['xi', 'yi', 'zi', 'ti']: setattr(self, index+'p', getattr(self, index).ctypes.data_as(c_void_p)) setattr(self, 'c'+index, getattr(self, index+'p').value)