Skip to content

Pendulum¤

Main module for a pendulum.

Pendulum ¤

Generate time series data for a pendulum.

We describe a generic pendulum system by the Lagrangian action $$ S_L[\theta] = I \int_{t_0}^{t_1} \mathbb{d}t \left\{\frac{1}{2} \dot\theta^2 + \omega_0^2 \cos\theta \right\}\,, $$ where \(\theta\) is the angle from the vertical to the pendulum; \(I\) is the inertia parameter introduced for dimensional reasons, and \(\omega_0\) the frequency parameter.

Details are collected in the tutorial.

Source code in hamilflow/models/pendulum.py
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
class Pendulum:
    r"""Generate time series data for a pendulum.

    We describe a generic pendulum system by the Lagrangian action
    $$
    S_L\[\theta\] = I \int_{t_0}^{t_1} \mathbb{d}t
    \left\\{\frac{1}{2} \dot\theta^2 + \omega_0^2 \cos\theta \right\\}\,,
    $$
    where $\theta$ is the _angle_ from the vertical to the pendulum;
    $I$ is the _inertia parameter_ introduced for dimensional reasons,
    and $\omega_0$ the _frequency parameter_.

    Details are collected in the tutorial.
    """

    def __init__(
        self,
        system: float | Mapping[str, float],
        initial_condition: float | Mapping[str, float],
    ) -> None:
        if isinstance(system, float | int):
            system = {"omega0": system}
        if isinstance(initial_condition, float | int):
            initial_condition = {"theta0": initial_condition}
        self.system = PendulumSystem.model_validate(system)
        self.initial_condition = PendulumIC.model_validate(initial_condition)

    @cached_property
    def definition(self) -> dict[str, dict[str, Any]]:
        """Model params and initial conditions defined as a dictionary."""
        return {
            "system": self.system.model_dump(),
            "initial_condition": self.initial_condition.model_dump(),
        }

    @property
    def omega0(self) -> float:
        """Original angular frequency of the system."""
        return self.system.omega0

    @property
    def _k(self) -> float:
        return self.initial_condition.k

    @property
    def _math_m(self) -> float:
        return self._k**2

    @cached_property
    def freq(self) -> float:
        r"""Frequency.

        :return: $\frac{\pi}{2K(k^2)}\omega_0$, where
        $K(m)$ is [Legendre's complete elliptic integral of the first kind](https://dlmf.nist.gov/19.2#E8)
        """
        return math.pi * self.omega0 / (2 * ellipk(self._math_m))

    @cached_property
    def period(self) -> float:
        r"""Period.

        :return: $\frac{4K(k^2)}{\omega_0}$, where
        $K(m)$ is [Legendre's complete elliptic integral of the first kind](https://dlmf.nist.gov/19.2#E8)
        """
        return 4 * ellipk(self._math_m) / self.omega0

    def _math_u(
        self,
        t: "Sequence[float] | npt.ArrayLike",
    ) -> "npt.NDArray[np.float64]":
        return self.omega0 * np.asarray(t)

    def u(self, t: "Sequence[float] | npt.ArrayLike") -> "npt.NDArray[np.float64]":
        r"""Give the convenient generalised coordinate $u$, $\sin u \coloneqq \frac{\sin\frac{\theta}{2}}{\sin\frac{\theta_0}{2}}$.

        :param t: time
        :return: $u(t) = \mathrm{am}\!\big(\omega_0 t + K(k^2), k^2\big)$, where
        $\mathrm{am}(x, k)$ is [Jacobi's amplitude function](https://dlmf.nist.gov/22.16#E1),
        $K(m)$ is [Legendre's complete elliptic integral of the first kind](https://dlmf.nist.gov/19.2#E8)
        """
        _, _, _, ph = ellipj(self._math_u(t) + ellipk(self._math_m), self._math_m)

        return ph

    def theta(self, t: "Sequence[float] | npt.ArrayLike") -> "npt.NDArray[np.float64]":
        r"""Angle $\theta$.

        :param t: time
        :return: $\theta(t) = 2\arcsin\!\big(k\cdot\mathrm{cd}(\omega_0 t, k^2)\big)$, where
        $\mathrm{cd}(z, k)$ is a [Jacobian elliptic function](https://dlmf.nist.gov/22.2#E8)
        """
        _, cn, dn, _ = ellipj(self._math_u(t), self._math_m)

        return 2 * np.arcsin(cn / dn * self._k)

    def generate_from(self, n_periods: int, n_samples_per_period: int) -> pd.DataFrame:
        """Generate the time sequence from more interpretable params.

        :param n_periods: number of periods to include
        :param n_samples_per_period: number of samples in each period
        :return: an array that contains all the timesteps
        """
        time_delta = self.period / n_samples_per_period
        time_steps = np.arange(0, n_periods * n_samples_per_period) * time_delta

        return self(time_steps)

    def __call__(self, t: TypeTime) -> pd.DataFrame:
        """Generate the variables of the pendulum in time together with the time steps.

        :param t: time steps
        :return: values of the variables
            angle `x`, generalized coordinates `u`, and time `t`.
        """
        thetas = self.theta(t)
        us = self.u(t)

        return pd.DataFrame({"t": t, "x": thetas, "u": us})

definition: dict[str, dict[str, Any]] cached property ¤

Model params and initial conditions defined as a dictionary.

freq: float cached property ¤

Frequency.

Returns:

Type Description
float

\(\frac{\pi}{2K(k^2)}\omega_0\), where \(K(m)\) is Legendre's complete elliptic integral of the first kind

omega0: float property ¤

Original angular frequency of the system.

period: float cached property ¤

Period.

Returns:

Type Description
float

\(\frac{4K(k^2)}{\omega_0}\), where \(K(m)\) is Legendre's complete elliptic integral of the first kind

__call__(t) ¤

Generate the variables of the pendulum in time together with the time steps.

Parameters:

Name Type Description Default
t TypeTime

time steps

required

Returns:

Type Description
DataFrame

values of the variables angle x, generalized coordinates u, and time t.

Source code in hamilflow/models/pendulum.py
153
154
155
156
157
158
159
160
161
162
163
def __call__(self, t: TypeTime) -> pd.DataFrame:
    """Generate the variables of the pendulum in time together with the time steps.

    :param t: time steps
    :return: values of the variables
        angle `x`, generalized coordinates `u`, and time `t`.
    """
    thetas = self.theta(t)
    us = self.u(t)

    return pd.DataFrame({"t": t, "x": thetas, "u": us})

generate_from(n_periods, n_samples_per_period) ¤

Generate the time sequence from more interpretable params.

Parameters:

Name Type Description Default
n_periods int

number of periods to include

required
n_samples_per_period int

number of samples in each period

required

Returns:

Type Description
DataFrame

an array that contains all the timesteps

Source code in hamilflow/models/pendulum.py
141
142
143
144
145
146
147
148
149
150
151
def generate_from(self, n_periods: int, n_samples_per_period: int) -> pd.DataFrame:
    """Generate the time sequence from more interpretable params.

    :param n_periods: number of periods to include
    :param n_samples_per_period: number of samples in each period
    :return: an array that contains all the timesteps
    """
    time_delta = self.period / n_samples_per_period
    time_steps = np.arange(0, n_periods * n_samples_per_period) * time_delta

    return self(time_steps)

theta(t) ¤

Angle \(\theta\).

Parameters:

Name Type Description Default
t Sequence[float] | ArrayLike

time

required

Returns:

Type Description
NDArray[float64]

\(\theta(t) = 2\arcsin\!\big(k\cdot\mathrm{cd}(\omega_0 t, k^2)\big)\), where \(\mathrm{cd}(z, k)\) is a Jacobian elliptic function

Source code in hamilflow/models/pendulum.py
130
131
132
133
134
135
136
137
138
139
def theta(self, t: "Sequence[float] | npt.ArrayLike") -> "npt.NDArray[np.float64]":
    r"""Angle $\theta$.

    :param t: time
    :return: $\theta(t) = 2\arcsin\!\big(k\cdot\mathrm{cd}(\omega_0 t, k^2)\big)$, where
    $\mathrm{cd}(z, k)$ is a [Jacobian elliptic function](https://dlmf.nist.gov/22.2#E8)
    """
    _, cn, dn, _ = ellipj(self._math_u(t), self._math_m)

    return 2 * np.arcsin(cn / dn * self._k)

u(t) ¤

Give the convenient generalised coordinate \(u\), \(\sin u \coloneqq \frac{\sin\frac{\theta}{2}}{\sin\frac{\theta_0}{2}}\).

Parameters:

Name Type Description Default
t Sequence[float] | ArrayLike

time

required

Returns:

Type Description
NDArray[float64]

\(u(t) = \mathrm{am}\!\big(\omega_0 t + K(k^2), k^2\big)\), where \(\mathrm{am}(x, k)\) is Jacobi's amplitude function, \(K(m)\) is Legendre's complete elliptic integral of the first kind

Source code in hamilflow/models/pendulum.py
118
119
120
121
122
123
124
125
126
127
128
def u(self, t: "Sequence[float] | npt.ArrayLike") -> "npt.NDArray[np.float64]":
    r"""Give the convenient generalised coordinate $u$, $\sin u \coloneqq \frac{\sin\frac{\theta}{2}}{\sin\frac{\theta_0}{2}}$.

    :param t: time
    :return: $u(t) = \mathrm{am}\!\big(\omega_0 t + K(k^2), k^2\big)$, where
    $\mathrm{am}(x, k)$ is [Jacobi's amplitude function](https://dlmf.nist.gov/22.16#E1),
    $K(m)$ is [Legendre's complete elliptic integral of the first kind](https://dlmf.nist.gov/19.2#E8)
    """
    _, _, _, ph = ellipj(self._math_u(t) + ellipk(self._math_m), self._math_m)

    return ph

PendulumIC ¤

Bases: BaseModel

The initial condition for a pendulum.

Parameters:

Name Type Description Default
theta0

\(-\frac{\pi}{2} \le \theta_0 \le \frac{\pi}{2}\), the initial angle

required
Source code in hamilflow/models/pendulum.py
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
class PendulumIC(BaseModel):
    r"""The initial condition for a pendulum.

    :param theta0: $-\frac{\pi}{2} \le \theta_0 \le \frac{\pi}{2}$, the
    initial angle
    """

    theta0: float = Field(ge=-math.pi / 2, le=math.pi / 2, frozen=True)

    @computed_field  # type: ignore[misc]
    @cached_property
    def k(self) -> float:
        r"""A convenient number for elliptic functions.

        :return: $\sin\frac{\theta_0}{2}$
        """
        return math.sin(self.theta0 / 2)

k: float cached property ¤

A convenient number for elliptic functions.

Returns:

Type Description
float

\(\sin\frac{\theta_0}{2}\)

PendulumSystem ¤

Bases: BaseModel

The params for the pendulum.

Parameters:

Name Type Description Default
omega0

\(\omega_0 \coloneqq \sqrt{\frac{U}{I}} > 0\), frequency parameter

required
Source code in hamilflow/models/pendulum.py
17
18
19
20
21
22
23
24
class PendulumSystem(BaseModel):
    r"""The params for the pendulum.

    :param omega0: $\omega_0 \coloneqq \sqrt{\frac{U}{I}} > 0$, frequency
    parameter
    """

    omega0: float = Field(gt=0.0, frozen=True)