diff --git a/README.md b/README.md index 796412a..1897587 100644 --- a/README.md +++ b/README.md @@ -18,11 +18,11 @@ for controlling devices: |--------------------------------|-------------------------------------------------------------------| | `MockSpectrumDigitiserCard` | Mocking individual digitiser cards | | `MockSpectrumDigitiserStarHub` | Mocking digitiser cards aggregated with a StarHub | -| `MockSpectrumAWGCard` | Mocking individual AWG cards (Not yet implemented) | +| `MockSpectrumAWGCard` | Mocking individual AWG cards | | `MockSpectrumAWGStarHub` | Mocking AWG cards aggregated with a StarHub (Not yet implemented) | -For digitisers, `spectrumdevice` currently only supports 'Standard Single' and 'Multi FIFO' acquisition modes. See the -Limitations section for more information. +For digitisers, `spectrumdevice` currently only supports 'Standard Single' and 'Multi FIFO' acquisition modes. For AWGs, +'Standard Single' and Standard Single Restart' modes are supported. See the Limitations section for more information. * [Examples](https://github.com/KCL-BMEIS/spectrumdevice/tree/main/example_scripts) * [API reference documentation](https://kcl-bmeis.github.io/spectrumdevice/) @@ -55,29 +55,32 @@ pip install https://github.com/KCL-BMEIS/spectrumdevice/tarball/main. ``` `spectrumdevice` depends only on NumPy. `spectrumdevice` includes a module called `spectrum_gmbh` containing a few -files taken from the `spcm_examples` directory, provided with Spectrum hardware. The files in this module were written by Spectrum GMBH and are included with their permission. The files provide `spectrumdevice` with a low-level Python interface to the Spectrum driver and define global constants which are used throughout `spectrumdevice`. +files taken from the `spcm_examples` directory, provided with Spectrum hardware. The files in this module were written +by Spectrum GMBH and are included with their permission. The files provide `spectrumdevice` with a low-level Python +interface to the Spectrum driver and define global constants which are used throughout `spectrumdevice`. ## Limitations * Currently, `spectrumdevice` only supports Standard Single and Multi FIFO digitiser acquisition modes. See the Spectrum documentation for more information. +* Only Standard Single and Standard Single Restart modes have been implemented for AWGs. * If timestamping is enabled, timestamps are acquired using Spectrum's 'polling' mode. This seems to add around 5 to 10 ms of latency to the acquisition. * Only current digitisers from the [59xx](https://spectrum-instrumentation.com/de/59xx-16-bit-digitizer-125-mss), [44xx](https://spectrum-instrumentation.com/de/44xx-1416-bit-digitizers-500-mss) and [22xx](https://spectrum-instrumentation.com/de/22xx-8-bit-digitizers-5-gss) families are currently supported, and -`spectrumdevice` has only been tested on 59xx devices. However, `spectrumdevice` may work fine on older devices. If -you've tried `spectrumdevice` on an older device, please let us know if it works and raise any issues you encounter in -the issue tracker. It's likely possible to add support with minimal effort. +`spectrumdevice` has only been tested on 59xx digitisers and 65xx AWGs. However, `spectrumdevice` may work fine on older +devices. If you've tried `spectrumdevice` on an older device, please let us know if it works and raise any issues you +encounter in the issue tracker. It's likely possible to add support with minimal effort. ## Usage ### Connect to devices Connect to local (PCIe) cards: ```python -from spectrumdevice import SpectrumDigitiserCard +from spectrumdevice import SpectrumDigitiserCard, SpectrumAWGCard -card_0 = SpectrumDigitiserCard(device_number=0) -card_1 = SpectrumDigitiserCard(device_number=1) +digitiser_1 = SpectrumDigitiserCard(device_number=0) +awg_1 = SpectrumAWGCard(device_number=1) ``` Connect to networked cards (you can find a card's IP using the [Spectrum Control Centre](https://spectrum-instrumentation.com/en/spectrum-control-center) software): @@ -120,29 +123,39 @@ modules in a hardware device using the of the mock data source must also be set on construction. ```python -from spectrumdevice import MockSpectrumDigitiserCard, MockSpectrumDigitiserStarHub +from spectrumdevice import MockSpectrumDigitiserCard, MockSpectrumDigitiserStarHub, MockSpectrumAWGCard from spectrumdevice.settings import ModelNumber -mock_card = MockSpectrumDigitiserCard(device_number=0, model=ModelNumber.TYP_M2P5966_X4, - mock_source_frame_rate_hz=10.0, - num_modules=2, num_channels_per_module=4) -mock_hub = MockSpectrumDigitiserStarHub(device_number=0, child_cards=[mock_card], master_card_index=0) +mock_digitiser = MockSpectrumDigitiserCard( + device_number=0, + model=ModelNumber.TYP_M2P5966_X4, + mock_source_frame_rate_hz=10.0, + num_modules=2, + num_channels_per_module=4 +) +mock_hub = MockSpectrumDigitiserStarHub(device_number=0, child_cards=[mock_digitiser], master_card_index=0) +mock_awg = MockSpectrumAWGCard( + device_number=0, + model=ModelNumber.TYP_M2P6560_X4, + num_modules=1, + num_channels_per_module=1 +) ``` After construction, mock devices can be used identically to real devices. ### Configuring device settings -`SpectrumDigitiserCard` and `SpectrumDigitiserStarHub` provide methods for reading and writing device settings located -within on-device registers. Some settings must be set using Enums imported from the `settings` module. Others are set -using integer values. For example, to put a card in 'Standard Single' acquisition mode and set the sample rate to 10 -MHz: +`SpectrumDigitiserCard`, `SpectrumDigitiserStarHub` and `SpectrumAWGCard` provide methods for reading and writing device +settings located within on-device registers. Some settings must be set using Enums imported from the `settings` module. + Others are set using integer values. For example, to put a digitiser card in 'Standard Single' acquisition mode and set +the sample rate to 10 MHz: ```python from spectrumdevice import SpectrumDigitiserCard from spectrumdevice.settings import AcquisitionMode -card = SpectrumDigitiserCard(device_number=0) -card.set_acquisition_mode(AcquisitionMode.SPC_REC_STD_SINGLE) -card.set_sample_rate_in_hz(10000000) +digitiser_card = SpectrumDigitiserCard(device_number=0) +digitiser_card.set_acquisition_mode(AcquisitionMode.SPC_REC_STD_SINGLE) +digitiser_card.set_sample_rate_in_hz(10000000) ``` and to print the currently set sample rate: @@ -151,34 +164,41 @@ print(card.sample_rate_in_hz) ``` ### Configuring channel settings -The channels available to a spectrum device (card or StarHub) can be accessed via the `channels` property. This -property contains a list of `SpectrumDigitiserChannel` or `SpectrumAWGChannel` objects which provide methods for -independently configuring each channel. -For example, to change the vertical range of channel 2 of a digitiser card to 1V: +The analog channels available to a spectrum device (card or StarHub) can be accessed via the `analog_channels` property. + This property contains a list of `SpectrumDigitiserChannel` or `SpectrumAWGChannel` objects which provide methods for +independently configuring each channel. For example, to change the vertical range of channel 2 of a digitiser card to 1V: ```python -card.analog_channels[2].set_vertical_range_in_mv(1000) +digitiser_card.analog_channels[2].set_vertical_range_in_mv(1000) ``` and then print the vertical offset: ```python -print(card.analog_channels[2].vertical_offset_in_percent) +print(digitiser_card.analog_channels[2].vertical_offset_in_percent) ``` ### Configuring everything at once -You can set multiple settings at once using the `TriggerSettings` and `AcquisitionSettings` dataclasses and the -`configure_trigger()` and `configure_acquisition()` methods: +You can set multiple settings at once using the `TriggerSettings`, `AcquisitionSettings` and `GenerationSettings` +dataclasses and the `configure_trigger()`, `configure_acquisition()` and `configure_generation()` methods: ```python +import numpy as np + +from spectrumdevice import SpectrumDigitiserCard, SpectrumAWGCard from spectrumdevice.settings import TriggerSettings, AcquisitionSettings, TriggerSource, ExternalTriggerMode, \ -AcquisitionMode +AcquisitionMode, GenerationSettings, GenerationMode, OutputChannelFilter, OutputChannelStopLevelMode from spectrumdevice.settings.channel import InputImpedance +digitiser_card = SpectrumDigitiserCard(device_number=0) +awg_card = SpectrumAWGCard(device_number=1) + trigger_settings = TriggerSettings( trigger_sources=[TriggerSource.SPC_TMASK_EXT0], external_trigger_mode=ExternalTriggerMode.SPC_TM_POS, external_trigger_level_in_mv=1000, ) +digitiser_card.configure_trigger(trigger_settings) +awg_card.configure_trigger(trigger_settings) acquisition_settings = AcquisitionSettings( acquisition_mode=AcquisitionMode.SPC_REC_FIFO_MULTI, @@ -193,9 +213,20 @@ acquisition_settings = AcquisitionSettings( timestamping_enabled=True, batch_size=1 ) - -card.configure_trigger(trigger_settings) -card.configure_acquisition(acquisition_settings) +digitiser_card.configure_acquisition(acquisition_settings) + +generation_settings = GenerationSettings( + generation_mode=GenerationMode.SPC_REP_STD_SINGLERESTART, + waveform=np.array(np.ones(16), dtype=np.int16), + sample_rate_in_hz=40000000, + num_loops=5, + enabled_channels=[0], + signal_amplitudes_in_mv=[1000], + dc_offsets_in_mv=[0], + output_filters=[OutputChannelFilter.LOW_PASS_70_MHZ], + stop_level_modes=[OutputChannelStopLevelMode.SPCM_STOPLVL_ZERO], +) +awg_card.configure_generation(generation_settings) ``` ### Acquiring waveforms from a digitiser (standard single mode) @@ -272,6 +303,67 @@ card.stop() ``` and execute some logic to exit the `while` loop. +### Generating a signal with an AWG +After configuring your trigger and generation settings as shown above, you can start your card: +```python +awg_card.start() +``` +The card is now waiting for a trigger. If the card is in software trigger mode, you can trigger its output manually: +```python +awg_card.force_trigger() +``` +Then stop and disconnect when finished: +```python +awg_card.stop() +awg_card.disconnect() +``` + +### Using the optional Pulse Generator firmware add-on +For both AWGs and Digitisers, Spectrum provide an optional pulse generator feature which can be activated retrospectively. + +Each of the card's four multipurpose IO lines (X0, X1, X2 and X3) has a pulse generator. Choose the one you would like +to use and set it to pulse gen mode. Here we are using X0 (index 0) +```python +from spectrumdevice.settings import IOLineMode + +io_line_index = 0 +card.io_lines[io_line_index].set_mode(IOLineMode.SPCM_XMODE_PULSEGEN) +``` +Then get its pulse generator and configure its trigger and output settings +```python +from spectrumdevice.settings import ( + PulseGeneratorTriggerSettings, + PulseGeneratorTriggerMode, + PulseGeneratorTriggerDetectionMode, + PulseGeneratorMultiplexer1TriggerSource, + PulseGeneratorMultiplexer2TriggerSource, + PulseGeneratorOutputSettings +) + +pulse_gen = card.io_lines[io_line_index].pulse_generator +pg_trigger_settings = PulseGeneratorTriggerSettings( + trigger_mode=PulseGeneratorTriggerMode.SPCM_PULSEGEN_MODE_SINGLESHOT, + trigger_detection_mode=PulseGeneratorTriggerDetectionMode.RISING_EDGE, + multiplexer_1_source=PulseGeneratorMultiplexer1TriggerSource.SPCM_PULSEGEN_MUX1_SRC_UNUSED, + multiplexer_1_output_inversion=False, + multiplexer_2_source=PulseGeneratorMultiplexer2TriggerSource.SPCM_PULSEGEN_MUX2_SRC_SOFTWARE, + multiplexer_2_output_inversion=False, +) +pulse_gen.configure_trigger(pg_trigger_settings) +pulse_output_settings = PulseGeneratorOutputSettings( + period_in_seconds=1e-3, duty_cycle=0.5, num_pulses=10, delay_in_seconds=0.0, output_inversion=False +) +pulse_gen.configure_output(pulse_output_settings) +# Enable the pulse generator +pulse_gen.enable() +``` +We have set the pulse generator to use a software trigger, so you can manually trigger it to start pulsing: +```python +pulse_gen.force_trigger() + +card.stop() +card.disconnect() +``` ## Examples See the `example_scripts` directory. diff --git a/src/example_scripts/awg_example.py b/src/example_scripts/awg_example.py deleted file mode 100644 index 660c673..0000000 --- a/src/example_scripts/awg_example.py +++ /dev/null @@ -1,52 +0,0 @@ -from time import sleep - -from matplotlib.pyplot import plot, show -from numpy import int16 - -from spectrumdevice.devices.awg.awg_card import SpectrumAWGCard -from spectrumdevice.devices.awg.synthesis import make_full_scale_sine_waveform -from spectrumdevice.settings import TriggerSettings, TriggerSource, ExternalTriggerMode -from spectrumdevice.settings.channel import OutputChannelStopLevelMode -from spectrumdevice.settings.device_modes import GenerationMode - -PULSE_RATE_HZ = 5000 -NUM_PULSES = 5 -NUM_CYCLES = 2 -FREQUENCY = 20e3 -SAMPLE_RATE = 125000000 - - -if __name__ == "__main__": - - card = SpectrumAWGCard(device_number=0) - print(card) - - trigger_settings = TriggerSettings( - trigger_sources=[TriggerSource.SPC_TMASK_EXT0], - external_trigger_mode=ExternalTriggerMode.SPC_TM_POS, - external_trigger_level_in_mv=200, - ) - card.configure_trigger(trigger_settings) - - t, analog_wfm = make_full_scale_sine_waveform(FREQUENCY, SAMPLE_RATE, NUM_CYCLES, dtype=int16) - - card.set_sample_rate_in_hz(SAMPLE_RATE) - card.set_generation_mode(GenerationMode.SPC_REP_STD_SINGLERESTART) - card.set_num_loops(NUM_PULSES) - card.transfer_waveform(analog_wfm) - card.analog_channels[0].set_stop_level_mode(OutputChannelStopLevelMode.SPCM_STOPLVL_ZERO) - card.analog_channels[0].set_is_switched_on(True) - card.analog_channels[0].set_signal_amplitude_in_mv(1000) - - card.start() - - for _ in range(NUM_PULSES): - card.force_trigger_event() - sleep(1 / PULSE_RATE_HZ) - print("generated pulse") - - card.stop() - card.disconnect() - - plot(t * 1e6, analog_wfm) - show() diff --git a/src/example_scripts/awg_standard_single_restart_mode_example.py b/src/example_scripts/awg_standard_single_restart_mode_example.py new file mode 100644 index 0000000..4765929 --- /dev/null +++ b/src/example_scripts/awg_standard_single_restart_mode_example.py @@ -0,0 +1,80 @@ +from time import sleep + +from numpy import int16 + +from spectrumdevice.devices.awg.awg_card import SpectrumAWGCard +from spectrumdevice.devices.awg.synthesis import make_full_scale_sine_waveform +from spectrumdevice.devices.mocks import MockSpectrumAWGCard +from spectrumdevice.settings import ( + TriggerSettings, + TriggerSource, + ExternalTriggerMode, + GenerationSettings, + OutputChannelFilter, + ModelNumber, +) +from spectrumdevice.settings.channel import OutputChannelStopLevelMode +from spectrumdevice.settings.device_modes import GenerationMode + +PULSE_RATE_HZ = 5000 +NUM_PULSES = 5 +NUM_CYCLES = 2 +FREQUENCY = 20e3 +SAMPLE_RATE = 125000000 + + +def awg_single_restart_mode_example(mock_mode: bool) -> None: + + # create a connection to a mock or real AWG card + if not mock_mode: + card = SpectrumAWGCard(device_number=0) + else: + card = MockSpectrumAWGCard( + device_number=0, model=ModelNumber.TYP_M2P6560_X4, num_modules=1, num_channels_per_module=1 + ) + + sample_rate_in_hz = 1000000 + number_of_generations = 3 + + # create a waveform to generate + t, analog_wfm = make_full_scale_sine_waveform( + frequency_in_hz=20e3, sample_rate_in_hz=sample_rate_in_hz, num_cycles=1, dtype=int16 + ) + + # configure signal generation + generation_settings = GenerationSettings( + generation_mode=GenerationMode.SPC_REP_STD_SINGLERESTART, + waveform=analog_wfm, + sample_rate_in_hz=sample_rate_in_hz, + num_loops=number_of_generations, + enabled_channels=[0], + signal_amplitudes_in_mv=[1000], + dc_offsets_in_mv=[0], + output_filters=[OutputChannelFilter.LOW_PASS_70_MHZ], + stop_level_modes=[OutputChannelStopLevelMode.SPCM_STOPLVL_ZERO], + ) + card.configure_generation(generation_settings) + + # configure triggering (here we set to software trigger) + trigger_settings = TriggerSettings( + trigger_sources=[TriggerSource.SPC_TMASK_EXT0], + external_trigger_mode=ExternalTriggerMode.SPC_TM_POS, + external_trigger_level_in_mv=200, + ) + card.configure_trigger(trigger_settings) + + # start the card and then force a trigger for each generation we want to perform + # we are using GenerationMode.SPC_REP_STD_SINGLERESTART so the whole waveform will be generated each time the card + # is trigger, until "num_loops" triggers have been detected. + card.start() + for _ in range(number_of_generations): + card.force_trigger() + sleep(100e-3) # here we are waiting 0.1 seconds between triggers + print("generated pulse") + card.stop() + card.disconnect() + + +if __name__ == "__main__": + # change mock_mode to False to connect to a real card + awg_single_restart_mode_example(mock_mode=True) diff --git a/src/example_scripts/awg_trigger_with_pulse_generator_example.py b/src/example_scripts/awg_trigger_with_pulse_generator_example.py new file mode 100644 index 0000000..880668f --- /dev/null +++ b/src/example_scripts/awg_trigger_with_pulse_generator_example.py @@ -0,0 +1,133 @@ +from time import sleep + +from numpy import int16 + +from spectrumdevice.devices.awg.synthesis import make_full_scale_rect_waveform +from spectrumdevice.devices.mocks import MockSpectrumAWGCard +from spectrumdevice.settings import ( + TriggerSettings, + TriggerSource, + ExternalTriggerMode, + IOLineMode, + ModelNumber, + AdvancedCardFeature, + GenerationSettings, + OutputChannelFilter, +) +from spectrumdevice.settings.channel import OutputChannelStopLevelMode +from spectrumdevice.settings.device_modes import GenerationMode +from spectrumdevice.settings.pulse_generator import ( + PulseGeneratorOutputSettings, + PulseGeneratorTriggerSettings, + PulseGeneratorTriggerMode, + PulseGeneratorTriggerDetectionMode, + PulseGeneratorMultiplexer1TriggerSource, + PulseGeneratorMultiplexer2TriggerSource, +) + + +SAMPLE_RATE_IN_HZ = 40000000 +NUM_GENERATIONS = 4 + + +if __name__ == "__main__": + + # AWG CARD SETUP --------------------------------------------------------------------------------------------------- + + # Connect to a real AWG card with the optional pulse generator firmware option unlocked + # card = SpectrumAWGCard(device_number=0) + + # Or a mock AWG card + card = MockSpectrumAWGCard( + device_number=0, + model=ModelNumber.TYP_M2P6560_X4, + num_modules=1, + num_channels_per_module=1, + # make sure the mock card has the pulse generator feature unlocked! + advanced_card_features=[AdvancedCardFeature.SPCM_FEAT_EXTFW_PULSEGEN], + ) + + # Set up the AWG trigger settings using the X1 multipurpose IO Line as a trigger source + card_trigger_settings = TriggerSettings( + # ext1 trigger source is the X1 I/O Line (X0 cannot be used as a trigger source because it is output-only) + trigger_sources=[TriggerSource.SPC_TMASK_EXT1], + external_trigger_mode=ExternalTriggerMode.SPC_TM_POS, + ) + card.configure_trigger(card_trigger_settings) + + # Create an AWG waveform to generate + t, analog_wfm = make_full_scale_rect_waveform( + sample_rate_in_hz=SAMPLE_RATE_IN_HZ, duration_in_seconds=0.25e-3, dtype=int16 + ) + + # Configure signal generation. SINGLERESTART mode means that, on receipt of a trigger, the AWG will output its + # waveform and then wait for another trigger. The card will stop once num_loops waveforms have been generated. + generation_settings = GenerationSettings( + generation_mode=GenerationMode.SPC_REP_STD_SINGLERESTART, + waveform=analog_wfm, + sample_rate_in_hz=SAMPLE_RATE_IN_HZ, + num_loops=NUM_GENERATIONS, + enabled_channels=[0], + signal_amplitudes_in_mv=[1000], + dc_offsets_in_mv=[0], + output_filters=[OutputChannelFilter.LOW_PASS_70_MHZ], + stop_level_modes=[OutputChannelStopLevelMode.SPCM_STOPLVL_ZERO], + ) + card.configure_generation(generation_settings) + + # Each of the card's four multipurpose IO lines (X0, X1, X2 and X3) has a pulse generator + # Choose the one you would like to use and set it to pulse gen mode. Here we are using X1 (index 1) because + # X0 is output-only and can therefore not be selected as a trigger source for the AWG. + io_line_index = 1 + card.io_lines[io_line_index].set_mode(IOLineMode.SPCM_XMODE_PULSEGEN) + + # PULSE GENERATOR SETUP -------------------------------------------------------------------------------------------- + + # Get the IO Line's pulse generator + pulse_gen = card.io_lines[io_line_index].pulse_generator + + # Configure the pulse generator's trigger settings + # Here we are configuring a software trigger by setting multiplexer 2 source to SPCM_PULSEGEN_MUX2_SRC_SOFTWARE + # Setting multiplexer 1 source to SPCM_PULSEGEN_MUX1_SRC_UNUSED means that the software trigger will work weather or + # not the card is armed or started + # The trigger mode of SINGLESHOT means the pulse generator will wait for a single trigger, and then will output + # a predefined number of pulses before stopping (and not rearming) + pulse_trigger_settings = PulseGeneratorTriggerSettings( + trigger_mode=PulseGeneratorTriggerMode.SPCM_PULSEGEN_MODE_SINGLESHOT, + trigger_detection_mode=PulseGeneratorTriggerDetectionMode.RISING_EDGE, + multiplexer_1_source=PulseGeneratorMultiplexer1TriggerSource.SPCM_PULSEGEN_MUX1_SRC_UNUSED, + multiplexer_1_output_inversion=False, + multiplexer_2_source=PulseGeneratorMultiplexer2TriggerSource.SPCM_PULSEGEN_MUX2_SRC_SOFTWARE, + multiplexer_2_output_inversion=False, + ) + pulse_gen.configure_trigger(pulse_trigger_settings) + + # Configure the pulse generator output + # The pulse generator will output num_pulses pulses on receipt of a trigger before stopping. Set to 0 for + # continuous output + # The period is the length of the whole pulse (high-voltage length + 0V length) + # The duty cycle is the high-voltage length divided by the period + pulse_output_settings = PulseGeneratorOutputSettings( + period_in_seconds=1e-3, duty_cycle=0.5, num_pulses=NUM_GENERATIONS, delay_in_seconds=0.0, output_inversion=False + ) + pulse_gen.configure_output(pulse_output_settings) + + # Enable the pulse generator + pulse_gen.enable() + + # Start the AWG so it is waiting for a trigger + card.start() + + # Force a software trigger on the pulse generator, causing pulse to be generated on X1, triggering the AWG + pulse_gen.force_trigger() + # Wait for the pulse sequence to complete + sleep(1) + + # Note that there is a delay between a trigger being received and the AWG generating a signal. + # This is in the technical data section of the manual, and for my device is apparently 63 samples + 7 ns + # However I see 74 samples + 7 ns when testing this script, suggesting there is an additional delay of 11 samples + # between the pulse generator and the AWG trigger circuitry + print(f"Expected delay between pulse and signal: {(63 * 1 / SAMPLE_RATE_IN_HZ + 7e-9) * 1e6} microseconds") + + card.stop() + card.disconnect() diff --git a/src/example_scripts/continuous_averaging_fifo_mode.py b/src/example_scripts/digitiser_continuous_averaging_fifo_mode_example.py similarity index 100% rename from src/example_scripts/continuous_averaging_fifo_mode.py rename to src/example_scripts/digitiser_continuous_averaging_fifo_mode_example.py diff --git a/src/example_scripts/continuous_multi_fifo_mode.py b/src/example_scripts/digitiser_continuous_multi_fifo_mode_example.py similarity index 100% rename from src/example_scripts/continuous_multi_fifo_mode.py rename to src/example_scripts/digitiser_continuous_multi_fifo_mode_example.py diff --git a/src/example_scripts/finite_multi_fifo_mode.py b/src/example_scripts/digitiser_finite_multi_fifo_mode_example.py similarity index 100% rename from src/example_scripts/finite_multi_fifo_mode.py rename to src/example_scripts/digitiser_finite_multi_fifo_mode_example.py diff --git a/src/example_scripts/standard_single_mode.py b/src/example_scripts/digitiser_standard_single_mode_example.py similarity index 94% rename from src/example_scripts/standard_single_mode.py rename to src/example_scripts/digitiser_standard_single_mode_example.py index 1f3c903..3ef8805 100644 --- a/src/example_scripts/standard_single_mode.py +++ b/src/example_scripts/digitiser_standard_single_mode_example.py @@ -17,7 +17,7 @@ ) -def standard_single_mode_example( +def digitiser_standard_single_mode_example( mock_mode: bool, trigger_source: TriggerSource, device_number: int, @@ -30,8 +30,6 @@ def standard_single_mode_example( card = SpectrumDigitiserCard(device_number=device_number, ip_address=ip_address) else: # Set up a mock device - for item in MockSpectrumDigitiserCard.__mro__: - print(item) card = MockSpectrumDigitiserCard( device_number=device_number, model=ModelNumber.TYP_M2P5966_X4, @@ -76,7 +74,7 @@ def standard_single_mode_example( from matplotlib.pyplot import plot, show, xlabel, tight_layout, ylabel - meas = standard_single_mode_example( + meas = digitiser_standard_single_mode_example( mock_mode=True, trigger_source=TriggerSource.SPC_TMASK_EXT0, device_number=1, ip_address="169.254.13.35" ) diff --git a/src/example_scripts/star_hub_example.py b/src/example_scripts/digitiser_star_hub_example_example.py similarity index 100% rename from src/example_scripts/star_hub_example.py rename to src/example_scripts/digitiser_star_hub_example_example.py diff --git a/src/example_scripts/pulse_generator_example.py b/src/example_scripts/pulse_generator_example.py new file mode 100644 index 0000000..6abc5a8 --- /dev/null +++ b/src/example_scripts/pulse_generator_example.py @@ -0,0 +1,72 @@ +from spectrumdevice.devices.awg.awg_card import SpectrumAWGCard +from spectrumdevice.devices.mocks import MockSpectrumAWGCard +from spectrumdevice.settings import ModelNumber, IOLineMode, AdvancedCardFeature +from spectrumdevice.settings.pulse_generator import ( + PulseGeneratorTriggerSettings, + PulseGeneratorTriggerMode, + PulseGeneratorTriggerDetectionMode, + PulseGeneratorMultiplexer1TriggerSource, + PulseGeneratorMultiplexer2TriggerSource, + PulseGeneratorOutputSettings, +) + + +def pulse_generator_example(mock_mode: bool) -> None: + + # Create connection to a mock or real card. Here we are using an AWG but could be a digitiser + if not mock_mode: + card = SpectrumAWGCard(device_number=0) + else: + card = MockSpectrumAWGCard( + device_number=0, + model=ModelNumber.TYP_M2P6560_X4, + num_modules=1, + num_channels_per_module=1, + # make sure the mock card has the pulse generator feature unlocked! + advanced_card_features=[AdvancedCardFeature.SPCM_FEAT_EXTFW_PULSEGEN], + ) + + # Each of the card's four multipurpose IO lines (X0, X1, X2 and X3) has a pulse generator + # Choose the one you would like to use and set it to pulse gen mode. Here we are using X0 (index 0) + io_line_index = 0 + card.io_lines[io_line_index].set_mode(IOLineMode.SPCM_XMODE_PULSEGEN) + # Then get its pulse generator + pulse_gen = card.io_lines[io_line_index].pulse_generator + + # Configure the trigger behavior. + # Here we are configuring a software trigger by setting multiplexer 2 source to SPCM_PULSEGEN_MUX2_SRC_SOFTWARE + # Setting multiplexer 1 source to SPCM_PULSEGEN_MUX1_SRC_UNUSED means that the software trigger will work weather or + # not the card is armed or started + # The trigger mode of SINGLESHOT means the pulse generator will wait for a single trigger, and then will output + # a predefined number of pulses before stopping (and not rearming) + pg_trigger_settings = PulseGeneratorTriggerSettings( + trigger_mode=PulseGeneratorTriggerMode.SPCM_PULSEGEN_MODE_SINGLESHOT, + trigger_detection_mode=PulseGeneratorTriggerDetectionMode.RISING_EDGE, + multiplexer_1_source=PulseGeneratorMultiplexer1TriggerSource.SPCM_PULSEGEN_MUX1_SRC_UNUSED, + multiplexer_1_output_inversion=False, + multiplexer_2_source=PulseGeneratorMultiplexer2TriggerSource.SPCM_PULSEGEN_MUX2_SRC_SOFTWARE, + multiplexer_2_output_inversion=False, + ) + pulse_gen.configure_trigger(pg_trigger_settings) + + # Configure the pulse output + # The pulse generator will output num_pulses pulses on receipt of a trigger before stopping. Set to 0 for + # continuous output + # The period is the length of the whole pulse (high-voltage length + 0V length) + # The duty cycle is the high-voltage length divided by the period + pulse_output_settings = PulseGeneratorOutputSettings( + period_in_seconds=1e-3, duty_cycle=0.5, num_pulses=10, delay_in_seconds=0.0, output_inversion=False + ) + pulse_gen.configure_output(pulse_output_settings) + # Enable the pulse generator + pulse_gen.enable() + + # Generate a software trigger to start outputting pulses + pulse_gen.force_trigger() + + card.stop() + card.disconnect() + + +if __name__ == "__main__": + pulse_generator_example(mock_mode=True) diff --git a/src/example_scripts/trigger_awg_with_pulse_generator_example.py b/src/example_scripts/trigger_awg_with_pulse_generator_example.py deleted file mode 100644 index 27a5508..0000000 --- a/src/example_scripts/trigger_awg_with_pulse_generator_example.py +++ /dev/null @@ -1,68 +0,0 @@ -from time import sleep - -from numpy import int16 - -from spectrumdevice.devices.awg.awg_card import SpectrumAWGCard -from spectrumdevice.devices.awg.synthesis import make_full_scale_sine_waveform -from spectrumdevice.settings import TriggerSettings, TriggerSource, ExternalTriggerMode, IOLineMode -from spectrumdevice.settings.channel import OutputChannelStopLevelMode -from spectrumdevice.settings.device_modes import GenerationMode -from spectrumdevice.settings.pulse_generator import ( - PulseGeneratorOutputSettings, - PulseGeneratorTriggerSettings, - PulseGeneratorTriggerMode, - PulseGeneratorTriggerDetectionMode, - PulseGeneratorMultiplexer1TriggerSource, - PulseGeneratorMultiplexer2TriggerSource, -) - - -SAMPLE_RATE = 40000000 - - -if __name__ == "__main__": - - card = SpectrumAWGCard(device_number=0) - - card_trigger_settings = TriggerSettings( - trigger_sources=[TriggerSource.SPC_TMASK_EXT2], # ext1 trigger source is first IOLine - external_trigger_mode=ExternalTriggerMode.SPC_TM_POS, - ) - - t, analog_wfm = make_full_scale_sine_waveform( - frequency_in_hz=1e6, sample_rate_hz=SAMPLE_RATE, num_cycles=1, dtype=int16 - ) - - # Set up AWG card - card.configure_trigger(card_trigger_settings) - card.set_sample_rate_in_hz(SAMPLE_RATE) - card.set_generation_mode(GenerationMode.SPC_REP_STD_SINGLERESTART) - card.set_num_loops(1) - card.transfer_waveform(analog_wfm) - card.analog_channels[0].set_stop_level_mode(OutputChannelStopLevelMode.SPCM_STOPLVL_ZERO) - card.analog_channels[0].set_is_switched_on(True) - card.analog_channels[0].set_signal_amplitude_in_mv(1000) - - pulse_trigger_settings = PulseGeneratorTriggerSettings( - trigger_mode=PulseGeneratorTriggerMode.SPCM_PULSEGEN_MODE_SINGLESHOT, - trigger_detection_mode=PulseGeneratorTriggerDetectionMode.RISING_EDGE, - multiplexer_1_source=PulseGeneratorMultiplexer1TriggerSource.SPCM_PULSEGEN_MUX1_SRC_UNUSED, - multiplexer_1_output_inversion=False, - multiplexer_2_source=PulseGeneratorMultiplexer2TriggerSource.SPCM_PULSEGEN_MUX2_SRC_SOFTWARE, - multiplexer_2_output_inversion=False, - ) - - pulse_output_settings = PulseGeneratorOutputSettings( - period_in_seconds=1e-3, duty_cycle=0.5, num_pulses=10, delay_in_seconds=0.0, output_inversion=False - ) - - card.io_lines[0].set_mode(IOLineMode.SPCM_XMODE_PULSEGEN) - card.io_lines[0].pulse_generator.configure_trigger(pulse_trigger_settings) - card.io_lines[0].pulse_generator.configure_output(pulse_output_settings) - - card.start() - - card.io_lines[0].pulse_generator.force_trigger() - sleep(1) - card.stop() - card.disconnect() diff --git a/src/spectrumdevice/__init__.py b/src/spectrumdevice/__init__.py index 1c4a46f..d7886e9 100644 --- a/src/spectrumdevice/__init__.py +++ b/src/spectrumdevice/__init__.py @@ -60,7 +60,8 @@ from .devices.digitiser.digitiser_card import SpectrumDigitiserCard from .devices.digitiser.digitiser_channel import SpectrumDigitiserAnalogChannel from .devices.digitiser.digitiser_star_hub import SpectrumDigitiserStarHub -from .devices.mocks import MockSpectrumDigitiserCard, MockSpectrumDigitiserStarHub +from .devices.awg.awg_card import SpectrumAWGCard +from .devices.mocks import MockSpectrumDigitiserCard, MockSpectrumDigitiserStarHub, MockSpectrumAWGCard from .devices.abstract_device import ( AbstractSpectrumDevice, AbstractSpectrumCard, @@ -82,6 +83,8 @@ "AbstractSpectrumChannel", "settings", "Measurement", + "SpectrumAWGCard", + "MockSpectrumAWGCard", ] diff --git a/src/spectrumdevice/devices/abstract_device/abstract_spectrum_card.py b/src/spectrumdevice/devices/abstract_device/abstract_spectrum_card.py index 0acfa59..8fa8800 100644 --- a/src/spectrumdevice/devices/abstract_device/abstract_spectrum_card.py +++ b/src/spectrumdevice/devices/abstract_device/abstract_spectrum_card.py @@ -223,7 +223,7 @@ def io_lines(self) -> Sequence[IOLineInterfaceType]: return self._io_lines @property - def enabled_analog_channels(self) -> List[int]: + def enabled_analog_channel_nums(self) -> List[int]: """The indices of the currently enabled channels. Returns: enabled_channels (List[int]): The indices of the currently enabled channels. @@ -484,7 +484,7 @@ def __str__(self) -> str: def type(self) -> CardType: return CardType(self.read_spectrum_device_register(SPC_FNCTYPE)) - def force_trigger_event(self) -> None: + def force_trigger(self) -> None: """Force a trigger event to occur""" self.write_to_spectrum_device_register(SPC_M2CMD, M2CMD_CARD_FORCETRIGGER) diff --git a/src/spectrumdevice/devices/abstract_device/abstract_spectrum_channel.py b/src/spectrumdevice/devices/abstract_device/abstract_spectrum_channel.py index 9e665f6..ce9b2cd 100644 --- a/src/spectrumdevice/devices/abstract_device/abstract_spectrum_channel.py +++ b/src/spectrumdevice/devices/abstract_device/abstract_spectrum_channel.py @@ -1,5 +1,5 @@ """Provides a partially-implemented abstract class common to individual channels of Spectrum devices.""" -from abc import abstractmethod +from abc import abstractmethod, ABC from typing import Any, TypeVar, Generic # Christian Baker, King's College London @@ -77,7 +77,9 @@ def __repr__(self) -> str: return str(self) -class AbstractSpectrumAnalogChannel(AbstractSpectrumChannel[SpectrumAnalogChannelName], SpectrumAnalogChannelInterface): +class AbstractSpectrumAnalogChannel( + AbstractSpectrumChannel[SpectrumAnalogChannelName], SpectrumAnalogChannelInterface, ABC +): """Partially implemented abstract superclass contain code common for controlling an individual analog channel of all spectrum devices.""" diff --git a/src/spectrumdevice/devices/abstract_device/abstract_spectrum_device.py b/src/spectrumdevice/devices/abstract_device/abstract_spectrum_device.py index f0dae7b..47c9638 100644 --- a/src/spectrumdevice/devices/abstract_device/abstract_spectrum_device.py +++ b/src/spectrumdevice/devices/abstract_device/abstract_spectrum_device.py @@ -5,6 +5,7 @@ # Licensed under the MIT. You may obtain a copy at https://opensource.org/licenses/MIT. from abc import ABC +from copy import copy from spectrumdevice.devices.abstract_device.device_interface import ( SpectrumDeviceInterface, @@ -13,6 +14,12 @@ ) from spectrumdevice.exceptions import SpectrumDeviceNotConnected, SpectrumDriversNotFound from spectrumdevice.settings import SpectrumRegisterLength, TriggerSettings +from spectrumdevice.settings.output_channel_pairing import ( + ChannelPair, + ChannelPairingMode, + DIFFERENTIAL_CHANNEL_PAIR_COMMANDS, + DOUBLING_CHANNEL_PAIR_COMMANDS, +) from spectrumdevice.settings.triggering import EXTERNAL_TRIGGER_SOURCES from spectrum_gmbh.regs import ( M2CMD_CARD_ENABLETRIGGER, @@ -91,6 +98,45 @@ def configure_trigger(self, settings: TriggerSettings) -> None: # Write the configuration to the card self.write_to_spectrum_device_register(SPC_M2CMD, M2CMD_CARD_WRITESETUP) + def configure_channel_pairing(self, channel_pair: ChannelPair, mode: ChannelPairingMode) -> None: + """Configures a pair of consecutive channels to operate either independently, in differential mode or + in double mode. If enabling differential or double mode, then the odd-numbered channel will be automatically + configured to be identical to the even-numbered channel, and the odd-numbered channel will be disabled as is + required by the Spectrum API. + + Args: + channel_pair (ChannelPair): The pair of channels to configure + mode (ChannelPairingMode): The mode to enable: SINGLE, DOUBLE, or DIFFERENTIAL + """ + + doubling_enabled = int(mode == ChannelPairingMode.DOUBLE) + differential_mode_enabled = int(mode == ChannelPairingMode.DIFFERENTIAL) + + if doubling_enabled and channel_pair in (channel_pair.CHANNEL_4_AND_5, channel_pair.CHANNEL_6_AND_7): + raise ValueError("Doubling can only be enabled for channel pairs CHANNEL_0_AND_1 or CHANNEL_2_AND_3.") + + if doubling_enabled or differential_mode_enabled: + self._mirror_even_channel_settings_on_odd_channel(channel_pair) + self._disable_odd_channel(channel_pair) + + self.write_to_spectrum_device_register( + DIFFERENTIAL_CHANNEL_PAIR_COMMANDS[channel_pair], differential_mode_enabled + ) + self.write_to_spectrum_device_register(DOUBLING_CHANNEL_PAIR_COMMANDS[channel_pair], doubling_enabled) + + def _disable_odd_channel(self, channel_pair: ChannelPair) -> None: + try: + enabled_channels = copy(self.enabled_analog_channel_nums) + enabled_channels.remove(channel_pair.value + 1) + self.set_enabled_analog_channels(enabled_channels) + except ValueError: + pass # odd numbered channel was not enable, so no need to disable it. + + def _mirror_even_channel_settings_on_odd_channel(self, channel_pair: ChannelPair) -> None: + self.analog_channels[channel_pair.value + 1].copy_settings_from_other_channel( + self.analog_channels[channel_pair.value] + ) + def write_to_spectrum_device_register( self, spectrum_register: int, diff --git a/src/spectrumdevice/devices/abstract_device/abstract_spectrum_hub.py b/src/spectrumdevice/devices/abstract_device/abstract_spectrum_hub.py index 6811a5e..4c4a1dd 100644 --- a/src/spectrumdevice/devices/abstract_device/abstract_spectrum_hub.py +++ b/src/spectrumdevice/devices/abstract_device/abstract_spectrum_hub.py @@ -12,13 +12,13 @@ from numpy import arange -from spectrum_gmbh.regs import SPC_SYNC_ENABLEMASK +from spectrum_gmbh.py_header.regs import SPC_SYNC_ENABLEMASK from spectrumdevice.devices.abstract_device.abstract_spectrum_device import AbstractSpectrumDevice -from spectrumdevice.devices.abstract_device.channel_interfaces import ( - SpectrumAnalogChannelInterface, - SpectrumIOLineInterface, +from spectrumdevice.devices.abstract_device.device_interface import ( + SpectrumDeviceInterface, + IOLineInterfaceType, + AnalogChannelInterfaceType, ) -from spectrumdevice.devices.abstract_device.device_interface import SpectrumDeviceInterface from spectrumdevice.exceptions import SpectrumSettingsMismatchError from spectrumdevice.settings import ( AdvancedCardFeature, @@ -36,7 +36,9 @@ CardType = TypeVar("CardType", bound=SpectrumDeviceInterface) -class AbstractSpectrumStarHub(AbstractSpectrumDevice, Generic[CardType], ABC): +class AbstractSpectrumStarHub( + AbstractSpectrumDevice, Generic[CardType, AnalogChannelInterfaceType, IOLineInterfaceType], ABC +): """Composite abstract class of `AbstractSpectrumCard` implementing methods common to all StarHubs. StarHubs are composites of more than one Spectrum card. Acquisition and generation from the child cards of a StarHub is synchronised, aggregating the channels of all child cards.""" @@ -229,7 +231,7 @@ def apply_channel_enabling(self) -> None: d.apply_channel_enabling() @property - def enabled_analog_channels(self) -> List[int]: + def enabled_analog_channel_nums(self) -> List[int]: """The currently enabled channel indices, indexed over the whole hub (from 0 to N-1, where N is the total number of channels available to the hub). @@ -240,7 +242,7 @@ def enabled_analog_channels(self) -> List[int]: n_channels_in_previous_card = 0 for card in self._child_cards: enabled_channels += [ - channel_num + n_channels_in_previous_card for channel_num in card.enabled_analog_channels + channel_num + n_channels_in_previous_card for channel_num in card.enabled_analog_channel_nums ] n_channels_in_previous_card = len(card.analog_channels) return enabled_channels @@ -274,26 +276,26 @@ def transfer_buffers(self) -> List[TransferBuffer]: return [card.transfer_buffers[0] for card in self._child_cards] @property - def analog_channels(self) -> Sequence[SpectrumAnalogChannelInterface]: + def analog_channels(self) -> Sequence[AnalogChannelInterfaceType]: """A tuple containing of all the channels of the child cards of the hub. See `AbstractSpectrumCard.channels` for more information. Returns: channels (Sequence[`SpectrumChannelInterface`]): A tuple of `SpectrumDigitiserChannel` objects. """ - channels: List[SpectrumAnalogChannelInterface] = [] + channels: List[AnalogChannelInterfaceType] = [] for device in self._child_cards: channels += device.analog_channels return tuple(channels) @property - def io_lines(self) -> Sequence[SpectrumIOLineInterface]: + def io_lines(self) -> Sequence[IOLineInterfaceType]: """A tuple containing of all the Multipurpose IO Lines of the child cards of the hub. Returns: channels (Sequence[`SpectrumIOLineInterface`]): A tuple of `SpectrumIOLineInterface` objects. """ - io_lines: List[SpectrumIOLineInterface] = [] + io_lines: List[IOLineInterfaceType] = [] for device in self._child_cards: io_lines += device.io_lines return tuple(io_lines) # todo: this is probably wrong. I don't think both cards in a netbox have IO lines diff --git a/src/spectrumdevice/devices/abstract_device/channel_interfaces.py b/src/spectrumdevice/devices/abstract_device/channel_interfaces.py index 16cbfe1..9ecbd82 100644 --- a/src/spectrumdevice/devices/abstract_device/channel_interfaces.py +++ b/src/spectrumdevice/devices/abstract_device/channel_interfaces.py @@ -5,7 +5,7 @@ # Licensed under the MIT. You may obtain a copy at https://opensource.org/licenses/MIT. from abc import ABC, abstractmethod -from typing import TypeVar, Generic +from typing import TypeVar, Generic, Protocol from spectrumdevice.features.pulse_generator.interfaces import PulseGeneratorInterface from spectrumdevice.settings import ( @@ -45,11 +45,24 @@ def read_parent_device_register( raise NotImplementedError() -class SpectrumAnalogChannelInterface(SpectrumChannelInterface[SpectrumAnalogChannelName], ABC): +class GettableSettingsProtocol(Protocol): + @abstractmethod + def _get_settings_as_dict(self) -> dict: + raise NotImplementedError() + + @abstractmethod + def _set_settings_from_dict(self, settings: dict) -> None: + raise NotImplementedError() + + +class SpectrumAnalogChannelInterface( + SpectrumChannelInterface[SpectrumAnalogChannelName], GettableSettingsProtocol, ABC +): """Defines the common public interface for control of the analog channels of Digitiser and AWG devices. All properties are read-only and must be set with their respective setter methods.""" - pass + def copy_settings_from_other_channel(self, channel_to_copy: GettableSettingsProtocol) -> None: + self._set_settings_from_dict(channel_to_copy._get_settings_as_dict()) class SpectrumIOLineInterface(SpectrumChannelInterface[SpectrumIOLineName], ABC): diff --git a/src/spectrumdevice/devices/abstract_device/device_interface.py b/src/spectrumdevice/devices/abstract_device/device_interface.py index 2febe12..b57f79a 100644 --- a/src/spectrumdevice/devices/abstract_device/device_interface.py +++ b/src/spectrumdevice/devices/abstract_device/device_interface.py @@ -19,7 +19,7 @@ TriggerSource, ) from spectrumdevice.settings.card_dependent_properties import CardType - +from spectrumdevice.settings.output_channel_pairing import ChannelPair, ChannelPairingMode AnalogChannelInterfaceType = TypeVar("AnalogChannelInterfaceType", bound=SpectrumAnalogChannelInterface) IOLineInterfaceType = TypeVar("IOLineInterfaceType", bound=SpectrumIOLineInterface) @@ -91,7 +91,7 @@ def io_lines(self) -> Sequence[IOLineInterfaceType]: @property @abstractmethod - def enabled_analog_channels(self) -> List[int]: + def enabled_analog_channel_nums(self) -> List[int]: raise NotImplementedError() @abstractmethod @@ -170,6 +170,10 @@ def available_io_modes(self) -> AvailableIOModes: def feature_list(self) -> List[Tuple[List[CardFeature], List[AdvancedCardFeature]]]: raise NotImplementedError() + @abstractmethod + def configure_channel_pairing(self, channel_pair: ChannelPair, mode: ChannelPairingMode) -> None: + raise NotImplementedError() + @abstractmethod def write_to_spectrum_device_register( self, @@ -197,7 +201,7 @@ def set_timeout_in_ms(self, timeout_in_ms: int) -> None: raise NotImplementedError() @abstractmethod - def force_trigger_event(self) -> None: + def force_trigger(self) -> None: raise NotImplementedError() @property diff --git a/src/spectrumdevice/devices/awg/abstract_spectrum_awg.py b/src/spectrumdevice/devices/awg/abstract_spectrum_awg.py index 9b2b826..1755d74 100644 --- a/src/spectrumdevice/devices/awg/abstract_spectrum_awg.py +++ b/src/spectrumdevice/devices/awg/abstract_spectrum_awg.py @@ -1,21 +1,56 @@ from abc import ABC -from copy import copy -from typing import cast +from typing import Optional -from spectrum_gmbh.regs import SPC_CARDMODE, SPC_LOOPS +from spectrum_gmbh.py_header.regs import M2CMD_CARD_WRITESETUP, SPC_CARDMODE, SPC_LOOPS, SPC_M2CMD from spectrumdevice.devices.abstract_device import AbstractSpectrumDevice -from spectrumdevice.devices.awg.awg_channel import SpectrumAWGAnalogChannel -from spectrumdevice.devices.awg.awg_interface import SpectrumAWGInterface -from spectrumdevice.settings.device_modes import GenerationMode -from spectrumdevice.settings.output_channel_pairing import ( - ChannelPair, - ChannelPairingMode, - DIFFERENTIAL_CHANNEL_PAIR_COMMANDS, - DOUBLING_CHANNEL_PAIR_COMMANDS, +from spectrumdevice.devices.awg.awg_interface import ( + SpectrumAWGAnalogChannelInterface, + SpectrumAWGIOLineInterface, + SpectrumAWGInterface, ) +from spectrumdevice.settings import GenerationSettings +from spectrumdevice.settings.device_modes import GenerationMode + +class AbstractSpectrumAWG( + AbstractSpectrumDevice[SpectrumAWGAnalogChannelInterface, SpectrumAWGIOLineInterface], SpectrumAWGInterface, ABC +): + def configure_generation(self, generation_settings: GenerationSettings) -> None: + """Apply all the settings contained in an `GenerationSettings` dataclass to the device. + + Args: + generation_settings (`GenerationSettings`): A `GenerationSettings` dataclass containing the setting values + to apply. + """ + self.set_generation_mode(generation_settings.generation_mode) + self.set_sample_rate_in_hz(generation_settings.sample_rate_in_hz) + self.transfer_waveform(generation_settings.waveform) + self.set_num_loops(generation_settings.num_loops) + self.set_enabled_analog_channels(generation_settings.enabled_channels) + if generation_settings.custom_stop_levels is None: + custom_stop_levels: list[Optional[int]] = [None] * len(self.enabled_analog_channel_nums) + else: + custom_stop_levels = generation_settings.custom_stop_levels + for channel_num, amp, dc, filt, stop_mode, stop_level in zip( + self.enabled_analog_channel_nums, + generation_settings.signal_amplitudes_in_mv, + generation_settings.dc_offsets_in_mv, + generation_settings.output_filters, + generation_settings.stop_level_modes, + custom_stop_levels, + ): + channel = self.analog_channels[channel_num] + channel.set_signal_amplitude_in_mv(amp) + channel.set_dc_offset_in_mv(dc) + channel.set_output_filter(filt) + channel.set_stop_level_mode(stop_mode) + if stop_level is not None: + channel.set_custom_stop_level(stop_level) + channel.set_is_switched_on(True) + + # Write the configuration to the card + self.write_to_spectrum_device_register(SPC_M2CMD, M2CMD_CARD_WRITESETUP) -class AbstractSpectrumAWG(AbstractSpectrumDevice, SpectrumAWGInterface, ABC): @property def generation_mode(self) -> GenerationMode: """Change the currently enabled card mode. See `GenerationMode` and the Spectrum documentation @@ -31,48 +66,3 @@ def num_loops(self) -> int: def set_num_loops(self, num_loops: int) -> None: self.write_to_spectrum_device_register(SPC_LOOPS, num_loops) - - def configure_channel_pairing(self, channel_pair: ChannelPair, mode: ChannelPairingMode) -> None: - """Configures a pair of consecutive channels to operate either independently, in differential mode or - in double mode. If enabling differential or double mode, then the odd-numbered channel will be automatically - configured to be identical to the even-numbered channel, and the odd-numbered channel will be disabled as is - required by the Spectrum API. - - Args: - channel_pair (ChannelPair): The pair of channels to configure - mode (ChannelPairingMode): The mode to enable: SINGLE, DOUBLE, or DIFFERENTIAL - """ - - doubling_enabled = int(mode == ChannelPairingMode.DOUBLE) - differential_mode_enabled = int(mode == ChannelPairingMode.DIFFERENTIAL) - - if doubling_enabled and channel_pair in (channel_pair.CHANNEL_4_AND_5, channel_pair.CHANNEL_6_AND_7): - raise ValueError("Doubling can only be enabled for channel pairs CHANNEL_0_AND_1 or CHANNEL_2_AND_3.") - - if doubling_enabled or differential_mode_enabled: - self._mirror_even_channel_settings_on_odd_channel(channel_pair) - self._disable_odd_channel(channel_pair) - - self.write_to_spectrum_device_register( - DIFFERENTIAL_CHANNEL_PAIR_COMMANDS[channel_pair], differential_mode_enabled - ) - self.write_to_spectrum_device_register(DOUBLING_CHANNEL_PAIR_COMMANDS[channel_pair], doubling_enabled) - - def _disable_odd_channel(self, channel_pair: ChannelPair) -> None: - try: - enabled_channels = copy(self.enabled_analog_channels) - enabled_channels.remove(channel_pair.value + 1) - self.set_enabled_analog_channels(enabled_channels) - except ValueError: - pass # odd numbered channel was not enable, so no need to disable it. - - def _mirror_even_channel_settings_on_odd_channel(self, channel_pair: ChannelPair) -> None: - cast(SpectrumAWGAnalogChannel, self.analog_channels[channel_pair.value + 1]).set_signal_amplitude_in_mv( - cast(SpectrumAWGAnalogChannel, self.analog_channels[channel_pair.value]).signal_amplitude_in_mv - ) - cast(SpectrumAWGAnalogChannel, self.analog_channels[channel_pair.value + 1]).set_dc_offset_in_mv( - cast(SpectrumAWGAnalogChannel, self.analog_channels[channel_pair.value]).dc_offset_in_mv - ) - cast(SpectrumAWGAnalogChannel, self.analog_channels[channel_pair.value + 1]).set_output_filter( - cast(SpectrumAWGAnalogChannel, self.analog_channels[channel_pair.value]).output_filter - ) diff --git a/src/spectrumdevice/devices/awg/awg_card.py b/src/spectrumdevice/devices/awg/awg_card.py index 7fdfaa1..6cedb9f 100644 --- a/src/spectrumdevice/devices/awg/awg_card.py +++ b/src/spectrumdevice/devices/awg/awg_card.py @@ -1,10 +1,16 @@ import logging from typing import Optional, Sequence -from numpy import int16 +from numpy import int16, concatenate, zeros from numpy.typing import NDArray -from spectrum_gmbh.regs import SPC_MIINST_CHPERMODULE, SPC_MIINST_MODULES, TYP_SERIESMASK, TYP_M2PEXPSERIES, SPC_MEMSIZE +from spectrum_gmbh.py_header.regs import ( + SPC_MIINST_CHPERMODULE, + SPC_MIINST_MODULES, + TYP_SERIESMASK, + TYP_M2PEXPSERIES, + SPC_MEMSIZE, +) from spectrumdevice.devices.abstract_device import AbstractSpectrumCard from spectrumdevice.devices.awg.abstract_spectrum_awg import AbstractSpectrumAWG from spectrumdevice.devices.awg.awg_channel import SpectrumAWGAnalogChannel, SpectrumAWGIOLine @@ -37,16 +43,8 @@ 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: - buffer = transfer_buffer_factory( - buffer_type=BufferType.SPCM_BUF_DATA, - direction=BufferDirection.SPCM_DIR_PCTOCARD, - size_in_samples=len(waveform), - bytes_per_sample=self.bytes_per_sample, - ) if len(waveform) < 16: raise ValueError("Waveform must be at least 16 samples long") - buffer.data_array[:] = waveform - self.define_transfer_buffer((buffer,)) step_size = get_memsize_step_size(self._model_number) remainder = len(waveform) % step_size if remainder > 0: @@ -55,6 +53,15 @@ def transfer_waveform(self, waveform: NDArray[int16]) -> None: "zero-padded to the next multiple of 8." ) coerced_mem_size = len(waveform) if remainder == 0 else len(waveform) + (step_size - remainder) + + buffer = transfer_buffer_factory( + buffer_type=BufferType.SPCM_BUF_DATA, + direction=BufferDirection.SPCM_DIR_PCTOCARD, + size_in_samples=coerced_mem_size, + bytes_per_sample=self.bytes_per_sample, + ) + buffer.data_array[:] = concatenate([waveform, zeros(coerced_mem_size - len(waveform), dtype=int16)]) + self.define_transfer_buffer((buffer,)) self.write_to_spectrum_device_register(SPC_MEMSIZE, coerced_mem_size) self.start_transfer() self.wait_for_transfer_chunk_to_complete() diff --git a/src/spectrumdevice/devices/awg/awg_channel.py b/src/spectrumdevice/devices/awg/awg_channel.py index 6e4b649..5a37643 100644 --- a/src/spectrumdevice/devices/awg/awg_channel.py +++ b/src/spectrumdevice/devices/awg/awg_channel.py @@ -15,7 +15,7 @@ from spectrumdevice.settings.card_dependent_properties import CardType, OUTPUT_AMPLITUDE_LIMITS_IN_MV from spectrumdevice.settings.channel import ( OUTPUT_AMPLITUDE_COMMANDS, - OUTPUT_CHANNEL_ON_OFF_COMMANDS, + OUTPUT_CHANNEL_ENABLED_COMMANDS, OUTPUT_DC_OFFSET_COMMANDS, OUTPUT_FILTER_COMMANDS, OUTPUT_STOP_LEVEL_CUSTOM_VALUE_COMMANDS, @@ -60,15 +60,31 @@ def __init__(self, parent_device: SpectrumAWGInterface, **kwargs: Any) -> None: raise SpectrumCardIsNotAnAWG(parent_device.type) super().__init__(parent_device=parent_device, **kwargs) # pass unused args up the inheritance hierarchy + def _get_settings_as_dict(self) -> dict: + return { + SpectrumAWGAnalogChannel.signal_amplitude_in_mv.__name__: self.signal_amplitude_in_mv, + SpectrumAWGAnalogChannel.dc_offset_in_mv.__name__: self.dc_offset_in_mv, + SpectrumAWGAnalogChannel.output_filter.__name__: self.output_filter, + SpectrumAWGAnalogChannel.stop_level_mode.__name__: self.stop_level_mode, + SpectrumAWGAnalogChannel.stop_level_custom_value.__name__: self.stop_level_custom_value, + } + + def _set_settings_from_dict(self, settings: dict) -> None: + self.set_signal_amplitude_in_mv(settings[SpectrumAWGAnalogChannel.signal_amplitude_in_mv.__name__]) + self.set_dc_offset_in_mv(settings[SpectrumAWGAnalogChannel.dc_offset_in_mv.__name__]) + self.set_output_filter(settings[SpectrumAWGAnalogChannel.output_filter.__name__]) + self.set_stop_level_mode(settings[SpectrumAWGAnalogChannel.stop_level_mode.__name__]) + self.set_stop_level_custom_value(settings[SpectrumAWGAnalogChannel.stop_level_custom_value.__name__]) + @property def is_switched_on(self) -> bool: """Returns "True" if the output channel is switched on, or "False" if it is muted.""" - return bool(self._parent_device.read_spectrum_device_register(OUTPUT_CHANNEL_ON_OFF_COMMANDS[self._number])) + return bool(self._parent_device.read_spectrum_device_register(OUTPUT_CHANNEL_ENABLED_COMMANDS[self._number])) - def set_is_switched_on(self, is_switched_on: bool) -> None: + def set_is_switched_on(self, is_enabled: bool) -> None: """Switches the output channel on ("True") or off ("False").""" self._parent_device.write_to_spectrum_device_register( - OUTPUT_CHANNEL_ON_OFF_COMMANDS[self._number], int(is_switched_on) + OUTPUT_CHANNEL_ENABLED_COMMANDS[self._number], int(is_enabled) ) @property diff --git a/src/spectrumdevice/devices/awg/awg_interface.py b/src/spectrumdevice/devices/awg/awg_interface.py index d63b938..4e61734 100644 --- a/src/spectrumdevice/devices/awg/awg_interface.py +++ b/src/spectrumdevice/devices/awg/awg_interface.py @@ -8,10 +8,10 @@ SpectrumAnalogChannelInterface, SpectrumIOLineInterface, ) +from spectrumdevice.settings import GenerationSettings from spectrumdevice.settings.channel import OutputChannelFilter, OutputChannelStopLevelMode from spectrumdevice.settings.device_modes import GenerationMode from spectrumdevice.settings.io_lines import DigOutIOLineModeSettings -from spectrumdevice.settings.output_channel_pairing import ChannelPair, ChannelPairingMode class SpectrumAWGIOLineInterface(SpectrumIOLineInterface, ABC): @@ -101,10 +101,6 @@ def generation_mode(self) -> GenerationMode: def set_generation_mode(self, mode: GenerationMode) -> None: raise NotImplementedError() - @abstractmethod - def configure_channel_pairing(self, channel_pair: ChannelPair, mode: ChannelPairingMode) -> None: - raise NotImplementedError() - @abstractmethod def transfer_waveform(self, waveform: NDArray[int16]) -> None: raise NotImplementedError() @@ -117,3 +113,7 @@ def num_loops(self) -> int: @abstractmethod def set_num_loops(self, num_loops: int) -> None: raise NotImplementedError() + + @abstractmethod + def configure_generation(self, generation_settings: GenerationSettings) -> None: + raise NotImplementedError() diff --git a/src/spectrumdevice/devices/awg/synthesis.py b/src/spectrumdevice/devices/awg/synthesis.py index 7be57e7..0ad5bc9 100644 --- a/src/spectrumdevice/devices/awg/synthesis.py +++ b/src/spectrumdevice/devices/awg/synthesis.py @@ -1,14 +1,24 @@ -from numpy import float_, iinfo, issubdtype, signedinteger, pi, sin, linspace, int_ +from numpy import float_, iinfo, issubdtype, signedinteger, pi, sin, linspace, int_, ones from numpy.typing import NDArray def make_full_scale_sine_waveform( - frequency_in_hz: float, sample_rate_hz: int, num_cycles: float, dtype: type + frequency_in_hz: float, sample_rate_in_hz: int, num_cycles: float, dtype: type ) -> tuple[NDArray[float_], NDArray[int_]]: if not issubdtype(dtype, signedinteger): raise ValueError("dtype must be a signed integer type") - full_scale_max_value = iinfo(dtype).max duration = num_cycles / frequency_in_hz - t = linspace(0, duration, int(duration * sample_rate_hz + 1)) + t = linspace(0, duration, int(duration * sample_rate_in_hz + 1)) return t, (sin(2 * pi * frequency_in_hz * t) * full_scale_max_value).astype(dtype) + + +def make_full_scale_rect_waveform( + sample_rate_in_hz: int, duration_in_seconds: float, dtype: type +) -> tuple[NDArray[float_], NDArray[int_]]: + 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) + full_scale_max_value = iinfo(dtype).max + t = linspace(0, duration_in_samples - 1 / sample_rate_in_hz, duration_in_samples) + return t, (ones(duration_in_samples) * full_scale_max_value).astype(dtype) diff --git a/src/spectrumdevice/devices/digitiser/abstract_spectrum_digitiser.py b/src/spectrumdevice/devices/digitiser/abstract_spectrum_digitiser.py index eaebea7..3e445ae 100644 --- a/src/spectrumdevice/devices/digitiser/abstract_spectrum_digitiser.py +++ b/src/spectrumdevice/devices/digitiser/abstract_spectrum_digitiser.py @@ -5,18 +5,25 @@ # Licensed under the MIT. You may obtain a copy at https://opensource.org/licenses/MIT. from abc import ABC -from typing import List, cast +from typing import List from spectrumdevice.measurement import Measurement from spectrumdevice.devices.abstract_device import AbstractSpectrumDevice -from spectrumdevice.devices.digitiser.digitiser_interface import SpectrumDigitiserInterface -from spectrumdevice.devices.digitiser.digitiser_channel import SpectrumDigitiserAnalogChannel +from spectrumdevice.devices.digitiser.digitiser_interface import ( + SpectrumDigitiserAnalogChannelInterface, + SpectrumDigitiserIOLineInterface, + SpectrumDigitiserInterface, +) from spectrumdevice.exceptions import SpectrumWrongAcquisitionMode from spectrumdevice.settings import AcquisitionMode, AcquisitionSettings -from spectrum_gmbh.regs import M2CMD_CARD_WRITESETUP, SPC_M2CMD +from spectrum_gmbh.py_header.regs import M2CMD_CARD_WRITESETUP, SPC_M2CMD -class AbstractSpectrumDigitiser(AbstractSpectrumDevice, SpectrumDigitiserInterface, ABC): +class AbstractSpectrumDigitiser( + AbstractSpectrumDevice[SpectrumDigitiserAnalogChannelInterface, SpectrumDigitiserIOLineInterface], + SpectrumDigitiserInterface, + ABC, +): """Abstract superclass which implements methods common to all Spectrum digitiser devices. Instances of this class cannot be constructed directly. Instead, construct instances of the concrete classes (`SpectrumDigitiserCard`, `SpectrumDigitiserStarHub` or their mock equivalents) which inherit the methods defined here. Note that @@ -42,25 +49,26 @@ def configure_acquisition(self, settings: AcquisitionSettings) -> None: self.set_enabled_analog_channels(settings.enabled_channels) # Apply channel dependent settings - for channel, v_range, v_offset, impedance in zip( - self.analog_channels, + for channel_num, v_range, v_offset, impedance in zip( + self.enabled_analog_channel_nums, settings.vertical_ranges_in_mv, settings.vertical_offsets_in_percent, settings.input_impedances, ): - cast(SpectrumDigitiserAnalogChannel, channel).set_vertical_range_in_mv(v_range) - cast(SpectrumDigitiserAnalogChannel, channel).set_vertical_offset_in_percent(v_offset) - cast(SpectrumDigitiserAnalogChannel, channel).set_input_impedance(impedance) + channel = self.analog_channels[channel_num] + channel.set_vertical_range_in_mv(v_range) + channel.set_vertical_offset_in_percent(v_offset) + channel.set_input_impedance(impedance) # Only some hardware has software programmable input coupling, so coupling can be None if settings.input_couplings is not None: for channel, coupling in zip(self.analog_channels, settings.input_couplings): - cast(SpectrumDigitiserAnalogChannel, channel).set_input_coupling(coupling) + channel.set_input_coupling(coupling) # Only some hardware has software programmable input paths, so it can be None if settings.input_paths is not None: for channel, path in zip(self.analog_channels, settings.input_paths): - cast(SpectrumDigitiserAnalogChannel, channel).set_input_path(path) + channel.set_input_path(path) # Write the configuration to the card self.write_to_spectrum_device_register(SPC_M2CMD, M2CMD_CARD_WRITESETUP) diff --git a/src/spectrumdevice/devices/digitiser/digitiser_card.py b/src/spectrumdevice/devices/digitiser/digitiser_card.py index 298e62e..d6230d6 100644 --- a/src/spectrumdevice/devices/digitiser/digitiser_card.py +++ b/src/spectrumdevice/devices/digitiser/digitiser_card.py @@ -46,7 +46,7 @@ BufferType, create_samples_acquisition_transfer_buffer, set_transfer_buffer, - NOTIFY_SIZE_PAGE_SIZE_IN_BYTES, + PAGE_SIZE_IN_BYTES, DEFAULT_NOTIFY_SIZE_IN_PAGES, ) @@ -129,7 +129,7 @@ def get_waveforms(self) -> List[List[NDArray[float_]]]: raise SpectrumNoTransferBufferDefined("Cannot find a samples transfer buffer") num_read_bytes = 0 - num_samples_per_frame = self.acquisition_length_in_samples * len(self.enabled_analog_channels) + num_samples_per_frame = self.acquisition_length_in_samples * len(self.enabled_analog_channel_nums) num_expected_bytes_per_frame = num_samples_per_frame * self._transfer_buffer.data_array.itemsize raw_samples = zeros(num_samples_per_frame * self._batch_size, dtype=self._transfer_buffer.data_array.dtype) @@ -164,7 +164,7 @@ def get_waveforms(self) -> List[List[NDArray[float_]]]: num_read_bytes += num_available_bytes waveforms_in_columns = raw_samples.reshape( - (self._batch_size, self.acquisition_length_in_samples, len(self.enabled_analog_channels)) + (self._batch_size, self.acquisition_length_in_samples, len(self.enabled_analog_channel_nums)) ) repeat_acquisitions = [] @@ -174,7 +174,7 @@ def get_waveforms(self) -> List[List[NDArray[float_]]]: cast( SpectrumDigitiserAnalogChannel, self.analog_channels[ch_num] ).convert_raw_waveform_to_voltage_waveform(squeeze(waveform)) - for ch_num, waveform in zip(self.enabled_analog_channels, waveforms_in_columns[n, :, :].T) + for ch_num, waveform in zip(self.enabled_analog_channel_nums, waveforms_in_columns[n, :, :].T) ] ) @@ -302,9 +302,9 @@ def _set_or_update_transfer_buffer_attribute(self, buffer: Optional[Sequence[Tra elif self._transfer_buffer is None: if self.acquisition_mode in (AcquisitionMode.SPC_REC_FIFO_MULTI, AcquisitionMode.SPC_REC_FIFO_AVERAGE): samples_per_batch = ( - self.acquisition_length_in_samples * len(self.enabled_analog_channels) * self._batch_size + self.acquisition_length_in_samples * len(self.enabled_analog_channel_nums) * self._batch_size ) - pages_per_batch = samples_per_batch * self.bytes_per_sample / NOTIFY_SIZE_PAGE_SIZE_IN_BYTES + pages_per_batch = samples_per_batch * self.bytes_per_sample / PAGE_SIZE_IN_BYTES if pages_per_batch < DEFAULT_NOTIFY_SIZE_IN_PAGES: notify_size = pages_per_batch @@ -319,7 +319,7 @@ def _set_or_update_transfer_buffer_attribute(self, buffer: Optional[Sequence[Tra ) elif self.acquisition_mode in (AcquisitionMode.SPC_REC_STD_SINGLE, AcquisitionMode.SPC_REC_STD_AVERAGE): self._transfer_buffer = create_samples_acquisition_transfer_buffer( - size_in_samples=self.acquisition_length_in_samples * len(self.enabled_analog_channels), + size_in_samples=self.acquisition_length_in_samples * len(self.enabled_analog_channel_nums), notify_size_in_pages=0, bytes_per_sample=self.bytes_per_sample, ) diff --git a/src/spectrumdevice/devices/digitiser/digitiser_channel.py b/src/spectrumdevice/devices/digitiser/digitiser_channel.py index 4ecb8cb..7cd5e57 100644 --- a/src/spectrumdevice/devices/digitiser/digitiser_channel.py +++ b/src/spectrumdevice/devices/digitiser/digitiser_channel.py @@ -59,6 +59,24 @@ def __init__(self, channel_number: int, parent_device: SpectrumDigitiserInterfac self._vertical_range_mv = self.vertical_range_in_mv self._vertical_offset_in_percent = self.vertical_offset_in_percent + def _get_settings_as_dict(self) -> dict: + return { + SpectrumDigitiserAnalogChannel.input_path.__name__: self.input_path, + SpectrumDigitiserAnalogChannel.input_coupling.__name__: self.input_coupling, + SpectrumDigitiserAnalogChannel.input_impedance.__name__: self.input_impedance, + SpectrumDigitiserAnalogChannel.vertical_range_in_mv.__name__: self.vertical_range_in_mv, + SpectrumDigitiserAnalogChannel.vertical_offset_in_percent.__name__: self.vertical_offset_in_percent, + } + + def _set_settings_from_dict(self, settings: dict) -> None: + self.set_input_path(settings[SpectrumDigitiserAnalogChannel.input_path.__name__]) + self.set_input_coupling(settings[SpectrumDigitiserAnalogChannel.input_coupling.__name__]) + self.set_input_impedance(settings[SpectrumDigitiserAnalogChannel.input_impedance.__name__]) + self.set_vertical_range_in_mv(settings[SpectrumDigitiserAnalogChannel.vertical_range_in_mv.__name__]) + self.set_vertical_offset_in_percent( + settings[SpectrumDigitiserAnalogChannel.vertical_offset_in_percent.__name__] + ) + def convert_raw_waveform_to_voltage_waveform(self, raw_waveform: ndarray) -> ndarray: vertical_offset_mv = 0.01 * float(self._vertical_range_mv * self._vertical_offset_in_percent) return 1e-3 * ( diff --git a/src/spectrumdevice/devices/digitiser/digitiser_star_hub.py b/src/spectrumdevice/devices/digitiser/digitiser_star_hub.py index bf59a68..f2970a2 100644 --- a/src/spectrumdevice/devices/digitiser/digitiser_star_hub.py +++ b/src/spectrumdevice/devices/digitiser/digitiser_star_hub.py @@ -14,14 +14,21 @@ AbstractSpectrumStarHub, ) from spectrumdevice.devices.abstract_device.abstract_spectrum_hub import check_settings_constant_across_devices +from spectrumdevice.devices.digitiser import SpectrumDigitiserAnalogChannelInterface from spectrumdevice.devices.digitiser.digitiser_card import SpectrumDigitiserCard from spectrumdevice.devices.digitiser.abstract_spectrum_digitiser import AbstractSpectrumDigitiser +from spectrumdevice.devices.digitiser.digitiser_interface import SpectrumDigitiserIOLineInterface from spectrumdevice.settings import ModelNumber, TransferBuffer from spectrumdevice.settings.card_dependent_properties import CardType from spectrumdevice.settings.device_modes import AcquisitionMode -class SpectrumDigitiserStarHub(AbstractSpectrumStarHub[SpectrumDigitiserCard], AbstractSpectrumDigitiser): +class SpectrumDigitiserStarHub( + AbstractSpectrumStarHub[ + SpectrumDigitiserCard, SpectrumDigitiserAnalogChannelInterface, SpectrumDigitiserIOLineInterface + ], + AbstractSpectrumDigitiser, +): """Composite class of `SpectrumDigitiserCard` for controlling a StarHub digitiser device, for example the Spectrum NetBox. StarHub digitiser devices are composites of more than one Spectrum digitiser card. Acquisition from the child cards of a StarHub is synchronised, aggregating the channels of all child cards. This class enables the @@ -184,9 +191,9 @@ def set_batch_size(self, batch_size: int) -> None: for d in self._child_cards: d.set_batch_size(batch_size) - def force_trigger_event(self) -> None: + def force_trigger(self) -> None: for d in self._child_cards: - d.force_trigger_event() + d.force_trigger() @property def type(self) -> CardType: @@ -195,3 +202,14 @@ def type(self) -> CardType: @property def model_number(self) -> ModelNumber: return self._child_cards[0].model_number + + @property + def analog_channels(self) -> Sequence[SpectrumDigitiserAnalogChannelInterface]: + """A tuple containing of all the channels of the child cards of the hub. See `AbstractSpectrumCard.channels` for + more information. + + Returns: + channels (Sequence[`SpectrumDigitiserAnalogChannelInterface`]): + A tuple of `SpectrumDigitiserAnalogChannelInterface` objects. + """ + return super().analog_channels diff --git a/src/spectrumdevice/devices/mocks/__init__.py b/src/spectrumdevice/devices/mocks/__init__.py index bd08046..479ce76 100644 --- a/src/spectrumdevice/devices/mocks/__init__.py +++ b/src/spectrumdevice/devices/mocks/__init__.py @@ -168,8 +168,8 @@ def __init__( model: ModelNumber, num_modules: int, num_channels_per_module: int, - card_features: Optional[list[CardFeature]], - advanced_card_features: Optional[list[AdvancedCardFeature]], + card_features: Optional[list[CardFeature]] = None, + advanced_card_features: Optional[list[AdvancedCardFeature]] = None, ) -> None: """ Args: diff --git a/src/spectrumdevice/devices/mocks/mock_abstract_devices.py b/src/spectrumdevice/devices/mocks/mock_abstract_devices.py index b5e55bf..750a34f 100644 --- a/src/spectrumdevice/devices/mocks/mock_abstract_devices.py +++ b/src/spectrumdevice/devices/mocks/mock_abstract_devices.py @@ -336,7 +336,7 @@ def start(self) -> None: self._source_frame_rate_hz, amplitude, self.transfer_buffers[0].data_array, - self.acquisition_length_in_samples * len(self.enabled_analog_channels), + self.acquisition_length_in_samples * len(self.enabled_analog_channel_nums), self._buffer_lock, ), ) diff --git a/src/spectrumdevice/devices/mocks/mock_waveform_source.py b/src/spectrumdevice/devices/mocks/mock_waveform_source.py index 7c74af1..9cdac98 100644 --- a/src/spectrumdevice/devices/mocks/mock_waveform_source.py +++ b/src/spectrumdevice/devices/mocks/mock_waveform_source.py @@ -14,7 +14,7 @@ from spectrum_gmbh.regs import SPC_DATA_AVAIL_USER_LEN, SPC_DATA_AVAIL_USER_POS from spectrumdevice.settings import AcquisitionMode -from spectrumdevice.settings.transfer_buffer import NOTIFY_SIZE_PAGE_SIZE_IN_BYTES +from spectrumdevice.settings.transfer_buffer import PAGE_SIZE_IN_BYTES TRANSFER_CHUNK_COUNTER = -1 # this is a custom key used in the _para_dict to count the number of transfers @@ -104,7 +104,7 @@ def __call__( """ bytes_per_sample = transfer_buffer_data_array.itemsize - notify_size_in_samples = int(self._notify_size_in_pages * NOTIFY_SIZE_PAGE_SIZE_IN_BYTES / bytes_per_sample) + notify_size_in_samples = int(self._notify_size_in_pages * PAGE_SIZE_IN_BYTES / bytes_per_sample) notify_size_in_samples = min((samples_per_frame, notify_size_in_samples)) samples_per_second = frame_rate * samples_per_frame notify_sizes_per_second = samples_per_second / notify_size_in_samples diff --git a/src/spectrumdevice/settings/__init__.py b/src/spectrumdevice/settings/__init__.py index 54d958d..6d1a507 100644 --- a/src/spectrumdevice/settings/__init__.py +++ b/src/spectrumdevice/settings/__init__.py @@ -9,16 +9,33 @@ from enum import Enum from typing import List, Optional +from numpy import int16 +from numpy.typing import NDArray + from spectrumdevice.settings.card_dependent_properties import ModelNumber from spectrumdevice.settings.card_features import CardFeature, AdvancedCardFeature -from spectrumdevice.settings.channel import InputImpedance, InputCoupling, InputPath -from spectrumdevice.settings.device_modes import AcquisitionMode, ClockMode +from spectrumdevice.settings.channel import ( + InputImpedance, + InputCoupling, + InputPath, + OutputChannelFilter, + OutputChannelStopLevelMode, +) +from spectrumdevice.settings.device_modes import AcquisitionMode, ClockMode, GenerationMode from spectrumdevice.settings.io_lines import IOLineMode, AvailableIOModes from spectrumdevice.settings.transfer_buffer import ( TransferBuffer, ) from spectrumdevice.settings.triggering import TriggerSource, ExternalTriggerMode from spectrumdevice.settings.status import CARD_STATUS_TYPE, DEVICE_STATUS_TYPE, StatusCode +from spectrumdevice.settings.pulse_generator import ( + PulseGeneratorTriggerSettings, + PulseGeneratorTriggerMode, + PulseGeneratorTriggerDetectionMode, + PulseGeneratorMultiplexer1TriggerSource, + PulseGeneratorMultiplexer2TriggerSource, + PulseGeneratorOutputSettings, +) __all__ = [ @@ -31,8 +48,6 @@ "IOLineMode", "AvailableIOModes", "TransferBuffer", - "CardToPCDataTransferBuffer", - "PCToCardDataTransferBuffer", "TriggerSource", "ExternalTriggerMode", "CARD_STATUS_TYPE", @@ -40,6 +55,16 @@ "StatusCode", "SpectrumRegisterLength", "ModelNumber", + "GenerationSettings", + "OutputChannelFilter", + "OutputChannelStopLevelMode", + "GenerationMode", + "PulseGeneratorTriggerSettings", + "PulseGeneratorTriggerMode", + "PulseGeneratorTriggerDetectionMode", + "PulseGeneratorMultiplexer1TriggerSource", + "PulseGeneratorMultiplexer2TriggerSource", + "PulseGeneratorOutputSettings", ] @@ -94,6 +119,33 @@ class AcquisitionSettings: """The input path (HF or Buffered) to apply to each channel. Only available on some hardware, so default is None.""" +@dataclass +class GenerationSettings: + """A dataclass collecting all settings required to configure signal generation. See Spectrum documentation.""" + + generation_mode: GenerationMode + """SPC_REP_STD_SINGLE , SPC_REP_STD_SINGLERESTART""" + waveform: NDArray[int16] + """The waveform to generate.""" + sample_rate_in_hz: int + """Generation rate in samples per second.""" + num_loops: int + """In SPC_REP_STD_SINGLE mode: the number of times to repeat the waveform after a trigger is received. In + SPC_REP_STD_SINGLERESTART: The number of times to wait for a trigger and generate waveform once.""" + enabled_channels: list[int] + """List of analog channel indices to enable for signal generation""" + signal_amplitudes_in_mv: list[int] + """The amplitude of each enabled channel.""" + dc_offsets_in_mv: list[int] + """The dc offset of each enabled channel.""" + output_filters: list[OutputChannelFilter] + """The output filter setting for each enabled channel.""" + stop_level_modes: list[OutputChannelStopLevelMode] + """The behavior of each enabled channel after the waveform ends.""" + custom_stop_levels: Optional[list[Optional[int]]] = None + """The stop level each channel will use it stop level mode is set to custom.""" + + class SpectrumRegisterLength(Enum): """Enum defining the possible lengths of a spectrum register.""" diff --git a/src/spectrumdevice/settings/channel.py b/src/spectrumdevice/settings/channel.py index d734092..e8672f4 100644 --- a/src/spectrumdevice/settings/channel.py +++ b/src/spectrumdevice/settings/channel.py @@ -182,7 +182,7 @@ OUTPUT_AMPLITUDE_COMMANDS = VERTICAL_RANGE_COMMANDS OUTPUT_DC_OFFSET_COMMANDS = VERTICAL_OFFSET_COMMANDS -OUTPUT_CHANNEL_ON_OFF_COMMANDS = ( +OUTPUT_CHANNEL_ENABLED_COMMANDS = ( SPC_ENABLEOUT0, SPC_ENABLEOUT1, SPC_ENABLEOUT2, diff --git a/src/spectrumdevice/settings/transfer_buffer.py b/src/spectrumdevice/settings/transfer_buffer.py index d8aa1d4..d5982b8 100644 --- a/src/spectrumdevice/settings/transfer_buffer.py +++ b/src/spectrumdevice/settings/transfer_buffer.py @@ -130,8 +130,8 @@ def __init__(self, direction: BufferDirection, board_memory_offset_bytes: int) - BufferType.SPCM_BUF_TIMESTAMP, direction, board_memory_offset_bytes, - zeros(NOTIFY_SIZE_PAGE_SIZE_IN_BYTES, dtype=uint8), - NOTIFY_SIZE_PAGE_SIZE_IN_BYTES, + zeros(PAGE_SIZE_IN_BYTES, dtype=uint8), + PAGE_SIZE_IN_BYTES, ) def read_chunk(self, chunk_position_in_bytes: int, chunk_size_in_bytes: int) -> ndarray: @@ -220,7 +220,7 @@ def set_transfer_buffer(device_handle: DEVICE_HANDLE_TYPE, buffer: TransferBuffe device_handle, buffer.type.value, buffer.direction.value, - int(buffer.notify_size_in_pages * NOTIFY_SIZE_PAGE_SIZE_IN_BYTES) + int(buffer.notify_size_in_pages * PAGE_SIZE_IN_BYTES) if buffer.direction == BufferDirection.SPCM_DIR_CARDTOPC else 0, buffer.data_array_pointer, @@ -230,5 +230,5 @@ def set_transfer_buffer(device_handle: DEVICE_HANDLE_TYPE, buffer: TransferBuffe DEFAULT_NOTIFY_SIZE_IN_PAGES = 10 -NOTIFY_SIZE_PAGE_SIZE_IN_BYTES = 4096 +PAGE_SIZE_IN_BYTES = 4096 ALLOWED_FRACTIONAL_NOTIFY_SIZES_IN_PAGES = [1 / 2, 1 / 4, 1 / 8, 1 / 16, 1 / 32, 1 / 64, 1 / 128, 1 / 256] diff --git a/src/tests/test_integration.py b/src/tests/test_integration.py index 1e803c1..4811c38 100644 --- a/src/tests/test_integration.py +++ b/src/tests/test_integration.py @@ -5,11 +5,13 @@ import pytest from numpy import array, concatenate -from example_scripts.star_hub_example import connect_to_star_hub_example -from example_scripts.continuous_averaging_fifo_mode import continuous_averaging_multi_fifo_example -from example_scripts.continuous_multi_fifo_mode import continuous_multi_fifo_example -from example_scripts.finite_multi_fifo_mode import finite_multi_fifo_example -from example_scripts.standard_single_mode import standard_single_mode_example +from example_scripts.awg_standard_single_restart_mode_example import awg_single_restart_mode_example +from example_scripts.digitiser_star_hub_example_example import connect_to_star_hub_example +from example_scripts.digitiser_continuous_averaging_fifo_mode_example import continuous_averaging_multi_fifo_example +from example_scripts.digitiser_continuous_multi_fifo_mode_example import continuous_multi_fifo_example +from example_scripts.digitiser_finite_multi_fifo_mode_example import finite_multi_fifo_example +from example_scripts.digitiser_standard_single_mode_example import digitiser_standard_single_mode_example +from example_scripts.pulse_generator_example import pulse_generator_example from spectrumdevice.measurement import Measurement from spectrumdevice.exceptions import SpectrumDriversNotFound from tests.configuration import ( @@ -24,17 +26,19 @@ SpectrumTestMode, TEST_DIGITISER_IP, TEST_DIGITISER_NUMBER, + SINGLE_AWG_CARD_TEST_MODE, ) @pytest.mark.integration class SingleCardIntegrationTests(TestCase): def setUp(self) -> None: - self._single_card_mock_mode = SINGLE_DIGITISER_CARD_TEST_MODE == SpectrumTestMode.MOCK_HARDWARE + self._single_digitiser_card_mock_mode = SINGLE_DIGITISER_CARD_TEST_MODE == SpectrumTestMode.MOCK_HARDWARE + self._single_awg_card_mock_mode = SINGLE_AWG_CARD_TEST_MODE == SpectrumTestMode.MOCK_HARDWARE - def test_standard_single_mode(self) -> None: - measurement = standard_single_mode_example( - mock_mode=self._single_card_mock_mode, + def test_digitiser_standard_single_mode(self) -> None: + measurement = digitiser_standard_single_mode_example( + mock_mode=self._single_digitiser_card_mock_mode, trigger_source=INTEGRATION_TEST_TRIGGER_SOURCE, device_number=TEST_DIGITISER_NUMBER, ip_address=TEST_DIGITISER_IP, @@ -42,7 +46,7 @@ def test_standard_single_mode(self) -> None: ) self.assertEqual(len(measurement.waveforms), 1) self.assertEqual([wfm.shape for wfm in measurement.waveforms], [(ACQUISITION_LENGTH,)]) - if self._single_card_mock_mode: + if self._single_digitiser_card_mock_mode: # mock waveform source generates random values covering full ADC range, which is set to += 0.2 V expected_pk_to_pk_volts = 0.4 self.assertAlmostEqual( @@ -57,9 +61,15 @@ def test_standard_single_mode(self) -> None: else: raise IOError("No timestamp available") + def test_awg_standard_single_restart_mode(self) -> None: + awg_single_restart_mode_example(self._single_awg_card_mock_mode) + + def test_awg_pulse_generator(self) -> None: + pulse_generator_example(self._single_awg_card_mock_mode) + def test_finite_multi_fifo_mode(self) -> None: measurements = finite_multi_fifo_example( - mock_mode=self._single_card_mock_mode, + mock_mode=self._single_digitiser_card_mock_mode, num_measurements=5, batch_size=5, trigger_source=INTEGRATION_TEST_TRIGGER_SOURCE, @@ -72,7 +82,7 @@ def test_finite_multi_fifo_mode(self) -> None: def test_continuous_multi_fifo_mode(self) -> None: measurements = continuous_multi_fifo_example( - mock_mode=self._single_card_mock_mode, + mock_mode=self._single_digitiser_card_mock_mode, time_to_keep_acquiring_for_in_seconds=0.5, batch_size=1, trigger_source=INTEGRATION_TEST_TRIGGER_SOURCE, @@ -84,7 +94,7 @@ def test_continuous_multi_fifo_mode(self) -> None: def test_averaging_continuous_multi_fifo_example(self) -> None: measurements = continuous_averaging_multi_fifo_example( - mock_mode=self._single_card_mock_mode, + mock_mode=self._single_digitiser_card_mock_mode, acquisition_duration_in_seconds=0.5, num_averages=2, trigger_source=INTEGRATION_TEST_TRIGGER_SOURCE, @@ -132,6 +142,6 @@ def test_star_hub(self) -> None: class NoDriversTest(TestCase): def test_fails_with_no_driver_without_mock_mode(self) -> None: with self.assertRaises(SpectrumDriversNotFound): - standard_single_mode_example( + digitiser_standard_single_mode_example( mock_mode=False, trigger_source=INTEGRATION_TEST_TRIGGER_SOURCE, device_number=TEST_DIGITISER_NUMBER ) diff --git a/src/tests/test_single_card.py b/src/tests/test_single_card.py index 95fbe49..b029240 100644 --- a/src/tests/test_single_card.py +++ b/src/tests/test_single_card.py @@ -2,7 +2,7 @@ from typing import Generic, TypeVar from unittest import TestCase -from numpy import array, iinfo, int16 +from numpy import array, iinfo, int16, zeros from numpy.testing import assert_array_equal from spectrum_gmbh.regs import SPC_CHENABLE @@ -16,6 +16,13 @@ SpectrumExternalTriggerNotEnabled, SpectrumTriggerOperationNotImplemented, ) +from spectrumdevice.settings import ( + AcquisitionSettings, + InputImpedance, + GenerationSettings, + OutputChannelFilter, + OutputChannelStopLevelMode, +) from spectrumdevice.settings.channel import SpectrumAnalogChannelName from spectrumdevice.settings.device_modes import AcquisitionMode, ClockMode, GenerationMode from spectrumdevice.settings.transfer_buffer import ( @@ -187,6 +194,44 @@ def test_transfer_buffer(self) -> None: self._device.define_transfer_buffer([buffer]) self.assertEqual(buffer, self._device.transfer_buffers[0]) + def test_configure_acquisition(self) -> None: + channel_to_enable = 1 + acquisition_settings = AcquisitionSettings( + acquisition_mode=AcquisitionMode.SPC_REC_STD_SINGLE, + sample_rate_in_hz=int(4e6), + acquisition_length_in_samples=400, + pre_trigger_length_in_samples=0, + timeout_in_ms=1000, + enabled_channels=[channel_to_enable], # enable only second channel + vertical_ranges_in_mv=[1000], + vertical_offsets_in_percent=[10], + input_impedances=[InputImpedance.FIFTY_OHM], + timestamping_enabled=False, + ) + self._device.configure_acquisition(acquisition_settings) + + expected_posttrigger_len = ( + acquisition_settings.acquisition_length_in_samples - acquisition_settings.pre_trigger_length_in_samples + ) + + self.assertEqual(acquisition_settings.acquisition_mode, self._device.acquisition_mode) + self.assertEqual(acquisition_settings.sample_rate_in_hz, self._device.sample_rate_in_hz) + self.assertEqual(acquisition_settings.acquisition_length_in_samples, self._device.acquisition_length_in_samples) + self.assertEqual(expected_posttrigger_len, self._device.post_trigger_length_in_samples) + self.assertEqual(acquisition_settings.timeout_in_ms, self._device.timeout_in_ms) + self.assertEqual(acquisition_settings.enabled_channels, self._device.enabled_analog_channel_nums) + self.assertEqual( + acquisition_settings.vertical_ranges_in_mv[0], + self._device.analog_channels[channel_to_enable].vertical_range_in_mv, + ) + self.assertEqual( + acquisition_settings.vertical_offsets_in_percent[0], + self._device.analog_channels[channel_to_enable].vertical_offset_in_percent, + ) + self.assertEqual( + acquisition_settings.input_impedances[0], self._device.analog_channels[channel_to_enable].input_impedance + ) + class AWGCardTest(SingleCardTest[SpectrumAWGInterface]): __test__ = True @@ -240,3 +285,28 @@ def test_transfer_buffer(self) -> None: ) self._device.define_transfer_buffer([buffer]) self.assertEqual(buffer, self._device.transfer_buffers[0]) + + def test_configure_generation(self) -> None: + generation_settings = GenerationSettings( + generation_mode=GenerationMode.SPC_REP_STD_SINGLERESTART, + waveform=zeros(16, dtype=int16), + sample_rate_in_hz=1000000, + num_loops=1, + enabled_channels=[0], + signal_amplitudes_in_mv=[1000], + dc_offsets_in_mv=[0], + output_filters=[OutputChannelFilter.LOW_PASS_70_MHZ], + stop_level_modes=[OutputChannelStopLevelMode.SPCM_STOPLVL_ZERO], + ) + self._device.configure_generation(generation_settings) + self.assertEqual(generation_settings.generation_mode, self._device.generation_mode) + assert_array_equal(generation_settings.waveform, self._device.transfer_buffers[0].data_array) + self.assertEqual(generation_settings.sample_rate_in_hz, self._device.sample_rate_in_hz) + self.assertEqual(generation_settings.num_loops, self._device.num_loops) + self.assertEqual(generation_settings.enabled_channels, self._device.enabled_analog_channel_nums) + self.assertEqual( + generation_settings.signal_amplitudes_in_mv[0], self._device.analog_channels[0].signal_amplitude_in_mv + ) + self.assertEqual(generation_settings.dc_offsets_in_mv[0], self._device.analog_channels[0].dc_offset_in_mv) + self.assertEqual(generation_settings.output_filters[0], self._device.analog_channels[0].output_filter) + self.assertEqual(generation_settings.stop_level_modes[0], self._device.analog_channels[0].stop_level_mode) diff --git a/src/tests/test_star_hub.py b/src/tests/test_star_hub.py index 875048d..766c85f 100644 --- a/src/tests/test_star_hub.py +++ b/src/tests/test_star_hub.py @@ -1,9 +1,10 @@ import pytest from numpy import array -from spectrum_gmbh.regs import SPC_CHENABLE +from spectrum_gmbh.py_header.regs import SPC_CHENABLE from spectrumdevice import SpectrumDigitiserAnalogChannel, SpectrumDigitiserStarHub from spectrumdevice.exceptions import SpectrumInvalidNumberOfEnabledChannels +from spectrumdevice.settings import AcquisitionSettings, InputImpedance, AcquisitionMode from spectrumdevice.settings.channel import SpectrumAnalogChannelName from spectrumdevice.settings.transfer_buffer import create_samples_acquisition_transfer_buffer from tests.configuration import ( @@ -32,6 +33,58 @@ def setUp(self) -> None: def tearDown(self) -> None: self._device.disconnect() + def test_configure_acquisition(self) -> None: + channels_to_enable = [0, 8] + acquisition_settings = AcquisitionSettings( + acquisition_mode=AcquisitionMode.SPC_REC_STD_SINGLE, + sample_rate_in_hz=int(4e6), + acquisition_length_in_samples=400, + pre_trigger_length_in_samples=0, + timeout_in_ms=1000, + enabled_channels=channels_to_enable, # enable only second channel + vertical_ranges_in_mv=[1000, 2000], + vertical_offsets_in_percent=[10, 20], + input_impedances=[InputImpedance.FIFTY_OHM, InputImpedance.ONE_MEGA_OHM], + timestamping_enabled=False, + ) + + self._device.configure_acquisition(acquisition_settings) + + expected_posttrigger_len = ( + acquisition_settings.acquisition_length_in_samples - acquisition_settings.pre_trigger_length_in_samples + ) + + self.assertEqual(acquisition_settings.acquisition_mode, self._device.acquisition_mode) + self.assertEqual(acquisition_settings.sample_rate_in_hz, self._device.sample_rate_in_hz) + self.assertEqual(acquisition_settings.acquisition_length_in_samples, self._device.acquisition_length_in_samples) + self.assertEqual(expected_posttrigger_len, self._device.post_trigger_length_in_samples) + self.assertEqual(acquisition_settings.timeout_in_ms, self._device.timeout_in_ms) + self.assertEqual(acquisition_settings.enabled_channels, self._device.enabled_analog_channel_nums) + self.assertEqual( + acquisition_settings.vertical_ranges_in_mv[0], + self._device.analog_channels[channels_to_enable[0]].vertical_range_in_mv, + ) + self.assertEqual( + acquisition_settings.vertical_offsets_in_percent[0], + self._device.analog_channels[channels_to_enable[0]].vertical_offset_in_percent, + ) + self.assertEqual( + acquisition_settings.input_impedances[0], + self._device.analog_channels[channels_to_enable[0]].input_impedance, + ) + self.assertEqual( + acquisition_settings.vertical_ranges_in_mv[1], + self._device.analog_channels[channels_to_enable[1]].vertical_range_in_mv, + ) + self.assertEqual( + acquisition_settings.vertical_offsets_in_percent[1], + self._device.analog_channels[channels_to_enable[1]].vertical_offset_in_percent, + ) + self.assertEqual( + acquisition_settings.input_impedances[1], + self._device.analog_channels[channels_to_enable[1]].input_impedance, + ) + def test_count_channels(self) -> None: channels = self._device.analog_channels self.assertEqual(len(channels), self._expected_total_num_channels)