import logging
import numpy as np
from numpy.typing import ArrayLike
from QInstrument.lib.QSerialInstrument import QSerialInstrument
logger = logging.getLogger(__name__)
[docs]
class QDS345(QSerialInstrument):
'''SRS DS345 Function Generator
Properties
==========
Function Output
---------------
amplitude: float [V]
Peak-to-peak output amplitude.
Range: 0 <= amplitude <= 5, amplitude + \\|offset\\| <= 5
frequency: float [Hz]
Output frequency.
Range by waveform:
- sine, square: 1 μHz – 30.2 MHz
- triangle, ramp: 1 μHz – 100 kHz
- arbitrary: 2.329 mHz – 40 MHz (sampling rate)
- noise: 10 MHz (fixed)
invert: bool
True: invert output polarity.
mute: bool
True: silence output (saves and restores amplitude).
offset: float [V]
DC offset voltage.
Range: -5 <= offset <= 5, amplitude + \\|offset\\| <= 5
phase: float [degrees]
Output waveform phase.
Range: 0.001 – 7199.999
Note: raises an instrument error if waveform is noise (4)
or arbitrary (5), or if a sweep or FM is active.
waveform: int
Output waveform.
0: sine, 1: square, 2: triangle, 3: ramp, 4: noise, 5: arbitrary
Output Modulation
-----------------
modulation: bool
True: enable output modulation.
modulation_type: int
0: linear sweep, 1: log sweep, 2: AM, 3: FM, 4: PM, 5: burst
modulation_waveform: int
Modulating waveform.
0: single, 1: ramp, 2: triangle, 3: sine, 4: square,
5: arbitrary, 6: none (burst mode)
Note: arbitrary (5) is valid for AM, FM, and PM only, and the
waveform must be loaded before enabling modulation.
modulation_rate: float [Hz]
Modulation rate.
Range: 0.001 – 10000
burst_count: int
Number of cycles per burst.
Range: 1 – 30000 (burst time must not exceed 500 s)
am_depth: int [%]
AM modulation depth.
Range: 0 – 100. Negative values enable double-sideband
suppressed-carrier (DSBSC) modulation.
fm_span: float [Hz]
Peak frequency deviation for FM.
Range: 0 – 2 * frequency
pm_span: float [degrees]
Peak phase deviation for PM.
Range: 0 – 7199.999
sweep_center_frequency: float [Hz]
Center frequency for frequency sweep.
Range: 0 – waveform maximum
sweep_span: float [Hz]
Span for frequency sweep. Negative values sweep downward.
Range: \\|sweep_span\\| <= 2 * sweep_center_frequency
sweep_start_frequency: float [Hz]
Start frequency for sweep.
Range: 0 – waveform maximum
sweep_stop_frequency: float [Hz]
Stop frequency for sweep.
Range: 0 – waveform maximum
trigger_rate: float [Hz]
Internal trigger rate for bursts and sweeps.
Range: 0.001 – 10000 (rounded to 2 significant digits)
trigger_source: int
Trigger source for bursts and sweeps.
0: single (see trigger()), 1: internal rate,
2: external positive slope, 3: external negative slope,
4: line
Arbitrary Waveform
------------------
sampling_frequency: float [Hz]
Sampling rate for arbitrary waveform playback.
Range: 1 mHz – 40 MHz
'''
comm = dict(baudRate=QSerialInstrument.BaudRate.Baud9600,
dataBits=QSerialInstrument.DataBits.Data8,
stopBits=QSerialInstrument.StopBits.TwoStop,
parity=QSerialInstrument.Parity.NoParity,
flowControl=QSerialInstrument.FlowControl.NoFlowControl,
eol='\n')
def _registerProperties(self) -> None:
'''Register all instrument properties via ``registerProperty()``.
Called automatically by ``QAbstractInstrument.__init__``. Subclasses
that extend the property set should call
``super()._registerProperties()`` first.
'''
self._muted: bool = False
self._saved_amplitude: float
register = self.registerProperty
register('amplitude',
getter=lambda: float(self.handshake('AMPL?')[:-4]),
setter=lambda v: self.transmit(f'AMPL {float(v)}VP'))
register('mute', ptype=bool,
getter=lambda: self._muted,
setter=self._setMute)
self._register('frequency', 'FREQ')
self._register('offset', 'OFFS')
self._register('phase', 'PHSE')
self._register('sampling_frequency', 'FSMP')
self._register('waveform', 'FUNC', int)
self._register('invert', 'INVT', bool)
self._register('modulation', 'MENA', bool)
self._register('modulation_type', 'MTYP', int)
self._register('modulation_waveform', 'MDWF', int)
self._register('modulation_rate', 'RATE')
self._register('burst_count', 'BCNT', int)
self._register('am_depth', 'DPTH', int)
self._register('fm_span', 'FDEV')
self._register('pm_span', 'PDEV')
self._register('sweep_span', 'SPAN')
self._register('sweep_center_frequency', 'SPCF')
self._register('sweep_start_frequency', 'STFR')
self._register('sweep_stop_frequency', 'SPFR')
self._register('trigger_rate', 'TRAT')
self._register('trigger_source', 'TSRC', int)
def _registerMethods(self) -> None:
'''Register all instrument methods via ``registerMethod()``.
Called automatically by ``QAbstractInstrument.__init__``. Subclasses
that add methods should call ``super()._registerMethods()`` first.
'''
self.registerMethod('reset', self.reset)
self.registerMethod('trigger', self.trigger)
def _register(self, name: str, cmd: str, dtype: type = float) -> None:
'''Register a standard instrument property.
Builds getter and setter from the DS345 command convention:
query is ``cmd + '?'``, set is ``cmd + value``. Bool properties
are transmitted as integers (0/1) per the instrument protocol.
Parameters
----------
name : str
Property name passed to ``registerProperty``.
cmd : str
DS345 command mnemonic (e.g. ``'FREQ'``).
dtype : type, optional
Value type: ``float`` (default), ``int``, or ``bool``.
'''
if dtype is bool:
def getter(): return bool(self.getValue(f'{cmd}?', int))
def setter(v): return self.transmit(f'{cmd}{int(bool(v))}')
else:
def getter(): return self.getValue(f'{cmd}?', dtype)
def setter(v): return self.transmit(f'{cmd}{dtype(v)}')
self.registerProperty(name, getter=getter, setter=setter, ptype=dtype)
[docs]
def identify(self) -> bool:
'''Return True if the connected device identifies as a DS345.
Queries the instrument identification string (``*IDN?``) and
checks for the ``'DS345'`` model token in the response.
'''
return 'DS345' in self.handshake('*IDN?')
[docs]
def reset(self) -> None:
'''Reset the DS345 to its factory default settings.'''
self.transmit('*RST')
[docs]
def trigger(self) -> None:
'''Issue a single trigger for a burst or sweep.
Only effective when ``trigger_source`` is 0 (single trigger
mode). Logs a warning otherwise.
'''
if self.getValue('TSRC?', int) != 0:
logger.warning(
'trigger() is only effective when trigger_source is 0.')
self.transmit('*TRG')
def _setMute(self, value: bool) -> None:
'''Setter for the ``mute`` property.
On mute: reads and saves the current amplitude, then sets it
to zero. On unmute: restores the saved amplitude. Idempotent —
repeated calls with the same value are no-ops.
'''
value = bool(value)
if value == self._muted:
return
if value:
amplitude = self.get('amplitude')
assert amplitude is not None
self._saved_amplitude = float(amplitude)
self.transmit('AMPL 0.0VP')
self._muted = True
else:
self._muted = False
self.transmit(f'AMPL {self._saved_amplitude}VP')
[docs]
def set_span_from_markers(self) -> None:
'''Set the sweep span from the current marker positions (MKSP).'''
self.transmit('MKSP')
[docs]
def set_markers_from_span(self) -> None:
'''Set the marker positions from the current sweep span (SPMK).'''
self.transmit('SPMK')
[docs]
def set_ttl(self) -> None:
'''Set output amplitude and offset to TTL levels.'''
self.transmit('ATTL')
[docs]
def set_ecl(self) -> None:
'''Set output amplitude and offset to ECL levels.'''
self.transmit('AECL')
[docs]
def amplitude_modulation(self, waveform: ArrayLike) -> None:
'''Load an arbitrary amplitude modulation waveform.
Configures the instrument for arbitrary AM and uploads the
waveform. Modulation is enabled on completion.
Parameters
----------
waveform : ArrayLike
Up to 10000 samples normalized to [-1, 1], where -1 is
full off and +1 is full on.
'''
data = np.asarray(waveform)
if len(data) > 10000:
logger.error('waveform can contain at most 10000 points')
return
signal = np.round(32767. * data).astype('>i2')
checksum = (np.sum(signal) & 0xFFFF).astype('>i2').tobytes()
self.transmit('MENA0')
self.transmit('MTYP2')
self.transmit('MDWF5')
self.transmit(f'AMOD?{len(signal)}')
self.receive()
self.transmit(signal.tobytes())
self.transmit(checksum)
self.transmit('MENA1')
if __name__ == '__main__':
QDS345.example()
__all__ = ['QDS345']