Skip to content

Commit

Permalink
#50 updated AWG and pulse gen docstrings
Browse files Browse the repository at this point in the history
  • Loading branch information
crnbaker committed Jan 24, 2024
1 parent 3b912a4 commit 1509329
Show file tree
Hide file tree
Showing 4 changed files with 78 additions and 5 deletions.
13 changes: 11 additions & 2 deletions src/spectrumdevice/devices/awg/abstract_spectrum_awg.py
Original file line number Diff line number Diff line change
Expand Up @@ -53,16 +53,25 @@ def configure_generation(self, generation_settings: GenerationSettings) -> None:

@property
def generation_mode(self) -> GenerationMode:
"""Change the currently enabled card mode. See `GenerationMode` and the Spectrum documentation
for the available modes."""
"""The currently enabled card mode."""
return GenerationMode(self.read_spectrum_device_register(SPC_CARDMODE))

def set_generation_mode(self, mode: GenerationMode) -> None:
"""Change the currently enabled card mode. See `GenerationMode` and the Spectrum documentation
for the available modes."""
self.write_to_spectrum_device_register(SPC_CARDMODE, mode.value)

@property
def num_loops(self) -> int:
"""In GenerationMode.SPC_REP_STD_SINGLE, the arbitrary waveform will be repeated this many times after a single
trigger event. In GenerationMode.SPC_REP_STD_SINGLERESTART, the card will wait for this many triggers before
stopping, and will generate the arbitrary waveform once for each received trigger even. Set to 0 for continuous
output."""
return self.read_spectrum_device_register(SPC_LOOPS)

def set_num_loops(self, num_loops: int) -> None:
"""In GenerationMode.SPC_REP_STD_SINGLE, the arbitrary waveform will be repeated this many times after a single
trigger event. In GenerationMode.SPC_REP_STD_SINGLERESTART, the card will wait for this many triggers before
stopping, and will generate the arbitrary waveform once for each received trigger even. Set to 0 for continuous
output."""
self.write_to_spectrum_device_register(SPC_LOOPS, num_loops)
10 changes: 10 additions & 0 deletions src/spectrumdevice/devices/awg/awg_card.py
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,16 @@ def _init_io_lines(self) -> Sequence[SpectrumAWGIOLineInterface]:
raise NotImplementedError("Don't know how many IO lines other types of card have. Only M2P series.")

def transfer_waveform(self, waveform: NDArray[int16]) -> None:
""" "Write an arbitrary waveform to the card's on-board memory.
Args:
waveform (NDArray[int16]): A numpy array of signed 16-bit integers representing the samples of the
waveform to transfer. The amplitude and offset of the generated signals can be set per-channel (see
SpectrumAWGAnalogChannel), so the waveform provided here should be scaled to the full range of int16
(i.e. -32767 to 32767). Must be at least 16 samples long. If the waveform length is not a multiple of
8 samples, the waveform will be zero-padded so its length is the next multiple of 8.
"""
if len(waveform) < 16:
raise ValueError("Waveform must be at least 16 samples long")
step_size = get_memsize_step_size(self._model_number)
Expand Down
25 changes: 22 additions & 3 deletions src/spectrumdevice/devices/awg/synthesis.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,20 @@
from numpy import float_, iinfo, issubdtype, signedinteger, pi, sin, linspace, int_, ones
from numpy import float_, iinfo, issubdtype, signedinteger, pi, sin, linspace, int_, ones, int16
from numpy.typing import NDArray


def make_full_scale_sine_waveform(
frequency_in_hz: float, sample_rate_in_hz: int, num_cycles: float, dtype: type
frequency_in_hz: float, sample_rate_in_hz: int, num_cycles: float, dtype: type = int16
) -> tuple[NDArray[float_], NDArray[int_]]:
"""Create a sine waveform covering the full range of the given data type. The resulting waveform is intended to
be transferred to the AWG's on-board memory for generation.
Args:
frequency_in_hz (float): The frequency of the sine waveform in Hz
sample_rate_in_hz (int): The sampling rate of the waveform to synthesise. Make sure your card is set to the
same sampler rate.
num_cycles (int): The number of sinusoidal cycles to synthesise.
dtype (type): Signed integer type to use. Default int16.
"""
if not issubdtype(dtype, signedinteger):
raise ValueError("dtype must be a signed integer type")
full_scale_max_value = iinfo(dtype).max
Expand All @@ -14,8 +24,17 @@ def make_full_scale_sine_waveform(


def make_full_scale_rect_waveform(
sample_rate_in_hz: int, duration_in_seconds: float, dtype: type
sample_rate_in_hz: int, duration_in_seconds: float, dtype: type = int16
) -> tuple[NDArray[float_], NDArray[int_]]:
"""Create a rectangular waveform covering the full range of the given data type. The resulting waveform is intended
to be transferred to the AWG's on-board memory for generation.
Args:
sample_rate_in_hz (int): The sampling rate of the waveform to synthesise. Make sure your card is set to the
same sampler rate.
duration_in_seconds (float): The duration of the rectangular waveform in seconds.
dtype (type): Signed integer type to use. Default int16.
"""
if not issubdtype(dtype, signedinteger):
raise ValueError("dtype must be a signed integer type")
duration_in_samples = int(duration_in_seconds * sample_rate_in_hz)
Expand Down
35 changes: 35 additions & 0 deletions src/spectrumdevice/features/pulse_generator/pulse_generator.py
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,7 @@ def configure_output(
return coerced_settings

def configure_trigger(self, settings: PulseGeneratorTriggerSettings) -> None:
"""Configure all pulse generator trigger settings at once."""
self.set_trigger_mode(settings.trigger_mode)
self.set_trigger_detection_mode(settings.trigger_detection_mode)
self.multiplexer_1.set_trigger_source(settings.multiplexer_1_source)
Expand All @@ -93,6 +94,7 @@ def configure_trigger(self, settings: PulseGeneratorTriggerSettings) -> None:
self.write_to_parent_device_register(SPC_M2CMD, M2CMD_CARD_WRITESETUP)

def force_trigger(self) -> None:
"""Generates a pulse when the pulse generator trigger source (mux 2) is set to 'software'."""
if (
self._multiplexer_2.trigger_source
!= PulseGeneratorMultiplexer2TriggerSource.SPCM_PULSEGEN_MUX2_SRC_SOFTWARE
Expand All @@ -102,14 +104,17 @@ def force_trigger(self) -> None:

@property
def number(self) -> int:
"""The index of the pulse generator. Corresponds to the index of the IO line to which it belongs."""
return self._number

@property
def multiplexer_1(self) -> PulseGeneratorMultiplexer1:
"""Change the trigger source of this multiplexer to control when it is possible to trigger the pulse generator."""
return self._multiplexer_1

@property
def multiplexer_2(self) -> PulseGeneratorMultiplexer2:
"""Change the trigger source of this multiplexer to control how the pulse generator is triggered."""
return self._multiplexer_2

def read_parent_device_register(
Expand Down Expand Up @@ -137,22 +142,29 @@ def _get_enabled_pulse_generator_ids(self) -> list[int]:

@property
def clock_rate_in_hz(self) -> int:
"""The current pulse generator clock rate. Affected by the sample rate of the parent card, and the number of
channels enabled. Effects the precision with which pulse timings can be set, and their min and max values."""
return self.read_parent_device_register(SPC_XIO_PULSEGEN_CLOCK)

@property
def clock_period_in_seconds(self) -> float:
"""The reciprocal of the clock rate, in seconds."""
return 1 / self.clock_rate_in_hz

@property
def enabled(self) -> bool:
"""True if the pulse generator is currently enabled."""
return PULSE_GEN_ENABLE_COMMANDS[self._number] in self._get_enabled_pulse_generator_ids()

def enable(self) -> None:
"""Enable the pulse generator. Note that the mode of the parent IO Line must also be set to
IOLineMOdO.SPCM_XMODE_PULSEGEN."""
current_register_value = self.read_parent_device_register(SPC_XIO_PULSEGEN_ENABLE)
new_register_value = toggle_bitmap_value(current_register_value, PULSE_GEN_ENABLE_COMMANDS[self._number], True)
self.write_to_parent_device_register(SPC_XIO_PULSEGEN_ENABLE, new_register_value)

def disable(self) -> None:
"""Disable the pulse generator."""
current_register_value = self.read_parent_device_register(SPC_XIO_PULSEGEN_ENABLE)
new_register_value = toggle_bitmap_value(current_register_value, PULSE_GEN_ENABLE_COMMANDS[self._number], False)
self.write_to_parent_device_register(SPC_XIO_PULSEGEN_ENABLE, new_register_value)
Expand All @@ -171,6 +183,7 @@ def set_output_inversion(self, inverted: bool) -> None:

@property
def trigger_detection_mode(self) -> PulseGeneratorTriggerDetectionMode:
"""How the pulse generator trigger circuit responds to a trigger signal, .e.g rising edge..."""
currently_enabled_config_options = decode_pulse_gen_config(
self.read_parent_device_register(PULSE_GEN_CONFIG_COMMANDS[self._number])
)
Expand All @@ -180,6 +193,7 @@ def trigger_detection_mode(self) -> PulseGeneratorTriggerDetectionMode:
return PulseGeneratorTriggerDetectionMode.RISING_EDGE

def set_trigger_detection_mode(self, mode: PulseGeneratorTriggerDetectionMode) -> None:
"""e.g. rising edge, high-voltage..."""
current_register_value = self.read_parent_device_register(PULSE_GEN_CONFIG_COMMANDS[self._number])
high_voltage_mode_value = PulseGeneratorTriggerDetectionMode.SPCM_PULSEGEN_CONFIG_HIGH.value
new_register_value = toggle_bitmap_value(
Expand All @@ -191,21 +205,25 @@ def set_trigger_detection_mode(self, mode: PulseGeneratorTriggerDetectionMode) -

@property
def trigger_mode(self) -> PulseGeneratorTriggerMode:
"""Gated, triggered or single-shot. See PulseGeneratorTriggerMode for more information."""
return PulseGeneratorTriggerMode(
self.read_parent_device_register(PULSE_GEN_TRIGGER_MODE_COMMANDS[self._number])
)

def set_trigger_mode(self, mode: PulseGeneratorTriggerMode) -> None:
"""Gated, triggered or single-shot. See PulseGeneratorTriggerMode for more information."""
self.write_to_parent_device_register(PULSE_GEN_TRIGGER_MODE_COMMANDS[self._number], mode.value)

@property
def min_allowed_period_in_seconds(self) -> float:
"""Minimum allowed pulse period in seconds, given the current clock rate."""
reg_val = self.read_parent_device_register(SPC_XIO_PULSEGEN_AVAILLEN_MIN)
reg_val = 0 if reg_val < 0 else reg_val
return self._convert_clock_cycles_to_seconds(reg_val)

@property
def max_allowed_period_in_seconds(self) -> float:
"""Maximum allowed pulse period in seconds, given the current clock rate."""
reg_val = self.read_parent_device_register(SPC_XIO_PULSEGEN_AVAILLEN_MAX)
reg_val = iinfo(int16).max if reg_val < 0 else reg_val
return self._convert_clock_cycles_to_seconds(reg_val)
Expand All @@ -216,10 +234,12 @@ def _allowed_period_step_size_in_clock_cycles(self) -> int:

@property
def allowed_period_step_size_in_seconds(self) -> float:
"""Resolution with which the pulse period can be set, given the current clock rate."""
return self._convert_clock_cycles_to_seconds(self._allowed_period_step_size_in_clock_cycles)

@property
def period_in_seconds(self) -> float:
"""The pulse length in seconds, including both the high-voltage and low-voltage sections."""
return self._convert_clock_cycles_to_seconds(
self.read_parent_device_register(PULSE_GEN_PULSE_PERIOD_COMMANDS[self._number])
)
Expand Down Expand Up @@ -251,12 +271,14 @@ def set_period_in_seconds(self, period: float, coerce: bool = False) -> float:

@property
def min_allowed_high_voltage_duration_in_seconds(self) -> float:
"""Minimum allowed duration of the high-voltage part of the pulse in seconds, given the current clock rate."""
reg_val = self.read_parent_device_register(SPC_XIO_PULSEGEN_AVAILHIGH_MIN)
reg_val = 0 if reg_val < 0 else reg_val
return self._convert_clock_cycles_to_seconds(reg_val)

@property
def max_allowed_high_voltage_duration_in_seconds(self) -> float:
"""Maximum allowed duration of the high-voltage part of the pulse in seconds, given the current clock rate."""
reg_val = self.read_parent_device_register(SPC_XIO_PULSEGEN_AVAILHIGH_MAX)
reg_val = iinfo(int16).max if reg_val < 0 else reg_val
return self._convert_clock_cycles_to_seconds(reg_val)
Expand All @@ -267,20 +289,24 @@ def _allowed_high_voltage_duration_step_size_in_clock_cycles(self) -> int:

@property
def allowed_high_voltage_duration_step_size_in_seconds(self) -> float:
"""Resolution with which the high-voltage duration can be set, in seconds, given the current clock rate."""
return self._convert_clock_cycles_to_seconds(self._allowed_high_voltage_duration_step_size_in_clock_cycles)

@property
def duration_of_high_voltage_in_seconds(self) -> float:
"""The length of the high-voltage part of a pulse, in seconds. Equal to the pulse duration * duty cycle."""
return self._convert_clock_cycles_to_seconds(
self.read_parent_device_register(PULSE_GEN_HIGH_DURATION_COMMANDS[self._number])
)

@property
def duration_of_low_voltage_in_seconds(self) -> float:
"""The length of the low-voltage part of a pulse, in seconds. Equal to the pulse duration * (1 - duty cycle)."""
return self.period_in_seconds - self.duration_of_high_voltage_in_seconds

@property
def duty_cycle(self) -> float:
"""The ratio between the high-voltage and low-voltage parts of the pulse."""
return self.duration_of_high_voltage_in_seconds / self.period_in_seconds

def set_duty_cycle(self, duty_cycle: float, coerce: bool = False) -> float:
Expand Down Expand Up @@ -311,16 +337,19 @@ def set_duty_cycle(self, duty_cycle: float, coerce: bool = False) -> float:

@property
def min_allowed_pulses(self) -> int:
"""Minimum allowed number of pulses to transmit."""
return self.read_parent_device_register(SPC_XIO_PULSEGEN_AVAILLOOPS_MIN)

@property
def max_allowed_pulses(self) -> int:
"""Maximum allowed number of pulses to transmit."""
reg_val = self.read_parent_device_register(SPC_XIO_PULSEGEN_AVAILLOOPS_MAX)
# my card has this register set to -2, which I assume means no limit (can't work it out from the docs)
return reg_val if reg_val > 0 else iinfo(int16).max

@property
def allowed_num_pulses_step_size(self) -> int:
"""Resolution with which the number of pulses to transmit can be set."""
return self.read_parent_device_register(SPC_XIO_PULSEGEN_AVAILLOOPS_STEP)

@property
Expand Down Expand Up @@ -354,18 +383,24 @@ def set_num_pulses(self, num_pulses: int, coerce: bool = False) -> int:

@property
def min_allowed_delay_in_seconds(self) -> float:
"""Minimum allowed delay between the trigger event and pulse generation, in seconds, given the current clock
rate."""
reg_value = self.read_parent_device_register(602007) # SPC_XIO_PULSEGEN_AVAILDELAY_MIN not in regs.py
reg_value = 0 if reg_value == -1 else reg_value
return self._convert_clock_cycles_to_seconds(reg_value)

@property
def max_allowed_delay_in_seconds(self) -> float:
"""Maximum allowed delay between the trigger event and pulse generation, in seconds, given the current clock
rate."""
reg_value = self.read_parent_device_register(602008) # SPC_XIO_PULSEGEN_AVAILDELAY_MAX not in regs.py
reg_value = iinfo(int16).max if reg_value == -1 else reg_value
return self._convert_clock_cycles_to_seconds(reg_value)

@property
def allowed_delay_step_size_in_seconds(self) -> float:
"""resolution with which the delay between the trigger event and pulse generation can be set, in seconds, given
the current clock rate."""
return self._convert_clock_cycles_to_seconds(
self.read_parent_device_register(602009) # SPC_XIO_PULSEGEN_AVAILDELAY_STEP not in regs.py
)
Expand Down

0 comments on commit 1509329

Please sign in to comment.