Introduction to Qoqo

Quantum Operation Quantum Operation
Yes, we use reduplication

What Qoqo is

  • A toolkit to represent quantum operations and circuits
  • A thin runtime to run quantum measurements
  • A way to serialize quantum circuits and measurement information
  • A set of optional interfaces to devices, simulators and toolkits

What Qoqo is not

  • A decomposer translating circuits to a specific set of gates
  • A quantum circuit optimizer
  • A collection of quantum algorithms

A simple circuit and measurement

We show the construction of a simple entangling circuit and an observable measurement based on this circuit

Entangling circuit snippet

Similar to many other toolkits the unitary entangling circuit can be constructed by adding operations to a circuit

In [1]:
from qoqo import Circuit
from qoqo import operations as ops

circuit_snippet = Circuit()
circuit_snippet += ops.Hadamard(qubit=0)
circuit_snippet += ops.CNOT(control=0, target=1)
print(circuit_snippet)
print(len(circuit_snippet))
print(circuit_snippet.get_operation_types())

assert len(circuit_snippet) == 2
assert circuit_snippet.get_operation_types() == set(['Hadamard', 'CNOT'])
Hadamard(Hadamard { qubit: 0 })
CNOT(CNOT { control: 0, target: 1 })

2
{'Hadamard', 'CNOT'}

Measuring qubits

Qoqo uses classical registers for the readout. We need to add a classical register definition to the circuit and a measurement statement. The number of projective measurements can be directly set in the circuit.
The simulation and measurement of the circuit is handled by the qoqo_pyquest interface (in this example).

In [2]:
from qoqo_pyquest import PyQuestBackend
from qoqo import Circuit
from qoqo import operations as ops

circuit = Circuit()
circuit += ops.DefinitionBit(name='ro', length=2, is_output=True)
circuit += ops.Hadamard(qubit=0)
circuit += ops.CNOT(control=0, target=1)
circuit += ops.PragmaRepeatedMeasurement(readout='ro', number_measurements=10, qubit_mapping=None)
backend = PyQuestBackend(number_qubits=2)
(result_bit_registers, result_float_registers, result_complex_registers) = backend.run_circuit(circuit)
for single_projective_measurement in result_bit_registers['ro'] :
    print(single_projective_measurement)
    
assert len(result_bit_registers['ro']) == 10
[1, 1]
[1, 1]
[1, 1]
[0, 0]
[0, 0]
[1, 1]
[1, 1]
[1, 1]
[1, 1]
[1, 1]

Measuring Observables

Qoqo includes the direct evaluation of projective measurements to an observable measurement e.g. 3 * < Z0 > + < Z0 Z1 >
The measurement is defined by a set of expectation values of a product of pauli operators and a matrix that combines the expectation values

In [3]:
from qoqo.measurements import BasisRotationInput, BasisRotation
from qoqo import DoUnitary
from qoqo_pyquest import PyQuestBackend
from qoqo import Circuit
from qoqo import operations as ops
import numpy as np
import scipy.sparse as sp

circuit = Circuit()
circuit += ops.DefinitionBit(name='ro', length=2, is_output=True)
circuit += ops.PauliX(qubit=0)
#circuit += ops.Hadamard(qubit=0)
circuit += ops.CNOT(control=0, target=1)
circuit += ops.PragmaRepeatedMeasurement(readout='ro', number_measurements=10, qubit_mapping=None)

measurement_input = BasisRotationInput(number_qubits=2, use_flipped_measurement=False)
index0 = measurement_input.add_pauli_product(readout="ro", pauli_product_mask=[0])
index1 = measurement_input.add_pauli_product(readout="ro", pauli_product_mask=[0,1]) # From readout 'ro' measure two pauli products 0: < Z0 > and 1: < Z0 Z1 >
measurement_input.add_linear_exp_val(name="example", linear={0:1.0, 1: 0.0}) # One expectation value: 3 * pauli_product0 + 1 * pauli_product1

measurement = BasisRotation(input=measurement_input, circuits=[circuit], constant_circuit=None )

backend = PyQuestBackend(number_qubits=2)

do_unitary = DoUnitary(measurement=measurement, backend=backend, free_parameters=[]) # This quantum program does not have any free parameters
res = do_unitary()["example"]

print("Result of DoUnitary", res)

assert res > -4.0 * 10
assert res < 4.0 * 10
{}
Result of DoUnitary -1.0

De/Serializing the quantum program

In [4]:
from qoqo.measurements import BasisRotationInput, BasisRotation
from qoqo import DoUnitary
from qoqo_pyquest import PyQuestBackend
from qoqo import Circuit
from qoqo import operations as ops
import numpy as np
import scipy.sparse as sp

circuit = Circuit()
circuit += ops.DefinitionBit(name='ro', length=2, is_output=True)
circuit += ops.Hadamard(qubit=0)
circuit += ops.CNOT(control=0, target=1)
circuit += ops.PragmaRepeatedMeasurement(readout='ro', number_measurements=10, qubit_mapping=None)

measurement_input = BasisRotationInput(number_qubits=2, use_flipped_measurement=False)
index0 = measurement_input.add_pauli_product(readout="ro", pauli_product_mask=[0])
index1 = measurement_input.add_pauli_product(readout="ro", pauli_product_mask=[0,1]) # From readout 'ro' measure two pauli products 0: < Z0 > and 1: < Z0 Z1 >
measurement_input.add_linear_exp_val(name="example", linear={0:3.0, 1: 1.0}) # One expectation value: 3 * pauli_product0 + 1 * pauli_product1

measurement = BasisRotation(input=measurement_input, circuits=[circuit], constant_circuit=None )
measurement_json = measurement.to_json()

measurement = BasisRotation.from_json(measurement_json)

print(measurement_json)

assert measurement_json != ""
{"constant_circuit":null,"circuits":[{"definitions":[{"DefinitionBit":{"name":"ro","length":2,"is_output":true}}],"operations":[{"Hadamard":{"qubit":0}},{"CNOT":{"control":0,"target":1}},{"PragmaRepeatedMeasurement":{"readout":"ro","qubit_mapping":null,"number_measurements":10}}]}],"input":{"pauli_product_qubit_masks":{"ro":{"1":[0,1],"0":[0]}},"number_qubits":2,"number_pauli_products":2,"measured_exp_vals":{"example":{"Linear":{"0":3.0,"1":1.0}}},"use_flipped_measurement":false}}

Fine control over decoherence

Qoqo allows full control over decoherence by placing decoherence operations in the circuit on the same level as gates.
Example: Letting only one qubit decay.
The backend automatically switches from statevector simulation to density matrix simulation in the presence of noise.

In [5]:
from qoqo_pyquest import PyQuestBackend
from qoqo import Circuit
from qoqo import operations as ops

damping = 0.1
number_measurements = 100
circuit = Circuit()
circuit += ops.DefinitionBit(name='ro', length=2, is_output=True)
circuit += ops.PauliX(qubit=0)
circuit += ops.PauliX(qubit=1)
circuit += ops.PragmaDamping(qubit=0, gate_time=1, rate=damping)
circuit += ops.PragmaRepeatedMeasurement(readout='ro', number_measurements=number_measurements, qubit_mapping=None)
print(circuit)
backend = PyQuestBackend(number_qubits=2)
(result_bit_registers, result_float_registers, result_complex_registers) = backend.run_circuit(circuit)
sum_test = np.array([0.0, 0.0])
for single_projective_measurement in result_bit_registers['ro']:
    #print(single_projective_measurement)
    sum_test += single_projective_measurement
scaled_result = sum_test/number_measurements
print("Scaled result", scaled_result)

assert len(scaled_result) == 2
DefinitionBit(DefinitionBit { name: "ro", length: 2, is_output: true })
PauliX(PauliX { qubit: 0 })
PauliX(PauliX { qubit: 1 })
PragmaDamping(PragmaDamping { qubit: 0, gate_time: Float(1.0), rate: Float(0.1) })
PragmaRepeatedMeasurement(PragmaRepeatedMeasurement { readout: "ro", qubit_mapping: None, number_measurements: 100 })

Scaled result [0.86 1.  ]

Symbolic parameters

In many cases, operation parameters depend on a symbolic parameter of the whole quantum program (time in time-evolution, overrotation, variational parameters...)
Qoqo allows the fast calculation of symbolic parameter expressions.
Expressions are provided in string form.
DoUnitary can automatically replace symbolic parameters using call parameters.

Writing the symbolic circuit and replacing symbolic parameters

In [6]:
from qoqo import Circuit
from qoqo import operations as ops
circuit = Circuit()
print('Symbolic circuit')
circuit += ops.RotateX(qubit=0, theta='3*time+offset')

print(circuit)

circuit2 = circuit.substitute_parameters({'time': 1/3, 'offset':1})
print('After substitution')
print(circuit2)
Symbolic circuit
RotateX(RotateX { qubit: 0, theta: Str("3*time+offset") })

After substitution
RotateX(RotateX { qubit: 0, theta: Float(2.0) })

Symbolic parameters in a full quantum program

In [7]:
from qoqo.measurements import BasisRotationInput, BasisRotation
from qoqo import DoUnitary
from qoqo_pyquest import PyQuestBackend
from qoqo import Circuit
from qoqo import operations as ops
import numpy as np
import scipy.sparse as sp

number_measurements = 100000

circuit = Circuit()
circuit += ops.DefinitionBit(name='ro', length=2, is_output=True)
circuit += ops.RotateX(qubit=0, theta='3*time+offset')
circuit += ops.PragmaRepeatedMeasurement(readout='ro', number_measurements=number_measurements, qubit_mapping=None)

measurement_input = BasisRotationInput(number_qubits=2, use_flipped_measurement=False)
index0 = measurement_input.add_pauli_product(readout="ro", pauli_product_mask=[0])
index1 = measurement_input.add_pauli_product(readout="ro", pauli_product_mask=[0,1]) # From readout 'ro' measure two pauli products 0: < Z0 > and 1: < Z0 Z1 >
measurement_input.add_linear_exp_val(name="example", linear={0:3.0, 1: 1.0}) # One expectation value: 3 * pauli_product0 + 1 * pauli_product1

measurement = BasisRotation(input=measurement_input, circuits=[circuit], constant_circuit=None )

backend = PyQuestBackend(number_qubits=2)

do_unitary = DoUnitary(measurement=measurement, backend=backend, free_parameters=['time', 'offset']) # The symbolic parameter is the free parameter
result = do_unitary([0.5, 0])
print("Result", result)

assert len(result) == 3
{'time': 0.5, 'offset': 0}
Result {'example': 0.29, 'time': 0.5, 'offset': 0}

Testing scaling performance with qoqo_mock

Quantum simulators cannot simulate systems with a significant number of qubits fast enough to benchmark qoqo with a large number of qubits and operations. The qoqo_mock interface can be used to benchmark qoqo without simulating a quantum computer.

In [8]:
from qoqo.measurements import BasisRotationInput, BasisRotation
from qoqo import DoUnitary
from qoqo_mock import MockedBackend
from qoqo import Circuit
from qoqo import operations as ops
import numpy as np
import timeit

# Default values are small to reduce load for automated testing uncomment values to test large systems

number_measurements = 10 # 1000
number_operations = 100 # 1000000
number_qubits = 5 # 500

circuit = Circuit()

circuit += ops.DefinitionBit(name='ro', length=number_qubits, is_output=True)

for i, q in zip(np.random.randint(0,4,number_operations), np.random.randint(0,500,number_operations)):
    if i == 0:
        circuit += ops.RotateX(qubit=q, theta="4*theta_x")
    if i == 1:
        circuit += ops.RotateY(qubit=q, theta="2*theta_y")
    if i == 2:
        circuit += ops.RotateZ(qubit=q, theta="3*theta_z")
    if i == 4:
        circuit += ops.ControlledPauliZ(qubit=q, control=0)
circuit += ops.PragmaRepeatedMeasurement(readout='ro', number_measurements=number_measurements, qubit_mapping=None)

pp_dict = dict()

measurement_input = BasisRotationInput(number_qubits=number_qubits, use_flipped_measurement=False)
for i in range(number_qubits):
    index0 = measurement_input.add_pauli_product(readout="ro", pauli_product_mask=[i])
    pp_dict[number_qubits] = i

measurement_input.add_linear_exp_val(name="example", linear={0:1.0})

measurement = BasisRotation(input=measurement_input, circuits=[circuit], constant_circuit=None )

backend= MockedBackend(number_qubits=number_qubits)
do_unitary = DoUnitary(measurement=measurement, backend=backend, free_parameters=['theta_x', 'theta_y', 'theta_z'])
res = do_unitary([0,1,2])
print("Result", res)
time_taken = timeit.timeit('do_unitary([0,1,2])', globals=globals(),number=1)
print("Time taken", time_taken)

assert len(res) == 4 
assert time_taken < 30
{'theta_x': 0, 'theta_y': 1, 'theta_z': 2}
Result {'example': -0.00324, 'theta_x': 0, 'theta_y': 1, 'theta_z': 2}
{'theta_x': 0, 'theta_y': 1, 'theta_z': 2}
Time taken 9.808817166999999
In [ ]: