Coupled Masses and Springs

Consider a 1D system of point bodies to which two springs are attached in series. The force acting on each body is simply:

\[F_i = k_{i, l} \left[L_{i, l} - (x_i - x_{i - 1})\right] - k_{i, r} \left[L_{i, r} - (x_{i + 1} - x_i)\right]\]

Therefore, a reasonable approach in building a calculation is implementing the following equations:

\[\begin{split} \begin{align} \frac{dx}{dt} &= v \\ \frac{dv}{dt} &= a \\ F &= ma \end{align}\end{split}\]
[1]:
import matplotlib.pyplot as plt
import numpy as np
from matplotlib import rc

rc("font", family="serif", size=15)
rc("savefig", dpi=600)
rc("figure", figsize=(8, 6))
rc("mathtext", fontset="dejavuserif")
rc("lines", linewidth=3)

from networkx import DiGraph

from stream import Aggregator, Calculation, unpacked
from stream.aggregator import vars_
[2]:
class Mass(Calculation):
    def __init__(self, m, k_right, k_left, L_right, L_left, name):
        self.name = name
        self.m = m
        self.k_left, self.k_right = k_left, k_right
        self.L_left, self.L_right = L_left, L_right

    @property
    def mass_vector(self):
        return True, True, False

    @unpacked
    def calculate(self, variables, *, x_left, x_right):
        x, v, a = variables
        left_spring = -self.k_left * (self.L_left - (x - x_left))
        right_spring = -self.k_right * (self.L_right - (x_right - x))
        total_force = -left_spring + right_spring
        return self.load(dict(x=v, v=a, a=total_force - a * self.m))

    def indices(self, variable, asking=None):
        try:
            return super().indices(variable)
        except KeyError:
            pass
        return dict(x_left=0, x_right=1)[variable]

    @property
    def variables(self):
        return dict(x=0, v=1, a=2)
[3]:
M1 = Mass(name="M1", m=1.5, k_left=1.0, k_right=1.0, L_left=1.0, L_right=1.0)
M2 = Mass(name="M2", m=1.0, k_left=M1.k_right, k_right=1.0, L_left=M1.L_right, L_right=1.0)

agr = Aggregator(
    graph=DiGraph([(M1, M2, vars_("x_left")), (M2, M1, vars_("x_right"))]),
    funcs={M1: dict(x_left=0.0), M2: dict(x_right=3.0)},
)

from stream.analysis.report import report

report(agr)

Contains 6 Equations, 2 Nodes, 2 Edges.

Calculation

Type

Equation No.

Unset

Set Externally

Missing

M1

Mass

0 - 3

x_left

M2

Mass

3 - 6

x_right

[4]:
plt.figure(figsize=(8, 2))
agr.draw(
    node_options=dict(
        pos={M1: (0, 0), M2: (1, 0)},
        labels={M1: "M1", M2: "M2"},
        node_size=2000,
        font_size=20,
    ),
    edge_options=dict(label_pos=0.3, font_size=18),
)
plt.box()
_images/CoupledSprings_4_0.png
[5]:
y0 = np.array([1.2, 0.0, 0.0, 1.9, 0.0, 0.0])
time = np.linspace(0, 20.0, 400)

sol = agr.solve(y0=y0, time=time, yp0=agr.compute(y0, 0))
[6]:
plt.figure()
plt.plot(time, sol[:, 0] - 1, label="M1 position")
plt.plot(time, sol[:, 3] - 2, label="M2 position")
plt.grid()
_images/CoupledSprings_6_0.png
[7]:
plt.figure()
plt.plot(time, sol[:, 1], label="M1 velocity")
plt.plot(time, sol[:, 4], label="M2 velocity")
plt.grid()
_images/CoupledSprings_7_0.png
[8]:
plt.figure()
plt.plot(time, sol[:, 2] / M1.m, label="M1 force")
plt.plot(time, sol[:, 5] / M2.m, label="M2 force")
plt.grid()
_images/CoupledSprings_8_0.png