Skip to main contentIBM Quantum Documentation
Important

IBM Quantum Platform is moving and this version will be sunset on July 1. To get started on the new platform, read the migration guide.

Circuit annotations

qiskit.circuit.annotation

This module contains the infrastructure for working with custom circuit annotations.

The main user-facing class is the base class qiskit.circuit.Annotation, which is also re-exported from this module.


Custom annotation subclasses

The Annotation class is intended to be subclassed. Subclasses must set their namespace field. This can be specific to an instance, or static for an entire subclass. The namespace is used as part of the dispatch mechanism, as described in Namespacing.

Circuit equality checks also compare annotations on objects in an order-dependent manner. You will likely want to implement the __eq__() magic method on any subclasses.

If you intend your annotation to be able to be serialized via QPY or :ref:` OpenQASM 3 <qiskit-qasm3>`, you must provide separate implementations of the serialization and deserialization methods as discussed in Serialization and deserialization.

Namespacing

The “namespace” of an annotation is used as a look-up key when any consumer is deciding which handler to invoke. This includes in QPY and OpenQASM 3 serialization contexts, but in general, transpiler passes will also look at annotations’ namespaces to determine if they are relevant, and so on.

This can be standard Python identifier (e.g. my_namespace), or a dot-separated list of identifiers (e.g. my_namespace.subnamespace). The namespace is used by all consumers of annotations to determine what handler should be invoked.

A stand-alone function allows iterating through namespaces and parent namespaces in priority order from most specific to least specific.

iter_namespaces

qiskit.circuit.annotation.iter_namespaces(namespace)

GitHub

An iterator over all namespaces that can be used to lookup the given namespace.

This includes the namespace and all parents, including the root empty-string namespace.

Examples:

from qiskit.circuit.annotation import iter_namespaces
assert list(iter_namespaces("hello.world")) == ["hello.world", "hello", ""]

Parameters

namespace (str) –

Return type

Iterator[str]

Serialization and deserialization

Annotations represent completely custom data, that may persist after compilation. This may include data that should be serialized for later consumption, such as additional data that is interpreted by a backend-compiler. Qiskit’s native binary QPY format (see qiskit.qpy) supports the concept of arbitrary annotations in its payloads from version 15 onwards. In OpenQASM 3 (see qiskit.qasm3), annotations are a core language feature, and Qiskit’s import/export support for OpenQASM 3 includes serialization of annotations.

However, since annotations are generally custom subclasses and unknown to Qiskit, we cannot have built-in support for serialization. On the deserialization front, Qiskit will not, in general, have an existing Annotation object to call deserialization methods from. It is also expected that annotations may relate to some unknown-to-Qiskit shared state within a given circuit context.

For all of these reasons, serialization and deserialization of annotations is handled by custom objects, which must be passed at the interface points of the relevant serialization functions. For example in QPY, the annotation_factories argument in qpy.dump() and qpy.load() are used to pass serializers.

QPYSerializer

class qiskit.circuit.annotation.QPYSerializer

GitHub

Bases: ABC

The interface for serializers and deserializers of Annotation objects to QPY.

For more information on QPY, see qiskit.qpy.

This interface-definition class is designed to be subclassed. The individual methods describe their contracts, and how they will be called.

During QPY serialization and deserialization, the main QPY logic will call a factory function to create instances of subclasses of this class. The return value from a given factory function will be used in either a serialization or deserialization context, but not both.

The structure of calls during serialization of a single circuit is:

  1. many calls to dump_annotation(), which will all share the same namespace argument, which will always be a (non-strict) prefix of all the Annotation objects given.
  2. one call to dump_state().

The general structure of calls during deserialization of a single circuit out of a QPY payload is:

  1. one call to load_state(), passing a namespace (with the same non-strict prefixing behavior as the “serializing” form).
  2. many calls to load_annotation(), corresponding to annotations serialized under that namespace-prefix lookup.

When subclassing this, recall that QPY is intended to have strict backwards-compatibility guarantees, and it is strongly recommended that annotation-serialisation subclasses maintain this. In particular, it is suggested that any non-trivial serializer includes “version” information for the serializer in its total “state” (see dump_state()), and the deserialization should make every effort to support backwards compatibility with previous versions of the same serializer.

QPYFromOpenQASM3Serializer

class qiskit.circuit.annotation.QPYFromOpenQASM3Serializer(inner)

GitHub

Bases: QPYSerializer

An adaptor that converts a OpenQASM3Serializer into a QPYSerializer.

This works because OpenQASM 3 annotation serializers are required to be stateless and return UTF-8-encoded single lines of text, which is a subset of what QPY permits.

Typically you create one of these using the as_qpy() method of an OpenQASM 3 annotation serializer.

Examples:

Instances of this class can be called like a zero-argument function and return themselves. This lets you use them directly as a factory function to the QPY entry points, such as:

import io
from qiskit.circuit import OpenQASM3Serializer, Annotation
from qiskit import qpy
 
class MyAnnotation(Annotation):
    namespace = "my_namespace"
 
class MySerializer(OpenQASM3Serializer):
    def dump(self, annotation):
        if not isinstance(annotation, MyAnnotation):
            return NotImplemented
        return ""
 
    def load(self, namespace, payload):
        assert namespace == "my_namespace"
        assert payload == ""
        return MyAnnotation()
 
qc = QuantumCircuit(2)
with qc.box(annotations=[MyAnnotation()]):
    qc.cx(0, 1)
 
with io.BytesIO() as fptr:
    qpy.dump(fptr, qc, annotation_serializers = {"my_namespace": MySerializer().as_qpy()})

This is safe, without returning separate instances, because the base OpenQASM 3 serializers are necessarily stateless.

Parameters

inner (OpenQASM3Serializer) – the OpenQASM 3 serializer that this is derived from.

OpenQASM3Serializer

class qiskit.circuit.annotation.OpenQASM3Serializer

GitHub

Bases: ABC

The interface for serializers and deserializers of Annotation objects to OpenQASM 3.

For more information on OpenQASM 3 support in Qiskit, see qiskit.qasm3.

This interface-definition class is designed to be subclassed. OpenQASM 3 annotations are stateless within a program, therefore a subclass must not track state.


Examples

A block-collection transpiler pass

A principal goal of the annotation framework is to allow custom analyses and commands to be stored on circuits in an instruction-local manner, either by the user on entry to the compiler, or for one compiler pass to store information for later consumption.

For example, we can write a simple transpiler pass that collects runs of single-qubit operations, and puts each run into a BoxOp, the calculates the total unitary action and attaches it as a custom annotation, so the same analysis does not need to be repeated later, even if the internals of each block are optimized.

from qiskit.circuit import annotation, QuantumCircuit, BoxOp
from qiskit.quantum_info import Operator
from qiskit.transpiler import TransformationPass
 
class PerformsUnitary(annotation.Annotation):
    namespace = "unitary"
    def __init__(self, matrix):
        self.matrix = matrix
 
class Collect1qRuns(TransformationPass):
    def run(self, dag):
        for run in dag.collect_1q_runs():
            block = QuantumCircuit(1)
            for node in run:
                block.append(node.op, [0], [])
            box = BoxOp(block, annotations=[PerformsUnitary(Operator(block).data)])
            dag.replace_block_with_op(run, box, {run[0].qargs[0]: 0})
        return dag

In order to serialize the annotation to OpenQASM 3, we must define custom logic, since the analysis itself is entirely custom. The serialization is separate to the annotation; there may be circumstances in which serialization should be done differently.

import ast
import numpy as np
 
class Serializer(annotation.OpenQASM3Serializer):
    def dump(self, annotation):
        if annotation.namespace != "unitary":
            return NotImplemented
        line = lambda row: "[" + ", ".join(repr(x) for x in row) + "]"
        return "[" + ", ".join(line(row) for row in annotation.matrix.tolist()) + "]"
 
    def load(self, namespace, payload):
        if namespace != "unitary":
            return NotImplemented
        return PerformsUnitary(np.array(ast.literal_eval(payload), dtype=complex))

Finally, this can be put together, showing the output OpenQASM 3.

from qiskit import qasm3
 
qc = QuantumCircuit(3)
qc.s(0)
qc.t(0)
qc.y(1)
qc.x(1)
qc.h(2)
qc.s(2)
collected = Collect1qRuns()(qc)
 
handlers = {"unitary": Serializer()}
dumped = qasm3.dumps(collected, annotation_handlers=handlers)
print(dumped)
OPENQASM 3.0;
include "stdgates.inc";
qubit[3] q;
@unitary[[(1+0j), 0j], [0j, (-0.7071067811865475+0.7071067811865475j)]]
box {
  s q[0];
  t q[0];
}
@unitary[[1j, 0j], [0j, -1j]]
box {
  y q[1];
  x q[1];
}
@unitary[[(0.7071067811865475+0j), (0.7071067811865475+0j)], [0.7071067811865475j, -0.7071067811865475j]]
box {
  h q[2];
  s q[2];
}
Was this page helpful?
Report a bug or request content on GitHub.