Skip to content

Commit 521faf3

Browse files
committed
Reworked the integration of a system
And made WaterMolecule2DExample a "complete" scene
1 parent ee1510b commit 521faf3

File tree

8 files changed

+196
-111
lines changed

8 files changed

+196
-111
lines changed

example_scenes.py

Lines changed: 27 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -906,9 +906,12 @@ def construct(self) -> None:
906906
[3., -1., 0.], # position of mass1
907907
[0., 2., 0.], # position of mass2
908908
[-3., -1., 0.], # position of mass3
909-
[0.5, 0., 0.], # velocity of mass1
910-
[0., 0.05, 0.], # velocity of mass2
911-
[-0.2, -0.2, 0.], # velocity of mass3
909+
# [0.5, 0., 0.], # velocity of mass1
910+
[0., 0., 0.], # velocity of mass1
911+
# [0., 0.05, 0.], # velocity of mass2
912+
[0., 0., 0.], # velocity of mass2
913+
# [-0.2, -0.2, 0.], # velocity of mass3
914+
[0., 0., 0.], # velocity of mass3
912915
]
913916
)
914917
masses: np.ndarray = np.array(
@@ -975,7 +978,7 @@ def construct(self) -> None:
975978
HarmonicBondForce(
976979
(body1, body2),
977980
mobjects=(line,),
978-
k=0.15,
981+
k=0.3,
979982
r0=2.9
980983
)
981984
)
@@ -1014,19 +1017,26 @@ def construct(self) -> None:
10141017
)
10151018
)
10161019
# LangevinHeatBathForce(s)
1017-
# T: float = 0.1
1018-
# kb: float = 0.1
1019-
# global_seed: int = 1
1020-
# generator: np.random.Generator = np.random.default_rng(seed=global_seed)
1021-
# for body in bodies:
1022-
# forces.append(
1023-
# LangevinHeatBathForce(
1024-
# (body,),
1025-
# seed=generator.integers(0, 9999),
1026-
# kb=kb,
1027-
# T=T
1028-
# )
1029-
# )
1020+
temp: float = 20.0
1021+
global_seed: int = 4
1022+
max_magnitude: float = 60.0
1023+
generator: np.random.Generator = np.random.default_rng(seed=global_seed)
1024+
for body in bodies:
1025+
forces.append(
1026+
LangevinHeatBathForce(
1027+
(body,),
1028+
seed=generator.integers(0, 9999),
1029+
T=temp,
1030+
max_magnitude=max_magnitude
1031+
)
1032+
)
1033+
# LangevinFrictionForce(s)
1034+
for body in bodies:
1035+
forces.append(
1036+
LangevinFrictionForce(
1037+
(body,)
1038+
)
1039+
)
10301040
# Create system
10311041
system: PhysicalSystem = PhysicalSystem(bodies, forces)
10321042
# Make sure the system mobjects are positioned correctly

manimlib/__init__.py

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -53,9 +53,10 @@
5353
from manimlib.mobject.value_tracker import *
5454
from manimlib.mobject.vector_field import *
5555

56-
from manimlib.physics.physical_system import *
57-
from manimlib.physics.force import *
5856
from manimlib.physics.body import *
57+
from manimlib.physics.force import *
58+
from manimlib.physics.integrator import *
59+
from manimlib.physics.physical_system import *
5960

6061
from manimlib.scene.interactive_scene import *
6162
from manimlib.scene.scene import *
@@ -70,9 +71,9 @@
7071
from manimlib.utils.file_ops import *
7172
from manimlib.utils.images import *
7273
from manimlib.utils.iterables import *
73-
from manimlib.utils.math import *
7474
from manimlib.utils.paths import *
7575
from manimlib.utils.rate_functions import *
7676
from manimlib.utils.simple_functions import *
7777
from manimlib.utils.sounds import *
7878
from manimlib.utils.space_ops import *
79+
from manimlib.utils.trigonometry import *

manimlib/animation/physics.py

Lines changed: 19 additions & 83 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,14 @@
11
from __future__ import annotations
22

33
import numpy as np
4-
from scipy.integrate import odeint
54

65
from typing import TYPE_CHECKING
76

87
from manimlib.mobject.mobject import Mobject
98
from manimlib.scene.scene import Scene
109
from manimlib.animation.animation import Animation
1110
from manimlib.physics.physical_system import PhysicalSystem
11+
from manimlib.physics.integrator import symplectic_euler
1212
from manimlib.constants import DIMENSIONS, DEFAULT_FPS
1313

1414
if TYPE_CHECKING:
@@ -26,13 +26,11 @@ def __init__(
2626
mobject: PhysicalSystem,
2727
integrator: Callable[
2828
[
29-
Callable[[np.ndarray, float, PhysicalSystem], np.ndarray],
30-
np.ndarray,
31-
np.ndarray,
32-
tuple
29+
PhysicalSystem,
30+
np.ndarray
3331
],
34-
np.ndarray
35-
]=odeint,
32+
tuple[np.ndarray, np.ndarray]
33+
]=symplectic_euler,
3634
t: np.ndarray=np.linspace(0, 10, DEFAULT_FPS*100),
3735
scene: Scene=None,
3836
background_mobjects: tuple[Mobject]=(),
@@ -44,26 +42,18 @@ def __init__(
4442
4543
Keyword arguments
4644
-----------------
47-
integrator (Callable[[], np.ndarray]):
4845
mobject (PhysicalSystem): the PhysicalSystem mobject
4946
integrator (Callable[
5047
[
51-
Callable[[np.ndarray, float, PhysicalSystem], np.ndarray],
52-
np.ndarray,
53-
np.ndarray,
54-
tuple
48+
PhysicalSystem,
49+
np.ndarray
5550
],
56-
np.ndarray
57-
]): function in terms of (dydt, y0, t, args) returning a matrix where each row
58-
is the state at every point in the t vector.
59-
dydt (provided by this class) is a function in terms of the state y0 at a
60-
certain time and the physical system (coming from args[0]), returning the
61-
derivative (vector) of the state with respect to time.
62-
y0 is the initial state (vector).
63-
t is the vector of time-points in which to get the state.
64-
args is a tuple containing any extra arguments dydt may need.
65-
For an example of the 'integrator' function, see odeint in scipy.integrate, which
66-
is the default value.
51+
tuple[np.ndarray, np.ndarray]
52+
]): function in terms of (system, t) returning two tensors (pos, vel) where
53+
pos[i,j,k] represents the component along the k-th axis of the position of
54+
body j at time-point i (same holds por vel).
55+
For an example of the 'integrator' function, see symplectic_euler in
56+
manimlib.physics.integrator, which is the default value.
6757
t (np.ndarray[N]): vector containing all time-points of integration, must be monotonic
6858
(default np.linspace(0, 10, DEFAULT_FPS*100))
6959
scene (Scene): scene is needed if we want to keep the body mobject, force mobject,
@@ -89,73 +79,19 @@ def __init__(
8979
self.scene: Scene = scene
9080
self.background_mobjects: tuple[Mobject] = background_mobjects
9181
self.foreground_mobjects: tuple[Mobject] = foreground_mobjects
92-
# Assemble initial state (positions and velocities)
93-
y0: np.ndarray = np.concatenate(
94-
[
95-
body.position for body in mobject.bodies
96-
] + [
97-
body.velocity for body in mobject.bodies
98-
]
99-
)
100-
# Integrate (single element tuples must have a comma at the end)
101-
self.y: np.ndarray = integrator(self.dydt, y0, t, (self.mobject,))
82+
# Integrate the system
83+
self.positions, self.velocities = integrator(self.mobject, t)
10284

10385
def interpolate_mobject(self, alpha: float) -> None:
10486
row_index: int = min(
105-
int(np.floor(alpha*self.y.shape[0])),
106-
self.y.shape[0]-1
87+
int(np.floor(alpha*self.positions.shape[0])),
88+
self.positions.shape[0]-1
10789
)
108-
state: np.ndarray = self.y[row_index,:]
109-
for i, body in enumerate(self.mobject.bodies):
110-
pos = state[i*DIMENSIONS:(i+1)*DIMENSIONS]
111-
vel = state[(i+self.n_bodies)*DIMENSIONS:(i+self.n_bodies+1)*DIMENSIONS]
112-
body.set_velocity(vel)
113-
body.set_position(pos, update_mobject_position=False) # Will update mobject later
90+
self.mobject.update_positions(self.positions[row_index])
91+
self.mobject.update_velocities(self.velocities[row_index])
11492
# Update mobjects in the system
11593
self.mobject.update_mobjects(
11694
self.scene,
11795
self.background_mobjects,
11896
self.foreground_mobjects
11997
)
120-
121-
@staticmethod
122-
def dydt(
123-
y: np.ndarray,
124-
t: float,
125-
system: PhysicalSystem
126-
) -> np.ndarray:
127-
"""
128-
Compute the derivative of the state a physical system
129-
130-
Keyword arguments
131-
-----------------
132-
y (np.ndarray[nbodies*2*DIMENSIONS]): state containing
133-
the position of each body and then its velocity
134-
t (float): current time
135-
system (PhysicalSystem): the physical system instance
136-
137-
Returns
138-
-----------------
139-
The derivative of the state vector
140-
(np.ndarray[nbodies*2*DIMENSIONS]) containing the
141-
velocities and then accelerations
142-
"""
143-
n_bodies: int = system.get_n_bodies()
144-
masses: np.ndarray = system.get_masses()
145-
# Split info from state vector
146-
velocities: np.ndarray = y[DIMENSIONS*n_bodies:].reshape((n_bodies, DIMENSIONS))
147-
# Reshape positions so that every row contains the location of that mass
148-
positions: np.ndarray = y[:DIMENSIONS*n_bodies].reshape((n_bodies, DIMENSIONS))
149-
# Update the positions and velocities of the bodies in the system
150-
system.update_positions(positions)
151-
system.update_velocities(velocities)
152-
# Create structure for forces
153-
forces: np.ndarray = np.zeros((n_bodies, DIMENSIONS))
154-
# Apply the forces in the system
155-
for force in system.forces:
156-
force.apply(forces)
157-
print(f"{forces}\n")
158-
# Get the accelerations (a=F/m)
159-
accelerations: np.ndarray = forces/masses.reshape((n_bodies, 1))
160-
# Return derivative of the state
161-
return np.concatenate((velocities.flatten(), accelerations.flatten()))

manimlib/physics/force.py

Lines changed: 17 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,8 @@
77
from typing import Union
88
from manimlib.constants import DIMENSIONS, KB, PI, ROOM_TEMPERATURE, TAU, EPS0, NO_SEED
99
from manimlib.mobject.mobject import Mobject
10-
from manimlib.utils.math import arctan2
10+
from manimlib.utils.simple_functions import cap_magnitude
11+
from manimlib.utils.trigonometry import arctan2
1112

1213
from manimlib.physics.body import Body
1314
from manimlib.mobject.geometry import Line, Arc
@@ -319,8 +320,9 @@ def __init__(
319320
tridimensional: bool=False,
320321
seed: int=NO_SEED,
321322
gamma: float=1.0,
322-
kb: float=KB,
323-
T: float=ROOM_TEMPERATURE
323+
kb: float=1.0,
324+
T: float=1.0,
325+
max_magnitude: float=None,
324326
) -> None:
325327
"""
326328
Initialize a new LangevinHeatBathForce object
@@ -337,14 +339,17 @@ def __init__(
337339
seed (int): seed for the random number generator. If -1, then no seed is
338340
passed to the generator, yielding non-reproducible results (default: -1)
339341
gamma (float): friction coefficient (default: 1.0)
340-
kb (float): Boltzmann constant (default: 1.380649e-23)
341-
T (float): temperature (default: 293.0)
342+
kb (float): Boltzmann constant (default: 1.0)
343+
T (float): temperature (default: 1.0)
344+
max_magnitude (float): maximum magnitude allowed for a generated random force
345+
(default: None, disabled)
342346
"""
343347
self.tridimensional: bool = tridimensional
344348
self.generator: np.random.Generator = np.random.default_rng(seed=None if seed==NO_SEED else seed)
345349
self.gamma: float = gamma
346350
self.kb: float = kb
347351
self.T: float = T
352+
self.max_magnitude: float = max_magnitude
348353
super().__init__(bodies, mobjects)
349354

350355
def __str__(self) -> str:
@@ -360,7 +365,13 @@ def apply(self, forces: np.ndarray) -> None:
360365
deviation: float = np.sqrt(2*body.mass*self.gamma*self.kb*self.T)
361366
force: np.ndarray = self.generator.normal(scale=deviation, size=DIMENSIONS)
362367
if not self.tridimensional:
363-
force[2] == 0.0
368+
force[2] = 0.
369+
if self.max_magnitude is not None and cap_magnitude(force, self.max_magnitude):
370+
print(
371+
f"({self.__class__.__name__}) WARNING: "
372+
f"force was capped to maximum "
373+
f"magnitude ({self.max_magnitude})"
374+
)
364375
forces[body.index] += force
365376

366377

manimlib/physics/integrator.py

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
from __future__ import annotations
2+
3+
import numpy as np
4+
from manimlib.constants import DIMENSIONS
5+
6+
from manimlib.physics.physical_system import PhysicalSystem
7+
8+
9+
def symplectic_euler(system: PhysicalSystem, t: np.ndarray) -> tuple[np.ndarray, np.ndarray]:
10+
"""
11+
Perform symplectic euler integration of the physical system
12+
13+
Keyword arguments
14+
-----------------
15+
system (PhysicalSystem): the system to integrate
16+
t (np.ndarray[N]): vector containing all time-points of integration, must be monotonic
17+
18+
Returns
19+
-----------------
20+
positions (np.ndarray[N, n_bodies, DIMENSIONS]): the position of each body for every
21+
time-point in t
22+
velocities (np.ndarray[N, n_bodies, DIMENSIONS]): the velocity of each body for every
23+
time-point in t
24+
"""
25+
n_bodies: int = system.get_n_bodies()
26+
n_tpoints: int = t.shape[0]
27+
positions: np.ndarray = np.zeros((n_tpoints, n_bodies, DIMENSIONS))
28+
velocities: np.ndarray = np.zeros((n_tpoints, n_bodies, DIMENSIONS))
29+
# Set initial positions and velocities
30+
positions[0] = system.get_positions()
31+
velocities[0] = system.get_velocities()
32+
# Integrate
33+
for i in range(1, n_tpoints):
34+
# Compute dt (delta time)
35+
dt: float = t[i] - t[i-1]
36+
# Compute accelerations
37+
system.update_positions(positions[i-1])
38+
system.update_velocities(velocities[i-1])
39+
accelerations: np.ndarray = system.compute_accelerations()
40+
# Record new velocities
41+
velocities[i] = velocities[i-1] + dt * accelerations
42+
# Record new positions
43+
positions[i] = positions[i-1] + dt * velocities[i]
44+
# Set the initial state back in the system
45+
system.update_positions(positions[0])
46+
system.update_velocities(velocities[0])
47+
# Return
48+
return positions, velocities

0 commit comments

Comments
 (0)