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