Skip to content

Commit

Permalink
Merge remote-tracking branch 'upstream/main' into ParameterExpression…
Browse files Browse the repository at this point in the history
…_rust
  • Loading branch information
doichanj committed Jan 8, 2025
2 parents 2767ffd + 93d796f commit 49ef9fb
Show file tree
Hide file tree
Showing 18 changed files with 369 additions and 51 deletions.
48 changes: 29 additions & 19 deletions crates/accelerate/src/commutation_checker.rs
Original file line number Diff line number Diff line change
Expand Up @@ -28,24 +28,29 @@ use qiskit_circuit::circuit_instruction::{ExtraInstructionAttributes, OperationF
use qiskit_circuit::dag_node::DAGOpNode;
use qiskit_circuit::imports::QI_OPERATOR;
use qiskit_circuit::operations::OperationRef::{Gate as PyGateType, Operation as PyOperationType};
use qiskit_circuit::operations::{Operation, OperationRef, Param, StandardGate};
use qiskit_circuit::operations::{
get_standard_gate_names, Operation, OperationRef, Param, StandardGate,
};
use qiskit_circuit::{BitType, Clbit, Qubit};

use crate::unitary_compose;
use crate::QiskitError;

const TWOPI: f64 = 2.0 * std::f64::consts::PI;

// These gates do not commute with other gates, we do not check them.
static SKIPPED_NAMES: [&str; 4] = ["measure", "reset", "delay", "initialize"];
static NO_CACHE_NAMES: [&str; 2] = ["annotated", "linear_function"];

// We keep a hash-set of operations eligible for commutation checking. This is because checking
// eligibility is not for free.
static SUPPORTED_OP: Lazy<HashSet<&str>> = Lazy::new(|| {
HashSet::from([
"rxx", "ryy", "rzz", "rzx", "h", "x", "y", "z", "sx", "sxdg", "t", "tdg", "s", "sdg", "cx",
"cy", "cz", "swap", "iswap", "ecr", "ccx", "cswap",
])
});

const TWOPI: f64 = 2.0 * std::f64::consts::PI;

// map rotation gates to their generators, or to ``None`` if we cannot currently efficiently
// Map rotation gates to their generators, or to ``None`` if we cannot currently efficiently
// represent the generator in Rust and store the commutation relation in the commutation dictionary
static SUPPORTED_ROTATIONS: Lazy<HashMap<&str, Option<OperationRef>>> = Lazy::new(|| {
HashMap::from([
Expand Down Expand Up @@ -322,15 +327,17 @@ impl CommutationChecker {
(qargs1, qargs2)
};

let skip_cache: bool = NO_CACHE_NAMES.contains(&first_op.name()) ||
NO_CACHE_NAMES.contains(&second_op.name()) ||
// Skip params that do not evaluate to floats for caching and commutation library
first_params.iter().any(|p| !matches!(p, Param::Float(_))) ||
second_params.iter().any(|p| !matches!(p, Param::Float(_)))
&& !SUPPORTED_OP.contains(op1.name())
&& !SUPPORTED_OP.contains(op2.name());

if skip_cache {
// For our cache to work correctly, we require the gate's definition to only depend on the
// ``params`` attribute. This cannot be guaranteed for custom gates, so we only check
// the cache for our standard gates, which we know are defined by the ``params`` AND
// that the ``params`` are float-only at this point.
let whitelist = get_standard_gate_names();
let check_cache = whitelist.contains(&first_op.name())
&& whitelist.contains(&second_op.name())
&& first_params.iter().all(|p| matches!(p, Param::Float(_)))
&& second_params.iter().all(|p| matches!(p, Param::Float(_)));

if !check_cache {
return self.commute_matmul(
py,
first_op,
Expand Down Expand Up @@ -630,21 +637,24 @@ fn map_rotation<'a>(
) -> (&'a OperationRef<'a>, &'a [Param], bool) {
let name = op.name();
if let Some(generator) = SUPPORTED_ROTATIONS.get(name) {
// if the rotation angle is below the tolerance, the gate is assumed to
// If the rotation angle is below the tolerance, the gate is assumed to
// commute with everything, and we simply return the operation with the flag that
// it commutes trivially
// it commutes trivially.
if let Param::Float(angle) = params[0] {
if (angle % TWOPI).abs() < tol {
return (op, params, true);
};
};

// otherwise, we check if a generator is given -- if not, we'll just return the operation
// itself (e.g. RXX does not have a generator and is just stored in the commutations
// dictionary)
// Otherwise we need to cover two cases -- either a generator is given, in which case
// we return it, or we don't have a generator yet, but we know we have the operation
// stored in the commutation library. For example, RXX does not have a generator in Rust
// yet (PauliGate is not in Rust currently), but it is stored in the library, so we
// can strip the parameters and just return the gate.
if let Some(gate) = generator {
return (gate, &[], false);
};
return (op, &[], false);
}
(op, params, false)
}
Expand Down
8 changes: 3 additions & 5 deletions crates/accelerate/src/synthesis/clifford/random_clifford.rs
Original file line number Diff line number Diff line change
Expand Up @@ -125,17 +125,15 @@ pub fn random_clifford_tableau_inner(num_qubits: usize, seed: Option<u64>) -> Ar

// Compute the full stabilizer tableau

// The code below is identical to the Python implementation, but is based on the original
// code in the paper.

// The code below is based on the original code in the referenced paper.
let mut table = Array2::from_elem((2 * num_qubits, 2 * num_qubits), false);

// Apply qubit permutation
for i in 0..num_qubits {
replace_row_inner(table.view_mut(), i, table2.slice(s![i, ..]));
replace_row_inner(table.view_mut(), i, table2.slice(s![perm[i], ..]));
replace_row_inner(
table.view_mut(),
perm[i] + num_qubits,
i + num_qubits,
table2.slice(s![perm[i] + num_qubits, ..]),
);
}
Expand Down
3 changes: 2 additions & 1 deletion crates/accelerate/src/unitary_synthesis.rs
Original file line number Diff line number Diff line change
Expand Up @@ -372,7 +372,8 @@ fn py_run_main_loop(
None,
None,
)?;
out_dag = synth_dag;
let out_qargs = dag.get_qargs(packed_instr.qubits);
apply_synth_dag(py, &mut out_dag, out_qargs, &synth_dag)?;
}
}
}
Expand Down
5 changes: 5 additions & 0 deletions crates/circuit/src/operations.rs
Original file line number Diff line number Diff line change
Expand Up @@ -431,6 +431,11 @@ static STANDARD_GATE_NAME: [&str; STANDARD_GATE_SIZE] = [
"rcccx", // 51 ("rc3x")
];

/// Get a slice of all standard gate names.
pub fn get_standard_gate_names() -> &'static [&'static str] {
&STANDARD_GATE_NAME
}

impl StandardGate {
pub fn create_py_op(
&self,
Expand Down
21 changes: 19 additions & 2 deletions qiskit/primitives/containers/observables_array.py
Original file line number Diff line number Diff line change
Expand Up @@ -97,8 +97,25 @@ def __repr__(self):
array = np.array2string(self._array, prefix=prefix, suffix=suffix, threshold=50)
return prefix + array + suffix

def tolist(self) -> list:
"""Convert to a nested list"""
def tolist(self) -> list | ObservableLike:
"""Convert to a nested list.
Similar to Numpy's ``tolist`` method, the level of nesting
depends on the dimension of the observables array. In the
case of dimension 0 the method returns a single observable
(``dict`` in the case of a weighted sum of Paulis) instead of a list.
Examples::
Return values for a one-element list vs one element:
>>> from qiskit.primitives.containers.observables_array import ObservablesArray
>>> oa = ObservablesArray.coerce(["Z"])
>>> print(type(oa.tolist()))
<class 'list'>
>>> oa = ObservablesArray.coerce("Z")
>>> print(type(oa.tolist()))
<class 'dict'>
"""
return self._array.tolist()

def __array__(self, dtype=None, copy=None):
Expand Down
57 changes: 48 additions & 9 deletions qiskit/transpiler/passes/synthesis/hls_plugins.py
Original file line number Diff line number Diff line change
Expand Up @@ -420,12 +420,13 @@
C3XGate,
C4XGate,
PauliEvolutionGate,
PermutationGate,
MCMTGate,
ModularAdderGate,
HalfAdderGate,
FullAdderGate,
MultiplierGate,
)
from qiskit.transpiler.exceptions import TranspilerError
from qiskit.transpiler.coupling import CouplingMap

from qiskit.synthesis.clifford import (
Expand Down Expand Up @@ -467,6 +468,7 @@
multiplier_qft_r17,
multiplier_cumulative_h18,
)
from qiskit.quantum_info.operators import Clifford
from qiskit.transpiler.passes.routing.algorithms import ApproximateTokenSwapper
from .plugin import HighLevelSynthesisPlugin

Expand All @@ -484,6 +486,9 @@ class DefaultSynthesisClifford(HighLevelSynthesisPlugin):

def run(self, high_level_object, coupling_map=None, target=None, qubits=None, **options):
"""Run synthesis for the given Clifford."""
if not isinstance(high_level_object, Clifford):
return None

decomposition = synth_clifford_full(high_level_object)
return decomposition

Expand All @@ -497,6 +502,9 @@ class AGSynthesisClifford(HighLevelSynthesisPlugin):

def run(self, high_level_object, coupling_map=None, target=None, qubits=None, **options):
"""Run synthesis for the given Clifford."""
if not isinstance(high_level_object, Clifford):
return None

decomposition = synth_clifford_ag(high_level_object)
return decomposition

Expand All @@ -513,10 +521,14 @@ class BMSynthesisClifford(HighLevelSynthesisPlugin):

def run(self, high_level_object, coupling_map=None, target=None, qubits=None, **options):
"""Run synthesis for the given Clifford."""
if not isinstance(high_level_object, Clifford):
return None

if high_level_object.num_qubits <= 3:
decomposition = synth_clifford_bm(high_level_object)
else:
decomposition = None

return decomposition


Expand All @@ -530,6 +542,9 @@ class GreedySynthesisClifford(HighLevelSynthesisPlugin):

def run(self, high_level_object, coupling_map=None, target=None, qubits=None, **options):
"""Run synthesis for the given Clifford."""
if not isinstance(high_level_object, Clifford):
return None

decomposition = synth_clifford_greedy(high_level_object)
return decomposition

Expand All @@ -544,6 +559,9 @@ class LayerSynthesisClifford(HighLevelSynthesisPlugin):

def run(self, high_level_object, coupling_map=None, target=None, qubits=None, **options):
"""Run synthesis for the given Clifford."""
if not isinstance(high_level_object, Clifford):
return None

decomposition = synth_clifford_layers(high_level_object)
return decomposition

Expand All @@ -559,6 +577,9 @@ class LayerLnnSynthesisClifford(HighLevelSynthesisPlugin):

def run(self, high_level_object, coupling_map=None, target=None, qubits=None, **options):
"""Run synthesis for the given Clifford."""
if not isinstance(high_level_object, Clifford):
return None

decomposition = synth_clifford_depth_lnn(high_level_object)
return decomposition

Expand All @@ -572,6 +593,9 @@ class DefaultSynthesisLinearFunction(HighLevelSynthesisPlugin):

def run(self, high_level_object, coupling_map=None, target=None, qubits=None, **options):
"""Run synthesis for the given LinearFunction."""
if not isinstance(high_level_object, LinearFunction):
return None

decomposition = synth_cnot_count_full_pmh(high_level_object.linear)
return decomposition

Expand All @@ -595,11 +619,8 @@ class KMSSynthesisLinearFunction(HighLevelSynthesisPlugin):

def run(self, high_level_object, coupling_map=None, target=None, qubits=None, **options):
"""Run synthesis for the given LinearFunction."""

if not isinstance(high_level_object, LinearFunction):
raise TranspilerError(
"PMHSynthesisLinearFunction only accepts objects of type LinearFunction"
)
return None

use_inverted = options.get("use_inverted", False)
use_transposed = options.get("use_transposed", False)
Expand Down Expand Up @@ -646,11 +667,8 @@ class PMHSynthesisLinearFunction(HighLevelSynthesisPlugin):

def run(self, high_level_object, coupling_map=None, target=None, qubits=None, **options):
"""Run synthesis for the given LinearFunction."""

if not isinstance(high_level_object, LinearFunction):
raise TranspilerError(
"PMHSynthesisLinearFunction only accepts objects of type LinearFunction"
)
return None

section_size = options.get("section_size", 2)
use_inverted = options.get("use_inverted", False)
Expand Down Expand Up @@ -682,6 +700,9 @@ class KMSSynthesisPermutation(HighLevelSynthesisPlugin):

def run(self, high_level_object, coupling_map=None, target=None, qubits=None, **options):
"""Run synthesis for the given Permutation."""
if not isinstance(high_level_object, PermutationGate):
return None

decomposition = synth_permutation_depth_lnn_kms(high_level_object.pattern)
return decomposition

Expand All @@ -695,6 +716,9 @@ class BasicSynthesisPermutation(HighLevelSynthesisPlugin):

def run(self, high_level_object, coupling_map=None, target=None, qubits=None, **options):
"""Run synthesis for the given Permutation."""
if not isinstance(high_level_object, PermutationGate):
return None

decomposition = synth_permutation_basic(high_level_object.pattern)
return decomposition

Expand All @@ -708,6 +732,9 @@ class ACGSynthesisPermutation(HighLevelSynthesisPlugin):

def run(self, high_level_object, coupling_map=None, target=None, qubits=None, **options):
"""Run synthesis for the given Permutation."""
if not isinstance(high_level_object, PermutationGate):
return None

decomposition = synth_permutation_acg(high_level_object.pattern)
return decomposition

Expand Down Expand Up @@ -858,6 +885,9 @@ class TokenSwapperSynthesisPermutation(HighLevelSynthesisPlugin):
def run(self, high_level_object, coupling_map=None, target=None, qubits=None, **options):
"""Run synthesis for the given Permutation."""

if not isinstance(high_level_object, PermutationGate):
return None

trials = options.get("trials", 5)
seed = options.get("seed", 0)
parallel_threshold = options.get("parallel_threshold", 50)
Expand Down Expand Up @@ -1156,6 +1186,9 @@ class MCMTSynthesisDefault(HighLevelSynthesisPlugin):

def run(self, high_level_object, coupling_map=None, target=None, qubits=None, **options):
# first try to use the V-chain synthesis if enough auxiliary qubits are available
if not isinstance(high_level_object, MCMTGate):
return None

if (
decomposition := MCMTSynthesisVChain().run(
high_level_object, coupling_map, target, qubits, **options
Expand All @@ -1170,6 +1203,9 @@ class MCMTSynthesisNoAux(HighLevelSynthesisPlugin):
"""A V-chain based synthesis for ``MCMTGate``."""

def run(self, high_level_object, coupling_map=None, target=None, qubits=None, **options):
if not isinstance(high_level_object, MCMTGate):
return None

base_gate = high_level_object.base_gate
ctrl_state = options.get("ctrl_state", None)

Expand All @@ -1195,6 +1231,9 @@ class MCMTSynthesisVChain(HighLevelSynthesisPlugin):
"""A V-chain based synthesis for ``MCMTGate``."""

def run(self, high_level_object, coupling_map=None, target=None, qubits=None, **options):
if not isinstance(high_level_object, MCMTGate):
return None

if options.get("num_clean_ancillas", 0) < high_level_object.num_ctrl_qubits - 1:
return None # insufficient number of auxiliary qubits

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
---
fixes:
- |
Commutation relations of :class:`~.circuit.Instruction`\ s with float-only ``params``
were eagerly cached by the :class:`.CommutationChecker`, using the ``params`` as key to
query the relation. This could lead to faulty results, if the instruction's definition
depended on additional information that just the :attr:`~.circuit.Instruction.params`
attribute, such as e.g. the case for :class:`.PauliEvolutionGate`.
This behavior is now fixed, and the commutation checker only conservatively caches
commutations for Qiskit-native standard gates. This can incur a performance cost if you were
relying on your custom gates being cached, however, we cannot guarantee safe caching for
custom gates, as they might rely on information beyond :attr:`~.circuit.Instruction.params`.
- |
Fixed a bug in the :class:`.CommmutationChecker`, where checking commutation of instruction
with non-numeric values in the :attr:`~.circuit.Instruction.params` attribute (such as the
:class:`.PauliGate`) could raise an error.
Fixed `#13570 <https://github.com/Qiskit/qiskit/issues/13570>`__.
15 changes: 15 additions & 0 deletions releasenotes/notes/fix-mcmt-to-gate-ec84e1c625312444.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
---
fixes:
- |
Fixed a bug where any instruction called ``"mcmt"`` would be passed into the high-level
synthesis routine for a :class:`.MCMTGate`, which causes a failure or invalid result.
In particular, this could happen accidentally when handling the :class:`.MCMT` _circuit_,
named ``"mcmt"``, and implicitly converting it into an instruction e.g. when appending
it to a circuit.
Fixed `#13563 <https://github.com/Qiskit/qiskit/issues/13563>`__.
upgrade_synthesis:
- |
The plugins for :class:`.LinearFunction` no longer raise an error if another object
than :class:`.LinearFunction` is passed into the ``run`` method. Instead, ``None`` is
returned, which is consistent with the other plugins. If you relied on this error being raised,
you can manually perform an instance-check.
5 changes: 5 additions & 0 deletions releasenotes/notes/fix-random-clifford-c0394becbdd7db50.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
fixes:
- |
Fixed a bug in :func:`~qiskit.quantum_info.random_clifford` that stopped it
from sampling the full Clifford group.
Loading

0 comments on commit 49ef9fb

Please sign in to comment.