Skip to content

Commit 3889fc6

Browse files
authored
Merge pull request #21 from lightwave-lab/development
Version 1.0.7
2 parents e63b2b9 + 0a1aaf0 commit 3889fc6

File tree

12 files changed

+413
-41
lines changed

12 files changed

+413
-41
lines changed

lightlab/equipment/abstract_drivers/TekScopeAbstract.py

Lines changed: 3 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
11
import numpy as np
2-
import pyvisa
32

43
from lightlab import logger
54
from lightlab.util.data import Waveform, FunctionBundle
@@ -180,17 +179,8 @@ def __transferData(self, chan):
180179
chStr = 'CH' + str(chan)
181180
self.setConfigParam('DATA:ENCDG', 'ASCII')
182181
self.setConfigParam('DATA:SOURCE', chStr)
183-
self.open()
184-
try:
185-
voltRaw = self.mbSession.query_ascii_values('CURV?')
186-
except pyvisa.VisaIOError as err:
187-
logger.error('Problem during query_ascii_values(\'CURV?\')')
188-
try:
189-
self.close()
190-
except pyvisa.VisaIOError:
191-
logger.error('Failed to close! %s', self.address)
192-
raise err
193-
self.close()
182+
183+
voltRaw = self.query_ascii_values('CURV?')
194184
return voltRaw
195185

196186
def __scaleData(self, voltRaw):
@@ -217,7 +207,7 @@ def __scaleData(self, voltRaw):
217207
* get(self._yScaleParam) \
218208
+ get('YZERO')
219209

220-
timeDivision = float(self.getConfigParam('HORIZONTAL:MAIN:SCALE'))
210+
timeDivision = float(self.getConfigParam('HORIZONTAL:MAIN:SCALE', forceHardware=True))
221211
time = np.linspace(-1, 1, len(voltage)) / 2 * timeDivision * 10
222212

223213
return time, voltage

lightlab/equipment/lab_instruments/HP_8152A_PM.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -67,10 +67,10 @@ def proccessWeirdRead(readString):
6767
val *= -1
6868
return str(val)
6969

70-
def query(self, *args, **kwargs): # pylint: disable=arguments-differ
70+
def robust_query(self, *args, **kwargs): # pylint: disable=arguments-differ
7171
''' Conditionally check for read character doubling
7272
'''
73-
retRaw = super().query(*args, **kwargs) # pylint: disable=arguments-differ
73+
retRaw = self.query(*args, **kwargs) # pylint: disable=arguments-differ
7474
if self.doReadDoubleCheck:
7575
return self.proccessWeirdRead(retRaw)
7676
else:
@@ -89,7 +89,7 @@ def powerDbm(self, channel=1):
8989
trial = 0
9090
while trial < 10: # Sometimes it gets out of range, so we have to try a few times
9191
self.write('CH' + str(channel))
92-
powStr = self.query('TRG')
92+
powStr = self.robust_query('TRG')
9393
v = float(powStr)
9494
if abs(v) < 999: # check if it's reasonable
9595
break
Lines changed: 151 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,156 @@
1+
import numpy as np
2+
from lightlab.util.data import Waveform
13
from .Tektronix_DPO4034_Oscope import Tektronix_DPO4034_Oscope
4+
import pyvisa
5+
from lightlab import visalogger as logger
26

37

48
class Tektronix_DPO4032_Oscope(Tektronix_DPO4034_Oscope):
9+
'''
10+
Manual: https://www.imperial.ac.uk/media/imperial-college/research-centres-and-groups/centre-for-bio-inspired-technology/7293027.PDF
11+
'''
512
totalChans = 2
13+
_recLenParam = 'HORIZONTAL:RECORDLENGTH'
14+
15+
def timebaseConfig(self, avgCnt=None, duration=None):
16+
''' Timebase and acquisition configure
17+
18+
Args:
19+
avgCnt (int): averaging done by the scope
20+
duration (float): time, in seconds, for data to be acquired
21+
22+
Returns:
23+
(dict) The present values of all settings above
24+
'''
25+
self.setConfigParam('HORIZONTAL:MAIN:SAMPLERATE', 2.5e9)
26+
27+
if avgCnt is not None and avgCnt > 1:
28+
self.setConfigParam('ACQUIRE:NUMAVG', avgCnt, forceHardware=True)
29+
if duration is not None:
30+
self.setConfigParam('HORIZONTAL:MAIN:SCALE', duration / 10)
31+
self.setConfigParam(self._recLenParam, 10 * int(duration * 2.5e9))
32+
self.setConfigParam('DATA:START', 1)
33+
self.setConfigParam('DATA:STOP', int(duration * 2.5e9))
34+
35+
presentSettings = dict()
36+
presentSettings['avgCnt'] = self.getConfigParam('ACQUIRE:NUMAVG', forceHardware=True)
37+
presentSettings['duration'] = self.getConfigParam(
38+
'HORIZONTAL:MAIN:SCALE', forceHardware=True)
39+
# presentSettings['position'] = self.getConfigParam('HORIZONTAL:MAIN:POSITION', forceHardware=True)
40+
presentSettings['nPts'] = self.getConfigParam(self._recLenParam, forceHardware=True)
41+
return presentSettings
42+
43+
def __scaleData(self, voltRaw):
44+
''' Scale to second and voltage units.
45+
46+
DSA and DPO are very annoying about treating ymult and yscale differently.
47+
TDS uses ymult not yscale
48+
49+
Args:
50+
voltRaw (ndarray): what is returned from ``__transferData``
51+
52+
Returns:
53+
(ndarray): time in seconds, centered at t=0 regardless of timebase position
54+
(ndarray): voltage in volts
55+
56+
Notes:
57+
The formula for real voltage should be (Y - YOFF) * YSCALE + YZERO.
58+
The Y represents the position of the sampled point on-screen,
59+
YZERO, the reference voltage, YOFF, the offset position, and
60+
YSCALE, the conversion factor between position and voltage.
61+
'''
62+
get = lambda param: float(self.getConfigParam('WFMOUTPRE:' + param, forceHardware=True))
63+
voltage = (np.array(voltRaw) - get('YOFF')) \
64+
* get(self._yScaleParam) \
65+
+ get('YZERO')
66+
67+
sample_rate = float(self.getConfigParam('HORIZONTAL:MAIN:SAMPLERATE', forceHardware=True))
68+
# time = np.linspace(-1, 1, len(voltage)) / 2 * timeDivision * 10
69+
time = np.arange(len(voltage)) / sample_rate
70+
time -= np.mean(time)
71+
72+
return time, voltage
73+
74+
def acquire(self, chans=None, timeout=None, **kwargs):
75+
''' Get waveforms from the scope.
76+
77+
If chans is None, it won't actually trigger, but it will configure.
78+
79+
If unspecified, the kwargs will be derived from the previous state of the scope.
80+
This is useful if you want to play with it in lab while working with this code too.
81+
82+
Args:
83+
chans (list): which channels to record at the same time and return
84+
avgCnt (int): number of averages. special behavior when it is 1
85+
duration (float): window width in seconds
86+
position (float): trigger delay
87+
nPts (int): number of sample points
88+
timeout (float): time to wait for averaging to complete in seconds
89+
If it is more than a minute, it will do a test first
90+
91+
92+
Returns:
93+
list[Waveform]: recorded signals
94+
'''
95+
self.timebaseConfig(**kwargs)
96+
if chans is None:
97+
return
98+
99+
for c in chans:
100+
if c > self.totalChans:
101+
raise Exception('Received channel: ' + str(c) +
102+
'. Max channels of this scope is ' + str(self.totalChans))
103+
104+
# Channel select
105+
for ich in range(1, 1 + self.totalChans):
106+
thisState = 1 if ich in chans else 0
107+
self.setConfigParam('SELECT:CH' + str(ich), thisState)
108+
109+
isSampling = kwargs.get('avgCnt', 0) == 1
110+
self._setupSingleShot(isSampling)
111+
self._triggerAcquire(timeout=timeout)
112+
wfms = [None] * len(chans)
113+
for i, c in enumerate(chans):
114+
vRaw = self.__transferData(c)
115+
t, v = self.__scaleData(vRaw)
116+
# Optical modules might produce 'W' instead of 'V'
117+
unit = self.__getUnit()
118+
wfms[i] = Waveform(t, v, unit=unit)
119+
120+
return wfms
121+
122+
def __getUnit(self):
123+
''' Gets the unit of the waveform as a string.
124+
125+
Normally, this will be '"V"', which can be converted to 'V'
126+
'''
127+
128+
yunit_query = self.getConfigParam('WFMOUTPRE:YUNIT', forceHardware=True)
129+
return yunit_query.replace('"', '')
130+
131+
def __transferData(self, chan):
132+
''' Returns the raw data pulled from the scope as time (seconds) and voltage (Volts)
133+
Args:
134+
chan (int): one channel at a time
135+
136+
Returns:
137+
:mod:`data.Waveform`: a time, voltage paired signal
138+
139+
Todo:
140+
Make this binary transfer to go even faster
141+
'''
142+
chStr = 'CH' + str(chan)
143+
self.setConfigParam('DATA:ENCDG', 'ASCII')
144+
self.setConfigParam('DATA:SOURCE', chStr)
145+
self.open()
146+
try:
147+
voltRaw = self.mbSession.query_ascii_values('CURV?')
148+
except pyvisa.VisaIOError as err:
149+
logger.error('Problem during query_ascii_values(\'CURV?\')')
150+
try:
151+
self.close()
152+
except pyvisa.VisaIOError:
153+
logger.error('Failed to close! %s', self.address)
154+
raise err
155+
self.close()
156+
return voltRaw
Lines changed: 132 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,132 @@
1+
from . import VISAInstrumentDriver
2+
from lightlab.equipment.abstract_drivers import Configurable
3+
from lightlab.laboratory.instruments import PatternGenerator
4+
5+
import numpy as np
6+
import time
7+
from lightlab import visalogger as logger
8+
9+
10+
class Tektronix_PPG3202(VISAInstrumentDriver, Configurable):
11+
''' Python driver for Tektronix PPG 3202.
12+
13+
Basic functionality includes setting all parameters on the main pannel and specifying data rate.
14+
Other functionality includes setting output data pattern on specifies channel.
15+
16+
`Manual <https://www.tek.com/bit-error-rate-tester/patternpro-ppg-series-pattern-generator-manual/ppg1600-ppg3000-ppg3200-0>`
17+
'''
18+
19+
instrument_category = PatternGenerator
20+
__ClockDivider = np.array([1, 2, 4, 8, 16])
21+
__channels = np.array([1, 2])
22+
__patternType = np.array(['PRBS', 'DATA'])
23+
__waitTime = 0 # May be needed to prevent Timeout error
24+
25+
def __init__(self, name='Pattern Generator', address=None, **kwargs):
26+
# Address should be something like 'USB0::0xXXXX::0xXXXX::XXXXXXX::INSTR' when using USB connection
27+
VISAInstrumentDriver.__init__(self, name=name, address=address, **kwargs)
28+
Configurable.__init__(self)
29+
30+
def __setVoltage(self, chan=None, amp=None, offset=None):
31+
''' Set the voltage on the specified channel
32+
'''
33+
if amp is not None and chan in self.__channels:
34+
time.sleep(self.__waitTime)
35+
cmd = str(':VOLT' + str(chan) + ':POS ' + str(amp) + 'V')
36+
self.setConfigParam(cmd, None, True)
37+
if offset is not None:
38+
time.sleep(self.__waitTime)
39+
cmd = str(':VOLT' + str(chan) + ':POS:OFFS ' + str(offset) + 'V')
40+
self.setConfigParam(cmd, None, True)
41+
42+
def __setPatternType(self, chan=None, ptype=None):
43+
''' Set the data pattern on the specified channel. Type can only be 'PRBS'
44+
or 'DATA'
45+
'''
46+
if ptype is not None and chan in self.__channels:
47+
if ptype not in self.__patternType:
48+
logger.exception('Wrong Pattern Type!')
49+
else:
50+
time.sleep(self.__waitTime)
51+
cmd = str(':DIG' + str(chan) + ':PATT:TYPE ' + str(ptype))
52+
self.setConfigParam(cmd, None, True)
53+
54+
def setDataRate(self, rate=None):
55+
''' Set the data rate of the PPG. Data rate can only be in the range of
56+
1.5 Gb/s to 32 Gb/s
57+
'''
58+
if rate is not None:
59+
if rate < 1.5 or rate > 32:
60+
logger.exception('Invalid Data Rate!')
61+
else:
62+
time.sleep(self.__waitTime)
63+
cmd = str(':FREQ ' + str(rate) + 'e9')
64+
self.setConfigParam(cmd, None, True)
65+
66+
def setMainParam(self, chan=None, amp=None, offset=None, ptype=None):
67+
''' One function to set all parameters on the main window
68+
'''
69+
if chan is None:
70+
logger.exception('Please Specify Channel Number!')
71+
else:
72+
self.__setVoltage(chan, amp, offset)
73+
self.__setPatternType(chan, ptype)
74+
75+
def setClockDivider(self, div=None):
76+
if div is not None:
77+
if (div in self.__ClockDivider):
78+
time.sleep(self.__waitTime)
79+
cmd = str(':OUTP:CLOC:DIV ' + str(div))
80+
self.setConfigParam(cmd, None, True)
81+
else:
82+
logger.exception('Wrong Clock Divider Value!')
83+
84+
def setDataMemory(self, chan=None, startAddr=None, bit=None, data=None):
85+
if chan is not None and chan in self.__channels:
86+
time.sleep(self.__waitTime)
87+
cmd = str(':DIG' + str(chan) + ':PATT:DATA ' + str(startAddr) + ',' + str(bit) + ',' + str(data))
88+
self.setConfigParam(cmd, None, True)
89+
else:
90+
logger.exception('Please choose Channel 1 or 2!')
91+
92+
def setHexDataMemory(self, chan=None, startAddr=None, bit=None, Hdata=None):
93+
if chan is not None and chan in self.__channels:
94+
time.sleep(self.__waitTime)
95+
cmd = str(':DIG' + str(chan) + ':PATT:HDAT ' + str(startAddr) + ',' + str(bit) + ',' + str(Hdata))
96+
self.setConfigParam(cmd, None, True)
97+
else:
98+
logger.exception('Please choose Channel 1 or 2!')
99+
100+
def channelOn(self, chan=None):
101+
if chan is not None and chan in self.__channels:
102+
time.sleep(self.__waitTime)
103+
cmd = str(':OUTP' + str(chan) + ' ON')
104+
self.setConfigParam(cmd, None, True)
105+
else:
106+
logger.exception('Please choose Channel 1 or 2!')
107+
108+
def channelOff(self, chan=None):
109+
if chan is not None and chan in self.__channels:
110+
time.sleep(self.__waitTime)
111+
cmd = str(':OUTP' + str(chan) + ' OFF')
112+
self.setConfigParam(cmd, None, True)
113+
else:
114+
logger.exception('Please choose Channel 1 or 2!')
115+
116+
def getAmplitude(self, chan=None):
117+
if chan is not None and chan in self.__channels:
118+
return self.query(':VOLT' + str(chan) + ':POS?')
119+
120+
def getOffset(self, chan=None):
121+
if chan is not None and chan in self.__channels:
122+
return self.query(':VOLT' + str(chan) + ':POS:OFFS?')
123+
124+
def getDataRate(self):
125+
return self.query(':FREQ?')
126+
127+
def getPatternType(self, chan=None):
128+
if chan is not None and chan in self.__channels:
129+
return self.query(':DIG' + str(chan) + ':PATT:TYPE?')
130+
131+
def getClockDivider(self):
132+
return self.query(':OUTP:CLOC:DIV?')

lightlab/equipment/visa_bases/driver_base.py

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
import socket
44
import time
55
from lightlab import visalogger as logger
6+
from pyvisa.util import from_ascii_block
67

78

89
class InstrumentSessionBase(ABC):
@@ -37,6 +38,10 @@ def write(self):
3738
def query(self):
3839
pass
3940

41+
@abstractmethod
42+
def wait(self):
43+
pass
44+
4045
@abstractmethod
4146
def clear(self):
4247
pass
@@ -45,6 +50,13 @@ def clear(self):
4550
def query_raw_binary(self):
4651
pass
4752

53+
def query_ascii_values(self, message, converter='f', separator=',',
54+
container=list):
55+
''' Taken from pvisa.'''
56+
57+
block = self.query(message)
58+
return from_ascii_block(block, converter, separator, container)
59+
4860
def instrID(self):
4961
r"""Returns the \*IDN? string"""
5062
return self.query('*IDN?')
@@ -56,7 +68,7 @@ def timeout(self):
5668

5769
@timeout.setter
5870
@abstractmethod
59-
def termination(self, newTimeout):
71+
def timeout(self, newTimeout):
6072
pass
6173

6274

0 commit comments

Comments
 (0)