Skip to content

Commit f18d505

Browse files
committed
Expose LitinskiTrafo to C
1 parent d001de2 commit f18d505

4 files changed

Lines changed: 312 additions & 0 deletions

File tree

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
// This code is part of Qiskit.
2+
//
3+
// (C) Copyright IBM 2025
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 qiskit_circuit::{circuit_data::CircuitData, dag_circuit::DAGCircuit};
14+
use qiskit_transpiler::passes::run_litinski_transformation;
15+
16+
use crate::pointers::mut_ptr_as_ref;
17+
18+
/// @ingroup QkTranspilerPasses
19+
/// Run the ``LitinskiTransformation`` pass in-place on a circuit.
20+
///
21+
/// This pass commutes all Clifford gates to the end of the circuit, converting Pauli rotation
22+
/// gates into ``QkPauliProductRotation`` gates and measurements into ``QkPauliProductMeasurement``
23+
/// instructions. Note that this pass currently only supports circuits that have ``QkGate_T``,
24+
/// ``QkGate_Tdg`` or ``QkGate_RZ`` gates as non-Cliffords and panics otherwise. The suggested
25+
/// workflow is to first transpile into a Clifford+RZ basis and then call this pass.
26+
///
27+
/// @param circuit A pointer to the circuit on which to run the pass.
28+
/// @param fix_clifford If ``true``, leave the Clifford gates at the end of the circuit. If
29+
/// ``false`` they are omitted.
30+
///
31+
/// # Safety
32+
///
33+
/// Behavior is undefined if ``circuit`` is not valid, non-null pointers to a ``QkCircuit``.
34+
#[unsafe(no_mangle)]
35+
pub unsafe extern "C" fn qk_transpiler_pass_standalone_litinski_transformation(
36+
circuit: *mut CircuitData,
37+
fix_clifford: bool,
38+
) {
39+
// SAFETY: The user guarantees the pointer is safe to read.
40+
let circuit = unsafe { mut_ptr_as_ref(circuit) };
41+
let dag = match DAGCircuit::from_circuit_data(circuit, false, None, None, None, None) {
42+
Ok(dag) => dag,
43+
Err(_) => panic!("Internal Circuit -> DAG conversion failed"),
44+
};
45+
// TODO: Potentially propagate exit code to the user
46+
let maybe_out = run_litinski_transformation(&dag, fix_clifford, false, false)
47+
.expect("Failed running Litinski transformation");
48+
// If a DAG is returned, the circuit has been modified. Else just leave it as is.
49+
if let Some(out) = maybe_out {
50+
*circuit =
51+
CircuitData::from_dag_ref(&out).expect("Internal DAG -> Circuit conversion failed")
52+
};
53+
}

crates/cext/src/transpiler/passes/mod.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ pub mod consolidate_blocks;
1616
pub mod elide_permutations;
1717
pub mod gate_direction;
1818
pub mod inverse_cancellation;
19+
pub mod litinski_transformation;
1920
pub mod optimize_1q_sequences;
2021
pub mod remove_diagonal_gates_before_measure;
2122
pub mod remove_identity_equiv;
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
features_c:
3+
- Added :c:func:`qk_transpiler_pass_standalone_litinski_transformation` to apply a Litinski
4+
transformation to a ``QkCircuit`` in the C API. This is the equivalent of Python's
5+
:class:`.LitinskiTransformation` pass.
Lines changed: 253 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,253 @@
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+
#include "common.h"
14+
#include <complex.h>
15+
#include <math.h>
16+
#include <qiskit.h>
17+
#include <string.h>
18+
19+
static int test_counts(void) {
20+
QkCircuit *circuit = qk_circuit_new(4, 0);
21+
qk_circuit_gate(circuit, QkGate_H, (uint32_t[1]){0}, NULL);
22+
qk_circuit_gate(circuit, QkGate_CX, (uint32_t[2]){0, 1}, NULL);
23+
qk_circuit_gate(circuit, QkGate_T, (uint32_t[1]){1}, NULL);
24+
qk_circuit_gate(circuit, QkGate_CX, (uint32_t[2]){0, 2}, NULL);
25+
qk_circuit_gate(circuit, QkGate_T, (uint32_t[1]){1}, NULL);
26+
qk_circuit_gate(circuit, QkGate_Tdg, (uint32_t[1]){0}, NULL);
27+
qk_circuit_gate(circuit, QkGate_S, (uint32_t[1]){2}, NULL);
28+
qk_circuit_gate(circuit, QkGate_T, (uint32_t[1]){2}, NULL);
29+
30+
qk_transpiler_pass_standalone_litinski_transformation(circuit, false);
31+
int result = Ok;
32+
if (qk_circuit_num_instructions(circuit) != 4) {
33+
result = EqualityError;
34+
goto cleanup;
35+
}
36+
37+
QkOpCounts op_counts = qk_circuit_count_ops(circuit);
38+
for (size_t i = 0; i < op_counts.len; i++) {
39+
if (strcmp(op_counts.data[i].name, "pauli_product_rotation") != 0) {
40+
if (op_counts.data[i].count != 4) {
41+
result = EqualityError;
42+
break;
43+
}
44+
}
45+
}
46+
cleanup:
47+
qk_circuit_free(circuit);
48+
return result;
49+
}
50+
51+
/**
52+
* An enum representing non-identity Paulis.
53+
*/
54+
enum Pauli {
55+
PX,
56+
PY,
57+
PZ,
58+
};
59+
60+
/**
61+
* Helper function checking if an ZX representation matches an expected Pauli product.
62+
*/
63+
int check_paulis(enum Pauli *expected_paulis, bool *x, bool *z, size_t len) {
64+
int result = Ok;
65+
66+
bool expected_z;
67+
bool expected_x;
68+
for (size_t i = 0; i < len; i++) {
69+
switch (expected_paulis[i]) {
70+
case PX:
71+
expected_x = true;
72+
expected_z = false;
73+
break;
74+
case PY:
75+
expected_x = true;
76+
expected_z = true;
77+
break;
78+
case PZ:
79+
expected_x = false;
80+
expected_z = true;
81+
}
82+
if (x[i] != expected_x || z[i] != expected_z) {
83+
printf("Expected (%i, %i) but got (%i, %i)\n", expected_x, expected_z, x[i], z[i]);
84+
result = EqualityError;
85+
break;
86+
}
87+
}
88+
89+
return result;
90+
}
91+
92+
/**
93+
* A helper to check a PPR matches the expectation.
94+
*/
95+
int check_pauli_rotation(QkCircuit *circuit, size_t index, enum Pauli *paulis, uint32_t *qubits,
96+
const size_t len, double expected_angle) {
97+
QkPauliProductRotation ppr;
98+
if (qk_circuit_get_pauli_rotation(circuit, index, &ppr) != QkExitCode_Success) {
99+
return RuntimeError;
100+
}
101+
102+
int result = Ok;
103+
104+
// (1) check the angle matches
105+
double angle = qk_param_as_real(ppr.angle);
106+
if (fabs(angle - expected_angle) > 1e-12) {
107+
printf("Expected angle %f but got %f\n", expected_angle, angle);
108+
result = EqualityError;
109+
goto cleanup;
110+
}
111+
112+
// (2) check the Paulis match
113+
if (ppr.len != len) {
114+
printf("Expected %zu Paulis but got %zu\n", len, ppr.len);
115+
result = EqualityError;
116+
goto cleanup;
117+
}
118+
result = check_paulis(paulis, ppr.x, ppr.z, len);
119+
if (result != Ok)
120+
goto cleanup;
121+
122+
// (3) check the qubits match
123+
QkCircuitInstruction inst;
124+
qk_circuit_get_instruction(circuit, index, &inst);
125+
for (size_t i = 0; i < len; i++) {
126+
if (inst.qubits[i] != qubits[i]) {
127+
result = EqualityError;
128+
goto cleanup_inst;
129+
}
130+
}
131+
132+
cleanup_inst:
133+
qk_circuit_instruction_clear(&inst);
134+
cleanup:
135+
qk_pauli_product_rotation_clear(&ppr);
136+
return result;
137+
}
138+
139+
int check_pauli_measurement(QkCircuit *circuit, size_t index, enum Pauli *paulis, uint32_t *qubits,
140+
const size_t len, uint32_t clbit, bool flip_outcome) {
141+
QkPauliProductMeasurement ppm;
142+
if (qk_circuit_get_pauli_product_measurement(circuit, index, &ppm) != QkExitCode_Success) {
143+
return RuntimeError;
144+
}
145+
146+
int result = Ok;
147+
// (1) check the sign matches
148+
if (flip_outcome != ppm.flip_outcome) {
149+
result = EqualityError;
150+
goto cleanup;
151+
}
152+
153+
// (2) check the Paulis match
154+
if (ppm.len != len) {
155+
printf("Expected %zu Paulis but got %zu\n", len, ppm.len);
156+
result = EqualityError;
157+
goto cleanup;
158+
}
159+
result = check_paulis(paulis, ppm.x, ppm.z, len);
160+
if (result != Ok)
161+
goto cleanup;
162+
163+
// (3) check the qubits and the clbit match
164+
QkCircuitInstruction inst;
165+
qk_circuit_get_instruction(circuit, index, &inst);
166+
if (clbit != inst.clbits[0]) {
167+
result = EqualityError;
168+
goto cleanup_inst;
169+
}
170+
for (size_t i = 0; i < len; i++) {
171+
if (inst.qubits[i] != qubits[i]) {
172+
result = EqualityError;
173+
goto cleanup_inst;
174+
}
175+
}
176+
177+
cleanup_inst:
178+
qk_circuit_instruction_clear(&inst);
179+
180+
cleanup:
181+
qk_pauli_product_measurement_clear(&ppm);
182+
return result;
183+
}
184+
185+
static int test_concrete(void) {
186+
QkCircuit *circuit = qk_circuit_new(4, 4);
187+
qk_circuit_gate(circuit, QkGate_T, (uint32_t[1]){0}, NULL);
188+
qk_circuit_gate(circuit, QkGate_CX, (uint32_t[2]){2, 1}, NULL);
189+
qk_circuit_gate(circuit, QkGate_SXdg, (uint32_t[1]){3}, NULL);
190+
qk_circuit_gate(circuit, QkGate_CX, (uint32_t[2]){1, 0}, NULL);
191+
qk_circuit_gate(circuit, QkGate_SX, (uint32_t[1]){2}, NULL);
192+
qk_circuit_gate(circuit, QkGate_T, (uint32_t[1]){3}, NULL);
193+
qk_circuit_gate(circuit, QkGate_CX, (uint32_t[2]){3, 0}, NULL);
194+
qk_circuit_gate(circuit, QkGate_T, (uint32_t[1]){0}, NULL);
195+
qk_circuit_gate(circuit, QkGate_S, (uint32_t[1]){1}, NULL);
196+
qk_circuit_gate(circuit, QkGate_T, (uint32_t[1]){2}, NULL);
197+
qk_circuit_gate(circuit, QkGate_S, (uint32_t[1]){3}, NULL);
198+
qk_circuit_gate(circuit, QkGate_SXdg, (uint32_t[1]){0}, NULL);
199+
qk_circuit_gate(circuit, QkGate_SX, (uint32_t[1]){1}, NULL);
200+
qk_circuit_gate(circuit, QkGate_SX, (uint32_t[1]){2}, NULL);
201+
qk_circuit_gate(circuit, QkGate_SX, (uint32_t[1]){3}, NULL);
202+
203+
for (uint32_t i = 0; i < 4; i++) {
204+
qk_circuit_measure(circuit, i, i);
205+
}
206+
207+
qk_transpiler_pass_standalone_litinski_transformation(circuit, false);
208+
209+
int result = Ok;
210+
result = check_pauli_rotation(circuit, 0, (enum Pauli[1]){PZ}, (uint32_t[1]){0}, 1, M_PI_4);
211+
if (result != Ok)
212+
goto cleanup;
213+
result =
214+
check_pauli_rotation(circuit, 1, (enum Pauli[2]){PX, PY}, (uint32_t[2]){1, 2}, 2, M_PI_4);
215+
if (result != Ok)
216+
goto cleanup;
217+
result = check_pauli_rotation(circuit, 2, (enum Pauli[1]){PY}, (uint32_t[1]){3}, 1, -M_PI_4);
218+
if (result != Ok)
219+
goto cleanup;
220+
result = check_pauli_rotation(circuit, 3, (enum Pauli[4]){PZ, PZ, PZ, PY},
221+
(uint32_t[4]){0, 1, 2, 3}, 4, -M_PI_4);
222+
if (result != Ok)
223+
goto cleanup;
224+
result = check_pauli_measurement(circuit, 4, (enum Pauli[4]){PY, PZ, PZ, PY},
225+
(uint32_t[4]){0, 1, 2, 3}, 4, 0, false);
226+
if (result != Ok)
227+
goto cleanup;
228+
result = check_pauli_measurement(circuit, 5, (enum Pauli[2]){PX, PX}, (uint32_t[2]){0, 1}, 2, 1,
229+
false);
230+
if (result != Ok)
231+
goto cleanup;
232+
result = check_pauli_measurement(circuit, 6, (enum Pauli[1]){PZ}, (uint32_t[1]){2}, 1, 2, true);
233+
if (result != Ok)
234+
goto cleanup;
235+
result = check_pauli_measurement(circuit, 7, (enum Pauli[2]){PX, PX}, (uint32_t[2]){0, 3}, 2, 3,
236+
false);
237+
238+
cleanup:
239+
qk_circuit_free(circuit);
240+
return result;
241+
}
242+
243+
int test_litinski_transformation(void) {
244+
int num_failed = 0;
245+
246+
num_failed += RUN_TEST(test_counts);
247+
num_failed += RUN_TEST(test_concrete);
248+
249+
fflush(stderr);
250+
fprintf(stderr, "=== Number of failed subtests (Litinski transformation): %i\n", num_failed);
251+
252+
return num_failed;
253+
}

0 commit comments

Comments
 (0)