systems.builtin.deterministic.discrete.DiscretePendulum

systems.builtin.deterministic.discrete.DiscretePendulum(*args, **kwargs)
Discrete-time simple pendulum with friction and optional control.

Physical System:

A point mass m suspended by a massless, rigid rod of length L, swinging
in a vertical plane under the influence of gravity. The pendulum can be:
- Free (no control): Natural oscillatory motion
- Forced (with control): External torque applied at pivot

**Mechanical Configuration:**
              ● Pivot (fixed)
              |
              | L (rod length)
              |
              ● m (point mass)
              ↓
              g (gravity)
Angle θ measured from downward vertical:
- θ = 0: Hanging down (stable equilibrium)
- θ = π: Standing up (unstable equilibrium)
- θ = π/2: Horizontal position (maximum potential energy during swing)

**Equation of Motion (from Newton's Second Law):**
The continuous-time dynamics are:

    m·L²·θ̈ = -m·g·L·sin(θ) - b·θ̇ + τ

Dividing by m·L²:

    θ̈ = -(g/L)·sin(θ) - (b/m·L²)·θ̇ + τ/(m·L²)

Define:
    ω₀² = g/L: Natural frequency squared [rad²/s²]
    β = b/(m·L²): Damping coefficient [1/s]
    u = τ/(m·L²): Normalized torque [rad/s²]

Then:
    θ̈ = -ω₀²·sin(θ) - β·θ̇ + u

**Discrete-Time Dynamics:**
Multiple discretization methods available (see parameters).

State Space:

State: x[k] = [θ[k], ω[k]]
    Angular position:
    - θ: Angle from vertical [rad]
      * θ = 0: Downward equilibrium (lowest energy)
      * θ = π: Upward equilibrium (highest energy)
      * θ = ±π/2: Horizontal (maximum torque)
      * Periodic: sin(θ) and cos(θ) are 2π-periodic
      * Can be wrapped to [-π, π] or left unwrapped

    Angular velocity:
    - ω: Rate of change of angle [rad/s]
      * ω > 0: Counterclockwise rotation
      * ω < 0: Clockwise rotation
      * ω = 0: Instantaneous rest
      * In phase space: forms closed curves (periodic) or rotations

Control: u[k] = [τ[k]]
    Applied torque (optional):
    - τ: External torque at pivot [N·m]
      * Normalized: u = τ/(m·L²) [rad/s²]
      * τ > 0: Pushes pendulum counterclockwise
      * τ < 0: Pushes pendulum clockwise
      * For swing-up or stabilization control

Output: y[k] = [θ[k]] or [θ[k], ω[k]]
    - Position-only measurement (typical)
    - Full state if velocity sensor available
    - In practice: encoder for θ, gyroscope for ω

Dynamics (Physical Regimes):

**Small Angle Approximation (|θ| << 1):**
For small displacements, sin(θ) ≈ θ:
    θ̈ ≈ -ω₀²·θ - β·θ̇

This is a LINEAR harmonic oscillator! (See DiscreteOscillator)
- Valid for θ < 0.2 rad (~10°)
- Error: O(θ³)

**Nonlinear Regime (arbitrary θ):**
Must use full sin(θ) term:
- Period depends on amplitude (not constant like linear!)
- Large swings take longer
- Complete rotation possible if sufficient energy

**Phase Space Structure:**
The (θ, ω) phase portrait has rich structure:

1. **Fixed points:**
   - (0, 0): Stable focus/center (depends on damping)
   - (±π, 0): Saddle points (unstable)

2. **Periodic orbits (β = 0):**
   - Closed curves around (0, 0)
   - Period increases with amplitude

3. **Separatrix (β = 0):**
   - Special trajectory through saddle points
   - Separates oscillations from rotations
   - Homoclinic orbit (starts and ends at saddle)

4. **Rotations (high energy):**
   - Open curves (pendulum goes over the top)
   - ω doesn't change sign
   - Continuous rotation in one direction

Parameters:

m : float, default=1.0
    Mass of the pendulum bob [kg]
    - Affects inertia (m·L²)
    - Typical: 0.1-10 kg

L : float, default=1.0
    Length of the pendulum rod [m]
    - Determines natural frequency: ω₀ = √(g/L)
    - Longer pendulum → slower oscillations
    - Typical: 0.1-2.0 m

g : float, default=9.81
    Gravitational acceleration [m/s²]
    - Earth: 9.81
    - Moon: 1.62
    - Mars: 3.71

b : float, default=0.1
    Damping coefficient [N·m·s/rad]
    - Air resistance + friction at pivot
    - Larger b → faster decay
    - b = 0: Undamped (conservative, energy-preserving)
    - Typical: 0.01-1.0

dt : float, default=0.01
    Sampling period [s]
    - Digital control update rate
    - Must satisfy Nyquist criterion
    - Smaller dt → better accuracy
    - Typical: 0.001-0.1 s

method : str, default='zoh'
    Discretization method:
    - 'zoh': Zero-order hold (exact for constant τ)
    - 'euler': Forward Euler (simple, O(dt))
    - 'rk4': Runge-Kutta 4th order (accurate, O(dt⁴))
    - 'exact': Exact discretization for linear approximation

use_control : bool, default=True
    If True, includes control input τ
    If False, free pendulum (autonomous)

Equilibria:

**Downward Equilibrium (θ = 0, ω = 0):**
Stability: STABLE (center if undamped, stable focus if damped)
- Minimum potential energy
- Small perturbations oscillate (if β > 0: with decay)
- Linearization: θ̈ = -ω₀²·θ - β·θ̇ (harmonic oscillator)

**Upward Equilibrium (θ = ±π, ω = 0):**
Stability: UNSTABLE (saddle point)
- Maximum potential energy
- Small perturbations grow exponentially
- Linearization: θ̈ = +ω₀²·(θ - π) - β·θ̇ (inverted)
- Famous "inverted pendulum" control problem

**Separatrix Energy (undamped):**
The energy separating oscillations from rotations:
    E_sep = 2·m·g·L

If E < E_sep: Oscillation (back and forth)
If E > E_sep: Rotation (goes over the top)
If E = E_sep: Asymptotic approach to upward position

Controllability:

**With Control (u ≠ 0):**
Completely controllable - can reach any state from any other state.

**Without Control (Free Pendulum):**
NOT controllable - energy determines accessible states.
Can only reach states with same or lower energy (due to damping).

Observability:

**Position-only measurement (y = θ):**
Observable almost everywhere.
- Can reconstruct ω from θ measurements over time
- Singularity at equilibria (θ = constant → ω ambiguous)

**Full state measurement:**
Trivially observable.

Energy and Integrals of Motion:

**Total Mechanical Energy:**
    E = (1/2)·m·L²·ω² + m·g·L·(1 - cos(θ))

Kinetic: K = (1/2)·m·L²·ω²
Potential: V = m·g·L·(1 - cos(θ))

**For Undamped, Unforced Pendulum (β = 0, u = 0):**
Energy is conserved: dE/dt = 0
Phase space trajectories are level sets of E(θ, ω).

**For Damped Pendulum (β > 0, u = 0):**
Energy decreases: dE/dt = -b·ω² ≤ 0
All trajectories asymptotically approach (0, 0).

**For Forced Pendulum (u ≠ 0):**
Energy can increase or decrease:
    dE/dt = (m·L²·u - b)·ω²

Control Objectives:

**1. Stabilization at Downward Position:**
   Goal: θ → 0, ω → 0
   Method: PD control works well (linear regime)
   Challenge: Minimal (natural equilibrium)

**2. Swing-Up Control:**
   Goal: Move from θ = 0 to θ = π
   Methods:
   - Energy-based: Pump energy until E ≈ E_target
   - Bang-bang: Apply maximum torque
   - Trajectory optimization
   Challenge: Large control effort, multiple rotations

**3. Inverted Stabilization:**
   Goal: Stabilize at θ = π, ω = 0
   Method: LQR around linearization
   Challenge: Unstable equilibrium, requires continuous control

**4. Trajectory Tracking:**
   Goal: Follow θ_ref(t), ω_ref(t)
   Method: Feedforward + feedback
   Application: Robotic manipulation

**5. Limit Cycle Creation:**
   Goal: Create stable periodic oscillation
   Method: Nonlinear feedback
   Application: Rhythmic motion generation

State Constraints:

**1. Angle Limits (if physical stops exist):**
   θ_min ≤ θ ≤ θ_max
   Otherwise: θ ∈ ℝ (unbounded rotations possible)

**2. Velocity Limits:**
   |ω| ≤ ω_max
   Physical: Limited by energy or mechanism

**3. Torque Limits:**
   |τ| ≤ τ_max
   Most critical practical constraint
   Determines controllability

Numerical Considerations:

**Discretization Accuracy:**
- ZOH: Good for control applications
- Euler: Simple but may be inaccurate/unstable for large dt
- RK4: High accuracy, recommended for simulation

**Angle Wrapping:**
For visualization and analysis:
- Wrap θ to [-π, π]: θ_wrapped = atan2(sin(θ), cos(θ))
- Or leave unwrapped to count rotations

**Energy Verification:**
For undamped case, check energy conservation:
    |E[k+1] - E[k]| should be small (numerical precision)

Example Usage:

>>> # Create pendulum with realistic parameters
>>> system = DiscretePendulum(
...     m=0.5,      # 500g mass
...     L=1.0,      # 1m length
...     g=9.81,
...     b=0.05,     # Light damping
...     dt=0.01,
...     method='rk4'
... )
>>>
>>> print(f"Natural frequency: {system.natural_frequency:.3f} rad/s")
>>> print(f"Period: {system.period:.3f} s")
>>> print(f"Damping ratio: {system.damping_ratio:.4f}")
>>>
>>> # Free oscillation from initial displacement
>>> x0 = np.array([np.pi/4, 0.0])  # 45° release from rest
>>> result_free = system.simulate(
...     x0=x0,
...     u_sequence=None,
...     n_steps=1000
... )
>>>
>>> # Plot phase portrait
>>> import plotly.graph_objects as go
>>> fig = go.Figure()
>>> fig.add_trace(go.Scatter(
...     x=result_free['states'][:, 0],
...     y=result_free['states'][:, 1],
...     mode='lines',
...     name='Trajectory'
... ))
>>> fig.update_layout(
...     title='Pendulum Phase Portrait',
...     xaxis_title='θ [rad]',
...     yaxis_title='ω [rad/s]'
... )
>>> fig.show()
>>>
>>> # Energy analysis
>>> energies = np.array([
...     system.compute_total_energy(x[0], x[1])
...     for x in result_free['states']
... ])
>>>
>>> fig_energy = go.Figure()
>>> fig_energy.add_trace(go.Scatter(
...     x=result_free['time_steps'] * system.dt,
...     y=energies,
...     name='Total Energy'
... ))
>>> fig_energy.update_layout(
...     title='Energy vs Time (should decrease with damping)',
...     xaxis_title='Time [s]',
...     yaxis_title='Energy [J]'
... )
>>> fig_energy.show()
>>>
>>> # Swing-up control (energy-based)
>>> def swing_up_energy_control(x, k):
...     theta, omega = x
...
...     # Current energy
...     E = system.compute_total_energy(theta, omega)
...
...     # Target energy (upward position)
...     E_target = system.m * system.g * system.L * 2
...
...     # Energy error
...     E_error = E - E_target
...
...     # If close to upward, switch to stabilization
...     if abs(theta - np.pi) < 0.3 and abs(omega) < 1.0:
...         # LQR around upward
...         K_up = system.design_upward_stabilizer()
...         return -K_up @ np.array([theta - np.pi, omega])
...     else:
...         # Energy pumping: add energy when moving in right direction
...         k_swing = 5.0
...         return k_swing * E_error * np.sign(omega * np.cos(theta))
>>>
>>> result_swing = system.rollout(
...     x0=np.array([0.0, 0.0]),
...     policy=swing_up_energy_control,
...     n_steps=2000
... )
>>>
>>> # Visualize swing-up in phase space
>>> fig_swing = system.plot_phase_portrait_with_separatrix()
>>> fig_swing.add_trace(go.Scatter(
...     x=result_swing['states'][:, 0],
...     y=result_swing['states'][:, 1],
...     mode='lines',
...     line=dict(color='red', width=2),
...     name='Swing-up trajectory'
... ))
>>> fig_swing.show()
>>>
>>> # Compare small vs large angle dynamics
>>> # Small angle (linear regime)
>>> x0_small = np.array([0.1, 0.0])  # ~6°
>>> result_small = system.simulate(x0_small, None, n_steps=500)
>>>
>>> # Large angle (nonlinear regime)
>>> x0_large = np.array([2.0, 0.0])  # ~115°
>>> result_large = system.simulate(x0_large, None, n_steps=500)
>>>
>>> # Compare periods
>>> def find_period(states, dt):
...     # Find zero crossings
...     theta = states[:, 0]
...     crossings = np.where(np.diff(np.sign(theta)))[0]
...     if len(crossings) >= 2:
...         period = 2 * (crossings[1] - crossings[0]) * dt
...         return period
...     return None
>>>
>>> period_small = find_period(result_small['states'], system.dt)
>>> period_large = find_period(result_large['states'], system.dt)
>>>
>>> print(f"Small angle period: {period_small:.3f} s")
>>> print(f"Large angle period: {period_large:.3f} s")
>>> print(f"Linear theory: {system.period:.3f} s")
>>> print("Note: Large angle period > small angle period (nonlinearity)")

Physical Insights:

**Isochronism (Small Angles Only):**
For small oscillations, period is independent of amplitude:
    T = 2π√(L/g) = 2π/ω₀

This is why pendulum clocks work! (But only for small swings)

**Anharmonicity (Large Angles):**
For large amplitudes, period increases with amplitude:
    T(θ₀) ≈ T₀·(1 + θ₀²/16 + ...) for initial angle θ₀

This breaks isochronism → pendulum clocks must limit amplitude.

**Separatrix Dynamics:**
Trajectories on the separatrix (E = E_sep) take infinite time to reach
the unstable equilibrium. This is a homoclinic orbit.

**Conservation vs Dissipation:**
- No damping: Phase space filled with nested closed curves
- With damping: Spiral inward to stable equilibrium
- Energy landscape funnels trajectories toward rest

**Chaotic Forcing:**
Add periodic forcing: θ̈ = -ω₀²·sin(θ) + A·cos(Ω·t)
Can produce chaos (sensitive dependence on initial conditions)!
This is the "driven damped pendulum" - route to chaos.

Common Pitfalls:

1. **Using linear approximation for large angles:**
   sin(θ) ≈ θ only valid for |θ| < 0.2 rad
   Large errors for θ > π/4

2. **Forgetting angle periodicity:**
   θ = 0 and θ = 2π are the same state
   Important for phase portraits

3. **Ignoring separatrix:**
   Qualitative dynamics change across separatrix
   Control strategies differ for oscillations vs rotations

4. **Wrong linearization for upward:**
   Linearizing at θ = π gives θ̈ = +ω₀²·(θ - π)
   Note: POSITIVE coefficient (unstable)

5. **Energy not conserved numerically:**
   Discretization introduces energy errors
   Use symplectic integrators for better conservation

6. **Insufficient damping modeling:**
   Real pendulums have complex friction
   Viscous approximation b·ω may be inadequate

Extensions:

1. **Driven pendulum:**
   Add periodic forcing: u[k] = A·cos(Ω·k·dt)
   Can produce chaos and strange attractors

2. **Double pendulum:**
   Two coupled pendula
   Exhibits deterministic chaos

3. **Spherical pendulum:**
   3D motion (θ, φ)
   Rich dynamics, Coriolis effects

4. **Elastic pendulum:**
   Pendulum with spring (varying L)
   Coupled radial-angular motion

5. **Pendulum on cart:**
   Inverted pendulum (classic underactuated system)
   Noncollocated control problem

6. **Parametric excitation:**
   Varying L sinusoidally
   Parametric resonance effects

See Also:

DiscreteRobotArm : Similar dynamics (different notation)
DiscreteOscillator : Linear limit (small angles)
DoublePendulum : Chaotic extension

Attributes

Name Description
damping_ratio Damping ratio ζ = β/(2ω₀) [-].
natural_frequency Natural frequency ω₀ = √(g/L) [rad/s].
period Period of small oscillations T = 2π/ω₀ [s].

Methods

Name Description
compute_kinetic_energy Kinetic energy K = (1/2)·I·ω² [J].
compute_potential_energy Potential energy V = m·g·L·(1 - cos(θ)) [J].
compute_separatrix Compute separatrix trajectory (for undamped case).
compute_separatrix_energy Energy of separatrix (boundary between oscillation/rotation) [J].
compute_total_energy Total mechanical energy E = K + V [J].
define_system Define discrete-time pendulum dynamics.
design_upward_stabilizer Design LQR controller for upward stabilization.
setup_equilibria Set up downward and upward equilibria.

compute_kinetic_energy

systems.builtin.deterministic.discrete.DiscretePendulum.compute_kinetic_energy(
    omega,
)

Kinetic energy K = (1/2)·I·ω² [J].

compute_potential_energy

systems.builtin.deterministic.discrete.DiscretePendulum.compute_potential_energy(
    theta,
)

Potential energy V = m·g·L·(1 - cos(θ)) [J].

compute_separatrix

systems.builtin.deterministic.discrete.DiscretePendulum.compute_separatrix(
    n_points=1000,
)

Compute separatrix trajectory (for undamped case).

Returns

Name Type Description
tuple (theta, omega) points on separatrix

compute_separatrix_energy

systems.builtin.deterministic.discrete.DiscretePendulum.compute_separatrix_energy(
)

Energy of separatrix (boundary between oscillation/rotation) [J].

compute_total_energy

systems.builtin.deterministic.discrete.DiscretePendulum.compute_total_energy(
    theta,
    omega,
)

Total mechanical energy E = K + V [J].

define_system

systems.builtin.deterministic.discrete.DiscretePendulum.define_system(
    m=1.0,
    L=1.0,
    g=9.81,
    b=0.1,
    dt=0.01,
    method='zoh',
    use_control=True,
)

Define discrete-time pendulum dynamics.

Parameters

Name Type Description Default
m float Mass [kg] 1.0
L float Length [m] 1.0
g float Gravity [m/s²] 9.81
b float Damping coefficient [N·m·s/rad] 0.1
dt float Sampling period [s] 0.01
method str Discretization method (‘zoh’, ‘euler’, ‘rk4’) 'zoh'
use_control bool If True, include control input True

design_upward_stabilizer

systems.builtin.deterministic.discrete.DiscretePendulum.design_upward_stabilizer(
)

Design LQR controller for upward stabilization.

Returns

Name Type Description
np.ndarray LQR gain K

setup_equilibria

systems.builtin.deterministic.discrete.DiscretePendulum.setup_equilibria()

Set up downward and upward equilibria.