Episode 2: Routing Signals

Discover how to manage signal flow within your pipelines. This episode covers combining, selecting, splitting, and mixing signals, which are the foundation for building complex, multi-stream processing setups.

The Router node in g.Pype is a flexible tool for managing signal flow in your pipeline. With a single node, you can:

  • Combine signals from multiple input ports into one output port

  • Select specific channels from one input port to one output port

  • Split signals from one input port to multiple output ports

  • Mix signals from several input ports to several output ports

All these behaviors are controlled by the input_channels and output_channels parameters. Below, we will explore how to use the Router for each scenario.

Combine Signals

The Router node can be configured to combine signals from multiple input sources. To demonstrate this, create four sources source1 to source4 as shown in the code below. Each source generates a single-channel sinusoid with frequencies 2, 4, 6, and 8 Hz, respectively. Next, create a Router node named combiner with input_channels=[[0], [0], [0], [0]] to combine these four single-channel sources into one multi-channel signal:

 1import gpype as gp
 2
 3if __name__ == "__main__":
 4
 5    app = gp.MainApp()
 6    p = gp.Pipeline()
 7
 8    source1, source2, source3, source4 = [
 9        gp.Generator(channel_count=1,
10                     signal_frequency=f,
11                     signal_amplitude=25) for f in (2, 4, 6, 8)
12    ]
13
14    combiner = gp.Router(input_channels=[[0], [0], [0], [0]])
15
16    scope = gp.TimeSeriesScope()

Note – input_channels List Notation

The input_channels parameter can be a list of lists: each entry of the outer list corresponds to an input port, while each inner list specifies the channel indices to be taken from that port. We will explore an alternative dictionary-style notation below.

In this example, the combiner node configured with four input ports, each taking the first channel of its connected signal. To connect these input ports to the four sources created above, it is necessary to understand the port-naming convention in g.Pype nodes.

Note – Router Input Port Naming

The Router node automatically creates its input ports from the input_channels parameter. These ports are named implicitly as in1 through inN, where N is the number of input ports.

Note – Default Port Naming

By default, g.Pype nodes with a single input or output port assign the names in and out to these ports, unless specified otherwise.

Given this convention, the four input ports of the combiner node are named in1 through in4. Connect each of these ports to the corresponding source as shown below:

16    scope = gp.TimeSeriesScope()
17
18    p.connect(source1, combiner["in1"])
19    p.connect(source2, combiner["in2"])
20    p.connect(source3, combiner["in3"])
21    p.connect(source4, combiner["in4"])
22    p.connect(combiner, scope)
23
24    app.add_widget(scope)
25
26    p.start()
27    app.run()  # blocking until window is closed
28    p.stop()

Note – Port Addressing Scheme

In the connect() method of the Pipeline class, ports are addressed using dictionary-style notation. For example, p.connect(senderNode["out"], receiverNode["in"]) connects the output port out of senderNode to the input port in of receiverNode.

For nodes with only one input and/or one output port, this explicit addressing scheme is usually omitted. In such cases, the recommended shorthand is p.connect(senderNode, receiverNode), which connects the single output port of senderNode to the single input receiverNode.

Run the script and observe how the four individual sources are shown as one multi-channel signal in the scope.

S2E2 example screenshot

Figure 21: Four different sources combined into one signal.

Select Channels

The Router node can select individual channels from its input and route them to the output. To demonstrate this, let’s extract the first and last channel from the combined signal in the previous example. To do this, add a selector node with parameter output_channels=[[0, 3]]. This configuration routes the first and last channel of the input signal to the output port. Place the selector node between the combiner and the scope in the pipeline.

14    combiner = gp.Router(input_channels=[[0], [0], [0], [0]])
15    selector = gp.Router(output_channels=[[0, 3]])
16
17    scope = gp.TimeSeriesScope()
18
19    p.connect(source1, combiner["in1"])
20    p.connect(source2, combiner["in2"])
21    p.connect(source3, combiner["in3"])
22    p.connect(source4, combiner["in4"])
23    p.connect(combiner, selector)
24    p.connect(selector, scope)

Note – Internal Routing Procedure

The Router node first combines the input channels (as defined by input_channels) into a single internal multi-channel signal. This signal is then routed to the output ports according to the output_channels parameter.

Note – output_channels List Notation

The output_channels parameter can be a list of lists: Each entry of the outer list defines one output port, while each inner list specifies the channel indices (from the internal multi-channel signal) to be routed to that port. We will explore an alternative dictionary-style notation below.

Run the script and observe that only the first and last channels are shown in the scope.

S2E2 example screenshot

Figure 22: Selected first and last channel from the combined four-channel signal.

Split Signals

The Router node can split a signal by routing selected input channels to multiple output ports. To demonstrate this, modify the parameter in line 15 of the code above to output_channels=[[0, 3], [2, 1]] and, for clarity, rename the node to splitter. This configuration creates two output ports: out1 receives the first and last channel [0, 3] of the input signal, while out2 receives the third and second channel [2, 1]. To visualize the two data streams simulataneously, create two scopes scope1 and scope2, and connect them to the two output ports of the splitter node.

14    combiner = gp.Router(input_channels=[[0], [0], [0], [0]])
15    splitter = gp.Router(output_channels=[[0, 3], [1, 2]])
16
17    scope1 = gp.TimeSeriesScope()
18    scope2 = gp.TimeSeriesScope()
19
20    p.connect(source1, combiner["in1"])
21    p.connect(source2, combiner["in2"])
22    p.connect(source3, combiner["in3"])
23    p.connect(source4, combiner["in4"])
24    p.connect(combiner, splitter)
25    p.connect(splitter["out1"], scope1)
26    p.connect(splitter["out2"], scope2)

Run the script and observe how the four-channel signal is split into two data streams, each shown in its own scope.

S2E2 example screenshot

Figure 23: Split signal shown in two scopes.

Mix Signals

The Router node can mix signals by routing channels from multiple input ports to multiple output ports. To illustrate this, let’s restore the original channel order from the previous example. In that example, the output channels were defined as output_channels=[[0, 3], [2, 1]]. Because the internal, combined signal is indexed sequentially, the original order is restored via the index sequence 0, 2, 3, 1. To achieve this, create a new Router node named mixer with parameters input_channels set to [gp.Router.ALL, gp.Router.ALL] and output_channels set to [[0, 2], [3, 1]]. Insert the mixer node between the splitter and the two scopes in the pipeline:

14    combiner = gp.Router(input_channels=[[0], [0], [0], [0]])
15    splitter = gp.Router(output_channels=[[0, 3], [1, 2]])
16    mixer = gp.Router(input_channels=[gp.Router.ALL, gp.Router.ALL],
17                      output_channels=[[0, 2], [3, 1]])
18
19    scope1 = gp.TimeSeriesScope()
20    scope2 = gp.TimeSeriesScope()
21    p.connect(source1, combiner["in1"])
22    p.connect(source2, combiner["in2"])
23    p.connect(source3, combiner["in3"])
24    p.connect(source4, combiner["in4"])
25    p.connect(combiner, splitter)
26    p.connect(splitter["out1"], mixer["in1"])
27    p.connect(splitter["out2"], mixer["in2"])
28    p.connect(mixer["out1"], scope1)
29    p.connect(mixer["out2"], scope2)

Note – Router.ALL Selector

For convenience and readability, the gp.Router.ALL constant can be used to reference all available channels, either received from a given input port or routed to a given output port.

Run the script and observe how the original channel order is restored, showing the signals with increasing frequency in their respective scopes.

S2E2 example screenshot

Figure 24: Restored order of signals.

Great! You now know how to flexibly route signals within a g.Pype pipeline. Next, we’ll begin a project that extracts alpha power from EEG signals. This project will span several upcoming episodes, gradually expanding as new nodes are introduced. Be sure to follow along with the first step below!

Project: Alpha Power Extraction (Step 1)

In this training project, we will build a g.Pype application step by step to extract alpha-band power from EEG using standard library nodes. First, create three signal sources with the Generator to synthesize a realistic composite test signal:

  • an alpha source source_alpha producing a 10 Hz sinusoid,

  • a modulator source_mod producing a 0.2 Hz sinusoid that modulates the alpha amplitude,

  • a noise source source_noise to mimic background EEG.

To visualize these three sources (and the derived signals you will create next) in a single TimeSeriesScope, instantiate a Router (router) to combine them into one multi-channel stream.

 1import gpype as gp
 2
 3if __name__ == "__main__":
 4
 5    app = gp.MainApp()
 6    p = gp.Pipeline()
 7
 8    source_alpha = gp.Generator(channel_count=1,
 9                                signal_frequency=10,
10                                signal_amplitude=25)
11
12    source_mod = gp.Generator(channel_count=1,
13                              signal_frequency=0.2,
14                              signal_amplitude=25)
15
16    source_noise = gp.Generator(channel_count=1,
17                                noise_amplitude=25)
18
19    router = gp.Router(input_channels={"alpha": [0],
20                                       "modulator": [0],
21                                       "noise": [0]})
22
23    scope = gp.TimeSeriesScope()

Note – input_channels / output_channels Dictionary Notation

Both the input_channels and output_channels can also be specified using a dictionary-style notation. In this form, each dictionary key corresponds to a port name, and the associated value is a list of channel indices to be taken from (for input ports) or routed to (for output ports).

Using the dictionary-style notation for the input_channels parameter, the router node is created with three input ports alpha, modulator, and noise. Connect the three sources to these ports.

25    p.connect(source_alpha, router["alpha"])
26    p.connect(source_mod, router["modulator"])
27    p.connect(source_noise, router["noise"])
28    p.connect(router, scope)
29
30    app.add_widget(scope)
31
32    p.start()
33    app.run()  # blocking until window is closed
34    p.stop()
S2E2 example screenshot

Figure 25: Prototype signals for alpha modulation.

Finished! Run the script to see the three sources in the scope.

With your understanding now extended through the alpha power example, we are ready to conclude this episode.

Summary

In this episode, you learned how to use the Router node to

  • combine signals from multiple input ports into one output port,

  • select specific channels from one input port to one output port,

  • split signals from one input port to multiple output ports, and

  • mix signals from several input ports to several output ports.

In addition, we took the first step in our alpha power extraction project, which we will continue to expand in the upcoming episodes.

Proceed to episode 3, where we will apply basic transforms to create a surrogate EEG signal with amplitude-modulated alpha activity.

File s2e2_combiner.pyView file on GitHub

 1# --------------------------------------------------------------
 2# Example file s2e2_combiner.py
 3# For details and usage, see g.Pype Training Season 2, Episode 2
 4# --------------------------------------------------------------
 5
 6import gpype as gp
 7
 8if __name__ == "__main__":
 9
10    # Create main app and pipeline
11    app = gp.MainApp()
12    p = gp.Pipeline()
13
14    # Create 4 sources with different frequencies
15    source1, source2, source3, source4 = [
16        gp.Generator(channel_count=1,
17                     signal_frequency=f,
18                     signal_amplitude=25) for f in (2, 4, 6, 8)
19    ]
20
21    # Create router to combine signals
22    combiner = gp.Router(input_channels=[[0], [0], [0], [0]])
23
24    # Create scope
25    scope = gp.TimeSeriesScope()
26
27    # Connect nodes
28    p.connect(source1, combiner["in1"])
29    p.connect(source2, combiner["in2"])
30    p.connect(source3, combiner["in3"])
31    p.connect(source4, combiner["in4"])
32    p.connect(combiner, scope)
33
34    # Add widget to main app
35    app.add_widget(scope)
36
37    # Start pipeline and run application
38    p.start()
39    app.run()  # blocking until window is closed
40    p.stop()

File s2e2_selector.pyView file on GitHub

 1# --------------------------------------------------------------
 2# Example file s2e2_selector.py
 3# For details and usage, see g.Pype Training Season 2, Episode 2
 4# --------------------------------------------------------------
 5
 6import gpype as gp
 7
 8if __name__ == "__main__":
 9
10    # Create main app and pipeline
11    app = gp.MainApp()
12    p = gp.Pipeline()
13
14    # Create 4 sources with different frequencies
15    source1, source2, source3, source4 = [
16        gp.Generator(channel_count=1,
17                     signal_frequency=f,
18                     signal_amplitude=25) for f in (2, 4, 6, 8)
19    ]
20
21    # Create router to combine signals
22    combiner = gp.Router(input_channels=[[0], [0], [0], [0]])
23
24    # Create router to select signals
25    selector = gp.Router(output_channels=[[0, 3]])
26
27    # Create scope
28    scope = gp.TimeSeriesScope()
29
30    # Connect nodes
31    p.connect(source1, combiner["in1"])
32    p.connect(source2, combiner["in2"])
33    p.connect(source3, combiner["in3"])
34    p.connect(source4, combiner["in4"])
35    p.connect(combiner, selector)
36    p.connect(selector, scope)
37
38    # Add widget to main app
39    app.add_widget(scope)
40
41    # Start pipeline and run application
42    p.start()
43    app.run()  # blocking until window is closed
44    p.stop()

File s2e2_splitter.pyView file on GitHub

 1# --------------------------------------------------------------
 2# Example file s2e2_splitter.py
 3# For details and usage, see g.Pype Training Season 2, Episode 2
 4# --------------------------------------------------------------
 5
 6import gpype as gp
 7
 8if __name__ == "__main__":
 9
10    # Create main app and pipeline
11    app = gp.MainApp()
12    p = gp.Pipeline()
13
14    # Create 4 sources with different frequencies
15    source1, source2, source3, source4 = [
16        gp.Generator(channel_count=1,
17                     signal_frequency=f,
18                     signal_amplitude=25) for f in (2, 4, 6, 8)
19    ]
20
21    # Create router to combine signals
22    combiner = gp.Router(input_channels=[[0], [0], [0], [0]])
23
24    # Create router to split signals
25    splitter = gp.Router(output_channels=[[0, 3], [1, 2]])
26
27    # Create scopes
28    scope1 = gp.TimeSeriesScope()
29    scope2 = gp.TimeSeriesScope()
30
31    # Connect nodes
32    p.connect(source1, combiner["in1"])
33    p.connect(source2, combiner["in2"])
34    p.connect(source3, combiner["in3"])
35    p.connect(source4, combiner["in4"])
36    p.connect(combiner, splitter)
37    p.connect(splitter["out1"], scope1)
38    p.connect(splitter["out2"], scope2)
39
40    # Add widget to main app
41    app.add_widget(scope1)
42    app.add_widget(scope2)
43
44    # Start pipeline and run application
45    p.start()
46    app.run()  # blocking until window is closed
47    p.stop()

File s2e2_mixer.pyView file on GitHub

 1# --------------------------------------------------------------
 2# Example file s2e2_mixer.py
 3# For details and usage, see g.Pype Training Season 2, Episode 2
 4# --------------------------------------------------------------
 5
 6import gpype as gp
 7
 8if __name__ == "__main__":
 9
10    # Create main app and pipeline
11    app = gp.MainApp()
12    p = gp.Pipeline()
13
14    # Create 4 sources with different frequencies
15    source1, source2, source3, source4 = [
16        gp.Generator(channel_count=1,
17                     signal_frequency=f,
18                     signal_amplitude=25) for f in (2, 4, 6, 8)
19    ]
20
21    # Create router to combine signals
22    combiner = gp.Router(input_channels=[[0], [0], [0], [0]])
23
24    # Create router to split signals
25    splitter = gp.Router(output_channels=[[0, 3], [1, 2]])
26
27    # Create router to mix signals
28    mixer = gp.Router(input_channels=[gp.Router.ALL, gp.Router.ALL],
29                      output_channels=[[0, 2], [3, 1]])
30
31    # Create scopes
32    scope1 = gp.TimeSeriesScope()
33    scope2 = gp.TimeSeriesScope()
34
35    # Connect nodes
36    p.connect(source1, combiner["in1"])
37    p.connect(source2, combiner["in2"])
38    p.connect(source3, combiner["in3"])
39    p.connect(source4, combiner["in4"])
40    p.connect(combiner, splitter)
41    p.connect(splitter["out1"], mixer["in1"])
42    p.connect(splitter["out2"], mixer["in2"])
43    p.connect(mixer["out1"], scope1)
44    p.connect(mixer["out2"], scope2)
45
46    # Add widget to main app
47    app.add_widget(scope1)
48    app.add_widget(scope2)
49
50    # Start pipeline and run application
51    p.start()
52    app.run()  # blocking until window is closed
53    p.stop()

File s2e2_alpha1.pyView file on GitHub

 1# --------------------------------------------------------------
 2# Example file s2e2_alpha1.py
 3# For details and usage, see g.Pype Training Season 2, Episode 2
 4# --------------------------------------------------------------
 5
 6import gpype as gp
 7
 8if __name__ == "__main__":
 9
10    # Create main app and pipeline
11    app = gp.MainApp()
12    p = gp.Pipeline()
13
14    # Create alpha source
15    source_alpha = gp.Generator(channel_count=1,
16                                signal_frequency=10,
17                                signal_amplitude=25)
18
19    source_mod = gp.Generator(channel_count=1,
20                              signal_frequency=0.2,
21                              signal_amplitude=25)
22
23    source_noise = gp.Generator(channel_count=1,
24                                noise_amplitude=25)
25
26    # Create router to combine signals
27    router = gp.Router(input_channels={"alpha": [0],
28                                       "modulator": [0],
29                                       "noise": [0]})
30
31    # Create scope
32    scope = gp.TimeSeriesScope()
33
34    # Connect nodes
35    p.connect(source_alpha, router["alpha"])
36    p.connect(source_mod, router["modulator"])
37    p.connect(source_noise, router["noise"])
38    p.connect(router, scope)
39
40    # Add widget to main app
41    app.add_widget(scope)
42
43    # Start pipeline and run application
44    p.start()
45    app.run()  # blocking until window is closed
46    p.stop()