Example #1
0
class HDAWGChannelGroup(AWG):
    """Represents a channel pair of the Zurich Instruments HDAWG as an independent AWG entity.
    It represents a set of channels that have to have(hardware enforced) the same control flow and sample rate.

    It keeps track of the AWG state and manages waveforms and programs on the hardware.
    """

    MIN_WAVEFORM_LEN = 192
    WAVEFORM_LEN_QUANTUM = 16

    def __init__(self,
                 group_idx: int,
                 group_size: int,
                 identifier: str,
                 timeout: float) -> None:
        super().__init__(identifier)
        self._device = None

        assert group_idx in range(4)
        assert group_size in (2, 4, 8)

        self._group_idx = group_idx
        self._group_size = group_size
        self.timeout = timeout

        self._awg_module = None
        self._program_manager = HDAWGProgramManager()
        self._elf_manager = None
        self._required_seqc_source = self._program_manager.to_seqc_program()
        self._uploaded_seqc_source = None
        self._current_program = None  # Currently armed program.
        self._upload_generator = ()

    def _initialize_awg_module(self):
        """Only run once"""
        if self._awg_module:
            self._awg_module.clear()
        self._awg_module = self.device.api_session.awgModule()
        self.awg_module.set('awgModule/device', self.device.serial)
        self.awg_module.set('awgModule/index', self.awg_group_index)
        self.awg_module.execute()
        self._elf_manager = ELFManager(self.awg_module)

    def disconnect_group(self):
        """Disconnect this group from device so groups of another size can be used"""
        if self._awg_module:
            self.awg_module.clear()
        self._device = None

    def connect_group(self, hdawg_device: HDAWGRepresentation):
        """"""
        self.disconnect_group()
        self._device = weakref.proxy(hdawg_device)
        assert self.device.channel_grouping.group_size() == self._group_size, f"{self.device.channel_grouping} != {self._group_size}"
        self._initialize_awg_module()
        # Seems creating AWG module sets SINGLE (single execution mode of sequence) to 0 per default.
        self.device.api_session.setInt('/{}/awgs/{:d}/single'.format(self.device.serial, self.awg_group_index), 1)

    def is_connected(self) -> bool:
        return self._device is not None

    @property
    def num_channels(self) -> int:
        """Number of channels"""
        return self._group_size

    def _channels(self, index_start=1) -> Tuple[int, ...]:
        """1 indexed channel"""
        offset = index_start + self._group_size * self._group_idx
        return tuple(ch + offset for ch in range(self._group_size))

    @property
    def num_markers(self) -> int:
        """Number of marker channels"""
        return 2 * self.num_channels

    def upload(self, name: str,
               program: Loop,
               channels: Tuple[Optional[ChannelID], ...],
               markers: Tuple[Optional[ChannelID], ...],
               voltage_transformation: Tuple[Callable, ...],
               force: bool = False) -> None:
        """Upload a program to the AWG.

        Physically uploads all waveforms required by the program - excluding those already present -
        to the device and sets up playback sequences accordingly.
        This method should be cheap for program already on the device and can therefore be used
        for syncing. Programs that are uploaded should be fast(~1 sec) to arm.

        Args:
            name: A name for the program on the AWG.
            program: The program (a sequence of instructions) to upload.
            channels: Tuple of length num_channels that ChannelIDs of  in the program to use. Position in the list
            corresponds to the AWG channel
            markers: List of channels in the program to use. Position in the List in the list corresponds to
            the AWG channel
            voltage_transformation: transformations applied to the waveforms extracted rom the program. Position
            in the list corresponds to the AWG channel
            force: If a different sequence is already present with the same name, it is
                overwritten if force is set to True. (default = False)

        Known programs are handled in host memory most of the time. Only when uploading the
        device memory is touched at all.

        Returning from setting user register in seqc can take from 50ms to 60 ms. Fluctuates heavily. Not a good way to
        have deterministic behaviour "setUserReg(PROG_SEL, PROG_IDLE);".
        """
        if len(channels) != self.num_channels:
            raise HDAWGValueError('Channel ID not specified')
        if len(markers) != self.num_markers:
            raise HDAWGValueError('Markers not specified')
        if len(voltage_transformation) != self.num_channels:
            raise HDAWGValueError('Wrong number of voltage transformations')

        if name in self.programs and not force:
            raise HDAWGValueError('{} is already known on {}'.format(name, self.identifier))

        # Go to qupulse nanoseconds time base.
        q_sample_rate = self.sample_rate / 10**9

        # Adjust program to fit criteria.
        make_compatible(program,
                        minimal_waveform_length=self.MIN_WAVEFORM_LEN,
                        waveform_quantum=self.WAVEFORM_LEN_QUANTUM,
                        sample_rate=q_sample_rate)

        if self._amplitude_offset_handling == AWGAmplitudeOffsetHandling.IGNORE_OFFSET:
            voltage_offsets = (0.,) * self.num_channels
        elif self._amplitude_offset_handling == AWGAmplitudeOffsetHandling.CONSIDER_OFFSET:
            voltage_offsets = self.offsets()
        else:
            raise ValueError('{} is invalid as AWGAmplitudeOffsetHandling'.format(self._amplitude_offset_handling))

        amplitudes = self.amplitudes()

        if name in self._program_manager.programs:
            self._program_manager.remove(name)

        self._program_manager.add_program(name,
                                          program,
                                          channels=channels,
                                          markers=markers,
                                          voltage_transformations=voltage_transformation,
                                          sample_rate=q_sample_rate,
                                          amplitudes=amplitudes,
                                          offsets=voltage_offsets)

        self._required_seqc_source = self._program_manager.to_seqc_program()
        self._program_manager.waveform_memory.sync_to_file_system(self.device.waveform_file_system)

        # start compiling the source (non-blocking)
        self._start_compile_and_upload()

    def _start_compile_and_upload(self):
        self._upload_generator = self._elf_manager.compile_and_upload(self._required_seqc_source)

    def _wait_for_compile_and_upload(self):
        for state in self._upload_generator:
            logger.debug("wait_for_compile_and_upload: %r", state)
            time.sleep(.1)
        self._uploaded_seqc_source = self._required_seqc_source
        logger.debug("AWG %d: wait_for_compile_and_upload has finished", self.awg_group_index)

    def was_current_program_finished(self) -> bool:
        """Return true if the current program has finished at least once"""
        playback_finished_mask = int(HDAWGProgramManager.Constants.PLAYBACK_FINISHED_MASK, 2)
        return bool(self.user_register(HDAWGProgramManager.Constants.PROG_SEL_REGISTER) & playback_finished_mask)

    def set_volatile_parameters(self, program_name: str, parameters: Mapping[str, ConstantParameter]):
        """Set the values of parameters which were marked as volatile on program creation."""
        new_register_values = self._program_manager.get_register_values_to_update_volatile_parameters(program_name,
                                                                                                      parameters)
        if self._current_program == program_name:
            for register, value in new_register_values.items():
                self.user_register(register, value)

    def remove(self, name: str) -> None:
        """Remove a program from the AWG.

        Also discards all waveforms referenced only by the program identified by name.

        Args:
            name: The name of the program to remove.
        """
        self._program_manager.remove(name)
        self._required_seqc_source = self._program_manager.to_seqc_program()

    def clear(self) -> None:
        """Removes all programs and waveforms from the AWG.

        Caution: This affects all programs and waveforms on the AWG, not only those uploaded using qupulse!
        """
        self._program_manager.clear()
        self._current_program = None
        self._required_seqc_source = self._program_manager.to_seqc_program()
        self._start_compile_and_upload()
        self.arm(None)

    def arm(self, name: Optional[str]) -> None:
        """Load the program 'name' and arm the device for running it. If name is None the awg will "dearm" its current
        program.

        Currently hardware triggering is not implemented. The HDAWGProgramManager needs to emit code that calls
        `waitDigTrigger` to do that.
        """
        if self._required_seqc_source != self._uploaded_seqc_source:
            self._wait_for_compile_and_upload()

        self.user_register(self._program_manager.Constants.TRIGGER_REGISTER, 0)

        if name is None:
            self.user_register(self._program_manager.Constants.PROG_SEL_REGISTER,
                               self._program_manager.Constants.PROG_SEL_NONE)
            self._current_program = None
        else:
            if name not in self.programs:
                raise HDAWGValueError('{} is unknown on {}'.format(name, self.identifier))
            self._current_program = name

            # set the registers of initial repetition counts
            for register, value in self._program_manager.get_register_values(name).items():
                assert register not in (self._program_manager.Constants.PROG_SEL_REGISTER,
                                        self._program_manager.Constants.TRIGGER_REGISTER)
                self.user_register(register, value)

            self.user_register(self._program_manager.Constants.PROG_SEL_REGISTER,
                               self._program_manager.name_to_index(name) | int(self._program_manager.Constants.NO_RESET_MASK, 2))

        # this is a workaround for problems in the past and should be re-thought in case of a re-write
        for ch_pair in self.device.channel_tuples:
            ch_pair._wait_for_compile_and_upload()
        self.enable(True)

    def run_current_program(self) -> None:
        """Run armed program."""
        if self._current_program is not None:
            if self._current_program not in self.programs:
                raise HDAWGValueError('{} is unknown on {}'.format(self._current_program, self.identifier))
            if not self.enable():
                self.enable(True)
            self.user_register(self._program_manager.Constants.TRIGGER_REGISTER,
                               int(self._program_manager.Constants.TRIGGER_RESET_MASK, 2))
        else:
            raise HDAWGRuntimeError('No program active')

    @property
    def programs(self) -> Set[str]:
        """The set of program names that can currently be executed on the hardware AWG."""
        return set(self._program_manager.programs.keys())

    @property
    def sample_rate(self) -> TimeType:
        """The default sample rate of the AWG channel group."""
        node_path = '/{}/awgs/{}/time'.format(self.device.serial, self.awg_group_index)
        sample_rate_num = self.device.api_session.getInt(node_path)
        node_path = '/{}/system/clocks/sampleclock/freq'.format(self.device.serial)
        sample_clock = self.device.api_session.getDouble(node_path)

        """Calculate exact rational number based on (sample_clock Sa/s) / 2^sample_rate_num. Otherwise numerical
        imprecision will give rise to errors for very long pulses. fractions.Fraction does not accept floating point
        numerator, which sample_clock could potentially be."""
        return time_from_float(sample_clock) / 2 ** sample_rate_num

    @property
    def awg_group_index(self) -> int:
        """AWG node group index assuming 4x2 channel grouping. Then 0...3 will give appropriate index of group."""
        return self._group_idx

    @property
    def device(self) -> HDAWGRepresentation:
        """Reference to HDAWG representation."""
        if self._device is None:
            raise HDAWGValueError('Channel group is currently not connected')
        return self._device

    @property
    def awg_module(self) -> zhinst.ziPython.AwgModule:
        """Each AWG channel group has its own awg module to manage program compilation and upload."""
        if self._awg_module is None:
            raise HDAWGValueError('Channel group is not connected and was never initialized')
        return self._awg_module

    @property
    def user_directory(self) -> str:
        """LabOne user directory with subdirectories: "awg/src" (seqc sourcefiles), "awg/elf" (compiled AWG binaries),
        "awag/waves" (user defined csv waveforms)."""
        return self.awg_module.getString('awgModule/directory')

    def enable(self, status: bool = None) -> bool:
        """Start the AWG sequencer."""
        # There is also 'awgModule/awg/enable', which seems to have the same functionality.
        node_path = '/{}/awgs/{:d}/enable'.format(self.device.serial, self.awg_group_index)
        if status is not None:
            self.device.api_session.setInt(node_path, int(status))
            self.device.api_session.sync()  # Global sync: Ensure settings have taken effect on the device.
        return bool(self.device.api_session.getInt(node_path))

    def user_register(self, reg: UserRegister, value: int = None) -> int:
        """Query user registers (1-16) and optionally set it.

        Args:
            reg: User register. If it is an int, a warning is raised and it is interpreted as a one based index
            value: Value to set

        Returns:
            User Register value after setting it
        """
        if isinstance(reg, int):
            warnings.warn("User register is not a UserRegister instance. It is interpreted as one based index.")
            reg = UserRegister(one_based_value=reg)

        if reg.to_web_interface() not in range(1, 17):
            raise HDAWGValueError('{reg:repr} not a valid (1-16) register.'.format(reg=reg))

        node_path = '/{}/awgs/{:d}/userregs/{:labone}'.format(self.device.serial, self.awg_group_index, reg)
        if value is not None:
            self.device.api_session.setInt(node_path, value)
            self.device.api_session.sync()  # Global sync: Ensure settings have taken effect on the device.
        return self.device.api_session.getInt(node_path)

    def _amplitude_scales(self) -> Tuple[float, ...]:
        """not affected by grouping"""
        return tuple(self.device.api_session.getDouble(f'/{self.device.serial}/awgs/{ch // 2:d}/outputs/{ch % 2:d}/amplitude')
                     for ch in self._channels(index_start=0))

    def amplitudes(self) -> Tuple[float, ...]:
        """Query AWG channel amplitude value (not peak to peak).

        From manual:
        The final signal amplitude is given by the product of the full scale
        output range of 1 V[in this example], the dimensionless amplitude
        scaling factor 1.0, and the actual dimensionless signal amplitude
        stored in the waveform memory."""
        amplitudes = []

        for ch, zi_amplitude in zip(self._channels(), self._amplitude_scales()):
            zi_range = self.device.range(ch)
            amplitudes.append(zi_amplitude * zi_range / 2)
        return tuple(amplitudes)

    def offsets(self) -> Tuple[float, ...]:
        return tuple(map(self.device.offset, self._channels()))
Example #2
0
class HDAWGChannelPair(AWG):
    """Represents a channel pair of the Zurich Instruments HDAWG as an independent AWG entity.
    It represents a set of channels that have to have(hardware enforced) the same:
        -control flow
        -sample rate

    It keeps track of the AWG state and manages waveforms and programs on the hardware.
    """

    MIN_WAVEFORM_LEN = 192

    def __init__(self, hdawg_device: HDAWGRepresentation, channels: Tuple[int,
                                                                          int],
                 identifier: str, timeout: float) -> None:
        super().__init__(identifier)
        self._device = weakref.proxy(hdawg_device)

        if channels not in ((1, 2), (3, 4), (5, 6), (7, 8)):
            raise HDAWGValueError('Invalid channel pair: {}'.format(channels))
        self._channels = channels
        self.timeout = timeout

        self._awg_module = self.device.api_session.awgModule()
        self.awg_module.set('awgModule/device', self.device.serial)
        self.awg_module.set('awgModule/index', self.awg_group_index)
        self.awg_module.execute()
        # Seems creating AWG module sets SINGLE (single execution mode of sequence) to 0 per default.
        self.device.api_session.setInt(
            '/{}/awgs/{:d}/single'.format(self.device.serial,
                                          self.awg_group_index), 1)

        self._program_manager = HDAWGProgramManager()
        self._required_seqc_source = ''
        self._uploaded_seqc_source = None
        self._current_program = None  # Currently armed program.

    @property
    def num_channels(self) -> int:
        """Number of channels"""
        return 2

    @property
    def num_markers(self) -> int:
        """Number of marker channels"""
        return 4

    def upload(self,
               name: str,
               program: Loop,
               channels: Tuple[Optional[ChannelID], Optional[ChannelID]],
               markers: Tuple[Optional[ChannelID], Optional[ChannelID],
                              Optional[ChannelID], Optional[ChannelID]],
               voltage_transformation: Tuple[Callable, Callable],
               force: bool = False) -> None:
        """Upload a program to the AWG.

        Physically uploads all waveforms required by the program - excluding those already present -
        to the device and sets up playback sequences accordingly.
        This method should be cheap for program already on the device and can therefore be used
        for syncing. Programs that are uploaded should be fast(~1 sec) to arm.

        Args:
            name: A name for the program on the AWG.
            program: The program (a sequence of instructions) to upload.
            channels: Tuple of length num_channels that ChannelIDs of  in the program to use. Position in the list
            corresponds to the AWG channel
            markers: List of channels in the program to use. Position in the List in the list corresponds to
            the AWG channel
            voltage_transformation: transformations applied to the waveforms extracted rom the program. Position
            in the list corresponds to the AWG channel
            force: If a different sequence is already present with the same name, it is
                overwritten if force is set to True. (default = False)

        Known programs are handled in host memory most of the time. Only when uploading the
        device memory is touched at all.

        Returning from setting user register in seqc can take from 50ms to 60 ms. Fluctuates heavily. Not a good way to
        have deterministic behaviour "setUserReg(PROG_SEL, PROG_IDLE);".
        """
        if len(channels) != self.num_channels:
            raise HDAWGValueError('Channel ID not specified')
        if len(markers) != self.num_markers:
            raise HDAWGValueError('Markers not specified')
        if len(voltage_transformation) != self.num_channels:
            raise HDAWGValueError('Wrong number of voltage transformations')

        if name in self.programs and not force:
            raise HDAWGValueError('{} is already known on {}'.format(
                name, self.identifier))

        # Go to qupulse nanoseconds time base.
        q_sample_rate = self.sample_rate / 10**9

        # Adjust program to fit criteria.
        make_compatible(
            program,
            minimal_waveform_length=self.MIN_WAVEFORM_LEN,
            waveform_quantum=
            16,  # 8 samples for single, 4 for dual channel waveforms.
            sample_rate=q_sample_rate)

        if self._amplitude_offset_handling == AWGAmplitudeOffsetHandling.IGNORE_OFFSET:
            voltage_offsets = (0., 0.)
        elif self._amplitude_offset_handling == AWGAmplitudeOffsetHandling.CONSIDER_OFFSET:
            voltage_offsets = (self._device.offset(self._channels[0]),
                               self._device.offset(self._channels[1]))
        else:
            raise ValueError(
                '{} is invalid as AWGAmplitudeOffsetHandling'.format(
                    self._amplitude_offset_handling))

        amplitudes = self._device.range(self._channels[0]), self._device.range(
            self._channels[1])

        if name in self._program_manager.programs:
            self._program_manager.remove(name)

        self._program_manager.add_program(
            name,
            program,
            channels=channels,
            markers=markers,
            voltage_transformations=voltage_transformation,
            sample_rate=q_sample_rate,
            amplitudes=amplitudes,
            offsets=voltage_offsets)

        self._required_seqc_source = self._program_manager.to_seqc_program()
        self._program_manager.waveform_memory.sync_to_file_system(
            Path(self.user_directory).joinpath('awg', 'waves'))

    def _upload_sourcestring(self, sourcestring: str) -> None:
        """Transfer AWG sequencer program as string to HDAWG and block till compilation and upload finish.
        Allows upload without access to data server file system."""
        if not sourcestring:
            raise HDAWGTypeError(
                'sourcestring must not be empty or compilation will not start.'
            )
        logger = logging.getLogger('ziHDAWG')

        # Transfer the AWG sequence program. Compilation starts automatically if sourcestring is set.
        self.awg_module.set('awgModule/compiler/sourcestring', sourcestring)
        self._poll_compile_and_upload_finished(logger)
        self._uploaded_seqc_source = sourcestring

    def _poll_compile_and_upload_finished(self,
                                          logger: logging.Logger) -> None:
        """Blocks till compilation on data server and upload to HDAWG succeed,
        if process takes less time than timeout."""
        time_start = time.time()
        logger.info('Compilation started')
        while self.awg_module.getInt('awgModule/compiler/status') == -1:
            time.sleep(0.1)
        if time.time() - time_start > self._timeout:
            raise HDAWGTimeoutError("Compilation timeout out")

        if self.awg_module.getInt('awgModule/compiler/status') == 1:
            msg = self.awg_module.getString('awgModule/compiler/statusstring')
            logger.error(msg)
            raise HDAWGCompilationException(msg)

        if self.awg_module.getInt('awgModule/compiler/status') == 0:
            logger.info('Compilation successful')
        if self.awg_module.getInt('awgModule/compiler/status') == 2:
            msg = self.awg_module.getString('awgModule/compiler/statusstring')
            logger.warning(msg)

        i = 0
        while ((self.awg_module.getDouble('awgModule/progress') < 1.0)
               and (self.awg_module.getInt('awgModule/elf/status') != 1)):
            time.sleep(0.2)
            logger.info("{} awgModule/progress: {:.2f}".format(
                i, self.awg_module.getDouble('awgModule/progress')))
            i = i + 1
            if time.time() - time_start > self._timeout:
                raise HDAWGTimeoutError("Upload timeout out")
        logger.info("{} awgModule/progress: {:.2f}".format(
            i, self.awg_module.getDouble('awgModule/progress')))

        if self.awg_module.getInt('awgModule/elf/status') == 0:
            logger.info('Upload to the instrument successful')
            logger.info('Process took {:.3f} seconds'.format(time.time() -
                                                             time_start))
        if self.awg_module.getInt('awgModule/elf/status') == 1:
            raise HDAWGUploadException()

    def remove(self, name: str) -> None:
        """Remove a program from the AWG.

        Also discards all waveforms referenced only by the program identified by name.

        Args:
            name: The name of the program to remove.
        """
        self._program_manager.remove(name)
        self._required_seqc_source = self._program_manager.to_seqc_program()

    def clear(self) -> None:
        """Removes all programs and waveforms from the AWG.

        Caution: This affects all programs and waveforms on the AWG, not only those uploaded using qupulse!
        """
        self._program_manager.clear()
        self._current_program = None
        self._required_seqc_source = ''
        self.arm(None)

    def arm(self, name: Optional[str]) -> None:
        """Load the program 'name' and arm the device for running it. If name is None the awg will "dearm" its current
        program.

        Currently hardware triggering is not implemented. The HDAWGProgramManager needs to emit code that calls
        `waitDigTrigger` to do that.
        """
        if self._required_seqc_source != self._uploaded_seqc_source:
            self._upload_sourcestring(self._required_seqc_source)

        self.user_register(
            self._program_manager.GLOBAL_CONSTS['TRIGGER_REGISTER'], 0)

        if not name:
            self.user_register(
                self._program_manager.GLOBAL_CONSTS['PROG_SEL_REGISTER'] + 1,
                self._program_manager.GLOBAL_CONSTS['PROG_SEL_NONE'])
            self._current_program = None
        else:
            if name not in self.programs:
                raise HDAWGValueError('{} is unknown on {}'.format(
                    name, self.identifier))
            self._current_program = name
            self.user_register(
                self._program_manager.GLOBAL_CONSTS['PROG_SEL_REGISTER'] + 1,
                self._program_manager.name_to_index(name)
                | int(self._program_manager.GLOBAL_CONSTS['NO_RESET_MASK'], 2))
        self.enable(True)

    def run_current_program(self) -> None:
        """Run armed program."""
        if self._current_program is not None:
            if self._current_program not in self.programs:
                raise HDAWGValueError('{} is unknown on {}'.format(
                    self._current_program, self.identifier))
            if not self.enable():
                self.enable(True)
            self.user_register(
                self._program_manager.GLOBAL_CONSTS['TRIGGER_REGISTER'] + 1,
                int(self._program_manager.GLOBAL_CONSTS['TRIGGER_RESET_MASK'],
                    2))
        else:
            raise HDAWGRuntimeError('No program active')

    @property
    def programs(self) -> Set[str]:
        """The set of program names that can currently be executed on the hardware AWG."""
        return set(self._program_manager.programs.keys())

    @property
    def sample_rate(self) -> TimeType:
        """The default sample rate of the AWG channel group."""
        node_path = '/{}/awgs/{}/time'.format(self.device.serial,
                                              self.awg_group_index)
        sample_rate_num = self.device.api_session.getInt(node_path)
        node_path = '/{}/system/clocks/sampleclock/freq'.format(
            self.device.serial)
        sample_clock = self.device.api_session.getDouble(node_path)
        """Calculate exact rational number based on (sample_clock Sa/s) / 2^sample_rate_num. Otherwise numerical
        imprecision will give rise to errors for very long pulses. fractions.Fraction does not accept floating point
        numerator, which sample_clock could potentially be."""
        return time_from_float(sample_clock) / 2**sample_rate_num

    @property
    def awg_group_index(self) -> int:
        """AWG node group index assuming 4x2 channel grouping. Then 0...3 will give appropriate index of group."""
        return self._channels[0] // 2

    @property
    def device(self) -> HDAWGRepresentation:
        """Reference to HDAWG representation."""
        return self._device

    @property
    def awg_module(self) -> zhinst.ziPython.AwgModule:
        """Each AWG channel group has its own awg module to manage program compilation and upload."""
        return self._awg_module

    @property
    def user_directory(self) -> str:
        """LabOne user directory with subdirectories: "awg/src" (seqc sourcefiles), "awg/elf" (compiled AWG binaries),
        "awag/waves" (user defined csv waveforms)."""
        return self.awg_module.getString('awgModule/directory')

    def enable(self, status: bool = None) -> bool:
        """Start the AWG sequencer."""
        # There is also 'awgModule/awg/enable', which seems to have the same functionality.
        node_path = '/{}/awgs/{:d}/enable'.format(self.device.serial,
                                                  self.awg_group_index)
        if status is not None:
            self.device.api_session.setInt(node_path, int(status))
            self.device.api_session.sync(
            )  # Global sync: Ensure settings have taken effect on the device.
        return bool(self.device.api_session.getInt(node_path))

    def user_register(self, reg: int, value: int = None) -> int:
        """Query user registers (1-16) and optionally set it."""
        if reg not in range(1, 17):
            raise HDAWGValueError(
                '{} not a valid (1-16) register.'.format(reg))
        node_path = '/{}/awgs/{:d}/userregs/{:d}'.format(
            self.device.serial, self.awg_group_index, reg - 1)
        if value is not None:
            self.device.api_session.setInt(node_path, value)
            self.device.api_session.sync(
            )  # Global sync: Ensure settings have taken effect on the device.
        return self.device.api_session.getInt(node_path)

    def amplitude(self, channel: int, value: float = None) -> float:
        """Query AWG channel amplitude value and optionally set it. Amplitude in units of full scale of the given
         AWG Output. The full scale corresponds to the Range voltage setting of the Signal Outputs."""
        if channel not in (1, 2):
            raise HDAWGValueError(
                '{} not a valid (1-2) channel.'.format(channel))
        node_path = '/{}/awgs/{:d}/outputs/{:d}/amplitude'.format(
            self.device.serial, self.awg_group_index, channel - 1)
        if value is not None:
            self.device.api_session.setDouble(node_path, value)
            self.device.api_session.sync(
            )  # Global sync: Ensure settings have taken effect on the device.
        return self.device.api_session.getDouble(node_path)