Repository: vispy/vispy

Issue:
New example: data acquisition system
Imagine a Data Acquisition System receiving real-time multi-channel digital signals. There are two ways to display the incoming data:
- Scrolling: the window shows `[t-h:t]` where `t` is the current time, and `h` the size (duration) of the window. Old data is discarded, and new buffered data comes from the right side of the window.
- Rotating: the window is split in two parts. The left part (new data) shows `[t-a*t,t]`, the right part (old data) shows `[t-b*t,t-a*t]`. In other words, the screen is constantly refreshed following a rotating vertical line (current time) that goes from left to right.

It should be possible to toggle dynamically between the two.

Technically, this could be made efficient by considering a VBO containing only the data samples (not the time samples). This VBO is updated in real-time with `set_subdata` on a circularly changing sub-window. There is another VBO with integer indices for all vertices (static).

Besides, there are variables containing the current time, the window duration, and the current position (integer) within the window. The time samples should not be computed on the CPU and transferred to the GPU. Rather, they should be generated dynamically on the vertex shader using the vertex indices and the current time. This saves 2x memory transfer.

Suggested by @samuelgarcia.



Discussion:
If you do that, I will pay to each of you and all your descendant on 3 generations a bottle of wine. Promise.

There is also something to consider for this kind of "oscilloscope" is a by channel gain and offset. On pyacq project, I did that. It allow the user to spread channel with fake gain/offset like a real oscilloscope.

Good idea. This is also useful for purely offline visualization. (ping @nippoo)

> If you do that, I will pay to each of you and all your descendant on 3 generations a bottle of wine. 

That's a lot of wine :)


Resolving patch:
diff --git a/examples/demo/realtime_signals.py b/examples/demo/realtime_signals.py
new file mode 100644
--- /dev/null
+++ b/examples/demo/realtime_signals.py
@@ -0,0 +1,166 @@
+#!/usr/bin/env python
+# -*- coding: utf-8 -*-
+# vispy: gallery 2
+# Copyright (c) 2014, Vispy Development Team.
+# Distributed under the (new) BSD License. See LICENSE.txt for more info.
+
+"""
+Multiple real-time digital signals with GLSL-based clipping.
+"""
+
+from vispy import gloo
+from vispy import app
+import numpy as np
+import math
+
+# Number of cols and rows in the table.
+nrows = 16
+ncols = 20
+
+# Number of signals.
+m = nrows*ncols
+
+# Number of samples per signal.
+n = 1000
+
+# Various signal amplitudes.
+amplitudes = .1 + .2 * np.random.rand(m, 1).astype(np.float32)
+
+# Generate the signals as a (m, n) array.
+y = amplitudes * np.random.randn(m, n).astype(np.float32)
+
+# Color of each vertex (TODO: make it more efficient by using a GLSL-based 
+# color map and the index).
+color = np.repeat(np.random.uniform(size=(m, 3), low=.5, high=.9),
+                  n, axis=0).astype(np.float32)
+
+# Signal 2D index of each vertex (row and col) and x-index (sample index
+# within each signal).
+index = np.c_[np.repeat(np.repeat(np.arange(ncols), nrows), n),
+              np.repeat(np.tile(np.arange(nrows), ncols), n),
+              np.tile(np.arange(n), m)].astype(np.float32)
+
+VERT_SHADER = """
+#version 120
+
+// y coordinate of the position.
+attribute float a_position;
+
+// row, col, and time index.
+attribute vec3 a_index;
+varying vec3 v_index;
+
+// 2D scaling factor (zooming).
+uniform vec2 u_scale;
+
+// Size of the table.
+uniform vec2 u_size;
+
+// Number of samples per signal.
+uniform float u_n;
+
+// Color.
+attribute vec3 a_color;
+varying vec4 v_color;
+
+// Varying variables used for clipping in the fragment shader.
+varying vec2 v_position;
+varying vec4 v_ab;
+
+void main() {
+    float nrows = u_size.x;
+    float ncols = u_size.y;
+   
+    // Compute the x coordinate from the time index.
+    float x = -1 + 2*a_index.z / (u_n-1);
+    vec2 position = vec2(x, a_position);
+    
+    // Find the affine transformation for the subplots.
+    vec2 a = vec2(1./ncols, 1./nrows)*.9;
+    vec2 b = vec2(-1 + 2*(a_index.x+.5) / ncols, 
+                  -1 + 2*(a_index.y+.5) / nrows);
+    // Apply the static subplot transformation + scaling.
+    gl_Position = vec4(a*u_scale*position+b, 0.0, 1.0);
+    
+    v_color = vec4(a_color, 1.);
+    v_index = a_index;
+    
+    // For clipping test in the fragment shader.
+    v_position = gl_Position.xy;
+    v_ab = vec4(a, b);
+}
+"""
+
+FRAG_SHADER = """
+#version 120
+
+varying vec4 v_color;
+varying vec3 v_index;
+
+varying vec2 v_position;
+varying vec4 v_ab;
+
+void main() {
+    gl_FragColor = v_color;
+    
+    // Discard the fragments between the signals (emulate glMultiDrawArrays).
+    if ((fract(v_index.x) > 0.) || (fract(v_index.y) > 0.))
+        discard;
+      
+    // Clipping test.
+    vec2 test = abs((v_position.xy-v_ab.za)/v_ab.xy);
+    if ((test.x > 1) || (test.y > 1))
+        discard;
+}
+"""
+
+
+class Canvas(app.Canvas):
+    def __init__(self):
+        app.Canvas.__init__(self, title='Use your wheel to zoom!', 
+                            close_keys='escape')
+        self.program = gloo.Program(VERT_SHADER, FRAG_SHADER)
+        self.program['a_position'] = y.ravel()
+        self.program['a_color'] = color
+        self.program['a_index'] = index
+        self.program['u_scale'] = (1., 1.)
+        self.program['u_size'] = (nrows, ncols)
+        self.program['u_n'] = n
+        
+        self.timer = app.Timer(1. / 60)
+        self.timer.connect(self.on_timer)
+        self.timer.start()
+
+    def on_initialize(self, event):
+        gloo.set_state(clear_color=(1, 1, 1, 1), blend=True, 
+                       blend_func=('src_alpha', 'one_minus_src_alpha'))
+
+    def on_resize(self, event):
+        self.width, self.height = event.size
+        gloo.set_viewport(0, 0, self.width, self.height)
+        
+    def on_mouse_wheel(self, event):
+        dx = np.sign(event.delta[1]) * .05
+        scale_x, scale_y = self.program['u_scale']     
+        scale_x_new, scale_y_new = (scale_x * math.exp(2.5*dx), 
+                                    scale_y * math.exp(0.0*dx))
+        self.program['u_scale'] = (max(1, scale_x_new), max(1, scale_y_new))
+        self.update()
+
+    def on_timer(self, event):
+        """Add some data at the end of each signal (real-time signals)."""
+        k = 10
+        y[:, :-k] = y[:, k:]
+        y[:, -k:] = amplitudes * np.random.randn(m, k)
+        
+        self.program['a_position'].set_data(y.ravel().astype(np.float32))
+        self.update()
+        
+    def on_draw(self, event):
+        gloo.clear(color=(0.0, 0.0, 0.0, 1.0))
+        self.program.draw('line_strip')
+        
+if __name__ == '__main__':
+    c = Canvas()
+    c.show()
+    app.run()


Test patch: