CW-DoublePendulum 0.1.0__py3-none-any.whl → 0.2.1__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.
- {cw_doublependulum-0.1.0.dist-info → cw_doublependulum-0.2.1.dist-info}/METADATA +1 -1
- cw_doublependulum-0.2.1.dist-info/RECORD +7 -0
- cw_doublependulum-0.2.1.dist-info/entry_points.txt +2 -0
- cw_doublependulum-0.2.1.dist-info/top_level.txt +1 -0
- sim.py +386 -0
- cw_doublependulum-0.1.0.dist-info/RECORD +0 -6
- cw_doublependulum-0.1.0.dist-info/top_level.txt +0 -1
- main.py +0 -362
- {cw_doublependulum-0.1.0.dist-info → cw_doublependulum-0.2.1.dist-info}/WHEEL +0 -0
- {cw_doublependulum-0.1.0.dist-info → cw_doublependulum-0.2.1.dist-info}/licenses/LICENSE +0 -0
@@ -0,0 +1,7 @@
|
|
1
|
+
sim.py,sha256=S_DkT3_6DFTJixb3mcXJGUsSq_cPcjUntcAcd4aoVfE,14399
|
2
|
+
cw_doublependulum-0.2.1.dist-info/licenses/LICENSE,sha256=P_M8rIY4EFOQArRFqXCsxqdZyDp2kUCszxsaDapqA_4,1090
|
3
|
+
cw_doublependulum-0.2.1.dist-info/METADATA,sha256=lVVZNGXuEvAsin14PcUMfzBicWremJ5_Ps_-0IllRNY,719
|
4
|
+
cw_doublependulum-0.2.1.dist-info/WHEEL,sha256=0CuiUZ_p9E4cD6NyLD6UG80LBXYyiSYZOKDm5lp32xk,91
|
5
|
+
cw_doublependulum-0.2.1.dist-info/entry_points.txt,sha256=bl8GVAAafizZ1OprX6HOmfdP0pcJRYjsAEeKdxMTCiw,65
|
6
|
+
cw_doublependulum-0.2.1.dist-info/top_level.txt,sha256=lf-JWfiGnB0ZZ1Sd9fz8Zh9R5MCupexpcxpg21zinzs,4
|
7
|
+
cw_doublependulum-0.2.1.dist-info/RECORD,,
|
@@ -0,0 +1 @@
|
|
1
|
+
sim
|
sim.py
ADDED
@@ -0,0 +1,386 @@
|
|
1
|
+
def runSim():
|
2
|
+
import numpy as np
|
3
|
+
import matplotlib
|
4
|
+
matplotlib.use('TkAgg')
|
5
|
+
import matplotlib.pyplot as plt
|
6
|
+
from matplotlib.widgets import Slider, CheckButtons, Button
|
7
|
+
from matplotlib.animation import FuncAnimation
|
8
|
+
from matplotlib.collections import LineCollection
|
9
|
+
import time
|
10
|
+
import csv
|
11
|
+
import hashlib
|
12
|
+
|
13
|
+
# Initial parameters (all can be modified with sliders)
|
14
|
+
l1 = 1.0
|
15
|
+
l2 = 1.0
|
16
|
+
m1 = 1.0
|
17
|
+
m2 = 1.0
|
18
|
+
g = 9.81
|
19
|
+
dt = 0.02
|
20
|
+
|
21
|
+
# Initial conditions: [theta1, omega1, theta2, omega2]
|
22
|
+
state = np.array([np.pi / 2, 0, np.pi / 2, 0], dtype=float)
|
23
|
+
|
24
|
+
# Control flags
|
25
|
+
animationRunning = True
|
26
|
+
collectingData = False
|
27
|
+
|
28
|
+
collectedData = []
|
29
|
+
|
30
|
+
def derivs(state, l1, l2, m1, m2):
|
31
|
+
theta1, omega1, theta2, omega2 = state
|
32
|
+
delta = theta2 - theta1
|
33
|
+
|
34
|
+
denom1 = (m1 + m2) * l1 - m2 * l1 * np.cos(delta)**2
|
35
|
+
denom2 = (l2 / l1) * denom1
|
36
|
+
|
37
|
+
domega1 = (m2 * l1 * omega1**2 * np.sin(delta) * np.cos(delta) +
|
38
|
+
m2 * g * np.sin(theta2) * np.cos(delta) +
|
39
|
+
m2 * l2 * omega2**2 * np.sin(delta) -
|
40
|
+
(m1 + m2) * g * np.sin(theta1)) / denom1
|
41
|
+
|
42
|
+
domega2 = (-m2 * l2 * omega2**2 * np.sin(delta) * np.cos(delta) +
|
43
|
+
(m1 + m2) * g * np.sin(theta1) * np.cos(delta) -
|
44
|
+
(m1 + m2) * l1 * omega1**2 * np.sin(delta) -
|
45
|
+
(m1 + m2) * g * np.sin(theta2)) / denom2
|
46
|
+
|
47
|
+
return np.array([omega1, domega1, omega2, domega2])
|
48
|
+
|
49
|
+
def step():
|
50
|
+
global state
|
51
|
+
# 4th-order Runge-Kutta integrator for ODE
|
52
|
+
k1 = derivs(state, l1, l2, m1, m2)
|
53
|
+
k2 = derivs(state + 0.5 * dt * k1, l1, l2, m1, m2)
|
54
|
+
k3 = derivs(state + 0.5 * dt * k2, l1, l2, m1, m2)
|
55
|
+
k4 = derivs(state + dt * k3, l1, l2, m1, m2)
|
56
|
+
state += (dt / 6) * (k1 + 2 * k2 + 2 * k3 + k4)
|
57
|
+
|
58
|
+
def getPos():
|
59
|
+
# Returns full positional (and angle/omega) state, to minimize repeated code
|
60
|
+
theta1, omega1, theta2, omega2 = state
|
61
|
+
x1 = l1 * np.sin(theta1)
|
62
|
+
y1 = -l1 * np.cos(theta1)
|
63
|
+
x2 = x1 + l2 * np.sin(theta2)
|
64
|
+
y2 = y1 - l2 * np.cos(theta2)
|
65
|
+
return x1, y1, x2, y2, theta1, omega1, theta2, omega2
|
66
|
+
|
67
|
+
def updatePlotLimits():
|
68
|
+
# Calculate the range based on the diagonal extent of both pendulums
|
69
|
+
# Add 20% margin
|
70
|
+
maxRange = (l1 + l2) * 1.2
|
71
|
+
ax.set_xlim(-maxRange, maxRange)
|
72
|
+
ax.set_ylim(-maxRange, maxRange)
|
73
|
+
fig.canvas.draw_idle()
|
74
|
+
|
75
|
+
# Function to convert mass to marker size
|
76
|
+
def massToSize(mass):
|
77
|
+
return 3 + 5 * (mass ** 0.7)
|
78
|
+
|
79
|
+
# Make large figure for buttons and whatnot
|
80
|
+
fig = plt.figure(figsize=(12, 8))
|
81
|
+
# Adjust the main plot area to make room for UI elements
|
82
|
+
plt.subplots_adjust(left=0.2, right=0.8, bottom=0.2)
|
83
|
+
ax = plt.subplot(111)
|
84
|
+
ax.set_aspect('equal')
|
85
|
+
updatePlotLimits()
|
86
|
+
ax.plot(0, 0, 'ko') # Center point/pivot
|
87
|
+
ax.set_title('Double Pendulum Simulation')
|
88
|
+
|
89
|
+
# Pendulum components
|
90
|
+
line, = ax.plot([], [], '-', lw=2, color='black')
|
91
|
+
mass1, = ax.plot([], [], 'bo', markersize=massToSize(m1))
|
92
|
+
mass2, = ax.plot([], [], 'ro', markersize=massToSize(m2))
|
93
|
+
|
94
|
+
# Trail lines
|
95
|
+
trailLength = 100
|
96
|
+
trailSegments1 = []
|
97
|
+
trailSegments2 = []
|
98
|
+
trailCmap1 = plt.get_cmap('winter') # these don't really have any gradient for some reason, idc anymore
|
99
|
+
trailCmap2 = plt.get_cmap('autumn')
|
100
|
+
trail1 = ax.add_collection(LineCollection([], linewidths=2, cmap=trailCmap1, alpha=0.6))
|
101
|
+
trail2 = ax.add_collection(LineCollection([], linewidths=2, cmap=trailCmap2, alpha=0.6))
|
102
|
+
|
103
|
+
# Trace flags
|
104
|
+
showTrace1 = True
|
105
|
+
showTrace2 = True
|
106
|
+
|
107
|
+
def init():
|
108
|
+
# Clear pendulum lines and trails
|
109
|
+
line.set_data([], [])
|
110
|
+
mass1.set_data([], [])
|
111
|
+
mass2.set_data([], [])
|
112
|
+
trail1.set_segments([])
|
113
|
+
trail2.set_segments([])
|
114
|
+
return line, mass1, mass2, trail1, trail2
|
115
|
+
|
116
|
+
def update(frame):
|
117
|
+
global collectedData
|
118
|
+
|
119
|
+
if animationRunning:
|
120
|
+
step()
|
121
|
+
|
122
|
+
x1, y1, x2, y2, theta1, omega1, theta2, omega2 = getPos()
|
123
|
+
|
124
|
+
# Collect data and setup keys
|
125
|
+
if collectingData:
|
126
|
+
timestamp = time.time()
|
127
|
+
collectedData.append({
|
128
|
+
'time': timestamp,
|
129
|
+
'theta1': theta1,
|
130
|
+
'omega1': omega1,
|
131
|
+
'theta2': theta2,
|
132
|
+
'omega2': omega2,
|
133
|
+
'x1': x1,
|
134
|
+
'y1': y1,
|
135
|
+
'x2': x2,
|
136
|
+
'y2': y2,
|
137
|
+
'l1': l1,
|
138
|
+
'l2': l2,
|
139
|
+
'm1': m1,
|
140
|
+
'm2': m2,
|
141
|
+
'g': g,
|
142
|
+
'dt': dt
|
143
|
+
})
|
144
|
+
|
145
|
+
# Update pendulum position
|
146
|
+
line.set_data([0, x1, x2], [0, y1, y2])
|
147
|
+
mass1.set_data([x1], [y1])
|
148
|
+
mass2.set_data([x2], [y2])
|
149
|
+
|
150
|
+
# Update trail if animation is running
|
151
|
+
if animationRunning:
|
152
|
+
if len(trailSegments1) > 0:
|
153
|
+
prevX1, prevY1 = trailSegments1[-1][-1]
|
154
|
+
prevX2, prevY2 = trailSegments2[-1][-1]
|
155
|
+
trailSegments1.append([[prevX1, prevY1], [x1, y1]])
|
156
|
+
trailSegments2.append([[prevX2, prevY2], [x2, y2]])
|
157
|
+
else:
|
158
|
+
trailSegments1.append([[x1, y1], [x1, y1]])
|
159
|
+
trailSegments2.append([[x2, y2], [x2, y2]])
|
160
|
+
|
161
|
+
# Limit trail length
|
162
|
+
if len(trailSegments1) > trailLength:
|
163
|
+
trailSegments1.pop(0)
|
164
|
+
trailSegments2.pop(0)
|
165
|
+
|
166
|
+
# Update trail if enabled
|
167
|
+
if showTrace1 and len(trailSegments1) > 0:
|
168
|
+
n = len(trailSegments1)
|
169
|
+
alphas = np.linspace(0.0, 1.0, n)
|
170
|
+
trail1.set_segments(trailSegments1)
|
171
|
+
trail1.set_array(alphas)
|
172
|
+
else:
|
173
|
+
trail1.set_segments([])
|
174
|
+
|
175
|
+
if showTrace2 and len(trailSegments2) > 0:
|
176
|
+
n = len(trailSegments2)
|
177
|
+
alphas = np.linspace(0.0, 1.0, n)
|
178
|
+
trail2.set_segments(trailSegments2)
|
179
|
+
trail2.set_array(alphas)
|
180
|
+
else:
|
181
|
+
trail2.set_segments([])
|
182
|
+
|
183
|
+
return line, mass1, mass2, trail1, trail2
|
184
|
+
|
185
|
+
ani = FuncAnimation(fig, update, init_func=init, blit=True, interval=20)
|
186
|
+
|
187
|
+
# -------------------- LAYOUT ARRANGEMENT --------------------
|
188
|
+
axcolor = 'lightgoldenrodyellow'
|
189
|
+
|
190
|
+
# ---- SLIDERS (RIGHT SIDE) ----
|
191
|
+
sliderWidth = 0.15
|
192
|
+
sliderHeight = 0.03
|
193
|
+
sliderLeft = 0.82
|
194
|
+
sliderGap = 0.05
|
195
|
+
|
196
|
+
# Create slider axes on the right side
|
197
|
+
axM1 = plt.axes([sliderLeft, 0.7, sliderWidth, sliderHeight], facecolor=axcolor)
|
198
|
+
axM2 = plt.axes([sliderLeft, 0.7 - sliderGap, sliderWidth, sliderHeight], facecolor=axcolor)
|
199
|
+
axL1 = plt.axes([sliderLeft, 0.7 - 2*sliderGap, sliderWidth, sliderHeight], facecolor=axcolor)
|
200
|
+
axL2 = plt.axes([sliderLeft, 0.7 - 3*sliderGap, sliderWidth, sliderHeight], facecolor=axcolor)
|
201
|
+
axG = plt.axes([sliderLeft, 0.7 - 4*sliderGap, sliderWidth, sliderHeight], facecolor=axcolor)
|
202
|
+
axDt = plt.axes([sliderLeft, 0.7 - 5*sliderGap, sliderWidth, sliderHeight], facecolor=axcolor)
|
203
|
+
|
204
|
+
# Create sliders
|
205
|
+
sliderM1 = Slider(axM1, 'Mass 1', 0.1, 5.0, valinit=m1)
|
206
|
+
sliderM2 = Slider(axM2, 'Mass 2', 0.1, 5.0, valinit=m2)
|
207
|
+
sliderL1 = Slider(axL1, 'Length 1', 0.1, 2.0, valinit=l1)
|
208
|
+
sliderL2 = Slider(axL2, 'Length 2', 0.1, 2.0, valinit=l2)
|
209
|
+
sliderG = Slider(axG, 'Gravity', 0.1, 20.0, valinit=g)
|
210
|
+
sliderDt = Slider(axDt, 'Time Step', 0.005, 0.05, valinit=dt)
|
211
|
+
|
212
|
+
# ---- BUTTONS (LEFT SIDE) ----
|
213
|
+
buttonWidth = 0.15
|
214
|
+
buttonHeight = 0.05
|
215
|
+
buttonLeft = 0.02
|
216
|
+
buttonGap = 0.06
|
217
|
+
|
218
|
+
# Add checkbox buttons for trails
|
219
|
+
rax = plt.axes([buttonLeft, 0.7, buttonWidth, 0.1], facecolor=axcolor)
|
220
|
+
check = CheckButtons(rax, ['Trail 1', 'Trail 2'], [showTrace1, showTrace2])
|
221
|
+
|
222
|
+
# Add buttons
|
223
|
+
resetAx = plt.axes([buttonLeft, 0.6, buttonWidth, buttonHeight], facecolor=axcolor)
|
224
|
+
pauseAx = plt.axes([buttonLeft, 0.6 - buttonGap, buttonWidth, buttonHeight], facecolor=axcolor)
|
225
|
+
collectAx = plt.axes([buttonLeft, 0.6 - 2*buttonGap, buttonWidth, buttonHeight], facecolor=axcolor)
|
226
|
+
testAx = plt.axes([buttonLeft, 0.6 - 3*buttonGap, buttonWidth, buttonHeight], facecolor=axcolor)
|
227
|
+
|
228
|
+
resetButton = Button(resetAx, 'Reset', color=axcolor)
|
229
|
+
pauseButton = Button(pauseAx, 'Pause', color=axcolor)
|
230
|
+
collectButton = Button(collectAx, 'Collect Data', color=axcolor)
|
231
|
+
testButton = Button(testAx, 'Verify/test Simulation', color=axcolor)
|
232
|
+
|
233
|
+
# -------------------- CALLBACKS -------------------- be careful >:(
|
234
|
+
def updateSliders(val):
|
235
|
+
global m1, m2, l1, l2, g, dt
|
236
|
+
if not collectingData: # Only allow changes when not collecting data
|
237
|
+
m1 = sliderM1.val
|
238
|
+
m2 = sliderM2.val
|
239
|
+
l1 = sliderL1.val
|
240
|
+
l2 = sliderL2.val
|
241
|
+
g = sliderG.val
|
242
|
+
dt = sliderDt.val
|
243
|
+
# Update mass marker sizes based on new mass values
|
244
|
+
mass1.set_markersize(massToSize(m1))
|
245
|
+
mass2.set_markersize(massToSize(m2))
|
246
|
+
updatePlotLimits()
|
247
|
+
# Clear trails when parameters change
|
248
|
+
trailSegments1.clear()
|
249
|
+
trailSegments2.clear()
|
250
|
+
trail1.set_segments([])
|
251
|
+
trail2.set_segments([])
|
252
|
+
|
253
|
+
sliderM1.on_changed(updateSliders)
|
254
|
+
sliderM2.on_changed(updateSliders)
|
255
|
+
sliderL1.on_changed(updateSliders)
|
256
|
+
sliderL2.on_changed(updateSliders)
|
257
|
+
sliderG.on_changed(updateSliders)
|
258
|
+
sliderDt.on_changed(updateSliders)
|
259
|
+
|
260
|
+
def toggleTrace(label):
|
261
|
+
global showTrace1, showTrace2
|
262
|
+
if label == 'Trail 1':
|
263
|
+
showTrace1 = not showTrace1
|
264
|
+
if not showTrace1:
|
265
|
+
trail1.set_segments([])
|
266
|
+
elif label == 'Trail 2':
|
267
|
+
showTrace2 = not showTrace2
|
268
|
+
if not showTrace2:
|
269
|
+
trail2.set_segments([])
|
270
|
+
|
271
|
+
check.on_clicked(toggleTrace)
|
272
|
+
|
273
|
+
def reset(event):
|
274
|
+
global state, trailSegments1, trailSegments2
|
275
|
+
if not collectingData: # Only reset when not collecting data dummy
|
276
|
+
state = np.array([np.pi / 2, 0, np.pi / 2, 0], dtype=float)
|
277
|
+
trailSegments1.clear()
|
278
|
+
trailSegments2.clear()
|
279
|
+
trail1.set_segments([])
|
280
|
+
trail2.set_segments([])
|
281
|
+
|
282
|
+
resetButton.on_clicked(reset)
|
283
|
+
|
284
|
+
def toggleAnimation(event):
|
285
|
+
global animationRunning
|
286
|
+
animationRunning = not animationRunning
|
287
|
+
pauseButton.label.set_text('Play' if not animationRunning else 'Pause')
|
288
|
+
|
289
|
+
pauseButton.on_clicked(toggleAnimation)
|
290
|
+
|
291
|
+
def toggleDataCollection(event):
|
292
|
+
global collectingData, collectedData
|
293
|
+
|
294
|
+
collectingData = not collectingData
|
295
|
+
|
296
|
+
if collectingData:
|
297
|
+
collectButton.label.set_text('Stop Collecting')
|
298
|
+
collectedData = [] # Clear previous data
|
299
|
+
# Disable sliders to ensure good data | might change and allow different stuff, but i see weird behavior idk
|
300
|
+
sliderM1.active = False
|
301
|
+
sliderM2.active = False
|
302
|
+
sliderL1.active = False
|
303
|
+
sliderL2.active = False
|
304
|
+
sliderG.active = False
|
305
|
+
sliderDt.active = False
|
306
|
+
# Change slider colors to show they have been imprisoned, how sad it doesn't work
|
307
|
+
axM1.set_facecolor('red')
|
308
|
+
axM2.set_facecolor('red')
|
309
|
+
axL1.set_facecolor('red')
|
310
|
+
axL2.set_facecolor('red')
|
311
|
+
axG.set_facecolor('red')
|
312
|
+
axDt.set_facecolor('red')
|
313
|
+
else:
|
314
|
+
# Save data
|
315
|
+
collectButton.label.set_text('Collect Data')
|
316
|
+
# Reenable
|
317
|
+
sliderM1.active = True
|
318
|
+
sliderM2.active = True
|
319
|
+
sliderL1.active = True
|
320
|
+
sliderL2.active = True
|
321
|
+
sliderG.active = True
|
322
|
+
sliderDt.active = True
|
323
|
+
# Restore da colors
|
324
|
+
axM1.set_facecolor(axcolor)
|
325
|
+
axM2.set_facecolor(axcolor)
|
326
|
+
axL1.set_facecolor(axcolor)
|
327
|
+
axL2.set_facecolor(axcolor)
|
328
|
+
axG.set_facecolor(axcolor)
|
329
|
+
axDt.set_facecolor(axcolor)
|
330
|
+
# Save data to file
|
331
|
+
if collectedData:
|
332
|
+
filename = f"doublePendulumData{time.time()}.csv"
|
333
|
+
saveDataToFile(filename)
|
334
|
+
plt.figtext(0.5, 0.01, f"Data saved to {filename}", ha="center", bbox={"facecolor":"green", "alpha":0.5, "pad":5})
|
335
|
+
fig.canvas.draw_idle()
|
336
|
+
|
337
|
+
collectButton.on_clicked(toggleDataCollection)
|
338
|
+
|
339
|
+
def saveDataToFile(filename):
|
340
|
+
with open(filename, 'w', newline='') as csvfile:
|
341
|
+
fieldnames = ['time', 'theta1', 'omega1', 'theta2', 'omega2', 'x1', 'y1', 'x2', 'y2', 'l1', 'l2', 'm1', 'm2', 'g', 'dt']
|
342
|
+
writer = csv.DictWriter(csvfile, fieldnames=fieldnames)
|
343
|
+
writer.writeheader()
|
344
|
+
for dataPoint in collectedData:
|
345
|
+
writer.writerow(dataPoint)
|
346
|
+
|
347
|
+
# Super duper text box that tells you super duper controls idk what im saying anymore
|
348
|
+
infoText = """Controls:
|
349
|
+
- Use sliders to adjust parameters
|
350
|
+
- Toggle trails with checkboxes
|
351
|
+
- Pause/Play the simulation
|
352
|
+
- Reset to initial state
|
353
|
+
- Collect data for analysis
|
354
|
+
- Test your program. Reset after testing!
|
355
|
+
"""
|
356
|
+
|
357
|
+
# Make da box
|
358
|
+
fig.text(0.02, 0.2, infoText, fontsize=9, bbox=dict(facecolor=axcolor, alpha=0.5))
|
359
|
+
|
360
|
+
def runVerification(event):
|
361
|
+
# Set fixed values
|
362
|
+
global l1, l2, m1, m2, g, dt, state
|
363
|
+
l1, l2 = 1.0, 1.0
|
364
|
+
m1, m2 = 1.0, 1.0
|
365
|
+
g = 9.81
|
366
|
+
dt = 0.02
|
367
|
+
state = np.array([np.pi / 2, 0, np.pi / 2, 0], dtype=float)
|
368
|
+
|
369
|
+
steps = 1000
|
370
|
+
for _ in range(steps):
|
371
|
+
step()
|
372
|
+
|
373
|
+
# Final result to hash
|
374
|
+
result = np.round(state, decimals=6)
|
375
|
+
resultStr = ','.join(map(str, result))
|
376
|
+
hashDigest = hashlib.sha256(resultStr.encode()).hexdigest()
|
377
|
+
testHash = np.loadtxt('testHash.txt', dtype=str)
|
378
|
+
if hashDigest != testHash:
|
379
|
+
print("Test failed! Please re-download source code")
|
380
|
+
else:
|
381
|
+
print("Test passed")
|
382
|
+
|
383
|
+
|
384
|
+
testButton.on_clicked(runVerification)
|
385
|
+
|
386
|
+
plt.show()
|
@@ -1,6 +0,0 @@
|
|
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,,
|
@@ -1 +0,0 @@
|
|
1
|
-
main
|
main.py
DELETED
@@ -1,362 +0,0 @@
|
|
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()
|
File without changes
|
File without changes
|