emulsim 0.5.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- emulsim/__init__.py +32 -0
- emulsim/_version.py +24 -0
- emulsim/actors/__init__.py +59 -0
- emulsim/actors/autonomous/__init__.py +31 -0
- emulsim/actors/autonomous/active_particles.py +128 -0
- emulsim/actors/autonomous/box.py +177 -0
- emulsim/actors/autonomous/brownian_motion.py +135 -0
- emulsim/actors/autonomous/coalescence.py +125 -0
- emulsim/actors/autonomous/emitters.py +104 -0
- emulsim/actors/autonomous/fields.py +448 -0
- emulsim/actors/base.py +237 -0
- emulsim/actors/coupling/__init__.py +21 -0
- emulsim/actors/coupling/fields.py +557 -0
- emulsim/actors/coupling/multicomponent_droplet.py +775 -0
- emulsim/actors/coupling/nucleation.py +302 -0
- emulsim/actors/coupling/point_droplet.py +455 -0
- emulsim/actors/coupling/spherical_droplet.py +1728 -0
- emulsim/actors/function.py +162 -0
- emulsim/elements/__init__.py +33 -0
- emulsim/elements/base.py +822 -0
- emulsim/elements/fields.py +1165 -0
- emulsim/elements/multicomponent_droplets.py +236 -0
- emulsim/elements/points.py +269 -0
- emulsim/elements/spherical_droplets.py +340 -0
- emulsim/py.typed +1 -0
- emulsim/simulation.py +940 -0
- emulsim/state.py +513 -0
- emulsim/trackers.py +353 -0
- emulsim-0.5.0.dist-info/METADATA +86 -0
- emulsim-0.5.0.dist-info/RECORD +33 -0
- emulsim-0.5.0.dist-info/WHEEL +5 -0
- emulsim-0.5.0.dist-info/licenses/LICENSE +21 -0
- emulsim-0.5.0.dist-info/top_level.txt +1 -0
emulsim/__init__.py
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
r"""The `emulsim` package provides classes for describing physical system that consists of
|
|
2
|
+
multiple `elements`, which together describe the state of the system.
|
|
3
|
+
|
|
4
|
+
The dynamical
|
|
5
|
+
rules are encoded in `actors`, which either act on individual elements, encoding their
|
|
6
|
+
autonomous dynamics, or on multiple elements, introducing couplings.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
# determine the package version
|
|
10
|
+
try:
|
|
11
|
+
# try reading version from the automatically generated module
|
|
12
|
+
from ._version import __version__ # type: ignore
|
|
13
|
+
except ImportError:
|
|
14
|
+
# determine version automatically from CVS information
|
|
15
|
+
from importlib.metadata import PackageNotFoundError, version
|
|
16
|
+
|
|
17
|
+
try:
|
|
18
|
+
__version__ = version("emulsim")
|
|
19
|
+
except PackageNotFoundError:
|
|
20
|
+
# package is not installed, so we cannot determine any version
|
|
21
|
+
__version__ = "unknown"
|
|
22
|
+
del PackageNotFoundError, version # clean name space
|
|
23
|
+
|
|
24
|
+
# make key classes from modelrunner available
|
|
25
|
+
from modelrunner import Parameter
|
|
26
|
+
|
|
27
|
+
# import key classes from emulsim package into general namespace
|
|
28
|
+
from .actors import *
|
|
29
|
+
from .elements import *
|
|
30
|
+
from .simulation import Simulation
|
|
31
|
+
from .state import State
|
|
32
|
+
from .trackers import *
|
emulsim/_version.py
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
# file generated by vcs-versioning
|
|
2
|
+
# don't change, don't track in version control
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
__all__ = [
|
|
6
|
+
"__version__",
|
|
7
|
+
"__version_tuple__",
|
|
8
|
+
"version",
|
|
9
|
+
"version_tuple",
|
|
10
|
+
"__commit_id__",
|
|
11
|
+
"commit_id",
|
|
12
|
+
]
|
|
13
|
+
|
|
14
|
+
version: str
|
|
15
|
+
__version__: str
|
|
16
|
+
__version_tuple__: tuple[int | str, ...]
|
|
17
|
+
version_tuple: tuple[int | str, ...]
|
|
18
|
+
commit_id: str | None
|
|
19
|
+
__commit_id__: str | None
|
|
20
|
+
|
|
21
|
+
__version__ = version = '0.5.0'
|
|
22
|
+
__version_tuple__ = version_tuple = (0, 5, 0)
|
|
23
|
+
|
|
24
|
+
__commit_id__ = commit_id = None
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
"""Provides actors that determine the dynamics of the simulation by modifying the state
|
|
2
|
+
of elements during each time step. Actors are separated into several categories, which
|
|
3
|
+
we describe separately below.
|
|
4
|
+
|
|
5
|
+
**General actors** are classes that provide basic infrastructure to implement custom
|
|
6
|
+
implementations:
|
|
7
|
+
|
|
8
|
+
.. autosummary::
|
|
9
|
+
:nosignatures:
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
~base.ActorBase
|
|
13
|
+
~function.FunctionActor
|
|
14
|
+
~function.NumbaFunctionActor
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
**Autonomous actors** only affect a single element and thus describe the autonomous
|
|
18
|
+
dynamics of this element when it is not coupled to other elements.
|
|
19
|
+
|
|
20
|
+
.. autosummary::
|
|
21
|
+
:nosignatures:
|
|
22
|
+
|
|
23
|
+
~autonomous.active_particles.ActiveParticleActor
|
|
24
|
+
~autonomous.box.BoxActor
|
|
25
|
+
~autonomous.brownian_motion.BrownianMotionActor
|
|
26
|
+
~autonomous.coalescence.CoalescenceDropletActor
|
|
27
|
+
~autonomous.emitters.EmittersActor
|
|
28
|
+
~autonomous.fields.LocalReactionsActor
|
|
29
|
+
~autonomous.fields.ScalarPDEActor
|
|
30
|
+
~autonomous.fields.DiffusionActor
|
|
31
|
+
~autonomous.fields.ReactionDiffusionActor
|
|
32
|
+
~autonomous.fields.CollectionPDEActor
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
**Coupling actors** affect several elements and thus describe a coupling between these
|
|
36
|
+
elements.
|
|
37
|
+
|
|
38
|
+
.. autosummary::
|
|
39
|
+
:nosignatures:
|
|
40
|
+
|
|
41
|
+
~coupling.nucleation.DropletNucleationActor
|
|
42
|
+
~coupling.fields.FieldCouplingActor
|
|
43
|
+
~coupling.fields.FieldExchangeActor
|
|
44
|
+
~coupling.fields.FieldBoundaryExchangeActor
|
|
45
|
+
~coupling.point_droplet.PointDropletActor
|
|
46
|
+
~coupling.spherical_droplet.SphericalDropletActor
|
|
47
|
+
~coupling.multicomponent_droplet.MulticomponentDropletActor
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
Use :func:`~base.find_actors` to discover actors that are compatible with a given list
|
|
51
|
+
of elements.
|
|
52
|
+
|
|
53
|
+
.. codeauthor:: David Zwicker <david.zwicker@ds.mpg.de>
|
|
54
|
+
"""
|
|
55
|
+
|
|
56
|
+
from .autonomous import *
|
|
57
|
+
from .base import ActorBase, find_actors
|
|
58
|
+
from .coupling import *
|
|
59
|
+
from .function import FunctionActor, NumbaFunctionActor
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
"""Provides actors that affect single elements in a simulation.
|
|
2
|
+
|
|
3
|
+
.. autosummary::
|
|
4
|
+
:nosignatures:
|
|
5
|
+
|
|
6
|
+
~active_particles.ActiveParticleActor
|
|
7
|
+
~box.BoxActor
|
|
8
|
+
~brownian_motion.BrownianMotionActor
|
|
9
|
+
~coalescence.CoalescenceDropletActor
|
|
10
|
+
~emitters.EmittersActor
|
|
11
|
+
~fields.LocalReactionsActor
|
|
12
|
+
~fields.ScalarPDEActor
|
|
13
|
+
~fields.DiffusionActor
|
|
14
|
+
~fields.ReactionDiffusionActor
|
|
15
|
+
~fields.CollectionPDEActor
|
|
16
|
+
|
|
17
|
+
.. codeauthor:: David Zwicker <david.zwicker@ds.mpg.de>
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
from .active_particles import ActiveParticleActor
|
|
21
|
+
from .box import BoxActor
|
|
22
|
+
from .brownian_motion import BrownianMotionActor
|
|
23
|
+
from .coalescence import CoalescenceDropletActor
|
|
24
|
+
from .emitters import EmittersActor
|
|
25
|
+
from .fields import (
|
|
26
|
+
CollectionPDEActor,
|
|
27
|
+
DiffusionActor,
|
|
28
|
+
LocalReactionsActor,
|
|
29
|
+
ReactionDiffusionActor,
|
|
30
|
+
ScalarPDEActor,
|
|
31
|
+
)
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
"""
|
|
2
|
+
.. codeauthor:: David Zwicker <david.zwicker@ds.mpg.de>
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
from __future__ import annotations
|
|
6
|
+
|
|
7
|
+
from collections.abc import Callable
|
|
8
|
+
|
|
9
|
+
import numba as nb
|
|
10
|
+
import numpy as np
|
|
11
|
+
|
|
12
|
+
from pde.backends.numba.utils import jit
|
|
13
|
+
|
|
14
|
+
from ... import Parameter
|
|
15
|
+
from ...elements import ArrowsElement
|
|
16
|
+
from ..base import ActorBase, ElementsType
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class ActiveParticleActor(ActorBase):
|
|
20
|
+
"""Actor moving arrows according to their direction."""
|
|
21
|
+
|
|
22
|
+
parameters_default = [
|
|
23
|
+
Parameter(
|
|
24
|
+
"rotational_diffusion", 0.0, float, "The rotational diffusion strength"
|
|
25
|
+
)
|
|
26
|
+
]
|
|
27
|
+
|
|
28
|
+
element_classes = (ArrowsElement,)
|
|
29
|
+
|
|
30
|
+
def estimate_dt(self, elements: ElementsType) -> float:
|
|
31
|
+
"""Estimate the maximal time step for simulating this actor.
|
|
32
|
+
|
|
33
|
+
Args:
|
|
34
|
+
elements (tuple of :class:`~emulsim.elements.points.ArrowsElement`):
|
|
35
|
+
The element that is affected by the directed motion
|
|
36
|
+
|
|
37
|
+
Returns:
|
|
38
|
+
float: the maximal time step
|
|
39
|
+
"""
|
|
40
|
+
return float("inf")
|
|
41
|
+
|
|
42
|
+
def make_evolver_numba( # type: ignore
|
|
43
|
+
self, elements: ElementsType
|
|
44
|
+
) -> Callable[[tuple[np.ndarray], float, float], None]:
|
|
45
|
+
"""Return a function evolve the field state from time `t` to `t + dt`
|
|
46
|
+
|
|
47
|
+
Args:
|
|
48
|
+
elements (tuple of :class:`~emulsim.elements.points.ArrowsElement`):
|
|
49
|
+
The element that is affected by the directed motion
|
|
50
|
+
|
|
51
|
+
Returns:
|
|
52
|
+
callable: A function with signature
|
|
53
|
+
(state_data: :class:`~numpy.ndarray`, t: float, dt: float),
|
|
54
|
+
evolving `state_data`
|
|
55
|
+
"""
|
|
56
|
+
dim = int(elements[0].dim) # type: ignore
|
|
57
|
+
|
|
58
|
+
rot_diff = float(self.parameters["rotational_diffusion"])
|
|
59
|
+
if rot_diff > 0 and dim > 2:
|
|
60
|
+
raise NotImplementedError
|
|
61
|
+
|
|
62
|
+
@jit
|
|
63
|
+
def evolver(state_data: tuple[np.ndarray], t: float, dt: float) -> None:
|
|
64
|
+
"""Evolve all points explicitly."""
|
|
65
|
+
points = state_data[0]
|
|
66
|
+
|
|
67
|
+
for i in nb.prange(len(state_data[0])):
|
|
68
|
+
# update the position
|
|
69
|
+
for j in range(dim):
|
|
70
|
+
points[i].position[j] += dt * points[i].direction[j]
|
|
71
|
+
|
|
72
|
+
# apply rotational diffusion if requested
|
|
73
|
+
if rot_diff > 0:
|
|
74
|
+
if dim == 1:
|
|
75
|
+
# interpret rot_diff as rate of flipping
|
|
76
|
+
if np.random.rand() < dt * rot_diff:
|
|
77
|
+
points[i].direction[:] *= -1.0
|
|
78
|
+
|
|
79
|
+
elif dim == 2:
|
|
80
|
+
# rotate by angle chosen from normal distribution
|
|
81
|
+
φ = np.random.normal(0, dt * rot_diff)
|
|
82
|
+
cosφ, sinφ = np.cos(φ), np.sin(φ)
|
|
83
|
+
dx, dy = points[i].direction
|
|
84
|
+
points[i].direction[0] = cosφ * dx - sinφ * dy
|
|
85
|
+
points[i].direction[1] = sinφ * dx + cosφ * dy
|
|
86
|
+
|
|
87
|
+
else:
|
|
88
|
+
# higher dimensions are not currently supported
|
|
89
|
+
raise NotImplementedError
|
|
90
|
+
|
|
91
|
+
return evolver # type: ignore
|
|
92
|
+
|
|
93
|
+
def evolve(self, elements: ElementsType, t: float, dt: float) -> None:
|
|
94
|
+
"""Evolve the field state from time `t` to `t + dt`
|
|
95
|
+
|
|
96
|
+
Args:
|
|
97
|
+
elements (tuple of :class:`~emulsim.elements.points.ArrowsElement`):
|
|
98
|
+
The element that is affected by the directed motion
|
|
99
|
+
t (float):
|
|
100
|
+
The current time point
|
|
101
|
+
dt (float):
|
|
102
|
+
The time step
|
|
103
|
+
"""
|
|
104
|
+
(points,) = elements # extract single element
|
|
105
|
+
|
|
106
|
+
# update the position
|
|
107
|
+
points.positions += dt * points.directions # type: ignore
|
|
108
|
+
|
|
109
|
+
# apply rotational diffusion if requested
|
|
110
|
+
rot_diff = self.parameters["rotational_diffusion"]
|
|
111
|
+
if rot_diff > 0:
|
|
112
|
+
if points.dim == 1:
|
|
113
|
+
# interpret rot_diff as rate of flipping
|
|
114
|
+
flip = np.random.rand(len(points.data)) < dt * rot_diff
|
|
115
|
+
points.directions[flip] *= -1 # type: ignore
|
|
116
|
+
|
|
117
|
+
elif points.dim == 2:
|
|
118
|
+
# rotate by angle chosen from normal distribution
|
|
119
|
+
φ = np.random.normal(0, dt * rot_diff, size=len(points.data))
|
|
120
|
+
rot_mat = np.array([[np.cos(φ), -np.sin(φ)], [np.sin(φ), np.cos(φ)]])
|
|
121
|
+
new_direction = np.einsum("pi,ijp->pj", points.directions, rot_mat) # type: ignore
|
|
122
|
+
points.directions[:] = new_direction # type: ignore
|
|
123
|
+
|
|
124
|
+
else:
|
|
125
|
+
raise NotImplementedError(
|
|
126
|
+
"Rotational diffusion is not implemented for points with "
|
|
127
|
+
f"{points.dim} dimensions."
|
|
128
|
+
)
|
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
"""
|
|
2
|
+
.. codeauthor:: David Zwicker <david.zwicker@ds.mpg.de>
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
from __future__ import annotations
|
|
6
|
+
|
|
7
|
+
from collections.abc import Callable
|
|
8
|
+
from typing import Any
|
|
9
|
+
|
|
10
|
+
import numpy as np
|
|
11
|
+
|
|
12
|
+
from pde.backends.numba.utils import jit
|
|
13
|
+
from pde.grids.cartesian import CartesianGrid
|
|
14
|
+
|
|
15
|
+
from ... import Parameter
|
|
16
|
+
from ...elements import ArrowsElement, PointsElement, SphericalDropletsElement
|
|
17
|
+
from ..base import ActorBase, ElementsType
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class BoxActor(ActorBase):
|
|
21
|
+
"""Actor containing particles in a box."""
|
|
22
|
+
|
|
23
|
+
parameters_default = [
|
|
24
|
+
Parameter("bounds", [], np.array, "The bounds of the box"),
|
|
25
|
+
Parameter("periodic", False, np.array, "The bounds of the box"),
|
|
26
|
+
Parameter(
|
|
27
|
+
"point_like",
|
|
28
|
+
True,
|
|
29
|
+
bool,
|
|
30
|
+
"When False, the radius of the object is used in the distance calculation",
|
|
31
|
+
),
|
|
32
|
+
]
|
|
33
|
+
|
|
34
|
+
element_classes = ((PointsElement, ArrowsElement, SphericalDropletsElement),)
|
|
35
|
+
|
|
36
|
+
def __init__(self, parameters: dict[str, Any] | None = None):
|
|
37
|
+
"""
|
|
38
|
+
Args:
|
|
39
|
+
parameters (dict):
|
|
40
|
+
Parameters affecting the actor. Call
|
|
41
|
+
:meth:`~BoxActor.show_parameters` for details.
|
|
42
|
+
"""
|
|
43
|
+
super().__init__(parameters=parameters)
|
|
44
|
+
|
|
45
|
+
# convert periodicity information into useful format
|
|
46
|
+
periodic = self.parameters["periodic"]
|
|
47
|
+
if isinstance(periodic, np.ndarray) and periodic.size == 1:
|
|
48
|
+
periodic = periodic.item()
|
|
49
|
+
|
|
50
|
+
self._grid = CartesianGrid(self.parameters["bounds"], 1, periodic)
|
|
51
|
+
|
|
52
|
+
@classmethod
|
|
53
|
+
def from_grid(cls, grid: CartesianGrid):
|
|
54
|
+
"""Create BoxActor from a Cartesian grid.
|
|
55
|
+
|
|
56
|
+
Args:
|
|
57
|
+
grid (:class:`pde.grids.cartesian.CartesianGrid`):
|
|
58
|
+
The Cartesian grid that defines the box
|
|
59
|
+
"""
|
|
60
|
+
return cls({"bounds": grid.axes_bounds, "periodic": grid.periodic})
|
|
61
|
+
|
|
62
|
+
def estimate_dt(self, elements: ElementsType) -> float:
|
|
63
|
+
"""Estimate the maximal time step for simulating this actor.
|
|
64
|
+
|
|
65
|
+
Args:
|
|
66
|
+
elements (tuple of :class:`~emulsim.elements.points.PointsElement`):
|
|
67
|
+
The element that is affected by the directed motion
|
|
68
|
+
|
|
69
|
+
Returns:
|
|
70
|
+
float: the maximal time step
|
|
71
|
+
"""
|
|
72
|
+
return float("inf")
|
|
73
|
+
|
|
74
|
+
def make_evolver_numba( # type: ignore
|
|
75
|
+
self, elements: ElementsType
|
|
76
|
+
) -> Callable[[tuple[np.ndarray], float, float], None]:
|
|
77
|
+
"""Return a function evolve the field state from time `t` to `t + dt`
|
|
78
|
+
|
|
79
|
+
Args:
|
|
80
|
+
elements (tuple of :class:`~emulsim.elements.points.PointsElement`):
|
|
81
|
+
The element that is affected by this actor
|
|
82
|
+
|
|
83
|
+
Returns:
|
|
84
|
+
callable: A function with signature
|
|
85
|
+
(state_data: :class:`~numpy.ndarray`, t: float, dt: float),
|
|
86
|
+
evolving `state_data`
|
|
87
|
+
"""
|
|
88
|
+
(points_element,) = elements # extract single element
|
|
89
|
+
|
|
90
|
+
num_points = len(points_element.data)
|
|
91
|
+
num_axes = self._grid.num_axes
|
|
92
|
+
periodic = np.array(self._grid.periodic) # using a tuple led to a numba error
|
|
93
|
+
bounds = np.array(self._grid.axes_bounds)
|
|
94
|
+
midpoint = self._grid.cuboid.centroid
|
|
95
|
+
xmin = bounds[:, 0]
|
|
96
|
+
xmax = bounds[:, 1]
|
|
97
|
+
size = bounds[:, 1] - bounds[:, 0]
|
|
98
|
+
|
|
99
|
+
# figure out which axes need to be considered for flipping direction
|
|
100
|
+
if "direction" in points_element.data.dtype.fields:
|
|
101
|
+
flip_ax: np.ndarray = np.flatnonzero(np.logical_not(self._grid.periodic))
|
|
102
|
+
else:
|
|
103
|
+
flip_ax = np.empty((0,))
|
|
104
|
+
test_for_flipping = flip_ax.size > 0
|
|
105
|
+
|
|
106
|
+
point_like = self.parameters["point_like"]
|
|
107
|
+
|
|
108
|
+
@jit
|
|
109
|
+
def evolver(state_data: tuple[np.ndarray], t: float, dt: float) -> None:
|
|
110
|
+
"""Evolve all points explicitly."""
|
|
111
|
+
points = state_data[0] # data of the points
|
|
112
|
+
for i in range(num_points):
|
|
113
|
+
pos = points[i].position
|
|
114
|
+
|
|
115
|
+
if point_like:
|
|
116
|
+
radius = 0
|
|
117
|
+
else:
|
|
118
|
+
radius = points[i].radius
|
|
119
|
+
|
|
120
|
+
# flip direction if out of bound
|
|
121
|
+
if test_for_flipping:
|
|
122
|
+
for ax in flip_ax:
|
|
123
|
+
dist_norm = (pos[ax] - midpoint[ax]) / (size[ax] - 2 * radius)
|
|
124
|
+
if (dist_norm - 0.5) % 2 - 1 < 0:
|
|
125
|
+
points[i].direction[ax] *= -1
|
|
126
|
+
# TODO: this function's performance could be improved by calculating
|
|
127
|
+
# the distance only once
|
|
128
|
+
|
|
129
|
+
# move the points to inside the box
|
|
130
|
+
for ax in range(num_axes):
|
|
131
|
+
if periodic[ax]:
|
|
132
|
+
pos[ax] = (pos[ax] - xmin[ax]) % size[ax] + xmin[ax]
|
|
133
|
+
else:
|
|
134
|
+
dist_left = pos[ax] - (xmax[ax] - radius)
|
|
135
|
+
size_red = size[ax] - 2 * radius
|
|
136
|
+
arg = (dist_left) % (2 * size_red) - size_red
|
|
137
|
+
pos[ax] = xmin[ax] + radius + abs(arg)
|
|
138
|
+
|
|
139
|
+
return evolver # type: ignore
|
|
140
|
+
|
|
141
|
+
def evolve(self, elements: ElementsType, t: float, dt: float) -> None:
|
|
142
|
+
"""Evolve the field state from time `t` to `t + dt`
|
|
143
|
+
|
|
144
|
+
Args:
|
|
145
|
+
elements (tuple of :class:`~emulsim.elements.points.PointsElement`):
|
|
146
|
+
The element that is affected by this actor
|
|
147
|
+
t (float):
|
|
148
|
+
The current time point
|
|
149
|
+
dt (float):
|
|
150
|
+
The time step
|
|
151
|
+
"""
|
|
152
|
+
if not self.parameters["point_like"]:
|
|
153
|
+
raise NotImplementedError("numpy backend can only deal with point-objects")
|
|
154
|
+
|
|
155
|
+
(points,) = elements # extract single element
|
|
156
|
+
|
|
157
|
+
if "direction" in points.data.dtype.fields:
|
|
158
|
+
# flip direction if out of bound
|
|
159
|
+
midpoint = self._grid.cuboid.centroid
|
|
160
|
+
size = np.array(self._grid.cuboid.size)
|
|
161
|
+
if not self.parameters["point_like"]:
|
|
162
|
+
size = size[:, np.newaxis] - 2 * points.radius[np.newaxis, :] # type: ignore
|
|
163
|
+
|
|
164
|
+
for ax in range(points.dim): # type: ignore
|
|
165
|
+
if self._grid.periodic[ax]:
|
|
166
|
+
continue # do nothing for periodic axes
|
|
167
|
+
dist_norm = (points.positions[..., ax] - midpoint[ax]) / size[ax] # type: ignore
|
|
168
|
+
factor = np.sign((dist_norm - 0.5) % 2 - 1)
|
|
169
|
+
factor[factor == 0] = 1 # don't flip corner cases
|
|
170
|
+
points.directions[..., ax] *= factor # type: ignore
|
|
171
|
+
|
|
172
|
+
# move the points to inside the box
|
|
173
|
+
# TODO: support extended objects, where `point_like` is False
|
|
174
|
+
points.positions[...] = self._grid.normalize_point( # type: ignore
|
|
175
|
+
points.positions, # type: ignore
|
|
176
|
+
reflect=True,
|
|
177
|
+
)
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
"""
|
|
2
|
+
.. codeauthor:: David Zwicker <david.zwicker@ds.mpg.de>
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
from __future__ import annotations
|
|
6
|
+
|
|
7
|
+
from collections.abc import Callable
|
|
8
|
+
|
|
9
|
+
import numpy as np
|
|
10
|
+
|
|
11
|
+
from pde.backends.numba.utils import jit
|
|
12
|
+
from pde.tools.expressions import ScalarExpression
|
|
13
|
+
|
|
14
|
+
from ... import Parameter
|
|
15
|
+
from ...elements import ArrowsElement, PointsElement, SphericalDropletsElement
|
|
16
|
+
from ..base import ActorBase, ElementsType
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class BrownianMotionActor(ActorBase):
|
|
20
|
+
"""Actor moving objects according to Brownian motion."""
|
|
21
|
+
|
|
22
|
+
parameters_default = [
|
|
23
|
+
Parameter(
|
|
24
|
+
"diffusivity",
|
|
25
|
+
"1",
|
|
26
|
+
str,
|
|
27
|
+
"Expression that determines the strength of the Brownian motion of "
|
|
28
|
+
"droplets. The expression may depend on time and potentially on a radius "
|
|
29
|
+
"if this is defined for the element on which the actor acts",
|
|
30
|
+
),
|
|
31
|
+
]
|
|
32
|
+
|
|
33
|
+
element_classes = ((ArrowsElement, PointsElement, SphericalDropletsElement),)
|
|
34
|
+
|
|
35
|
+
def estimate_dt(self, elements: ElementsType) -> float:
|
|
36
|
+
"""Estimate the maximal time step for simulating this actor.
|
|
37
|
+
|
|
38
|
+
Args:
|
|
39
|
+
elements (tuple of :class:`~emulsim.elements.droplets.SphericalDropletsElement`):
|
|
40
|
+
The element that is affected by the Brownian motion
|
|
41
|
+
|
|
42
|
+
Returns:
|
|
43
|
+
float: the maximal time step
|
|
44
|
+
"""
|
|
45
|
+
return float("inf")
|
|
46
|
+
|
|
47
|
+
def _update_cache(self, elements: ElementsType) -> None:
|
|
48
|
+
"""Prepare the simulation doing pre-calculations.
|
|
49
|
+
|
|
50
|
+
Args:
|
|
51
|
+
elements (tuple):
|
|
52
|
+
The state of all the droplets and of the field
|
|
53
|
+
"""
|
|
54
|
+
fields = elements[0].data.dtype.fields
|
|
55
|
+
if "position" not in fields:
|
|
56
|
+
raise ValueError("Could not find field `positions` in element data")
|
|
57
|
+
self._cache["has_radius"] = "radius" in fields
|
|
58
|
+
if self._cache["has_radius"]:
|
|
59
|
+
self._cache["diffusivity"] = ScalarExpression(
|
|
60
|
+
self.parameters["diffusivity"], [["radius", "R"], ["time", "t"]]
|
|
61
|
+
)
|
|
62
|
+
else:
|
|
63
|
+
self._cache["diffusivity"] = ScalarExpression(
|
|
64
|
+
self.parameters["diffusivity"], [["time", "t"]]
|
|
65
|
+
)
|
|
66
|
+
|
|
67
|
+
def make_evolver_numba( # type: ignore
|
|
68
|
+
self, elements: ElementsType
|
|
69
|
+
) -> Callable[[tuple[np.ndarray], float, float], None]:
|
|
70
|
+
"""Return a function evolve the field state from time `t` to `t + dt`
|
|
71
|
+
|
|
72
|
+
Args:
|
|
73
|
+
elements (tuple of :class:`~emulsim.elements.droplets.SphericalDropletsElement`):
|
|
74
|
+
The field element that is affected by the Brownian motion
|
|
75
|
+
|
|
76
|
+
Returns:
|
|
77
|
+
callable: A function with signature
|
|
78
|
+
(state_data: :class:`~numpy.ndarray`, t: float, dt: float),
|
|
79
|
+
evolving `state_data`
|
|
80
|
+
"""
|
|
81
|
+
self._check_cache(elements)
|
|
82
|
+
diffusivity = self._cache["diffusivity"].get_function(backend="numba")
|
|
83
|
+
dim = int(elements[0].dim) # type: ignore
|
|
84
|
+
|
|
85
|
+
if self._cache["has_radius"]:
|
|
86
|
+
|
|
87
|
+
@jit
|
|
88
|
+
def evolver(state_data: tuple[np.ndarray], t: float, dt: float):
|
|
89
|
+
"""Evolve all points explicitly."""
|
|
90
|
+
(droplets_data,) = state_data
|
|
91
|
+
for droplet_data in droplets_data:
|
|
92
|
+
if droplet_data.radius > 0:
|
|
93
|
+
scale = np.sqrt(dt * diffusivity(droplet_data.radius, t))
|
|
94
|
+
for i in range(dim):
|
|
95
|
+
droplet_data.position[i] += scale * np.random.randn()
|
|
96
|
+
|
|
97
|
+
else:
|
|
98
|
+
|
|
99
|
+
@jit
|
|
100
|
+
def evolver(state_data: tuple[np.ndarray], t: float, dt: float):
|
|
101
|
+
"""Evolve all points explicitly."""
|
|
102
|
+
(droplets_data,) = state_data
|
|
103
|
+
scale = np.sqrt(dt * diffusivity(t))
|
|
104
|
+
for droplet_data in droplets_data:
|
|
105
|
+
for i in range(dim):
|
|
106
|
+
droplet_data.position[i] += scale * np.random.randn()
|
|
107
|
+
|
|
108
|
+
return evolver # type: ignore
|
|
109
|
+
|
|
110
|
+
def evolve(self, elements: ElementsType, t: float, dt: float) -> None:
|
|
111
|
+
"""Evolve the state from time `t` to `t + dt`
|
|
112
|
+
|
|
113
|
+
Args:
|
|
114
|
+
elements (tuple of :class:`~emulsim.elements.droplets.SphericalDropletsElement`):
|
|
115
|
+
The element that is affected by the Brownian motion
|
|
116
|
+
t (float):
|
|
117
|
+
The current time point
|
|
118
|
+
dt (float):
|
|
119
|
+
The time step
|
|
120
|
+
"""
|
|
121
|
+
self._check_cache(elements)
|
|
122
|
+
(objs,) = elements # extract single element
|
|
123
|
+
diffusivity = self._cache["diffusivity"]
|
|
124
|
+
dim = objs.dim
|
|
125
|
+
if dim is None:
|
|
126
|
+
raise ValueError("`dim` must not be None")
|
|
127
|
+
|
|
128
|
+
if self._cache["has_radius"]:
|
|
129
|
+
for droplet in objs.droplets: # type: ignore
|
|
130
|
+
if droplet.radius > 0:
|
|
131
|
+
scale = np.sqrt(dt * diffusivity(droplet.radius, t))
|
|
132
|
+
droplet.position += scale * np.random.randn(dim)
|
|
133
|
+
else:
|
|
134
|
+
scale = np.sqrt(dt * diffusivity(t))
|
|
135
|
+
objs.positions[...] += scale * np.random.randn(len(objs), objs.dim) # type: ignore
|