Architecture and data structures

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 requiering 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)

Returned Value

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

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.ABORT

The 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.

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:

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__ operator

  • tensor product using the overloaded __mult__ operator

  • serialization/deserialization using the dump() and load() methods

  • abstract variables binding using the bind_variables() method

  • easy 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

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 (int): unique identifyer of the gate (if operator is a gate)

  • qbits (list): indices of qubits on which the operator acts

  • OpType (enum type): type of operator (gate, measure…)

  • cbits (list): indices of classical bits in which to store output (if applicable)

  • formula (str): a string in RPF representing a boolean formula whose leaves are cbit indexes (for resets, etc.)

  • remap (list): 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 the qat.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 of GateDefinition 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 of GateDefinition 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 of GateDefinition 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)]