1
1
from abc import ABC , abstractmethod
2
2
from functools import cached_property
3
- from typing import Literal
3
+ from typing import Literal , Mapping , Sequence
4
4
5
5
import numpy as np
6
6
import pandas as pd
7
7
from numpy .typing import ArrayLike
8
- from pydantic import BaseModel , computed_field , field_validator
8
+ from pydantic import BaseModel , Field , computed_field , field_validator , model_validator
9
+
10
+ try :
11
+ from typing import Self
12
+ except ImportError :
13
+ from typing_extensions import Self
9
14
10
15
11
16
class HarmonicOscillatorSystem (BaseModel ):
12
17
"""The params for the harmonic oscillator
13
18
14
19
:cvar omega: angular frequency of the harmonic oscillator
15
20
:cvar zeta: damping ratio
21
+ :cvar real: use real solution (only supported for the undamped case)
16
22
"""
17
23
18
24
omega : float
19
25
zeta : float = 0.0
20
26
27
+ real : bool = Field (default = True )
28
+
21
29
@computed_field # type: ignore[misc]
22
30
@cached_property
23
31
def period (self ) -> float :
@@ -53,6 +61,13 @@ def check_zeta_non_negative(cls, v: float) -> float:
53
61
54
62
return v
55
63
64
+ @model_validator (mode = "after" )
65
+ def check_real_zeta (self ) -> Self :
66
+ if not self .real and self .zeta != 0.0 :
67
+ raise NotImplementedError ("real = False only implemented for zeta = 0.0" )
68
+
69
+ return self
70
+
56
71
57
72
class HarmonicOscillatorIC (BaseModel ):
58
73
"""The initial condition for a harmonic oscillator
@@ -77,23 +92,23 @@ class HarmonicOscillatorBase(ABC):
77
92
78
93
def __init__ (
79
94
self ,
80
- system : dict [str , float ],
81
- initial_condition : dict [str , float ] | None = None ,
95
+ system : Mapping [str , float | int | bool ],
96
+ initial_condition : Mapping [str , float | int ] | None = None ,
82
97
) -> None :
83
98
initial_condition = initial_condition or {}
84
99
self .system = HarmonicOscillatorSystem .model_validate (system )
85
100
self .initial_condition = HarmonicOscillatorIC .model_validate (initial_condition )
86
101
87
102
@cached_property
88
- def definition (self ) -> dict [str , float ]:
103
+ def definition (self ) -> dict [str , dict [ str , float | int | bool ] ]:
89
104
"""model params and initial conditions defined as a dictionary."""
90
105
return {
91
106
"system" : self .system .model_dump (),
92
107
"initial_condition" : self .initial_condition .model_dump (),
93
108
}
94
109
95
110
@abstractmethod
96
- def _x (self , t : ArrayLike ) -> ArrayLike :
111
+ def _x (self , t : float | int | Sequence [ float | int ] ) -> ArrayLike :
97
112
r"""Solution to simple harmonic oscillators."""
98
113
...
99
114
@@ -129,13 +144,17 @@ class SimpleHarmonicOscillator(HarmonicOscillatorBase):
129
144
130
145
The mass behaves like a simple harmonic oscillator.
131
146
132
- In general, the solution to a simple harmonic oscillator is
147
+ In general, the solution to a real simple harmonic oscillator is
133
148
134
149
$$
135
150
x(t) = A \cos(\omega t + \phi),
136
151
$$
137
152
138
153
where $\omega$ is the angular frequency, $\phi$ is the initial phase, and $A$ is the amplitude.
154
+ The complex solution is
155
+ $$
156
+ x(t) = A \exp(-\mathbb{i} (\omega t + \phi)).
157
+ $$
139
158
140
159
141
160
To use this generator,
@@ -153,23 +172,32 @@ class SimpleHarmonicOscillator(HarmonicOscillatorBase):
153
172
154
173
def __init__ (
155
174
self ,
156
- system : dict [str , float ],
157
- initial_condition : dict [str , float ] | None = None ,
175
+ system : Mapping [str , float | int | bool ],
176
+ initial_condition : Mapping [str , float | int ] | None = None ,
158
177
) -> None :
159
178
super ().__init__ (system , initial_condition )
160
179
if self .system .type != "simple" :
161
180
raise ValueError (
162
181
f"System is not a Simple Harmonic Oscillator: { self .system } "
163
182
)
164
183
165
- def _x (self , t : ArrayLike ) -> ArrayLike :
184
+ def _f (self , phase : float | int | Sequence [float | int ]) -> np .ndarray :
185
+ np_phase = np .array (phase , copy = False )
186
+ return np .cos (np_phase ) if self .system .real else np .exp (- 1j * np_phase )
187
+
188
+ def _x (self , t : float | int | Sequence [float | int ]) -> np .ndarray :
166
189
r"""Solution to simple harmonic oscillators:
167
190
168
191
$$
169
- x(t) = x_0 \cos(\omega t + \phi).
192
+ x(t) = x_0 \cos(\omega t + \phi)
193
+ $$
194
+ if real, or
195
+ $$
196
+ x(t) = x_0 \exp(-\mathbb{i} (\omega t + \phi))
170
197
$$
198
+ if not real.
171
199
"""
172
- return self .initial_condition .x0 * np . cos (
200
+ return self .initial_condition .x0 * self . _f (
173
201
self .system .omega * t + self .initial_condition .phi
174
202
)
175
203
@@ -225,8 +253,8 @@ class DampedHarmonicOscillator(HarmonicOscillatorBase):
225
253
226
254
def __init__ (
227
255
self ,
228
- system : dict [str , float ],
229
- initial_condition : dict [str , float ] | None = None ,
256
+ system : Mapping [str , float | int ],
257
+ initial_condition : Mapping [str , float | int ] | None = None ,
230
258
) -> None :
231
259
super ().__init__ (system , initial_condition )
232
260
if self .system .type == "simple" :
@@ -235,7 +263,7 @@ def __init__(
235
263
f"This is a simple harmonic oscillator, use `SimpleHarmonicOscillator`."
236
264
)
237
265
238
- def _x_under_damped (self , t : float | np . ndarray ) -> float | np . ndarray :
266
+ def _x_under_damped (self , t : float | int | Sequence [ float | int ]) -> ArrayLike :
239
267
r"""Solution to under damped harmonic oscillators:
240
268
241
269
$$
@@ -260,7 +288,7 @@ def _x_under_damped(self, t: float | np.ndarray) -> float | np.ndarray:
260
288
* np .sin (omega_damp * t )
261
289
) * np .exp (- self .system .zeta * self .system .omega * t )
262
290
263
- def _x_critical_damped (self , t : float | np . ndarray ) -> float | np . ndarray :
291
+ def _x_critical_damped (self , t : float | int | Sequence [ float | int ]) -> ArrayLike :
264
292
r"""Solution to critical damped harmonic oscillators:
265
293
266
294
$$
@@ -278,7 +306,7 @@ def _x_critical_damped(self, t: float | np.ndarray) -> float | np.ndarray:
278
306
- self .system .zeta * self .system .omega * t
279
307
)
280
308
281
- def _x_over_damped (self , t : float | np . ndarray ) -> float | np . ndarray :
309
+ def _x_over_damped (self , t : float | int | Sequence [ float | int ]) -> ArrayLike :
282
310
r"""Solution to over harmonic oscillators:
283
311
284
312
$$
@@ -304,7 +332,7 @@ def _x_over_damped(self, t: float | np.ndarray) -> float | np.ndarray:
304
332
* np .sinh (gamma_damp * t )
305
333
) * np .exp (- self .system .zeta * self .system .omega * t )
306
334
307
- def _x (self , t : float | np . ndarray ) -> float | np . ndarray :
335
+ def _x (self , t : float | int | Sequence [ float | int ]) -> ArrayLike :
308
336
r"""Solution to damped harmonic oscillators."""
309
337
if self .system .type == "under_damped" :
310
338
x = self ._x_under_damped (t )
0 commit comments