-
Notifications
You must be signed in to change notification settings - Fork 86
/
particle_physics.py
238 lines (179 loc) · 7.29 KB
/
particle_physics.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
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
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
#!/usr/bin/env python3
"""A 1D physics simulating LFO, inspired by the ADDAC503
(https://www.addacsystem.com/en/products/modules/addac500-series/addac503)
"""
from europi import *
from europi_script import EuroPiScript
from experimental.knobs import KnobBank
from experimental.math_extras import rescale
import math
import time
EARTH_GRAVITY = 9.8
MIN_GRAVITY = 0.1
MAX_GRAVITY = 20
MIN_HEIGHT = 0.1
MAX_HEIGHT = 10.0
MIN_SPEED = -5.0
MAX_SPEED = 5.0
MIN_ELASTICITY = 0.0
MAX_ELASTICITY = 0.9
## If a bounce reaches no higher than this, assume we've come to rest
ASSUME_STOP_PEAK = 0.002
class Particle:
def __init__(self):
self.y = 0.0
self.dy = 0.0
self.last_update_at = time.ticks_ms()
self.hit_ground = False
self.reached_apogee = False
self.stopped = True
self.peak_height = 0.0
def set_initial_position(self, height, velocity):
self.peak_height = height
self.y = height
self.dy = velocity
self.last_update_at = time.ticks_ms()
def update(self, g, elasticity):
"""Update the particle position based on the ambient gravity & elasticy of the particle
"""
now = time.ticks_ms()
delta_t = time.ticks_diff(now, self.last_update_at) / 1000.0
new_dy = self.dy - delta_t * g
new_y = self.y + self.dy * delta_t
# if we were going up, but now we're going down we've reached apogee
self.reached_apogee = new_dy <= 0 and self.dy >= 0
if self.reached_apogee:
self.peak_height = self.y
# if the vertical position is zero or negative, we've hit the ground
self.hit_ground = new_y <= 0
if self.hit_ground:
#new_y = 0
new_dy = abs(self.dy * elasticity) # bounce upwards, reduding the velocity by our elasticity modifier
self.stopped = self.peak_height <= ASSUME_STOP_PEAK
if self.stopped:
new_y = 0
new_dy = 0
self.dy = new_dy
self.y = new_y
self.last_update_at = now
class ParticlePhysics(EuroPiScript):
def __init__(self):
settings = self.load_state_json()
self.gravity = settings.get("gravity", 9.8)
self.initial_velocity = settings.get("initial_velocity", 0.0)
self.release_height = settings.get("height", 10.0)
self.elasticity = settings.get("elasticity", 0.75)
self.k1_bank = (
KnobBank.builder(k1)
.with_locked_knob("height", initial_percentage_value=rescale(self.release_height, MIN_HEIGHT, MAX_HEIGHT, 0, 1))
.with_locked_knob("gravity", initial_percentage_value=rescale(self.gravity, MIN_GRAVITY, MAX_GRAVITY, 0, 1))
.build()
)
self.k2_bank = (
KnobBank.builder(k2)
.with_locked_knob("elasticity", initial_percentage_value=rescale(self.elasticity, MIN_ELASTICITY, MAX_ELASTICITY, 0, 1))
.with_locked_knob("speed", initial_percentage_value=rescale(self.initial_velocity, MIN_SPEED, MAX_SPEED, 0, 1))
.build()
)
self.particle = Particle()
self.release_before_next_update = False
self.alt_knobs = False
@din.handler
def on_din_rising():
self.reset()
@b1.handler
def on_b1_press():
self.reset()
@b2.handler
def on_b2_press():
self.alt_knobs = True
self.k1_bank.next()
self.k2_bank.next()
@b2.handler_falling
def on_b2_release():
self.alt_knobs = False
self.k1_bank.next()
self.k2_bank.next()
@classmethod
def display_name(cls):
return "ParticlePhysics"
def save(self):
state = {
"gravity" : self.gravity,
"initial_velocity" : self.initial_velocity,
"height" : self.release_height,
"elasticity" : self.elasticity
}
self.save_state_json(state)
def reset(self):
self.release_before_next_update = True
def draw(self):
oled.fill(0)
row_1_color = 1
row_2_color = 2
if self.alt_knobs:
oled.fill_rect(0, CHAR_HEIGHT+1, OLED_WIDTH, CHAR_HEIGHT+1, 1)
row_2_color = 0
else:
oled.fill_rect(0, 0, OLED_WIDTH, CHAR_HEIGHT+1, 1)
row_1_color = 0
oled.text(f"h: {self.release_height:0.2f} e: {self.elasticity:0.2f}", 0, 0, row_1_color)
oled.text(f"g: {self.gravity:0.2f} v: {self.initial_velocity:0.2f}", 0, CHAR_HEIGHT+1, row_2_color)
# a horizontal representation of the particle bouncing off the left edge of the screen
oled.pixel(int(rescale(self.particle.y, 0, self.release_height, 0, OLED_WIDTH)), 3 * CHAR_HEIGHT, 1)
oled.show()
def main(self):
while True:
g = round(rescale(self.k1_bank["gravity"].percent(), 0, 1, MIN_GRAVITY, MAX_GRAVITY), 2)
h = round(rescale(self.k1_bank["height"].percent(), 0, 1, MIN_HEIGHT, MAX_HEIGHT), 2)
v = round(rescale(self.k2_bank["speed"].percent(), 0, 1, MIN_SPEED, MAX_SPEED), 2)
e = round(rescale(self.k2_bank["elasticity"].percent(), 0, 1, MIN_ELASTICITY, MAX_ELASTICITY), 2)
# the maximum veliocity we can attain, given the current parameters
# d = 1/2 aT^2 -> T = sqrt(2d/a)
h2 = 0
v2 = 0
if v > 0:
# initial upward velocity; add this to the initial height
t = v / g
h2 = v * t
else:
v2 = abs(v)
t = math.sqrt(2 * (h+h2) / g)
max_v = g * t + v2
if g != self.gravity or \
h != self.release_height or \
v != self.initial_velocity or \
e != self.elasticity:
self.gravity = g
self.initial_velocity = v
self.release_height = h
self.elasticity = e
self.save()
self.draw()
if self.release_before_next_update:
self.particle.set_initial_position(self.release_height, self.initial_velocity)
self.release_before_next_update = False
self.particle.update(self.gravity, self.elasticity)
# CV 1 outputs a gate whenever we hit the ground
if self.particle.hit_ground:
cv1.on()
else:
cv1.off()
# CV 2 outputs a trigger whenever we reach peak altitude and start falling again
if self.particle.reached_apogee:
cv2.on()
else:
cv2.off()
# CV 3 outputs a gate when the particle comes to rest
if self.particle.stopped:
cv3.on()
else:
cv3.off()
# CV 4 outputs control voltage based on the height of the particle
cv4.voltage(rescale(self.particle.y, 0, MAX_HEIGHT, 0, MAX_OUTPUT_VOLTAGE))
# CV 5 outputs control voltage based on the speed of the particle
cv5.voltage(rescale(abs(self.particle.dy), 0, max_v, 0, MAX_OUTPUT_VOLTAGE))
# TODO: I don't know what to use CV6 for. But hopefully I'll think of something
cv6.off()
if __name__ == "__main__":
ParticlePhysics().main()