Data model
The main idea of the emulsim package is to separate the description of the state
of the simulation from the description that govern the dynamics.
The state gives all the necessary information to represent the system at a particular
time point.
Since we restrict ourself to memory-less systems (i.e., Markov chains), the state
contains all information to evolve the system forward in time.
The full dynamics then leads to a sequence of states, collectively called a
trajectory.
The aim of the package is to evolve an initial state according to some dynamics to
obtain the entire trajectory or only the final state.
System state (Elements)
A full physical system consist of elements that define what is present in the system
and actors that define how these elements interact and evolve in time.
All the elements in a system together described by the system’s
State. All elements defined by the package are collected in the
List of Elements.
Each element has internal degrees of freedom, which can change over time and can
accessed by the data attribute.
Elements might also have attributes that do not
change over time.
Attributes are typically given when initializing an element.
For instance, most elements accept an attribute parameters, which defines the physical
parameters like the mass of an object.
Together, data and
attributes fully describe an element.
In particular, those two elements should contain all information to re-create the
element, e.g., from a file.
To summarize, the key assumption of emulsim is that elements have fixed properties,
described by attributes, and dynamical degrees of freedom, described by data.
Some elements represent collections of identical items, e.g., the
SphericalDropletsElement. In this case, the
data attribute is an array specifying the degree of freedoms for each item.
Since each individual item could have multiple degrees of freedom (e.g., position and
radius for the a droplet), we use structured datatypes of numpy, which can be created
with the numpy.dtype class.
To define a custom element, you need to define a class that inherits from
ArrayElementBase,
ArrayCollectionElementBase, or
ObjectElementBase depending on what data is best describing
the degrees of freedom of your element.
These base elements can already support the data attribute and are fully functional.
To customize the element, you can add model parameters to it by defining the class
attribute parameters_default.
If you want to add attributes other than parameters, you need to overwrite the
attributes property and the class method
_init_state(), which initializes objects from a
supplied state.
These aspects are explained in the code example below:
from emulsim.elements.base import ElementBase
class CustomElement(ElementBase):
parameters_default = {'mass': 10}
def __init__(self, data, name="Custom", parameters=None):
super().__init__(data, parameters)
self.name = name
@property
def attributes(self):
attrs = super().attributes
attrs['name'] = self.name
return attrs
def _init_state(self, attributes, data):
super()._init_state(attributes, data)
self.name = attributes.get("name", "No name")
Simulation dynamics (Actors)
The system state changes according to physical principles.
Since the state is encoded in elements, the physics must change the dynamical degrees
of freedom of the elements, i.e., their data attributes.
In the emulsim package, the dynamics are defined by a
Simulation object.
Each simulation contains one or more actors, which each affect one or multiple
elements.
All actors defined by the package are collected in the List of Actors.
This approach allows combining multiple actors without redefining their code simply by
combining them in a Simulation.
Each actor inherits from ActorBase, which defines the necessary
behavior.
In the simplest case, a custom actor only needs to overwrite the
evolve() method, which evolves its elements from time
t to t + dt, changing the respective data attributes in place:
class BrownianParticlesActor(emulsim.ActorBase):
diffusivity = 1
def evolve(self, elements, t, dt):
""" evolve the particles in time """
(particles,) = elements
scale = np.sqrt(dt) * self.diffusivity
particles.data[...] += scale * np.random.normal(size=particles.data.shape)
def make_evolver_numba(self, elements):
"""return a function evolve the field state from time `t` to `t + dt` """
diffusivity = self.diffusivity
@jit
def evolver(state_data, t, dt):
""" evolve all points explicitly """
scale = np.sqrt(dt * diffusivity)
for i in range(state_data[0].size):
state_data[0].flat[i] += scale * np.random.randn()
return evolver