You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Add infrastructure for gates, instruction, and operations in Rust
This commit adds a native representation of Gates, Instruction, and
Operations to rust's circuit module. At a high level this works by
either wrapping the Python object in a rust wrapper struct that tracks
metadata about the operations (name, num_qubits, etc) and then for other
details it calls back to Python to get dynamic details like the
definition, matrix, etc. For standard library gates like Swap, CX, H,
etc this replaces the on-circuit representation with a new rust enum
StandardGate. The enum representation is much more efficient and has a
minimal memory footprint (just the enum variant and then any parameters
or other mutable state stored in the circuit instruction). All the gate
properties such as the matrix, definiton, name, etc are statically
defined in rust code based on the enum variant (which represents the
gate).
The use of an enum to represent standard gates does mean a change in
what we store on a CircuitInstruction. To represent a standard gate
fully we need to store the mutable properties of the existing Gate class
on the circuit instruction as the gate by itself doesn't contain this
detail. That means, the parameters, label, unit, duration, and condition
are added to the rust side of circuit instrucion. However no Python side
access methods are added for these as they're internal only to the Rust
code. In Qiskit 2.0 to simplify this storage we'll be able to drop, unit,
duration, and condition from the api leaving only label and parameters.
But for right now we're tracking all of the fields.
To facilitate working with circuits and gates full from rust the
setting the `operation` attribute of a `CircuitInstruction` object now
transltates the python object to an internal rust representation.
For standard gates this translates it to the enum form described earlier,
and for other circuit operations 3 new Rust structs: PyGate,
PyInstruction, and PyOperation are used to wrap the underlying Python
object in a Rust api. These structs cache some commonly accessed static
properties of the operation, such as the name, number of qubits, etc.
However for dynamic pieces, such as the definition or matrix, callback
to python to get a rust representation for those.
Similarly whenever the `operation` attribute is accessed from Python
it converts it back to the normal Python object representation. For
standard gates this involves creating a new instance of a Python object
based on it's internal rust representation. For the wrapper structs a
reference to the wrapped PyObject is returned.
To manage the 4 variants of operation (`StandardGate`, `PyGate`,
`PyInstruction`, and `PyOperation`) a new Rust trait `Operation` is
created that defines a standard interface for getting the properties
of a given circuit operation. This common interface is implemented for
the 4 variants as well as the `OperationType` enum which wraps all 4
(and is used as the type for `CircuitInstruction.operation` in the
rust code.
As everything in the `QuantumCircuit` data model is quite coupled moving
the source of truth for the operations to exist in Rust means that more
of the underlying `QuantumCircuit`'s responsibility has to move to Rust
as well. Primarily this involves the `ParameterTable` which was an
internal class for tracking which instructions in the circuit have a
`ParameterExpression` parameter so that when we go to bind parameters we
can lookup which operations need to be updated with the bind value.
Since the representation of those instructions now lives in Rust and
Python only recieves a ephemeral copy of the instructions the
ParameterTable had to be reimplemented in Rust to track the
instructions. This new parameter table maps the Parameter's uuid (as a
u128) as a unique identifier for each parameter and maps this to a
positional index in the circuit data to the underlying instruction using
that parameter. This is a bit different from the Python parameter table
which was mapping a parameter object to the id of the operation object
using that parmaeter. This also leads to a difference in the binding
mechanics as the parameter assignment was done by reference in the old
model, but now we need to update the entire instruction more explicitly
in rust. Additionally, because the global phase of a circuit can be
parameterized the ownership of global phase is moved from Python into
Rust in this commit as well.
After this commit the only properties of a circuit that are not defined
in Rust for the source of truth are the bits (and vars) of the circuit,
and when creating circuits from rust this is what causes a Python
interaction to still be required.
This commit does not translate the full standard library of gates as
that would make the pull request huge, instead this adds the basic
infrastructure for having a more efficient standard gate representation
on circuits. There will be follow up pull requests to add the missing
gates and round out support in rust.
The goal of this pull request is primarily to add the infrastructure for
representing the full circuit model (and dag model in the future) in
rust. By itself this is not expected to improve runtime performance (if
anything it will probably hurt performance because of extra type
conversions) but it is intended to enable writing native circuit
manipulations in Rust, including transpiler passes without needing
involvement from Python. Longer term this should greatly improve the
runtime performance and reduce the memory overhead of Qiskit. But,
this is just an early step towards that goal, and is more about
unlocking the future capability. The next steps after this commit are
to finish migrating the standard gate library and also update the
`QuantumCircuit` methods to better leverage the more complete rust
representation (which should help offset the performance penalty
introduced by this).
Fixes: Qiskit#12205
Copy file name to clipboardexpand all lines: crates/circuit/README.md
+63
Original file line number
Diff line number
Diff line change
@@ -4,3 +4,66 @@ The Rust-based data structures for circuits.
4
4
This currently defines the core data collections for `QuantumCircuit`, but may expand in the future to back `DAGCircuit` as well.
5
5
6
6
This crate is a very low part of the Rust stack, if not the very lowest.
7
+
8
+
The data model exposed by this crate is as follows.
9
+
10
+
## CircuitData
11
+
12
+
The core representation of a quantum circuit in Rust is the `CircuitData` struct. This containts the list
13
+
of instructions that are comprising the circuit. Each element in this list is modeled by a
14
+
`CircuitInstruction` struct. The `CircuitInstruction` contains the operation object and it's operands.
15
+
This includes the parameters and bits. It also contains the potential mutable state of the Operation representation from the legacy Python data model; namely `duration`, `unit`, `condition`, and `label`.
16
+
In the future we'll be able to remove all of that except for label.
17
+
18
+
At rest a `CircuitInstruction` is compacted into a `PackedInstruction` which caches reused qargs
19
+
in the instructions to reduce the memory overhead of `CircuitData`. The `PackedInstruction` objects
20
+
get unpacked back to `CircuitInstruction` when accessed for a more convienent working form.
21
+
22
+
Additionally the `CircuitData` contains a `param_table` field which is used to track parameterized
23
+
instructions that are using python defined `ParmaeterExpression` objects for any parameters and also
24
+
a global phase field which is used to track the global phase of the circuit.
25
+
26
+
## Operation Model
27
+
28
+
In the circuit crate all the operations used in a `CircuitInstruction` are part of the `OperationType`
29
+
enum. The `OperationType` enum has four variants which are used to define the different types of
30
+
operation objects that can be on a circuit:
31
+
32
+
-`StandardGate`: a rust native representation of a member of the Qiskit standard gate library. This is
33
+
an `enum` that enuerates all the gates in the library and statically defines all the gate properties
34
+
except for gates that take parameters,
35
+
-`PyGate`: A struct that wraps a gate outside the standard library defined in Python. This struct wraps
36
+
a `Gate` instance (or subclass) as a `PyObject`. The static properties of this object (such as name,
37
+
number of qubits, etc) are stored in Rust for performance but the dynamic properties such as
38
+
the matrix or definition are accessed by calling back into Python to get them from the stored
39
+
`PyObject`
40
+
-`PyInstruction`: A struct that wraps an instruction defined in Python. This struct wraps an
41
+
`Instruction` instance (or subclass) as a `PyObject`. The static properties of this object (such as
42
+
name, number of qubits, etc) are stored in Rust for performance but the dynamic properties such as
43
+
the definition are accessed by calling back into Python to get them from the stored `PyObject`. As
44
+
the primary difference between `Gate` and `Instruction` in the python data model are that `Gate` is a
45
+
specialized `Instruction` subclass that represents unitary operations the primary difference between
46
+
this and `PyGate` are that `PyInstruction` will always return `None` when it's matrix is accessed.
47
+
-`PyOperation`: A struct that wraps an operation defined in Python. This struct wraps an `Operation`
48
+
instance (or subclass)` as a `PyObject`. The static properties of this object (such as name, number
49
+
of qubits, etc) are stored in Rust for performance. As `Operation` is the base abstract interface
50
+
definition of what can be put on a circuit this is mostly just a container for custom Python objects.
51
+
Anything that's operating on a bare operation will likely need to access it via the `PyObject`
52
+
manually because the interface doesn't define many standard properties outside of what's cached in
53
+
the struct.
54
+
55
+
There is also an `Operation` trait defined which defines the common access pattern interface to these
56
+
4 types along with the `OperationType` parent. This trait defined methods to access the standard data
57
+
model attributes of operations in Qiskit. This includes things like the name, number of qubits, the matrix, the definition, etc.
58
+
59
+
## ParameterTable
60
+
61
+
The `ParameterTable` struct is used to track which circuit instructions are using `ParameterExpression`
62
+
objects for any of their parameters. The Python space `ParameterExpression` is comprised of a symengine
63
+
symbolic expression that defines operations using `Parameter` objects. Each `Parameter` is modeled by
64
+
a uuid and a name to uniquely identify it. The parameter table maps the `Parameter` objects to the
65
+
`CircuitInstruction` in the `CircuitData` that are using them. The `Parameter` comprised of 3 `HashMaps` internally that map the uuid (as `u128`, which is accesible in Python by using `uuid.int`) to the `ParameterEntry`, the `name` to the uuid, and the uuid to the PyObject for the actual `Parameter`.
66
+
67
+
The `ParameterEntry` is just a `HashSet` of 2-tuples with usize elements. The two usizes represent the instruction index in the `CircuitData` and the index of the for the `CircuitInstruction.params` field of
68
+
a give instruction where the given `Parameter` is used in the circuit. If the instruction index is
69
+
`usize::MAX` that points to the global phase property of the circuit instead of a `CircuitInstruction`.
0 commit comments