CW-DoublePendulum 0.1.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,20 @@
1
+ Metadata-Version: 2.4
2
+ Name: CW-DoublePendulum
3
+ Version: 0.1.0
4
+ Summary: A Python package that uses NumPy and Matplotlib to simulate an interactive double pendulum.
5
+ Author-email: Chase Worsley <your_email@example.com>
6
+ License: MIT
7
+ Project-URL: Homepage, https://github.com/Worsleychase/InteractiveDoublePendulum
8
+ Classifier: Programming Language :: Python :: 3
9
+ Classifier: License :: OSI Approved :: MIT License
10
+ Classifier: Operating System :: OS Independent
11
+ Requires-Python: >=3.9
12
+ Description-Content-Type: text/markdown
13
+ License-File: LICENSE
14
+ Requires-Dist: numpy
15
+ Requires-Dist: matplotlib
16
+ Dynamic: license-file
17
+
18
+ # Interactive Double Pendulum
19
+
20
+ Final project for Mathematical & Computational Physics II
@@ -0,0 +1,6 @@
1
+ main.py,sha256=T9MMJ_Nzi0p2lX6XWCkdRCroP5-qPcpVfFxxKg_tLMg,12114
2
+ cw_doublependulum-0.1.0.dist-info/licenses/LICENSE,sha256=P_M8rIY4EFOQArRFqXCsxqdZyDp2kUCszxsaDapqA_4,1090
3
+ cw_doublependulum-0.1.0.dist-info/METADATA,sha256=KpvoXdLoey1OcCT7q_myQhBSzkVk4PnKXl5-jCYJWzA,719
4
+ cw_doublependulum-0.1.0.dist-info/WHEEL,sha256=0CuiUZ_p9E4cD6NyLD6UG80LBXYyiSYZOKDm5lp32xk,91
5
+ cw_doublependulum-0.1.0.dist-info/top_level.txt,sha256=ZAMgPdWghn6xTRBO6Kc3ML1y3ZrZLnjZlqbboKXc_AE,5
6
+ cw_doublependulum-0.1.0.dist-info/RECORD,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (80.3.1)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 Worsleychase
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1 @@
1
+ main
main.py ADDED
@@ -0,0 +1,362 @@
1
+ import numpy as np
2
+ import matplotlib.pyplot as plt
3
+ from matplotlib.widgets import Slider, CheckButtons, Button
4
+ from matplotlib.animation import FuncAnimation
5
+ from matplotlib.collections import LineCollection
6
+ import time
7
+ import csv
8
+
9
+ # Initial parameters (all can be modified with sliders)
10
+ l1 = 1.0
11
+ l2 = 1.0
12
+ m1 = 1.0
13
+ m2 = 1.0
14
+ g = 9.81
15
+ dt = 0.02
16
+
17
+ # Initial conditions: [theta1, omega1, theta2, omega2]
18
+ state = np.array([np.pi / 2, 0, np.pi / 2, 0], dtype=float)
19
+
20
+ # Control flags
21
+ animationRunning = True
22
+ collectingData = False
23
+
24
+ collectedData = []
25
+
26
+ def derivs(state, l1, l2, m1, m2):
27
+ theta1, omega1, theta2, omega2 = state
28
+ delta = theta2 - theta1
29
+
30
+ denom1 = (m1 + m2) * l1 - m2 * l1 * np.cos(delta)**2
31
+ denom2 = (l2 / l1) * denom1
32
+
33
+ domega1 = (m2 * l1 * omega1**2 * np.sin(delta) * np.cos(delta) +
34
+ m2 * g * np.sin(theta2) * np.cos(delta) +
35
+ m2 * l2 * omega2**2 * np.sin(delta) -
36
+ (m1 + m2) * g * np.sin(theta1)) / denom1
37
+
38
+ domega2 = (-m2 * l2 * omega2**2 * np.sin(delta) * np.cos(delta) +
39
+ (m1 + m2) * g * np.sin(theta1) * np.cos(delta) -
40
+ (m1 + m2) * l1 * omega1**2 * np.sin(delta) -
41
+ (m1 + m2) * g * np.sin(theta2)) / denom2
42
+
43
+ return np.array([omega1, domega1, omega2, domega2])
44
+
45
+ def step():
46
+ global state
47
+ k1 = derivs(state, l1, l2, m1, m2)
48
+ k2 = derivs(state + 0.5 * dt * k1, l1, l2, m1, m2)
49
+ k3 = derivs(state + 0.5 * dt * k2, l1, l2, m1, m2)
50
+ k4 = derivs(state + dt * k3, l1, l2, m1, m2)
51
+ state += (dt / 6) * (k1 + 2 * k2 + 2 * k3 + k4)
52
+
53
+ def getPos():
54
+ theta1, omega1, theta2, omega2 = state
55
+ x1 = l1 * np.sin(theta1)
56
+ y1 = -l1 * np.cos(theta1)
57
+ x2 = x1 + l2 * np.sin(theta2)
58
+ y2 = y1 - l2 * np.cos(theta2)
59
+ return x1, y1, x2, y2, theta1, omega1, theta2, omega2
60
+
61
+ def updatePlotLimits():
62
+ # Calculate the range based on the diagonal extent of both pendulums
63
+ # Add 20% margin
64
+ maxRange = (l1 + l2) * 1.2
65
+ ax.set_xlim(-maxRange, maxRange)
66
+ ax.set_ylim(-maxRange, maxRange)
67
+ fig.canvas.draw_idle()
68
+
69
+ # Function to convert mass to marker size
70
+ def massToSize(mass):
71
+ return 3 + 5 * (mass ** 0.7)
72
+
73
+ # Make large figure for buttons and whatnot
74
+ fig = plt.figure(figsize=(12, 8))
75
+ # Adjust the main plot area to make room for UI elements
76
+ plt.subplots_adjust(left=0.2, right=0.8, bottom=0.2)
77
+ ax = plt.subplot(111)
78
+ ax.set_aspect('equal')
79
+ updatePlotLimits()
80
+ ax.plot(0, 0, 'ko') # Center point/pivot
81
+ ax.set_title('Double Pendulum Simulation')
82
+
83
+ # Pendulum components
84
+ line, = ax.plot([], [], '-', lw=2, color='black')
85
+ mass1, = ax.plot([], [], 'bo', markersize=massToSize(m1))
86
+ mass2, = ax.plot([], [], 'ro', markersize=massToSize(m2))
87
+
88
+ # Trail lines
89
+ trailLength = 100
90
+ trailSegments1 = []
91
+ trailSegments2 = []
92
+ trailCmap1 = plt.get_cmap('winter') # these don't really have any gradient for some reason, idc anymore
93
+ trailCmap2 = plt.get_cmap('autumn')
94
+ trail1 = ax.add_collection(LineCollection([], linewidths=2, cmap=trailCmap1, alpha=0.6))
95
+ trail2 = ax.add_collection(LineCollection([], linewidths=2, cmap=trailCmap2, alpha=0.6))
96
+
97
+ # Trace flags
98
+ showTrace1 = True
99
+ showTrace2 = True
100
+
101
+ def init():
102
+ line.set_data([], [])
103
+ mass1.set_data([], [])
104
+ mass2.set_data([], [])
105
+ trail1.set_segments([])
106
+ trail2.set_segments([])
107
+ return line, mass1, mass2, trail1, trail2
108
+
109
+ def update(frame):
110
+ global collectedData
111
+
112
+ if animationRunning:
113
+ step()
114
+
115
+ x1, y1, x2, y2, theta1, omega1, theta2, omega2 = getPos()
116
+
117
+ # Collect data and setup keys
118
+ if collectingData:
119
+ timestamp = time.time()
120
+ collectedData.append({
121
+ 'time': timestamp,
122
+ 'theta1': theta1,
123
+ 'omega1': omega1,
124
+ 'theta2': theta2,
125
+ 'omega2': omega2,
126
+ 'x1': x1,
127
+ 'y1': y1,
128
+ 'x2': x2,
129
+ 'y2': y2,
130
+ 'l1': l1,
131
+ 'l2': l2,
132
+ 'm1': m1,
133
+ 'm2': m2,
134
+ 'g': g,
135
+ 'dt': dt
136
+ })
137
+
138
+ # Update pendulum position
139
+ line.set_data([0, x1, x2], [0, y1, y2])
140
+ mass1.set_data([x1], [y1])
141
+ mass2.set_data([x2], [y2])
142
+
143
+ # Update trail if animation is running
144
+ if animationRunning:
145
+ if len(trailSegments1) > 0:
146
+ prevX1, prevY1 = trailSegments1[-1][-1]
147
+ prevX2, prevY2 = trailSegments2[-1][-1]
148
+
149
+ trailSegments1.append([[prevX1, prevY1], [x1, y1]])
150
+ trailSegments2.append([[prevX2, prevY2], [x2, y2]])
151
+ else:
152
+ trailSegments1.append([[x1, y1], [x1, y1]])
153
+ trailSegments2.append([[x2, y2], [x2, y2]])
154
+
155
+ # Limit trail length
156
+ if len(trailSegments1) > trailLength:
157
+ trailSegments1.pop(0)
158
+ trailSegments2.pop(0)
159
+
160
+ # Update trail if enabled
161
+ if showTrace1 and len(trailSegments1) > 0:
162
+ n = len(trailSegments1)
163
+ alphas = np.linspace(0.0, 1.0, n)
164
+ trail1.set_segments(trailSegments1)
165
+ trail1.set_array(alphas)
166
+ else:
167
+ trail1.set_segments([])
168
+
169
+ if showTrace2 and len(trailSegments2) > 0:
170
+ n = len(trailSegments2)
171
+ alphas = np.linspace(0.0, 1.0, n)
172
+ trail2.set_segments(trailSegments2)
173
+ trail2.set_array(alphas)
174
+ else:
175
+ trail2.set_segments([])
176
+
177
+ return line, mass1, mass2, trail1, trail2
178
+
179
+ ani = FuncAnimation(fig, update, init_func=init, blit=True, interval=20)
180
+
181
+ # -------------------- LAYOUT ARRANGEMENT --------------------
182
+ axcolor = 'lightgoldenrodyellow'
183
+
184
+ # ---- SLIDERS (RIGHT SIDE) ----
185
+ sliderWidth = 0.15
186
+ sliderHeight = 0.03
187
+ sliderLeft = 0.82
188
+ sliderGap = 0.05
189
+
190
+ # Create slider axes on the right side
191
+ axM1 = plt.axes([sliderLeft, 0.7, sliderWidth, sliderHeight], facecolor=axcolor)
192
+ axM2 = plt.axes([sliderLeft, 0.7 - sliderGap, sliderWidth, sliderHeight], facecolor=axcolor)
193
+ axL1 = plt.axes([sliderLeft, 0.7 - 2*sliderGap, sliderWidth, sliderHeight], facecolor=axcolor)
194
+ axL2 = plt.axes([sliderLeft, 0.7 - 3*sliderGap, sliderWidth, sliderHeight], facecolor=axcolor)
195
+ axG = plt.axes([sliderLeft, 0.7 - 4*sliderGap, sliderWidth, sliderHeight], facecolor=axcolor)
196
+ axDt = plt.axes([sliderLeft, 0.7 - 5*sliderGap, sliderWidth, sliderHeight], facecolor=axcolor)
197
+
198
+ # Create sliders
199
+ sliderM1 = Slider(axM1, 'Mass 1', 0.1, 5.0, valinit=m1)
200
+ sliderM2 = Slider(axM2, 'Mass 2', 0.1, 5.0, valinit=m2)
201
+ sliderL1 = Slider(axL1, 'Length 1', 0.1, 2.0, valinit=l1)
202
+ sliderL2 = Slider(axL2, 'Length 2', 0.1, 2.0, valinit=l2)
203
+ sliderG = Slider(axG, 'Gravity', 0.1, 20.0, valinit=g)
204
+ sliderDt = Slider(axDt, 'Time Step', 0.005, 0.05, valinit=dt)
205
+
206
+ # ---- BUTTONS (LEFT SIDE) ----
207
+ buttonWidth = 0.15
208
+ buttonHeight = 0.05
209
+ buttonLeft = 0.02
210
+ buttonGap = 0.06
211
+
212
+ # Add checkbox buttons for trails
213
+ rax = plt.axes([buttonLeft, 0.7, buttonWidth, 0.1], facecolor=axcolor)
214
+ check = CheckButtons(
215
+ rax, ['Trail 1', 'Trail 2'],
216
+ [showTrace1, showTrace2]
217
+ )
218
+
219
+ # Add buttons
220
+ resetAx = plt.axes([buttonLeft, 0.6, buttonWidth, buttonHeight], facecolor=axcolor)
221
+ pauseAx = plt.axes([buttonLeft, 0.6 - buttonGap, buttonWidth, buttonHeight], facecolor=axcolor)
222
+ collectAx = plt.axes([buttonLeft, 0.6 - 2*buttonGap, buttonWidth, buttonHeight], facecolor=axcolor)
223
+
224
+ resetButton = plt.Button(resetAx, 'Reset', color=axcolor)
225
+ pauseButton = plt.Button(pauseAx, 'Pause', color=axcolor)
226
+ collectButton = plt.Button(collectAx, 'Collect Data', color=axcolor)
227
+
228
+ # -------------------- CALLBACKS -------------------- be careful >:(
229
+ def updateSliders(val):
230
+ global m1, m2, l1, l2, g, dt
231
+ if not collectingData: # Only allow changes when not collecting data
232
+ m1 = sliderM1.val
233
+ m2 = sliderM2.val
234
+ l1 = sliderL1.val
235
+ l2 = sliderL2.val
236
+ g = sliderG.val
237
+ dt = sliderDt.val
238
+
239
+ # Update mass marker sizes based on new mass values
240
+ mass1.set_markersize(massToSize(m1))
241
+ mass2.set_markersize(massToSize(m2))
242
+ updatePlotLimits()
243
+
244
+ # Clear trails when parameters change
245
+ trailSegments1.clear()
246
+ trailSegments2.clear()
247
+ trail1.set_segments([])
248
+ trail2.set_segments([])
249
+
250
+ sliderM1.on_changed(updateSliders)
251
+ sliderM2.on_changed(updateSliders)
252
+ sliderL1.on_changed(updateSliders)
253
+ sliderL2.on_changed(updateSliders)
254
+ sliderG.on_changed(updateSliders)
255
+ sliderDt.on_changed(updateSliders)
256
+
257
+ def toggleTrace(label):
258
+ global showTrace1, showTrace2
259
+ if label == 'Trail 1':
260
+ showTrace1 = not showTrace1
261
+ if not showTrace1:
262
+ trail1.set_segments([])
263
+ elif label == 'Trail 2':
264
+ showTrace2 = not showTrace2
265
+ if not showTrace2:
266
+ trail2.set_segments([])
267
+
268
+ check.on_clicked(toggleTrace)
269
+
270
+ def reset(event):
271
+ global state, trailSegments1, trailSegments2
272
+ if not collectingData: # Only reset when not collecting data dummy
273
+ state = np.array([np.pi / 2, 0, np.pi / 2, 0], dtype=float)
274
+ trailSegments1.clear()
275
+ trailSegments2.clear()
276
+ trail1.set_segments([])
277
+ trail2.set_segments([])
278
+
279
+ resetButton.on_clicked(reset)
280
+
281
+ def toggleAnimation(event):
282
+ global animationRunning
283
+ animationRunning = not animationRunning
284
+ pauseButton.label.set_text('Play' if not animationRunning else 'Pause')
285
+
286
+ pauseButton.on_clicked(toggleAnimation)
287
+
288
+ def toggleDataCollection(event):
289
+ global collectingData, collectedData
290
+
291
+ collectingData = not collectingData
292
+
293
+ if collectingData:
294
+ collectButton.label.set_text('Stop Collecting')
295
+ collectedData = [] # Clear previous data
296
+
297
+ # Disable sliders to ensure good data | might change and allow different stuff, but i see weird behavior idk
298
+ sliderM1.active = False
299
+ sliderM2.active = False
300
+ sliderL1.active = False
301
+ sliderL2.active = False
302
+ sliderG.active = False
303
+ sliderDt.active = False
304
+
305
+ # Change slider colors to show they have been imprisoned, how sad it doesn't work
306
+ axM1.set_facecolor('red')
307
+ axM2.set_facecolor('red')
308
+ axL1.set_facecolor('red')
309
+ axL2.set_facecolor('red')
310
+ axG.set_facecolor('red')
311
+ axDt.set_facecolor('red')
312
+
313
+ else:
314
+ # Save data
315
+ collectButton.label.set_text('Collect Data')
316
+
317
+ # Reenable
318
+ sliderM1.active = True
319
+ sliderM2.active = True
320
+ sliderL1.active = True
321
+ sliderL2.active = True
322
+ sliderG.active = True
323
+ sliderDt.active = True
324
+
325
+ # Restore da colors
326
+ axM1.set_facecolor(axcolor)
327
+ axM2.set_facecolor(axcolor)
328
+ axL1.set_facecolor(axcolor)
329
+ axL2.set_facecolor(axcolor)
330
+ axG.set_facecolor(axcolor)
331
+ axDt.set_facecolor(axcolor)
332
+
333
+ # Save data to file
334
+ if collectedData:
335
+ filename = f"pendulum_data_{int(time.time())}.csv"
336
+ saveDataToFile(filename)
337
+ plt.figtext(0.5, 0.01, f"Data saved to {filename}", ha="center", bbox={"facecolor":"green", "alpha":0.5, "pad":5})
338
+ fig.canvas.draw_idle()
339
+
340
+ collectButton.on_clicked(toggleDataCollection)
341
+
342
+ def saveDataToFile(filename):
343
+ with open(filename, 'w', newline='') as csvfile:
344
+ fieldnames = ['time', 'theta1', 'omega1', 'theta2', 'omega2', 'x1', 'y1', 'x2', 'y2', 'l1', 'l2', 'm1', 'm2', 'g', 'dt']
345
+ writer = csv.DictWriter(csvfile, fieldnames=fieldnames)
346
+
347
+ writer.writeheader()
348
+ for dataPoint in collectedData:
349
+ writer.writerow(dataPoint)
350
+
351
+ # Super duper text box that tells you super duper controls idk what im saying anymore
352
+ infoText = """Controls:
353
+ - Use sliders to adjust parameters
354
+ - Toggle trails with checkboxes
355
+ - Pause/Play the simulation
356
+ - Reset to initial state
357
+ - Collect data for analysis
358
+ """
359
+ # Make da box
360
+ fig.text(0.02, 0.2, infoText, fontsize=9, bbox=dict(facecolor=axcolor, alpha=0.5))
361
+
362
+ plt.show()