Welcome to the qoqo-qryd/roqoqo-qryd user documentation!

This software is designed to support the QRydDemo project on quantum computing with Rydberg atoms. It provides components to support QRydDemo quantum computers based on the qoqo quantum toolkit by HQS Quantum Simulations used to represent quantum circuits.

To learn more about the general approach to create quantum programs and executing them in qoqo see Introduction and Execution.

This software is split into two packages:

  • roqoqo-qryd: Implementing the core functionality and a Rust interface.
  • qoqo-qryd: The Python interface.

The packages are based on the open source qoqo quantum computing toolkit.

Interface to the current QRydDemo WebAPI

At the moment QRydDemo WebAPI allows access to Quantum Hardware Emulators of different device topologies. qoqo-qryd/roqoqo-qryd support interfacing with the corresponding REST-API with low level calls, i.e. using Circuit, as well as with high-level backend based functionalities, i.e. by using QuantumPrograms in qoqo. For more details see WebAPI.

QRydDemo specific hardware operations (prototype)

Rydberg atom based quantum devices support, in principle, operations not commonly found in other quantum hardware. Changes in device topology are one of these operations. roqoqo-qryd/qoqo-qryd adds support for changes in device topology to qoqo. For more details see QRyd Specifics. Note that this is a preview prototype and does not represent a finalized set of operations on the QRydDemo hardware.

Local simulator supporting specific hardware operations

qoqo-qryd/roqoqo-qryd include a local QuEST based simulator for quantum devices supporting the Rydberg specific quantum operations. The simulator is intended to let users test the capabilities of quantum hardware with the additional operations. For more details see QRyd Specifics. Note that the devices for the simulator do not represent a finalized design for QRydDemo.

Examples

A collection of example python programs can be found in Examples.

OpenSSL

Acknowledgments related to using OpenSSL for http requests:

This product includes software developed by the OpenSSL Project for use in the OpenSSL Toolkit (http://www.openssl.org/).

This product includes cryptographic software written by Eric Young (eay@cryptsoft.com). This product includes software written by Tim Hudson (tjh@cryptsoft.com).

Introduction

The qoqo-qryd package is designed to enable the execution of quantum algorithms implemented in qoqo on QRydDemo hardware.

Low level: quantum circuits

Qoqo is a circuit based quantum computing toolkit. Like many other quantum computing toolkits it can be used to construct quantum circuits - sequences of quantum operations that are to be executed on a quantum computer.

Examples for quantum operations are the controlled NOT (CNOT) operation on two qubits and the Hadamard gate on a single qubit.

   from qoqo import Circuit
   from qoqo import operations as ops
   circuit = Circuit()
   # Initializing first qubit in superposition of |0> and |1>
   circuit += ops.Hadamard(0)
   # Entangling qubits 0 and 1 with CNOT
   circuit += ops.CNOT(0,1)

To extract information from a quantum computer results must be measured. For measurements qoqo defines classical registers in the quantum circuits. The classical measurement results will be written to the classical registers. The definition of classical registers is similar to a variable declaration in normal programs.

The measurement of a qubit (on hardware) is always a projective measurement in the Z-basis yielding 0 or 1.

   from qoqo import Circuit
   from qoqo import operations as ops
   circuit = Circuit()
   # Defining a bit readout with name "ro" containing 2 bits
   circuit += ops.DefinitionBit("ro", 2, is_output=True)
   # Initializing first qubit in superposition of |0> and |1>
   circuit += ops.Hadamard(0)
   # Entangling qubits 0 and 1 with CNOT
   circuit += ops.CNOT(0,1)
   # Measuring all qubits and writing the results into register 'ro'
   # Repeating the circuit 100 times to create 100 projective measurements
   circuit += ops.PragmaRepeatedMeasurement("ro", 100, None)

High-level: quantum programs

On a more abstract level a quantum program can be defined as a program that can be executed on a quantum computer after receiving a list of classical parameters and returns a list of classical results.

Qoqo provides the QuantumProgram class for this purpose. A QuantumProgram is the preferred way to communicate between different programs (for example with hardware or simulators) and to save quantum programs with the qoqo toolkit.

For many applications the measurement results of several circuits need to be combined to extract the required information from a quantum state prepared by the quantum operations in a quantum circuit. The combination of the results of each quantum circuit happens in a post-processing of classical measurement.

A qoqo measurement combines one constant_circuit that is always executed first, a list of circuits that are executed after the constant circuit, and a measurement_input that encodes the classical post-processing.

As an example take the measurement of a Hamiltonian H = 0.1 * X + 0.2 * Z where X and Z are Pauli operators. We want to measure H with respect to a state |psi> = (|0> + |1>)/sqrt(2). We will use a Hadamard gate in the constant_circuit to prepare |psi>. Since we cannot measure X and Z at the same time, the circuits list will include one quantum circuit that does not apply any additional gate and one circuit that rotates the qubit basis into the X-basis so that the expectation value <X> is equivalent to the measurement of <Z> in the new basis. This kind of measurement is referred to as a PauliZProduct measurement because each qubit is rotated in the correct basis for the readout. For the post-processing the PauliZProduct measurement needs two more details to be added to the input (PauliZProductInput): Which qubits to combine into expectation values (add_pauliz_product()) and which weight to use for each result (add_linear_exp_val()).

In general one can measure the expectation values of the products of local Z operators, e.g. <Z0>, <Z1>, <Z0*Z1>, <Z0*Z3>, ... The PauliZProductInput needs to define all of these products that are measured. Here we will measure two products <Z0> after a rotation in the X basis and <Z0> without an additional rotation. The PauliZProductInput also defines the weights of the products in the final result. Here 0.1 for the first product and 0.2 for the second.

   from qoqo import Circuit
   from qoqo import operations as ops
   from qoqo.measurements import PauliZProduct, PauliZProductInput
   from qoqo import QuantumProgram
   # Initialize |psi>
   init_circuit = Circuit()
   init_circuit += ops.Hadamard(0)
   # Z-basis measurement circuit with 1000 shots
   z_circuit = Circuit()
   z_circuit += ops.DefinitionBit("ro_z", 1, is_output=True)
   z_circuit += ops.PragmaRepeatedMeasurement("ro_z", 1000, None)
   # X-basis measurement circuit with 1000 shots   
   x_circuit = Circuit()
   x_circuit += ops.DefinitionBit("ro_x", 1, is_output=True)
   # Changing to the X basis with a Hadamard gate
   x_circuit += ops.Hadamard(0)
   x_circuit += ops.PragmaRepeatedMeasurement("ro_x", 1000, None)

   # Preparing the measurement input for one qubit
   # The PauliZProductInput starts with just the number of qubits
   # and if to use a flipped measurements set.
   measurement_input = PauliZProductInput(1, False)
   # Next, pauli products are added to the PauliZProductInput
   # Read out product of Z on site 0 for register ro_z (no basis change)
   z_basis_index = measurement_input.add_pauliz_product("ro_z", [0,])
   # Read out product of Z on site 0 for register ro_x
   # (after basis change effectively a <X> measurement)
   x_basis_index = measurement_input.add_pauliz_product("ro_x", [0,])
   
   # Last, instructions on how to combine the single expectation values
   # into the total result are provided.
   # Add a result (the expectation value of H) that is a combination of
   # the PauliProduct expectation values.
   measurement_input.add_linear_exp_val("<H>", {x_basis_index: 0.1, z_basis_index: 0.2})

   measurement = PauliZProduct(
      constant_circuit=init_circuit,
      circuits=[z_circuit, x_circuit],
      input=measurement_input,
   )

For details on how to execute QuantumPrograms see Execution.

The qoqo QuantumProgram combines a measurement with a list of free parameters that are not set at compilation time but can be dynamically set whenever the QuantumProgram is run. To demonstrate this we modify the example from above to use a state |psi> with a free angle between |0> and |1>. Such a state can be prepared by a RotateX quantum operation.

   from qoqo import Circuit
   from qoqo import operations as ops
   from qoqo.measurements import PauliZProduct, PauliZProductInput
   from qoqo import QuantumProgram
   # initialize |psi>
   init_circuit = Circuit()
   # Apply a RotateY gate with a symbolic angle
   # To execute the circuit this symbolic parameter needs to be replaced 
   # by a real number with the help of a QuantumProgram
   init_circuit += ops.RotateX(0, "angle")
   # Z-basis measurement circuit with 1000 shots
   z_circuit = Circuit()
   z_circuit += ops.DefinitionBit("ro_z", 1, is_output=True)
   z_circuit += ops.PragmaRepeatedMeasurement("ro_z", 1000, None)
   # X-basis measurement circuit with 1000 shots   
   x_circuit = Circuit()
   x_circuit += ops.DefinitionBit("ro_x", 1, is_output=True)
   # Changing to the X basis with a Hadamard gate
   x_circuit += ops.Hadamard(0)
   x_circuit += ops.PragmaRepeatedMeasurement("ro_x", 1000, None)

   # Preparing the measurement input for one qubit
   measurement_input = PauliZProductInput(1, False)
   # Read out product of Z on site 0 for register ro_z (no basis change)
   z_basis_index = measurement_input.add_pauliz_product("ro_z", [0,])
   # Read out product of Z on site 0 for register ro_x
   # (after basis change effectively a <X> measurement)
   x_basis_index = measurement_input.add_pauliz_product("ro_x", [0,])
   
   # Add a result (the expectation value of H) that is a combination of
   # the PauliProduct expectation values
   measurement_input.add_linear_exp_val("<H>", {x_basis_index: 0.1, z_basis_index: 0.2})

   measurement = PauliZProduct(
      constant_circuit=init_circuit,
      circuits=[z_circuit, x_circuit],
      input=measurement_input,
   )

   # A quantum program is created from the measurement and "angle" is registered as
   # a free input parameter.
   # The QuantumProgram now has one free parameter that needs to set when executing it.
   # The symbolic value angle in the circuits will be replaced by that free parameter
   # during execution.
   program = QuantumProgram(
      measurement=measurement,
      input_parameter_names=["angle"],
   )

For details on how to execute QuantumPrograms see Execution.

A QuantumProgram returning unprocessed measurements

There also exist many use cases where end users want to receive the full measurement output without post-processing. For example when working with external tools that expect full measurement records or when implementing custom post-processing. For these use cases the ClassicalRegister measurement can be used to create three dictionaries, one for all registers with bit values, one for all registers with float values and one for all registers with complex values. Note that this measurement does not need a separate measurement input as no post-processing takes place.

   from qoqo import Circuit
   from qoqo import operations as ops
   from qoqo.measurements import ClassicalRegister
   from qoqo import QuantumProgram
   # initialize |psi>
   init_circuit = Circuit()
   # Apply a RotateY gate with a symbolic angle
   # To execute the circuit this symbolic parameter needs to be replaced 
   # with a real number with the help of a QuantumProgram
   init_circuit += ops.RotateX(0, "angle")
   # Z-basis measurement circuit with 1000 shots
   z_circuit = Circuit()
   z_circuit += ops.DefinitionBit("ro_z", 1, is_output=True)
   z_circuit += ops.PragmaRepeatedMeasurement("ro_z", 1000, None)
   # X-basis measurement circuit with 1000 shots   
   x_circuit = Circuit()
   x_circuit += ops.DefinitionBit("ro_x", 1, is_output=True)
   # Changing to the X basis with a Hadamard gate
   x_circuit += ops.Hadamard(0)
   x_circuit += ops.PragmaRepeatedMeasurement("ro_x", 1000, None)

   measurement = ClassicalRegister(constant_circuit=init_circuit, circuits=[z_circuit, x_circuit])

   # A quantum program is created from the measurement and "angle" is registered as a free input parameter
   # The QuantumProgram now has one free parameter that needs to set when executing it.
   # The symbolic value angle in the circuits will be replaced by that free parameter during execution.
   program = QuantumProgram(measurement=measurement, input_parameter_names=["angle"])

For details on how to execute QuantumPrograms see Execution.

Executing Quantum Programs

To obtain results from a QuantumProgram, Measurement or Circuit it needs to be executed on real quantum computing hardware or run on a simulator.

Qoqo uses separate backends for this evaluation. For each hardware or simulator a backend can be created that implements qoqo's EvaluatingBackend interface and runs QuantumPrograms. For an overview of backends see the qoqo website. Backends which provide the functionality to run a single circuit are so-called EvaluatingBackend. The QRydDemo backends fall in this category.

An EvaluatingBackend can run:

  1. A single circuit. The backend will execute just the circuit and return the measurement results of all registers in a tuple (bit-registers, float-registers, complex-registers). bit_registers is a dictionary of all registers with bit values, float_registers of all registers with float values and complex_registers of all registers with complex values. All the post-processing needs to be done manually.

  2. A measurement. All circuits in the measurement are run and the post-processed expectation values are returned.

  3. A quantum program. A QuantumProgram also handles replacement of variables. It provides its own run method and calls a provided backend internally.

As an example we will use the quantum program from Introduction and the qoqo-quest simulator backend. Here we show three alternative options that can be ran: a single circuit, a measurement, and a quantum program.

   from qoqo import Circuit, QuantumProgram
   from qoqo import operations as ops
   from qoqo.measurements import PauliZProduct, PauliZProductInput
   from qoqo_quest import Backend
   # Initialize |psi>
   init_circuit = Circuit()
   # Apply a RotateY gate with a symbolic angle
   # To execute the circuit this symbolic parameter needs to be replaced 
   # with a real number with the help of a QuantumProgram
   init_circuit += ops.RotateX(0, "angle")
   # Z-basis measurement circuit with 1000 shots
   z_circuit = Circuit()
   z_circuit += ops.DefinitionBit("ro_z", 1, is_output=True)
   z_circuit += ops.PragmaRepeatedMeasurement("ro_z", 1000, None)
   # X-basis measurement circuit with 1000 shots   
   x_circuit = Circuit()
   x_circuit += ops.DefinitionBit("ro_x", 1, is_output=True)
   # Changing to the X basis with a Hadamard gate
   x_circuit += ops.Hadamard(0)
   x_circuit += ops.PragmaRepeatedMeasurement("ro_x", 1000, None)

   # Preparing the measurement input for one qubit
   measurement_input = PauliZProductInput(1, False)
   # Read out product of Z on site 0 for register ro_z (no basis change)
   z_basis_index = measurement_input.add_pauliz_product("ro_z", [0,])
   # Read out product of Z on site 0 for register ro_x
   # (after basis change effectively a <X> measurement)
   x_basis_index = measurement_input.add_pauliz_product("ro_x", [0,])
   
   # Add a result (the expectation value of H) that is a combination of the PauliProduct
   # expectation values
   measurement_input.add_linear_exp_val("<H>", {x_basis_index: 0.1, z_basis_index: 0.2})

   measurement = PauliZProduct(
      constant_circuit=init_circuit,
      circuits=[z_circuit, x_circuit],
      input=measurement_input,
   )

   # Here we show three alternative options that can be ran:
   # a single circuit, a measurement, and a quantum program.

   # Create a backend simulating one qubit
   backend = Backend(1)

   # a) Run a single circuit 
   (bit_registers, float_registers, complex_registers) = backend.run_circuit(z_circuit)

   # b) To run a measurement we need to replace the free parameter by hand
   executable_measurement = measurement.substitute_parameters({"angle": 0.2})
   expectation_values = backend.run_measurement(executable_measurement)
   print(expectation_values)

   # c) Run a quantum program
   # The QuantumProgram now has one free parameter that needs to bet set when executing it.
   # The symbolic value "angle" in the circuits will be replaced by that free parameter
   # during execution.
   program = QuantumProgram(measurement=measurement, input_parameter_names=["angle"])
   # Run the program with  0.1 substituting `angle`
   expectation_values = program.run(backend, [0.1])

Note: The QuantumProgram can be run in the same way with the qoqo_qryd SimulatorBackend when all quantum operations are replaced by sequences of operations directly supported by the QRydDemo hardware. However, in order to use the qoqo_qryd SimulatorBackend, a device needs to be defined first, as shown in the SimulatorBackend subsection of QRyd Specifics.

In general, to distinguish between a command returning expectation values and a program returning register the command run_registers is used here.

   from qoqo import Circuit
   from qoqo import operations as ops
   from qoqo.measurements import ClassicalRegister
   from qoqo import QuantumProgram
   from qoqo_quest import Backend
   # Initialize |psi>
   init_circuit = Circuit()
   # Apply a RotateY gate with a symbolic angle
   # To execute the circuit this symbolic parameter needs to be replaced 
   # with a real number with the help of a QuantumProgram
   init_circuit += ops.RotateY(0, "angle")
   # Z-basis measurement circuit with 1000 shots
   z_circuit = Circuit()
   z_circuit += ops.DefinitionBit("ro_z", 1, is_output=True)
   z_circuit += ops.PragmaRepeatedMeasurement("ro_z", 1000, None)
   # X-basis measurement circuit with 1000 shots   
   x_circuit = Circuit()
   x_circuit += ops.DefinitionBit("ro_x", 1, is_output=True)
   # Changing to the X basis with a Hadamard gate
   x_circuit += ops.Hadamard(0)
   x_circuit += ops.PragmaRepeatedMeasurement("ro_x", 1000, None)

   measurement = ClassicalRegister(constant_circuit=init_circuit, circuits=[z_circuit, x_circuit])

   # A quantum program is created from the measurement and "angle" is registered as a free input
   # parameter. The QuantumProgram now has one free parameter that needs to be set when
   # executing it. The symbolic value angle in the circuits will be replaced by that free parameter
   # during execution.
   program = QuantumProgram(measurement=measurement, input_parameter_names=["angle"])

   backend = Backend(1)
   (bit_registers, float_registers, complex_registers) = program.run_registers(backend, [0.1])
   print(bit_registers)

Executing QuantumPrograms without returning expecation values

As described in Introduction the ClassicalRegister measurement can be used to return the full measurement record.

Non-executing backends

Qoqo also has backends that cannot be used to run or evaluate a quantum circuit. These backends typically are used to translate qoqo circuits to other quantum toolkits or languages. One example is qoqo_qasm

QRydDemo WebAPI

The QRydDemo project at the moment provides access to a Quantum Hardware Emulator via a REST-API. qoqo-qryd and roqoqo-qryd allow interfacing with the WebAPI. The WebAPI can be accessed on the level of direct API calls. It is also possible to use the WebAPI as an EvaluatingBackend for general (non-parameterized) qoqo QuantumPrograms via the WebAPI as described in Execution.

Getting access to the WebAPI

To use the WebAPI, a QRydDemo account is required. Users can register via the online registration form. Access is controlled via a Token. The token can either be directly provided to the APIBackend on creation or the backend tries to read it from the QRYD_API_TOKEN environmental variable.

Devices

At the moment the QrydDemo WebAPI supports two device emulators. Both support 30 qubits, one in a square and another with a triangular topology.

Square topologyTriangular topology

The Emulators allow the user to set the phase shifts in the PhaseShiftedControlledZ and PhaseShiftedControlledPhase native gates and the seed for the random number generator. In qoqo-qryd these are set when creating the devices.

    import numpy as np
    from qoqo_qryd.api_devices import QrydEmuSquareDevice
    from qoqo_qryd import APIBackend
    # Creating a new device.
    # All calculations on this device will use 1 as the random seed
    # and 0.23 as the phase shift in the PhaseShiftedControlledZ gate 
    device = QrydEmuSquareDevice(seed=1, controlled_z_phase_relation="0.23")
    # Create a new backend that will use the device.
    # Timeout controls how often the backend is queried 
    # for a result when backend is used as an evaluating backend.
    # The wait time between queries is 0.2 seconds.
    backend = APIBackend(device=device, timeout=20)

APIBackend

The APIBackend of qoqo-qryd supports the four direct API-calls of the QRydDemo WebAPI:

  1. Queueing a job of running a qoqo QuantumProgram with a ClassicalRegister measurement (post_job(quantumprogram)).
  2. Querying the status of the job (get_job_status(job)).
  3. Retrieving the results of the job (get_job_result(job)).
  4. Deleting a posted job (delete_job(job)).

Additional information can be found on the API documentation

The four API calls can be used to obtain a result the following way:

    import numpy as np
    from qoqo import operations as ops
    from qoqo import Circuit
    from qoqo import QuantumProgram
    from qoqo_qryd.api_devices import QrydEmuSquareDevice
    from qoqo.measurements import ClassicalRegister
    from time import sleep
    from qoqo_qryd import APIBackend

    # Creating a new device.
    # All calculations on this device will use 1 as the random seed
    # and 0.23 as the phase shift in the PhaseShiftedControlledZ gate 
    device = QrydEmuSquareDevice(seed=1, controlled_z_phase_relation="0.23")
    # Create a new backend that will use the device.
    # Timeout controls how often the backend is queried 
    # for a result when backend is used as an evaluating backend.
    # The wait time between queries is 0.2 seconds.
    backend = APIBackend(device=device, timeout=20)
    
    # Creating a quantum circuit that will be measured
    circuit = Circuit()

    circuit += ops.RotateX(0, np.pi/2)
    circuit += ops.RotateX(2, np.pi/2)
    circuit += ops.RotateX(4, np.pi/2)

    circuit += ops.DefinitionBit("ro", 6, is_output=True)
    circuit += ops.PragmaRepeatedMeasurement("ro", 1000, None)
    
    # The direct API calls only support ClassicalRegister measurements
    measurement = ClassicalRegister(constant_circuit=None, circuits=[circuit])
    program = QuantumProgram(measurement=measurement, input_parameter_names=[])
    # First API call: queueing the job
    # Returns url for further queries.
    job_location = backend.post_job(program)
    for i in range(20):
        print(i)
        sleep(30)
        # Query the job status via an API call
        job_status = backend.get_job_status(job_location)
        if job_status["status"] == "completed":
            # If the job is completed retrieve result via API call
            result = backend.get_job_result(job_location)
            print(result)
            break
    # alternatively delete job
    # backend.delete_job(job_location)

TweezerDevice

By calling the .from_api() static method, an instance of the TweezerDevice class can be created with Tweezer information already set.

from qoqo_qryd.tweezer_devices import TweezerDevice

# Creating a new TweezerDevice instance.
device = TweezerDevice.from_api(
    # The name of the device to use.
    device_name="test_device",
    # Optional token. This is not necessary if the token is already an environment variable.
    access_token="YOUR_QRYD_API_TOKEN"
)

Rydberg devices and operations

Due to the nature of the QRydDemo hardware based on Rydberg atoms, QRydDemo quantum computing devices can have special capabilities that are 'not' present in all universal quantum computers.

The devices can have two-dimensional grids of optical tweezer-positions and different two-dimensional grids can be calibrated. A tweezer-position is a physical spot that can be populated by a qubit. Not every tweezer position needs to be filled by a qubit and qubit can be moved between tweezer positions. Note that all functionality described here is a preview and does not represent a finalized QRydDemo design.

Special operations

To support the full flexibility of the QRydDemo devices, four additional qoqo operations are provided PragmaChangeQRydLayout, PragmaSwitchDeviceLayout, PragmaShiftQRydQubit and PragmaShiftQubitsTweezers. PragmaChangeQRydLayout allows a quantum circuit to change between predefined calibrated optical tweezer positions. It indexes the layouts by integer. A similar operation, but meant for TweezerDevice and TweezerMutableDevice, is PragmaSwitchDeviceLayout, which indexes the new layout via a string. PragmaShiftQRydQubit allows a quantum circuit to shift a qubit from one tweezer position to another. PragmaShiftQubitsTweezers is the equivalent operation meant to be used with TweezerDevice and TweezerMutableDevice.

   from qoqo import Circuit
   from qoqo_qryd.pragma_operations import PragmaChangeQRydLayout, PragmaShiftQRydQubit, PragmaSwitchDeviceLayout, PragmaShiftQubitsTweezers

   circuit = Circuit()
   # Switch to predefined layout 1
   circuit += PragmaChangeQRydLayout(new_layout=1).to_pragma_change_device()
   # Switch to predefined layout "triangle"
   circuit += PragmaSwitchDeviceLayout(new_layout="triangle").to_pragma_change_device()
   # Shift qubit 0 to tweezer position: row 0, column 1 and qubit 1 to postion row 1, column 1
   circuit += PragmaShiftQRydQubit(new_positions={0: (0,1), 1: (1,1)}).to_pragma_change_device()
   # Shift the qubit state present in tweezer 0 to tweezer 1, and the qubit state present in tweezer 2 to tweezer 3
   circuit += PragmaShiftQubitsTweezers(shifts=[(0, 1), (2, 3)]).to_pragma_change_device()
   ()

QRyd and Tweezer devices

Each type of QRydDemo hardware or Simulator device can be represented by either the QRydDevice or the TweezerDevice classes. The available hardware operations are defined in the devices. They save the 2D connectivity and can be queried for the availability of certain gate operations on the qubit. At the moment the most general example Device class is FirstDevice, that can be used for simulations. Other available devices include TweezerMutableDevice, which works by first defining the underlying tweezer structure and then populate it with qubits as needed. TweezerDevice works in a similar way. The main difference is it does not allow any setting of the Tweezer information. This should be obtained via the .from_api() call (see the WebAPI section).

The fundamental gates that are available on the QRydDemo devices are the following qoqo operations: RotateX, RotateY, RotateZ, RotateXY, PauliX, PauliY, PauliZ, PhaseShiftState1 , SqrtPauliX, InvSqrtPauliX, PhaseShiftedControlledZ and PhaseShiftedControlledPhase. The single-qubit gates are assumed to be available on all qubits. The PhaseShiftedControlledZ and PhaseShiftedControlledPhase are available between a subset of qubit pairs. The PhaseShiftedControlledZ is a ControlledPauliZ gate that also applies single qubit phases whereas the PhaseShiftedControlledPhase is equivalent to PhaseShiftedControlledZ but with a variable phase rotation. The phase shifts can in principle be device dependent.

The devices can optionally contain the controlled_z_phase_relation and controlled_phase_phase_relation parameters that define the phase shift relations of the two-qubit gates for the device. The first parameter can also be set explicitly by putting a string defining a float value as input.

   from qoqo_qryd.qryd_devices import FirstDevice
   from qoqo_qryd.tweezer_devices import TweezerMutableDevice
   import numpy as np

   # Create a FirstDevice
   first_device = FirstDevice(
      # The number of tweezer position rows in the 2D Grid is fixed
      number_rows=2,
      # The number of tweezer position  columns in the 2D grid is also fixed
      number_columns=4,
      # As not all tweezer positions must be filled, the number of positions
      # occupied by qubits per row is fixed
      qubits_per_row=[2, 2],
      # The (model) physical distance between rows is fixed
      row_distance=1.0,
      # The initial layout (layout number 0 for PragmaChangeQRydLayout) is defined 
      # by the physical positions of the tweezers in each row
      initial_layout=np.array([
         [0.0, 1.0, 2.0, 3.0],
         [0.0, 1.0, 2.0, 3.0]]),
      # The phase shift value related to the PhaseShiftedControlledZ gate
      # Using a string that defines a relation is also possible
      controlled_z_phase_relation="0.23",
      # The relation to use for the PhaseShiftedControlledPhase phase shift value
      controlled_phase_phase_relation="DefaultRelation"
   )

   # Print the two-qubit-operation connectivity graph of the device
   print(first_device.two_qubit_edges())

   # Create a TweezerMutableDevice
   tweezer_device = TweezerMutableDevice(
      # The phase shift value related to the PhaseShiftedControlledZ gate
      # Using a string that defines a relation is also possible
      controlled_z_phase_relation="0.23",
      # The relation to use for the PhaseShiftedControlledPhase phase shift value
      controlled_phase_phase_relation="DefaultRelation"
   )

   # Add a tweezer layout to the device
   tweezer_device.add_layout(name="triangle")

   # Set single-qubit tweezer gate time information on the new layout
   tweezer_device.set_tweezer_single_qubit_gate_time(
      hqslang="PhaseShiftState1",
      tweezer=0,
      gate_time=0.23,
      layout="triangle",
   )

   # Populate the newly set tweezer with qubit indexed as 0
   tweezer_device.add_qubit_tweezer_mapping(
      qubit=0,
      tweezer=0,
   )

   # Print the available layouts of the device
   print(tweezer_device.available_layouts())

SimulatorBackend

The SimulatorBackend of qoqo-qryd can execute qoqo QuantumPrograms depending on the provided devices. At the moment only the FirstDevice is available for the QRydDemo project. Executing a circuit with the SimulatorBackend initialized by the FirstDevice corresponds to running a simulation of the QuantumProgram which validates that only operations available in FirstDevice are used.

   from qoqo_qryd.qryd_devices import FirstDevice
   from qoqo_qryd import SimulatorBackend
   import numpy as np
   # Create a FirstDevice
   device = FirstDevice(
      # The number of tweezer position rows in the 2D Grid is fixed
      number_rows=2,
      # The number of tweezer position  columns in the 2D grid is also fixed
      number_columns=4,
      # As not all tweezer positions must be filled, the number of positions
      # occupied by qubits per row is fixed
      qubits_per_row=[2, 2],
      # The (model) physical distance between rows is fixed
      row_distance=1.0,
      # The initial layout (layout number 0 for PragmaChangeQRydLayout) is defined 
      # by the physical positions of the tweezers in each row
      initial_layout=np.array([
         [0.0, 1.0, 2.0, 3.0],
         [0.0, 1.0, 2.0, 3.0]]))

   # Initialize Backend
   backend = SimulatorBackend(device)

qoqo-qryd Examples

Using qubit shift operations

"""A simple example for shifting the qubit positions."""

# Copyright © 2021-2022 HQS Quantum Simulations GmbH.
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
# in compliance with the License. You may obtain a copy of the License at
#
#     http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software distributed under the License
# is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
# or implied. See the License for the specific language governing permissions and limitations under
# the License.

import numpy as np
from qoqo import Circuit
import qoqo.operations as ops
from qoqo_qryd import qryd_devices, SimulatorBackend
from qoqo_qryd import pragma_operations as qrydops


# ------------------------- The set-up of the device -------------------------- #

# Initializing Device with a square lattice
#
#   Tweezer positions:
#   (0, 0) ----- (0, 1) ----- (0, 2) ----- (0, 3)
#   (1, 0) ----- (1, 1) ----- (1, 2) ----- (1, 3)
#
#   Qubit positions:
#   0 --- 1
#   2 --- 3
#
device = qryd_devices.FirstDevice(
    number_rows=2,
    number_columns=4,
    qubits_per_row=[2, 2],
    row_distance=1.0,
    initial_layout=np.array([
        [0.0, 1.0, 2.0, 3.0],
        [0.0, 1.0, 2.0, 3.0]
    ])
)

# Adding a triangular lattice
#
#   Tweezer positions:
#   (0, 0) ----- (0, 1) ----- (0, 2) ----- (0, 3)
#          (1, 0) ----- (1, 1) ----- (1, 2) ----- (1, 3)
#
#   Qubit positions:
#   0 --- 1
#      2 --- 3
device = device.add_layout(
    1,
    np.array([
       [0.0, 1.0, 2.0, 3.0],
       [0.5, 1.5, 2.5, 3.5]
   ])
)
# Set the cut-off distance for two-qubit interactions
device.set_cutoff(1.0)
# Setting up the device in the square lattice
device.change_qubit_positions({0: (0, 0), 1: (0, 1), 2: (1, 0), 3: (1, 1)})
backend = SimulatorBackend(device)

# ------------------------ The set-up of the circuit ------------------------ #

circuit = Circuit()
# Qubits 1 and 2 are not close enough for interaction in square lattice
circuit += ops.PhaseShiftedControlledZ(control=1, target=2, phi=0.0)
# This should fail
# result = backend.run_circuit(circuit)


# ------------------ The set-up of the circuit with device change --------------- #

circuit = Circuit()
# Qubits 1 and 2 are close enough for interaction in square lattice after shift
#   Qubit positions after shift:
#   0 --- 1
#         2 --- 3
circuit += ops.DefinitionComplex("state_vector_before", 16, True)
circuit += ops.DefinitionComplex("state_vector_after", 16, True)
circuit += ops.RotateX(1, np.pi)
circuit += ops.RotateX(2, np.pi / 2)
circuit += ops.PragmaGetStateVector("state_vector_before", None)
circuit += qrydops.PragmaShiftQRydQubit(
    {0: (0, 0), 1: (0, 1), 2: (1, 1), 3: (1, 2)}).to_pragma_change_device()
circuit += ops.PhaseShiftedControlledZ(control=1, target=2, phi=0.0)
circuit += ops.PragmaGetStateVector("state_vector_before", None)
# This should pass
result = backend.run_circuit(circuit)
print("State vector before applying shift and two-qubit gate")
print(result[2]["state_vector_before"])
print("State vector after applying shift and two-qubit gate")
print(result[2]["state_vector_after"])

Changing the device layout

"""A simple example for switching between a square lattice and a triangular lattice layout."""

# Copyright © 2021-2022 HQS Quantum Simulations GmbH.
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
# in compliance with the License. You may obtain a copy of the License at
#
#     http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software distributed under the License
# is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
# or implied. See the License for the specific language governing permissions and limitations under
# the License.

import numpy as np
from qoqo import Circuit
import qoqo.operations as ops
from qoqo_qryd import qryd_devices, SimulatorBackend
from qoqo_qryd import pragma_operations as qrydops


# ------------------------- The set-up of the device -------------------------- #

# Initializing device with a square lattice
#
#   Tweezer positions:
#   (0, 0) ----- (0, 1) ----- (0, 2) ----- (0, 3)
#   (1, 0) ----- (1, 1) ----- (1, 2) ----- (1, 3)
#
#   Qubit positions:
#   0 --- 1
#   2 --- 3
#
device = qryd_devices.FirstDevice(
    number_rows=2,
    number_columns=4,
    qubits_per_row=[2, 2],
    row_distance=1.0,
    initial_layout=np.array([
        [0.0, 1.0, 2.0, 3.0],
        [0.0, 1.0, 2.0, 3.0]]))
# Adding a triangular lattice
#
#   Tweezer positions:
#   (0, 0) ----- (0, 1) ----- (0, 2) ----- (0, 3)
#          (1, 0) ----- (1, 1) ----- (1, 2) ----- (1, 3)
#
#   Qubit positions:
#   0 --- 1
#      2 --- 3
device = device.add_layout(
    1,
   np.array([
       [0.0, 1.0, 2.0, 3.0],
       [0.5, 1.5, 2.5, 3.5]
   ])
)
# Set the cut-off distance for two-qubit interactions
device.set_cutoff(1.0)
# Setting up the device in the triangular lattice
device.switch_layout(1)
device.change_qubit_positions({0: (0, 0), 1: (0, 1), 2: (1, 0), 3: (1, 1)})
backend = SimulatorBackend(device)

# ----------------------- The set-up of the circuit -------------------------- #

circuit = Circuit()
# Qubits 0 and 2 are not close enough
# for interaction in triangular lattice
circuit += ops.PhaseShiftedControlledZ(control=0, target=2, phi=0.0)
# This should fail
# result = backend.run_circuit(circuit)


# ----------------- The set-up of the circuit with device change --------------- #

circuit = Circuit()
circuit += ops.DefinitionComplex("state_vector_before", 16, True)
circuit += ops.DefinitionComplex("state_vector_after", 16, True)
circuit += ops.RotateX(0, np.pi)
circuit += ops.RotateX(2, np.pi / 2)
circuit += ops.PragmaGetStateVector("state_vector_before", None)
# Qubits 0 and 2 are close enough for interaction in square lattice
circuit += qrydops.PragmaChangeQRydLayout(0).to_pragma_change_device()
circuit += ops.PhaseShiftedControlledZ(control=0, target=2, phi=0.0)
circuit += ops.PragmaGetStateVector("state_vector_after", None)
# This should pass
result = backend.run_circuit(circuit)
print("State vector before applying shift and two-qubit gate")
print(result[2]["state_vector_before"])
print("State vector after applying shift and two-qubit gate")
print(result[2]["state_vector_after"])

Using multi-qubit operations

"""A simple example demonstrating multi qubit operations."""

# Copyright © 2021 - 2022 HQS Quantum Simulations GmbH.
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
# in compliance with the License. You may obtain a copy of the License at
#
#     http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software distributed under the License
# is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
# or implied. See the License for the specific language governing permissions and limitations under
# the License.
import numpy as np
from qoqo import Circuit
import qoqo.operations as ops
from qoqo_qryd import qryd_devices, SimulatorBackend
from qoqo_qryd import pragma_operations as qrydops

# --------------------- The set-up of the device ----------------------- #

# Initializing Device with a square lattice
#
#   Tweezer positions:
#   (0, 0) ----- (0, 1) ----- (0, 2) ----- (0, 3)
#   (1, 0) ----- (1, 1) ----- (1, 2) ----- (1, 3)
#   (2, 0) ----- (2, 1) ----- (2, 2) ----- (2, 3)
#
#   Qubit positions:
#   0 --- 1 --- 2 --- 3
#   4 --- 5 --- 6 --- 7
#   8 --- 9 --- 10 --- 11
#
device = qryd_devices.FirstDevice(
    number_rows=3,
    number_columns=4,
    qubits_per_row=[4, 4, 4],
    row_distance=1.0,
    initial_layout=np.array([
        [0.0, 1.0, 2.0, 3.0],
        [0.0, 1.0, 2.0, 3.0],
        [0.0, 1.0, 2.0, 3.0]]))

backend = SimulatorBackend(device)

# ---------------- Multi Qubit Circuits that will fail ---------------------- #

# For the Prototype we assume that only MultiQubitZZ operations 
# are allowed between qubits in one row.
# This is an arbitrary limitation implemented to showcase
# how a restricted operation would be implemented
# in the final device.

# Use a MultiQubitMS Molemer-Sorensen Gate
circuit = Circuit()
# MultiQubitMS not supported natively
circuit += ops.MultiQubitMS(qubits=[0, 1, 2, 3], theta=1.0)
# This should fail
# result = backend.run_circuit(circuit)

# Use a MultiQubitZZ  Gate but in the wrong direction
circuit = Circuit()
# MultiQubitZZ not supported along a column
circuit += ops.MultiQubitZZ(qubits=[0, 4, 8], theta=1.0)
# This should fail
#result = backend.run_circuit(circuit)


# --------------------- Working Multi Qubit Circuit ------------------------- #

circuit = Circuit()
circuit += ops.DefinitionBit("ro", 12, True)
circuit += ops.MultiQubitZZ(qubits=[0, 1, 2, 3], theta=1.0)
circuit += ops.PragmaRepeatedMeasurement("ro", 1, None)
# This should pass
result = backend.run_circuit(circuit)
print("Result of ZZ on first four qubits")
print(result)

qoqo-qryd API documentation

The qoqo-qryd provides software components for qoqo that support QRydDemo quantum computers.

The qoqo-qryd module provides:

  1. Backends that execute a compiled qoqo QuantumProgram on QRydDemo hardware (when available), via a WebAPI emulation (APIBackend) or simulators (SimulatorBackend).
  2. A set of specific operations only available on QRydDemo hardware.
  3. A collection of devices, representations of the hardware devices available in QrydDemo.

This is the API documentation of qoqo-qryd, for a more general overview see the User Documentation.