Skip to content

Commit 104f04a

Browse files
mtreinisheliarbeljakelishmanjlapeyrekevinhartman
authored andcommitted
Add infrastructure for gates, instruction, and operations in Rust (Qiskit#12459)
* 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 * Fix Python->Rust Param conversion This commit adds a custom implementation of the FromPyObject trait for the Param enum. Previously, the Param trait derived it's impl of the trait, but this logic wasn't perfect. In cases whern a ParameterExpression was effectively a constant (such as `0 * x`) the trait's attempt to coerce to a float first would result in those ParameterExpressions being dropped from the circuit at insertion time. This was a change in behavior from before having gates in Rust as the parameters would disappear from the circuit at insertion time instead of at bind time. This commit fixes this by having a custom impl for FromPyObject that first tries to figure out if the parameter is a ParameterExpression (or a QuantumCircuit) by using a Python isinstance() check, then tries to extract it as a float, and finally stores a non-parameter object; which is a new variant in the Param enum. This new variant also lets us simplify the logic around adding gates to the parameter table as we're able to know ahead of time which gate parameters are `ParameterExpression`s and which are other objects (and don't need to be tracked in the parameter table. Additionally this commit tweaks two tests, the first is test.python.circuit.library.test_nlocal.TestNLocal.test_parameters_setter which was adjusted in the previous commit to workaround the bug fixed by this commit. The second is test.python.circuit.test_parameters which was testing that a bound ParameterExpression with a value of 0 defaults to an int which was a side effect of passing an int input to symengine for the bind value and not part of the api and didn't need to be checked. This assertion was removed from the test because the rust representation is only storing f64 values for the numeric parameters and it is never an int after binding from the Python perspective it isn't any different to have float(0) and int(0) unless you explicit isinstance check like the test previously was. * Fix qasm3 exporter for std gates without stdgates.inc This commit fixes the handling of standard gates in Qiskit when the user specifies excluding the use of the stdgates.inc file from the exported qasm. Previously the object id of the standard gates were used to maintain a lookup table of the global definitions for all the standard gates explicitly in the file. However, the rust refactor means that every time the exporter accesses `circuit.data[x].operation` a new instance is returned. This means that on subsequent lookups for the definition the gate definitions are never found. To correct this issue this commit adds to the lookup table a fallback of the gate name + parameters to do the lookup for. This should be unique for any standard gate and not interfere with the previous logic that's still in place and functional for other custom gate definitions. While this fixes the logic in the exporter the test is still failing because the test is asserting the object ids are the same in the qasm3 file, which isn't the case anymore. The test will be updated in a subsequent commit to validate the qasm3 file is correct without using a hardcoded object id. * Fix base scheduler analysis pass duration setting When ALAPScheduleAnalysis and ASAPScheduleAnalysis were setting the duration of a gate they were doing `node.op.duration = duration` this wasn't always working because if `node.op` was a standard gate it returned a new Python object created from the underlying rust representation. This commit fixes the passes so that they modify the duration and then explicit set the operation to update it's rust representation. * Fix python lint * Fix last failing qasm3 test for std gates without stdgates.inc While the logic for the qasm3 exporter was fixed in commit a6e69ba to handle the edge case of a user specifying that the qasm exporter does not use the stdgates.inc include file in the output, but also has qiskit's standard gates in their circuit being exported. The one unit test to provide coverage for that scenario was not passing because when an id was used for the gate definitions in the qasm3 file it was being referenced against a temporary created by accessing a standard gate from the circuit and the ids weren't the same so the reference string didn't match what the exporter generated. This commit fixes this by changing the test to not do an exact string comparison, but instead a line by line comparison that either does exact equality check or a regex search for the expected line and the ids are checked as being any 15 character integer. * Remove superfluous comment * Cache imported classes with GILOnceCell * Remove unused python variables * Add missing file * Update QuantumCircuit gate methods to bypass Python object This commit updates the QuantumCircuit gate methods which add a given gate to the circuit to bypass the python gate object creation and directly insert a rust representation of the gate. This avoids a conversion in the rust side of the code. While in practice this is just the Python side object creation and a getattr for the rust code to determine it's a standard gate that we're skipping. This may add up over time if there are a lot of gates being created by the method. To accomplish this the rust code handling the mapping of rust StandardGate variants to the Python classes that represent those gates needed to be updated as well. By bypassing the python object creation we need a fallback to populate the gate class for when a user access the operation object from Python. Previously this mapping was only being populated at insertion time and if we never insert the python object (for a circuit created only via the methods) then we need a way to find what the gate class is. A static lookup table of import paths and class names are added to `qiskit_circuit::imports` module to faciliate this and helper functions are added to facilitate interacting with the class objects that represent each gate. * Deduplicate gate matrix definitions * Fix lint * Attempt to fix qasm3 test failure * Add compile time option to cache py gate returns for rust std gates This commit adds a new rust crate feature flag for the qiskit-circuits and qiskit-pyext that enables caching the output from CircuitInstruction.operation to python space. Previously, for memory efficiency we were reconstructing the python object on demand for every access. This was to avoid carrying around an extra pointer and keeping the ephemeral python object around longer term if it's only needed once. But right now nothing is directly using the rust representation yet and everything is accessing via the python interface, so recreating gate objects on the fly has a huge performance penalty. To avoid that this adds caching by default as a temporary solution to avoid this until we have more usage of the rust representation of gates. There is an inherent tension between an optimal rust representation and something that is performant for Python access and there isn't a clear cut answer on which one is better to optimize for. A build time feature lets the user pick, if what we settle on for the default doesn't agree with their priorities or use case. Personally I'd like to see us disable the caching longer term (hopefully before releasing this functionality), but that's dependent on a sufficent level of usage from rust superseding the current Python space usage in the core of Qiskit. * Add num_nonlocal_gates implementation in rust This commit adds a native rust implementation to rust for the num_nonlocal_gates method on QuantumCircuit. Now that we have a rust representation of gates it is potentially faster to do the count because the iteration and filtering is done rust side. * Performance tuning circuit construction This commit fixes some performance issues with the addition of standard gates to a circuit. To workaround potential reference cycles in Python when calling rust we need to check the parameters of the operation. This was causing our fast path for standard gates to access the `operation` attribute to get the parameters. This causes the gate to be eagerly constructed on the getter. However, the reference cycle case can only happen in situations without a standard gate, and the fast path for adding standard gates directly won't need to run this so a skip is added if we're adding a standard gate. * Add back validation of parameters on gate methods In the previous commit a side effect of the accidental eager operation creation was that the parameter input for gates were being validated by that. By fixing that in the previous commit the validation of input parameters on the circuit methods was broken. This commit fixes that oversight and adds back the validation. * Skip validation on gate creation from rust * Offload operation copying to rust This commit fixes a performance regression in the `QuantumCircuit.copy()` method which was previously using Python to copy the operations which had extra overhead to go from rust to python and vice versa. This moves that logic to exist in rust and improve the copy performance. * Fix lint * Perform deepcopy in rust This commit moves the deepcopy handling to occur solely in Rust. Previously each instruction would be directly deepcopied by iterating over the circuit data. However, we can do this rust side now and doing this is more efficient because while we need to rely on Python to run a deepcopy we can skip it for the Rust standard gates and rely on Rust to copy those gates. * Fix QuantumCircuit.compose() performance regression This commit fixes a performance regression in the compose() method. This was caused by the checking for classical conditions in the method requiring eagerly converting all standard gates to a Python object. This changes the logic to do this only if we know we have a condition (which we can determine Python side now). * Fix map_ops test case with no caching case * Fix typos in docs This commit fixes several docs typos that were caught during code review. Co-authored-by: Eli Arbel <46826214+eliarbel@users.noreply.github.com> * Shrink memory usage for extra mutable instruction state This commit changes how we store the extra mutable instruction state (condition, duration, unit, and label) for each `CircuitInstruction` and `PackedInstruction` in the circuit. Previously it was all stored as separate `Option<T>` fields on the struct, which required at least a pointer's width for each field which was wasted space the majority of the time as using these fields are not common. To optimize the memory layout of the struct this moves these attributes to a new struct which is put in an `Option<Box<_>>` which reduces it from 4 pointer widths down to 1 per object. This comes from extra runtime cost from the extra layer of pointer indirection but as this is the uncommon path this tradeoff is fine. * Remove Option<> from params field in CircuitInstruction This commit removes the Option<> from the params field in CircuitInstruction. There is no real distinction between an empty vec and None in this case, so the option just added another layer in the API that we didn't need to deal with. Also depending on the memory alignment using an Option<T> might have ended up in a little extra memory usage too, so removing it removes that potential source of overhead. * Eagerly construct rust python wrappers in .append() This commit updates the Python code in QuantumCircuit.append() method to eagerly construct the rust wrapper objects for python defined circuit operations. * Simplify code around handling python errors in rust * Revert "Skip validation on gate creation from rust" This reverts commit 2f81bde. The validation skipping was unsound in some cases and could lead to invalid circuit being generated. If we end up needing this as an optimization we can remove this in the future in a follow-up PR that explores this in isolation. * Temporarily use git for qasm3 import In Qiskit/qiskit-qasm3-import#34 the issue we're hitting caused by qiskit-qasm3-import using the private circuit attributes removed in this PR was fixed. This commit temporarily moves to installing it from git so we can fully run CI. When qiskit-qasm3-import is released we should revert this commit. * Fix lint * Fix lint for real (we really need to use a py312 compatible version of pylint) * Fix test failure caused by incorrect lint fix * Relax trait-method typing requirements * Encapsulate `GILOnceCell` initialisers to local logic * Simplify Interface for building circuit of standard gates in rust * Simplify complex64 creation in gate_matrix.rs This just switches Complex64::new(re, im) to be c64(re, im) to reduce the amount of typing. c64 needs to be defined inplace so it can be a const fn. * Simplify initialization of array of elements that are not Copy (Qiskit#28) * Simplify initialization of array of elements that are not Copy * Only generate array when necessary * Fix doc typos Co-authored-by: Kevin Hartman <kevin@hart.mn> * Add conversion trait for OperationType -> OperationInput and simplify CircuitInstruction::replace() * Use destructuring for operation_type_to_py extra attr handling * Simplify trait bounds for map_indices() The map_indices() method previously specified both Iterator and ExactSizeIterator for it's trait bounds, but Iterator is a supertrait of ExactSizeIterator and we don't need to explicitly list both. This commit removes the duplicate trait bound. * Make Qubit and Clbit newtype member public As we start to use Qubit and Clbit for creating circuits from accelerate and other crates in the Qiskit workspace we need to be able to create instances of them. However, the newtype member BitType was not public which prevented creating new Qubits. This commit fixes this by making it public. * Use snakecase for gate matrix names * Remove pointless underscore prefix * Use downcast instead of bound * Rwork _append reference cycle handling This commit reworks the multiple borrow handling in the _append() method to leveraging `Bound.try_borrow()` to return a consistent error message if we're unable to borrow a CircuitInstruction in the rust code meaning there is a cyclical reference in the code. Previously we tried to detect this cycle up-front which added significant overhead for a corner case. * Make CircuitData.global_phase_param_index a class attr * Use &[Param] instead of &SmallVec<..> for operation_type_and_data_to_py * Have get_params_unsorted return a set * Use lookup table for static property methods of StandardGate * Use PyTuple::empty_bound() * Fix lint * Add missing test method docstring * Reuse allocations in parameter table update * Remove unnecessary global phase zeroing * Move manually set params to a separate function * Fix release note typo * Use constant for global-phase index * Switch requirement to release version --------- Co-authored-by: Eli Arbel <46826214+eliarbel@users.noreply.github.com> Co-authored-by: Jake Lishman <jake.lishman@ibm.com> Co-authored-by: John Lapeyre <jlapeyre@users.noreply.github.com> Co-authored-by: Kevin Hartman <kevin@hart.mn>
1 parent 5d7b775 commit 104f04a

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

60 files changed

+3780
-936
lines changed

.github/workflows/tests.yml

+9
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,15 @@ jobs:
3636
python -m pip install -U -r requirements.txt -c constraints.txt
3737
python -m pip install -U -r requirements-dev.txt -c constraints.txt
3838
python -m pip install -c constraints.txt -e .
39+
if: matrix.python-version == '3.10'
40+
env:
41+
QISKIT_NO_CACHE_GATES: 1
42+
- name: 'Install dependencies'
43+
run: |
44+
python -m pip install -U -r requirements.txt -c constraints.txt
45+
python -m pip install -U -r requirements-dev.txt -c constraints.txt
46+
python -m pip install -c constraints.txt -e .
47+
if: matrix.python-version == '3.12'
3948
- name: 'Install optionals'
4049
run: |
4150
python -m pip install -r requirements-optional.txt -c constraints.txt

CONTRIBUTING.md

+12
Original file line numberDiff line numberDiff line change
@@ -135,6 +135,18 @@ Note that in order to run `python setup.py ...` commands you need have build
135135
dependency packages installed in your environment, which are listed in the
136136
`pyproject.toml` file under the `[build-system]` section.
137137

138+
### Compile time options
139+
140+
When building qiskit from source there are options available to control how
141+
Qiskit is built. Right now the only option is if you set the environment
142+
variable `QISKIT_NO_CACHE_GATES=1` this will disable runtime caching of
143+
Python gate objects when accessing them from a `QuantumCircuit` or `DAGCircuit`.
144+
This makes a tradeoff between runtime performance for Python access and memory
145+
overhead. Caching gates will result in better runtime for users of Python at
146+
the cost of increased memory consumption. If you're working with any custom
147+
transpiler passes written in python or are otherwise using a workflow that
148+
repeatedly accesses the `operation` attribute of a `CircuitInstruction` or `op`
149+
attribute of `DAGOpNode` enabling caching is recommended.
138150

139151
## Issues and pull requests
140152

Cargo.lock

+4
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

+5
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,11 @@ license = "Apache-2.0"
1616
[workspace.dependencies]
1717
indexmap.version = "2.2.6"
1818
hashbrown.version = "0.14.0"
19+
num-complex = "0.4"
20+
ndarray = "^0.15.6"
21+
numpy = "0.21.0"
22+
smallvec = "1.13"
23+
1924
# Most of the crates don't need the feature `extension-module`, since only `qiskit-pyext` builds an
2025
# actual C extension (the feature disables linking in `libpython`, which is forbidden in Python
2126
# distributions). We only activate that feature when building the C extension module; we still need

crates/accelerate/Cargo.toml

+4-4
Original file line numberDiff line numberDiff line change
@@ -11,29 +11,29 @@ doctest = false
1111

1212
[dependencies]
1313
rayon = "1.10"
14-
numpy = "0.21.0"
14+
numpy.workspace = true
1515
rand = "0.8"
1616
rand_pcg = "0.3"
1717
rand_distr = "0.4.3"
1818
ahash = "0.8.11"
1919
num-traits = "0.2"
20-
num-complex = "0.4"
20+
num-complex.workspace = true
2121
num-bigint = "0.4"
2222
rustworkx-core = "0.14"
2323
faer = "0.19.0"
2424
itertools = "0.13.0"
2525
qiskit-circuit.workspace = true
2626

2727
[dependencies.smallvec]
28-
version = "1.13"
28+
workspace = true
2929
features = ["union"]
3030

3131
[dependencies.pyo3]
3232
workspace = true
3333
features = ["hashbrown", "indexmap", "num-complex", "num-bigint", "smallvec"]
3434

3535
[dependencies.ndarray]
36-
version = "^0.15.6"
36+
workspace = true
3737
features = ["rayon", "approx-0_5"]
3838

3939
[dependencies.approx]

crates/accelerate/src/isometry.rs

+1-1
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ use itertools::Itertools;
2323
use ndarray::prelude::*;
2424
use numpy::{IntoPyArray, PyReadonlyArray1, PyReadonlyArray2};
2525

26-
use crate::two_qubit_decompose::ONE_QUBIT_IDENTITY;
26+
use qiskit_circuit::gate_matrix::ONE_QUBIT_IDENTITY;
2727

2828
/// Find special unitary matrix that maps [c0,c1] to [r,0] or [0,r] if basis_state=0 or
2929
/// basis_state=1 respectively

crates/accelerate/src/two_qubit_decompose.rs

+8-60
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,7 @@ use rand::prelude::*;
5151
use rand_distr::StandardNormal;
5252
use rand_pcg::Pcg64Mcg;
5353

54+
use qiskit_circuit::gate_matrix::{CX_GATE, H_GATE, ONE_QUBIT_IDENTITY, SX_GATE, X_GATE};
5455
use qiskit_circuit::SliceOrInt;
5556

5657
const PI2: f64 = PI / 2.0;
@@ -60,11 +61,6 @@ const TWO_PI: f64 = 2.0 * PI;
6061

6162
const C1: c64 = c64 { re: 1.0, im: 0.0 };
6263

63-
pub static ONE_QUBIT_IDENTITY: [[Complex64; 2]; 2] = [
64-
[Complex64::new(1., 0.), Complex64::new(0., 0.)],
65-
[Complex64::new(0., 0.), Complex64::new(1., 0.)],
66-
];
67-
6864
static B_NON_NORMALIZED: [[Complex64; 4]; 4] = [
6965
[
7066
Complex64::new(1.0, 0.),
@@ -342,54 +338,6 @@ fn rz_matrix(theta: f64) -> Array2<Complex64> {
342338
]
343339
}
344340

345-
static HGATE: [[Complex64; 2]; 2] = [
346-
[
347-
Complex64::new(FRAC_1_SQRT_2, 0.),
348-
Complex64::new(FRAC_1_SQRT_2, 0.),
349-
],
350-
[
351-
Complex64::new(FRAC_1_SQRT_2, 0.),
352-
Complex64::new(-FRAC_1_SQRT_2, 0.),
353-
],
354-
];
355-
356-
static CXGATE: [[Complex64; 4]; 4] = [
357-
[
358-
Complex64::new(1., 0.),
359-
Complex64::new(0., 0.),
360-
Complex64::new(0., 0.),
361-
Complex64::new(0., 0.),
362-
],
363-
[
364-
Complex64::new(0., 0.),
365-
Complex64::new(0., 0.),
366-
Complex64::new(0., 0.),
367-
Complex64::new(1., 0.),
368-
],
369-
[
370-
Complex64::new(0., 0.),
371-
Complex64::new(0., 0.),
372-
Complex64::new(1., 0.),
373-
Complex64::new(0., 0.),
374-
],
375-
[
376-
Complex64::new(0., 0.),
377-
Complex64::new(1., 0.),
378-
Complex64::new(0., 0.),
379-
Complex64::new(0., 0.),
380-
],
381-
];
382-
383-
static SXGATE: [[Complex64; 2]; 2] = [
384-
[Complex64::new(0.5, 0.5), Complex64::new(0.5, -0.5)],
385-
[Complex64::new(0.5, -0.5), Complex64::new(0.5, 0.5)],
386-
];
387-
388-
static XGATE: [[Complex64; 2]; 2] = [
389-
[Complex64::new(0., 0.), Complex64::new(1., 0.)],
390-
[Complex64::new(1., 0.), Complex64::new(0., 0.)],
391-
];
392-
393341
fn compute_unitary(sequence: &TwoQubitSequenceVec, global_phase: f64) -> Array2<Complex64> {
394342
let identity = aview2(&ONE_QUBIT_IDENTITY);
395343
let phase = Complex64::new(0., global_phase).exp();
@@ -402,10 +350,10 @@ fn compute_unitary(sequence: &TwoQubitSequenceVec, global_phase: f64) -> Array2<
402350
// sequence. If we get a different gate this is getting called
403351
// by something else and is invalid.
404352
let gate_matrix = match inst.0.as_ref() {
405-
"sx" => aview2(&SXGATE).to_owned(),
353+
"sx" => aview2(&SX_GATE).to_owned(),
406354
"rz" => rz_matrix(inst.1[0]),
407-
"cx" => aview2(&CXGATE).to_owned(),
408-
"x" => aview2(&XGATE).to_owned(),
355+
"cx" => aview2(&CX_GATE).to_owned(),
356+
"x" => aview2(&X_GATE).to_owned(),
409357
_ => unreachable!("Undefined gate"),
410358
};
411359
(gate_matrix, &inst.2)
@@ -1481,15 +1429,15 @@ impl TwoQubitBasisDecomposer {
14811429
} else {
14821430
euler_matrix_q0 = rz_matrix(euler_q0[0][2] + euler_q0[1][0]).dot(&euler_matrix_q0);
14831431
}
1484-
euler_matrix_q0 = aview2(&HGATE).dot(&euler_matrix_q0);
1432+
euler_matrix_q0 = aview2(&H_GATE).dot(&euler_matrix_q0);
14851433
self.append_1q_sequence(&mut gates, &mut global_phase, euler_matrix_q0.view(), 0);
14861434

14871435
let rx_0 = rx_matrix(euler_q1[0][0]);
14881436
let rz = rz_matrix(euler_q1[0][1]);
14891437
let rx_1 = rx_matrix(euler_q1[0][2] + euler_q1[1][0]);
14901438
let mut euler_matrix_q1 = rz.dot(&rx_0);
14911439
euler_matrix_q1 = rx_1.dot(&euler_matrix_q1);
1492-
euler_matrix_q1 = aview2(&HGATE).dot(&euler_matrix_q1);
1440+
euler_matrix_q1 = aview2(&H_GATE).dot(&euler_matrix_q1);
14931441
self.append_1q_sequence(&mut gates, &mut global_phase, euler_matrix_q1.view(), 1);
14941442

14951443
gates.push(("cx".to_string(), smallvec![], smallvec![1, 0]));
@@ -1550,12 +1498,12 @@ impl TwoQubitBasisDecomposer {
15501498
return None;
15511499
}
15521500
gates.push(("cx".to_string(), smallvec![], smallvec![1, 0]));
1553-
let mut euler_matrix = rz_matrix(euler_q0[2][2] + euler_q0[3][0]).dot(&aview2(&HGATE));
1501+
let mut euler_matrix = rz_matrix(euler_q0[2][2] + euler_q0[3][0]).dot(&aview2(&H_GATE));
15541502
euler_matrix = rx_matrix(euler_q0[3][1]).dot(&euler_matrix);
15551503
euler_matrix = rz_matrix(euler_q0[3][2]).dot(&euler_matrix);
15561504
self.append_1q_sequence(&mut gates, &mut global_phase, euler_matrix.view(), 0);
15571505

1558-
let mut euler_matrix = rx_matrix(euler_q1[2][2] + euler_q1[3][0]).dot(&aview2(&HGATE));
1506+
let mut euler_matrix = rx_matrix(euler_q1[2][2] + euler_q1[3][0]).dot(&aview2(&H_GATE));
15591507
euler_matrix = rz_matrix(euler_q1[3][1]).dot(&euler_matrix);
15601508
euler_matrix = rx_matrix(euler_q1[3][2]).dot(&euler_matrix);
15611509
self.append_1q_sequence(&mut gates, &mut global_phase, euler_matrix.view(), 1);

crates/circuit/Cargo.toml

+14-1
Original file line numberDiff line numberDiff line change
@@ -11,4 +11,17 @@ doctest = false
1111

1212
[dependencies]
1313
hashbrown.workspace = true
14-
pyo3.workspace = true
14+
num-complex.workspace = true
15+
ndarray.workspace = true
16+
numpy.workspace = true
17+
18+
[dependencies.pyo3]
19+
workspace = true
20+
features = ["hashbrown", "indexmap", "num-complex", "num-bigint", "smallvec"]
21+
22+
[dependencies.smallvec]
23+
workspace = true
24+
features = ["union"]
25+
26+
[features]
27+
cache_pygates = []

crates/circuit/README.md

+63
Original file line numberDiff line numberDiff line change
@@ -4,3 +4,66 @@ The Rust-based data structures for circuits.
44
This currently defines the core data collections for `QuantumCircuit`, but may expand in the future to back `DAGCircuit` as well.
55

66
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 `ParameterExpression` 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 enumerates 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 defines 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+
## ParamTable
60+
61+
The `ParamTable` 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 `ParamEntry`, the `name` to the uuid, and the uuid to the PyObject for the actual `Parameter`.
66+
67+
The `ParamEntry` 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 `CircuitInstruction.params` field of
68+
a give instruction where the given `Parameter` is used in the circuit. If the instruction index is
69+
`GLOBAL_PHASE_MAX`, that points to the global phase property of the circuit instead of a `CircuitInstruction`.

crates/circuit/src/bit_data.rs

+11-2
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@
1212

1313
use crate::BitType;
1414
use hashbrown::HashMap;
15-
use pyo3::exceptions::{PyRuntimeError, PyValueError};
15+
use pyo3::exceptions::{PyKeyError, PyRuntimeError, PyValueError};
1616
use pyo3::prelude::*;
1717
use pyo3::types::PyList;
1818
use std::fmt::Debug;
@@ -83,6 +83,15 @@ pub(crate) struct BitData<T> {
8383

8484
pub(crate) struct BitNotFoundError<'py>(pub(crate) Bound<'py, PyAny>);
8585

86+
impl<'py> From<BitNotFoundError<'py>> for PyErr {
87+
fn from(error: BitNotFoundError) -> Self {
88+
PyKeyError::new_err(format!(
89+
"Bit {:?} has not been added to this circuit.",
90+
error.0
91+
))
92+
}
93+
}
94+
8695
impl<T> BitData<T>
8796
where
8897
T: From<BitType> + Copy,
@@ -142,7 +151,7 @@ where
142151
/// Map the provided native indices to the corresponding Python
143152
/// bit instances.
144153
/// Panics if any of the indices are out of range.
145-
pub fn map_indices(&self, bits: &[T]) -> impl Iterator<Item = &Py<PyAny>> + ExactSizeIterator {
154+
pub fn map_indices(&self, bits: &[T]) -> impl ExactSizeIterator<Item = &Py<PyAny>> {
146155
let v: Vec<_> = bits.iter().map(|i| self.get(*i).unwrap()).collect();
147156
v.into_iter()
148157
}

0 commit comments

Comments
 (0)