Episode 10: Choosing a Reference
Re-reference a stream with Reference, to the common average or to one electrode, and measure what each choice does to the signal.
An EEG channel is not a measurement of one electrode. It is a voltage difference between that electrode and a reference, so choosing the reference is choosing what “zero” means, and it changes every number that follows.
The generator puts an identical 10 Hz sine on every channel and independent noise on each: a fair model of the real thing, since much of what all electrodes share is not brain, though a broad potential such as alpha or a P300 is shared too, and the average removes that as well. The channels are named first, as Episode 9 taught, so a reference can be chosen by electrode.
31 # The same 10 Hz sine on every channel, independent noise on each: what
32 # all electrodes share is usually not brain
33 source = gp.Generator(sampling_rate=SAMPLING_RATE, channel_count=8,
34 signal_frequency=10, signal_amplitude=1.0,
35 noise_amplitude=0.2)
36
37 # Named first (Episode 9), so the reference can be chosen by electrode
38 labeler = gp.ChannelLabeler(labels=ELECTRODES)
With no argument Reference takes the common average: every channel minus the mean over all of them. Given a label it references against that single electrode; given two, their mean, the linked-mastoid convention; and pairs= builds explicit [active, reference] combinations, one output channel each. A Collector keeps each version, and the as-recorded one for comparison.
40 # No argument: the common average. A label: that electrode
41 car = gp.Reference()
42 cz = gp.Reference(reference="Cz")
43
44 as_recorded = gp.Collector(name="as_recorded")
45 common_average = gp.Collector(name="common_average")
46 against_cz = gp.Collector(name="against_Cz")
47
48 p.connect(source, labeler)
49 p.connect(labeler, as_recorded)
50 p.connect(labeler, car)
51 p.connect(car, common_average)
52 p.connect(labeler, cz)
53 p.connect(cz, against_cz)
Run the script. One number per version, the root mean square over the second half of the run:
as recorded rms = ... channels = 8
common average rms = ... channels = 8
referenced to Cz rms = ... channels = 8
As recorded, the shared sine dominates. The common average cancels it almost exactly, leaving each channel’s own noise. Referencing to Cz cancels it too, but adds Cz’s own noise to every channel, which is why that number is larger, and turns Cz itself into a row of zeros, since a channel referenced to itself is nothing: drop that row before anything downstream sees it. Eight electrodes also make a rough common average; the ideal one needs dozens spread over the head. The noise is random, so your numbers differ a little; the order does not.
In this episode, you learned how to:
re-reference to the common average or to an electrode with Reference
name channels first so a reference can be chosen by name
compare choices on one number rather than by eye
state the choice you made: there is no neutral one
Proceed to Episode 11, where BandPower reduces a spectrum to a few named bands.
Nodes in this episode: Generator, ChannelLabeler, Reference, Collector
File s2e10_choosing_a_reference.py – View file on GitHub
1"""Season 2, Episode 10: a Reference decides what zero means."""
2
3import time
4
5import numpy as np
6
7import gpype as gp
8
9SAMPLING_RATE = 250
10ELECTRODES = ["Fz", "C3", "Cz", "C4", "P3", "Pz", "P4", "Oz"]
11
12
13def rms(collector):
14 """Root mean square of the second half of what was collected."""
15 result = collector.result
16 if result is None:
17 return float("nan")
18 block = result.data
19 return float(np.sqrt(np.mean(np.square(block[len(block) // 2:]))))
20
21
22def channels(collector):
23 result = collector.result
24 return 0 if result is None else result.channel_count
25
26
27if __name__ == "__main__":
28
29 p = gp.Pipeline()
30
31 # The same 10 Hz sine on every channel, independent noise on each: what
32 # all electrodes share is usually not brain
33 source = gp.Generator(sampling_rate=SAMPLING_RATE, channel_count=8,
34 signal_frequency=10, signal_amplitude=1.0,
35 noise_amplitude=0.2)
36
37 # Named first (Episode 9), so the reference can be chosen by electrode
38 labeler = gp.ChannelLabeler(labels=ELECTRODES)
39
40 # No argument: the common average. A label: that electrode
41 car = gp.Reference()
42 cz = gp.Reference(reference="Cz")
43
44 as_recorded = gp.Collector(name="as_recorded")
45 common_average = gp.Collector(name="common_average")
46 against_cz = gp.Collector(name="against_Cz")
47
48 p.connect(source, labeler)
49 p.connect(labeler, as_recorded)
50 p.connect(labeler, car)
51 p.connect(car, common_average)
52 p.connect(labeler, cz)
53 p.connect(cz, against_cz)
54
55 p.start()
56 time.sleep(2.0)
57 p.stop()
58 p.close()
59
60 print(f"as recorded rms = {rms(as_recorded):6.3f}"
61 f" channels = {channels(as_recorded)}")
62 print(f"common average rms = {rms(common_average):6.3f}"
63 f" channels = {channels(common_average)}")
64 print(f"referenced to Cz rms = {rms(against_cz):6.3f}"
65 f" channels = {channels(against_cz)}")