diff --git a/Cargo.lock b/Cargo.lock index fc4d75c9f44a..53b46e23d33e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1246,6 +1246,7 @@ dependencies = [ name = "qiskit-qasm3" version = "1.3.0" dependencies = [ + "ahash 0.8.11", "hashbrown 0.14.5", "indexmap 2.3.0", "oq3_semantics", diff --git a/Cargo.toml b/Cargo.toml index a505a4e7c22b..5fffe8ff4dd7 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -23,6 +23,7 @@ ndarray = "^0.15.6" numpy = "0.21.0" smallvec = "1.13" thiserror = "1.0" +ahash = "0.8.11" # Most of the crates don't need the feature `extension-module`, since only `qiskit-pyext` builds an # actual C extension (the feature disables linking in `libpython`, which is forbidden in Python diff --git a/crates/accelerate/Cargo.toml b/crates/accelerate/Cargo.toml index 0b23cf08743e..854c1ed05706 100644 --- a/crates/accelerate/Cargo.toml +++ b/crates/accelerate/Cargo.toml @@ -15,7 +15,7 @@ numpy.workspace = true rand = "0.8" rand_pcg = "0.3" rand_distr = "0.4.3" -ahash = "0.8.11" +ahash.workspace = true num-traits = "0.2" num-complex.workspace = true num-bigint.workspace = true diff --git a/crates/accelerate/src/synthesis/clifford/greedy_synthesis.rs b/crates/accelerate/src/synthesis/clifford/greedy_synthesis.rs index e53e25282005..81ddfb1d6503 100644 --- a/crates/accelerate/src/synthesis/clifford/greedy_synthesis.rs +++ b/crates/accelerate/src/synthesis/clifford/greedy_synthesis.rs @@ -10,6 +10,7 @@ // copyright notice, and modified files need to carry a notice indicating // that they have been altered from the originals. +use ahash::RandomState; use indexmap::IndexSet; use ndarray::{s, ArrayView2}; use smallvec::smallvec; @@ -102,7 +103,7 @@ pub struct GreedyCliffordSynthesis<'a> { symplectic_matrix: SymplecticMatrix, /// Unprocessed qubits. - unprocessed_qubits: IndexSet, + unprocessed_qubits: IndexSet, } impl GreedyCliffordSynthesis<'_> { @@ -121,7 +122,7 @@ impl GreedyCliffordSynthesis<'_> { smat: tableau.slice(s![.., 0..2 * num_qubits]).to_owned(), }; - let unprocessed_qubits: IndexSet = (0..num_qubits).collect(); + let unprocessed_qubits = (0..num_qubits).collect(); Ok(GreedyCliffordSynthesis { tableau, diff --git a/crates/accelerate/src/target_transpiler/mod.rs b/crates/accelerate/src/target_transpiler/mod.rs index bb0ec166c07e..0954e8e347f0 100644 --- a/crates/accelerate/src/target_transpiler/mod.rs +++ b/crates/accelerate/src/target_transpiler/mod.rs @@ -305,7 +305,7 @@ impl Target { match instruction { TargetOperation::Variadic(_) => { qargs_val = PropsMap::with_capacity(1); - qargs_val.extend([(None, None)].into_iter()); + qargs_val.extend([(None, None)]); self.variable_class_operations.insert(name.to_string()); } TargetOperation::Normal(_) => { @@ -872,7 +872,7 @@ impl Target { .unwrap() .extract::()? .into_iter() - .map(|(name, prop_map)| (name, PropsMap::from_iter(prop_map.into_iter()))), + .map(|(name, prop_map)| (name, PropsMap::from_iter(prop_map))), ); self._gate_name_map = state .get_item("gate_name_map")? diff --git a/crates/accelerate/src/target_transpiler/nullable_index_map.rs b/crates/accelerate/src/target_transpiler/nullable_index_map.rs index d3056c9edd8a..e6e2a0fca3a3 100644 --- a/crates/accelerate/src/target_transpiler/nullable_index_map.rs +++ b/crates/accelerate/src/target_transpiler/nullable_index_map.rs @@ -164,7 +164,7 @@ where pub fn iter(&self) -> Iter { Iter { map: self.map.iter(), - null_value: &self.null_val, + null_value: self.null_val.as_ref(), } } @@ -209,7 +209,7 @@ where /// Iterator for the key-value pairs in `NullableIndexMap`. pub struct Iter<'a, K, V> { map: BaseIter<'a, K, V>, - null_value: &'a Option, + null_value: Option<&'a V>, } impl<'a, K, V> Iterator for Iter<'a, K, V> { @@ -218,12 +218,8 @@ impl<'a, K, V> Iterator for Iter<'a, K, V> { fn next(&mut self) -> Option { if let Some((key, val)) = self.map.next() { Some((Some(key), val)) - } else if let Some(value) = self.null_value { - let value = value; - self.null_value = &None; - Some((None, value)) } else { - None + self.null_value.take().map(|value| (None, value)) } } diff --git a/crates/accelerate/src/two_qubit_decompose.rs b/crates/accelerate/src/two_qubit_decompose.rs index b423b1d65267..2072c5034ff7 100644 --- a/crates/accelerate/src/two_qubit_decompose.rs +++ b/crates/accelerate/src/two_qubit_decompose.rs @@ -398,8 +398,6 @@ impl Specialization { } } -type WeylCircuitSequence = Vec<(StandardGate, SmallVec<[Param; 3]>, SmallVec<[Qubit; 2]>)>; - #[derive(Clone, Debug)] #[allow(non_snake_case)] #[pyclass(module = "qiskit._accelerate.two_qubit_decompose", subclass)] @@ -430,56 +428,49 @@ impl TwoQubitWeylDecomposition { fn weyl_gate( &self, simplify: bool, - sequence: &mut WeylCircuitSequence, + sequence: &mut CircuitData, atol: f64, global_phase: &mut f64, - ) { + ) -> PyResult<()> { match self.specialization { Specialization::MirrorControlledEquiv => { - sequence.push(( - StandardGate::SwapGate, - SmallVec::new(), - smallvec![Qubit(0), Qubit(1)], - )); - sequence.push(( + sequence.push_standard_gate(StandardGate::SwapGate, &[], &[Qubit(0), Qubit(1)])?; + sequence.push_standard_gate( StandardGate::RZZGate, - smallvec![Param::Float((PI4 - self.c) * 2.)], - smallvec![Qubit(0), Qubit(1)], - )); + &[Param::Float((PI4 - self.c) * 2.)], + &[Qubit(0), Qubit(1)], + )?; *global_phase += PI4 } Specialization::SWAPEquiv => { - sequence.push(( - StandardGate::SwapGate, - SmallVec::new(), - smallvec![Qubit(0), Qubit(1)], - )); + sequence.push_standard_gate(StandardGate::SwapGate, &[], &[Qubit(0), Qubit(1)])?; *global_phase -= 3. * PI / 4. } _ => { if !simplify || self.a.abs() > atol { - sequence.push(( + sequence.push_standard_gate( StandardGate::RXXGate, - smallvec![Param::Float(-self.a * 2.)], - smallvec![Qubit(0), Qubit(1)], - )); + &[Param::Float(-self.a * 2.)], + &[Qubit(0), Qubit(1)], + )?; } if !simplify || self.b.abs() > atol { - sequence.push(( + sequence.push_standard_gate( StandardGate::RYYGate, - smallvec![Param::Float(-self.b * 2.)], - smallvec![Qubit(0), Qubit(1)], - )); + &[Param::Float(-self.b * 2.)], + &[Qubit(0), Qubit(1)], + )?; } if !simplify || self.c.abs() > atol { - sequence.push(( + sequence.push_standard_gate( StandardGate::RZZGate, - smallvec![Param::Float(-self.c * 2.)], - smallvec![Qubit(0), Qubit(1)], - )); + &[Param::Float(-self.c * 2.)], + &[Qubit(0), Qubit(1)], + )?; } } } + Ok(()) } /// Instantiate a new TwoQubitWeylDecomposition with rust native @@ -1070,7 +1061,7 @@ impl TwoQubitWeylDecomposition { }; let target_1q_basis_list: Vec = vec![euler_basis]; - let mut gate_sequence: WeylCircuitSequence = Vec::with_capacity(21); + let mut gate_sequence = CircuitData::with_capacity(py, 2, 0, 21, Param::Float(0.))?; let mut global_phase: f64 = self.global_phase; let c2r = unitary_to_gate_sequence_inner( @@ -1083,11 +1074,11 @@ impl TwoQubitWeylDecomposition { ) .unwrap(); for gate in c2r.gates { - gate_sequence.push(( + gate_sequence.push_standard_gate( gate.0, - gate.1.into_iter().map(Param::Float).collect(), - smallvec![Qubit(0)], - )) + &gate.1.into_iter().map(Param::Float).collect::>(), + &[Qubit(0)], + )? } global_phase += c2r.global_phase; let c2l = unitary_to_gate_sequence_inner( @@ -1100,11 +1091,11 @@ impl TwoQubitWeylDecomposition { ) .unwrap(); for gate in c2l.gates { - gate_sequence.push(( + gate_sequence.push_standard_gate( gate.0, - gate.1.into_iter().map(Param::Float).collect(), - smallvec![Qubit(1)], - )) + &gate.1.into_iter().map(Param::Float).collect::>(), + &[Qubit(1)], + )? } global_phase += c2l.global_phase; self.weyl_gate( @@ -1112,7 +1103,7 @@ impl TwoQubitWeylDecomposition { &mut gate_sequence, atol.unwrap_or(ANGLE_ZERO_EPSILON), &mut global_phase, - ); + )?; let c1r = unitary_to_gate_sequence_inner( self.K1r.view(), &target_1q_basis_list, @@ -1123,11 +1114,11 @@ impl TwoQubitWeylDecomposition { ) .unwrap(); for gate in c1r.gates { - gate_sequence.push(( + gate_sequence.push_standard_gate( gate.0, - gate.1.into_iter().map(Param::Float).collect(), - smallvec![Qubit(0)], - )) + &gate.1.into_iter().map(Param::Float).collect::>(), + &[Qubit(0)], + )? } global_phase += c2r.global_phase; let c1l = unitary_to_gate_sequence_inner( @@ -1140,13 +1131,14 @@ impl TwoQubitWeylDecomposition { ) .unwrap(); for gate in c1l.gates { - gate_sequence.push(( + gate_sequence.push_standard_gate( gate.0, - gate.1.into_iter().map(Param::Float).collect(), - smallvec![Qubit(1)], - )) + &gate.1.into_iter().map(Param::Float).collect::>(), + &[Qubit(1)], + )? } - CircuitData::from_standard_gates(py, 2, gate_sequence, Param::Float(global_phase)) + gate_sequence.set_global_phase(py, Param::Float(global_phase))?; + Ok(gate_sequence) } } diff --git a/crates/circuit/src/circuit_data.rs b/crates/circuit/src/circuit_data.rs index dd2618f03f9a..3249b609e154 100644 --- a/crates/circuit/src/circuit_data.rs +++ b/crates/circuit/src/circuit_data.rs @@ -15,7 +15,7 @@ use std::cell::RefCell; use crate::bit_data::BitData; use crate::circuit_instruction::{CircuitInstruction, OperationFromPython}; -use crate::imports::{ANNOTATED_OPERATION, QUANTUM_CIRCUIT, QUBIT}; +use crate::imports::{ANNOTATED_OPERATION, CLBIT, QUANTUM_CIRCUIT, QUBIT}; use crate::interner::{IndexedInterner, Interner, InternerKey}; use crate::operations::{Operation, OperationRef, Param, StandardGate}; use crate::packed_instruction::PackedInstruction; @@ -131,22 +131,13 @@ impl CircuitData { I: IntoIterator, SmallVec<[Qubit; 2]>)>, { let instruction_iter = instructions.into_iter(); - let mut res = CircuitData { - data: Vec::with_capacity(instruction_iter.size_hint().0), - qargs_interner: IndexedInterner::new(), - cargs_interner: IndexedInterner::new(), - qubits: BitData::new(py, "qubits".to_string()), - clbits: BitData::new(py, "clbits".to_string()), - param_table: ParameterTable::new(), + let mut res = Self::with_capacity( + py, + num_qubits, + 0, + instruction_iter.size_hint().0, global_phase, - }; - if num_qubits > 0 { - let qubit_cls = QUBIT.get_bound(py); - for _i in 0..num_qubits { - let bit = qubit_cls.call0()?; - res.add_qubit(py, &bit, true)?; - } - } + )?; let no_clbit_index = (&mut res.cargs_interner) .intern(InternerKey::Value(Vec::new()))? .index; @@ -169,6 +160,66 @@ impl CircuitData { Ok(res) } + /// Build an empty CircuitData object with an initially allocated instruction capacity + pub fn with_capacity( + py: Python, + num_qubits: u32, + num_clbits: u32, + instruction_capacity: usize, + global_phase: Param, + ) -> PyResult { + let mut res = CircuitData { + data: Vec::with_capacity(instruction_capacity), + qargs_interner: IndexedInterner::new(), + cargs_interner: IndexedInterner::new(), + qubits: BitData::new(py, "qubits".to_string()), + clbits: BitData::new(py, "clbits".to_string()), + param_table: ParameterTable::new(), + global_phase, + }; + if num_qubits > 0 { + let qubit_cls = QUBIT.get_bound(py); + for _i in 0..num_qubits { + let bit = qubit_cls.call0()?; + res.add_qubit(py, &bit, true)?; + } + } + if num_clbits > 0 { + let clbit_cls = CLBIT.get_bound(py); + for _i in 0..num_clbits { + let bit = clbit_cls.call0()?; + res.add_clbit(py, &bit, true)?; + } + } + Ok(res) + } + + /// Append a standard gate to this CircuitData + pub fn push_standard_gate( + &mut self, + operation: StandardGate, + params: &[Param], + qargs: &[Qubit], + ) -> PyResult<()> { + let no_clbit_index = (&mut self.cargs_interner) + .intern(InternerKey::Value(Vec::new()))? + .index; + let params = (!params.is_empty()).then(|| Box::new(params.iter().cloned().collect())); + let qubits = (&mut self.qargs_interner) + .intern(InternerKey::Value(qargs.to_vec()))? + .index; + self.data.push(PackedInstruction { + op: operation.into(), + qubits, + clbits: no_clbit_index, + params, + extra_attrs: None, + #[cfg(feature = "cache_pygates")] + py_op: RefCell::new(None), + }); + Ok(()) + } + /// Add the entries from the `PackedInstruction` at the given index to the internal parameter /// table. fn track_instruction_parameters( @@ -1233,6 +1284,11 @@ impl CircuitData { } Ok(()) } + + /// Retrieves the python `Param` object based on its `ParameterUuid`. + pub fn get_parameter_by_uuid(&self, uuid: ParameterUuid) -> Option<&Py> { + self.param_table.py_parameter_by_uuid(uuid) + } } /// Helper struct for `assign_parameters` to allow use of `Param::extract_no_coerce` in diff --git a/crates/circuit/src/parameter_table.rs b/crates/circuit/src/parameter_table.rs index 8825fbd71772..38cabf10c69f 100644 --- a/crates/circuit/src/parameter_table.rs +++ b/crates/circuit/src/parameter_table.rs @@ -225,6 +225,11 @@ impl ParameterTable { .map(|uuid| &self.by_uuid[uuid].object) } + /// Lookup the Python parameter object by uuid. + pub fn py_parameter_by_uuid(&self, uuid: ParameterUuid) -> Option<&Py> { + self.by_uuid.get(&uuid).map(|param| ¶m.object) + } + /// Get the (maybe cached) Python list of the sorted `Parameter` objects. pub fn py_parameters<'py>(&mut self, py: Python<'py>) -> Bound<'py, PyList> { if let Some(py_parameters) = self.py_parameters.as_ref() { diff --git a/crates/qasm3/Cargo.toml b/crates/qasm3/Cargo.toml index 4dd0d977786e..8e42f3f0701b 100644 --- a/crates/qasm3/Cargo.toml +++ b/crates/qasm3/Cargo.toml @@ -14,3 +14,4 @@ pyo3.workspace = true indexmap.workspace = true hashbrown.workspace = true oq3_semantics = "0.6.0" +ahash.workspace = true diff --git a/crates/qasm3/src/build.rs b/crates/qasm3/src/build.rs index f5cf2fd4efca..912066df839e 100644 --- a/crates/qasm3/src/build.rs +++ b/crates/qasm3/src/build.rs @@ -13,6 +13,8 @@ use pyo3::prelude::*; use pyo3::types::{PySequence, PyString, PyTuple}; +use ahash::RandomState; + use hashbrown::HashMap; use indexmap::IndexMap; @@ -190,8 +192,9 @@ impl BuilderState { let qubits = if let Some(asg_qubits) = barrier.qubits().as_ref() { // We want any deterministic order for easier circuit reproducibility in Python space, // and to include each seen qubit once. This simply maintains insertion order. - let mut qubits = IndexMap::<*const ::pyo3::ffi::PyObject, Py>::with_capacity( + let mut qubits = IndexMap::<*const ::pyo3::ffi::PyObject, Py, RandomState>::with_capacity_and_hasher( asg_qubits.len(), + RandomState::default() ); for qarg in asg_qubits.iter() { let qarg = expr::expect_gate_operand(qarg)?; diff --git a/qiskit/primitives/estimator.py b/qiskit/primitives/estimator.py index 6ae50ee7f0f5..d8e0a00f45e1 100644 --- a/qiskit/primitives/estimator.py +++ b/qiskit/primitives/estimator.py @@ -22,7 +22,6 @@ from qiskit.circuit import QuantumCircuit from qiskit.exceptions import QiskitError -from qiskit.quantum_info import Statevector from qiskit.quantum_info.operators.base_operator import BaseOperator from qiskit.utils.deprecation import deprecate_func @@ -31,7 +30,7 @@ from .utils import ( _circuit_key, _observable_key, - bound_circuit_to_instruction, + _statevector_from_circuit, init_observable, ) @@ -43,13 +42,21 @@ class Estimator(BaseEstimator[PrimitiveJob[EstimatorResult]]): :Run Options: - **shots** (None or int) -- - The number of shots. If None, it calculates the exact expectation - values. Otherwise, it samples from normal distributions with standard errors as standard + The number of shots. If None, it calculates the expectation values + with full state vector simulation. + Otherwise, it samples from normal distributions with standard errors as standard deviations using normal distribution approximation. - **seed** (np.random.Generator or int) -- Set a fixed seed or generator for the normal distribution. If shots is None, this option is ignored. + + .. note:: + The result of this class is exact if the circuit contains only unitary operations. + On the other hand, the result could be stochastic if the circuit contains a non-unitary + operation such as a reset for a some subsystems. + The stochastic result can be made reproducible by setting ``seed``, e.g., + ``Estimator(options={"seed":123})``. """ @deprecate_func( @@ -112,7 +119,7 @@ def _call( f"The number of qubits of a circuit ({circ.num_qubits}) does not match " f"the number of qubits of a observable ({obs.num_qubits})." ) - final_state = Statevector(bound_circuit_to_instruction(circ)) + final_state = _statevector_from_circuit(circ, rng) expectation_value = final_state.expectation_value(obs) if shots is None: expectation_values.append(expectation_value) diff --git a/qiskit/primitives/statevector_estimator.py b/qiskit/primitives/statevector_estimator.py index c57f2c5b77d5..f96d409631a3 100644 --- a/qiskit/primitives/statevector_estimator.py +++ b/qiskit/primitives/statevector_estimator.py @@ -19,13 +19,13 @@ import numpy as np -from qiskit.quantum_info import SparsePauliOp, Statevector +from qiskit.quantum_info import SparsePauliOp from .base import BaseEstimatorV2 from .containers import DataBin, EstimatorPubLike, PrimitiveResult, PubResult from .containers.estimator_pub import EstimatorPub from .primitive_job import PrimitiveJob -from .utils import bound_circuit_to_instruction +from .utils import _statevector_from_circuit class StatevectorEstimator(BaseEstimatorV2): @@ -41,6 +41,13 @@ class StatevectorEstimator(BaseEstimatorV2): called an estimator primitive unified bloc (PUB), produces its own array-based result. The :meth:`~.EstimatorV2.run` method can be given a sequence of pubs to run in one call. + .. note:: + The result of this class is exact if the circuit contains only unitary operations. + On the other hand, the result could be stochastic if the circuit contains a non-unitary + operation such as a reset for a some subsystems. + The stochastic result can be made reproducible by setting ``seed``, e.g., + ``StatevectorEstimator(seed=123)``. + .. plot:: :include-source: @@ -151,7 +158,7 @@ def _run_pub(self, pub: EstimatorPub) -> PubResult: for index in np.ndindex(*bc_circuits.shape): bound_circuit = bc_circuits[index] observable = bc_obs[index] - final_state = Statevector(bound_circuit_to_instruction(bound_circuit)) + final_state = _statevector_from_circuit(bound_circuit, rng) paulis, coeffs = zip(*observable.items()) obs = SparsePauliOp(paulis, coeffs) # TODO: support non Pauli operators expectation_value = np.real_if_close(final_state.expectation_value(obs)) diff --git a/qiskit/primitives/utils.py b/qiskit/primitives/utils.py index db3fcbd132dc..0bc362aa0ef4 100644 --- a/qiskit/primitives/utils.py +++ b/qiskit/primitives/utils.py @@ -225,3 +225,23 @@ def bound_circuit_to_instruction(circuit: QuantumCircuit) -> Instruction: ) inst.definition = circuit return inst + + +def _statevector_from_circuit( + circuit: QuantumCircuit, rng: np.random.Generator | None +) -> Statevector: + """Generate a statevector from a circuit + + If the input circuit includes any resets for a some subsystem, + :meth:`.Statevector.reset` behaves in a stochastic way in :meth:`.Statevector.evolve`. + This function sets a random number generator to be reproducible. + + See :meth:`.Statevector.reset` for details. + + Args: + circuit: The quantum circuit. + seed: The random number generator or None. + """ + sv = Statevector.from_int(0, 2**circuit.num_qubits) + sv.seed(rng) + return sv.evolve(bound_circuit_to_instruction(circuit)) diff --git a/qiskit/transpiler/passes/basis/basis_translator.py b/qiskit/transpiler/passes/basis/basis_translator.py index dda9b0d32acb..6483316abf79 100644 --- a/qiskit/transpiler/passes/basis/basis_translator.py +++ b/qiskit/transpiler/passes/basis/basis_translator.py @@ -162,7 +162,8 @@ def run(self, dag): # If the source basis is a subset of the target basis and we have no circuit # instructions on qargs that have non-global operations there is nothing to # translate and we can exit early. - if source_basis.issubset(target_basis) and not qargs_local_source_basis: + source_basis_names = {x[0] for x in source_basis} + if source_basis_names.issubset(target_basis) and not qargs_local_source_basis: return dag logger.info( diff --git a/qiskit/transpiler/passes/optimization/consolidate_blocks.py b/qiskit/transpiler/passes/optimization/consolidate_blocks.py index 72a08efe0f7d..12f7285af9dc 100644 --- a/qiskit/transpiler/passes/optimization/consolidate_blocks.py +++ b/qiskit/transpiler/passes/optimization/consolidate_blocks.py @@ -28,6 +28,7 @@ from qiskit.transpiler.passes.synthesis import unitary_synthesis from qiskit.circuit.controlflow import CONTROL_FLOW_OP_NAMES from qiskit._accelerate.convert_2q_block_matrix import blocks_to_matrix +from qiskit.exceptions import QiskitError from .collect_1q_runs import Collect1qRuns from .collect_2q_blocks import Collect2qBlocks @@ -125,7 +126,12 @@ def run(self, dag): qc.append(nd.op, [q[block_index_map[i]] for i in nd.qargs]) unitary = UnitaryGate(Operator(qc), check_input=False) else: - matrix = blocks_to_matrix(block, block_index_map) + try: + matrix = blocks_to_matrix(block, block_index_map) + except QiskitError: + # If building a matrix for the block fails we should not consolidate it + # because there is nothing we can do with it. + continue unitary = UnitaryGate(matrix, check_input=False) max_2q_depth = 20 # If depth > 20, there will be 1q gates to consolidate. diff --git a/qiskit/transpiler/preset_passmanagers/builtin_plugins.py b/qiskit/transpiler/preset_passmanagers/builtin_plugins.py index d7e6a3b2c174..7e3e2611546f 100644 --- a/qiskit/transpiler/preset_passmanagers/builtin_plugins.py +++ b/qiskit/transpiler/preset_passmanagers/builtin_plugins.py @@ -14,7 +14,6 @@ import os -from qiskit.circuit import Instruction from qiskit.transpiler.passes.optimization.split_2q_unitaries import Split2QUnitaries from qiskit.transpiler.passmanager import PassManager from qiskit.transpiler.exceptions import TranspilerError @@ -66,7 +65,6 @@ CYGate, SXGate, SXdgGate, - get_standard_gate_name_mapping, ) from qiskit.utils.parallel import CPU_COUNT from qiskit import user_config @@ -173,58 +171,16 @@ def pass_manager(self, pass_manager_config, optimization_level=None) -> PassMana ) ) init.append(CommutativeCancellation()) - # skip peephole optimization before routing if target basis gate set is discrete, - # i.e. only consists of Cliffords that an user might want to keep - # use rz, sx, x, cx as basis, rely on physical optimziation to fix everything later one - stdgates = get_standard_gate_name_mapping() - - def _is_one_op_non_discrete(ops): - """Checks if one operation in `ops` is not discrete, i.e. is parameterizable - Args: - ops (List(Operation)): list of operations to check - Returns - True if at least one operation in `ops` is not discrete, False otherwise - """ - found_one_continuous_gate = False - for op in ops: - if isinstance(op, str): - if op in _discrete_skipped_ops: - continue - op = stdgates.get(op, None) - - if op is not None and op.name in _discrete_skipped_ops: - continue - - if op is None or not isinstance(op, Instruction): - return False - - if len(op.params) > 0: - found_one_continuous_gate = True - return found_one_continuous_gate - - target = pass_manager_config.target - basis = pass_manager_config.basis_gates - # consolidate gates before routing if the user did not specify a discrete basis gate, i.e. - # * no target or basis gate set has been specified - # * target has been specified, and we have one non-discrete gate in the target's spec - # * basis gates have been specified, and we have one non-discrete gate in that set - do_consolidate_blocks_init = target is None and basis is None - do_consolidate_blocks_init |= target is not None and _is_one_op_non_discrete( - target.operations - ) - do_consolidate_blocks_init |= basis is not None and _is_one_op_non_discrete(basis) - - if do_consolidate_blocks_init: - init.append(Collect2qBlocks()) - init.append(ConsolidateBlocks()) - # If approximation degree is None that indicates a request to approximate up to the - # error rates in the target. However, in the init stage we don't yet know the target - # qubits being used to figure out the fidelity so just use the default fidelity parameter - # in this case. - if pass_manager_config.approximation_degree is not None: - init.append(Split2QUnitaries(pass_manager_config.approximation_degree)) - else: - init.append(Split2QUnitaries()) + init.append(Collect2qBlocks()) + init.append(ConsolidateBlocks()) + # If approximation degree is None that indicates a request to approximate up to the + # error rates in the target. However, in the init stage we don't yet know the target + # qubits being used to figure out the fidelity so just use the default fidelity parameter + # in this case. + if pass_manager_config.approximation_degree is not None: + init.append(Split2QUnitaries(pass_manager_config.approximation_degree)) + else: + init.append(Split2QUnitaries()) else: raise TranspilerError(f"Invalid optimization level {optimization_level}") return init diff --git a/qiskit/visualization/dag_visualization.py b/qiskit/visualization/dag_visualization.py index ad2fca6e9bcf..dae97b51b0a4 100644 --- a/qiskit/visualization/dag_visualization.py +++ b/qiskit/visualization/dag_visualization.py @@ -174,10 +174,13 @@ def node_attr_func(node): label = register_bit_labels.get( node.wire, f"q_{dag.find_bit(node.wire).index}" ) - else: + elif isinstance(node.wire, Clbit): label = register_bit_labels.get( node.wire, f"c_{dag.find_bit(node.wire).index}" ) + else: + label = str(node.wire.name) + n["label"] = label n["color"] = "black" n["style"] = "filled" @@ -187,10 +190,12 @@ def node_attr_func(node): label = register_bit_labels.get( node.wire, f"q[{dag.find_bit(node.wire).index}]" ) - else: + elif isinstance(node.wire, Clbit): label = register_bit_labels.get( node.wire, f"c[{dag.find_bit(node.wire).index}]" ) + else: + label = str(node.wire.name) n["label"] = label n["color"] = "black" n["style"] = "filled" @@ -203,8 +208,10 @@ def edge_attr_func(edge): e = {} if isinstance(edge, Qubit): label = register_bit_labels.get(edge, f"q_{dag.find_bit(edge).index}") - else: + elif isinstance(edge, Clbit): label = register_bit_labels.get(edge, f"c_{dag.find_bit(edge).index}") + else: + label = str(edge.name) e["label"] = label return e diff --git a/releasenotes/notes/fix-consolidate-blocks-custom-gate-no-target-e2d1e0b0ee7ace11.yaml b/releasenotes/notes/fix-consolidate-blocks-custom-gate-no-target-e2d1e0b0ee7ace11.yaml new file mode 100644 index 000000000000..e4cf03778e3a --- /dev/null +++ b/releasenotes/notes/fix-consolidate-blocks-custom-gate-no-target-e2d1e0b0ee7ace11.yaml @@ -0,0 +1,9 @@ +--- +fixes: + - | + Fixed a bug in the :class:`.ConsolidateBlocks` transpiler pass, when the + input circuit contains a custom opaque gate and neither the + ``basis_gates`` or ``target`` options are set the pass would raise a + ``QiskitError`` and fail. This has been corrected so that the in these + situations the transpiler pass will not consolidate the block identified + containing a custom gate instead of failing. diff --git a/releasenotes/notes/fix-estimator-reset-9e7539776df4cac4.yaml b/releasenotes/notes/fix-estimator-reset-9e7539776df4cac4.yaml new file mode 100644 index 000000000000..dba0d8f1c22b --- /dev/null +++ b/releasenotes/notes/fix-estimator-reset-9e7539776df4cac4.yaml @@ -0,0 +1,19 @@ +--- +features_primitives: + - | + :class:`.Estimator` and :class:`.StatevectorEstimator` return + expectation values in a stochastic way if the input circuit includes + a reset for a some subsystems. + The result was not reproducible, but it is now reproducible + if a random seed is set. For example:: + + from qiskit.primitives import StatevectorEstimator + + estimator = StatevectorEstimator(seed=123) + + or:: + + from qiskit.primitives import Estimator + + estimator = Estimator(options={"seed":123}) + diff --git a/releasenotes/notes/fix-var-wires-4ebc40e0b19df253.yaml b/releasenotes/notes/fix-var-wires-4ebc40e0b19df253.yaml new file mode 100644 index 000000000000..7cd1e74806b0 --- /dev/null +++ b/releasenotes/notes/fix-var-wires-4ebc40e0b19df253.yaml @@ -0,0 +1,8 @@ +--- +fixes: + - | + Fixed an issue with :func:`.dag_drawer` and :meth:`.DAGCircuit.draw` + when attempting to visualize a :class:`.DAGCircuit` instance that contained + :class:`.Var` wires. The visualizer would raise an exception trying to + do this which has been fixed so the expected visualization will be + generated. diff --git a/test/python/compiler/test_transpiler.py b/test/python/compiler/test_transpiler.py index f465c9997039..a348ad8b749d 100644 --- a/test/python/compiler/test_transpiler.py +++ b/test/python/compiler/test_transpiler.py @@ -84,7 +84,6 @@ from qiskit.transpiler import CouplingMap, Layout, PassManager, TransformationPass from qiskit.transpiler.exceptions import TranspilerError, CircuitTooWideForTarget from qiskit.transpiler.passes import BarrierBeforeFinalMeasurements, GateDirection, VF2PostLayout -from qiskit.transpiler.passes.optimization.split_2q_unitaries import Split2QUnitaries from qiskit.transpiler.passmanager_config import PassManagerConfig from qiskit.transpiler.preset_passmanagers import generate_preset_pass_manager, level_0_pass_manager @@ -874,42 +873,6 @@ def test_do_not_run_gatedirection_with_symmetric_cm(self): transpile(circ, coupling_map=coupling_map, initial_layout=layout) self.assertFalse(mock_pass.called) - def tests_conditional_run_split_2q_unitaries(self): - """Tests running `Split2QUnitaries` when basis gate set is (non-) discrete""" - qc = QuantumCircuit(3) - qc.sx(0) - qc.t(0) - qc.cx(0, 1) - qc.cx(1, 2) - - orig_pass = Split2QUnitaries() - with patch.object(Split2QUnitaries, "run", wraps=orig_pass.run) as mock_pass: - basis = ["t", "sx", "cx"] - backend = GenericBackendV2(3, basis_gates=basis) - transpile(qc, backend=backend) - transpile(qc, basis_gates=basis) - transpile(qc, target=backend.target) - self.assertFalse(mock_pass.called) - - orig_pass = Split2QUnitaries() - with patch.object(Split2QUnitaries, "run", wraps=orig_pass.run) as mock_pass: - basis = ["rz", "sx", "cx"] - backend = GenericBackendV2(3, basis_gates=basis) - transpile(qc, backend=backend, optimization_level=2) - self.assertTrue(mock_pass.called) - mock_pass.called = False - transpile(qc, basis_gates=basis, optimization_level=2) - self.assertTrue(mock_pass.called) - mock_pass.called = False - transpile(qc, target=backend.target, optimization_level=2) - self.assertTrue(mock_pass.called) - mock_pass.called = False - transpile(qc, backend=backend, optimization_level=3) - self.assertTrue(mock_pass.called) - mock_pass.called = False - transpile(qc, basis_gates=basis, optimization_level=3) - self.assertTrue(mock_pass.called) - def test_optimize_to_nothing(self): """Optimize gates up to fixed point in the default pipeline See https://github.com/Qiskit/qiskit-terra/issues/2035 diff --git a/test/python/primitives/test_estimator.py b/test/python/primitives/test_estimator.py index 783461c7e4ad..2c251af65d1b 100644 --- a/test/python/primitives/test_estimator.py +++ b/test/python/primitives/test_estimator.py @@ -13,6 +13,7 @@ """Tests for Estimator.""" import unittest +from test import QiskitTestCase import numpy as np from ddt import data, ddt, unpack @@ -24,7 +25,6 @@ from qiskit.primitives.base import validation from qiskit.primitives.utils import _observable_key from qiskit.quantum_info import Pauli, SparsePauliOp -from test import QiskitTestCase # pylint: disable=wrong-import-order class TestEstimator(QiskitTestCase): @@ -355,6 +355,41 @@ def get_op(i): keys = [_observable_key(get_op(i)) for i in range(5)] self.assertEqual(len(keys), len(set(keys))) + def test_reset(self): + """Test for circuits with reset.""" + qc = QuantumCircuit(2) + qc.h(0) + qc.cx(0, 1) + qc.reset(0) + op = SparsePauliOp("ZI") + + seed = 12 + n = 1000 + with self.subTest("shots=None"): + with self.assertWarns(DeprecationWarning): + estimator = Estimator(options={"seed": seed}) + result = estimator.run([qc for _ in range(n)], [op] * n).result() + # expectation values should be stochastic due to reset for subsystems + np.testing.assert_allclose(result.values.mean(), 0, atol=1e-1) + + with self.assertWarns(DeprecationWarning): + result2 = estimator.run([qc for _ in range(n)], [op] * n).result() + # expectation values should be reproducible due to seed + np.testing.assert_allclose(result.values, result2.values) + + with self.subTest("shots=10000"): + shots = 10000 + with self.assertWarns(DeprecationWarning): + estimator = Estimator(options={"seed": seed}) + result = estimator.run([qc for _ in range(n)], [op] * n, shots=shots).result() + # expectation values should be stochastic due to reset for subsystems + np.testing.assert_allclose(result.values.mean(), 0, atol=1e-1) + + with self.assertWarns(DeprecationWarning): + result2 = estimator.run([qc for _ in range(n)], [op] * n, shots=shots).result() + # expectation values should be reproducible due to seed + np.testing.assert_allclose(result.values, result2.values) + @ddt class TestObservableValidation(QiskitTestCase): diff --git a/test/python/primitives/test_statevector_estimator.py b/test/python/primitives/test_statevector_estimator.py index 4eaa70e07a07..0cd76550f844 100644 --- a/test/python/primitives/test_statevector_estimator.py +++ b/test/python/primitives/test_statevector_estimator.py @@ -13,17 +13,17 @@ """Tests for Estimator.""" import unittest +from test import QiskitTestCase import numpy as np from qiskit.circuit import Parameter, QuantumCircuit from qiskit.circuit.library import RealAmplitudes from qiskit.primitives import StatevectorEstimator +from qiskit.primitives.containers.bindings_array import BindingsArray from qiskit.primitives.containers.estimator_pub import EstimatorPub from qiskit.primitives.containers.observables_array import ObservablesArray -from qiskit.primitives.containers.bindings_array import BindingsArray from qiskit.quantum_info import SparsePauliOp -from test import QiskitTestCase # pylint: disable=wrong-import-order class TestStatevectorEstimator(QiskitTestCase): @@ -307,6 +307,36 @@ def test_metadata(self): result[1].metadata, {"target_precision": 0.1, "circuit_metadata": qc2.metadata} ) + def test_reset(self): + """Test for circuits with reset.""" + qc = QuantumCircuit(2) + qc.h(0) + qc.cx(0, 1) + qc.reset(0) + op = SparsePauliOp("ZI") + + seed = 12 + n = 1000 + estimator = StatevectorEstimator(seed=seed) + with self.subTest("precision=0"): + result = estimator.run([(qc, [op] * n)]).result() + # expectation values should be stochastic due to reset for subsystems + np.testing.assert_allclose(result[0].data.evs.mean(), 0, atol=1e-1) + + result2 = estimator.run([(qc, [op] * n)]).result() + # expectation values should be reproducible due to seed + np.testing.assert_allclose(result[0].data.evs, result2[0].data.evs) + + with self.subTest("precision=0.01"): + precision = 0.01 + result = estimator.run([(qc, [op] * n)], precision=precision).result() + # expectation values should be stochastic due to reset for subsystems + np.testing.assert_allclose(result[0].data.evs.mean(), 0, atol=1e-1) + + result2 = estimator.run([(qc, [op] * n)], precision=precision).result() + # expectation values should be reproducible due to seed + np.testing.assert_allclose(result[0].data.evs, result2[0].data.evs) + if __name__ == "__main__": unittest.main() diff --git a/test/python/transpiler/test_consolidate_blocks.py b/test/python/transpiler/test_consolidate_blocks.py index 8a11af2bd688..1984ad1a3dc4 100644 --- a/test/python/transpiler/test_consolidate_blocks.py +++ b/test/python/transpiler/test_consolidate_blocks.py @@ -17,7 +17,7 @@ import unittest import numpy as np -from qiskit.circuit import QuantumCircuit, QuantumRegister, IfElseOp +from qiskit.circuit import QuantumCircuit, QuantumRegister, IfElseOp, Gate from qiskit.circuit.library import U2Gate, SwapGate, CXGate, CZGate, UnitaryGate from qiskit.converters import circuit_to_dag from qiskit.transpiler.passes import ConsolidateBlocks @@ -553,6 +553,23 @@ def test_inverted_order(self): ) self.assertEqual(expected, actual) + def test_custom_no_target(self): + """Test pass doesn't fail with custom gate.""" + + class MyCustomGate(Gate): + """Custom gate.""" + + def __init__(self): + super().__init__(name="my_custom", num_qubits=2, params=[]) + + qc = QuantumCircuit(2) + qc.append(MyCustomGate(), [0, 1]) + + pm = PassManager([Collect2qBlocks(), ConsolidateBlocks()]) + res = pm.run(qc) + + self.assertEqual(res, qc) + if __name__ == "__main__": unittest.main() diff --git a/test/python/visualization/test_dag_drawer.py b/test/python/visualization/test_dag_drawer.py index 4b920390e880..d789b1e70682 100644 --- a/test/python/visualization/test_dag_drawer.py +++ b/test/python/visualization/test_dag_drawer.py @@ -16,12 +16,14 @@ import tempfile import unittest -from qiskit.circuit import QuantumRegister, ClassicalRegister, QuantumCircuit, Qubit, Clbit +from qiskit.circuit import QuantumRegister, ClassicalRegister, QuantumCircuit, Qubit, Clbit, Store from qiskit.visualization import dag_drawer from qiskit.exceptions import InvalidFileError from qiskit.visualization import VisualizationError from qiskit.converters import circuit_to_dag, circuit_to_dagdependency from qiskit.utils import optionals as _optionals +from qiskit.dagcircuit import DAGCircuit +from qiskit.circuit.classical import expr, types from .visualization import path_to_diagram_reference, QiskitVisualizationTestCase @@ -108,6 +110,17 @@ def test_dag_drawer_with_dag_dep(self): image = Image.open(tmp_path) self.assertImagesAreEqual(image, image_ref, 0.1) + @unittest.skipUnless(_optionals.HAS_GRAPHVIZ, "Graphviz not installed") + @unittest.skipUnless(_optionals.HAS_PIL, "PIL not installed") + def test_dag_drawer_with_var_wires(self): + """Test visualization works with var nodes.""" + a = expr.Var.new("a", types.Bool()) + dag = DAGCircuit() + dag.add_input_var(a) + dag.apply_operation_back(Store(a, a), (), ()) + image = dag_drawer(dag) + self.assertIsNotNone(image) + if __name__ == "__main__": unittest.main(verbosity=2)