Skip to content

Commit

Permalink
Follow up on #12327: circuit construction in Rust (#12605)
Browse files Browse the repository at this point in the history
* Follow up on #12327

also port circuit construction to rust and add a reno

* move _get_ordered_swap to Rust only

* drop redundant Ok(expect())

* proper synthesis structure
  • Loading branch information
Cryoris authored Jun 24, 2024
1 parent 87aa89c commit de6c6eb
Show file tree
Hide file tree
Showing 11 changed files with 124 additions and 90 deletions.
2 changes: 1 addition & 1 deletion crates/accelerate/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -23,12 +23,12 @@ pub mod isometry;
pub mod nlayout;
pub mod optimize_1q_gates;
pub mod pauli_exp_val;
pub mod permutation;
pub mod results;
pub mod sabre;
pub mod sampled_exp_val;
pub mod sparse_pauli_op;
pub mod stochastic_swap;
pub mod synthesis;
pub mod two_qubit_decompose;
pub mod uc_gate;
pub mod utils;
Expand Down
22 changes: 22 additions & 0 deletions crates/accelerate/src/synthesis/mod.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
// This code is part of Qiskit.
//
// (C) Copyright IBM 2024
//
// This code is licensed under the Apache License, Version 2.0. You may
// obtain a copy of this license in the LICENSE.txt file in the root directory
// of this source tree or at http://www.apache.org/licenses/LICENSE-2.0.
//
// Any modifications or derivative works of this code must retain this
// copyright notice, and modified files need to carry a notice indicating
// that they have been altered from the originals.

mod permutation;

use pyo3::prelude::*;
use pyo3::wrap_pymodule;

#[pymodule]
pub fn synthesis(m: &Bound<PyModule>) -> PyResult<()> {
m.add_wrapped(wrap_pymodule!(permutation::permutation))?;
Ok(())
}
68 changes: 68 additions & 0 deletions crates/accelerate/src/synthesis/permutation/mod.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
// This code is part of Qiskit.
//
// (C) Copyright IBM 2024
//
// This code is licensed under the Apache License, Version 2.0. You may
// obtain a copy of this license in the LICENSE.txt file in the root directory
// of this source tree or at http://www.apache.org/licenses/LICENSE-2.0.
//
// Any modifications or derivative works of this code must retain this
// copyright notice, and modified files need to carry a notice indicating
// that they have been altered from the originals.

use numpy::PyArrayLike1;
use smallvec::smallvec;

use pyo3::prelude::*;
use pyo3::wrap_pyfunction;

use qiskit_circuit::circuit_data::CircuitData;
use qiskit_circuit::operations::{Param, StandardGate};
use qiskit_circuit::Qubit;

mod utils;

/// Checks whether an array of size N is a permutation of 0, 1, ..., N - 1.
#[pyfunction]
#[pyo3(signature = (pattern))]
pub fn _validate_permutation(py: Python, pattern: PyArrayLike1<i64>) -> PyResult<PyObject> {
let view = pattern.as_array();
utils::validate_permutation(&view)?;
Ok(py.None())
}

/// Finds inverse of a permutation pattern.
#[pyfunction]
#[pyo3(signature = (pattern))]
pub fn _inverse_pattern(py: Python, pattern: PyArrayLike1<i64>) -> PyResult<PyObject> {
let view = pattern.as_array();
let inverse_i64: Vec<i64> = utils::invert(&view).iter().map(|&x| x as i64).collect();
Ok(inverse_i64.to_object(py))
}

#[pyfunction]
#[pyo3(signature = (pattern))]
pub fn _synth_permutation_basic(py: Python, pattern: PyArrayLike1<i64>) -> PyResult<CircuitData> {
let view = pattern.as_array();
let num_qubits = view.len();
CircuitData::from_standard_gates(
py,
num_qubits as u32,
utils::get_ordered_swap(&view).iter().map(|(i, j)| {
(
StandardGate::SwapGate,
smallvec![],
smallvec![Qubit(*i as u32), Qubit(*j as u32)],
)
}),
Param::Float(0.0),
)
}

#[pymodule]
pub fn permutation(m: &Bound<PyModule>) -> PyResult<()> {
m.add_function(wrap_pyfunction!(_validate_permutation, m)?)?;
m.add_function(wrap_pyfunction!(_inverse_pattern, m)?)?;
m.add_function(wrap_pyfunction!(_synth_permutation_basic, m)?)?;
Ok(())
}
Original file line number Diff line number Diff line change
Expand Up @@ -11,12 +11,11 @@
// that they have been altered from the originals.

use ndarray::{Array1, ArrayView1};
use numpy::PyArrayLike1;
use pyo3::exceptions::PyValueError;
use pyo3::prelude::*;
use std::vec::Vec;

fn validate_permutation(pattern: &ArrayView1<i64>) -> PyResult<()> {
pub fn validate_permutation(pattern: &ArrayView1<i64>) -> PyResult<()> {
let n = pattern.len();
let mut seen: Vec<bool> = vec![false; n];

Expand Down Expand Up @@ -47,15 +46,24 @@ fn validate_permutation(pattern: &ArrayView1<i64>) -> PyResult<()> {
Ok(())
}

fn invert(pattern: &ArrayView1<i64>) -> Array1<usize> {
pub fn invert(pattern: &ArrayView1<i64>) -> Array1<usize> {
let mut inverse: Array1<usize> = Array1::zeros(pattern.len());
pattern.iter().enumerate().for_each(|(ii, &jj)| {
inverse[jj as usize] = ii;
});
inverse
}

fn get_ordered_swap(pattern: &ArrayView1<i64>) -> Vec<(i64, i64)> {
/// Sorts the input permutation by iterating through the permutation list
/// and putting each element to its correct position via a SWAP (if it's not
/// at the correct position already). If ``n`` is the length of the input
/// permutation, this requires at most ``n`` SWAPs.
///
/// More precisely, if the input permutation is a cycle of length ``m``,
/// then this creates a quantum circuit with ``m-1`` SWAPs (and of depth ``m-1``);
/// if the input permutation consists of several disjoint cycles, then each cycle
/// is essentially treated independently.
pub fn get_ordered_swap(pattern: &ArrayView1<i64>) -> Vec<(i64, i64)> {
let mut permutation: Vec<usize> = pattern.iter().map(|&x| x as usize).collect();
let mut index_map = invert(pattern);

Expand All @@ -76,45 +84,3 @@ fn get_ordered_swap(pattern: &ArrayView1<i64>) -> Vec<(i64, i64)> {
swaps[..].reverse();
swaps
}

/// Checks whether an array of size N is a permutation of 0, 1, ..., N - 1.
#[pyfunction]
#[pyo3(signature = (pattern))]
fn _validate_permutation(py: Python, pattern: PyArrayLike1<i64>) -> PyResult<PyObject> {
let view = pattern.as_array();
validate_permutation(&view)?;
Ok(py.None())
}

/// Finds inverse of a permutation pattern.
#[pyfunction]
#[pyo3(signature = (pattern))]
fn _inverse_pattern(py: Python, pattern: PyArrayLike1<i64>) -> PyResult<PyObject> {
let view = pattern.as_array();
let inverse_i64: Vec<i64> = invert(&view).iter().map(|&x| x as i64).collect();
Ok(inverse_i64.to_object(py))
}

/// Sorts the input permutation by iterating through the permutation list
/// and putting each element to its correct position via a SWAP (if it's not
/// at the correct position already). If ``n`` is the length of the input
/// permutation, this requires at most ``n`` SWAPs.
///
/// More precisely, if the input permutation is a cycle of length ``m``,
/// then this creates a quantum circuit with ``m-1`` SWAPs (and of depth ``m-1``);
/// if the input permutation consists of several disjoint cycles, then each cycle
/// is essentially treated independently.
#[pyfunction]
#[pyo3(signature = (permutation_in))]
fn _get_ordered_swap(py: Python, permutation_in: PyArrayLike1<i64>) -> PyResult<PyObject> {
let view = permutation_in.as_array();
Ok(get_ordered_swap(&view).to_object(py))
}

#[pymodule]
pub fn permutation(m: &Bound<PyModule>) -> PyResult<()> {
m.add_function(wrap_pyfunction!(_validate_permutation, m)?)?;
m.add_function(wrap_pyfunction!(_inverse_pattern, m)?)?;
m.add_function(wrap_pyfunction!(_get_ordered_swap, m)?)?;
Ok(())
}
10 changes: 5 additions & 5 deletions crates/pyext/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -17,10 +17,10 @@ use qiskit_accelerate::{
convert_2q_block_matrix::convert_2q_block_matrix, dense_layout::dense_layout,
error_map::error_map, euler_one_qubit_decomposer::euler_one_qubit_decomposer,
isometry::isometry, nlayout::nlayout, optimize_1q_gates::optimize_1q_gates,
pauli_exp_val::pauli_expval, permutation::permutation, results::results, sabre::sabre,
sampled_exp_val::sampled_exp_val, sparse_pauli_op::sparse_pauli_op,
stochastic_swap::stochastic_swap, two_qubit_decompose::two_qubit_decompose, uc_gate::uc_gate,
utils::utils, vf2_layout::vf2_layout,
pauli_exp_val::pauli_expval, results::results, sabre::sabre, sampled_exp_val::sampled_exp_val,
sparse_pauli_op::sparse_pauli_op, stochastic_swap::stochastic_swap, synthesis::synthesis,
two_qubit_decompose::two_qubit_decompose, uc_gate::uc_gate, utils::utils,
vf2_layout::vf2_layout,
};

#[pymodule]
Expand All @@ -36,7 +36,7 @@ fn _accelerate(m: &Bound<PyModule>) -> PyResult<()> {
m.add_wrapped(wrap_pymodule!(nlayout))?;
m.add_wrapped(wrap_pymodule!(optimize_1q_gates))?;
m.add_wrapped(wrap_pymodule!(pauli_expval))?;
m.add_wrapped(wrap_pymodule!(permutation))?;
m.add_wrapped(wrap_pymodule!(synthesis))?;
m.add_wrapped(wrap_pymodule!(results))?;
m.add_wrapped(wrap_pymodule!(sabre))?;
m.add_wrapped(wrap_pymodule!(sampled_exp_val))?;
Expand Down
2 changes: 1 addition & 1 deletion qiskit/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -80,7 +80,7 @@
sys.modules["qiskit._accelerate.stochastic_swap"] = _accelerate.stochastic_swap
sys.modules["qiskit._accelerate.two_qubit_decompose"] = _accelerate.two_qubit_decompose
sys.modules["qiskit._accelerate.vf2_layout"] = _accelerate.vf2_layout
sys.modules["qiskit._accelerate.permutation"] = _accelerate.permutation
sys.modules["qiskit._accelerate.synthesis.permutation"] = _accelerate.synthesis.permutation

from qiskit.exceptions import QiskitError, MissingOptionalLibraryError

Expand Down
17 changes: 8 additions & 9 deletions qiskit/circuit/library/generalized_gates/permutation.py
Original file line number Diff line number Diff line change
Expand Up @@ -80,15 +80,13 @@ def __init__(

name = "permutation_" + np.array_str(pattern).replace(" ", ",")

circuit = QuantumCircuit(num_qubits, name=name)

super().__init__(num_qubits, name=name)

# pylint: disable=cyclic-import
from qiskit.synthesis.permutation.permutation_utils import _get_ordered_swap
from qiskit.synthesis.permutation import synth_permutation_basic

for i, j in _get_ordered_swap(pattern):
circuit.swap(i, j)
circuit = synth_permutation_basic(pattern)
circuit.name = name

all_qubits = self.qubits
self.append(circuit.to_gate(), all_qubits)
Expand Down Expand Up @@ -184,10 +182,11 @@ def inverse(self, annotated: bool = False):

def _qasm2_decomposition(self):
# pylint: disable=cyclic-import
from qiskit.synthesis.permutation.permutation_utils import _get_ordered_swap
from qiskit.synthesis.permutation import synth_permutation_basic

name = f"permutation__{'_'.join(str(n) for n in self.pattern)}_"
out = QuantumCircuit(self.num_qubits, name=name)
for i, j in _get_ordered_swap(self.pattern):
out.swap(i, j)

out = synth_permutation_basic(self.pattern)
out.name = name

return out.to_gate()
14 changes: 2 additions & 12 deletions qiskit/synthesis/permutation/permutation_full.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,8 @@

import numpy as np
from qiskit.circuit.quantumcircuit import QuantumCircuit
from qiskit._accelerate.synthesis.permutation import _synth_permutation_basic
from .permutation_utils import (
_get_ordered_swap,
_inverse_pattern,
_pattern_to_cycles,
_decompose_cycles,
Expand All @@ -44,17 +44,7 @@ def synth_permutation_basic(pattern: list[int] | np.ndarray[int]) -> QuantumCirc
Returns:
The synthesized quantum circuit.
"""
# This is the very original Qiskit algorithm for synthesizing permutations.

num_qubits = len(pattern)
qc = QuantumCircuit(num_qubits)

swaps = _get_ordered_swap(pattern)

for swap in swaps:
qc.swap(swap[0], swap[1])

return qc
return QuantumCircuit._from_circuit_data(_synth_permutation_basic(pattern))


def synth_permutation_acg(pattern: list[int] | np.ndarray[int]) -> QuantumCircuit:
Expand Down
3 changes: 1 addition & 2 deletions qiskit/synthesis/permutation/permutation_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,9 +13,8 @@
"""Utility functions for handling permutations."""

# pylint: disable=unused-import
from qiskit._accelerate.permutation import (
from qiskit._accelerate.synthesis.permutation import (
_inverse_pattern,
_get_ordered_swap,
_validate_permutation,
)

Expand Down
4 changes: 4 additions & 0 deletions releasenotes/notes/oxidize-permbasic-be27578187ac472f.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
---
upgrade_synthesis:
- |
Port :func:`.synth_permutation_basic`, used to synthesize qubit permutations, to Rust.
14 changes: 0 additions & 14 deletions test/python/synthesis/test_permutation_synthesis.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,6 @@
)
from qiskit.synthesis.permutation.permutation_utils import (
_inverse_pattern,
_get_ordered_swap,
_validate_permutation,
)
from test import QiskitTestCase # pylint: disable=wrong-import-order
Expand All @@ -47,19 +46,6 @@ def test_inverse_pattern(self, width):
for ii, jj in enumerate(pattern):
self.assertTrue(inverse[jj] == ii)

@data(4, 5, 10, 15, 20)
def test_get_ordered_swap(self, width):
"""Test _get_ordered_swap function produces correct swap list."""
np.random.seed(1)
for _ in range(5):
pattern = np.random.permutation(width)
swap_list = _get_ordered_swap(pattern)
output = list(range(width))
for i, j in swap_list:
output[i], output[j] = output[j], output[i]
self.assertTrue(np.array_equal(pattern, output))
self.assertLess(len(swap_list), width)

@data(10, 20)
def test_invalid_permutations(self, width):
"""Check that _validate_permutation raises exceptions when the
Expand Down

0 comments on commit de6c6eb

Please sign in to comment.