Skip to content

Commit 517469d

Browse files
committed
Update manifest.json with new modification times and add new markdown files for week 2 and week 4 manuals.
1 parent 9e5174e commit 517469d

14 files changed

Lines changed: 2421 additions & 536 deletions

AGENTS.md

Lines changed: 245 additions & 15 deletions
Large diffs are not rendered by default.
Lines changed: 155 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,155 @@
1+
# Week 2 manual plan: 2.2 averaging extension
2+
3+
Draft extension plan for [2.2_Timed_acquisitions.ipynb](2.2_Timed_acquisitions.ipynb). The user decision is that averaging moves out of the reduced Week 3.1 signal-processing manual and into the timed-acquisition manual. This keeps Week 3 focused on aliasing, anti-aliasing, FFT interpretation, and deliberate undersampling.
4+
5+
The extension uses selected material from the legacy 9B and 9C manuals:
6+
7+
* [9B_Digital_Filtering_and_Averaging_STUDENT.ipynb](../../../old/EI_manuals/9B_Digital_Filtering_and_Averaging_STUDENT.ipynb)
8+
* [9C_Signal_Denoising_by_Average_STUDENT.ipynb](../../../old/EI_manuals/9C_Signal_Denoising_by_Average_STUDENT.ipynb)
9+
10+
## 1. Why this belongs in Manual 2.2
11+
12+
Manual 2.2 already teaches timed measurements, sampling intervals, timestamps, and repeated acquisition loops. Averaging is a measurement strategy before it is a signal-processing trick. Students need to know when repeated samples can be averaged, when repeated traces need alignment, and why averaging does not fix systematic timing mistakes.
13+
14+
Week 3.1 will still compare averaging with analog and digital filters, but the mechanics should be learned here.
15+
16+
## 2. Extension scope
17+
18+
Keep the current Manual 2.2 structure, then add one compact averaging arc:
19+
20+
1. Predict how random noise changes when you average $N$ independent samples.
21+
2. Simulate repeated noisy acquisitions.
22+
3. Measure a fixed voltage repeatedly on the ALPACA and verify the standard-deviation trend.
23+
4. Compare point averaging with trace averaging.
24+
5. Move phase-alignment code to a deep dive.
25+
26+
To keep the practicum within one 3 h block, shorten the existing frequency sweep in Task I3 from five frequencies to three frequencies, or make the full sweep a deep dive.
27+
28+
## 3. Source triage
29+
30+
### 3.1 From 9B
31+
32+
| Old task | Tag | Goes to | Notes |
33+
|---|---|---|---|
34+
| Background: digital filtering and averaging definitions | CORE | 2.2 Background extension | Keep only averaging and the contrast with filtering. The digital-filter details stay in 3.1. |
35+
| Anticipate: triggering needed for filtering or averaging | TRANSFERABLE | 2.2 A5 | Reframe as "when can you average repeated points or repeated traces?" |
36+
| Simulate: noisy acquisitions and average | CORE | 2.2 S1 | Keep with simpler code and clear axes. |
37+
| Implement: acquire noisy LED signal | OMIT | Future sensors or deep dive | Too much optical setup for 2.2. |
38+
| Implement: average measured traces | TRANSFERABLE | 2.2 I3 or deep dive | Keep a simplified ALPACA version. |
39+
| Compare: choose filtering vs averaging | TRANSFERABLE | 2.2 C4 and 3.1 C3 | Keep the averaging half here. |
40+
41+
### 3.2 From 9C
42+
43+
| Old task | Tag | Goes to | Notes |
44+
|---|---|---|---|
45+
| Background: untriggered trace averaging fails | CORE | 2.2 Background extension | Keep because it is the central warning. |
46+
| Anticipate: phase-alignment strategy | TRANSFERABLE | 2.2 A5 | Keep short, no long code. |
47+
| Simulate: zero crossing and rolling arrays | NICE | Deep dive DD1 | Useful but too detailed for the main route. |
48+
| Implement: average untriggered synthetic data | CORE | 2.2 S1 | Keep as the visual proof. |
49+
| Implement: align by maximum and average | NICE | Deep dive DD1 | Keep as a course deep dive. |
50+
| Compare: artifacts from imperfect alignment | NICE | Deep dive DD1 | Keep for TA check-off. |
51+
52+
## 4. Proposed Manual 2.2 changes
53+
54+
### 4.1 Background addition
55+
56+
Add a short subsection after the existing sampling-rate explanation.
57+
58+
Working title: **Repeated measurements and averaging**
59+
60+
Content to add:
61+
62+
* If random noise has standard deviation $\sigma$, the average of $N$ independent samples has standard deviation approximately $\sigma / \sqrt{N}$.
63+
* Averaging does not remove a wrong offset, a wrong calibration, or aliasing.
64+
* There are two different operations:
65+
* Point averaging: repeat the same measurement condition and average the values.
66+
* Trace averaging: repeat the same waveform and average corresponding time points.
67+
* Trace averaging only works when traces are aligned in time or phase. If the start phase drifts, the signal can average away.
68+
69+
### 4.2 New Anticipate task
70+
71+
Add after current Task A4.
72+
73+
* **A5: Predict what averaging can and cannot improve**. A fixed voltage is measured repeatedly. The raw standard deviation is $12\,\mathrm{mV}$. Predict the standard deviation after averaging $N = 4$, $16$, and $64$ samples. Then answer two short questions: does averaging remove a $20\,\mathrm{mV}$ calibration offset, and does averaging recover a sine wave if every trace starts at a random phase?
74+
75+
### 4.3 New Simulate section
76+
77+
Manual 2.2 currently jumps from Anticipate to Practicum. Add one short Simulate task to fit the BASIC format more closely.
78+
79+
* **S1: Average noisy repeated measurements**. Run provided Python code that generates:
80+
* repeated measurements of a fixed voltage with random noise;
81+
* repeated traces of the same sine with aligned phase;
82+
* repeated traces of the same sine with random phase.
83+
84+
Students plot raw values and averages. They identify which average improves and which average fails.
85+
86+
### 4.4 New Implement and Investigate task
87+
88+
Add after current Task I2, before the frequency sweep. Shorten the frequency sweep if needed.
89+
90+
* **I3: Average repeated ADC readings of a fixed input**. Keep the potentiometer fixed near mid-scale. Collect 500 timed ADC samples. Compute block averages for $N = 1$, $4$, $16$, and $64$. Plot the measured standard deviation of the block averages versus $N$. Compare with the $\sigma / \sqrt{N}$ prediction from A5.
91+
92+
Rename the current frequency-response sweep from I3 to I4 if the notebook is edited.
93+
94+
Optional implementation detail:
95+
96+
* If the ALPACA input is too quiet to show a clear trend, use a high-value source resistor or a deliberately long wire to introduce small pickup. Keep the voltage inside the safe ALPACA input range.
97+
98+
### 4.5 New Compare and Conclude task
99+
100+
Add after current C3.
101+
102+
* **C4: Averaging decision table**. Fill a table with four cases: fixed noisy voltage, drifting voltage, aligned repeated sine traces, and unaligned repeated sine traces. For each case, decide whether averaging helps and what must be true for the answer to be yes.
103+
104+
### 4.6 Deep dive
105+
106+
Add one `deep-dive dropdown`.
107+
108+
* **DD1: Align traces before averaging**. Use a provided synthetic dataset adapted from 9C. First average unaligned traces and observe that the sine nearly disappears. Then align each trace by its maximum, crop to a common length, and average again. Explain the improvement and the artifacts.
109+
110+
This deep dive can later be referenced from Week 3.1 DD3.
111+
112+
## 5. Revised time budget for Manual 2.2
113+
114+
| Section | Current intent | New intent |
115+
|---|---:|---:|
116+
| Background | 10 to 15 min | 15 to 20 min |
117+
| Anticipate A1 to A4 | 35 to 45 min | 40 to 50 min with A5 |
118+
| Simulate | none | 10 to 15 min with S1 |
119+
| Implement I1 to I2 | 45 to 55 min | unchanged |
120+
| Averaging I3 | none | 20 min |
121+
| Frequency sweep | 35 to 45 min | 20 to 30 min, reduced to three frequencies |
122+
| Compare and Conclude | 25 min | 30 min with C4 |
123+
124+
The extension is feasible only if the frequency sweep is trimmed or moved partly into a deep dive.
125+
126+
## 6. Artifacts to add when editing the notebook
127+
128+
### 6.1 Code helpers
129+
130+
* `block_average(values, block_size)`: returns block means and their timestamps.
131+
* `simulate_averaging(seed=None)`: generates fixed-voltage and trace examples for S1.
132+
* `plot_std_vs_block_size(values, block_sizes)`: makes the I3 comparison plot.
133+
134+
### 6.2 Figures
135+
136+
| File | Used in | Notes |
137+
|---|---|---|
138+
| `images/averaging_random_noise.svg` | Background | Raw samples and averaged samples around the same mean. |
139+
| `images/trace_averaging_alignment.svg` | Background, DD1 | Aligned traces average to a sine; unaligned traces average toward zero. |
140+
141+
## 7. Risks and open points
142+
143+
* **ADC noise may be too small to see clearly**. Pilot the fixed-voltage measurement. If the raw standard deviation is tiny, use synthetic data in S1 as the main visual and keep I3 as a real-data confirmation.
144+
* **Trace averaging can become a coding task**. Keep alignment in the deep dive. The main manual should teach the decision rule, not force students to debug array indexing.
145+
* **Manual 2.2 currently has no Simulate section**. Adding S1 improves alignment with the BASIC format, but it does change the notebook structure.
146+
* **Frequency sweep needs trimming**. Without trimming, the extension risks pushing the IC part over 3 h.
147+
148+
## 8. Cross-walk to Week 3.1
149+
150+
| Week 2.2 extension item | Week 3.1 dependency |
151+
|---|---|
152+
| A5, S1: averaging reduces random noise under repeated identical conditions | 3.1 C3 compares averaging with analog and digital filtering. |
153+
| I3: block averaging from timed ADC samples | 3.1 uses the same timestamp discipline for FFT and undersampling. |
154+
| DD1: phase alignment before trace averaging | 3.1 DD3 can reuse the dataset or refer students back to this deep dive. |
155+
| C4: averaging decision table | 3.1 asks students to choose the right tool: average, digital filter, analog anti-alias filter, or undersample deliberately. |

book/manuals/week3/3.1_aliasing_and_anti_aliasing.ipynb

Lines changed: 206 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,17 @@
88
"# Aliasing and anti-aliasing"
99
]
1010
},
11+
{
12+
"cell_type": "markdown",
13+
"id": "aa-interactive-note",
14+
"metadata": {},
15+
"source": [
16+
"```{admonition} Interactive page\n",
17+
":class: interactive\n",
18+
"This page contains interactive elements. Press the launch button at the top right side in order to use them.\n",
19+
"```"
20+
]
21+
},
1122
{
1223
"cell_type": "markdown",
1324
"id": "497bf687",
@@ -248,15 +259,207 @@
248259
},
249260
{
250261
"cell_type": "markdown",
251-
"id": "d49c7d32",
262+
"id": "aa-simulate",
263+
"metadata": {},
264+
"source": [
265+
"### Simulate\n",
266+
"\n",
267+
"The demos below let you test aliasing and anti-aliasing before the practicum. Launch the page first (button at the top right), run the helper cell once, then run each demo. Change the marked variables and watch how the sampled spectrum reacts."
268+
]
269+
},
270+
{
271+
"cell_type": "markdown",
272+
"id": "aa-helpers-md",
273+
"metadata": {},
274+
"source": [
275+
"#### FFT and sampling helper functions\n",
276+
"\n",
277+
"Run this cell before the demos. These functions are provided as measurement tools, so you do not need to write the FFT from scratch."
278+
]
279+
},
280+
{
281+
"cell_type": "code",
282+
"execution_count": null,
283+
"id": "08247426",
284+
"metadata": {},
285+
"outputs": [],
286+
"source": [
287+
"import numpy as np\n",
288+
"import matplotlib.pyplot as plt\n",
289+
"\n",
290+
"\n",
291+
"def dominant_fft_peak(signal, rate, ignore_dc=True):\n",
292+
" \"\"\"Return the dominant FFT peak frequency and amplitude.\"\"\"\n",
293+
" frequency_axis, spectrum = calculate_fft(signal, rate)\n",
294+
" start = 1 if ignore_dc else 0\n",
295+
" peak_index = start + np.argmax(spectrum[start:])\n",
296+
" return frequency_axis[peak_index], spectrum[peak_index]\n",
297+
"\n",
298+
"\n",
299+
"def alias_frequency(signal_frequency, sampling_rate):\n",
300+
" \"\"\"Fold a frequency into the range from 0 to the Nyquist frequency.\"\"\"\n",
301+
" remainder = signal_frequency % sampling_rate\n",
302+
" if np.isclose(remainder, sampling_rate / 2):\n",
303+
" return 0.0\n",
304+
" if remainder < sampling_rate / 2:\n",
305+
" return remainder\n",
306+
" return sampling_rate - remainder\n",
307+
"\n",
308+
"\n",
309+
"def alias_phase_sign(signal_frequency, sampling_rate):\n",
310+
" \"\"\"Return +1 for same initial slope, -1 for opposite initial slope, and 0 for Nyquist ambiguity.\"\"\"\n",
311+
" remainder = signal_frequency % sampling_rate\n",
312+
" if np.isclose(remainder, 0) or np.isclose(remainder, sampling_rate / 2):\n",
313+
" return 0\n",
314+
" return 1 if remainder < sampling_rate / 2 else -1\n",
315+
"\n",
316+
"\n",
317+
"def rc_lowpass_gain(frequency, cutoff_frequency):\n",
318+
" \"\"\"Magnitude response of a first-order RC low-pass filter.\"\"\"\n",
319+
" frequency = np.asarray(frequency)\n",
320+
" return 1 / np.sqrt(1 + (frequency / cutoff_frequency) ** 2)\n",
321+
"\n",
322+
"\n",
323+
"def fold_periodic_samples(time, voltage, period):\n",
324+
" \"\"\"Fold time-stamped samples onto one known period and sort by phase.\"\"\"\n",
325+
" phase = (np.asarray(time) % period) / period\n",
326+
" order = np.argsort(phase)\n",
327+
" return phase[order], np.asarray(voltage)[order]\n"
328+
]
329+
},
330+
{
331+
"cell_type": "markdown",
332+
"id": "aa-demo1-md",
333+
"metadata": {},
334+
"source": [
335+
"#### Demo 1: aliasing of a single sine\n",
336+
"\n",
337+
"This demo builds one analog sine, samples it at `sampling_rate`, and finds the FFT peak of the sampled record. A sine above the Nyquist frequency folds back to a lower peak. Change `signal_frequency` and watch the measured peak and the phase sign. Try 100, 450, 500, 650, and 1100 Hz at a sampling rate of 1000 Hz."
338+
]
339+
},
340+
{
341+
"cell_type": "code",
342+
"execution_count": null,
343+
"id": "c9e566c3",
344+
"metadata": {},
345+
"outputs": [],
346+
"source": [
347+
"signal_frequency = 650 # Hz\n",
348+
"sampling_rate = 1000 # samples per second\n",
349+
"duration = 0.08 # seconds\n",
350+
"\n",
351+
"continuous_time = np.linspace(0, duration, 5000, endpoint=False)\n",
352+
"continuous_voltage = np.sin(2 * np.pi * signal_frequency * continuous_time)\n",
353+
"\n",
354+
"sample_time = np.arange(0, duration, 1 / sampling_rate)\n",
355+
"sample_voltage = np.sin(2 * np.pi * signal_frequency * sample_time)\n",
356+
"\n",
357+
"peak_frequency, peak_amplitude = dominant_fft_peak(sample_voltage, sampling_rate)\n",
358+
"print(f\"Predicted alias: {alias_frequency(signal_frequency, sampling_rate):.1f} Hz\")\n",
359+
"print(f\"Measured FFT peak: {peak_frequency:.1f} Hz\")\n",
360+
"print(f\"Phase sign: {alias_phase_sign(signal_frequency, sampling_rate)}\")\n",
361+
"\n",
362+
"frequency_axis, spectrum = calculate_fft(sample_voltage, sampling_rate)\n",
363+
"\n",
364+
"fig, axes = plt.subplots(1, 2, figsize=(12, 4))\n",
365+
"axes[0].plot(continuous_time * 1000, continuous_voltage, label=\"analog sine\")\n",
366+
"axes[0].plot(sample_time * 1000, sample_voltage, \"o\", label=\"ADC samples\")\n",
367+
"axes[0].set_xlabel(\"time [ms]\")\n",
368+
"axes[0].set_ylabel(\"voltage [a.u.]\")\n",
369+
"axes[0].legend()\n",
370+
"axes[0].grid(True)\n",
371+
"\n",
372+
"axes[1].stem(frequency_axis, spectrum, basefmt=\" \")\n",
373+
"axes[1].axvline(sampling_rate / 2, color=\"C1\", label=\"Nyquist\")\n",
374+
"axes[1].set_xlabel(\"frequency [Hz]\")\n",
375+
"axes[1].set_ylabel(\"FFT amplitude [a.u.]\")\n",
376+
"axes[1].set_xlim(0, sampling_rate / 2)\n",
377+
"axes[1].legend()\n",
378+
"axes[1].grid(True)\n",
379+
"plt.tight_layout()\n"
380+
]
381+
},
382+
{
383+
"cell_type": "markdown",
384+
"id": "aa-demo2-md",
385+
"metadata": {},
386+
"source": [
387+
"#### Demo 2: the anti-aliasing RC filter\n",
388+
"\n",
389+
"A first-order RC low-pass filter placed before the ADC reduces the high harmonics that would otherwise alias. The magnitude response is $|H(f)| = 1 / \\sqrt{1 + (f/f_c)^2}$. Change `C` (try 47 nF and 100 nF with R = 10 kΩ) and see which square-wave harmonics are reduced before sampling."
390+
]
391+
},
392+
{
393+
"cell_type": "code",
394+
"execution_count": null,
395+
"id": "bb694068",
396+
"metadata": {},
397+
"outputs": [],
398+
"source": [
399+
"fundamental = 100 # Hz\n",
400+
"harmonic_numbers = np.array([1, 3, 5, 7, 9])\n",
401+
"harmonic_frequencies = fundamental * harmonic_numbers\n",
402+
"harmonic_amplitudes = 1 / harmonic_numbers\n",
403+
"\n",
404+
"R = 10_000\n",
405+
"C = 100e-9\n",
406+
"cutoff_frequency = 1 / (2 * np.pi * R * C)\n",
407+
"filtered_amplitudes = harmonic_amplitudes * rc_lowpass_gain(harmonic_frequencies, cutoff_frequency)\n",
408+
"\n",
409+
"print(f\"cutoff frequency: {cutoff_frequency:.1f} Hz\")\n",
410+
"for k, f, raw, filtered in zip(harmonic_numbers, harmonic_frequencies, harmonic_amplitudes, filtered_amplitudes):\n",
411+
" print(f\"k={k:2d}, f={f:5.0f} Hz, before={raw:.3f}, after={filtered:.3f}\")\n",
412+
"\n",
413+
"plt.figure(figsize=(8, 4))\n",
414+
"plt.stem(harmonic_frequencies, harmonic_amplitudes, linefmt=\"C0-\", markerfmt=\"C0o\", basefmt=\" \", label=\"before RC\")\n",
415+
"plt.stem(harmonic_frequencies, filtered_amplitudes, linefmt=\"C1-\", markerfmt=\"C1s\", basefmt=\" \", label=\"after RC\")\n",
416+
"plt.axvline(cutoff_frequency, color=\"C2\", label=\"cutoff\")\n",
417+
"plt.xlabel(\"frequency [Hz]\")\n",
418+
"plt.ylabel(\"relative amplitude [a.u.]\")\n",
419+
"plt.legend()\n",
420+
"plt.grid(True)\n",
421+
"plt.tight_layout()\n"
422+
]
423+
},
424+
{
425+
"cell_type": "markdown",
426+
"id": "aa-demo3-md",
427+
"metadata": {},
428+
"source": [
429+
"#### Demo 3: deliberate undersampling (equivalent-time sampling)\n",
430+
"\n",
431+
"Undersampling is normally a problem, but for a stable periodic signal it becomes a measurement strategy. You sample at many phases of the same repeating waveform, then fold all samples onto one known period. The cell below folds an example sparse acquisition. Replace the example arrays with your measured time and voltage arrays during the practicum."
432+
]
433+
},
434+
{
435+
"cell_type": "code",
436+
"execution_count": null,
437+
"id": "abc6ff10",
252438
"metadata": {},
439+
"outputs": [],
253440
"source": [
254-
"### Simulate"
441+
"# Replace these arrays with your measured time and voltage arrays from Task I3.\n",
442+
"signal_frequency = 73 # Hz, example value\n",
443+
"period = 1 / signal_frequency\n",
444+
"\n",
445+
"# Example sparse measurement. Delete this block when using real ALPACA data.\n",
446+
"example_time = np.arange(0, 0.8, 1 / 120)\n",
447+
"example_voltage = np.sin(2 * np.pi * signal_frequency * example_time)\n",
448+
"\n",
449+
"phase, folded_voltage = fold_periodic_samples(example_time, example_voltage, period)\n",
450+
"\n",
451+
"plt.figure(figsize=(8, 4))\n",
452+
"plt.plot(phase, folded_voltage, \"o\", label=\"folded samples\")\n",
453+
"plt.xlabel(\"phase within one period\")\n",
454+
"plt.ylabel(\"voltage [a.u.]\")\n",
455+
"plt.grid(True)\n",
456+
"plt.legend()\n",
457+
"plt.tight_layout()\n"
255458
]
256459
},
257460
{
258461
"cell_type": "markdown",
259-
"id": "685e192f",
462+
"id": "a1776758",
260463
"metadata": {},
261464
"source": [
262465
"### Implement and Investigate\n",
@@ -273,12 +476,6 @@
273476
"Tell them to set the sampling duration so they record 5 full cycles of the sine wave\n",
274477
"\n"
275478
]
276-
},
277-
{
278-
"cell_type": "markdown",
279-
"id": "82f4f7b4",
280-
"metadata": {},
281-
"source": []
282479
}
283480
],
284481
"metadata": {

book/manuals/week4/4.1_opamp_basics.ipynb

100644100755
File mode changed.

0 commit comments

Comments
 (0)