From 8c50ce46316a38898884449ace5411122bf5c7ba Mon Sep 17 00:00:00 2001 From: Matthew Treinish Date: Wed, 19 Feb 2025 04:29:59 -0500 Subject: [PATCH 01/10] Add method to compute estimated duration of scheduled circuit (#13783) * Add method to compute estimated duration of scheduled circuit This commit adds a new QuantumCircuit method to compute the estimated duration of a scheduled circuit. This is to replace the deprecated duration attribute that the transpiler was potentially setting during the scheduling stage. This method computes the longest duration path in the dag view of the circuit internally. This method should have been included in the 1.2.0 release prior to the deprecation of the `QuantumCircuit.duration` attribute in 1.3.0. But, this was an oversight in the path to deprecation, as it was part of larger deprecation of numerous scheduling pieces in the 1.3.0. We should definitely backport this for the 1.4.0 release for inclusion in that release prior to the Qiskit 2.0.0 release which removes the deprecated attribute * Simplify dag node indexing * Expand docs * Fix handling for StandardInstruction in rust and add first test * Expand test coverage * Fix lint --- crates/accelerate/src/circuit_duration.rs | 109 ++++++++++++++++ crates/accelerate/src/lib.rs | 1 + .../accelerate/src/target_transpiler/mod.rs | 11 ++ crates/pyext/src/lib.rs | 1 + qiskit/__init__.py | 1 + qiskit/circuit/quantumcircuit.py | 65 +++++++++- ...mate_duration-method-a35bf8eef4b2f210.yaml | 7 + test/python/circuit/test_scheduled_circuit.py | 120 ++++++++++++++++++ 8 files changed, 314 insertions(+), 1 deletion(-) create mode 100644 crates/accelerate/src/circuit_duration.rs create mode 100644 releasenotes/notes/add-estimate_duration-method-a35bf8eef4b2f210.yaml diff --git a/crates/accelerate/src/circuit_duration.rs b/crates/accelerate/src/circuit_duration.rs new file mode 100644 index 000000000000..0e9738822f84 --- /dev/null +++ b/crates/accelerate/src/circuit_duration.rs @@ -0,0 +1,109 @@ +// This code is part of Qiskit. +// +// (C) Copyright IBM 2025 +// +// 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 pyo3::prelude::*; +use pyo3::wrap_pyfunction; + +use qiskit_circuit::dag_circuit::{DAGCircuit, NodeType, Wire}; +use qiskit_circuit::operations::{DelayUnit, Operation, OperationRef, Param, StandardInstruction}; + +use crate::nlayout::PhysicalQubit; +use crate::target_transpiler::Target; +use crate::QiskitError; +use rustworkx_core::dag_algo::longest_path; +use rustworkx_core::petgraph::stable_graph::StableDiGraph; +use rustworkx_core::petgraph::visit::{EdgeRef, IntoEdgeReferences}; + +/// Estimate the duration of a scheduled circuit in seconds +#[pyfunction] +pub(crate) fn compute_estimated_duration(dag: &DAGCircuit, target: &Target) -> PyResult { + let dt = target.dt; + + let get_duration = + |edge: <&StableDiGraph as IntoEdgeReferences>::EdgeRef| -> PyResult { + let node_weight = &dag[edge.target()]; + match node_weight { + NodeType::Operation(inst) => { + let name = inst.op.name(); + let qubits = dag.get_qargs(inst.qubits); + let physical_qubits: Vec = + qubits.iter().map(|x| PhysicalQubit::new(x.0)).collect(); + + if let OperationRef::StandardInstruction(op) = inst.op.view() { + if let StandardInstruction::Delay(unit) = op { + let dur = &inst.params.as_ref().unwrap()[0]; + return if unit == DelayUnit::DT { + if let Some(dt) = dt { + match dur { + Param::Float(val) => + { + Ok(val / dt) + + }, + Param::Obj(val) => { + Python::with_gil(|py| { + let dur_float: f64 = val.extract(py)?; + Ok(dur_float * dt) + }) + }, + Param::ParameterExpression(_) => Err(QiskitError::new_err( + "Circuit contains parameterized delays, can't compute a duration estimate with this circuit" + )), + } + } else { + Err(QiskitError::new_err( + "Circuit contains delays in dt but the target doesn't specify dt" + )) + } + } else if unit == DelayUnit::S { + match dur { + Param::Float(val) => Ok(*val), + _ => Err(QiskitError::new_err( + "Invalid type for parameter value for delay in circuit", + )), + } + } else { + Err(QiskitError::new_err( + "Circuit contains delays in units other then seconds or dt, the circuit is not scheduled." + )) + }; + } else if let StandardInstruction::Barrier(_) = op { + return Ok(0.); + } + } + match target.get_duration(name, &physical_qubits) { + Some(dur) => Ok(dur), + None => Err(QiskitError::new_err(format!( + "Duration not found for {} on qubits: {:?}", + name, qubits + ))), + } + } + NodeType::QubitOut(_) | NodeType::ClbitOut(_) => Ok(0.), + NodeType::ClbitIn(_) | NodeType::QubitIn(_) => { + Err(QiskitError::new_err("Invalid circuit provided")) + } + _ => Err(QiskitError::new_err( + "Circuit contains Vars, duration can't be calculated with classical variables", + )), + } + }; + match longest_path(dag.dag(), get_duration)? { + Some((_, weight)) => Ok(weight), + None => Err(QiskitError::new_err("Invalid circuit provided")), + } +} + +pub fn compute_duration(m: &Bound) -> PyResult<()> { + m.add_wrapped(wrap_pyfunction!(compute_estimated_duration))?; + Ok(()) +} diff --git a/crates/accelerate/src/lib.rs b/crates/accelerate/src/lib.rs index e26689f4f0c3..76b26d22a771 100644 --- a/crates/accelerate/src/lib.rs +++ b/crates/accelerate/src/lib.rs @@ -17,6 +17,7 @@ use pyo3::import_exception; pub mod barrier_before_final_measurement; pub mod basis; pub mod check_map; +pub mod circuit_duration; pub mod circuit_library; pub mod commutation_analysis; pub mod commutation_cancellation; diff --git a/crates/accelerate/src/target_transpiler/mod.rs b/crates/accelerate/src/target_transpiler/mod.rs index 9df6047a494b..b03f2e95523f 100644 --- a/crates/accelerate/src/target_transpiler/mod.rs +++ b/crates/accelerate/src/target_transpiler/mod.rs @@ -977,6 +977,17 @@ impl Target { }) } + /// Get the duration of a given instruction in the target + pub fn get_duration(&self, name: &str, qargs: &[PhysicalQubit]) -> Option { + self.gate_map.get(name).and_then(|gate_props| { + let qargs_key: Qargs = qargs.iter().cloned().collect(); + match gate_props.get(Some(&qargs_key)) { + Some(props) => props.as_ref().and_then(|inst_props| inst_props.duration), + None => None, + } + }) + } + /// Get an iterator over the indices of all physical qubits of the target pub fn physical_qubits(&self) -> impl ExactSizeIterator { 0..self.num_qubits.unwrap_or_default() diff --git a/crates/pyext/src/lib.rs b/crates/pyext/src/lib.rs index d2a0db8d19fd..f93f2af32a81 100644 --- a/crates/pyext/src/lib.rs +++ b/crates/pyext/src/lib.rs @@ -31,6 +31,7 @@ fn _accelerate(m: &Bound) -> PyResult<()> { add_submodule(m, ::qiskit_accelerate::barrier_before_final_measurement::barrier_before_final_measurements_mod, "barrier_before_final_measurement")?; add_submodule(m, ::qiskit_accelerate::basis::basis, "basis")?; add_submodule(m, ::qiskit_accelerate::check_map::check_map_mod, "check_map")?; + add_submodule(m, ::qiskit_accelerate::circuit_duration::compute_duration, "circuit_duration")?; add_submodule(m, ::qiskit_accelerate::circuit_library::circuit_library, "circuit_library")?; add_submodule(m, ::qiskit_accelerate::commutation_analysis::commutation_analysis, "commutation_analysis")?; add_submodule(m, ::qiskit_accelerate::commutation_cancellation::commutation_cancellation, "commutation_cancellation")?; diff --git a/qiskit/__init__.py b/qiskit/__init__.py index 376881a56799..9ac2702f4cf2 100644 --- a/qiskit/__init__.py +++ b/qiskit/__init__.py @@ -110,6 +110,7 @@ sys.modules["qiskit._accelerate.twirling"] = _accelerate.twirling sys.modules["qiskit._accelerate.high_level_synthesis"] = _accelerate.high_level_synthesis sys.modules["qiskit._accelerate.remove_identity_equiv"] = _accelerate.remove_identity_equiv +sys.modules["qiskit._accelerate.circuit_duration"] = _accelerate.circuit_duration from qiskit.exceptions import QiskitError, MissingOptionalLibraryError diff --git a/qiskit/circuit/quantumcircuit.py b/qiskit/circuit/quantumcircuit.py index 012995e5021e..e73769a42bc0 100644 --- a/qiskit/circuit/quantumcircuit.py +++ b/qiskit/circuit/quantumcircuit.py @@ -41,6 +41,7 @@ import numpy as np from qiskit._accelerate.circuit import CircuitData from qiskit._accelerate.circuit import StandardGate +from qiskit._accelerate.circuit_duration import compute_estimated_duration from qiskit.exceptions import QiskitError from qiskit.utils.multiprocessing import is_main_process from qiskit.circuit.instruction import Instruction @@ -157,6 +158,8 @@ class QuantumCircuit: :attr:`data` List of individual :class:`CircuitInstruction`\\ s that make up the circuit. :attr:`duration` Total duration of the circuit, added by scheduling transpiler passes. + This attribute is deprecated and :meth:`.estimate_duration` should + be used instead. :attr:`layout` Hardware layout and routing information added by the transpiler. :attr:`num_ancillas` The number of ancilla qubits in the circuit. @@ -909,8 +912,9 @@ class QuantumCircuit: If a :class:`QuantumCircuit` has been scheduled as part of a transpilation pipeline, the timing information for individual qubits can be accessed. The whole-circuit timing information is - available through the :attr:`duration`, :attr:`unit` and :attr:`op_start_times` attributes. + available through the :meth:`estimate_duration` method and :attr:`op_start_times` attribute. + .. automethod:: estimate_duration .. automethod:: qubit_duration .. automethod:: qubit_start_time .. automethod:: qubit_stop_time @@ -6919,6 +6923,65 @@ def qubit_stop_time(self, *qubits: Union[Qubit, int]) -> float: else: return 0 # If there are no instructions over bits + def estimate_duration(self, target, unit: str = "s") -> int | float: + """Estimate the duration of a scheduled circuit + + This method computes the estimate of the circuit duration by finding + the longest duration path in the circuit based on the durations + provided by a given target. This method only works for simple circuits + that have no control flow or other classical feed-forward operations. + + Args: + target (Target): The :class:`.Target` instance that contains durations for + the instructions if the target is missing duration data for any of the + instructions in the circuit an :class:`.QiskitError` will be raised. This + should be the same target object used as the target for transpilation. + unit: The unit to return the duration in. This defaults to "s" for seconds + but this can be a supported SI prefix for seconds returns. For example + setting this to "n" will return in unit of nanoseconds. Supported values + of this type are "f", "p", "n", "u", "µ", "m", "k", "M", "G", "T", and + "P". Additionally, a value of "dt" is also accepted to output an integer + in units of "dt". For this to function "dt" must be specified in the + ``target``. + + Returns: + The estimated duration for the execution of a single shot of the circuit in + the specified unit. + + Raises: + QiskitError: If the circuit is not scheduled or contains other + details that prevent computing an estimated duration from + (such as parameterized delay). + """ + from qiskit.converters import circuit_to_dag + + dur = compute_estimated_duration(circuit_to_dag(self), target) + if unit == "s": + return dur + if unit == "dt": + from qiskit.circuit.duration import duration_in_dt # pylint: disable=cyclic-import + + return duration_in_dt(dur, target.dt) + + prefix_dict = { + "f": 1e-15, + "p": 1e-12, + "n": 1e-9, + "u": 1e-6, + "µ": 1e-6, + "m": 1e-3, + "k": 1e3, + "M": 1e6, + "G": 1e9, + "T": 1e12, + "P": 1e15, + } + if unit not in prefix_dict: + raise QiskitError( + f"Specified unit: {unit} is not a valid/supported SI prefix, 's', or 'dt'" + ) + return dur / prefix_dict[unit] + class _OuterCircuitScopeInterface(CircuitScopeInterface): # This is an explicit interface-fulfilling object friend of QuantumCircuit that acts as its diff --git a/releasenotes/notes/add-estimate_duration-method-a35bf8eef4b2f210.yaml b/releasenotes/notes/add-estimate_duration-method-a35bf8eef4b2f210.yaml new file mode 100644 index 000000000000..ae6bff75d89e --- /dev/null +++ b/releasenotes/notes/add-estimate_duration-method-a35bf8eef4b2f210.yaml @@ -0,0 +1,7 @@ +--- +features_circuits: + - | + Added a new method, :meth:`.QuantumCircuit.estimate_duration`, to compute + the estimated duration of a scheduled circuit output from the :mod:`.transpiler`. + This should be used if you need an estimate of the full circuit duration instead + of the deprecated :attr:`.QuantumCircuit.duration` attribute. diff --git a/test/python/circuit/test_scheduled_circuit.py b/test/python/circuit/test_scheduled_circuit.py index c09265d51b8b..0793139429ef 100644 --- a/test/python/circuit/test_scheduled_circuit.py +++ b/test/python/circuit/test_scheduled_circuit.py @@ -469,6 +469,126 @@ def test_convert_duration_to_dt(self): ref_unit, ) + @data("s", "dt", "f", "p", "n", "u", "µ", "m", "k", "M", "G", "T", "P") + def test_estimate_duration(self, unit): + """Test the circuit duration is computed correctly.""" + backend = GenericBackendV2(num_qubits=3, seed=42) + + circ = QuantumCircuit(2) + circ.cx(0, 1) + circ.measure_all() + + circuit_dt = transpile(circ, backend, scheduling_method="asap") + duration = circuit_dt.estimate_duration(backend.target, unit=unit) + expected_in_sec = 1.815516e-06 + expected_val = { + "s": expected_in_sec, + "dt": int(expected_in_sec / backend.target.dt), + "f": expected_in_sec / 1e-15, + "p": expected_in_sec / 1e-12, + "n": expected_in_sec / 1e-9, + "u": expected_in_sec / 1e-6, + "µ": expected_in_sec / 1e-6, + "m": expected_in_sec / 1e-3, + "k": expected_in_sec / 1e3, + "M": expected_in_sec / 1e6, + "G": expected_in_sec / 1e9, + "T": expected_in_sec / 1e12, + "P": expected_in_sec / 1e15, + } + self.assertEqual(duration, expected_val[unit]) + + @data("s", "dt", "f", "p", "n", "u", "µ", "m", "k", "M", "G", "T", "P") + def test_estimate_duration_with_long_delay(self, unit): + """Test the circuit duration is computed correctly.""" + backend = GenericBackendV2(num_qubits=3, seed=42) + + circ = QuantumCircuit(3) + circ.cx(0, 1) + circ.measure_all() + circ.delay(1e15, 2) + + circuit_dt = transpile(circ, backend, scheduling_method="asap") + duration = circuit_dt.estimate_duration(backend.target, unit=unit) + expected_in_sec = 222000.00000139928 + expected_val = { + "s": expected_in_sec, + "dt": int(expected_in_sec / backend.target.dt), + "f": expected_in_sec / 1e-15, + "p": expected_in_sec / 1e-12, + "n": expected_in_sec / 1e-9, + "u": expected_in_sec / 1e-6, + "µ": expected_in_sec / 1e-6, + "m": expected_in_sec / 1e-3, + "k": expected_in_sec / 1e3, + "M": expected_in_sec / 1e6, + "G": expected_in_sec / 1e9, + "T": expected_in_sec / 1e12, + "P": expected_in_sec / 1e15, + } + self.assertEqual(duration, expected_val[unit]) + + def test_estimate_duration_invalid_unit(self): + backend = GenericBackendV2(num_qubits=3, seed=42) + + circ = QuantumCircuit(2) + circ.cx(0, 1) + circ.measure_all() + + circuit_dt = transpile(circ, backend, scheduling_method="asap") + with self.assertRaises(QiskitError): + circuit_dt.estimate_duration(backend.target, unit="jiffy") + + def test_delay_circ(self): + backend = GenericBackendV2(num_qubits=3, seed=42) + + circ = QuantumCircuit(2) + circ.delay(100, 0, unit="dt") + + circuit_dt = transpile(circ, backend, scheduling_method="asap") + res = circuit_dt.estimate_duration(backend.target, unit="dt") + self.assertIsInstance(res, int) + self.assertEqual(res, 100) + + def test_estimate_duration_control_flow(self): + backend = GenericBackendV2(num_qubits=3, seed=42, control_flow=True) + + circ = QuantumCircuit(2) + circ.cx(0, 1) + circ.measure_all() + with circ.if_test((0, True)): + circ.x(0) + with self.assertRaises(QiskitError): + circ.estimate_duration(backend.target) + + def test_estimate_duration_with_var(self): + backend = GenericBackendV2(num_qubits=3, seed=42, control_flow=True) + + circ = QuantumCircuit(2) + circ.cx(0, 1) + circ.measure_all() + circ.add_var("a", False) + with self.assertRaises(QiskitError): + circ.estimate_duration(backend.target) + + def test_estimate_duration_parameterized_delay(self): + backend = GenericBackendV2(num_qubits=3, seed=42, control_flow=True) + + circ = QuantumCircuit(2) + circ.cx(0, 1) + circ.measure_all() + circ.delay(Parameter("t"), 0) + with self.assertRaises(QiskitError): + circ.estimate_duration(backend.target) + + def test_estimate_duration_dt_delay_no_dt(self): + backend = GenericBackendV2(num_qubits=3, seed=42) + circ = QuantumCircuit(1) + circ.delay(100, 0) + backend.target.dt = None + with self.assertRaises(QiskitError): + circ.estimate_duration(backend.target) + def test_change_dt_in_transpile(self): qc = QuantumCircuit(1, 1) qc.x(0) From bd30693331bf308c3a06aa7a18e326ceed0e6fa4 Mon Sep 17 00:00:00 2001 From: Matthew Treinish Date: Wed, 19 Feb 2025 05:43:52 -0500 Subject: [PATCH 02/10] Remove deprecated legacy scheduling passes (#13876) This commit removes the deprecated legacy scheduling passes. These passes were replaced by the new scheduling passes a long time ago and finally marked as deprecated in 1.1.0. This commit follows through on the promise of that deprecation and removes the passes marked as deprecated. --- qiskit/transpiler/passes/__init__.py | 8 - .../transpiler/passes/scheduling/__init__.py | 6 - qiskit/transpiler/passes/scheduling/alap.py | 153 ---- .../passes/scheduling/alignments/__init__.py | 1 - .../scheduling/alignments/align_measures.py | 255 ------ qiskit/transpiler/passes/scheduling/asap.py | 175 ---- .../passes/scheduling/base_scheduler.py | 310 ------- .../passes/scheduling/dynamical_decoupling.py | 313 ------- ...cy-scheduling-passes-ee1d593c41fe95c6.yaml | 9 + .../transpiler/legacy_scheduling/__init__.py | 13 - .../test_instruction_alignments.py | 327 ------- .../legacy_scheduling/test_scheduling_pass.py | 864 ------------------ 12 files changed, 9 insertions(+), 2425 deletions(-) delete mode 100644 qiskit/transpiler/passes/scheduling/alap.py delete mode 100644 qiskit/transpiler/passes/scheduling/alignments/align_measures.py delete mode 100644 qiskit/transpiler/passes/scheduling/asap.py delete mode 100644 qiskit/transpiler/passes/scheduling/base_scheduler.py delete mode 100644 qiskit/transpiler/passes/scheduling/dynamical_decoupling.py create mode 100644 releasenotes/notes/drop-legacy-scheduling-passes-ee1d593c41fe95c6.yaml delete mode 100644 test/python/transpiler/legacy_scheduling/__init__.py delete mode 100644 test/python/transpiler/legacy_scheduling/test_instruction_alignments.py delete mode 100644 test/python/transpiler/legacy_scheduling/test_scheduling_pass.py diff --git a/qiskit/transpiler/passes/__init__.py b/qiskit/transpiler/passes/__init__.py index 1fd8454159a3..ae4bb218ef4a 100644 --- a/qiskit/transpiler/passes/__init__.py +++ b/qiskit/transpiler/passes/__init__.py @@ -115,10 +115,6 @@ ConstrainedReschedule InstructionDurationCheck SetIOLatency - ALAPSchedule - ASAPSchedule - DynamicalDecoupling - AlignMeasures Circuit Analysis ================ @@ -273,10 +269,6 @@ from .scheduling import ConstrainedReschedule from .scheduling import InstructionDurationCheck from .scheduling import SetIOLatency -from .scheduling import ALAPSchedule -from .scheduling import ASAPSchedule -from .scheduling import DynamicalDecoupling -from .scheduling import AlignMeasures # additional utility passes from .utils import CheckMap diff --git a/qiskit/transpiler/passes/scheduling/__init__.py b/qiskit/transpiler/passes/scheduling/__init__.py index 2eeb29661d5e..4b02b471b07e 100644 --- a/qiskit/transpiler/passes/scheduling/__init__.py +++ b/qiskit/transpiler/passes/scheduling/__init__.py @@ -12,9 +12,6 @@ """Module containing circuit scheduling passes.""" -from .alap import ALAPSchedule -from .asap import ASAPSchedule -from .dynamical_decoupling import DynamicalDecoupling from .scheduling import ALAPScheduleAnalysis, ASAPScheduleAnalysis, SetIOLatency from .time_unit_conversion import TimeUnitConversion from .padding import PadDelay, PadDynamicalDecoupling @@ -22,6 +19,3 @@ # For backward compatibility from . import alignments as instruction_alignments - -# TODO Deprecated pass. Will be removed after deprecation period. -from .alignments import AlignMeasures diff --git a/qiskit/transpiler/passes/scheduling/alap.py b/qiskit/transpiler/passes/scheduling/alap.py deleted file mode 100644 index cdbdd4654f3c..000000000000 --- a/qiskit/transpiler/passes/scheduling/alap.py +++ /dev/null @@ -1,153 +0,0 @@ -# This code is part of Qiskit. -# -# (C) Copyright IBM 2020. -# -# 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. - -"""ALAP Scheduling.""" - -from qiskit.circuit import Delay, Qubit, Measure -from qiskit.dagcircuit import DAGCircuit -from qiskit.transpiler.exceptions import TranspilerError -from qiskit.transpiler.passes.scheduling.base_scheduler import BaseSchedulerTransform -from qiskit.utils.deprecation import deprecate_func - - -class ALAPSchedule(BaseSchedulerTransform): - """ALAP Scheduling pass, which schedules the **stop** time of instructions as late as possible. - - See :class:`~qiskit.transpiler.passes.scheduling.base_scheduler.BaseSchedulerTransform` for the - detailed behavior of the control flow operation, i.e. ``c_if``. - """ - - @deprecate_func( - additional_msg=( - "Instead, use :class:`~.ALAPScheduleAnalysis`, which is an " - "analysis pass that requires a padding pass to later modify the circuit." - ), - since="1.1.0", - ) - def __init__(self, *args, **kwargs): - super().__init__(*args, **kwargs) - - def run(self, dag): - """Run the ALAPSchedule pass on `dag`. - - Args: - dag (DAGCircuit): DAG to schedule. - - Returns: - DAGCircuit: A scheduled DAG. - - Raises: - TranspilerError: if the circuit is not mapped on physical qubits. - TranspilerError: if conditional bit is added to non-supported instruction. - """ - if len(dag.qregs) != 1 or dag.qregs.get("q", None) is None: - raise TranspilerError("ALAP schedule runs on physical circuits only") - - time_unit = self.property_set["time_unit"] - new_dag = DAGCircuit() - for qreg in dag.qregs.values(): - new_dag.add_qreg(qreg) - for creg in dag.cregs.values(): - new_dag.add_creg(creg) - - idle_before = {q: 0 for q in dag.qubits + dag.clbits} - for node in reversed(list(dag.topological_op_nodes())): - op_duration = self._get_node_duration(node, dag) - - # compute t0, t1: instruction interval, note that - # t0: start time of instruction - # t1: end time of instruction - - # since this is alap scheduling, node is scheduled in reversed topological ordering - # and nodes are packed from the very end of the circuit. - # the physical meaning of t0 and t1 is flipped here. - if isinstance(node.op, self.CONDITIONAL_SUPPORTED): - t0q = max(idle_before[q] for q in node.qargs) - if node.op.condition_bits: - # conditional is bit tricky due to conditional_latency - t0c = max(idle_before[c] for c in node.op.condition_bits) - # Assume following case (t0c > t0q): - # - # |t0q - # Q ░░░░░░░░░░░░░▒▒▒ - # C ░░░░░░░░▒▒▒▒▒▒▒▒ - # |t0c - # - # In this case, there is no actual clbit read before gate. - # - # |t0q' = t0c - conditional_latency - # Q ░░░░░░░░▒▒▒░░▒▒▒ - # C ░░░░░░▒▒▒▒▒▒▒▒▒▒ - # |t1c' = t0c + conditional_latency - # - # rather than naively doing - # - # |t1q' = t0c + duration - # Q ░░░░░▒▒▒░░░░░▒▒▒ - # C ░░▒▒░░░░▒▒▒▒▒▒▒▒ - # |t1c' = t0c + duration + conditional_latency - # - t0 = max(t0q, t0c - op_duration) - t1 = t0 + op_duration - for clbit in node.op.condition_bits: - idle_before[clbit] = t1 + self.conditional_latency - else: - t0 = t0q - t1 = t0 + op_duration - else: - if node.op.condition_bits: - raise TranspilerError( - f"Conditional instruction {node.op.name} is not supported in ALAP scheduler." - ) - - if isinstance(node.op, Measure): - # clbit time is always right (alap) justified - t0 = max(idle_before[bit] for bit in node.qargs + node.cargs) - t1 = t0 + op_duration - # - # |t1 = t0 + duration - # Q ░░░░░▒▒▒▒▒▒▒▒▒▒▒ - # C ░░░░░░░░░▒▒▒▒▒▒▒ - # |t0 + (duration - clbit_write_latency) - # - for clbit in node.cargs: - idle_before[clbit] = t0 + (op_duration - self.clbit_write_latency) - else: - # It happens to be directives such as barrier - t0 = max(idle_before[bit] for bit in node.qargs + node.cargs) - t1 = t0 + op_duration - - for bit in node.qargs: - delta = t0 - idle_before[bit] - if delta > 0 and self._delay_supported(dag.find_bit(bit).index): - new_dag.apply_operation_front(Delay(delta, time_unit), [bit], [], check=False) - idle_before[bit] = t1 - - new_dag.apply_operation_front(node.op, node.qargs, node.cargs, check=False) - - circuit_duration = max(idle_before.values()) - for bit, before in idle_before.items(): - delta = circuit_duration - before - if not (delta > 0 and isinstance(bit, Qubit)): - continue - if self._delay_supported(dag.find_bit(bit).index): - new_dag.apply_operation_front(Delay(delta, time_unit), [bit], [], check=False) - - new_dag.name = dag.name - new_dag.metadata = dag.metadata - new_dag._calibrations_prop = dag._calibrations_prop - - # set circuit duration and unit to indicate it is scheduled - new_dag.duration = circuit_duration - new_dag.unit = time_unit - - return new_dag diff --git a/qiskit/transpiler/passes/scheduling/alignments/__init__.py b/qiskit/transpiler/passes/scheduling/alignments/__init__.py index 8ecd68eacdbb..507fb95d3486 100644 --- a/qiskit/transpiler/passes/scheduling/alignments/__init__.py +++ b/qiskit/transpiler/passes/scheduling/alignments/__init__.py @@ -77,4 +77,3 @@ from .check_durations import InstructionDurationCheck from .reschedule import ConstrainedReschedule -from .align_measures import AlignMeasures diff --git a/qiskit/transpiler/passes/scheduling/alignments/align_measures.py b/qiskit/transpiler/passes/scheduling/alignments/align_measures.py deleted file mode 100644 index 5327d8b2a5ec..000000000000 --- a/qiskit/transpiler/passes/scheduling/alignments/align_measures.py +++ /dev/null @@ -1,255 +0,0 @@ -# This code is part of Qiskit. -# -# (C) Copyright IBM 2021. -# -# 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. - -"""Align measurement instructions.""" -from __future__ import annotations -import itertools -import warnings -from collections import defaultdict -from collections.abc import Iterable -from typing import Type - -from qiskit.circuit.quantumcircuit import ClbitSpecifier, QubitSpecifier - -from qiskit.circuit.delay import Delay -from qiskit.circuit.measure import Measure -from qiskit.circuit.parameterexpression import ParameterExpression -from qiskit.dagcircuit import DAGCircuit -from qiskit.transpiler.basepasses import TransformationPass -from qiskit.transpiler.exceptions import TranspilerError -from qiskit.utils.deprecation import deprecate_func - - -class AlignMeasures(TransformationPass): - """Measurement alignment. - - This is a control electronics aware optimization pass. - - In many quantum computing architectures gates (instructions) are implemented with - shaped analog stimulus signals. These signals are digitally stored in the - waveform memory of the control electronics and converted into analog voltage signals - by electronic components called digital to analog converters (DAC). - - In a typical hardware implementation of superconducting quantum processors, - a single qubit instruction is implemented by a - microwave signal with the duration of around several tens of ns with a per-sample - time resolution of ~0.1-10ns, as reported by ``backend.configuration().dt``. - In such systems requiring higher DAC bandwidth, control electronics often - defines a `pulse granularity`, in other words a data chunk, to allow the DAC to - perform the signal conversion in parallel to gain the bandwidth. - - Measurement alignment is required if a backend only allows triggering ``measure`` - instructions at a certain multiple value of this pulse granularity. - This value is usually provided by ``backend.configuration().timing_constraints``. - - In Qiskit SDK, the duration of delay can take arbitrary value in units of ``dt``, - thus circuits involving delays may violate the above alignment constraint (i.e. misalignment). - This pass shifts measurement instructions to a new time position to fix the misalignment, - by inserting extra delay right before the measure instructions. - The input of this pass should be scheduled :class:`~qiskit.dagcircuit.DAGCircuit`, - thus one should select one of the scheduling passes - (:class:`~qiskit.transpiler.passes.ALAPSchedule` or - :class:`~qiskit.trasnpiler.passes.ASAPSchedule`) before calling this. - - Examples: - We assume executing the following circuit on a backend with ``alignment=16``. - - .. code-block:: text - - ┌───┐┌────────────────┐┌─┐ - q_0: ┤ X ├┤ Delay(100[dt]) ├┤M├ - └───┘└────────────────┘└╥┘ - c: 1/════════════════════════╩═ - 0 - - Note that delay of 100 dt induces a misalignment of 4 dt at the measurement. - This pass appends an extra 12 dt time shift to the input circuit. - - .. code-block:: text - - ┌───┐┌────────────────┐┌─┐ - q_0: ┤ X ├┤ Delay(112[dt]) ├┤M├ - └───┘└────────────────┘└╥┘ - c: 1/════════════════════════╩═ - 0 - - This pass always inserts a positive delay before measurements - rather than reducing other delays. - - Notes: - The Backend may allow users to execute circuits violating the alignment constraint. - However, it may return meaningless measurement data mainly due to the phase error. - """ - - @deprecate_func( - additional_msg=( - "Instead, use :class:`~.ConstrainedReschedule`, which performs the same function " - "but also supports aligning to additional timing constraints." - ), - since="1.1.0", - ) - def __init__(self, alignment: int = 1): - """Create new pass. - - Args: - alignment: Integer number representing the minimum time resolution to - trigger measure instruction in units of ``dt``. This value depends on - the control electronics of your quantum processor. - """ - super().__init__() - self.alignment = alignment - - def run(self, dag: DAGCircuit): - """Run the measurement alignment pass on `dag`. - - Args: - dag (DAGCircuit): DAG to be checked. - - Returns: - DAGCircuit: DAG with consistent timing and op nodes annotated with duration. - - Raises: - TranspilerError: If circuit is not scheduled. - """ - time_unit = self.property_set["time_unit"] - - if not _check_alignment_required(dag, self.alignment, Measure): - # return input as-is to avoid unnecessary scheduling. - # because following procedure regenerate new DAGCircuit, - # we should avoid continuing if not necessary from performance viewpoint. - return dag - - # if circuit is not yet scheduled, schedule with ALAP method - if dag.duration is None: - raise TranspilerError( - f"This circuit {dag.name} may involve a delay instruction violating the " - "pulse controller alignment. To adjust instructions to " - "right timing, you should call one of scheduling passes first. " - "This is usually done by calling transpiler with scheduling_method='alap'." - ) - - # the following lines are basically copied from ASAPSchedule pass - # - # * some validations for non-scheduled nodes are dropped, since we assume scheduled input - # * pad_with_delay is called only with non-delay node to avoid consecutive delay - new_dag = dag.copy_empty_like() - - qubit_time_available: dict[QubitSpecifier, int] = defaultdict(int) # to track op start time - qubit_stop_times: dict[QubitSpecifier, int] = defaultdict( - int - ) # to track delay start time for padding - clbit_readable: dict[ClbitSpecifier, int] = defaultdict(int) - clbit_writeable: dict[ClbitSpecifier, int] = defaultdict(int) - - def pad_with_delays(qubits: Iterable[QubitSpecifier], until, unit) -> None: - """Pad idle time-slots in ``qubits`` with delays in ``unit`` until ``until``.""" - for q in qubits: - if qubit_stop_times[q] < until: - idle_duration = until - qubit_stop_times[q] - new_dag.apply_operation_back(Delay(idle_duration, unit), (q,), check=False) - - for node in dag.topological_op_nodes(): - # choose appropriate clbit available time depending on op - clbit_time_available = ( - clbit_writeable if isinstance(node.op, Measure) else clbit_readable - ) - # correction to change clbit start time to qubit start time - delta = node.op.duration if isinstance(node.op, Measure) else 0 - start_time = max( - itertools.chain( - (qubit_time_available[q] for q in node.qargs), - ( - clbit_time_available[c] - delta - for c in node.cargs + tuple(node.op.condition_bits) - ), - ) - ) - - if isinstance(node.op, Measure): - if start_time % self.alignment != 0: - start_time = ((start_time // self.alignment) + 1) * self.alignment - - if not isinstance(node.op, Delay): # exclude delays for combining consecutive delays - pad_with_delays(node.qargs, until=start_time, unit=time_unit) - new_dag.apply_operation_back(node.op, node.qargs, node.cargs, check=False) - - stop_time = start_time + node.op.duration - # update time table - for q in node.qargs: - qubit_time_available[q] = stop_time - if not isinstance(node.op, Delay): - qubit_stop_times[q] = stop_time - for c in node.cargs: # measure - clbit_writeable[c] = clbit_readable[c] = stop_time - for c in node.op.condition_bits: # conditional op - clbit_writeable[c] = max(start_time, clbit_writeable[c]) - - working_qubits = qubit_time_available.keys() - circuit_duration = max(qubit_time_available[q] for q in working_qubits) - pad_with_delays(new_dag.qubits, until=circuit_duration, unit=time_unit) - - new_dag.name = dag.name - new_dag.metadata = dag.metadata - - # set circuit duration and unit to indicate it is scheduled - new_dag.duration = circuit_duration - new_dag.unit = time_unit - - return new_dag - - -def _check_alignment_required( - dag: DAGCircuit, - alignment: int, - instructions: Type | list[Type], -) -> bool: - """Check DAG nodes and return a boolean representing if instruction scheduling is necessary. - - Args: - dag: DAG circuit to check. - alignment: Instruction alignment condition. - instructions: Target instructions. - - Returns: - If instruction scheduling is necessary. - """ - if not isinstance(instructions, list): - instructions = [instructions] - - if alignment == 1: - # disable alignment if arbitrary t0 value can be used - return False - - if all(len(dag.op_nodes(inst)) == 0 for inst in instructions): - # disable alignment if target instruction is not involved - return False - - # check delay durations - for delay_node in dag.op_nodes(Delay): - duration = delay_node.op.duration - if isinstance(duration, ParameterExpression): - # duration is parametrized: - # raise user warning if backend alignment is not 1. - warnings.warn( - f"Parametrized delay with {repr(duration)} is found in circuit {dag.name}. " - f"This backend requires alignment={alignment}. " - "Please make sure all assigned values are multiple values of the alignment.", - UserWarning, - ) - else: - # duration is bound: - # check duration and trigger alignment if it violates constraint - if duration % alignment != 0: - return True - - # disable alignment if all delays are multiple values of the alignment - return False diff --git a/qiskit/transpiler/passes/scheduling/asap.py b/qiskit/transpiler/passes/scheduling/asap.py deleted file mode 100644 index 13ff58b1b0f0..000000000000 --- a/qiskit/transpiler/passes/scheduling/asap.py +++ /dev/null @@ -1,175 +0,0 @@ -# This code is part of Qiskit. -# -# (C) Copyright IBM 2020. -# -# 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. - -"""ASAP Scheduling.""" - -from qiskit.circuit import Delay, Qubit, Measure -from qiskit.dagcircuit import DAGCircuit -from qiskit.transpiler.exceptions import TranspilerError -from qiskit.transpiler.passes.scheduling.base_scheduler import BaseSchedulerTransform -from qiskit.utils.deprecation import deprecate_func - - -class ASAPSchedule(BaseSchedulerTransform): - """ASAP Scheduling pass, which schedules the start time of instructions as early as possible.. - - See :class:`~qiskit.transpiler.passes.scheduling.base_scheduler.BaseSchedulerTransform` for the - detailed behavior of the control flow operation, i.e. ``c_if``. - - .. note:: - - This base class has been superseded by :class:`~.ASAPScheduleAnalysis` and - the new scheduling workflow. It will be deprecated and subsequently - removed in a future release. - """ - - @deprecate_func( - additional_msg=( - "Instead, use :class:`~.ASAPScheduleAnalysis`, which is an " - "analysis pass that requires a padding pass to later modify the circuit." - ), - since="1.1.0", - ) - def __init__(self, *args, **kwargs): - super().__init__(*args, **kwargs) - - def run(self, dag): - """Run the ASAPSchedule pass on `dag`. - - Args: - dag (DAGCircuit): DAG to schedule. - - Returns: - DAGCircuit: A scheduled DAG. - - Raises: - TranspilerError: if the circuit is not mapped on physical qubits. - TranspilerError: if conditional bit is added to non-supported instruction. - """ - if len(dag.qregs) != 1 or dag.qregs.get("q", None) is None: - raise TranspilerError("ASAP schedule runs on physical circuits only") - - time_unit = self.property_set["time_unit"] - - new_dag = DAGCircuit() - for qreg in dag.qregs.values(): - new_dag.add_qreg(qreg) - for creg in dag.cregs.values(): - new_dag.add_creg(creg) - - idle_after = {q: 0 for q in dag.qubits + dag.clbits} - for node in dag.topological_op_nodes(): - op_duration = self._get_node_duration(node, dag) - - # compute t0, t1: instruction interval, note that - # t0: start time of instruction - # t1: end time of instruction - if isinstance(node.op, self.CONDITIONAL_SUPPORTED): - t0q = max(idle_after[q] for q in node.qargs) - if node.op.condition_bits: - # conditional is bit tricky due to conditional_latency - t0c = max(idle_after[bit] for bit in node.op.condition_bits) - if t0q > t0c: - # This is situation something like below - # - # |t0q - # Q ▒▒▒▒▒▒▒▒▒░░ - # C ▒▒▒░░░░░░░░ - # |t0c - # - # In this case, you can insert readout access before tq0 - # - # |t0q - # Q ▒▒▒▒▒▒▒▒▒▒▒ - # C ▒▒▒░░░▒▒░░░ - # |t0q - conditional_latency - # - t0c = max(t0q - self.conditional_latency, t0c) - t1c = t0c + self.conditional_latency - for bit in node.op.condition_bits: - # Lock clbit until state is read - idle_after[bit] = t1c - # It starts after register read access - t0 = max(t0q, t1c) - else: - t0 = t0q - t1 = t0 + op_duration - else: - if node.op.condition_bits: - raise TranspilerError( - f"Conditional instruction {node.op.name} is not supported in ASAP scheduler." - ) - - if isinstance(node.op, Measure): - # measure instruction handling is bit tricky due to clbit_write_latency - t0q = max(idle_after[q] for q in node.qargs) - t0c = max(idle_after[c] for c in node.cargs) - # Assume following case (t0c > t0q) - # - # |t0q - # Q ▒▒▒▒░░░░░░░░░░░░ - # C ▒▒▒▒▒▒▒▒░░░░░░░░ - # |t0c - # - # In this case, there is no actual clbit access until clbit_write_latency. - # The node t0 can be push backward by this amount. - # - # |t0q' = t0c - clbit_write_latency - # Q ▒▒▒▒░░▒▒▒▒▒▒▒▒▒▒ - # C ▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒ - # |t0c' = t0c - # - # rather than naively doing - # - # |t0q' = t0c - # Q ▒▒▒▒░░░░▒▒▒▒▒▒▒▒ - # C ▒▒▒▒▒▒▒▒░░░▒▒▒▒▒ - # |t0c' = t0c + clbit_write_latency - # - t0 = max(t0q, t0c - self.clbit_write_latency) - t1 = t0 + op_duration - for clbit in node.cargs: - idle_after[clbit] = t1 - else: - # It happens to be directives such as barrier - t0 = max(idle_after[bit] for bit in node.qargs + node.cargs) - t1 = t0 + op_duration - - # Add delay to qubit wire - for bit in node.qargs: - delta = t0 - idle_after[bit] - if ( - delta > 0 - and isinstance(bit, Qubit) - and self._delay_supported(dag.find_bit(bit).index) - ): - new_dag.apply_operation_back(Delay(delta, time_unit), [bit], []) - idle_after[bit] = t1 - - new_dag.apply_operation_back(node.op, node.qargs, node.cargs) - - circuit_duration = max(idle_after.values()) - for bit, after in idle_after.items(): - delta = circuit_duration - after - if not (delta > 0 and isinstance(bit, Qubit)): - continue - if self._delay_supported(dag.find_bit(bit).index): - new_dag.apply_operation_back(Delay(delta, time_unit), [bit], []) - - new_dag.name = dag.name - new_dag.metadata = dag.metadata - new_dag._calibrations_prop = dag._calibrations_prop - - # set circuit duration and unit to indicate it is scheduled - new_dag.duration = circuit_duration - new_dag.unit = time_unit - return new_dag diff --git a/qiskit/transpiler/passes/scheduling/base_scheduler.py b/qiskit/transpiler/passes/scheduling/base_scheduler.py deleted file mode 100644 index c380c9f8c199..000000000000 --- a/qiskit/transpiler/passes/scheduling/base_scheduler.py +++ /dev/null @@ -1,310 +0,0 @@ -# This code is part of Qiskit. -# -# (C) Copyright IBM 2020. -# -# 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. - -"""Base circuit scheduling pass.""" -import warnings - -from qiskit.circuit import Delay, Gate, Measure, Reset -from qiskit.circuit.parameterexpression import ParameterExpression -from qiskit.dagcircuit import DAGOpNode, DAGCircuit, DAGOutNode -from qiskit.transpiler.basepasses import TransformationPass -from qiskit.transpiler.exceptions import TranspilerError -from qiskit.transpiler.instruction_durations import InstructionDurations -from qiskit.transpiler.passes.scheduling.time_unit_conversion import TimeUnitConversion -from qiskit.transpiler.target import Target - - -class BaseSchedulerTransform(TransformationPass): - """Base scheduler pass. - - .. warning:: - - This base class is not part of the public interface for this module - it should not be used to develop new scheduling passes as the passes - which are using this are pending a future deprecation and subsequent - removal. If you are developing new scheduling passes look at the - ``BaseScheduler`` class instead which is used in the new scheduling - pass workflow. - - Policy of topological node ordering in scheduling - - The DAG representation of ``QuantumCircuit`` respects the node ordering also in the - classical register wires, though theoretically two conditional instructions - conditioned on the same register are commute, i.e. read-access to the - classical register doesn't change its state. - - .. code-block:: text - - qc = QuantumCircuit(2, 1) - qc.delay(100, 0) - qc.x(0).c_if(0, True) - qc.x(1).c_if(0, True) - - The scheduler SHOULD comply with above topological ordering policy of the DAG circuit. - Accordingly, the `asap`-scheduled circuit will become - - .. code-block:: text - - ┌────────────────┐ ┌───┐ - q_0: ┤ Delay(100[dt]) ├───┤ X ├────────────── - ├────────────────┤ └─╥─┘ ┌───┐ - q_1: ┤ Delay(100[dt]) ├─────╫────────┤ X ├─── - └────────────────┘ ║ └─╥─┘ - ┌────╨────┐┌────╨────┐ - c: 1/══════════════════╡ c_0=0x1 ╞╡ c_0=0x1 ╞ - └─────────┘└─────────┘ - - Note that this scheduling might be inefficient in some cases, - because the second conditional operation can start without waiting the delay of 100 dt. - However, such optimization should be done by another pass, - otherwise scheduling may break topological ordering of the original circuit. - - Realistic control flow scheduling respecting for microarchitecture - - In the dispersive QND readout scheme, qubit is measured with microwave stimulus to qubit (Q) - followed by resonator ring-down (depopulation). This microwave signal is recorded - in the buffer memory (B) with hardware kernel, then a discriminated (D) binary value - is moved to the classical register (C). - The sequence from t0 to t1 of the measure instruction interval might be modeled as follows: - - .. code-block:: text - - Q ░▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒░ - B ░░▒▒▒▒▒▒▒▒░░░░░░░░░ - D ░░░░░░░░░░▒▒▒▒▒▒░░░ - C ░░░░░░░░░░░░░░░░▒▒░ - - However, ``QuantumCircuit`` representation is not enough accurate to represent - this model. In the circuit representation, thus ``Qubit`` is occupied by the - stimulus microwave signal during the first half of the interval, - and ``Clbit`` is only occupied at the very end of the interval. - - This precise model may induce weird edge case. - - .. code-block:: text - - ┌───┐ - q_0: ───┤ X ├────── - └─╥─┘ ┌─┐ - q_1: ─────╫─────┤M├ - ┌────╨────┐└╥┘ - c: 1/╡ c_0=0x1 ╞═╩═ - └─────────┘ 0 - - In this example, user may intend to measure the state of ``q_1``, after ``XGate`` is - applied to the ``q_0``. This is correct interpretation from viewpoint of - the topological node ordering, i.e. x gate node come in front of the measure node. - However, according to the measurement model above, the data in the register - is unchanged during the stimulus, thus two nodes are simultaneously operated. - If one `alap`-schedule this circuit, it may return following circuit. - - .. code-block:: text - - ┌────────────────┐ ┌───┐ - q_0: ┤ Delay(500[dt]) ├───┤ X ├────── - └────────────────┘ └─╥─┘ ┌─┐ - q_1: ───────────────────────╫─────┤M├ - ┌────╨────┐└╥┘ - c: 1/══════════════════╡ c_0=0x1 ╞═╩═ - └─────────┘ 0 - - Note that there is no delay on ``q_1`` wire, and the measure instruction immediately - start after t=0, while the conditional gate starts after the delay. - It looks like the topological ordering between the nodes are flipped in the scheduled view. - This behavior can be understood by considering the control flow model described above, - - .. code-block:: text - - : Quantum Circuit, first-measure - 0 ░░░░░░░░░░░░▒▒▒▒▒▒░ - 1 ░▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒░ - - : In wire q0 - Q ░░░░░░░░░░░░░░░▒▒▒░ - C ░░░░░░░░░░░░▒▒░░░░░ - - : In wire q1 - Q ░▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒░ - B ░░▒▒▒▒▒▒▒▒░░░░░░░░░ - D ░░░░░░░░░░▒▒▒▒▒▒░░░ - C ░░░░░░░░░░░░░░░░▒▒░ - - Since there is no qubit register (Q0, Q1) overlap, the node ordering is determined by the - shared classical register C. As you can see, the execution order is still - preserved on C, i.e. read C then apply ``XGate``, finally store the measured outcome in C. - Because ``DAGOpNode`` cannot define different durations for associated registers, - the time ordering of two nodes is inverted anyways. - - This behavior can be controlled by ``clbit_write_latency`` and ``conditional_latency``. - The former parameter determines the delay of the register write-access from - the beginning of the measure instruction t0, and another parameter determines - the delay of conditional gate operation from t0 which comes from the register read-access. - - Since we usually expect topological ordering and time ordering are identical - without the context of microarchitecture, both latencies are set to zero by default. - In this case, ``Measure`` instruction immediately locks the register C. - Under this configuration, the `alap`-scheduled circuit of above example may become - - .. code-block:: text - - ┌───┐ - q_0: ───┤ X ├────── - └─╥─┘ ┌─┐ - q_1: ─────╫─────┤M├ - ┌────╨────┐└╥┘ - c: 1/╡ c_0=0x1 ╞═╩═ - └─────────┘ 0 - - If the backend microarchitecture supports smart scheduling of the control flow, i.e. - it may separately schedule qubit and classical register, - insertion of the delay yields unnecessary longer total execution time. - - .. code-block:: text - - : Quantum Circuit, first-xgate - 0 ░▒▒▒░░░░░░░░░░░░░░░ - 1 ░▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒░ - - : In wire q0 - Q ░▒▒▒░░░░░░░░░░░░░░░ - C ░░░░░░░░░░░░░░░░░░░ (zero latency) - - : In wire q1 - Q ░▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒░ - C ░▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒░ (zero latency, scheduled after C0 read-access) - - However this result is much more intuitive in the topological ordering view. - If finite conditional latency is provided, for example, 30 dt, the circuit - is scheduled as follows. - - .. code-block:: text - - ┌───────────────┐ ┌───┐ - q_0: ┤ Delay(30[dt]) ├───┤ X ├────── - ├───────────────┤ └─╥─┘ ┌─┐ - q_1: ┤ Delay(30[dt]) ├─────╫─────┤M├ - └───────────────┘┌────╨────┐└╥┘ - c: 1/═════════════════╡ c_0=0x1 ╞═╩═ - └─────────┘ 0 - - with the timing model: - - .. code-block:: text - - : Quantum Circuit, first-xgate - 0 ░░▒▒▒░░░░░░░░░░░░░░░ - 1 ░░▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒░ - - : In wire q0 - Q ░░▒▒▒░░░░░░░░░░░░░░░ - C ░▒░░░░░░░░░░░░░░░░░░ (30dt latency) - - : In wire q1 - Q ░░▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒░ - C ░░▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒░ - - See https://arxiv.org/abs/2102.01682 for more details. - - """ - - CONDITIONAL_SUPPORTED = (Gate, Delay) - - def __init__( - self, - durations: InstructionDurations = None, - clbit_write_latency: int = 0, - conditional_latency: int = 0, - target: Target = None, - ): - """Scheduler initializer. - - Args: - durations: Durations of instructions to be used in scheduling - clbit_write_latency: A control flow constraints. Because standard superconducting - quantum processor implement dispersive QND readout, the actual data transfer - to the clbit happens after the round-trip stimulus signal is buffered - and discriminated into quantum state. - The interval ``[t0, t0 + clbit_write_latency]`` is regarded as idle time - for clbits associated with the measure instruction. - This defaults to 0 dt which is identical to Qiskit Pulse scheduler. - conditional_latency: A control flow constraints. This value represents - a latency of reading a classical register for the conditional operation. - The gate operation occurs after this latency. This appears as a delay - in front of the DAGOpNode of the gate. - This defaults to 0 dt. - target: The :class:`~.Target` representing the target backend, if both - ``durations`` and this are specified then this argument will take - precedence and ``durations`` will be ignored. - """ - super().__init__() - self.durations = durations - # Ensure op node durations are attached and in consistent unit - if target is not None: - self.durations = target.durations() - self.requires.append(TimeUnitConversion(self.durations)) - - # Control flow constraints. - self.clbit_write_latency = clbit_write_latency - self.conditional_latency = conditional_latency - - self.target = target - - @staticmethod - def _get_node_duration( - node: DAGOpNode, - dag: DAGCircuit, - ) -> int: - """A helper method to get duration from node or calibration.""" - indices = [dag.find_bit(qarg).index for qarg in node.qargs] - - if dag._has_calibration_for(node): - # If node has calibration, this value should be the highest priority - cal_key = tuple(indices), tuple(float(p) for p in node.op.params) - duration = dag._calibrations_prop[node.op.name][cal_key].duration - else: - duration = node.op.duration - - if isinstance(node.op, Reset): - warnings.warn( - "Qiskit scheduler assumes Reset works similarly to Measure instruction. " - "Actual behavior depends on the control system of your quantum backend. " - "Your backend may provide a plugin scheduler pass." - ) - elif isinstance(node.op, Measure): - is_mid_circuit = not any( - isinstance(x, DAGOutNode) for x in dag.quantum_successors(node) - ) - if is_mid_circuit: - warnings.warn( - "Qiskit scheduler assumes mid-circuit measurement works as a standard instruction. " - "Actual backend may apply custom scheduling. " - "Your backend may provide a plugin scheduler pass." - ) - - if isinstance(duration, ParameterExpression): - raise TranspilerError( - f"Parameterized duration ({duration}) " - f"of {node.op.name} on qubits {indices} is not bounded." - ) - if duration is None: - raise TranspilerError(f"Duration of {node.op.name} on qubits {indices} is not found.") - - return duration - - def _delay_supported(self, qarg: int) -> bool: - """Delay operation is supported on the qubit (qarg) or not.""" - if self.target is None or self.target.instruction_supported("delay", qargs=(qarg,)): - return True - return False - - def run(self, dag: DAGCircuit): - raise NotImplementedError diff --git a/qiskit/transpiler/passes/scheduling/dynamical_decoupling.py b/qiskit/transpiler/passes/scheduling/dynamical_decoupling.py deleted file mode 100644 index 08371c488422..000000000000 --- a/qiskit/transpiler/passes/scheduling/dynamical_decoupling.py +++ /dev/null @@ -1,313 +0,0 @@ -# This code is part of Qiskit. -# -# (C) Copyright IBM 2021, 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. - -"""Dynamical Decoupling insertion pass.""" - -import itertools -import warnings - -import numpy as np -from qiskit.circuit import Gate, Delay, Reset -from qiskit.circuit.library.standard_gates import IGate, UGate, U3Gate -from qiskit.dagcircuit import DAGOpNode, DAGInNode -from qiskit.quantum_info.operators.predicates import matrix_equal -from qiskit.synthesis.one_qubit import OneQubitEulerDecomposer -from qiskit.transpiler.basepasses import TransformationPass -from qiskit.transpiler.exceptions import TranspilerError -from qiskit.transpiler.instruction_durations import InstructionDurations -from qiskit.transpiler.passes.optimization import Optimize1qGates -from qiskit.utils.deprecation import deprecate_func - - -class DynamicalDecoupling(TransformationPass): - """Dynamical decoupling insertion pass. - - This pass works on a scheduled, physical circuit. It scans the circuit for - idle periods of time (i.e. those containing delay instructions) and inserts - a DD sequence of gates in those spots. These gates amount to the identity, - so do not alter the logical action of the circuit, but have the effect of - mitigating decoherence in those idle periods. - - As a special case, the pass allows a length-1 sequence (e.g. [XGate()]). - In this case the DD insertion happens only when the gate inverse can be - absorbed into a neighboring gate in the circuit (so we would still be - replacing Delay with something that is equivalent to the identity). - This can be used, for instance, as a Hahn echo. - - This pass ensures that the inserted sequence preserves the circuit exactly - (including global phase). - - .. plot:: - :alt: Output from the previous code. - :include-source: - - import numpy as np - from qiskit.circuit import QuantumCircuit - from qiskit.circuit.library import XGate - from qiskit.transpiler import PassManager, InstructionDurations - from qiskit.transpiler.passes import ALAPSchedule, DynamicalDecoupling - from qiskit.visualization import timeline_drawer - - # Because the legacy passes do not propagate the scheduling information correctly, it is - # necessary to run a no-op "re-schedule" before the output circuits can be drawn. - def draw(circuit): - from qiskit import transpile - - scheduled = transpile( - circuit, - optimization_level=0, - instruction_durations=InstructionDurations(), - scheduling_method="alap", - ) - return timeline_drawer(scheduled) - - circ = QuantumCircuit(4) - circ.h(0) - circ.cx(0, 1) - circ.cx(1, 2) - circ.cx(2, 3) - circ.measure_all() - durations = InstructionDurations( - [("h", 0, 50), ("cx", [0, 1], 700), ("reset", None, 10), - ("cx", [1, 2], 200), ("cx", [2, 3], 300), - ("x", None, 50), ("measure", None, 1000)] - ) - # balanced X-X sequence on all qubits - dd_sequence = [XGate(), XGate()] - pm = PassManager([ALAPSchedule(durations), - DynamicalDecoupling(durations, dd_sequence)]) - circ_dd = pm.run(circ) - draw(circ_dd) - - # Uhrig sequence on qubit 0 - n = 8 - dd_sequence = [XGate()] * n - def uhrig_pulse_location(k): - return np.sin(np.pi * (k + 1) / (2 * n + 2)) ** 2 - spacing = [] - for k in range(n): - spacing.append(uhrig_pulse_location(k) - sum(spacing)) - spacing.append(1 - sum(spacing)) - pm = PassManager( - [ - ALAPSchedule(durations), - DynamicalDecoupling(durations, dd_sequence, qubits=[0], spacing=spacing), - ] - ) - circ_dd = pm.run(circ) - draw(circ_dd) - """ - - @deprecate_func( - additional_msg=( - "Instead, use :class:`~.PadDynamicalDecoupling`, which performs the same " - "function but requires scheduling and alignment analysis passes to run prior to it." - ), - since="1.1.0", - ) - def __init__( - self, durations, dd_sequence, qubits=None, spacing=None, skip_reset_qubits=True, target=None - ): - """Dynamical decoupling initializer. - - Args: - durations (InstructionDurations): Durations of instructions to be - used in scheduling. - dd_sequence (list[Gate]): sequence of gates to apply in idle spots. - qubits (list[int]): physical qubits on which to apply DD. - If None, all qubits will undergo DD (when possible). - spacing (list[float]): a list of spacings between the DD gates. - The available slack will be divided according to this. - The list length must be one more than the length of dd_sequence, - and the elements must sum to 1. If None, a balanced spacing - will be used [d/2, d, d, ..., d, d, d/2]. - skip_reset_qubits (bool): if True, does not insert DD on idle - periods that immediately follow initialized/reset qubits (as - qubits in the ground state are less susceptible to decoherence). - target (Target): The :class:`~.Target` representing the target backend, if both - ``durations`` and this are specified then this argument will take - precedence and ``durations`` will be ignored. - """ - super().__init__() - self._durations = durations - self._dd_sequence = dd_sequence - self._qubits = qubits - self._spacing = spacing - self._skip_reset_qubits = skip_reset_qubits - self._target = target - if target is not None: - self._durations = target.durations() - for gate in dd_sequence: - if gate.name not in target.operation_names: - raise TranspilerError( - f"{gate.name} in dd_sequence is not supported in the target" - ) - - def run(self, dag): - """Run the DynamicalDecoupling pass on dag. - - Args: - dag (DAGCircuit): a scheduled DAG. - - Returns: - DAGCircuit: equivalent circuit with delays interrupted by DD, - where possible. - - Raises: - TranspilerError: if the circuit is not mapped on physical qubits. - """ - if len(dag.qregs) != 1 or dag.qregs.get("q", None) is None: - raise TranspilerError("DD runs on physical circuits only.") - - if dag.duration is None: - raise TranspilerError("DD runs after circuit is scheduled.") - - durations = self._update_inst_durations(dag) - - num_pulses = len(self._dd_sequence) - sequence_gphase = 0 - if num_pulses != 1: - if num_pulses % 2 != 0: - raise TranspilerError("DD sequence must contain an even number of gates (or 1).") - noop = np.eye(2) - for gate in self._dd_sequence: - noop = noop.dot(gate.to_matrix()) - if not matrix_equal(noop, IGate().to_matrix(), ignore_phase=True): - raise TranspilerError("The DD sequence does not make an identity operation.") - sequence_gphase = np.angle(noop[0][0]) - - if self._qubits is None: - self._qubits = set(range(dag.num_qubits())) - else: - self._qubits = set(self._qubits) - - if self._spacing: - if sum(self._spacing) != 1 or any(a < 0 for a in self._spacing): - raise TranspilerError( - "The spacings must be given in terms of fractions " - "of the slack period and sum to 1." - ) - else: # default to balanced spacing - mid = 1 / num_pulses - end = mid / 2 - self._spacing = [end] + [mid] * (num_pulses - 1) + [end] - - for qarg in list(self._qubits): - for gate in self._dd_sequence: - if not self.__gate_supported(gate, qarg): - self._qubits.discard(qarg) - break - - index_sequence_duration_map = {} - for physical_qubit in self._qubits: - dd_sequence_duration = 0 - for index, gate in enumerate(self._dd_sequence): - gate = gate.to_mutable() - self._dd_sequence[index] = gate - gate.duration = durations.get(gate, physical_qubit) - - dd_sequence_duration += gate.duration - index_sequence_duration_map[physical_qubit] = dd_sequence_duration - - new_dag = dag.copy_empty_like() - - for nd in dag.topological_op_nodes(): - if not isinstance(nd.op, Delay): - new_dag.apply_operation_back(nd.op, nd.qargs, nd.cargs, check=False) - continue - - dag_qubit = nd.qargs[0] - physical_qubit = dag.find_bit(dag_qubit).index - if physical_qubit not in self._qubits: # skip unwanted qubits - new_dag.apply_operation_back(nd.op, nd.qargs, nd.cargs, check=False) - continue - - pred = next(dag.predecessors(nd)) - succ = next(dag.successors(nd)) - if self._skip_reset_qubits: # discount initial delays - if isinstance(pred, DAGInNode) or isinstance(pred.op, Reset): - new_dag.apply_operation_back(nd.op, nd.qargs, nd.cargs, check=False) - continue - - dd_sequence_duration = index_sequence_duration_map[physical_qubit] - slack = nd.op.duration - dd_sequence_duration - if slack <= 0: # dd doesn't fit - new_dag.apply_operation_back(nd.op, nd.qargs, nd.cargs, check=False) - continue - - if num_pulses == 1: # special case of using a single gate for DD - u_inv = self._dd_sequence[0].inverse().to_matrix() - theta, phi, lam, phase = OneQubitEulerDecomposer().angles_and_phase(u_inv) - # absorb the inverse into the successor (from left in circuit) - if isinstance(succ, DAGOpNode) and isinstance(succ.op, (UGate, U3Gate)): - theta_r, phi_r, lam_r = succ.op.params - succ.op.params = Optimize1qGates.compose_u3( - theta_r, phi_r, lam_r, theta, phi, lam - ) - sequence_gphase += phase - # absorb the inverse into the predecessor (from right in circuit) - elif isinstance(pred, DAGOpNode) and isinstance(pred.op, (UGate, U3Gate)): - theta_l, phi_l, lam_l = pred.op.params - pred.op.params = Optimize1qGates.compose_u3( - theta, phi, lam, theta_l, phi_l, lam_l - ) - sequence_gphase += phase - # don't do anything if there's no single-qubit gate to absorb the inverse - else: - new_dag.apply_operation_back(nd.op, nd.qargs, nd.cargs, check=False) - continue - - # insert the actual DD sequence - taus = [int(slack * a) for a in self._spacing] - unused_slack = slack - sum(taus) # unused, due to rounding to int multiples of dt - middle_index = int((len(taus) - 1) / 2) # arbitrary: redistribute to middle - taus[middle_index] += unused_slack # now we add up to original delay duration - - for tau, gate in itertools.zip_longest(taus, self._dd_sequence): - if tau > 0: - new_dag.apply_operation_back(Delay(tau), [dag_qubit], check=False) - if gate is not None: - new_dag.apply_operation_back(gate, [dag_qubit], check=False) - - new_dag.global_phase = new_dag.global_phase + sequence_gphase - - return new_dag - - def _update_inst_durations(self, dag): - """Update instruction durations with circuit information. If the dag contains gate - calibrations and no instruction durations were provided through the target or as a - standalone input, the circuit calibration durations will be used. - The priority order for instruction durations is: target > standalone > circuit. - """ - circ_durations = InstructionDurations() - - if dag._calibrations_prop: - cal_durations = [] - with warnings.catch_warnings(): - warnings.simplefilter(action="ignore", category=DeprecationWarning) - # `schedule.duration` emits pulse deprecation warnings which we don't want - # to see here - for gate, gate_cals in dag._calibrations_prop.items(): - for (qubits, parameters), schedule in gate_cals.items(): - cal_durations.append((gate, qubits, parameters, schedule.duration)) - circ_durations.update(cal_durations, circ_durations.dt) - - if self._durations is not None: - circ_durations.update(self._durations, getattr(self._durations, "dt", None)) - - return circ_durations - - def __gate_supported(self, gate: Gate, qarg: int) -> bool: - """A gate is supported on the qubit (qarg) or not.""" - if self._target is None or self._target.instruction_supported(gate.name, qargs=(qarg,)): - return True - return False diff --git a/releasenotes/notes/drop-legacy-scheduling-passes-ee1d593c41fe95c6.yaml b/releasenotes/notes/drop-legacy-scheduling-passes-ee1d593c41fe95c6.yaml new file mode 100644 index 000000000000..b57a6dcc8920 --- /dev/null +++ b/releasenotes/notes/drop-legacy-scheduling-passes-ee1d593c41fe95c6.yaml @@ -0,0 +1,9 @@ +--- +upgrade_transpiler: + - | + The deprecated transpiler passes ``ASAPSchedule``, ``ALAPSchedule``, + ``DynamicalDecoupling``, and ``AlignMeasures`` have been removed. These + passes were marked as deprecated. They have been replaced by the + :class:`.ALAPScheduleAnalysis`, :class:`.ASAPScheduleAnalysis`, + :class:`.PadDynamicalDecoupling`, and :class:`.ConstrainedReschedule` + passes respectively which can be used instead. diff --git a/test/python/transpiler/legacy_scheduling/__init__.py b/test/python/transpiler/legacy_scheduling/__init__.py deleted file mode 100644 index 3ce633ebcc4d..000000000000 --- a/test/python/transpiler/legacy_scheduling/__init__.py +++ /dev/null @@ -1,13 +0,0 @@ -# This code is part of Qiskit. -# -# (C) Copyright IBM 2022. -# -# 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. - -"""Qiskit legacy scheduling transpiler pass unit tests.""" diff --git a/test/python/transpiler/legacy_scheduling/test_instruction_alignments.py b/test/python/transpiler/legacy_scheduling/test_instruction_alignments.py deleted file mode 100644 index aee567444cff..000000000000 --- a/test/python/transpiler/legacy_scheduling/test_instruction_alignments.py +++ /dev/null @@ -1,327 +0,0 @@ -# This code is part of Qiskit. -# -# (C) Copyright IBM 2021. -# -# 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. - -"""Testing legacy instruction alignment pass.""" - -from qiskit import QuantumCircuit -from qiskit.transpiler import InstructionDurations -from qiskit.transpiler.passes import ( - AlignMeasures, - ALAPSchedule, - TimeUnitConversion, -) -from test import QiskitTestCase # pylint: disable=wrong-import-order - - -class TestAlignMeasures(QiskitTestCase): - """A test for measurement alignment pass.""" - - def setUp(self): - super().setUp() - instruction_durations = InstructionDurations() - instruction_durations.update( - [ - ("rz", (0,), 0), - ("rz", (1,), 0), - ("x", (0,), 160), - ("x", (1,), 160), - ("sx", (0,), 160), - ("sx", (1,), 160), - ("cx", (0, 1), 800), - ("cx", (1, 0), 800), - ("measure", None, 1600), - ] - ) - self.time_conversion_pass = TimeUnitConversion(inst_durations=instruction_durations) - # reproduce old behavior of 0.20.0 before #7655 - # currently default write latency is 0 - with self.assertWarns(DeprecationWarning): - self.scheduling_pass = ALAPSchedule( - durations=instruction_durations, - clbit_write_latency=1600, - conditional_latency=0, - ) - self.align_measure_pass = AlignMeasures(alignment=16) - - def test_t1_experiment_type(self): - """Test T1 experiment type circuit. - - (input) - - ┌───┐┌────────────────┐┌─┐ - q_0: ┤ X ├┤ Delay(100[dt]) ├┤M├ - └───┘└────────────────┘└╥┘ - c: 1/════════════════════════╩═ - 0 - - (aligned) - - ┌───┐┌────────────────┐┌─┐ - q_0: ┤ X ├┤ Delay(112[dt]) ├┤M├ - └───┘└────────────────┘└╥┘ - c: 1/════════════════════════╩═ - 0 - - This type of experiment slightly changes delay duration of interest. - However the quantization error should be less than alignment * dt. - """ - circuit = QuantumCircuit(1, 1) - circuit.x(0) - circuit.delay(100, 0, unit="dt") - circuit.measure(0, 0) - - timed_circuit = self.time_conversion_pass(circuit) - scheduled_circuit = self.scheduling_pass(timed_circuit, property_set={"time_unit": "dt"}) - aligned_circuit = self.align_measure_pass( - scheduled_circuit, property_set={"time_unit": "dt"} - ) - - ref_circuit = QuantumCircuit(1, 1) - ref_circuit.x(0) - ref_circuit.delay(112, 0, unit="dt") - ref_circuit.measure(0, 0) - - self.assertEqual(aligned_circuit, ref_circuit) - - def test_hanh_echo_experiment_type(self): - """Test Hahn echo experiment type circuit. - - (input) - - ┌────┐┌────────────────┐┌───┐┌────────────────┐┌────┐┌─┐ - q_0: ┤ √X ├┤ Delay(100[dt]) ├┤ X ├┤ Delay(100[dt]) ├┤ √X ├┤M├ - └────┘└────────────────┘└───┘└────────────────┘└────┘└╥┘ - c: 1/══════════════════════════════════════════════════════╩═ - 0 - - (output) - - ┌────┐┌────────────────┐┌───┐┌────────────────┐┌────┐┌──────────────┐┌─┐ - q_0: ┤ √X ├┤ Delay(100[dt]) ├┤ X ├┤ Delay(100[dt]) ├┤ √X ├┤ Delay(8[dt]) ├┤M├ - └────┘└────────────────┘└───┘└────────────────┘└────┘└──────────────┘└╥┘ - c: 1/══════════════════════════════════════════════════════════════════════╩═ - 0 - - This type of experiment doesn't change duration of interest (two in the middle). - However induces slight delay less than alignment * dt before measurement. - This might induce extra amplitude damping error. - """ - circuit = QuantumCircuit(1, 1) - circuit.sx(0) - circuit.delay(100, 0, unit="dt") - circuit.x(0) - circuit.delay(100, 0, unit="dt") - circuit.sx(0) - circuit.measure(0, 0) - - timed_circuit = self.time_conversion_pass(circuit) - scheduled_circuit = self.scheduling_pass(timed_circuit, property_set={"time_unit": "dt"}) - aligned_circuit = self.align_measure_pass( - scheduled_circuit, property_set={"time_unit": "dt"} - ) - - ref_circuit = QuantumCircuit(1, 1) - ref_circuit.sx(0) - ref_circuit.delay(100, 0, unit="dt") - ref_circuit.x(0) - ref_circuit.delay(100, 0, unit="dt") - ref_circuit.sx(0) - ref_circuit.delay(8, 0, unit="dt") - ref_circuit.measure(0, 0) - - self.assertEqual(aligned_circuit, ref_circuit) - - def test_mid_circuit_measure(self): - """Test circuit with mid circuit measurement. - - (input) - - ┌───┐┌────────────────┐┌─┐┌───────────────┐┌───┐┌────────────────┐┌─┐ - q_0: ┤ X ├┤ Delay(100[dt]) ├┤M├┤ Delay(10[dt]) ├┤ X ├┤ Delay(120[dt]) ├┤M├ - └───┘└────────────────┘└╥┘└───────────────┘└───┘└────────────────┘└╥┘ - c: 2/════════════════════════╩══════════════════════════════════════════╩═ - 0 1 - - (output) - - ┌───┐┌────────────────┐┌─┐┌───────────────┐┌───┐┌────────────────┐┌─┐ - q_0: ┤ X ├┤ Delay(112[dt]) ├┤M├┤ Delay(10[dt]) ├┤ X ├┤ Delay(134[dt]) ├┤M├ - └───┘└────────────────┘└╥┘└───────────────┘└───┘└────────────────┘└╥┘ - c: 2/════════════════════════╩══════════════════════════════════════════╩═ - 0 1 - - Extra delay is always added to the existing delay right before the measurement. - Delay after measurement is unchanged. - """ - circuit = QuantumCircuit(1, 2) - circuit.x(0) - circuit.delay(100, 0, unit="dt") - circuit.measure(0, 0) - circuit.delay(10, 0, unit="dt") - circuit.x(0) - circuit.delay(120, 0, unit="dt") - circuit.measure(0, 1) - - timed_circuit = self.time_conversion_pass(circuit) - scheduled_circuit = self.scheduling_pass(timed_circuit, property_set={"time_unit": "dt"}) - aligned_circuit = self.align_measure_pass( - scheduled_circuit, property_set={"time_unit": "dt"} - ) - - ref_circuit = QuantumCircuit(1, 2) - ref_circuit.x(0) - ref_circuit.delay(112, 0, unit="dt") - ref_circuit.measure(0, 0) - ref_circuit.delay(10, 0, unit="dt") - ref_circuit.x(0) - ref_circuit.delay(134, 0, unit="dt") - ref_circuit.measure(0, 1) - - self.assertEqual(aligned_circuit, ref_circuit) - - def test_mid_circuit_multiq_gates(self): - """Test circuit with mid circuit measurement and multi qubit gates. - - (input) - - ┌───┐┌────────────────┐┌─┐ ┌─┐ - q_0: ┤ X ├┤ Delay(100[dt]) ├┤M├──■───────■──┤M├ - └───┘└────────────────┘└╥┘┌─┴─┐┌─┐┌─┴─┐└╥┘ - q_1: ────────────────────────╫─┤ X ├┤M├┤ X ├─╫─ - ║ └───┘└╥┘└───┘ ║ - c: 2/════════════════════════╩═══════╩═══════╩═ - 0 1 0 - - (output) - - ┌───┐ ┌────────────────┐┌─┐ ┌─────────────────┐ ┌─┐» - q_0: ───────┤ X ├───────┤ Delay(112[dt]) ├┤M├──■──┤ Delay(1600[dt]) ├──■──┤M├» - ┌──────┴───┴──────┐└────────────────┘└╥┘┌─┴─┐└───────┬─┬───────┘┌─┴─┐└╥┘» - q_1: ┤ Delay(1872[dt]) ├───────────────────╫─┤ X ├────────┤M├────────┤ X ├─╫─» - └─────────────────┘ ║ └───┘ └╥┘ └───┘ ║ » - c: 2/══════════════════════════════════════╩═══════════════╩═══════════════╩═» - 0 1 0 » - « - «q_0: ─────────────────── - « ┌─────────────────┐ - «q_1: ┤ Delay(1600[dt]) ├ - « └─────────────────┘ - «c: 2/═══════════════════ - « - - Delay for the other channel paired by multi-qubit instruction is also scheduled. - Delay (1872dt) = X (160dt) + Delay (100dt + extra 12dt) + Measure (1600dt). - """ - circuit = QuantumCircuit(2, 2) - circuit.x(0) - circuit.delay(100, 0, unit="dt") - circuit.measure(0, 0) - circuit.cx(0, 1) - circuit.measure(1, 1) - circuit.cx(0, 1) - circuit.measure(0, 0) - - timed_circuit = self.time_conversion_pass(circuit) - scheduled_circuit = self.scheduling_pass(timed_circuit, property_set={"time_unit": "dt"}) - aligned_circuit = self.align_measure_pass( - scheduled_circuit, property_set={"time_unit": "dt"} - ) - - ref_circuit = QuantumCircuit(2, 2) - ref_circuit.x(0) - ref_circuit.delay(112, 0, unit="dt") - ref_circuit.measure(0, 0) - ref_circuit.delay(160 + 112 + 1600, 1, unit="dt") - ref_circuit.cx(0, 1) - ref_circuit.delay(1600, 0, unit="dt") - ref_circuit.measure(1, 1) - ref_circuit.cx(0, 1) - ref_circuit.delay(1600, 1, unit="dt") - ref_circuit.measure(0, 0) - - self.assertEqual(aligned_circuit, ref_circuit) - - def test_alignment_is_not_processed(self): - """Test avoid pass processing if delay is aligned.""" - circuit = QuantumCircuit(2, 2) - circuit.x(0) - circuit.delay(160, 0, unit="dt") - circuit.measure(0, 0) - circuit.cx(0, 1) - circuit.measure(1, 1) - circuit.cx(0, 1) - circuit.measure(0, 0) - - # pre scheduling is not necessary because alignment is skipped - # this is to minimize breaking changes to existing code. - transpiled = self.align_measure_pass(circuit, property_set={"time_unit": "dt"}) - - self.assertEqual(transpiled, circuit) - - def test_circuit_using_clbit(self): - """Test a circuit with instructions using a common clbit. - - (input) - ┌───┐┌────────────────┐┌─┐ - q_0: ┤ X ├┤ Delay(100[dt]) ├┤M├────────────── - └───┘└────────────────┘└╥┘ ┌───┐ - q_1: ────────────────────────╫────┤ X ├────── - ║ └─╥─┘ ┌─┐ - q_2: ────────────────────────╫──────╫─────┤M├ - ║ ┌────╨────┐└╥┘ - c: 1/════════════════════════╩═╡ c_0 = T ╞═╩═ - 0 └─────────┘ 0 - - (aligned) - ┌───┐ ┌────────────────┐┌─┐┌────────────────┐ - q_0: ───────┤ X ├───────┤ Delay(112[dt]) ├┤M├┤ Delay(160[dt]) ├─── - ┌──────┴───┴──────┐└────────────────┘└╥┘└─────┬───┬──────┘ - q_1: ┤ Delay(1872[dt]) ├───────────────────╫───────┤ X ├────────── - └┬────────────────┤ ║ └─╥─┘ ┌─┐ - q_2: ─┤ Delay(432[dt]) ├───────────────────╫─────────╫─────────┤M├ - └────────────────┘ ║ ┌────╨────┐ └╥┘ - c: 1/══════════════════════════════════════╩════╡ c_0 = T ╞═════╩═ - 0 └─────────┘ 0 - - Looking at the q_0, the total schedule length T becomes - 160 (x) + 112 (aligned delay) + 1600 (measure) + 160 (delay) = 2032. - The last delay comes from ALAP scheduling called before the AlignMeasure pass, - which aligns stop times as late as possible, so the start time of x(1).c_if(0) - and the stop time of measure(0, 0) become T - 160. - """ - circuit = QuantumCircuit(3, 1) - circuit.x(0) - circuit.delay(100, 0, unit="dt") - circuit.measure(0, 0) - with self.assertWarns(DeprecationWarning): - circuit.x(1).c_if(0, 1) - circuit.measure(2, 0) - - timed_circuit = self.time_conversion_pass(circuit) - scheduled_circuit = self.scheduling_pass(timed_circuit, property_set={"time_unit": "dt"}) - aligned_circuit = self.align_measure_pass( - scheduled_circuit, property_set={"time_unit": "dt"} - ) - self.assertEqual(aligned_circuit.duration, 2032) - - ref_circuit = QuantumCircuit(3, 1) - ref_circuit.x(0) - ref_circuit.delay(112, 0, unit="dt") - ref_circuit.delay(1872, 1, unit="dt") # 2032 - 160 - ref_circuit.delay(432, 2, unit="dt") # 2032 - 1600 - ref_circuit.measure(0, 0) - with self.assertWarns(DeprecationWarning): - ref_circuit.x(1).c_if(0, 1) - ref_circuit.delay(160, 0, unit="dt") - ref_circuit.measure(2, 0) - - self.assertEqual(aligned_circuit, ref_circuit) diff --git a/test/python/transpiler/legacy_scheduling/test_scheduling_pass.py b/test/python/transpiler/legacy_scheduling/test_scheduling_pass.py deleted file mode 100644 index a13ca2e12de1..000000000000 --- a/test/python/transpiler/legacy_scheduling/test_scheduling_pass.py +++ /dev/null @@ -1,864 +0,0 @@ -# This code is part of Qiskit. -# -# (C) Copyright IBM 2020. -# -# 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. - -"""Test the legacy Scheduling passes""" - -import unittest - -from ddt import ddt, data, unpack - -from qiskit import QuantumCircuit -from qiskit.circuit import Delay, Parameter -from qiskit.circuit.library.standard_gates import XGate, YGate, CXGate -from qiskit.transpiler.exceptions import TranspilerError -from qiskit.transpiler.instruction_durations import InstructionDurations -from qiskit.transpiler.passes import ASAPSchedule, ALAPSchedule, DynamicalDecoupling -from qiskit.transpiler.passmanager import PassManager -from qiskit.transpiler.target import Target, InstructionProperties -from test import QiskitTestCase # pylint: disable=wrong-import-order - - -@ddt -class TestSchedulingPass(QiskitTestCase): - """Tests the Scheduling passes""" - - def test_alap_agree_with_reverse_asap_reverse(self): - """Test if ALAP schedule agrees with doubly-reversed ASAP schedule.""" - qc = QuantumCircuit(2) - qc.h(0) - qc.delay(500, 1) - qc.cx(0, 1) - qc.measure_all() - - durations = InstructionDurations( - [("h", 0, 200), ("cx", [0, 1], 700), ("measure", None, 1000)] - ) - - with self.assertWarns(DeprecationWarning): - pm = PassManager(ALAPSchedule(durations)) - alap_qc = pm.run(qc) - - with self.assertWarns(DeprecationWarning): - pm = PassManager(ASAPSchedule(durations)) - new_qc = pm.run(qc.reverse_ops()) - new_qc = new_qc.reverse_ops() - new_qc.name = new_qc.name - - self.assertEqual(alap_qc, new_qc) - - @data(ALAPSchedule, ASAPSchedule) - def test_classically_controlled_gate_after_measure(self, schedule_pass): - """Test if ALAP/ASAP schedules circuits with c_if after measure with a common clbit. - See: https://github.com/Qiskit/qiskit-terra/issues/7654 - - (input) - ┌─┐ - q_0: ┤M├─────────── - └╥┘ ┌───┐ - q_1: ─╫────┤ X ├─── - ║ └─╥─┘ - ║ ┌────╨────┐ - c: 1/═╩═╡ c_0 = T ╞ - 0 └─────────┘ - - (scheduled) - ┌─┐┌────────────────┐ - q_0: ───────────────────┤M├┤ Delay(200[dt]) ├ - ┌─────────────────┐└╥┘└─────┬───┬──────┘ - q_1: ┤ Delay(1000[dt]) ├─╫───────┤ X ├─────── - └─────────────────┘ ║ └─╥─┘ - ║ ┌────╨────┐ - c: 1/════════════════════╩════╡ c_0=0x1 ╞════ - 0 └─────────┘ - """ - qc = QuantumCircuit(2, 1) - qc.measure(0, 0) - with self.assertWarns(DeprecationWarning): - qc.x(1).c_if(0, True) - - durations = InstructionDurations([("x", None, 200), ("measure", None, 1000)]) - with self.assertWarns(DeprecationWarning): - pm = PassManager(schedule_pass(durations)) - scheduled = pm.run(qc) - - expected = QuantumCircuit(2, 1) - expected.measure(0, 0) - expected.delay(1000, 1) # x.c_if starts after measure - with self.assertWarns(DeprecationWarning): - expected.x(1).c_if(0, True) - expected.delay(200, 0) - - self.assertEqual(expected, scheduled) - - @data(ALAPSchedule, ASAPSchedule) - def test_measure_after_measure(self, schedule_pass): - """Test if ALAP/ASAP schedules circuits with measure after measure with a common clbit. - See: https://github.com/Qiskit/qiskit-terra/issues/7654 - - (input) - ┌───┐┌─┐ - q_0: ┤ X ├┤M├─── - └───┘└╥┘┌─┐ - q_1: ──────╫─┤M├ - ║ └╥┘ - c: 1/══════╩══╩═ - 0 0 - - (scheduled) - ┌───┐ ┌─┐┌─────────────────┐ - q_0: ───────┤ X ├───────┤M├┤ Delay(1000[dt]) ├ - ┌──────┴───┴──────┐└╥┘└───────┬─┬───────┘ - q_1: ┤ Delay(1200[dt]) ├─╫─────────┤M├──────── - └─────────────────┘ ║ └╥┘ - c: 1/════════════════════╩══════════╩═════════ - 0 0 - """ - qc = QuantumCircuit(2, 1) - qc.x(0) - qc.measure(0, 0) - qc.measure(1, 0) - - durations = InstructionDurations([("x", None, 200), ("measure", None, 1000)]) - with self.assertWarns(DeprecationWarning): - pm = PassManager(schedule_pass(durations)) - scheduled = pm.run(qc) - - expected = QuantumCircuit(2, 1) - expected.x(0) - expected.measure(0, 0) - expected.delay(1200, 1) - expected.measure(1, 0) - expected.delay(1000, 0) - - self.assertEqual(expected, scheduled) - - @data(ALAPSchedule, ASAPSchedule) - def test_c_if_on_different_qubits(self, schedule_pass): - """Test if ALAP/ASAP schedules circuits with `c_if`s on different qubits. - - (input) - ┌─┐ - q_0: ┤M├────────────────────── - └╥┘ ┌───┐ - q_1: ─╫────┤ X ├────────────── - ║ └─╥─┘ ┌───┐ - q_2: ─╫──────╫────────┤ X ├─── - ║ ║ └─╥─┘ - ║ ┌────╨────┐┌────╨────┐ - c: 1/═╩═╡ c_0 = T ╞╡ c_0 = T ╞ - 0 └─────────┘└─────────┘ - - (scheduled) - - ┌─┐┌────────────────┐ - q_0: ───────────────────┤M├┤ Delay(200[dt]) ├─────────── - ┌─────────────────┐└╥┘└─────┬───┬──────┘ - q_1: ┤ Delay(1000[dt]) ├─╫───────┤ X ├────────────────── - ├─────────────────┤ ║ └─╥─┘ ┌───┐ - q_2: ┤ Delay(1000[dt]) ├─╫─────────╫────────────┤ X ├─── - └─────────────────┘ ║ ║ └─╥─┘ - ║ ┌────╨────┐ ┌────╨────┐ - c: 1/════════════════════╩════╡ c_0=0x1 ╞════╡ c_0=0x1 ╞ - 0 └─────────┘ └─────────┘ - """ - qc = QuantumCircuit(3, 1) - qc.measure(0, 0) - with self.assertWarns(DeprecationWarning): - qc.x(1).c_if(0, True) - with self.assertWarns(DeprecationWarning): - qc.x(2).c_if(0, True) - - durations = InstructionDurations([("x", None, 200), ("measure", None, 1000)]) - with self.assertWarns(DeprecationWarning): - pm = PassManager(schedule_pass(durations)) - scheduled = pm.run(qc) - - expected = QuantumCircuit(3, 1) - expected.measure(0, 0) - expected.delay(1000, 1) - expected.delay(1000, 2) - with self.assertWarns(DeprecationWarning): - expected.x(1).c_if(0, True) - with self.assertWarns(DeprecationWarning): - expected.x(2).c_if(0, True) - expected.delay(200, 0) - - self.assertEqual(expected, scheduled) - - @data(ALAPSchedule, ASAPSchedule) - def test_shorter_measure_after_measure(self, schedule_pass): - """Test if ALAP/ASAP schedules circuits with shorter measure after measure with a common clbit. - - (input) - ┌─┐ - q_0: ┤M├─── - └╥┘┌─┐ - q_1: ─╫─┤M├ - ║ └╥┘ - c: 1/═╩══╩═ - 0 0 - - (scheduled) - ┌─┐┌────────────────┐ - q_0: ───────────────────┤M├┤ Delay(700[dt]) ├ - ┌─────────────────┐└╥┘└──────┬─┬───────┘ - q_1: ┤ Delay(1000[dt]) ├─╫────────┤M├──────── - └─────────────────┘ ║ └╥┘ - c: 1/════════════════════╩═════════╩═════════ - 0 0 - """ - qc = QuantumCircuit(2, 1) - qc.measure(0, 0) - qc.measure(1, 0) - - durations = InstructionDurations([("measure", [0], 1000), ("measure", [1], 700)]) - with self.assertWarns(DeprecationWarning): - pm = PassManager(schedule_pass(durations)) - scheduled = pm.run(qc) - - expected = QuantumCircuit(2, 1) - expected.measure(0, 0) - expected.delay(1000, 1) - expected.measure(1, 0) - expected.delay(700, 0) - - self.assertEqual(expected, scheduled) - - @data(ALAPSchedule, ASAPSchedule) - def test_measure_after_c_if(self, schedule_pass): - """Test if ALAP/ASAP schedules circuits with c_if after measure with a common clbit. - - (input) - ┌─┐ - q_0: ┤M├────────────── - └╥┘ ┌───┐ - q_1: ─╫────┤ X ├────── - ║ └─╥─┘ ┌─┐ - q_2: ─╫──────╫─────┤M├ - ║ ┌────╨────┐└╥┘ - c: 1/═╩═╡ c_0 = T ╞═╩═ - 0 └─────────┘ 0 - - (scheduled) - ┌─┐┌─────────────────┐ - q_0: ───────────────────┤M├┤ Delay(1000[dt]) ├────────────────── - ┌─────────────────┐└╥┘└──────┬───┬──────┘┌────────────────┐ - q_1: ┤ Delay(1000[dt]) ├─╫────────┤ X ├───────┤ Delay(800[dt]) ├ - ├─────────────────┤ ║ └─╥─┘ └──────┬─┬───────┘ - q_2: ┤ Delay(1000[dt]) ├─╫──────────╫────────────────┤M├──────── - └─────────────────┘ ║ ┌────╨────┐ └╥┘ - c: 1/════════════════════╩═════╡ c_0=0x1 ╞════════════╩═════════ - 0 └─────────┘ 0 - """ - qc = QuantumCircuit(3, 1) - qc.measure(0, 0) - with self.assertWarns(DeprecationWarning): - qc.x(1).c_if(0, 1) - qc.measure(2, 0) - - durations = InstructionDurations([("x", None, 200), ("measure", None, 1000)]) - with self.assertWarns(DeprecationWarning): - pm = PassManager(schedule_pass(durations)) - scheduled = pm.run(qc) - - expected = QuantumCircuit(3, 1) - expected.delay(1000, 1) - expected.delay(1000, 2) - expected.measure(0, 0) - with self.assertWarns(DeprecationWarning): - expected.x(1).c_if(0, 1) - expected.measure(2, 0) - expected.delay(1000, 0) - expected.delay(800, 1) - - self.assertEqual(expected, scheduled) - - def test_parallel_gate_different_length(self): - """Test circuit having two parallel instruction with different length. - - (input) - ┌───┐┌─┐ - q_0: ┤ X ├┤M├─── - ├───┤└╥┘┌─┐ - q_1: ┤ X ├─╫─┤M├ - └───┘ ║ └╥┘ - c: 2/══════╩══╩═ - 0 1 - - (expected, ALAP) - ┌────────────────┐┌───┐┌─┐ - q_0: ┤ Delay(200[dt]) ├┤ X ├┤M├ - └─────┬───┬──────┘└┬─┬┘└╥┘ - q_1: ──────┤ X ├────────┤M├──╫─ - └───┘ └╥┘ ║ - c: 2/════════════════════╩═══╩═ - 1 0 - - (expected, ASAP) - ┌───┐┌─┐┌────────────────┐ - q_0: ┤ X ├┤M├┤ Delay(200[dt]) ├ - ├───┤└╥┘└──────┬─┬───────┘ - q_1: ┤ X ├─╫────────┤M├──────── - └───┘ ║ └╥┘ - c: 2/══════╩═════════╩═════════ - 0 1 - - """ - qc = QuantumCircuit(2, 2) - qc.x(0) - qc.x(1) - qc.measure(0, 0) - qc.measure(1, 1) - - durations = InstructionDurations( - [("x", [0], 200), ("x", [1], 400), ("measure", None, 1000)] - ) - with self.assertWarns(DeprecationWarning): - pm = PassManager(ALAPSchedule(durations)) - qc_alap = pm.run(qc) - - alap_expected = QuantumCircuit(2, 2) - alap_expected.delay(200, 0) - alap_expected.x(0) - alap_expected.x(1) - alap_expected.measure(0, 0) - alap_expected.measure(1, 1) - - self.assertEqual(qc_alap, alap_expected) - - with self.assertWarns(DeprecationWarning): - pm = PassManager(ASAPSchedule(durations)) - qc_asap = pm.run(qc) - - asap_expected = QuantumCircuit(2, 2) - asap_expected.x(0) - asap_expected.x(1) - asap_expected.measure(0, 0) # immediately start after X gate - asap_expected.measure(1, 1) - asap_expected.delay(200, 0) - - self.assertEqual(qc_asap, asap_expected) - - def test_parallel_gate_different_length_with_barrier(self): - """Test circuit having two parallel instruction with different length with barrier. - - (input) - ┌───┐┌─┐ - q_0: ┤ X ├┤M├─── - ├───┤└╥┘┌─┐ - q_1: ┤ X ├─╫─┤M├ - └───┘ ║ └╥┘ - c: 2/══════╩══╩═ - 0 1 - - (expected, ALAP) - ┌────────────────┐┌───┐ ░ ┌─┐ - q_0: ┤ Delay(200[dt]) ├┤ X ├─░─┤M├─── - └─────┬───┬──────┘└───┘ ░ └╥┘┌─┐ - q_1: ──────┤ X ├─────────────░──╫─┤M├ - └───┘ ░ ║ └╥┘ - c: 2/═══════════════════════════╩══╩═ - 0 1 - - (expected, ASAP) - ┌───┐┌────────────────┐ ░ ┌─┐ - q_0: ┤ X ├┤ Delay(200[dt]) ├─░─┤M├─── - ├───┤└────────────────┘ ░ └╥┘┌─┐ - q_1: ┤ X ├───────────────────░──╫─┤M├ - └───┘ ░ ║ └╥┘ - c: 2/═══════════════════════════╩══╩═ - 0 1 - """ - qc = QuantumCircuit(2, 2) - qc.x(0) - qc.x(1) - qc.barrier() - qc.measure(0, 0) - qc.measure(1, 1) - - durations = InstructionDurations( - [("x", [0], 200), ("x", [1], 400), ("measure", None, 1000)] - ) - with self.assertWarns(DeprecationWarning): - pm = PassManager(ALAPSchedule(durations)) - qc_alap = pm.run(qc) - - alap_expected = QuantumCircuit(2, 2) - alap_expected.delay(200, 0) - alap_expected.x(0) - alap_expected.x(1) - alap_expected.barrier() - alap_expected.measure(0, 0) - alap_expected.measure(1, 1) - - self.assertEqual(qc_alap, alap_expected) - - with self.assertWarns(DeprecationWarning): - pm = PassManager(ASAPSchedule(durations)) - qc_asap = pm.run(qc) - - asap_expected = QuantumCircuit(2, 2) - asap_expected.x(0) - asap_expected.delay(200, 0) - asap_expected.x(1) - asap_expected.barrier() - asap_expected.measure(0, 0) - asap_expected.measure(1, 1) - - self.assertEqual(qc_asap, asap_expected) - - def test_measure_after_c_if_on_edge_locking(self): - """Test if ALAP/ASAP schedules circuits with c_if after measure with a common clbit. - - The scheduler is configured to reproduce behavior of the 0.20.0, - in which clbit lock is applied to the end-edge of measure instruction. - See https://github.com/Qiskit/qiskit-terra/pull/7655 - - (input) - ┌─┐ - q_0: ┤M├────────────── - └╥┘ ┌───┐ - q_1: ─╫────┤ X ├────── - ║ └─╥─┘ ┌─┐ - q_2: ─╫──────╫─────┤M├ - ║ ┌────╨────┐└╥┘ - c: 1/═╩═╡ c_0 = T ╞═╩═ - 0 └─────────┘ 0 - - (ASAP scheduled) - ┌─┐┌────────────────┐ - q_0: ───────────────────┤M├┤ Delay(200[dt]) ├───────────────────── - ┌─────────────────┐└╥┘└─────┬───┬──────┘ - q_1: ┤ Delay(1000[dt]) ├─╫───────┤ X ├──────────────────────────── - └─────────────────┘ ║ └─╥─┘ ┌─┐┌────────────────┐ - q_2: ────────────────────╫─────────╫─────────┤M├┤ Delay(200[dt]) ├ - ║ ┌────╨────┐ └╥┘└────────────────┘ - c: 1/════════════════════╩════╡ c_0=0x1 ╞═════╩═══════════════════ - 0 └─────────┘ 0 - - (ALAP scheduled) - ┌─┐┌────────────────┐ - q_0: ───────────────────┤M├┤ Delay(200[dt]) ├─── - ┌─────────────────┐└╥┘└─────┬───┬──────┘ - q_1: ┤ Delay(1000[dt]) ├─╫───────┤ X ├────────── - └┬────────────────┤ ║ └─╥─┘ ┌─┐ - q_2: ─┤ Delay(200[dt]) ├─╫─────────╫─────────┤M├ - └────────────────┘ ║ ┌────╨────┐ └╥┘ - c: 1/════════════════════╩════╡ c_0=0x1 ╞═════╩═ - 0 └─────────┘ 0 - - """ - qc = QuantumCircuit(3, 1) - qc.measure(0, 0) - with self.assertWarns(DeprecationWarning): - qc.x(1).c_if(0, 1) - qc.measure(2, 0) - - durations = InstructionDurations([("x", None, 200), ("measure", None, 1000)]) - - # lock at the end edge - with self.assertWarns(DeprecationWarning): - actual_asap = PassManager(ASAPSchedule(durations, clbit_write_latency=1000)).run(qc) - actual_alap = PassManager(ALAPSchedule(durations, clbit_write_latency=1000)).run(qc) - - # start times of 2nd measure depends on ASAP/ALAP - expected_asap = QuantumCircuit(3, 1) - expected_asap.measure(0, 0) - expected_asap.delay(1000, 1) - with self.assertWarns(DeprecationWarning): - expected_asap.x(1).c_if(0, 1) - expected_asap.measure(2, 0) - expected_asap.delay(200, 0) - expected_asap.delay(200, 2) - self.assertEqual(expected_asap, actual_asap) - - expected_alap = QuantumCircuit(3, 1) - expected_alap.measure(0, 0) - expected_alap.delay(1000, 1) - with self.assertWarns(DeprecationWarning): - expected_alap.x(1).c_if(0, 1) - expected_alap.delay(200, 2) - expected_alap.measure(2, 0) - expected_alap.delay(200, 0) - self.assertEqual(expected_alap, actual_alap) - - @data([100, 200], [500, 0], [1000, 200]) - @unpack - def test_active_reset_circuit(self, write_lat, cond_lat): - """Test practical example of reset circuit. - - Because of the stimulus pulse overlap with the previous XGate on the q register, - measure instruction is always triggered after XGate regardless of write latency. - Thus only conditional latency matters in the scheduling. - - (input) - ┌─┐ ┌───┐ ┌─┐ ┌───┐ ┌─┐ ┌───┐ - q: ┤M├───┤ X ├───┤M├───┤ X ├───┤M├───┤ X ├─── - └╥┘ └─╥─┘ └╥┘ └─╥─┘ └╥┘ └─╥─┘ - ║ ┌────╨────┐ ║ ┌────╨────┐ ║ ┌────╨────┐ - c: 1/═╩═╡ c_0=0x1 ╞═╩═╡ c_0=0x1 ╞═╩═╡ c_0=0x1 ╞ - 0 └─────────┘ 0 └─────────┘ 0 └─────────┘ - - """ - qc = QuantumCircuit(1, 1) - qc.measure(0, 0) - with self.assertWarns(DeprecationWarning): - qc.x(0).c_if(0, 1) - qc.measure(0, 0) - with self.assertWarns(DeprecationWarning): - qc.x(0).c_if(0, 1) - qc.measure(0, 0) - with self.assertWarns(DeprecationWarning): - qc.x(0).c_if(0, 1) - - durations = InstructionDurations([("x", None, 100), ("measure", None, 1000)]) - with self.assertWarns(DeprecationWarning): - actual_asap = PassManager( - ASAPSchedule(durations, clbit_write_latency=write_lat, conditional_latency=cond_lat) - ).run(qc) - actual_alap = PassManager( - ALAPSchedule(durations, clbit_write_latency=write_lat, conditional_latency=cond_lat) - ).run(qc) - - expected = QuantumCircuit(1, 1) - expected.measure(0, 0) - if cond_lat > 0: - expected.delay(cond_lat, 0) - with self.assertWarns(DeprecationWarning): - expected.x(0).c_if(0, 1) - expected.measure(0, 0) - if cond_lat > 0: - expected.delay(cond_lat, 0) - with self.assertWarns(DeprecationWarning): - expected.x(0).c_if(0, 1) - expected.measure(0, 0) - if cond_lat > 0: - expected.delay(cond_lat, 0) - with self.assertWarns(DeprecationWarning): - expected.x(0).c_if(0, 1) - - self.assertEqual(expected, actual_asap) - self.assertEqual(expected, actual_alap) - - def test_random_complicated_circuit(self): - """Test scheduling complicated circuit with control flow. - - (input) - ┌────────────────┐ ┌───┐ ░ ┌───┐ » - q_0: ┤ Delay(100[dt]) ├───┤ X ├────░──────────────────┤ X ├───» - └────────────────┘ └─╥─┘ ░ ┌───┐ └─╥─┘ » - q_1: ───────────────────────╫──────░───────┤ X ├────────╫─────» - ║ ░ ┌─┐ └─╥─┘ ║ » - q_2: ───────────────────────╫──────░─┤M├─────╫──────────╫─────» - ┌────╨────┐ ░ └╥┘┌────╨────┐┌────╨────┐» - c: 1/══════════════════╡ c_0=0x1 ╞════╩═╡ c_0=0x0 ╞╡ c_0=0x0 ╞» - └─────────┘ 0 └─────────┘└─────────┘» - « ┌────────────────┐┌───┐ - «q_0: ┤ Delay(300[dt]) ├┤ X ├─────■───── - « └────────────────┘└───┘ ┌─┴─┐ - «q_1: ────────■─────────────────┤ X ├─── - « ┌─┴─┐ ┌─┐ └─╥─┘ - «q_2: ──────┤ X ├────────┤M├──────╫───── - « └───┘ └╥┘ ┌────╨────┐ - «c: 1/════════════════════╩══╡ c_0=0x0 ╞ - « 0 └─────────┘ - - (ASAP scheduled) duration = 2800 dt - ┌────────────────┐┌────────────────┐ ┌───┐ ░ ┌─────────────────┐» - q_0: ┤ Delay(100[dt]) ├┤ Delay(100[dt]) ├───┤ X ├────░─┤ Delay(1400[dt]) ├» - ├────────────────┤└────────────────┘ └─╥─┘ ░ ├─────────────────┤» - q_1: ┤ Delay(300[dt]) ├───────────────────────╫──────░─┤ Delay(1200[dt]) ├» - ├────────────────┤ ║ ░ └───────┬─┬───────┘» - q_2: ┤ Delay(300[dt]) ├───────────────────────╫──────░─────────┤M├────────» - └────────────────┘ ┌────╨────┐ ░ └╥┘ » - c: 1/════════════════════════════════════╡ c_0=0x1 ╞════════════╩═════════» - └─────────┘ 0 » - « ┌───┐ ┌────────────────┐» - «q_0: ────────────────────────────────┤ X ├───┤ Delay(300[dt]) ├» - « ┌───┐ └─╥─┘ └────────────────┘» - «q_1: ───┤ X ├──────────────────────────╫─────────────■─────────» - « └─╥─┘ ┌────────────────┐ ║ ┌─┴─┐ » - «q_2: ─────╫─────┤ Delay(300[dt]) ├─────╫───────────┤ X ├───────» - « ┌────╨────┐└────────────────┘┌────╨────┐ └───┘ » - «c: 1/╡ c_0=0x0 ╞══════════════════╡ c_0=0x0 ╞══════════════════» - « └─────────┘ └─────────┘ » - « ┌───┐ ┌────────────────┐ - «q_0: ──────┤ X ├────────────■─────┤ Delay(700[dt]) ├ - « ┌─────┴───┴──────┐ ┌─┴─┐ ├────────────────┤ - «q_1: ┤ Delay(400[dt]) ├───┤ X ├───┤ Delay(700[dt]) ├ - « ├────────────────┤ └─╥─┘ └──────┬─┬───────┘ - «q_2: ┤ Delay(300[dt]) ├─────╫────────────┤M├──────── - « └────────────────┘┌────╨────┐ └╥┘ - «c: 1/══════════════════╡ c_0=0x0 ╞════════╩═════════ - « └─────────┘ 0 - - (ALAP scheduled) duration = 3100 - ┌────────────────┐┌────────────────┐ ┌───┐ ░ ┌─────────────────┐» - q_0: ┤ Delay(100[dt]) ├┤ Delay(100[dt]) ├───┤ X ├────░─┤ Delay(1400[dt]) ├» - ├────────────────┤└────────────────┘ └─╥─┘ ░ ├─────────────────┤» - q_1: ┤ Delay(300[dt]) ├───────────────────────╫──────░─┤ Delay(1200[dt]) ├» - ├────────────────┤ ║ ░ └───────┬─┬───────┘» - q_2: ┤ Delay(300[dt]) ├───────────────────────╫──────░─────────┤M├────────» - └────────────────┘ ┌────╨────┐ ░ └╥┘ » - c: 1/════════════════════════════════════╡ c_0=0x1 ╞════════════╩═════════» - └─────────┘ 0 » - « ┌───┐ ┌────────────────┐» - «q_0: ────────────────────────────────┤ X ├───┤ Delay(300[dt]) ├» - « ┌───┐ ┌────────────────┐ └─╥─┘ └────────────────┘» - «q_1: ───┤ X ├───┤ Delay(300[dt]) ├─────╫─────────────■─────────» - « └─╥─┘ ├────────────────┤ ║ ┌─┴─┐ » - «q_2: ─────╫─────┤ Delay(600[dt]) ├─────╫───────────┤ X ├───────» - « ┌────╨────┐└────────────────┘┌────╨────┐ └───┘ » - «c: 1/╡ c_0=0x0 ╞══════════════════╡ c_0=0x0 ╞══════════════════» - « └─────────┘ └─────────┘ » - « ┌───┐ ┌────────────────┐ - «q_0: ──────┤ X ├────────────■─────┤ Delay(700[dt]) ├ - « ┌─────┴───┴──────┐ ┌─┴─┐ ├────────────────┤ - «q_1: ┤ Delay(100[dt]) ├───┤ X ├───┤ Delay(700[dt]) ├ - « └──────┬─┬───────┘ └─╥─┘ └────────────────┘ - «q_2: ───────┤M├─────────────╫─────────────────────── - « └╥┘ ┌────╨────┐ - «c: 1/════════╩═════════╡ c_0=0x0 ╞══════════════════ - « 0 └─────────┘ - - """ - qc = QuantumCircuit(3, 1) - qc.delay(100, 0) - with self.assertWarns(DeprecationWarning): - qc.x(0).c_if(0, 1) - qc.barrier() - qc.measure(2, 0) - with self.assertWarns(DeprecationWarning): - qc.x(1).c_if(0, 0) - with self.assertWarns(DeprecationWarning): - qc.x(0).c_if(0, 0) - qc.delay(300, 0) - qc.cx(1, 2) - qc.x(0) - with self.assertWarns(DeprecationWarning): - qc.cx(0, 1).c_if(0, 0) - qc.measure(2, 0) - - durations = InstructionDurations( - [("x", None, 100), ("measure", None, 1000), ("cx", None, 200)] - ) - - with self.assertWarns(DeprecationWarning): - actual_asap = PassManager( - ASAPSchedule(durations, clbit_write_latency=100, conditional_latency=200) - ).run(qc) - actual_alap = PassManager( - ALAPSchedule(durations, clbit_write_latency=100, conditional_latency=200) - ).run(qc) - - expected_asap = QuantumCircuit(3, 1) - expected_asap.delay(100, 0) - expected_asap.delay(100, 0) # due to conditional latency of 200dt - expected_asap.delay(300, 1) - expected_asap.delay(300, 2) - with self.assertWarns(DeprecationWarning): - expected_asap.x(0).c_if(0, 1) - expected_asap.barrier() - expected_asap.delay(1400, 0) - expected_asap.delay(1200, 1) - expected_asap.measure(2, 0) - with self.assertWarns(DeprecationWarning): - expected_asap.x(1).c_if(0, 0) - with self.assertWarns(DeprecationWarning): - expected_asap.x(0).c_if(0, 0) - expected_asap.delay(300, 0) - expected_asap.x(0) - expected_asap.delay(300, 2) - expected_asap.cx(1, 2) - expected_asap.delay(400, 1) - with self.assertWarns(DeprecationWarning): - expected_asap.cx(0, 1).c_if(0, 0) - expected_asap.delay(700, 0) # creg is released at t0 of cx(0,1).c_if(0,0) - expected_asap.delay( - 700, 1 - ) # no creg write until 100dt. thus measure can move left by 300dt. - expected_asap.delay(300, 2) - expected_asap.measure(2, 0) - self.assertEqual(expected_asap, actual_asap) - self.assertEqual(actual_asap.duration, 3100) - - expected_alap = QuantumCircuit(3, 1) - expected_alap.delay(100, 0) - expected_alap.delay(100, 0) # due to conditional latency of 200dt - expected_alap.delay(300, 1) - expected_alap.delay(300, 2) - with self.assertWarns(DeprecationWarning): - expected_alap.x(0).c_if(0, 1) - expected_alap.barrier() - expected_alap.delay(1400, 0) - expected_alap.delay(1200, 1) - expected_alap.measure(2, 0) - with self.assertWarns(DeprecationWarning): - expected_alap.x(1).c_if(0, 0) - with self.assertWarns(DeprecationWarning): - expected_alap.x(0).c_if(0, 0) - expected_alap.delay(300, 0) - expected_alap.x(0) - expected_alap.delay(300, 1) - expected_alap.delay(600, 2) - expected_alap.cx(1, 2) - expected_alap.delay(100, 1) - with self.assertWarns(DeprecationWarning): - expected_alap.cx(0, 1).c_if(0, 0) - expected_alap.measure(2, 0) - expected_alap.delay(700, 0) - expected_alap.delay(700, 1) - self.assertEqual(expected_alap, actual_alap) - self.assertEqual(actual_alap.duration, 3100) - - def test_dag_introduces_extra_dependency_between_conditionals(self): - """Test dependency between conditional operations in the scheduling. - - In the below example circuit, the conditional x on q1 could start at time 0, - however it must be scheduled after the conditional x on q0 in ASAP scheduling. - That is because circuit model used in the transpiler passes (DAGCircuit) - interprets instructions acting on common clbits must be run in the order - given by the original circuit (QuantumCircuit). - - (input) - ┌────────────────┐ ┌───┐ - q_0: ┤ Delay(100[dt]) ├───┤ X ├─── - └─────┬───┬──────┘ └─╥─┘ - q_1: ──────┤ X ├────────────╫───── - └─╥─┘ ║ - ┌────╨────┐ ┌────╨────┐ - c: 1/═══╡ c_0=0x1 ╞════╡ c_0=0x1 ╞ - └─────────┘ └─────────┘ - - (ASAP scheduled) - ┌────────────────┐ ┌───┐ - q_0: ┤ Delay(100[dt]) ├───┤ X ├────────────── - ├────────────────┤ └─╥─┘ ┌───┐ - q_1: ┤ Delay(100[dt]) ├─────╫────────┤ X ├─── - └────────────────┘ ║ └─╥─┘ - ┌────╨────┐┌────╨────┐ - c: 1/══════════════════╡ c_0=0x1 ╞╡ c_0=0x1 ╞ - └─────────┘└─────────┘ - """ - qc = QuantumCircuit(2, 1) - qc.delay(100, 0) - with self.assertWarns(DeprecationWarning): - qc.x(0).c_if(0, True) - with self.assertWarns(DeprecationWarning): - qc.x(1).c_if(0, True) - - durations = InstructionDurations([("x", None, 160)]) - with self.assertWarns(DeprecationWarning): - pm = PassManager(ASAPSchedule(durations)) - scheduled = pm.run(qc) - - expected = QuantumCircuit(2, 1) - expected.delay(100, 0) - expected.delay(100, 1) # due to extra dependency on clbits - with self.assertWarns(DeprecationWarning): - expected.x(0).c_if(0, True) - with self.assertWarns(DeprecationWarning): - expected.x(1).c_if(0, True) - - self.assertEqual(expected, scheduled) - - @data(ALAPSchedule, ASAPSchedule) - def test_respect_target_instruction_constraints(self, schedule_pass): - """Test if ALAP/ASAP does not pad delays for qubits that do not support delay instructions. - See: https://github.com/Qiskit/qiskit-terra/issues/9993 - """ - target = Target(dt=1) - target.add_instruction(XGate(), {(1,): InstructionProperties(duration=200)}) - # delays are not supported - - qc = QuantumCircuit(2) - qc.x(1) - - with self.assertWarns(DeprecationWarning): - pm = PassManager(schedule_pass(target=target)) - scheduled = pm.run(qc) - - expected = QuantumCircuit(2) - expected.x(1) - # no delay on qubit 0 - - self.assertEqual(expected, scheduled) - - def test_dd_respect_target_instruction_constraints(self): - """Test if DD pass does not pad delays for qubits that do not support delay instructions - and does not insert DD gates for qubits that do not support necessary gates. - See: https://github.com/Qiskit/qiskit-terra/issues/9993 - """ - qc = QuantumCircuit(3) - qc.cx(0, 1) - qc.cx(1, 2) - - target = Target(dt=1) - # Y is partially supported (not supported on qubit 2) - target.add_instruction( - XGate(), {(q,): InstructionProperties(duration=100) for q in range(2)} - ) - target.add_instruction( - CXGate(), - { - (0, 1): InstructionProperties(duration=1000), - (1, 2): InstructionProperties(duration=1000), - }, - ) - # delays are not supported - - # No DD instructions nor delays are padded due to no delay support in the target - with self.assertWarns(DeprecationWarning): - pm_scheduler = PassManager( - [ - ALAPSchedule(target=target), - DynamicalDecoupling( - durations=None, dd_sequence=[XGate(), XGate()], target=target - ), - ] - ) - scheduled = pm_scheduler.run(qc) - self.assertEqual(qc, scheduled) - - # Fails since Y is not supported in the target - with self.assertWarns(DeprecationWarning): - with self.assertRaises(TranspilerError): - PassManager( - [ - ALAPSchedule(target=target), - DynamicalDecoupling( - durations=None, - dd_sequence=[XGate(), YGate(), XGate(), YGate()], - target=target, - ), - ] - ) - - # Add delay support to the target - target.add_instruction(Delay(Parameter("t")), {(q,): None for q in range(3)}) - # No error but no DD on qubit 2 (just delay is padded) since X is not supported on it - scheduled = pm_scheduler.run(qc) - - expected = QuantumCircuit(3) - expected.delay(1000, [2]) - expected.cx(0, 1) - expected.cx(1, 2) - expected.delay(200, [0]) - expected.x([0]) - expected.delay(400, [0]) - expected.x([0]) - expected.delay(200, [0]) - self.assertEqual(expected, scheduled) - - -if __name__ == "__main__": - unittest.main() From 7f0bdad994f822893f24c5fae6c718f13830f8a3 Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Wed, 19 Feb 2025 17:55:35 +0000 Subject: [PATCH 03/10] Pin symengine in backwards compatibility tests (#13885) (#13887) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Add symengine constraint in qpy backwards compat tests * Add comments * Only pin upper limit * Update comment (cherry picked from commit 8c06fd20be537377a7b96be687df76050e2e82f1) Co-authored-by: Elena Peña Tapia <57907331+ElePT@users.noreply.github.com> --- requirements.txt | 5 +++-- test/qpy_compat/qpy_test_constraints.txt | 5 +++++ test/qpy_compat/run_tests.sh | 2 ++ 3 files changed, 10 insertions(+), 2 deletions(-) diff --git a/requirements.txt b/requirements.txt index 6eb5902ae9fd..845aea9c8f29 100644 --- a/requirements.txt +++ b/requirements.txt @@ -7,6 +7,7 @@ python-dateutil>=2.8.0 stevedore>=3.0.0 typing-extensions -# If updating the version range here, consider updating 'test/qpy_compat/run_tests.sh' to update the -# list of symengine dependencies used in the cross-version tests. +# If updating the version range here, consider updating the +# list of symengine dependencies used in the cross-version tests +# in 'test/qpy_compat/run_tests.sh' and 'test/qpy_compat/qpy_test_constraints.txt' symengine>=0.11,<0.14 diff --git a/test/qpy_compat/qpy_test_constraints.txt b/test/qpy_compat/qpy_test_constraints.txt index 03f798b3c01a..9b407a59b6ba 100644 --- a/test/qpy_compat/qpy_test_constraints.txt +++ b/test/qpy_compat/qpy_test_constraints.txt @@ -1,2 +1,7 @@ numpy===1.24.4 scipy===1.10.1 + +# This is a loose constraint because we want to test different versions, +# as defined in 'test/qpy_compat/run_tests.sh', but any symengine version +# above (and including) 0.14 will be incompatible with qpy. +symengine<0.14 diff --git a/test/qpy_compat/run_tests.sh b/test/qpy_compat/run_tests.sh index f1c770809c77..fd9e17c58071 100755 --- a/test/qpy_compat/run_tests.sh +++ b/test/qpy_compat/run_tests.sh @@ -57,6 +57,8 @@ popd # This will likely duplicate the base dev-compatibility test, but the tests are fairly fast, and # it's better safe than sorry with the serialisation tests. +# Note that the constraint in the range of symengine versions is logically duplicated +# in `qpy_test_constraints.txt` symengine_versions=( '>=0.11,<0.12' '>=0.13,<0.14' From 55c7904879b3489ffe56384ae747c45bd0488695 Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Wed, 19 Feb 2025 18:28:00 +0000 Subject: [PATCH 04/10] Applying a slight perturbation for ill-conditioned matrices (#13882) (#13889) * Applying a slight perturbation for ill-conditioned matrices * Further improving the fix based on discussion with Jake and Gadi, and adding bugfix release note * Jake's suggestion from code review (cherry picked from commit 69eb2aa4f7edc9b8d189859ca8d6f3d2791b96b0) Co-authored-by: Alexander Ivrii --- .../operators/channel/transformations.py | 15 +++++++++++++++ .../notes/choi-to-kraus-3ae7d49f0a27f639.yaml | 5 +++++ 2 files changed, 20 insertions(+) create mode 100644 releasenotes/notes/choi-to-kraus-3ae7d49f0a27f639.yaml diff --git a/qiskit/quantum_info/operators/channel/transformations.py b/qiskit/quantum_info/operators/channel/transformations.py index 657ee62703ec..79619f798832 100644 --- a/qiskit/quantum_info/operators/channel/transformations.py +++ b/qiskit/quantum_info/operators/channel/transformations.py @@ -234,8 +234,23 @@ def _choi_to_kraus(data, input_dim, output_dim, atol=ATOL_DEFAULT): # # So the eigenvalues are on the diagonal, therefore the basis-transformation matrix must be # a spanning set of the eigenspace. + # + # In addition, to prevent `numpy.linalg` errors when the matrix A is ill-conditioned, + # we apply a small perturbation, replacing A by A + eI. Since (A + eI)x = kx is + # equivalent to Ax = (k-e)x, it means that the eigenvectors of A + eI and A are the same, + # and we can perfectly recover the eigenvalues of A from the eigenvalues of A + eI by + # subtracting e. + apply_perturbation = np.linalg.cond(data) >= 1e10 + + if apply_perturbation: + data += 1e-10 * np.eye(data.shape[0]) + triangular, vecs = scipy.linalg.schur(data) values = triangular.diagonal().real + + if apply_perturbation: + values = values - 1e-10 + # If we're not a CP map, fall-through back to the generalization handling. Since we needed # to get the eigenvalues anyway, we can do the CP check manually rather than deferring to a # separate re-calculation. diff --git a/releasenotes/notes/choi-to-kraus-3ae7d49f0a27f639.yaml b/releasenotes/notes/choi-to-kraus-3ae7d49f0a27f639.yaml new file mode 100644 index 000000000000..bd055121aff3 --- /dev/null +++ b/releasenotes/notes/choi-to-kraus-3ae7d49f0a27f639.yaml @@ -0,0 +1,5 @@ +--- +fixes: + - | + Applied a small regularisation factor against ill-conditioned Hermitian matrices + in super-operator representations. From 589b33892dfefebaaeff1750bbf5405af48e1e48 Mon Sep 17 00:00:00 2001 From: Jake Lishman Date: Thu, 20 Feb 2025 06:49:18 +0000 Subject: [PATCH 05/10] Remove accidentally-public methods from `QuantumCircuit` (#13893) These were never really meant to be public at all, and we didn't even notice they existed publicly til we rewrote the `QuantumCircuit` documentation. --- qiskit/circuit/quantumcircuit.py | 89 ------------------- ...remove-circuit-cruft-30740668cf2f04e0.yaml | 13 +++ 2 files changed, 13 insertions(+), 89 deletions(-) create mode 100644 releasenotes/notes/remove-circuit-cruft-30740668cf2f04e0.yaml diff --git a/qiskit/circuit/quantumcircuit.py b/qiskit/circuit/quantumcircuit.py index e73769a42bc0..868da61b95ab 100644 --- a/qiskit/circuit/quantumcircuit.py +++ b/qiskit/circuit/quantumcircuit.py @@ -962,19 +962,6 @@ class QuantumCircuit: .. automethod:: decompose .. automethod:: reverse_bits - - Internal utilities - ================== - - These functions are not intended for public use, but were accidentally left documented in the - public API during the 1.0 release. They will be removed in Qiskit 2.0, but will be supported - until then. - - .. automethod:: cast - .. automethod:: cbit_argument_conversion - .. automethod:: cls_instances - .. automethod:: cls_prefix - .. automethod:: qbit_argument_conversion """ instances = 0 @@ -1456,35 +1443,12 @@ def __deepcopy__(self, memo=None): def _increment_instances(cls): cls.instances += 1 - @classmethod - @deprecate_func( - since=1.2, - removal_timeline="in the 2.0 release", - additional_msg="This method is only used as an internal helper " - "and will be removed with no replacement.", - ) - def cls_instances(cls) -> int: - """Return the current number of instances of this class, - useful for auto naming.""" - return cls.instances - @classmethod def _cls_instances(cls) -> int: """Return the current number of instances of this class, useful for auto naming.""" return cls.instances - @classmethod - @deprecate_func( - since=1.2, - removal_timeline="in the 2.0 release", - additional_msg="This method is only used as an internal helper " - "and will be removed with no replacement.", - ) - def cls_prefix(cls) -> str: - """Return the prefix to use for auto naming.""" - return cls.prefix - @classmethod def _cls_prefix(cls) -> str: """Return the prefix to use for auto naming.""" @@ -2347,20 +2311,6 @@ def __getitem__(self, item): """Return indexed operation.""" return self._data[item] - @staticmethod - @deprecate_func( - since=1.2, - removal_timeline="in the 2.0 release", - additional_msg="This method is only used as an internal helper " - "and will be removed with no replacement.", - ) - def cast(value: S, type_: Callable[..., T]) -> Union[S, T]: - """Best effort to cast value to type. Otherwise, returns the value.""" - try: - return type_(value) - except (ValueError, TypeError): - return value - @staticmethod def _cast(value: S, type_: Callable[..., T]) -> Union[S, T]: """Best effort to cast value to type. Otherwise, returns the value.""" @@ -2369,26 +2319,6 @@ def _cast(value: S, type_: Callable[..., T]) -> Union[S, T]: except (ValueError, TypeError): return value - @deprecate_func( - since=1.2, - removal_timeline="in the 2.0 release", - additional_msg="This method is only used as an internal helper " - "and will be removed with no replacement.", - ) - def qbit_argument_conversion(self, qubit_representation: QubitSpecifier) -> list[Qubit]: - """ - Converts several qubit representations (such as indexes, range, etc.) - into a list of qubits. - - Args: - qubit_representation: Representation to expand. - - Returns: - The resolved instances of the qubits. - """ - - return self._qbit_argument_conversion(qubit_representation) - def _qbit_argument_conversion(self, qubit_representation: QubitSpecifier) -> list[Qubit]: """ Converts several qubit representations (such as indexes, range, etc.) @@ -2404,25 +2334,6 @@ def _qbit_argument_conversion(self, qubit_representation: QubitSpecifier) -> lis qubit_representation, self.qubits, self._qubit_indices, Qubit ) - @deprecate_func( - since=1.2, - removal_timeline="in the 2.0 release", - additional_msg="This method is only used as an internal helper " - "and will be removed with no replacement.", - ) - def cbit_argument_conversion(self, clbit_representation: ClbitSpecifier) -> list[Clbit]: - """ - Converts several classical bit representations (such as indexes, range, etc.) - into a list of classical bits. - - Args: - clbit_representation : Representation to expand. - - Returns: - A list of tuples where each tuple is a classical bit. - """ - return self._cbit_argument_conversion(clbit_representation) - def _cbit_argument_conversion(self, clbit_representation: ClbitSpecifier) -> list[Clbit]: """ Converts several classical bit representations (such as indexes, range, etc.) diff --git a/releasenotes/notes/remove-circuit-cruft-30740668cf2f04e0.yaml b/releasenotes/notes/remove-circuit-cruft-30740668cf2f04e0.yaml new file mode 100644 index 000000000000..182545c4e6ed --- /dev/null +++ b/releasenotes/notes/remove-circuit-cruft-30740668cf2f04e0.yaml @@ -0,0 +1,13 @@ +--- +upgrade_circuits: + - | + The :class:`.QuantumCircuit` methods: + + * ``cast`` + * ``cbit_argument_conversion`` + * ``cls_instances`` + * ``cls_prefix`` + * ``qbit_argument_conversion`` + + have been removed, following their deprecation in Qiskit 1.2. These methods were internal + helper functions, and never intended to be public API. No replacement is provided. From a2da24b5abe7fc7a77dd4fb26d0d2a266f076eb8 Mon Sep 17 00:00:00 2001 From: Matthew Treinish Date: Thu, 20 Feb 2025 04:25:25 -0500 Subject: [PATCH 06/10] Fix arithmetic bug in estimate_duration method (#13888) In the recently merged #13783 a bug slipped in an unexercised code path where we were dividing by dt instead of multiplying. This was fixed in the common code path where dt is an int, but nothing in the data model disallows float durations for dt units despite it not being real. Since the data model allowed it the rust code needed to support it and that path did the wrong operation. This commit fixes that oversight in the rust code and adds a test for it. --- crates/accelerate/src/circuit_duration.rs | 2 +- test/python/circuit/test_scheduled_circuit.py | 33 +++++++++++++++++++ 2 files changed, 34 insertions(+), 1 deletion(-) diff --git a/crates/accelerate/src/circuit_duration.rs b/crates/accelerate/src/circuit_duration.rs index 0e9738822f84..0fe7f998ac19 100644 --- a/crates/accelerate/src/circuit_duration.rs +++ b/crates/accelerate/src/circuit_duration.rs @@ -46,7 +46,7 @@ pub(crate) fn compute_estimated_duration(dag: &DAGCircuit, target: &Target) -> P match dur { Param::Float(val) => { - Ok(val / dt) + Ok(val * dt) }, Param::Obj(val) => { diff --git a/test/python/circuit/test_scheduled_circuit.py b/test/python/circuit/test_scheduled_circuit.py index 0793139429ef..494482811530 100644 --- a/test/python/circuit/test_scheduled_circuit.py +++ b/test/python/circuit/test_scheduled_circuit.py @@ -528,6 +528,39 @@ def test_estimate_duration_with_long_delay(self, unit): } self.assertEqual(duration, expected_val[unit]) + @data("s", "dt", "f", "p", "n", "u", "µ", "m", "k", "M", "G", "T", "P") + def test_estimate_duration_with_dt_float(self, unit): + # This is not a valid use case, but it is still expressible currently + # since we don't disallow fractional dt values. This should not be assumed + # to be a part of an api contract. If there is a refactor and this test + # breaks remove the test it is not valid. This was only added to provide + # explicit test coverage for a rust code path. + backend = GenericBackendV2(num_qubits=3, seed=42) + + circ = QuantumCircuit(3) + circ.cx(0, 1) + circ.measure_all() + circ.delay(1.23e15, 2, unit="dt") + circuit_dt = transpile(circ, backend, scheduling_method="asap") + duration = circuit_dt.estimate_duration(backend.target, unit=unit) + expected_in_sec = 273060.0000013993 + expected_val = { + "s": expected_in_sec, + "dt": int(expected_in_sec / backend.target.dt), + "f": expected_in_sec / 1e-15, + "p": expected_in_sec / 1e-12, + "n": expected_in_sec / 1e-9, + "u": expected_in_sec / 1e-6, + "µ": expected_in_sec / 1e-6, + "m": expected_in_sec / 1e-3, + "k": expected_in_sec / 1e3, + "M": expected_in_sec / 1e6, + "G": expected_in_sec / 1e9, + "T": expected_in_sec / 1e12, + "P": expected_in_sec / 1e15, + } + self.assertEqual(duration, expected_val[unit]) + def test_estimate_duration_invalid_unit(self): backend = GenericBackendV2(num_qubits=3, seed=42) From 34fa48905ef58761680b367571f144c0d5ec2d19 Mon Sep 17 00:00:00 2001 From: Jake Lishman Date: Thu, 20 Feb 2025 14:55:52 +0000 Subject: [PATCH 07/10] Update mergify to point to latest backport branch (#13896) With the release of 1.3.3 and the imminent release of 1.4.0, the regular backport branch is now `stable/1.4`. --- .mergify.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.mergify.yml b/.mergify.yml index 3b5ad2c9ed31..7d2ac03e4266 100644 --- a/.mergify.yml +++ b/.mergify.yml @@ -6,4 +6,4 @@ pull_request_rules: actions: backport: branches: - - stable/1.3 + - stable/1.4 From 83c20e99875695ee3147e44011147b6ff4733828 Mon Sep 17 00:00:00 2001 From: Luciano Bello Date: Thu, 20 Feb 2025 18:39:47 +0100 Subject: [PATCH 08/10] add documentation about instruction duration (#13592) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * add docuemntation about instruction duration * more docs and some spelling * Apply suggestions from code review Co-authored-by: Elena Peña Tapia <57907331+ElePT@users.noreply.github.com> * Update qiskit/circuit/instruction.py * Fix black * Fix lint --------- Co-authored-by: Elena Peña Tapia <57907331+ElePT@users.noreply.github.com> Co-authored-by: Elena Peña Tapia --- qiskit/circuit/instruction.py | 10 ++++++++-- qiskit/pulse/utils.py | 2 +- qiskit/visualization/timeline/core.py | 9 +++++++++ qiskit/visualization/timeline/interface.py | 4 ++++ 4 files changed, 22 insertions(+), 3 deletions(-) diff --git a/qiskit/circuit/instruction.py b/qiskit/circuit/instruction.py index cc51f2459b55..c53383446f23 100644 --- a/qiskit/circuit/instruction.py +++ b/qiskit/circuit/instruction.py @@ -63,14 +63,20 @@ class Instruction(Operation): def __init__(self, name, num_qubits, num_clbits, params, duration=None, unit="dt", label=None): """Create a new instruction. + .. deprecated:: 1.3 + The parameters ``duration`` and ``unit`` are deprecated since + Qiskit 1.3, and they will be removed in 2.0 or later. + An instruction's duration is defined in a backend's Target object. + Args: name (str): instruction name num_qubits (int): instruction's qubit width num_clbits (int): instruction's clbit width params (list[int|float|complex|str|ndarray|list|ParameterExpression]): list of parameters - duration (int or float): instruction's duration. it must be integer if ``unit`` is 'dt' - unit (str): time unit of duration + duration (int|float): (DEPRECATED) instruction's duration. it must be + an integer if ``unit`` is ``'dt'`` + unit (str): (DEPRECATED) time unit of duration label (str or None): An optional label for identifying the instruction. Raises: diff --git a/qiskit/pulse/utils.py b/qiskit/pulse/utils.py index 5f345917761e..2f2093817c8a 100644 --- a/qiskit/pulse/utils.py +++ b/qiskit/pulse/utils.py @@ -51,7 +51,7 @@ def format_parameter_value( decimal: Number of digit to round returned value. Returns: - Value casted to non-parameter data type, when possible. + Value cast to non-parameter data type, when possible. """ if isinstance(operand, ParameterExpression): try: diff --git a/qiskit/visualization/timeline/core.py b/qiskit/visualization/timeline/core.py index 71e2735559a8..a6f3ecff4c18 100644 --- a/qiskit/visualization/timeline/core.py +++ b/qiskit/visualization/timeline/core.py @@ -142,6 +142,15 @@ def add_data(self, data: drawings.ElementaryData): def load_program(self, program: circuit.QuantumCircuit, target: Target | None = None): """Load quantum circuit and create drawing.. + .. deprecated:: 1.3 + Visualization of unscheduled circuits with the timeline drawer has been + deprecated in Qiskit 1.3. + This circuit should be transpiled with a scheduler, despite having instructions + with explicit durations. + + .. deprecated:: 1.3 + Targets with duration-less operations are going to error in Qiskit 2.0. + Args: program: Scheduled circuit object to draw. target: The target the circuit is scheduled for. This contains backend information diff --git a/qiskit/visualization/timeline/interface.py b/qiskit/visualization/timeline/interface.py index 1818f78a9073..fa194403cbfa 100644 --- a/qiskit/visualization/timeline/interface.py +++ b/qiskit/visualization/timeline/interface.py @@ -52,6 +52,10 @@ def draw( ): r"""Generate visualization data for scheduled circuit programs. + .. deprecated:: 1.3 + The ``target`` parameter needs to be specified in Qiskit 2.0 in order to get the + instruction durations. + Args: program: Program to visualize. This program should be a `QuantumCircuit` which is transpiled with a scheduling_method, thus containing gate time information. From ca91bfd73cbc34d34d6f12b6e96f1e6a75ff6e68 Mon Sep 17 00:00:00 2001 From: Kevin Hartman Date: Thu, 20 Feb 2025 16:19:55 -0500 Subject: [PATCH 09/10] Don't use match since we still support Python 3.9. --- qiskit/qasm3/exporter.py | 23 +++++++++++------------ qiskit/qpy/binary_io/value.py | 35 +++++++++++++++++------------------ qiskit/qpy/type_keys.py | 28 ++++++++++++++-------------- 3 files changed, 42 insertions(+), 44 deletions(-) diff --git a/qiskit/qasm3/exporter.py b/qiskit/qasm3/exporter.py index 2a818cebbd7d..170738f7e8dc 100644 --- a/qiskit/qasm3/exporter.py +++ b/qiskit/qasm3/exporter.py @@ -1285,7 +1285,7 @@ def __init__(self, lookup): def visit_var(self, node, /): return self.lookup(node) if node.standalone else self.lookup(node.var) - # pylint: disable=R0911 + # pylint: disable=too-many-return-statements def visit_value(self, node, /): if node.type.kind is types.Bool: return ast.BooleanLiteral(node.value) @@ -1294,17 +1294,16 @@ def visit_value(self, node, /): if node.type.kind is types.Float: return ast.FloatLiteral(node.value) if node.type.kind is types.Duration: - match node.value: - case Duration.dt(dt): - return ast.DurationLiteral(dt, ast.DurationUnit.SAMPLE) - case Duration.ns(ns): - return ast.DurationLiteral(ns, ast.DurationUnit.NANOSECOND) - case Duration.us(us): - return ast.DurationLiteral(us, ast.DurationUnit.MICROSECOND) - case Duration.ms(ms): - return ast.DurationLiteral(ms, ast.DurationUnit.MILLISECOND) - case Duration.s(sec): - return ast.DurationLiteral(sec, ast.DurationUnit.SECOND) + if isinstance(node.value, Duration.dt): + return ast.DurationLiteral(node.value[0], ast.DurationUnit.SAMPLE) + if isinstance(node.value, Duration.ns): + return ast.DurationLiteral(node.value[0], ast.DurationUnit.NANOSECOND) + if isinstance(node.value, Duration.us): + return ast.DurationLiteral(node.value[0], ast.DurationUnit.MICROSECOND) + if isinstance(node.value, Duration.ms): + return ast.DurationLiteral(node.value[0], ast.DurationUnit.MILLISECOND) + if isinstance(node.value, Duration.sec): + return ast.DurationLiteral(node.value[0], ast.DurationUnit.SECOND) raise RuntimeError(f"unhandled Value type '{node}'") def visit_cast(self, node, /): diff --git a/qiskit/qpy/binary_io/value.py b/qiskit/qpy/binary_io/value.py index bbef0621c609..4f7a8a1297f8 100644 --- a/qiskit/qpy/binary_io/value.py +++ b/qiskit/qpy/binary_io/value.py @@ -426,24 +426,23 @@ def _write_expr_type_v14(file_obj, type_: types.Type): def _write_duration(file_obj, duration: Duration): - match duration: - case Duration.dt(dt): - file_obj.write(type_keys.CircuitDuration.DT) - file_obj.write(struct.pack(formats.DURATION_DT_PACK, *formats.DURATION_DT(dt))) - case Duration.ns(ns): - file_obj.write(type_keys.CircuitDuration.NS) - file_obj.write(struct.pack(formats.DURATION_NS_PACK, *formats.DURATION_NS(ns))) - case Duration.us(us): - file_obj.write(type_keys.CircuitDuration.US) - file_obj.write(struct.pack(formats.DURATION_US_PACK, *formats.DURATION_US(us))) - case Duration.ms(ms): - file_obj.write(type_keys.CircuitDuration.MS) - file_obj.write(struct.pack(formats.DURATION_MS_PACK, *formats.DURATION_MS(ms))) - case Duration.s(sec): - file_obj.write(type_keys.CircuitDuration.S) - file_obj.write(struct.pack(formats.DURATION_S_PACK, *formats.DURATION_S(sec))) - case _: - raise exceptions.QpyError(f"unhandled Duration object '{duration};") + if isinstance(duration, Duration.dt): + file_obj.write(type_keys.CircuitDuration.DT) + file_obj.write(struct.pack(formats.DURATION_DT_PACK, *formats.DURATION_DT(duration[0]))) + elif isinstance(duration, Duration.ns): + file_obj.write(type_keys.CircuitDuration.NS) + file_obj.write(struct.pack(formats.DURATION_NS_PACK, *formats.DURATION_NS(duration[0]))) + elif isinstance(duration, Duration.us): + file_obj.write(type_keys.CircuitDuration.US) + file_obj.write(struct.pack(formats.DURATION_US_PACK, *formats.DURATION_US(duration[0]))) + elif isinstance(duration, Duration.ms): + file_obj.write(type_keys.CircuitDuration.MS) + file_obj.write(struct.pack(formats.DURATION_MS_PACK, *formats.DURATION_MS(duration[0]))) + elif isinstance(duration, Duration.sec): + file_obj.write(type_keys.CircuitDuration.S) + file_obj.write(struct.pack(formats.DURATION_S_PACK, *formats.DURATION_S(duration[0]))) + else: + raise exceptions.QpyError(f"unhandled Duration object '{duration};") def _read_parameter(file_obj): diff --git a/qiskit/qpy/type_keys.py b/qiskit/qpy/type_keys.py index 016b4bc6f5cb..20efa48a504b 100644 --- a/qiskit/qpy/type_keys.py +++ b/qiskit/qpy/type_keys.py @@ -575,20 +575,20 @@ class CircuitDuration(TypeKeyBase): @classmethod def assign(cls, obj): - match obj: - case Duration.dt(_): - return cls.DT - case Duration.ns(_): - return cls.NS - case Duration.us(_): - return cls.US - case Duration.ms(_): - return cls.MS - case Duration.s(_): - return cls.S - raise exceptions.QpyError( - f"Object type '{type(obj)}' is not supported in {cls.__name__} namespace." - ) + if isinstance(obj, Duration.dt): + return cls.DT + elif isinstance(obj, Duration.ns): + return cls.NS + elif isinstance(obj, Duration.us): + return cls.US + elif isinstance(obj, Duration.ms): + return cls.MS + elif isinstance(obj, Duration.sec): + return cls.S + else: + raise exceptions.QpyError( + f"Object type '{type(obj)}' is not supported in {cls.__name__} namespace." + ) @classmethod def retrieve(cls, type_key): From 723e4f0b40e1643de3f065e9d9ace8b42f79bfcd Mon Sep 17 00:00:00 2001 From: Kevin Hartman Date: Thu, 20 Feb 2025 16:40:45 -0500 Subject: [PATCH 10/10] Block const stores. --- qiskit/circuit/quantumcircuit.py | 2 ++ test/python/circuit/test_store.py | 7 +++++++ 2 files changed, 9 insertions(+) diff --git a/qiskit/circuit/quantumcircuit.py b/qiskit/circuit/quantumcircuit.py index ee3d66ee1610..90671a0595e8 100644 --- a/qiskit/circuit/quantumcircuit.py +++ b/qiskit/circuit/quantumcircuit.py @@ -3786,6 +3786,8 @@ def store(self, lvalue: typing.Any, rvalue: typing.Any, /) -> InstructionSet: """ # As a convenience, lift integer-literal rvalues to the matching width. lvalue = expr.lift(lvalue) + if lvalue.type.const: + raise CircuitError("const variables are not supported.") rvalue_type = ( lvalue.type if isinstance(rvalue, int) and not isinstance(rvalue, bool) else None ) diff --git a/test/python/circuit/test_store.py b/test/python/circuit/test_store.py index 5f4d7de6702f..aaa590ff83b6 100644 --- a/test/python/circuit/test_store.py +++ b/test/python/circuit/test_store.py @@ -187,6 +187,13 @@ def test_implicitly_casts_const_scalars(self): Store(a, expr.Cast(expr.Value(1, types.Uint(8, const=True)), a.type, implicit=True)), ) + def test_rejects_const_target(self): + qc = QuantumCircuit() + with self.assertRaisesRegex(CircuitError, "const.*not supported"): + qc.store(expr.Var.new("a", types.Bool(const=True)), True) + with self.assertRaisesRegex(CircuitError, "const.*not supported"): + qc.store(expr.Var.new("a", types.Bool(const=True)), 1) + def test_does_not_widen_bool_literal(self): # `bool` is a subclass of `int` in Python (except some arithmetic operations have different # semantics...). It's not in Qiskit's value type system, though.