Skip to content

Commit 50c18fa

Browse files
authored
Invert qiskit-quantum-info and qiskit-circuit dependency (#15915)
This moves the last remaining dependency from `qiskit-quantum-info` out of that package and into `qiskit-circuit` itself; rather than `VersorU2` consuming the `StandardGate` to construct itself, we instead have the `StandardGate` learn how to construct `VersorU2` from itself. This allows us to invert the dependency, and put `qiskit-quantum-info` below `qiskit-circuit` in the crate hierarchy, opening up the ability to use `qiskit-quantum-info` types natively within core circuit IR types, and to re-use `quantum-info` routines around conversion of matrix-free objects to matrix form, and so on.
1 parent c33a092 commit 50c18fa

7 files changed

Lines changed: 167 additions & 143 deletions

File tree

Cargo.lock

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

crates/circuit/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ workspace = true
1414

1515
[dependencies]
1616
qiskit-util = { workspace = true, features = ["py"] }
17+
qiskit-quantum-info.workspace = true
1718
rayon.workspace = true
1819
ahash.workspace = true
1920
rustworkx-core.workspace = true
Lines changed: 145 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,145 @@
1+
// This code is part of Qiskit.
2+
//
3+
// (C) Copyright IBM 2026
4+
//
5+
// This code is licensed under the Apache License, Version 2.0. You may
6+
// obtain a copy of this license in the LICENSE.txt file in the root directory
7+
// of this source tree or at https://www.apache.org/licenses/LICENSE-2.0.
8+
//
9+
// Any modifications or derivative works of this code must retain this
10+
// copyright notice, and modified files need to carry a notice indicating
11+
// that they have been altered from the originals.
12+
13+
use super::StandardGate;
14+
use crate::operations::{Operation, Param};
15+
use qiskit_quantum_info::versor_u2::{VersorSU2, VersorU2, VersorU2Error};
16+
17+
use nalgebra::{Quaternion, Unit};
18+
use std::f64::consts::{FRAC_1_SQRT_2, FRAC_PI_2, FRAC_PI_4, FRAC_PI_8};
19+
20+
const COS_FRAC_PI_8: f64 = 0.9238795325112867;
21+
const SIN_FRAC_PI_8: f64 = 0.3826834323650898;
22+
23+
/// Conversion logic of `StandardGate::versor_u2`.
24+
pub fn versor_u2(gate: StandardGate, params: &[Param]) -> Result<VersorU2, VersorU2Error> {
25+
debug_assert_eq!(params.len(), gate.num_params() as usize);
26+
match gate {
27+
StandardGate::GlobalPhase => {
28+
let &[Param::Float(phase)] = params else {
29+
return Err(VersorU2Error::Symbolic);
30+
};
31+
Ok(VersorSU2::identity().with_phase(phase))
32+
}
33+
StandardGate::H => {
34+
Ok(
35+
VersorSU2::from_quaternion_unchecked(0., -FRAC_1_SQRT_2, 0., -FRAC_1_SQRT_2)
36+
.with_phase(FRAC_PI_2),
37+
)
38+
}
39+
StandardGate::I => Ok(VersorU2::identity()),
40+
StandardGate::X => {
41+
Ok(VersorSU2::from_quaternion_unchecked(0., 0., 0., -1.).with_phase(FRAC_PI_2))
42+
}
43+
StandardGate::Y => {
44+
Ok(VersorSU2::from_quaternion_unchecked(0., 0., -1., 0.).with_phase(FRAC_PI_2))
45+
}
46+
StandardGate::Z => {
47+
Ok(VersorSU2::from_quaternion_unchecked(0., -1., 0., 0.).with_phase(FRAC_PI_2))
48+
}
49+
StandardGate::Phase | StandardGate::U1 => {
50+
let &[Param::Float(angle)] = params else {
51+
return Err(VersorU2Error::Symbolic);
52+
};
53+
Ok(VersorSU2::from_rz(angle).with_phase(angle * 0.5))
54+
}
55+
StandardGate::R => {
56+
let &[Param::Float(angle), Param::Float(axis)] = params else {
57+
return Err(VersorU2Error::Symbolic);
58+
};
59+
let (sin_angle, cos_angle) = (angle * 0.5).sin_cos();
60+
let (sin_axis, cos_axis) = axis.sin_cos();
61+
Ok(VersorSU2::from_quaternion_unchecked(
62+
cos_angle,
63+
0.,
64+
-sin_axis * sin_angle,
65+
-cos_axis * sin_angle,
66+
)
67+
.into())
68+
}
69+
StandardGate::RX => {
70+
let &[Param::Float(angle)] = params else {
71+
return Err(VersorU2Error::Symbolic);
72+
};
73+
Ok(VersorSU2::from_rx(angle).into())
74+
}
75+
StandardGate::RY => {
76+
let &[Param::Float(angle)] = params else {
77+
return Err(VersorU2Error::Symbolic);
78+
};
79+
Ok(VersorSU2::from_ry(angle).into())
80+
}
81+
StandardGate::RZ => {
82+
let &[Param::Float(angle)] = params else {
83+
return Err(VersorU2Error::Symbolic);
84+
};
85+
Ok(VersorSU2::from_rz(angle).into())
86+
}
87+
StandardGate::S => {
88+
Ok(
89+
VersorSU2::from_quaternion_unchecked(FRAC_1_SQRT_2, -FRAC_1_SQRT_2, 0., 0.)
90+
.with_phase(FRAC_PI_4),
91+
)
92+
}
93+
StandardGate::Sdg => {
94+
Ok(
95+
VersorSU2::from_quaternion_unchecked(FRAC_1_SQRT_2, FRAC_1_SQRT_2, 0., 0.)
96+
.with_phase(-FRAC_PI_4),
97+
)
98+
}
99+
StandardGate::SX => {
100+
Ok(
101+
VersorSU2::from_quaternion_unchecked(FRAC_1_SQRT_2, 0., 0., -FRAC_1_SQRT_2)
102+
.with_phase(FRAC_PI_4),
103+
)
104+
}
105+
StandardGate::SXdg => {
106+
Ok(
107+
VersorSU2::from_quaternion_unchecked(FRAC_1_SQRT_2, 0., 0., FRAC_1_SQRT_2)
108+
.with_phase(-FRAC_PI_4),
109+
)
110+
}
111+
StandardGate::T => {
112+
Ok(
113+
VersorSU2::from_quaternion_unchecked(COS_FRAC_PI_8, -SIN_FRAC_PI_8, 0., 0.)
114+
.with_phase(FRAC_PI_8),
115+
)
116+
}
117+
StandardGate::Tdg => {
118+
Ok(
119+
VersorSU2::from_quaternion_unchecked(COS_FRAC_PI_8, SIN_FRAC_PI_8, 0., 0.)
120+
.with_phase(-FRAC_PI_8),
121+
)
122+
}
123+
StandardGate::U | StandardGate::U3 => {
124+
let &[Param::Float(theta), Param::Float(phi), Param::Float(lambda)] = params else {
125+
return Err(VersorU2Error::Symbolic);
126+
};
127+
Ok(
128+
(VersorSU2::from_rz(phi) * VersorSU2::from_ry(theta) * VersorSU2::from_rz(lambda))
129+
.with_phase((phi + lambda) * 0.5),
130+
)
131+
}
132+
StandardGate::U2 => {
133+
let &[Param::Float(phi), Param::Float(lambda)] = params else {
134+
return Err(VersorU2Error::Symbolic);
135+
};
136+
let (sin, cos) = (lambda * 0.5).sin_cos();
137+
// The RY(pi/2).RZ(lambda) part of the decomposition, without the phase term.
138+
let ry_rz = VersorSU2(Unit::new_unchecked(
139+
FRAC_1_SQRT_2 * Quaternion::new(cos, -sin, -cos, -sin),
140+
));
141+
Ok((VersorSU2::from_rz(phi) * ry_rz).with_phase((phi + lambda) * 0.5))
142+
}
143+
_ => Err(VersorU2Error::MultiQubit),
144+
}
145+
}

crates/circuit/src/standard_gate/mod.rs

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,9 +10,12 @@
1010
// copyright notice, and modified files need to carry a notice indicating
1111
// that they have been altered from the originals.
1212

13+
mod convert;
14+
1315
use crate::circuit_data::{CircuitData, PyCircuitData};
1416
use crate::operations::{Operation, Param, add_param, clone_param, multiply_param, radd_param};
1517
use crate::{Qubit, gate_matrix, impl_intopyobject_for_copy_pyclass, imports};
18+
use qiskit_quantum_info::versor_u2::{VersorU2, VersorU2Error};
1619

1720
use ndarray::{Array2, aview2};
1821
use num_complex::Complex64;
@@ -1843,6 +1846,13 @@ impl StandardGate {
18431846
Self::RC3X => None,
18441847
}
18451848
}
1849+
1850+
/// Get the versor representation of a 1q [StandardGate] without constructing a matrix.
1851+
///
1852+
/// Returns the error state if `gate` is not 1q, or if any of the parameters are symbolic.
1853+
pub fn versor_u2(&self, params: &[Param]) -> Result<VersorU2, VersorU2Error> {
1854+
convert::versor_u2(*self, params)
1855+
}
18461856
}
18471857

18481858
#[pymethods]

crates/quantum_info/Cargo.toml

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,6 @@ name = "qiskit_quantum_info"
1010
doctest = false
1111

1212
[dependencies]
13-
qiskit-circuit.workspace = true
1413
qiskit-util = { workspace = true, features = ["py"] }
1514
numpy.workspace = true
1615
num-complex.workspace = true
@@ -50,3 +49,6 @@ features = ["hashbrown", "indexmap", "num-complex", "num-bigint", "smallvec"]
5049

5150
[lints]
5251
workspace = true
52+
53+
[dev-dependencies]
54+
qiskit-circuit.workspace = true

0 commit comments

Comments
 (0)