Skip to content

Commit 4a2591a

Browse files
authored
Merge pull request #1047 from jepler/ulab
Add examples for upcoming ulab guide
2 parents 509ee94 + a17e8d1 commit 4a2591a

File tree

5 files changed

+539
-0
lines changed

5 files changed

+539
-0
lines changed

ulab_Crunch_Numbers_Fast/benchmark.py

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
import time
2+
import math
3+
import ulab
4+
import ulab.numerical
5+
6+
def mean(values):
7+
return sum(values) / len(values)
8+
9+
def normalized_rms(values):
10+
minbuf = int(mean(values))
11+
samples_sum = sum(
12+
float(sample - minbuf) * (sample - minbuf)
13+
for sample in values
14+
)
15+
16+
return math.sqrt(samples_sum / len(values))
17+
18+
def normalized_rms_ulab(values):
19+
minbuf = ulab.numerical.mean(values)
20+
values = values - minbuf
21+
samples_sum = ulab.numerical.sum(values * values)
22+
return math.sqrt(samples_sum / len(values))
23+
24+
25+
# Instead of using sensor data, we generate some data
26+
# The amplitude is 5000 so the rms should be around 5000/1.414 = 3536
27+
nums_list = [int(8000 + math.sin(i) * 5000) for i in range(100)]
28+
nums_array = ulab.array(nums_list)
29+
30+
def timeit(s, f, n=100):
31+
t0 = time.monotonic_ns()
32+
for _ in range(n):
33+
x = f()
34+
t1 = time.monotonic_ns()
35+
r = (t1 - t0) * 1e-6 / n
36+
print("%-20s : %8.3fms [result=%f]" % (s, r, x))
37+
38+
print("Computing the RMS value of 100 numbers")
39+
timeit("traditional", lambda: normalized_rms(nums_list))
40+
timeit("ulab", lambda: normalized_rms_ulab(nums_array))
Lines changed: 175 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,175 @@
1+
import time
2+
3+
import adafruit_bmp280
4+
import board
5+
import displayio
6+
import ulab
7+
import ulab.filter
8+
9+
# Blank the screen. Scrolling text causes unwanted delays.
10+
d = displayio.Group()
11+
board.DISPLAY.show(d)
12+
13+
# Sampling rate: 16Hz
14+
# Cutoff frequency: 0.16Hz
15+
# Transition bandwidth: 0.16Hz
16+
# Window type: Hamming
17+
# Filter has 311 coefficients
18+
taps = ulab.array([
19+
-0.000050679794726066, -0.000041099278318167, -0.000031279920668665,
20+
-0.000021183486597150, -0.000010770285292045, +0.000000000000000000,
21+
+0.000011167446754809, +0.000022770999889941, +0.000034847558259864,
22+
+0.000047431049079466, +0.000060551498686721, +0.000074234108254511,
23+
+0.000088498343199344, +0.000103357045109305, +0.000118815575023601,
24+
+0.000134870996840645, +0.000151511309510219, +0.000168714736477861,
25+
+0.000186449080596567, +0.000204671152403140, +0.000223326279275218,
26+
+0.000242347902542027, +0.000261657269119383, +0.000281163223679941,
27+
+0.000300762106756334, +0.000320337763510928, +0.000339761667195315,
28+
+0.000358893160569588, +0.000377579817760222, +0.000395657928211038,
29+
+0.000412953103529159, +0.000429281007152519, +0.000444448205872873,
30+
+0.000458253141344113, +0.000470487218795955, +0.000480936009263626,
31+
+0.000489380560741255, +0.000495598812776238, +0.000499367108150093,
32+
+0.000500461794444300, +0.000498660907473236, +0.000493745927786584,
33+
+0.000485503600706003, +0.000473727809671115, +0.000458221492033063,
34+
+0.000438798585855176, +0.000415285995764155, +0.000387525565446236,
35+
+0.000355376044004699, +0.000318715033091691, +0.000277440901501588,
36+
+0.000231474653767861, +0.000180761739242710, +0.000125273788160487,
37+
+0.000065010261293197, +0.000000000000000000, -0.000069697336247377,
38+
-0.000143989957415198, -0.000222752767634882, -0.000305826338672358,
39+
-0.000393016043088374, -0.000484091357342654, -0.000578785344322494,
40+
-0.000676794323931742, -0.000777777739462615, -0.000881358226495441,
41+
-0.000987121890034750, -0.001094618794499868, -0.001203363670049808,
42+
-0.001312836837542114, -0.001422485353209744, -0.001531724372895900,
43+
-0.001639938734420840, -0.001746484755374530, -0.001850692242341569,
44+
-0.001951866706278179, -0.002049291777482158, -0.002142231812333790,
45+
-0.002229934682745978, -0.002311634738053158, -0.002386555927898205,
46+
-0.002453915073551964, -0.002512925274028313, -0.002562799432345805,
47+
-0.002602753886341418, -0.002632012127569287, -0.002649808591023194,
48+
-0.002655392497711921, -0.002648031731496151, -0.002627016731069257,
49+
-0.002591664377536210, -0.002541321857718479, -0.002475370483091317,
50+
-0.002393229444145817, -0.002294359479963247, -0.002178266442894981,
51+
-0.002044504738458277, -0.001892680620886388, -0.001722455325210333,
52+
-0.001533548017297868, -0.001325738543930948, -0.001098869965763655,
53+
-0.000852850856865069, -0.000587657355512251, -0.000303334951952833,
54+
+0.000000000000000001, +0.000322159059450752, +0.000662880773589522,
55+
+0.001021830060775982, +0.001398597909331569, +0.001792701398335994,
56+
+0.002203584045179127, +0.002630616483032971, +0.003073097469789485,
57+
+0.003530255228366684, +0.004001249116626688, +0.004485171623483914,
58+
+0.004981050686118591, +0.005487852321559077, +0.006004483564265146,
59+
+0.006529795699742466, +0.007062587782654920, +0.007601610426384373,
60+
+0.008145569849526276, +0.008693132163411565, +0.009242927883419039,
61+
+0.009793556645595150, +0.010343592108937170, +0.010891587022627668,
62+
+0.011436078436539264, +0.011975593032464911, +0.012508652552774892,
63+
+0.013033779302562583, +0.013549501700820601, +0.014054359855790191,
64+
+0.014546911139352909, +0.015025735735186426, +0.015489442135386880,
65+
+0.015936672560369614, +0.016366108277098043, +0.016776474791055797,
66+
+0.017166546887869318, +0.017535153501103896, +0.017881182383493146,
67+
+0.018203584559716979, +0.018501378539810983, +0.018773654273367416,
68+
+0.019019576825867947, +0.019238389759765797, +0.019429418204303113,
69+
+0.019592071599501125, +0.019725846101288819, +0.019830326636332028,
70+
+0.019905188596781104, +0.019950199166862841, +0.019965218274992248,
71+
+0.019950199166862841, +0.019905188596781104, +0.019830326636332028,
72+
+0.019725846101288819, +0.019592071599501125, +0.019429418204303113,
73+
+0.019238389759765800, +0.019019576825867947, +0.018773654273367420,
74+
+0.018501378539810983, +0.018203584559716979, +0.017881182383493149,
75+
+0.017535153501103892, +0.017166546887869318, +0.016776474791055797,
76+
+0.016366108277098043, +0.015936672560369614, +0.015489442135386881,
77+
+0.015025735735186426, +0.014546911139352912, +0.014054359855790193,
78+
+0.013549501700820601, +0.013033779302562583, +0.012508652552774890,
79+
+0.011975593032464912, +0.011436078436539264, +0.010891587022627668,
80+
+0.010343592108937174, +0.009793556645595150, +0.009242927883419041,
81+
+0.008693132163411567, +0.008145569849526276, +0.007601610426384373,
82+
+0.007062587782654920, +0.006529795699742466, +0.006004483564265146,
83+
+0.005487852321559078, +0.004981050686118592, +0.004485171623483914,
84+
+0.004001249116626688, +0.003530255228366684, +0.003073097469789486,
85+
+0.002630616483032971, +0.002203584045179127, +0.001792701398335993,
86+
+0.001398597909331569, +0.001021830060775982, +0.000662880773589522,
87+
+0.000322159059450752, +0.000000000000000001, -0.000303334951952833,
88+
-0.000587657355512251, -0.000852850856865070, -0.001098869965763655,
89+
-0.001325738543930948, -0.001533548017297868, -0.001722455325210333,
90+
-0.001892680620886389, -0.002044504738458278, -0.002178266442894981,
91+
-0.002294359479963247, -0.002393229444145818, -0.002475370483091317,
92+
-0.002541321857718479, -0.002591664377536210, -0.002627016731069256,
93+
-0.002648031731496151, -0.002655392497711923, -0.002649808591023195,
94+
-0.002632012127569288, -0.002602753886341418, -0.002562799432345805,
95+
-0.002512925274028314, -0.002453915073551965, -0.002386555927898205,
96+
-0.002311634738053157, -0.002229934682745978, -0.002142231812333790,
97+
-0.002049291777482159, -0.001951866706278179, -0.001850692242341569,
98+
-0.001746484755374531, -0.001639938734420841, -0.001531724372895900,
99+
-0.001422485353209745, -0.001312836837542114, -0.001203363670049807,
100+
-0.001094618794499867, -0.000987121890034750, -0.000881358226495441,
101+
-0.000777777739462615, -0.000676794323931742, -0.000578785344322494,
102+
-0.000484091357342654, -0.000393016043088375, -0.000305826338672358,
103+
-0.000222752767634882, -0.000143989957415198, -0.000069697336247377,
104+
+0.000000000000000000, +0.000065010261293198, +0.000125273788160487,
105+
+0.000180761739242710, +0.000231474653767861, +0.000277440901501588,
106+
+0.000318715033091691, +0.000355376044004699, +0.000387525565446236,
107+
+0.000415285995764155, +0.000438798585855176, +0.000458221492033064,
108+
+0.000473727809671115, +0.000485503600706004, +0.000493745927786584,
109+
+0.000498660907473236, +0.000500461794444300, +0.000499367108150093,
110+
+0.000495598812776237, +0.000489380560741255, +0.000480936009263626,
111+
+0.000470487218795956, +0.000458253141344114, +0.000444448205872873,
112+
+0.000429281007152519, +0.000412953103529159, +0.000395657928211038,
113+
+0.000377579817760223, +0.000358893160569588, +0.000339761667195315,
114+
+0.000320337763510927, +0.000300762106756335, +0.000281163223679941,
115+
+0.000261657269119383, +0.000242347902542027, +0.000223326279275218,
116+
+0.000204671152403140, +0.000186449080596567, +0.000168714736477861,
117+
+0.000151511309510219, +0.000134870996840645, +0.000118815575023601,
118+
+0.000103357045109305, +0.000088498343199344, +0.000074234108254511,
119+
+0.000060551498686721, +0.000047431049079466, +0.000034847558259864,
120+
+0.000022770999889941, +0.000011167446754809, +0.000000000000000000,
121+
-0.000010770285292045, -0.000021183486597150, -0.000031279920668665,
122+
-0.000041099278318167, -0.000050679794726066,
123+
])
124+
125+
# How often we are going to poll the sensor (If you change this, you need
126+
# to change the filter above and the integration time below)
127+
dt = 62500000 # 16Hz, 62.5ms
128+
129+
# Wait until after deadline_ns has passed
130+
def sleep_deadline(deadline_ns):
131+
while time.monotonic_ns() < deadline_ns:
132+
pass
133+
134+
135+
# Initialize our sensor
136+
i2c = board.I2C()
137+
sensor = adafruit_bmp280.Adafruit_BMP280_I2C(i2c)
138+
sensor.standby_period = adafruit_bmp280.STANDBY_TC_1000
139+
# Disable in-sensor filtering, because we want to show how it's done in
140+
# CircuitPython
141+
sensor.iir_filter = adafruit_bmp280.IIR_FILTER_DISABLE
142+
sensor.overscan_pressure = adafruit_bmp280.OVERSCAN_X1
143+
144+
# And our data structures
145+
# The most recent data samples, equal in number to the filter taps
146+
data = ulab.zeros(len(taps))
147+
t0 = deadline = time.monotonic_ns()
148+
n = 0
149+
# Take an initial reading to subtract off later, so that the graph in mu
150+
# accentuates the small short term changes in pressure rather than the large
151+
# DC offset of around 980
152+
offset = sensor.pressure
153+
154+
while True:
155+
deadline += dt
156+
sleep_deadline(deadline)
157+
# Move the trace near the origin so small differences can be seen in the mu
158+
# plot window .. you wouldn't do this subtraction step if you are really
159+
# interested in absolute barometric pressure.
160+
value = sensor.pressure - offset
161+
if n == 0:
162+
# The first time, fill the filter with the initial value
163+
data = data + value
164+
else:
165+
# Otherwise, add it as the next sample
166+
ulab.numerical.roll(data, 1)
167+
data[-1] = value
168+
filtered = ulab.numerical.sum(data * taps)
169+
# Actually print every 10th value. This prints about 1.6 values per
170+
# second. You can print values more quickly by removing the 'if' and
171+
# making the print unconditional, or change the frequency of prints up
172+
# or down by changing the number '10'.
173+
if n % 10 == 0:
174+
print((filtered, value))
175+
n += 1

ulab_Crunch_Numbers_Fast/cluepulse.py

Lines changed: 148 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,148 @@
1+
import time
2+
3+
import adafruit_apds9960.apds9960
4+
import board
5+
import digitalio
6+
import ulab
7+
import ulab.filter
8+
9+
# Blank the screen. Scrolling text causes unwanted delays.
10+
import displayio
11+
d = displayio.Group()
12+
board.DISPLAY.show(d)
13+
14+
# Filter computed at https://fiiir.com/
15+
# Sampling rate: 8Hz
16+
# Cutoff freqency: 0.5Hz
17+
# Transition bandwidth 0.25Hz
18+
# Window type: Regular
19+
# Number of coefficients: 31
20+
# Manually trimmed to 16 coefficients
21+
taps = ulab.array([
22+
+0.861745279666917052/2,
23+
-0.134728583242092248,
24+
-0.124472980501612152,
25+
-0.108421190967457198,
26+
-0.088015688587190874,
27+
-0.065052714580474319,
28+
-0.041490993500537393,
29+
-0.019246940463156042,
30+
-0.000000000000000005,
31+
+0.014969842582454691,
32+
+0.024894596100322432,
33+
+0.029569415718397409,
34+
+0.029338562862396955,
35+
+0.025020274838643962,
36+
+0.017781854357373172,
37+
+0.008981905549472832,
38+
])
39+
40+
# How much reflected light is required before pulse sensor activates
41+
# These values are triggered when I bring my finger within a half inch.
42+
# The sensor works when the finger is pressed lightly against the sensor.
43+
PROXIMITY_THRESHOLD_HI = 225
44+
PROXIMITY_THRESHOLD_LO = 215
45+
46+
# These constants control how much the sensor amplifies received light
47+
APDS9660_AGAIN_1X = 0
48+
APDS9660_AGAIN_4X = 1
49+
APDS9660_AGAIN_16X = 2
50+
APDS9660_AGAIN_64X = 3
51+
52+
# How often we are going to poll the sensor (If you change this, you need
53+
# to change the filter above and the integration time below)
54+
dt = 125000000 # 8Hz, 125ms
55+
56+
# Wait until after deadline_ns has passed
57+
def sleep_deadline(deadline_ns):
58+
while time.monotonic_ns() < deadline_ns:
59+
pass
60+
61+
# Compute a high resolution crossing-time estimate for the sample, using a
62+
# linear model
63+
def estimated_cross_time(y0, y1, t0):
64+
m = (y1 - y0) / dt
65+
return t0 + round(-y1 / m)
66+
67+
i2c = board.I2C()
68+
sensor = adafruit_apds9960.apds9960.APDS9960(i2c)
69+
white_leds = digitalio.DigitalInOut(board.WHITE_LEDS)
70+
white_leds.switch_to_output(False)
71+
72+
def main():
73+
sensor.enable_proximity = True
74+
while True:
75+
# Wait for user to put finger over sensor
76+
while sensor.proximity() < PROXIMITY_THRESHOLD_HI:
77+
time.sleep(.01)
78+
79+
# After the finger is sensed, set up the color sensor
80+
sensor.enable_color = True
81+
# This sensor integration time is just a little bit shorter than 125ms,
82+
# so we should always have a fresh value when we ask for it, without
83+
# checking if a value is available.
84+
sensor.integration_time = 220
85+
# In my testing, 64X gain saturated the sensor, so this is the biggest
86+
# gain value that works properly.
87+
sensor.color_gain = APDS9660_AGAIN_4X
88+
white_leds.value = True
89+
90+
# And our data structures
91+
# The most recent data samples, equal in number to the filter taps
92+
data = ulab.zeros(len(taps))
93+
# The filtered value on the previous iteration
94+
old_value = 1
95+
# The times of the most recent pulses registered. Increasing this number
96+
# makes the estimation more accurate, but at the expense of taking longer
97+
# before a pulse number can be computed
98+
pulse_times = []
99+
# The estimated heart rate based on the recent pulse times
100+
rate = None
101+
# the number of samples taken
102+
n = 0
103+
104+
# Rather than sleeping for a fixed duration, we compute a deadline
105+
# in nanoseconds and wait for the new deadline time to arrive. This
106+
# helps the long term frequency of measurements better match the desired
107+
# frequency.
108+
t0 = deadline = time.monotonic_ns()
109+
# As long as their finger is over the sensor, capture data
110+
while sensor.proximity() >= PROXIMITY_THRESHOLD_LO:
111+
deadline += dt
112+
sleep_deadline(deadline)
113+
value = sum(sensor.color_data) # Combination of all channels
114+
ulab.numerical.roll(data, 1)
115+
data[-1] = value
116+
# Compute the new filtered variable by applying the filter to the
117+
# recent data samples
118+
filtered = ulab.numerical.sum(data * taps)
119+
120+
# We gathered enough data to fill the filters, and
121+
# the light value crossed the zero line in the positive direction
122+
# Therefore we need to record a pulse
123+
if n > len(taps) and old_value < 0 and filtered >= 0:
124+
# This crossing time is estimated, but it increases the pulse
125+
# estimate resolution quite a bit. If only the nearest 1/8s
126+
# was used for pulse estimation, the smallest pulse increment
127+
# that can be measured is 7.5bpm.
128+
cross = estimated_cross_time(old_value, filtered, deadline)
129+
# store this pulse time (in seconds since sensor-touch)
130+
pulse_times.append((cross - t0) * 1e-9)
131+
# and maybe delete an old pulse time
132+
del pulse_times[:-10]
133+
# And compute a rate based on the last recorded pulse times
134+
if len(pulse_times) > 1:
135+
rate = 60/(pulse_times[-1]-pulse_times[0])*(len(pulse_times)-1)
136+
old_value = filtered
137+
138+
# We gathered enough data to fill the filters, so report the light
139+
# value and possibly the estimated pulse rate
140+
if n > len(taps):
141+
print((filtered, rate))
142+
n += 1
143+
144+
# Turn off the sensor and the LED and go back to the top for another run
145+
sensor.enable_color = False
146+
white_leds.value = False
147+
print()
148+
main()

0 commit comments

Comments
 (0)