Main objects: Jobs, Observables, Circuits…
MyQLM comes in the shape of a series of Python libraries. These libraries
are all articulated around a common set of classes, data structures,
and services. This page describes the main classes of myQLM as well as their
usefulness. These structures are all defined in the Python namespace
qat.core
so please refer to qat-core: Core data structures and Abstract classes to read the source code
documentation of these structures.
Job and Batch
In the QLM, quantum circuits are viewed as routines preparing a quantum memory in a particular state.
Hence, some additional sampling information should be provided next to the quantum circuit.
The Job
class fits this purpose. To define a Job
, we will
provide various pieces of information such as:
The type of sampling we would like to perform : standard computational basis sampling, sampling of an observable, etc.
The number of samples, or “shots”, to perform
Which qubits should be measured
The format of the returned data
A Batch
object contains a list of jobs that allows to send several
circuits to a QPU with only a single request to the QPU.
An example using a batch to simulate a circuit:
# Import and instantiate a QPU
from qat.pylinalg import PyLinalg
qpu = PyLinalg()
# Create a job from a circuit
job = circuit.to_job(nbshots=1024, qubits=[1,7])
# Submit the job
result = qpu.submit(job)
# To iterate over final state vector produce by the circuit:
for sample in result:
print(sample)
Warning
The state vector could be large, saving it into an array may lead to memory issues.
Note
The job is defined by qat.core.Job
.
The batch is defined by qat.core.Batch
.
Observables
As mentioned above, it is possible to construct a Job
requiring the sampling of some observable on the final state produced by a quantum circuit. The Observable
class provides a basic interface to declare observables.
from qat.core import Observable, Term
my_observable = Observable(4, # A 4 qubits observable
pauli_terms=[
Term(1., "ZZ", [0, 1]),
Term(4., "XZ", [2, 0]),
Term(3., "ZXZX", [0, 1, 2, 3])
],
constant_coeff=23.)
print(my_observable)
23.0 * I^4 +
1.0 * (ZZ|[0, 1]) +
4.0 * (XZ|[2, 0]) +
3.0 * (ZXZX|[0, 1, 2, 3])
Observables can be added and multiplied by a scalar:
from qat.core import Observable, Term
obs1 = Observable(2, pauli_terms=[Term(1., "ZZ", [0, 1])])
obs2 = Observable(2, pauli_terms=[Term(1., "X", [0])])
print(obs1 + obs2)
1.0 * (ZZ|[0, 1]) +
1.0 * (X|[0])
from qat.core import Observable, Term
obs1 = Observable(2, pauli_terms=[Term(1., "ZZ", [0, 1])])
print(4 * obs1)
4.0 * (ZZ|[0, 1])
They can be composed via tensor product using the __xor__ operator:
from qat.core import Observable, Term
obs1 = Observable(2, pauli_terms=[Term(1., "ZZ", [0, 1])])
obs2 = Observable(2, pauli_terms=[Term(1., "X", [0])])
print(obs1 ^ obs2)
1.0 * (ZZX|[0, 1, 2])
The commutator of two observables can be computed using the __or__ operator:
from qat.core import Observable, Term
obs1 = Observable(2, pauli_terms=[Term(1., "ZZ", [0, 1])])
obs2 = Observable(2, pauli_terms=[Term(1., "X", [0])])
print(obs1 | obs2)
2j * (YZ|[0, 1])
And last but not least, observables can be attached to a circuit to form an observable sampling job:
from qat.core import Observable, Term
obs = Observable(2, pauli_terms=[Term(1., "ZZ", [0, 1])])
job = circuit.to_job(observable=obs, nbshots=2048)
Results
After sending a Job on a QPU, the returned results are
encapsulated in an object qat.core.Result
.
Attributes of Result
:
raw_data is a list of Sample (cf.
Sample
)value scalar output (when sampling an observable)
value_data informations on the scalar output
meta_data any information the quantum processor might want to transmit to the user.
Attributes of Sample
:
amplitude the amplitude of the measured state
probability is a float in [0,1] representing the probability of getting this state
intermediate_measurements is a list summarizing the results of intermediate measurements
err the sampling error
Circuits
Inside the environment of Python libraries offered by the QLM, quantum circuits are described using the Circuit
class. This class is in fact a wrapper of a serializable object. This wrapper provides and overloads various methods for an active manipulation of these objects.
The high-level wrapper
Most of the standard manipulations can be handled via the high-level interface of the circuit:
iterating over the instructions using the
qat.core.Circuit.iterate_simple()
method
from qat.lang.AQASM import Program
from qat.lang.AQASM.qftarith import QFT
prog = Program()
qbits = prog.qalloc(4)
prog.apply(QFT(4), qbits)
prog.measure([qbits[0], qbits[3]])
prog.reset(qbits[2])
circuit = prog.to_circ()
for instruction in circuit.iterate_simple():
print(instruction)
('H', [], [0])
('C-PH', [1.5707963267948966], [1, 0])
('C-PH', [0.7853981633974483], [2, 0])
('C-PH', [0.39269908169872414], [3, 0])
('H', [], [1])
('C-PH', [1.5707963267948966], [2, 1])
('C-PH', [0.7853981633974483], [3, 1])
('H', [], [2])
('C-PH', [1.5707963267948966], [3, 2])
('H', [], [3])
('MEASURE', [0, 3], [0, 3])
('RESET', [2], [])
iterating over raw instructions (for advanced usage):
from qat.lang.AQASM import Program
from qat.lang.AQASM.qftarith import QFT
prog = Program()
qbits = prog.qalloc(4)
prog.apply(QFT(4), qbits)
prog.measure([qbits[0], qbits[3]])
prog.reset(qbits[2])
circuit = prog.to_circ()
for instruction in circuit:
print(instruction)
Op(gate='H', qbits=[0], type=0, cbits=None, formula=None, remap=None)
Op(gate='_2', qbits=[1, 0], type=0, cbits=None, formula=None, remap=None)
Op(gate='_4', qbits=[2, 0], type=0, cbits=None, formula=None, remap=None)
Op(gate='_6', qbits=[3, 0], type=0, cbits=None, formula=None, remap=None)
Op(gate='H', qbits=[1], type=0, cbits=None, formula=None, remap=None)
Op(gate='_2', qbits=[2, 1], type=0, cbits=None, formula=None, remap=None)
Op(gate='_4', qbits=[3, 1], type=0, cbits=None, formula=None, remap=None)
Op(gate='H', qbits=[2], type=0, cbits=None, formula=None, remap=None)
Op(gate='_2', qbits=[3, 2], type=0, cbits=None, formula=None, remap=None)
Op(gate='H', qbits=[3], type=0, cbits=None, formula=None, remap=None)
Op(gate=None, qbits=[0, 3], type=1, cbits=[0, 3], formula=None, remap=None)
Op(gate=None, qbits=[2], type=2, cbits=[], formula=None, remap=None)
concatenation using the overloaded
__add__
operatortensor product using the overloaded
__mult__
operatorserialization/deserialization using the
dump()
andload()
methodsabstract variables binding using the
bind_variables()
methodeasy job generation using the
to_job()
method
Qubits and cbits
The number of qubits and classical bits declared in the circuit can be accessed like so:
circuit.nbqbits
circuit.nbcbits
At circuit generation, the convention is to extend the number of classical bits to match the number of declared qubits. So it might be that your didn’t declare any cbits in pyAQASM, and still end up with a non-zero number of classical bits.
The field nbqbits might also be extended to match the total number of qbits used by the circuit (for instance if a sub-routine is using some ancillae that are dynamically allocated at inlining/emulation/execution).
This extension requires to emulate the flow of the circuit.
All quantum registers declared in pyAQASM can be found in the .qregs field. The type of these registers is also stored in the QReg structure:
for qreg in circuit.qregs:
print("Register of length {} starting at {}".format(qreg.length, qreg.start))
Please refer to the section Quantum types to get more information on registers.
Instruction list
The main ‘body’ of a Circuit
is described as a list of operations - Op
.
Here is the formal documentation of these Op objects:
- class qat.comm.datamodel.ttypes.Op(gate=None, qbits=None, type=0, cbits=None, formula=None, remap=None)
Operator structure. This structure describes any operation that is accessible in AQASM/pyAQASM like quantum gate applications (optionally classically controled) or measures, resets, breaks, classical operations and classical remaps. The field .type is used to distinguish between these cases.
- Instance attributes:
gate (str): unique identifyer of the gate (if operator is a gate)
qbits (list): indices of qubits on which the operator acts
type (OpType, optional): type of operator (gate, measure…)
cbits (list, optional): indices of classical bits in which to store output (if applicable)
formula (str, optional): a string in RPF representing a boolean formula whose leaves are cbit indexes (for resets, etc.)
remap (list, optional): a list of qubit describing a permutation (if the gate is REMAP type)
More precisely the field .type in Op
can be:
['BREAK', 'CLASSIC', 'CLASSICCTRL', 'GATETYPE', 'MEASURE', 'REMAP', 'RESET']
Once the type is set, various attributes of the Op
object are used to store relevant pieces of information.
GATETYPE corresponds to a quantum gate application (without classical control). In that case:
op.gate will contain the name of the gate (see below the gate dictionary section for detailed use of this name)
op.qbits will contain the list of the target qubits
CLASSICCTRL corresponds to a quantum gate application with classical control. In addition to the fields used in the GATETYPE case, here we also have:
op.cbits will contain a list of size 1 with a single cbit to be used as control classical bit
MEASURE corresponds to a measure operation:
op.qbits will contain a list of qubits to measure
op.cbits will contain a list of cbits to store the results. The two lists will have the exact same size.
BREAK corresponds to a break operation:
op.formula will contain a (prefix formatted) string containing a boolean formula to evaluate over the current values of the cbits in order to determine if the computation should be aborted or not.
qat.core.formula_eval.evaluate
provides an implementation of this evaluation, if required.
from qat.core.formula_eval import evaluate formula = "AND 1 OR 0 2" cbit_values = [True, False, True] evaluate(formula, cbit_values) #should return False
RESET corresponds to reset operations.
op.qbits will contain a list of qubits to reset
op.cbits will contain a list of cbits to reset
REMAP corresponds to classical remaps/rewiring of the qubits. These can be seen as permutations of the index of the qubits.
op.qbits will contain a list of qubits that are to be rewired
op.remap will contain a list of integers describing the way the qubits are remapped
CLASSIC corresponds to a classical operation between cbits.
op.cbits will contain the cbit receiving a new value
op.formula will contain a (prefix formatted) string containing a boolean formula to evaluate over the current values of the cbits
Quantum gates and gate dictionary
All quantum gates names (the op.gate field) are in fact keys of a dictionary stored in the circuit.gateDic field of the circuit.
Entries in this dictionary are of the type GateDefinition
.
from qat.lang.AQASM import Program
prog = Program()
circuit = prog.to_circ()
print(circuit.gateDic["H"])
GateDefinition(name='H', arity=1, matrix=Matrix(nRows=2, nCols=2, data=[ComplexNumber(re=0.7071067811865475, im=0.0), ComplexNumber(re=0.7071067811865475, im=0.0), ComplexNumber(re=0.7071067811865475, im=0.0), ComplexNumber(re=-0.7071067811865475, im=0.0)]), is_ctrl=False, is_dag=None, is_trans=None, is_conj=None, subgate=None, syntax=GSyntax(name='H', parameters=[]), nbctrls=None, circuit_implementation=None)
Toggle to get the documentation of GateDefinition
Note
Class
GateDefinition
is not designed to be instantiated manually. Please refer to the Writting quantum circuits section or theqat.lang
module to create your own circuits
- class qat.comm.datamodel.ttypes.GateDefinition
A gate definition describes the implementation of a quantum gate. A quantum gate can be defined by:
a unitary matrix
a function of another gate (i.e. control of a subgate, dagger of a subgate, …)
a subcircuit
- Instance attributes:
matrix (optional): the matrix implementation of the gate. A matrix is defined with the following attributes:
nCols (int): the number of columns in the matrix
nRows (int): the number of rows in the matrix
data (list): list of complex numbers describing the content of this matrix
is_ctrl (bool, optional, deprecated): indicates if the gate is a controled version of another gate
is_dag (bool, optional): indicates if the gate is a dagger version of another gate
is_conj (bool, optional): indicates if the gate is a conjugate version of another gate
is_trans (bool, optional): indicates if the gate is a transpose version of another gate
nbctrls (int, optional): signifies that the gate is a multiple controled version of another gate. If set to a non-zero number, subgate will store the corresponding subgate.
subgate (str, optional): will store the name of the subgate if any one of the .is_ctrl, .is_dag, .is_conj, .is_trans is true, or if .nbctrls is a strict positive integer.
arity (int): an integer representing the number of qubits on which this gate can be applied
syntax (optional): the syntax of the gate (if any). A syntax is defined with the following attributes:
name (str): name of the gate (e.g. “H”, “RZ”, etc.)
parameters (list): parameters used to build the gate
circuit_implementation (optional): if the gate has an implementation in the form of a subroutine, this attribute contains the subcircuit corresponding to the gate. Definitions of gates generated with an
AbstractGate
may have this attribute defined. A circuit implementation is defined by the following attributes:
ops (list): list of
Op
ancillas (int): number of ancillas
nbqbits (int): number of qubits used by the subroutine
There are three different ways to define the implementation of a gate:
Using a matrix
The definition of a gate is given by a matrix. The attribute
matrix
ofGateDefinition
will contain the matrix.from qat.lang.AQASM import Program, H # Create a circuit prog = Program() qbit = prog.qalloc(1) prog.apply(H, qbit) circ = prog.to_circ() # Extract the definition of the gate H print(f"The ID of the first gate is {circ.ops[0].gate}") print(circ.gateDic["H"].matrix)The ID of the first gate is H Matrix(nRows=2, nCols=2, data=[ComplexNumber(re=0.7071067811865475, im=0.0), ComplexNumber(re=0.7071067811865475, im=0.0), ComplexNumber(re=0.7071067811865475, im=0.0), ComplexNumber(re=-0.7071067811865475, im=0.0)])
Using a subgate
The definition of a gate is given by a subgate and a transformation. The attribute
subgate
ofGateDefinition
will contain the name of the subgate.from qat.lang.AQASM import Program, H # Create a circuit prog = Program() qbits = prog.qalloc(2) prog.apply(H.ctrl(), qbits) circ = prog.to_circ() # Extract the definition of the gate C-H print(f"The ID of the first gate is {circ.ops[0].gate}") definition = circ.gateDic["_0"] print(f"This gate '_0' controls {definition.nbctrls} time the gate {definition.subgate}")The ID of the first gate is _0 This gate '_0' controls 1 time the gate H
Using a circuit implementation
The definition of a gate is given by a circuit implementation. The attribute
circuit_implementation
ofGateDefinition
contains the definition of the gate.from qat.lang.AQASM import Program from qat.lang.AQASM.qftarith import QFT # Create a circuit prog = Program() qbits = prog.qalloc(2) prog.apply(QFT(2), qbits) circ = prog.to_circ() # Extract the definition of QFT(2) print(f"The circuit is composed of {len(circ.ops)} gate") print("The definition of the gate is given by the subcircuit:") print(circ.gateDic["_0"].circuit_implementation.ops)The circuit is composed of 1 gate The definition of the gate is given by the subcircuit: [Op(gate='H', qbits=[0], type=0, cbits=None, formula=None, remap=None), Op(gate='_2', qbits=[1, 0], type=0, cbits=None, formula=None, remap=None), Op(gate='H', qbits=[1], type=0, cbits=None, formula=None, remap=None)]
Exceptions
All exceptions raised by QPUs and Plugins are Thrift exceptions. This is particularly usefull when using a remote QPU/Plugin, since it allows the server to cleanly catch the exception and transmit it to the client. Upon receiving the exception, the client will raise it, thus emulating a ‘local’ behavior.
Toggle to get the documentation of exceptions classes
- class qat.comm.exceptions.ttypes.QPUException
A QPU is supposed to raise an exception of type
QPUException
- Parameters:
code (int): error code
modulename (str, optional): module in which the exception is raised
message (str, optional): error message
file (str, optional): file in which the exception is raised
line (int, optional): line index
- class qat.comm.exceptions.ttypes.PluginException
A Plugin is supposed to raise an exception of type
PluginException
- Parameters:
code (int): error code
modulename (str, optional): module in which the exception is raised
message (str, optional): error message
file (str, optional): file in which the exception is raised
line (int, optional): line index
QPUs will raise exceptions called QPUException
,
while Plugins will raise PluginException
.
Usually, the exception will contain a message that should be clear enough for you to understand what went wrong.
Some additional information is packed inside the exception, taking the form of a file name and a line number.
Toggle to get the list of error codes
Each error code is defined in the enumeration
ErrorType
. This enumeration is composed of the following items:
Error ABORT (code \(1\)) raised when the execution is stopped
Error INVALID_ARGS (code \(2\)) raised when arguments are invalid
Error NONRESULT (code \(5\)) raised when the result is not available
Error BREAK (code \(10\)) raised by a BREAK gate in the circuit
Error ILLEGAL_GATES (code \(11\)) raised when the circuit contains a gate unknown for the QPU
Error NBQBITS (code \(12\)) raised when the number of qubits composing the circuit is not compatible with the QPU
Error NBCBITS (code \(13\)) raised when the number of cbits composing the circuit is not compatible with the QPU
Error NOT_SIMULATABLE (code \(14\)) raised when a job is not simulatable
- class qat.comm.exceptions.ttypes.ErrorType
Enumeration containing all the defined error code
from qat.comm.exceptions.ttypes import ErrorType abort_code = ErrorType.ABORTThe error codes are:
ABORT = 1
INVALID_ARGS = 3
NONRESULT = 5
BREAK = 10
ILLEGAL_GATES = 11
NBQBITS = 12
NBCBITS = 13
NOT_SIMULATABLE = 14
Additionally, exceptions come with an error code that characterizes the type of error that appeared inside the Plugin/QPU:
from qat.lang.AQASM import Program, RZ
from qat.qpus import PyLinalg
from qat.comm.exceptions.ttypes import QPUException
prog = Program()
qbits = prog.qalloc(1)
prog.apply(RZ(0.4), qbits)
circuit = prog.to_circ(include_matrices=False)
job = circuit.to_job()
try:
result = PyLinalg().submit(job)
except QPUException as excp:
print(excp)
QPUException(code=11, modulename='qat.pylinalg', message='Gate RZ has no matrix!', file='qat/pylinalg/simulator.py', line=103)
Here code \(14\) means that the simulator encountered a non supported gate (here a gate with no matrix).
Another useful code is the one raised when a break instruction is triggered:
from qat.lang.AQASM import Program
from qat.qpus import PyLinalg
from qat.comm.exceptions.ttypes import QPUException
prog = Program()
qbits = prog.qalloc(1)
cbits = prog.calloc(1)
prog.measure(qbits[0], cbits[0])
prog.cbreak(~cbits[0])
circuit = prog.to_circ()
job = circuit.to_job()
try:
result = PyLinalg().submit(job)
except QPUException as excp:
print(excp)
QPUException(code=10, modulename='qat.pylinalg', message='BREAK at gate #1 : formula : NOT 0, cbits : [(0, False)]', file=None, line=None)
Code \(10\) will always refer to a triggered break instruction.