Skip to content

Shaken frictional spring-block chain

A chain of blocks joined by springs on a frictional substrate, shaken and relaxed in turn.

  • Name: shaken_spring_block
  • Field: physics/soft_matter
  • Formalism: stochastic differential equations
  • References: Bertin 2021; Edwards and Oakeshott 1989; Bertin, Dauchot, and Droz 2004

Factory

@register_model(
    field='physics/soft_matter',
    tags=('field', 'sde', 'friction', 'granular matter', 'blocked states'),
    references=('Bertin 2021', 'Edwards and Oakeshott 1989', 'Bertin, Dauchot, and Droz 2004'),
    defaults={'until': 200.0, 'dt': 10.0, 'solver': EulerMaruyamaSolver(dt=0.01)},
)
def shaken_spring_block(
    n_blocks: int = 50,
    *,
    k: float = 1.0,
    anchor: float = 0.0,
    mu_s: float = 1.0,
    mu_d: float = 0.8,
    amplitude: float = 2.0,
    shake_time: float = 2.0,
    relax_time: float = 8.0,
    v_stick: float = 0.02,
) -> System:
    """Return a chain of blocks joined by springs on a frictional substrate, shaken and relaxed in turn.

    Every block of unit weight feels the springs to its neighbours, `k (x_{i+1} + x_{i-1} - 2 x_i)`, an optional
    spring of stiffness `anchor` to its rest position, and Coulomb friction. A sliding block feels the dynamic friction
    `-mu_d sign(v)`, and a block at rest stays at rest while the spring force on it is below the static friction
    `mu_s`. In every cycle a random force of intensity `amplitude^2` shakes the blocks for `shake_time`, and then the
    chain relaxes for `relax_time` to a blocked configuration, where the elongations `xi_i = x_{i+1} - x_i` satisfy
    `k |xi_{i+1} - xi_i| < mu_s`. Repeated cycles sample blocked configurations, whose energy per spring grows with
    the shaking, and whose elongations are correlated over a length that grows with that energy. Without springs
    between blocks and with an anchor, the blocks are independent, and their blocked positions satisfy
    `|x_i| < mu_s / anchor`. A block counts as at rest below the speed `v_stick`, which must exceed the change of
    speed `(mu_s + mu_d) h` of a solver step `h`. The ends of the chain are free, and the blocks start at rest.

    Args:
        n_blocks:
            The number of blocks.

        k:
            The stiffness of the springs between neighbouring blocks.

        anchor:
            The stiffness of the springs that hold every block to its rest position.

        mu_s:
            The static friction coefficient.

        mu_d:
            The dynamic friction coefficient.

        amplitude:
            The amplitude of the random shaking force.

        shake_time:
            The duration of the shaking in every cycle.

        relax_time:
            The duration of the relaxation in every cycle.

        v_stick:
            The speed below which a block counts as at rest.

    Returns:
        system:
            The model, with the positions `chain.x` and velocities `chain.v` of the blocks.

    Examples:
        >>> from skcomplex.models.physics.soft_matter import shaken_spring_block
        >>> shaken_spring_block(5).elements[0].check().prepare().size_
        5
    """
    chain = FieldElement(
        'chain',
        element=FrictionalBlockElement,
        space=LatticeSpace(n_blocks, boundary='reflecting'),
        boundary={'x': 'zero-flux', 'v': 'zero-flux'},
        values={
            'x': np.zeros(n_blocks),
            'k': k,
            'anchor': anchor,
            'mu_s': mu_s,
            'mu_d': mu_d,
            'amplitude': amplitude,
            'shake_time': shake_time,
            'relax_time': relax_time,
            'v_stick': v_stick,
        },
    )
    return System(elements=[chain])

Components

class FrictionalBlockElement(Element):
    """Block of a chain on a rough floor, at the position `x` with the velocity `v`, joined to its neighbours.

    The springs of stiffness `k` and an anchor spring pull the block, Coulomb friction holds it while the force stays
    below `mu_s` and slows it by `mu_d` while it slides, and periodic shaking of amplitude `amplitude` kicks it during
    the first `shake_time` of every `shake_time + relax_time`.

    Args:
        label:
            The label of the block.

        visual:
            How the block is drawn, or `None` for none.

        x:
            The initial position.

        v:
            The initial velocity.

        m:
            The mass.

        k:
            The stiffness of the springs between neighbours.

        anchor:
            The stiffness of the spring to the rest position.

        mu_s:
            The static friction force.

        mu_d:
            The dynamic friction force.

        amplitude:
            The amplitude of the shaking force.

        shake_time:
            The duration of every shaking.

        relax_time:
            The time between shakings.

        v_stick:
            The speed below which the block can stick.

    Examples:
        >>> from skcomplex.models.physics.soft_matter import FrictionalBlockElement
        >>> FrictionalBlockElement('block', mu_s=2.0).mu_s
        2.0
    """

    def __init__(
        self,
        label: str,
        visual: VisualSpec | None = None,
        *,
        x: Annotated[float, StateSpec()] = 0.0,
        v: Annotated[float, StateSpec()] = 0.0,
        m: Annotated[float, ParameterSpec(domain=RealSet.open(0, oo))] = 1.0,
        k: Annotated[float, ParameterSpec(domain=RealSet.closed_open(0, oo))] = 1.0,
        anchor: Annotated[float, ParameterSpec(domain=RealSet.closed_open(0, oo))] = 0.0,
        mu_s: Annotated[float, ParameterSpec(domain=RealSet.closed_open(0, oo))] = 1.0,
        mu_d: Annotated[float, ParameterSpec(domain=RealSet.closed_open(0, oo))] = 0.8,
        amplitude: Annotated[float, ParameterSpec(domain=RealSet.closed_open(0, oo))] = 2.0,
        shake_time: Annotated[float, ParameterSpec(domain=RealSet.open(0, oo))] = 2.0,
        relax_time: Annotated[float, ParameterSpec(domain=RealSet.open(0, oo))] = 8.0,
        v_stick: Annotated[float, ParameterSpec(domain=RealSet.open(0, oo))] = 0.02,
    ) -> None:
        super().__init__(label, visual=visual)
        self.x = x
        self.v = v
        self.m = m
        self.k = k
        self.anchor = anchor
        self.mu_s = mu_s
        self.mu_d = mu_d
        self.amplitude = amplitude
        self.shake_time = shake_time
        self.relax_time = relax_time
        self.v_stick = v_stick

    def _get_laws(self, s: SimpleNamespace) -> list[BaseLaw]:
        """Return the springs, the Coulomb friction with sticking, and the shaking of the block.

        Args:
            s:
                The namespace of the symbols the laws can use.

        Returns:
            laws:
                The rate laws of the position and the velocity, and the noise law of the shaking.
        """
        force = s.k * laplacian(s.x) - s.anchor * s.x
        moving = abs(s.v) > s.v_stick
        direction = where(moving, where(s.v > 0, 1, -1), where(force > 0, 1, -1))
        stuck = both(abs(s.v) <= s.v_stick, abs(force) < s.mu_s)
        shaking = pulse_train(s.t, s.shake_time + s.relax_time, s.shake_time)
        return [
            RateLaw(s.x, where(moving, s.v, 0)),
            RateLaw(s.v, where(stuck, 0, (force - s.mu_d * direction) / s.m)),
            NoiseLaw(s.v, s.amplitude * shaking / s.m),
        ]

Equations

chain.x[i]: diff(x_chain(t), t) == where(abs(v_chain(t)) > v_stick_chain, v_chain(t), 0)
chain.v[i]: diff(v_chain(t), t) == where(both(abs(v_chain(t)) <= v_stick_chain, abs(k_chain*laplacian(x_chain(t)) - anchor_chain*x_chain(t)) < mu_s_chain), 0, (k_chain*laplacian(x_chain(t)) - mu_d_chain*where(abs(v_chain(t)) > v_stick_chain, where(v_chain(t) > 0, 1, -1), where(k_chain*laplacian(x_chain(t)) - anchor_chain*x_chain(t) > 0, 1, -1)) - anchor_chain*x_chain(t))/m_chain)