Mass-spring oscillator with static and Coulomb friction
A mass on a linear spring with static, Coulomb, and viscous friction, m x'' + c x' + k x + f = 0.
- Name:
friction_oscillator - Field:
physics/mechanics - Formalism: ordinary differential equations
- References: Khalil 2002; Trzaska 2023
Select this system
from skcomplex.systems import create_simulation, create_system, describe
system = create_system('friction_oscillator')
entry = describe('friction_oscillator')
trajectory = create_simulation('friction_oscillator').prepare().run().trajectory_
Factory
@register_system(
field='physics/mechanics',
tags=('mechanics', 'ode', 'friction', 'stick-slip', 'piecewise linear'),
references=('Khalil 2002', 'Trzaska 2023'),
defaults={'until': 40.0, 'dt': 0.01, 'solver': RK4Solver(dt=0.005)},
)
def friction_oscillator(
x: float = 1.03,
v: float = 0.0,
*,
m: float = 1.0,
k: float = 1.0,
c: float = 0.0,
mu_s: float = 0.06,
mu_k: float = 0.05,
g: float = 1.0,
v_stick: float = 0.01,
) -> System:
"""Return a mass on a linear spring with static, Coulomb, and viscous friction, `m x'' + c x' + k x + f = 0`.
While the mass slides, the friction `f = mu_k m g sign(x')` opposes its motion, so in each half of the phase plane
the motion is linear, around the shifted centres `x = ∓ mu_k m g / k`. At rest the static friction holds the mass
while `|k x| <= mu_s m g`, so the system has a whole segment of equilibria rather than isolated ones. Without
viscous friction, every half swing ends `2 mu_k m g / k` closer to the origin, and the mass sticks at the first
turning point inside the segment, after finitely many swings. The friction is discontinuous, so the mass counts
as at rest below the speed `v_stick`.
Args:
x:
The initial position.
v:
The initial velocity.
m:
The mass.
k:
The stiffness of the spring.
c:
The viscous friction coefficient.
mu_s:
The static friction coefficient.
mu_k:
The kinetic friction coefficient.
g:
The gravitational acceleration.
v_stick:
The speed below which the mass counts as at rest.
Returns:
system:
The system, with the position `mass.x` and the velocity `mass.v`.
Examples:
>>> from skcomplex.systems.physics.mechanics import friction_oscillator
>>> friction_oscillator(mu_k=0.1).elements[0].mu_k
0.1
"""
mass = FrictionOscillatorElement('mass', x=x, v=v, m=m, k=k, c=c, mu_s=mu_s, mu_k=mu_k, g=g, v_stick=v_stick)
return System(elements=[mass])
Components
Elements:
FrictionOscillatorElement, fromElement
class FrictionOscillatorElement(Element):
"""Mass on a spring with static, Coulomb, and viscous friction, which sticks and slips.
The mass `m` at the position `x` with the velocity `v` feels the spring `-k x` and the viscous friction `-c v`.
While it slides it feels the Coulomb friction `-mu_k m g sign(v)`, and at rest it stays at rest while the spring
force stays within the static friction `mu_s m g`. The friction is discontinuous in the velocity, so the mass
counts as at rest below the speed `v_stick`, which must exceed the change of speed of one solver step, and its
position stays still while it is at rest.
Args:
label:
The label of the mass.
visual:
How the mass is drawn, or `None` for `default_visual_`.
x:
The initial position.
v:
The initial velocity.
m:
The mass.
k:
The stiffness of the spring.
c:
The viscous friction coefficient.
mu_s:
The static friction coefficient.
mu_k:
The kinetic friction coefficient.
g:
The gravitational acceleration.
v_stick:
The speed below which the mass counts as at rest.
Attributes:
default_visual_:
The visual of the mass when it sets none.
Examples:
>>> from skcomplex.systems.physics.mechanics import FrictionOscillatorElement
>>> FrictionOscillatorElement('mass').mu_s
0.06
"""
def __init__(
self,
label: str,
visual: VisualSpec | None = None,
*,
x: Annotated[float, StateSpec()] = 1.03,
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,
c: Annotated[float, ParameterSpec(domain=RealSet.closed_open(0, oo))] = 0.0,
mu_s: Annotated[float, ParameterSpec(domain=RealSet.closed_open(0, oo))] = 0.06,
mu_k: Annotated[float, ParameterSpec(domain=RealSet.closed_open(0, oo))] = 0.05,
g: Annotated[float, ParameterSpec(domain=RealSet.open(0, oo))] = 1.0,
v_stick: Annotated[float, ParameterSpec(domain=RealSet.open(0, oo))] = 0.01,
) -> None:
super().__init__(label, visual=visual)
self.x = x
self.v = v
self.m = m
self.k = k
self.c = c
self.mu_s = mu_s
self.mu_k = mu_k
self.g = g
self.v_stick = v_stick
def _get_laws(self, namespace: SimpleNamespace) -> list[BaseLaw]:
"""Return the motion of the mass under the spring and the friction that sticks and slips.
Args:
namespace:
The namespace of the symbols the laws can use.
Returns:
laws:
The rates of the position and the velocity.
"""
s = namespace
force = -s.k * s.x - s.c * s.v
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(s.k * s.x) <= s.mu_s * s.m * s.g)
return [
RateLaw(s.x, where(moving, s.v, 0)),
RateLaw(s.v, where(stuck, 0, (force - s.mu_k * s.m * s.g * direction) / s.m)),
]
Equations
mass.x: diff(x_mass(t), t) == where(abs(v_mass(t)) > v_stick_mass, v_mass(t), 0)
mass.v: diff(v_mass(t), t) == where(both(abs(v_mass(t)) <= v_stick_mass, abs(k_mass*x_mass(t)) <= g_mass*m_mass*mu_s_mass), 0, -(g_mass*m_mass*mu_k_mass*where(abs(v_mass(t)) > v_stick_mass, where(v_mass(t) > 0, 1, -1), where(-c_mass*v_mass(t) - k_mass*x_mass(t) > 0, 1, -1)) + c_mass*v_mass(t) + k_mass*x_mass(t))/m_mass)