diff --git a/src/spectrumdevice/devices/awg/abstract_spectrum_awg.py b/src/spectrumdevice/devices/awg/abstract_spectrum_awg.py index 1755d74..e729f3f 100644 --- a/src/spectrumdevice/devices/awg/abstract_spectrum_awg.py +++ b/src/spectrumdevice/devices/awg/abstract_spectrum_awg.py @@ -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) diff --git a/src/spectrumdevice/devices/awg/awg_card.py b/src/spectrumdevice/devices/awg/awg_card.py index 6cedb9f..a1fa238 100644 --- a/src/spectrumdevice/devices/awg/awg_card.py +++ b/src/spectrumdevice/devices/awg/awg_card.py @@ -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) diff --git a/src/spectrumdevice/devices/awg/synthesis.py b/src/spectrumdevice/devices/awg/synthesis.py index 0ad5bc9..3f16ce2 100644 --- a/src/spectrumdevice/devices/awg/synthesis.py +++ b/src/spectrumdevice/devices/awg/synthesis.py @@ -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 @@ -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) diff --git a/src/spectrumdevice/features/pulse_generator/pulse_generator.py b/src/spectrumdevice/features/pulse_generator/pulse_generator.py index 9d80e06..d2fe5b6 100644 --- a/src/spectrumdevice/features/pulse_generator/pulse_generator.py +++ b/src/spectrumdevice/features/pulse_generator/pulse_generator.py @@ -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) @@ -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 @@ -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( @@ -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) @@ -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]) ) @@ -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( @@ -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) @@ -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]) ) @@ -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) @@ -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: @@ -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 @@ -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 )