MonoDAQ Python

In this demo we connect to a single MonoDAQ and setup:

  • two analog input channels,
  • one current input channel,
  • one digital input channel and
  • one digital output channel

The digital output channel is toggled at a rate ~10 Hz. We may wire it to stimulate other analog and digital channels as a feedback signal. Then we show:

  1. how to make fetch samples and to make a simple plot
  2. how to add a custom filter function which waits for a rising edge and outputs P data before triggering, and M data after
In [1]:
from isotel.idm import monodaq, signal, gateway

mdu = monodaq.MonoDAQ_U()

# or connect to MonoDAQ with username and password
#mdu = monodaq.MonoDAQ_U(gateway.Group(username='user', password='test'))

Setup Channels

First we call reset() function to begin from start, then we configure the channels, and we print out the configuration. Configuration could also be stored in a 1-wire memory attached to the connector, which is automatically reloaded when connector with such memory is detected.

When directly accessing the parameters, you may want to use a Parameters view of the IDM, which helps you with browsing thru the device data structure.

In [2]:
mdu.reset()
mdu['ch.function.function2'] = 'Voltage Input'
mdu['ch.function.function3'] = 'Digital Input'
mdu['ch.function.function4'] = 'Voltage Input'
mdu['ch.function.function5'] = 'Digital Output'
mdu['ch.function.function10'] = 'Current Input'
mdu['ch.set.set5'] = '~1' # We set to High but as MDU will return actual voltage level 5 we need to add ~
mdu.print_setup()
N=0
pinfunctiontyperangerate [Hz]minmaxoffsetsetvalueunit
GND OneWire --- --- --- --- --- --- --- ---
IO --- --- --- --- --- --- --- --- ---
V1+ Voltage Input Single LPF 2V 5000 -0.1 2.0 --- --- --- V
DI1 Digital Input TTL 5 V 15000 0 5 --- --- 0 V
V2+ Voltage Input Single LPF 2V 5000 -0.1 2.0 --- --- --- V
DO1 Digital Output TTL 5 V --- 0 5 --- 5 0 V
5 Off --- --- --- --- --- --- --- ---
6 Off --- --- --- --- --- --- --- ---
7 Off --- --- --- --- --- --- --- ---
8 Off --- --- --- --- --- --- --- ---
CUR+ Current Input Internal Shunt 0.5 R ±500 mA 5000 -1000.0 1000.0 --- --- --- mA
CUR- --- --- --- --- --- --- --- --- ---
GND --- --- --- --- --- --- --- --- ---
PWR Off --- --- --- --- --- --- --- ---

Ambient

Tcjc=33.6 ±0.2 oC RH=56 ±2 %

Start a parallel thread to toggle DO1

The following thread starts a parallel independent instance of mdu to toggle the DO1 pin at rate of about 10 Hz. A channel number is obtained from a pin name. It uses mdu.time() to find the last received update from the MonoDAQ.

In [3]:
import threading

def toggle_digital__():
    mdu = monodaq.MonoDAQ_U()
    val = 0
    while True:
        mdu.set_channel_value('DO1', '~' + str(val), after=mdu.time() + 0.1)
        val ^= 1
        
threading.Thread(target=toggle_digital__).start()

Acquire Only

The fetch() is a python generator and fetches N samples and returns tuples organized as:

  • tuples of independent streams,
  • each stream holds time (x-axis) and tuple of channels synchronous to the time (y-axis),
  • tuple of channels

For example:

  • s[0][1][0] means from stream0, channels, and select 0-th channel

Generator provides meta information on channels in the first line (row).

In [4]:
list( mdu.fetch(N=2) )
Out[4]:
[(('t [s]', ('V1+ [V]', 'V2+ [V]', 'CUR+ [mA]')), ('t [s]', ('DI1 [V]',))),
 ((0.0, (0.0006, 0.0043, 0.4)), (0.0, (0,))),
 ((None, (nan,)), (6.7e-05, (0,))),
 ((None, (nan,)), (0.000133, (0,))),
 ((0.0002, (0.0004, -0.0005, 0.4)), (0.0002, (0,)))]

Scope

Scope offers real-time representation of data while being acquired.

Data must be re-arranged to a dict of lists via collect2dict() which has an optional parameter split=<N>. By specifying it, the collect2dict() cuts the input stream into slices of N. No data is lost.

The scope() updates the screen each time it receives suc a slice. If the split parameter is not given then entire stream is collected and then passed further to be drawn.

In [5]:
signal.scope( signal.collect2dict(mdu.fetch(100000), split=20000), title="Demo");
Loading BokehJS ...

Optional Pre-processing

Python generators offer easy cascading and propagation of information. In the following example we filter out the digital stream, and leave only the first stream 0.

In [6]:
def filter_din(source):
    yield (next(source)[0],) # copy header
    for s in source:
        yield (s[0],)
In [7]:
signal.scope(signal.collect2dict(filter_din(mdu.fetch(200))), title="Analog Only");
Loading BokehJS ...

Rising Edge Triggering

Example below shows how to wait for a rising edge on voltage channel V1 and by specifying condition and optional pre-condition. At the time condition triggers P pre-buffered samples are yielded, and continues for N samples.

In [8]:
if signal.scope(signal.collect2dict(signal.trigger(mdu.fetch(8000), precond='V1 < 1', cond='V1 > 1',
                                         P=50, N=50)), title="Rising Edge Trigger") == None:
    print('Signal not detected')
Loading BokehJS ...

Capture and Process in Separate Steps

Shows how to capture data in a list, and then re-generate the stream through triggering function.

In [9]:
sig1 = list( mdu.fetch(8000) );

Now lets post-process the signal in sig1 by passing it thru the signal's trigger() method. Note that in python regeneration is done as: (s for s in sig1)

In [10]:
sig2 = list(signal.trigger((s for s in sig1), cond='V1 > 2', precond='V1 < 1', P=15, N=15))

Let's plot the result in sig2:

In [11]:
signal.scope( signal.collect2dict( (s for s in sig2) ) );
Loading BokehJS ...

Add Math Channel

Let's reuse the last captured data, and pass it thru the addmath() function to calculate power of the V1+ [V] and CUR+ [mA] signals. One may specify just sufficient number of starting characters that uniquely define the channel name. The 2nd parameter defines the name of channel, and 3rd parameter defines an expression to evaluate.

In [12]:
signal.scope( signal.collect2dict( signal.addmath((s for s in sig2), 'P [mW]', expr='V1 * CUR')) );
Loading BokehJS ...

Post-analysis with Panda

Importing data from collected dictionaries to panda is demonstrated below.

We re-play sig2 into a math function, collect it, and take the fist sice out of the generated list to feed result to the panda DataFrame.

In [13]:
import pandas as pd

res = list( signal.collect2dict( signal.addmath((s for s in sig2), 'P [mW]', expr='V1 * CUR')) )[0]
d = pd.DataFrame.from_dict(res['P [mW]'])

d.head()
Out[13]:
y x
0 0.00008 0.0468
1 0.00010 0.0470
2 0.00016 0.0472
3 -0.00000 0.0474
4 0.00020 0.0476
In [14]:
d.describe()
Out[14]:
y x
count 10.000000 10.000000
mean 0.232074 0.047700
std 0.328083 0.000606
min -0.000000 0.046800
25% 0.000085 0.047250
50% 0.000180 0.047700
75% 0.464040 0.048150
max 0.928080 0.048600

Saving continous stream in a file

Let's write a simple sink that re-formats the streams' tuples into a TSV output:

In [15]:
def to_tsv(stream):
    def print_tsv(tup):
        line = ''
        for s in tup:
            line += str(s[0]) + '\t'
            for y in s[1]:
                line += str(y) + '\t'
        return line

    for s in stream:
        yield print_tsv(s)
In [16]:
for ln in to_tsv( (s for s in sig2) ):
    print(ln)
t [s]	V1+ [V]	V2+ [V]	CUR+ [mA]	t [s]	DI1 [V]	
0.0468	0.0002	0.0278	0.4	0.0468	0	
None	nan	0.046867	0	
None	nan	0.046933	0	
0.047	0.0005	0.028	0.2	0.047	0	
None	nan	0.047067	0	
None	nan	0.047133	0	
0.0472	0.0004	0.0312	0.4	0.0472	0	
None	nan	0.047267	0	
None	nan	0.047333	0	
0.0474	0.0006	0.0337	-0.0	0.0474	0	
None	nan	0.047467	0	
None	nan	0.047533	0	
0.0476	0.0005	0.0352	0.4	0.0476	0	
None	nan	0.047667	0	
None	nan	0.047733	0	
0.0478	2.3202	0.5558	0.2	0.0478	0	
None	nan	0.047867	1	
None	nan	0.047933	1	
0.048	2.3202	0.269	-0.0	0.048	1	
None	nan	0.048067	1	
None	nan	0.048133	1	
0.0482	2.3202	0.1465	0.2	0.0482	1	
None	nan	0.048267	1	
None	nan	0.048333	1	
0.0484	2.3202	0.0951	0.4	0.0484	1	
None	nan	0.048467	1	
None	nan	0.048533	1	
0.0486	2.3202	0.0722	0.2	0.0486	1	
None	nan	0.048667	1	
None	nan	0.048733	1	

Let's fetch directly or stream to a file:

In [17]:
for ln in to_tsv( mdu.fetch(5) ):
    print(ln)
t [s]	V1+ [V]	V2+ [V]	CUR+ [mA]	t [s]	DI1 [V]	
0.0	2.3202	-0.0378	0.4	0.0	1	
None	nan	6.7e-05	1	
None	nan	0.000133	1	
0.0002	2.3202	-0.032	0.2	0.0002	1	
None	nan	0.000267	1	
None	nan	0.000333	1	
0.0004	2.3202	-0.0287	0.6	0.0004	1	
None	nan	0.000467	1	
None	nan	0.000533	1	
0.0006	2.3202	-0.0261	0.6	0.0006	1	
None	nan	0.000667	1	
None	nan	0.000733	1	
0.0008	2.3202	-0.0203	0.6	0.0008	1	
In [18]:
with open('measurement.tsv', 'w') as file:
    for ln in to_tsv( (s for s in sig2) ):
        file.write(ln + '\n')
In [19]:
!cat measurement.tsv
t [s]	V1+ [V]	V2+ [V]	CUR+ [mA]	t [s]	DI1 [V]	
0.0468	0.0002	0.0278	0.4	0.0468	0	
None	nan	0.046867	0	
None	nan	0.046933	0	
0.047	0.0005	0.028	0.2	0.047	0	
None	nan	0.047067	0	
None	nan	0.047133	0	
0.0472	0.0004	0.0312	0.4	0.0472	0	
None	nan	0.047267	0	
None	nan	0.047333	0	
0.0474	0.0006	0.0337	-0.0	0.0474	0	
None	nan	0.047467	0	
None	nan	0.047533	0	
0.0476	0.0005	0.0352	0.4	0.0476	0	
None	nan	0.047667	0	
None	nan	0.047733	0	
0.0478	2.3202	0.5558	0.2	0.0478	0	
None	nan	0.047867	1	
None	nan	0.047933	1	
0.048	2.3202	0.269	-0.0	0.048	1	
None	nan	0.048067	1	
None	nan	0.048133	1	
0.0482	2.3202	0.1465	0.2	0.0482	1	
None	nan	0.048267	1	
None	nan	0.048333	1	
0.0484	2.3202	0.0951	0.4	0.0484	1	
None	nan	0.048467	1	
None	nan	0.048533	1	
0.0486	2.3202	0.0722	0.2	0.0486	1	
None	nan	0.048667	1	
None	nan	0.048733	1