diff --git a/crates/circuit/src/operations.rs b/crates/circuit/src/operations.rs index e133aaff02bb..bd668d490948 100644 --- a/crates/circuit/src/operations.rs +++ b/crates/circuit/src/operations.rs @@ -297,13 +297,14 @@ pub enum DelayUnit { MS, S, DT, + EXPR, } unsafe impl ::bytemuck::CheckedBitPattern for DelayUnit { type Bits = u8; fn is_valid_bit_pattern(bits: &Self::Bits) -> bool { - *bits < 6 + *bits < 7 } } unsafe impl ::bytemuck::NoUninit for DelayUnit {} @@ -320,6 +321,7 @@ impl fmt::Display for DelayUnit { DelayUnit::MS => "ms", DelayUnit::S => "s", DelayUnit::DT => "dt", + DelayUnit::EXPR => "expr", } ) } @@ -335,6 +337,7 @@ impl<'py> FromPyObject<'py> for DelayUnit { "ms" => DelayUnit::MS, "s" => DelayUnit::S, "dt" => DelayUnit::DT, + "expr" => DelayUnit::EXPR, unknown_unit => { return Err(PyValueError::new_err(format!( "Unit '{}' is invalid.", diff --git a/qiskit/circuit/controlflow/builder.py b/qiskit/circuit/controlflow/builder.py index e9f3174ff6fc..73c3d283edf2 100644 --- a/qiskit/circuit/controlflow/builder.py +++ b/qiskit/circuit/controlflow/builder.py @@ -540,11 +540,15 @@ def remove_stretch(self, stretch: expr.Stretch): self._stretches_local.pop(stretch.name) def get_var(self, name: str): + if name in self._stretches_local: + return None if (out := self._vars_local.get(name)) is not None: return out return self._parent.get_var(name) def get_stretch(self, name: str): + if name in self._vars_local: + return None if (out := self._stretches_local.get(name)) is not None: return out return self._parent.get_stretch(name) diff --git a/qiskit/circuit/delay.py b/qiskit/circuit/delay.py index 32be0f0537c0..63b356da8f84 100644 --- a/qiskit/circuit/delay.py +++ b/qiskit/circuit/delay.py @@ -14,6 +14,8 @@ Delay instruction (for circuit module). """ import numpy as np + +from qiskit.circuit.classical import expr, types from qiskit.circuit.exceptions import CircuitError from qiskit.circuit.instruction import Instruction from qiskit.circuit.gate import Gate @@ -28,13 +30,35 @@ class Delay(Instruction): _standard_instruction_type = StandardInstructionType.Delay - def __init__(self, duration, unit="dt"): + def __init__(self, duration, unit=None): """ Args: - duration: the length of time of the duration. Given in units of ``unit``. - unit: the unit of the duration. Must be ``"dt"`` or an SI-prefixed seconds unit. + duration: the length of time of the duration. If this is an + :class:`~.expr.Expr`, it must be a constant expression of type + :class:`~.types.Duration` and the ``unit`` parameter should be + omitted (or MUST be "expr" if it is specified). + unit: the unit of the duration, if ``duration`` is a numeric + value. Must be ``"dt"``, an SI-prefixed seconds unit, or "expr". + + Raises: + CircuitError: A ``duration`` expression was specified with a resolved + type that is not timing-based, or the ``unit`` was improperly specified. """ - if unit not in {"s", "ms", "us", "ns", "ps", "dt"}: + if isinstance(duration, expr.Expr): + if unit is not None and unit != "expr": + raise CircuitError( + "Argument 'unit' must not be specified for a duration expression." + ) + if duration.type.kind is not types.Duration: + raise CircuitError( + f"Expression of type '{duration.type}' is not valid for 'duration'." + ) + if not duration.const: + raise CircuitError("Duration expressions must be constant.") + unit = "expr" + elif unit is None: + unit = "dt" + elif unit not in {"s", "ms", "us", "ns", "ps", "dt"}: raise CircuitError(f"Unknown unit {unit} is specified.") # Double underscore to differentiate from the private attribute in # `Instruction`. This can be changed to `_unit` in 2.0 after we @@ -88,8 +112,9 @@ def __repr__(self): """Return the official string representing the delay.""" return f"{self.__class__.__name__}(duration={self.params[0]}[unit={self.unit}])" + # pylint: disable=too-many-return-statements def validate_parameter(self, parameter): - """Delay parameter (i.e. duration) must be int, float or ParameterExpression.""" + """Delay parameter (i.e. duration) must be Expr, int, float or ParameterExpression.""" if isinstance(parameter, int): if parameter < 0: raise CircuitError( @@ -107,6 +132,12 @@ def validate_parameter(self, parameter): raise CircuitError("Integer duration is expected for 'dt' unit.") return parameter_int return parameter + elif isinstance(parameter, expr.Expr): + if parameter.type.kind is not types.Duration: + raise CircuitError(f"Expression duration of type '{parameter.type}' is not valid.") + if not parameter.const: + raise CircuitError("Duration expressions must be constant.") + return parameter elif isinstance(parameter, ParameterExpression): if len(parameter.parameters) > 0: return parameter # expression has free parameters, we cannot validate it diff --git a/qiskit/circuit/quantumcircuit.py b/qiskit/circuit/quantumcircuit.py index 6fcb831f2517..085e58a3db0b 100644 --- a/qiskit/circuit/quantumcircuit.py +++ b/qiskit/circuit/quantumcircuit.py @@ -4555,17 +4555,20 @@ def barrier(self, *qargs: QubitSpecifier, label=None) -> InstructionSet: def delay( self, - duration: ParameterValueType, + duration: Union[ParameterValueType, expr.Expr], qarg: QubitSpecifier | None = None, - unit: str = "dt", + unit: str | None = None, ) -> InstructionSet: """Apply :class:`~.circuit.Delay`. If qarg is ``None``, applies to all qubits. When applying to multiple qubits, delays with the same duration will be created. Args: - duration (int or float or ParameterExpression): duration of the delay. + duration (Object): + duration of the delay. If this is an :class:`~.expr.Expr`, it must be + a constant expression of type :class:`~.types.Duration`. qarg (Object): qubit argument to apply this delay. - unit (str): unit of the duration. Supported units: ``'s'``, ``'ms'``, ``'us'``, + unit (str | None): unit of the duration, unless ``duration`` is an :class:`~.expr.Expr` + in which case it must not be specified. Supported units: ``'s'``, ``'ms'``, ``'us'``, ``'ns'``, ``'ps'``, and ``'dt'``. Default is ``'dt'``, i.e. integer time unit depending on the target backend. diff --git a/qiskit/qasm3/exporter.py b/qiskit/qasm3/exporter.py index 4969b7c2c898..692679d87a35 100644 --- a/qiskit/qasm3/exporter.py +++ b/qiskit/qasm3/exporter.py @@ -1151,6 +1151,8 @@ def build_duration(self, duration, unit) -> ast.Expression | None: """Build the expression of a given duration (if not ``None``).""" if duration is None: return None + if unit == "expr": + return self.build_expression(duration) if unit == "ps": return ast.DurationLiteral(1000 * duration, ast.DurationUnit.NANOSECOND) unit_map = { diff --git a/qiskit/transpiler/passes/scheduling/padding/base_padding.py b/qiskit/transpiler/passes/scheduling/padding/base_padding.py index cdb3478f72bc..ad8b5079b0af 100644 --- a/qiskit/transpiler/passes/scheduling/padding/base_padding.py +++ b/qiskit/transpiler/passes/scheduling/padding/base_padding.py @@ -210,6 +210,9 @@ def _pre_runhook(self, dag: DAGCircuit): f"The input circuit {dag.name} is not scheduled. Call one of scheduling passes " f"before running the {self.__class__.__name__} pass." ) + if self.property_set["time_unit"] == "stretch": + # This should have already been raised during scheduling, but just in case. + raise TranspilerError("Scheduling cannot run on circuits with stretch durations.") for qarg, _ in enumerate(dag.qubits): if not self.__delay_supported(qarg): logger.debug( diff --git a/qiskit/transpiler/passes/scheduling/scheduling/alap.py b/qiskit/transpiler/passes/scheduling/scheduling/alap.py index 212df5688e64..6aea0b870c68 100644 --- a/qiskit/transpiler/passes/scheduling/scheduling/alap.py +++ b/qiskit/transpiler/passes/scheduling/scheduling/alap.py @@ -39,6 +39,8 @@ def run(self, dag): """ if len(dag.qregs) != 1 or dag.qregs.get("q", None) is None: raise TranspilerError("ALAP schedule runs on physical circuits only") + if self.property_set["time_unit"] == "stretch": + raise TranspilerError("Scheduling cannot run on circuits with stretch durations.") clbit_write_latency = self.property_set.get("clbit_write_latency", 0) diff --git a/qiskit/transpiler/passes/scheduling/scheduling/asap.py b/qiskit/transpiler/passes/scheduling/scheduling/asap.py index a9de97f52fd9..c3378b2bedd9 100644 --- a/qiskit/transpiler/passes/scheduling/scheduling/asap.py +++ b/qiskit/transpiler/passes/scheduling/scheduling/asap.py @@ -39,6 +39,8 @@ def run(self, dag): """ if len(dag.qregs) != 1 or dag.qregs.get("q", None) is None: raise TranspilerError("ASAP schedule runs on physical circuits only") + if self.property_set["time_unit"] == "stretch": + raise TranspilerError("Scheduling cannot run on circuits with stretch durations.") clbit_write_latency = self.property_set.get("clbit_write_latency", 0) diff --git a/qiskit/transpiler/passes/scheduling/time_unit_conversion.py b/qiskit/transpiler/passes/scheduling/time_unit_conversion.py index 8332cbf04bc4..5db09dc8e9a2 100644 --- a/qiskit/transpiler/passes/scheduling/time_unit_conversion.py +++ b/qiskit/transpiler/passes/scheduling/time_unit_conversion.py @@ -13,12 +13,15 @@ """Unify time unit in circuit for scheduling and following passes.""" from typing import Set -from qiskit.circuit import Delay +from qiskit.circuit import Delay, Duration +from qiskit.circuit.classical import expr +from qiskit.circuit.duration import duration_in_dt from qiskit.dagcircuit import DAGCircuit from qiskit.transpiler.basepasses import TransformationPass from qiskit.transpiler.exceptions import TranspilerError from qiskit.transpiler.instruction_durations import InstructionDurations from qiskit.transpiler.target import Target +from qiskit.utils import apply_prefix class TimeUnitConversion(TransformationPass): @@ -71,52 +74,83 @@ def run(self, dag: DAGCircuit): if self._durations_provided: inst_durations.update(self.inst_durations, getattr(self.inst_durations, "dt", None)) + # The float-value converted units for delay expressions, either all in 'dt' + # or all in seconds. + expression_durations = {} + # Choose unit - if inst_durations.dt is not None: - time_unit = "dt" - else: - # Check what units are used in delays and other instructions: dt or SI or mixed - units_delay = self._units_used_in_delays(dag) - if self._unified(units_delay) == "mixed": + has_dt = False + has_si = False + + # We _always_ need to traverse duration expressions to convert them to + # a float. But we also use the opportunity to note if they intermix cycles + # and wall-time, in case we don't have a `dt` to use to unify all instruction + # durations. + for node in dag.op_nodes(op=Delay): + if isinstance(node.op.duration, expr.Expr): + if any( + isinstance(x, expr.Stretch) for x in expr.iter_identifiers(node.op.duration) + ): + # If any of the delays use a stretch expression, we can't run scheduling + # passes anyway, so we bail out. In theory, we _could_ still traverse + # through the stretch expression and replace any Duration value nodes it may + # contain with ones of the same units, but it'd be complex and probably unuseful. + self.property_set["time_unit"] = "stretch" + return dag + + visitor = _EvalDurationImpl(inst_durations.dt) + duration = node.op.duration.accept(visitor) + if visitor.in_cycles(): + has_dt = True + # We need to round in case the expression evaluated to a non-integral 'dt'. + duration = duration_in_dt(duration, 1.0) + else: + has_si = True + if duration < 0: + raise TranspilerError( + f"Expression '{node.op.duration}' resolves to a negative duration!" + ) + expression_durations[node._node_id] = duration + else: + if node.op.unit == "dt": + has_dt = True + else: + has_si = True + if inst_durations.dt is None and has_dt and has_si: raise TranspilerError( "Fail to unify time units in delays. SI units " "and dt unit must not be mixed when dt is not supplied." ) - units_other = inst_durations.units_used() - if self._unified(units_other) == "mixed": - raise TranspilerError( - "Fail to unify time units in instruction_durations. SI units " - "and dt unit must not be mixed when dt is not supplied." - ) - unified_unit = self._unified(units_delay | units_other) - if unified_unit == "SI": + if inst_durations.dt is None: + # Check what units are used in other instructions: dt or SI or mixed + units_other = inst_durations.units_used() + unified_unit = self._unified(units_other) + if unified_unit == "SI" and not has_dt: time_unit = "s" - elif unified_unit == "dt": + elif unified_unit == "dt" and not has_si: time_unit = "dt" else: raise TranspilerError( "Fail to unify time units. SI units " "and dt unit must not be mixed when dt is not supplied." ) + else: + time_unit = "dt" # Make instructions with local durations consistent. for node in dag.op_nodes(Delay): op = node.op.to_mutable() - op.duration = inst_durations._convert_unit(op.duration, op.unit, time_unit) + if node._node_id in expression_durations: + op.duration = expression_durations[node._node_id] + else: + op.duration = inst_durations._convert_unit(op.duration, op.unit, time_unit) op.unit = time_unit dag.substitute_node(node, op) self.property_set["time_unit"] = time_unit return dag - @staticmethod - def _units_used_in_delays(dag: DAGCircuit) -> Set[str]: - units_used = set() - for node in dag.op_nodes(op=Delay): - units_used.add(node.op.unit) - return units_used - @staticmethod def _unified(unit_set: Set[str]) -> str: if not unit_set: @@ -135,3 +169,65 @@ def _unified(unit_set: Set[str]) -> str: return "SI" return "mixed" + + +class _EvalDurationImpl(expr.ExprVisitor[float]): + """Evaluates the expression to a single float result. + + If ``dt`` is provided or all durations are already in ``dt``, the result is in ``dt``. + Otherwise, the result will be in seconds, and all durations MUST be in wall-time (SI). + """ + + __slots__ = ("dt", "has_dt", "has_si") + + def __init__(self, dt=None): + self.dt = dt + self.has_dt = False + self.has_si = False + + def in_cycles(self): + """Returns ``True`` if units are 'dt' after visit.""" + return self.has_dt or self.dt is not None + + def visit_value(self, node, /) -> float: + if isinstance(node.value, float): + return node.value + if isinstance(node.value, Duration.dt): + if self.has_si and self.dt is None: + raise TranspilerError( + "Fail to unify time units in delays. SI units " + "and dt unit must not be mixed when dt is not supplied." + ) + self.has_dt = True + return node.value[0] + if isinstance(node.value, Duration): + if self.has_dt and self.dt is None: + raise TranspilerError( + "Fail to unify time units in delays. SI units " + "and dt unit must not be mixed when dt is not supplied." + ) + self.has_si = True + # Setting 'divisor' to 1 when there's no 'dt' is just to simplify + # the logic (we don't need to divide). + divisor = self.dt if self.dt is not None else 1 + if isinstance(node.value, Duration.s): + return node.value[0] / divisor + from_unit = node.value.unit() + return apply_prefix(node.value[0], from_unit) / divisor + raise TranspilerError(f"invalid duration expression: {node}") + + def visit_binary(self, node, /) -> float: + left = node.left.accept(self) + right = node.right.accept(self) + if node.op == expr.Binary.Op.ADD: + return left + right + if node.op == expr.Binary.Op.SUB: + return left - right + if node.op == expr.Binary.Op.MUL: + return left * right + if node.op == expr.Binary.Op.DIV: + return left / right + raise TranspilerError(f"invalid duration expression: {node}") + + def visit_cast(self, node, /) -> float: + return node.operand.accept(self) diff --git a/test/python/circuit/test_control_flow_builders.py b/test/python/circuit/test_control_flow_builders.py index 4794aeb20af3..11f8ed54ef3a 100644 --- a/test/python/circuit/test_control_flow_builders.py +++ b/test/python/circuit/test_control_flow_builders.py @@ -10,7 +10,7 @@ # copyright notice, and modified files need to carry a notice indicating # that they have been altered from the originals. -# pylint: disable=missing-function-docstring +# pylint: disable=missing-function-docstring,invalid-name """Test operations on the builder interfaces for control flow in dynamic QuantumCircuits.""" @@ -2751,46 +2751,71 @@ def test_can_capture_input(self): def test_can_capture_declared(self): a = expr.Var.new("a", types.Bool()) b = expr.Var.new("b", types.Bool()) - base = QuantumCircuit(declarations=[(a, expr.lift(False)), (b, expr.lift(True))]) + c = expr.Stretch.new("c") + base = QuantumCircuit(1, declarations=[(a, expr.lift(False)), (b, expr.lift(True))]) + base.add_stretch(c) with base.if_test(expr.lift(False)): base.store(a, expr.lift(True)) + base.delay(c) self.assertEqual(set(base.data[-1].operation.blocks[0].iter_captured_vars()), {a}) + self.assertEqual(set(base.data[-1].operation.blocks[0].iter_captured_stretches()), {c}) def test_can_capture_capture(self): # It's a bit wild to be manually building an outer circuit that's intended to be a subblock, # but be using the control-flow builder interface internally, but eh, it should work. a = expr.Var.new("a", types.Bool()) b = expr.Var.new("b", types.Bool()) - base = QuantumCircuit(captures=[a, b]) + c = expr.Stretch.new("c") + d = expr.Stretch.new("d") + base = QuantumCircuit(1, captures=[a, b, c, d]) with base.while_loop(expr.lift(False)): base.store(a, expr.lift(True)) + base.delay(c) + self.assertEqual(set(base.data[-1].operation.blocks[0].iter_captured_vars()), {a}) + self.assertEqual(set(base.data[-1].operation.blocks[0].iter_captured_stretches()), {c}) def test_can_capture_from_nested(self): a = expr.Var.new("a", types.Bool()) b = expr.Var.new("b", types.Bool()) c = expr.Var.new("c", types.Bool()) - base = QuantumCircuit(inputs=[a, b]) + d = expr.Stretch.new("d") + e = expr.Stretch.new("e") + f = expr.Stretch.new("f") + base = QuantumCircuit(1, inputs=[a, b]) + base.add_stretch(d) + base.add_stretch(e) with base.switch(expr.lift(False)) as case, case(case.DEFAULT): base.add_var(c, expr.lift(False)) + base.add_stretch(f) with base.if_test(expr.lift(False)): base.store(a, c) + base.delay(expr.add(d, f)) outer_block = base.data[-1].operation.blocks[0] inner_block = outer_block.data[-1].operation.blocks[0] self.assertEqual(set(inner_block.iter_captured_vars()), {a, c}) + self.assertEqual(set(inner_block.iter_captured_stretches()), {d, f}) # The containing block should have captured it as well, despite not using it explicitly. self.assertEqual(set(outer_block.iter_captured_vars()), {a}) self.assertEqual(set(outer_block.iter_declared_vars()), {c}) + self.assertEqual(set(outer_block.iter_captured_stretches()), {d}) + self.assertEqual(set(outer_block.iter_declared_stretches()), {f}) def test_can_manually_capture(self): a = expr.Var.new("a", types.Bool()) b = expr.Var.new("b", types.Bool()) + c = expr.Stretch.new("c") + d = expr.Stretch.new("d") base = QuantumCircuit(inputs=[a, b]) + base.add_stretch(c) + base.add_stretch(d) with base.while_loop(expr.lift(False)): # Why do this? Who knows, but it clearly has a well-defined meaning. base.add_capture(a) + base.add_capture(c) self.assertEqual(set(base.data[-1].operation.blocks[0].iter_captured_vars()), {a}) + self.assertEqual(set(base.data[-1].operation.blocks[0].iter_captured_stretches()), {c}) def test_later_blocks_do_not_inherit_captures(self): """Neither 'if' nor 'switch' should have later blocks inherit the captures from the earlier @@ -2798,50 +2823,78 @@ def test_later_blocks_do_not_inherit_captures(self): a = expr.Var.new("a", types.Bool()) b = expr.Var.new("b", types.Bool()) c = expr.Var.new("c", types.Bool()) - - base = QuantumCircuit(inputs=[a, b, c]) + d = expr.Stretch.new("d") + e = expr.Stretch.new("e") + f = expr.Stretch.new("f") + + base = QuantumCircuit(1, inputs=[a, b, c]) + base.add_stretch(d) + base.add_stretch(e) + base.add_stretch(f) with base.if_test(expr.lift(False)) as else_: base.store(a, expr.lift(False)) + base.delay(d) with else_: base.store(b, expr.lift(False)) + base.delay(e) blocks = base.data[-1].operation.blocks self.assertEqual(set(blocks[0].iter_captured_vars()), {a}) + self.assertEqual(set(blocks[0].iter_captured_stretches()), {d}) self.assertEqual(set(blocks[1].iter_captured_vars()), {b}) + self.assertEqual(set(blocks[1].iter_captured_stretches()), {e}) - base = QuantumCircuit(inputs=[a, b, c]) + base = QuantumCircuit(1, inputs=[a, b, c]) + base.add_stretch(d) + base.add_stretch(e) + base.add_stretch(f) with base.switch(expr.lift(False)) as case: with case(0): base.store(a, expr.lift(False)) + base.delay(d) with case(case.DEFAULT): base.store(b, expr.lift(False)) + base.delay(e) blocks = base.data[-1].operation.blocks self.assertEqual(set(blocks[0].iter_captured_vars()), {a}) + self.assertEqual(set(blocks[0].iter_captured_stretches()), {d}) self.assertEqual(set(blocks[1].iter_captured_vars()), {b}) + self.assertEqual(set(blocks[1].iter_captured_stretches()), {e}) def test_blocks_have_independent_declarations(self): """The blocks of if and switch should be separate scopes for declarations.""" b1 = expr.Var.new("b", types.Bool()) b2 = expr.Var.new("b", types.Bool()) + c1 = expr.Stretch.new("c") + c2 = expr.Stretch.new("c") self.assertNotEqual(b1, b2) + self.assertNotEqual(c1, c2) base = QuantumCircuit() with base.if_test(expr.lift(False)) as else_: base.add_var(b1, expr.lift(False)) + base.add_stretch(c1) with else_: base.add_var(b2, expr.lift(False)) + base.add_stretch(c2) blocks = base.data[-1].operation.blocks self.assertEqual(set(blocks[0].iter_declared_vars()), {b1}) + self.assertEqual(set(blocks[0].iter_declared_stretches()), {c1}) self.assertEqual(set(blocks[1].iter_declared_vars()), {b2}) + self.assertEqual(set(blocks[1].iter_declared_stretches()), {c2}) base = QuantumCircuit() with base.switch(expr.lift(False)) as case: with case(0): base.add_var(b1, expr.lift(False)) + base.add_stretch(c1) with case(case.DEFAULT): base.add_var(b2, expr.lift(False)) + base.add_stretch(c2) blocks = base.data[-1].operation.blocks self.assertEqual(set(blocks[0].iter_declared_vars()), {b1}) + self.assertEqual(set(blocks[0].iter_declared_stretches()), {c1}) self.assertEqual(set(blocks[1].iter_declared_vars()), {b2}) + self.assertEqual(set(blocks[1].iter_declared_stretches()), {c2}) def test_can_shadow_outer_name(self): outer = expr.Var.new("a", types.Bool()) @@ -2853,57 +2906,124 @@ def test_can_shadow_outer_name(self): self.assertEqual(set(block.iter_declared_vars()), {inner}) self.assertEqual(set(block.iter_captured_vars()), set()) + def test_can_shadow_outer_name_stretch(self): + outer = expr.Stretch.new("a") + inner = expr.Stretch.new("a") + base = QuantumCircuit(captures=[outer]) + with base.if_test(expr.lift(False)): + base.add_stretch(inner) + block = base.data[-1].operation.blocks[0] + self.assertEqual(set(block.iter_declared_stretches()), {inner}) + self.assertEqual(set(block.iter_captured_stretches()), set()) + + def test_var_can_shadow_outer_stretch(self): + outer = expr.Stretch.new("a") + inner = expr.Var.new("a", types.Bool()) + base = QuantumCircuit(captures=[outer]) + with base.if_test(expr.lift(False)): + base.add_var(inner, expr.lift(True)) + block = base.data[-1].operation.blocks[0] + self.assertEqual(set(block.iter_declared_vars()), {inner}) + self.assertEqual(set(block.iter_captured_stretches()), set()) + + def test_stretch_can_shadow_outer_var(self): + outer = expr.Var.new("a", types.Bool()) + inner = expr.Stretch.new("a") + base = QuantumCircuit(captures=[outer]) + with base.if_test(expr.lift(False)): + base.add_stretch(inner) + block = base.data[-1].operation.blocks[0] + self.assertEqual(set(block.iter_declared_stretches()), {inner}) + self.assertEqual(set(block.iter_captured_vars()), set()) + def test_iterators_run_over_scope(self): a = expr.Var.new("a", types.Bool()) b = expr.Var.new("b", types.Bool()) c = expr.Var.new("c", types.Bool()) d = expr.Var.new("d", types.Bool()) - - base = QuantumCircuit(inputs=[a, b, c]) + e = expr.Stretch.new("e") + f = expr.Stretch.new("f") + g = expr.Stretch.new("g") + h = expr.Stretch.new("h") + + base = QuantumCircuit(1, inputs=[a, b, c]) + base.add_stretch(e) + base.add_stretch(f) + base.add_stretch(g) self.assertEqual(set(base.iter_input_vars()), {a, b, c}) self.assertEqual(set(base.iter_declared_vars()), set()) self.assertEqual(set(base.iter_captured_vars()), set()) + self.assertEqual(set(base.iter_declared_stretches()), {e, f, g}) + self.assertEqual(set(base.iter_captured_stretches()), set()) with base.switch(expr.lift(3)) as case: with case(0): # Nothing here. self.assertEqual(set(base.iter_vars()), set()) + self.assertEqual(set(base.iter_captures()), set()) self.assertEqual(set(base.iter_input_vars()), set()) self.assertEqual(set(base.iter_declared_vars()), set()) self.assertEqual(set(base.iter_captured_vars()), set()) + self.assertEqual(set(base.iter_stretches()), set()) + self.assertEqual(set(base.iter_declared_stretches()), set()) + self.assertEqual(set(base.iter_captured_stretches()), set()) # Capture a variable. base.store(a, expr.lift(False)) self.assertEqual(set(base.iter_captured_vars()), {a}) + # Capture a stretch. + base.delay(e) + self.assertEqual(set(base.iter_captured_stretches()), {e}) + # Declare a variable. base.add_var(d, expr.lift(False)) self.assertEqual(set(base.iter_declared_vars()), {d}) self.assertEqual(set(base.iter_vars()), {a, d}) + # Declare a stretch. + base.add_stretch(h) + self.assertEqual(set(base.iter_declared_stretches()), {h}) + self.assertEqual(set(base.iter_stretches()), {e, h}) + with case(1): # We should have reset. self.assertEqual(set(base.iter_vars()), set()) + self.assertEqual(set(base.iter_captures()), set()) self.assertEqual(set(base.iter_input_vars()), set()) self.assertEqual(set(base.iter_declared_vars()), set()) self.assertEqual(set(base.iter_captured_vars()), set()) + self.assertEqual(set(base.iter_stretches()), set()) + self.assertEqual(set(base.iter_declared_stretches()), set()) + self.assertEqual(set(base.iter_captured_stretches()), set()) # Capture a variable. base.store(b, expr.lift(False)) self.assertEqual(set(base.iter_captured_vars()), {b}) + # Capture a stretch. + base.delay(f) + self.assertEqual(set(base.iter_captured_stretches()), {f}) + # Capture some more in another scope. with base.while_loop(expr.lift(False)): self.assertEqual(set(base.iter_vars()), set()) + self.assertEqual(set(base.iter_stretches()), set()) base.store(c, expr.lift(False)) + base.delay(g) self.assertEqual(set(base.iter_captured_vars()), {c}) + self.assertEqual(set(base.iter_captured_stretches()), {g}) self.assertEqual(set(base.iter_captured_vars()), {b, c}) + self.assertEqual(set(base.iter_captured_stretches()), {f, g}) self.assertEqual(set(base.iter_vars()), {b, c}) + self.assertEqual(set(base.iter_stretches()), {f, g}) # And back to the outer scope. self.assertEqual(set(base.iter_input_vars()), {a, b, c}) + self.assertEqual(set(base.iter_declared_stretches()), {e, f, g}) self.assertEqual(set(base.iter_declared_vars()), set()) self.assertEqual(set(base.iter_captured_vars()), set()) + self.assertEqual(set(base.iter_captured_stretches()), set()) def test_get_var_respects_scope(self): outer = expr.Var.new("a", types.Bool()) @@ -2923,8 +3043,44 @@ def test_get_var_respects_scope(self): # ... until we shadow it. base.add_var(inner, expr.lift(False)) self.assertEqual(base.get_var("a"), inner) + with base.if_test(expr.lift(False)): + # New scope, so again we see the outer one. + self.assertEqual(base.get_var("a"), outer) + + # Now make sure shadowing the var with a stretch works. + s = base.add_stretch("a") + self.assertEqual(base.get_var("a", None), None) + self.assertEqual(base.get_stretch("a"), s) self.assertEqual(base.get_var("a"), outer) + def test_get_stretch_respects_scope(self): + outer = expr.Stretch.new("a") + inner = expr.Stretch.new("a") + base = QuantumCircuit(captures=[outer]) + self.assertEqual(base.get_stretch("a"), outer) + with base.if_test(expr.lift(False)) as else_: + # Before we've done anything, getting the stretch should get the outer one. + self.assertEqual(base.get_stretch("a"), outer) + + # If we shadow it, we should get the shadowed one after. + base.add_stretch(inner) + self.assertEqual(base.get_stretch("a"), inner) + with else_: + # In a new scope, we should see the outer one again. + self.assertEqual(base.get_stretch("a"), outer) + # ... until we shadow it. + base.add_stretch(inner) + self.assertEqual(base.get_stretch("a"), inner) + with base.if_test(expr.lift(False)): + # New scope, so again we see the outer one. + self.assertEqual(base.get_stretch("a"), outer) + + # Now make sure shadowing the stretch with a var works. + v = base.add_var("a", expr.lift(True)) + self.assertEqual(base.get_stretch("a", None), None) + self.assertEqual(base.get_var("a"), v) + self.assertEqual(base.get_stretch("a"), outer) + def test_has_var_respects_scope(self): outer = expr.Var.new("a", types.Bool()) inner = expr.Var.new("a", types.Bool()) @@ -2954,11 +3110,69 @@ def test_has_var_respects_scope(self): self.assertTrue(base.has_var("a")) self.assertFalse(base.has_var(outer)) self.assertTrue(base.has_var(inner)) + with base.if_test(expr.lift(False)): + # New scope, so again we see the outer one. + self.assertTrue(base.has_var("a")) + self.assertTrue(base.has_var(outer)) + self.assertFalse(base.has_var(inner)) + + # Now make sure shadowing the var with a stretch works. + s = base.add_stretch("a") + self.assertFalse(base.has_var("a")) + self.assertFalse(base.has_var(outer)) + self.assertFalse(base.has_var(inner)) + self.assertTrue(base.has_stretch(s)) self.assertTrue(base.has_var("a")) self.assertTrue(base.has_var(outer)) self.assertFalse(base.has_var(inner)) + def test_has_stretch_respects_scope(self): + outer = expr.Stretch.new("a") + inner = expr.Stretch.new("a") + base = QuantumCircuit(captures=[outer]) + self.assertEqual(base.get_stretch("a"), outer) + with base.if_test(expr.lift(False)) as else_: + self.assertFalse(base.has_stretch("b")) + + # Before we've done anything, we should see the outer one. + self.assertTrue(base.has_stretch("a")) + self.assertTrue(base.has_stretch(outer)) + self.assertFalse(base.has_stretch(inner)) + + # If we shadow it, we should see the shadowed one after. + base.add_stretch(inner) + self.assertTrue(base.has_stretch("a")) + self.assertFalse(base.has_stretch(outer)) + self.assertTrue(base.has_stretch(inner)) + with else_: + # In a new scope, we should see the outer one again. + self.assertTrue(base.has_stretch("a")) + self.assertTrue(base.has_stretch(outer)) + self.assertFalse(base.has_stretch(inner)) + + # ... until we shadow it. + base.add_stretch(inner) + self.assertTrue(base.has_stretch("a")) + self.assertFalse(base.has_stretch(outer)) + self.assertTrue(base.has_stretch(inner)) + with base.if_test(expr.lift(False)): + # New scope, so again we see the outer one. + self.assertTrue(base.has_stretch("a")) + self.assertTrue(base.has_stretch(outer)) + self.assertFalse(base.has_stretch(inner)) + + # Now make sure shadowing the stretch with a var works. + v = base.add_var("a", expr.lift(True)) + self.assertFalse(base.has_stretch("a")) + self.assertFalse(base.has_stretch(outer)) + self.assertFalse(base.has_stretch(inner)) + self.assertTrue(base.has_var(v)) + + self.assertTrue(base.has_stretch("a")) + self.assertTrue(base.has_stretch(outer)) + self.assertFalse(base.has_stretch(inner)) + def test_store_to_clbit_captures_bit(self): base = QuantumCircuit(1, 2) with base.if_test(expr.lift(False)): diff --git a/test/python/compiler/test_transpiler.py b/test/python/compiler/test_transpiler.py index 026c4761b9bb..6d66e911872b 100644 --- a/test/python/compiler/test_transpiler.py +++ b/test/python/compiler/test_transpiler.py @@ -14,6 +14,7 @@ import copy import io +import itertools import math import os import sys @@ -21,7 +22,7 @@ from unittest.mock import patch import numpy as np import rustworkx as rx -from ddt import data, ddt, unpack +from ddt import data, idata, ddt, unpack from qiskit import ( ClassicalRegister, @@ -1300,23 +1301,61 @@ def test_circuit_with_delay(self, optimization_level): qc.delay(500, 1) qc.cx(0, 1) - dt = 1e-9 - backend = GenericBackendV2( - 2, coupling_map=[[0, 1]], basis_gates=["cx", "h"], seed=42, dt=dt + target = Target(num_qubits=2, dt=1e-9) + target.add_instruction( + HGate(), {(i,): InstructionProperties(duration=200 * 1e-9) for i in range(2)} + ) + target.add_instruction( + CXGate(), + {(0, 1): InstructionProperties(duration=700 * 1e-9)}, + ) + target.add_instruction(Delay(Parameter("t")), {(i,): None for i in range(2)}) + out = transpile( + qc, + scheduling_method="alap", + target=target, + optimization_level=optimization_level, + seed_transpiler=42, + ) + + self.assertEqual(out.unit, "dt") + self.assertEqual(out.duration, 1200) + + @data(0, 1, 2, 3) + def test_circuit_with_delay_expr_duration(self, optimization_level): + """Verify a circuit with delay with a duration of type types.Duration + can transpile to a scheduled circuit.""" + + # This resolves to 500dt + delay_expr = expr.add( + expr.mul(expr.mul(Duration.dt(400), 2.0), expr.div(Duration.dt(200), Duration.dt(400))), + Duration.dt(100), + ) + + qc = QuantumCircuit(2) + qc.h(0) + qc.delay(delay_expr, 1) + qc.cx(0, 1) + + target = Target(num_qubits=2, dt=1e-9) + target.add_instruction( + HGate(), {(i,): InstructionProperties(duration=200 * 1e-9) for i in range(2)} + ) + target.add_instruction( + CXGate(), + {(0, 1): InstructionProperties(duration=700 * 1e-9)}, ) - # update durations - backend.target.update_instruction_properties("cx", (0, 1), InstructionProperties(700 * dt)) - backend.target.update_instruction_properties("h", (0,), InstructionProperties(200 * dt)) + target.add_instruction(Delay(Parameter("t")), {(i,): None for i in range(2)}) out = transpile( qc, scheduling_method="alap", - target=backend.target, - dt=1e-9, + target=target, optimization_level=optimization_level, seed_transpiler=42, ) + self.assertEqual(out.unit, "dt") self.assertEqual(out.duration, 1200) def test_delay_converts_to_dt(self): @@ -1333,6 +1372,204 @@ def test_delay_converts_to_dt(self): out = transpile(qc, dt=1e-9, seed_transpiler=42) self.assertEqual(out.data[0].operation.unit, "dt") + def test_delay_converts_expr_to_dt(self): + """Test that a delay instruction with a duration expression of type Duration + is converted to units of dt given a backend.""" + qc = QuantumCircuit(2) + qc.delay(expr.lift(Duration.us(1000)), [0]) + + backend = GenericBackendV2(num_qubits=4) + backend.target.dt = 0.5e-6 + out = transpile([qc, qc], backend, seed_transpiler=42) + self.assertEqual(out[0].data[0].operation.unit, "dt") + self.assertEqual(out[1].data[0].operation.unit, "dt") + + out = transpile(qc, dt=1e-9, seed_transpiler=42) + self.assertEqual(out.data[0].operation.unit, "dt") + + def test_delay_converts_expr_to_dt_with_rounding(self): + """Test that converting to 'dt' from wall-time correctly rounds to nearest + integer.""" + qc = QuantumCircuit(2) + qc.delay(expr.lift(Duration.ns(1234560)), [0]) + + backend = GenericBackendV2(num_qubits=4) + backend.target.dt = 5e-7 + + with self.assertWarnsRegex(UserWarning, "Duration is rounded"): + out = transpile(qc, backend, seed_transpiler=42) + + self.assertEqual(out.data[0].operation.unit, "dt") + self.assertEqual(type(out.data[0].operation.duration), int) + self.assertEqual(out.data[0].operation.duration, round(float(1234560) / 1e9 / 5e-7)) + + def test_delay_expr_evaluation_dt(self): + """Test that a delay instruction with a complex duration expression + of type Duration is evaluated to 'dt' properly.""" + # 500dt - 200dt = 300dt + delay_expr = expr.sub( + # 400dt + 100dt = 500dt + expr.add( + # 800dt * 0.5 = 400dt + expr.mul( + # 400dt * 2 = 800dt + expr.mul(Duration.s(0.0002), 2.0), + # 200dt / 400dt = 0.5 + expr.div(Duration.ms(0.1), Duration.us(200)), + ), + Duration.dt(100), + ), + Duration.ns(100_000), + ) + + qc = QuantumCircuit(2) + qc.delay(delay_expr, 1) + + backend = GenericBackendV2(num_qubits=2) + backend.target.dt = 5e-7 + out = transpile( + qc, + backend=backend, + seed_transpiler=42, + ) + + self.assertEqual(out.data[0].operation.unit, "dt") + self.assertTrue(math.isclose(out.data[0].operation.duration, 300, rel_tol=1e-07)) + + def test_delay_expr_evaluation_seconds(self): + """Test that a delay instruction with a complex duration expression + of type Duration is evaluated to seconds properly when the target 'dt' + is absent.""" + # .00025s - .0001s = .00015s + delay_expr = expr.sub( + # .0002s + .00005s = .00025s + expr.add( + # .0004s * 0.5 = .0002s + expr.mul( + # .0002s * 2 = .0004s + expr.mul(Duration.s(0.0002), 2.0), + # .0001s / .0002s = 0.5 + expr.div(Duration.ms(0.1), Duration.us(200)), + ), + Duration.s(0.00005), + ), + Duration.ns(100_000), + ) + + qc = QuantumCircuit(2) + qc.delay(delay_expr, 1) + + backend = GenericBackendV2(num_qubits=2) + backend.target.dt = None + out = transpile( + qc, + backend=backend, + seed_transpiler=42, + ) + + self.assertEqual(out.data[0].operation.unit, "s") + self.assertTrue(math.isclose(out.data[0].operation.duration, 0.00015, rel_tol=1e-07)) + + def test_delay_expr_evaluation_dt_without_target_dt(self): + """Test that a delay expression with only 'dt' is evaluated properly + even when the target doesn't specify a 'dt'.""" + delay_expr = expr.sub( + expr.add( + expr.mul( + expr.mul(Duration.dt(400), 2.0), + expr.div(Duration.dt(200), Duration.dt(400)), + ), + Duration.dt(100), + ), + Duration.dt(200), + ) + + qc = QuantumCircuit(2) + qc.delay(delay_expr, 1) + + target = Target(num_qubits=2, dt=None) + target.add_instruction(Delay(Parameter("t")), {(i,): None for i in range(2)}) + + out = transpile( + qc, + target=target, + seed_transpiler=42, + ) + + self.assertEqual(out.data[0].operation.unit, "dt") + self.assertTrue(math.isclose(out.data[0].operation.duration, 300, rel_tol=1e-07)) + + def test_rejects_negative_delay_expr(self): + """Test that a delay instruction with an expression duration is rejected + when the duration resolves to a negative number.""" + negative_delay = expr.sub(Duration.dt(100), Duration.dt(200)) + qc = QuantumCircuit(2) + qc.delay(negative_delay, 1) + + with self.assertRaisesRegex(TranspilerError, ".*negative duration"): + transpile( + qc, + backend=GenericBackendV2(num_qubits=2), + seed_transpiler=42, + ) + + def test_rejects_mixed_units_delay_expr_without_target_dt(self): + """Test that a delay instruction with wall time and cycles without target DT + is rejected.""" + mixed_delay = expr.sub(Duration.dt(100), Duration.s(200)) + qc = QuantumCircuit(2) + qc.delay(mixed_delay, 1) + + backend = GenericBackendV2(num_qubits=2) + backend.target.dt = None + with self.assertRaisesRegex(TranspilerError, ".*SI units and dt unit must not be mixed"): + transpile( + qc, + backend=backend, + seed_transpiler=42, + ) + + @data(0, 1, 2, 3) + def test_circuit_with_delay_expr_stretch(self, optimization_level): + """Verify a circuit with delay with a duration of type types.Duration + can pass through the transpiler without generating an error.""" + + qc = QuantumCircuit(2) + a = qc.add_stretch("a") + qc.h(0) + qc.delay(a, 1) + qc.cx(0, 1) + + out = transpile( + qc, + backend=GenericBackendV2(num_qubits=2, basis_gates=["cx", "h"]), + optimization_level=optimization_level, + seed_transpiler=42, + ) + + self.assertEqual(qc, out) + + @idata(itertools.product([0, 1, 2, 3], ["alap", "asap"])) + @unpack + def test_scheduling_with_delay_stretch_fails(self, optimization_level, scheduling_method): + """Scheduling should fail with an appropriate error message if it is attempted + on a circuit containing delays with stretch expressions. + """ + qc = QuantumCircuit(2) + a = qc.add_stretch("a") + qc.h(0) + qc.delay(a, 1) + qc.cx(0, 1) + + with self.assertRaisesRegex(TranspilerError, "Scheduling cannot run.*stretch"): + transpile( + qc, + backend=GenericBackendV2(num_qubits=2), + optimization_level=optimization_level, + scheduling_method=scheduling_method, + seed_transpiler=42, + ) + def test_scheduling_backend_v2(self): """Test that scheduling method works with Backendv2.""" qc = QuantumCircuit(2) @@ -2133,11 +2370,13 @@ def _standalone_var_circuit(self): a = expr.Var.new("a", types.Bool()) b = expr.Var.new("b", types.Uint(8)) c = expr.Var.new("c", types.Uint(8)) + d = expr.Stretch.new("d") qc = QuantumCircuit(5, 5, inputs=[a]) qc.add_var(b, 12) - qc.add_stretch("d") + qc.add_stretch(d) qc.h(0) + qc.delay(expr.add(Duration.dt(1000), d), 0) qc.cx(0, 1) qc.measure([0, 1], [0, 1]) qc.store(a, expr.bit_xor(qc.clbits[0], qc.clbits[1])) diff --git a/test/python/qasm3/test_export.py b/test/python/qasm3/test_export.py index 84a61dacd2a1..9e05b18664af 100644 --- a/test/python/qasm3/test_export.py +++ b/test/python/qasm3/test_export.py @@ -13,7 +13,7 @@ """Test QASM3 exporter.""" # We can't really help how long the lines output by the exporter are in some cases. -# pylint: disable=line-too-long +# pylint: disable=line-too-long,invalid-name from io import StringIO from math import pi @@ -22,7 +22,7 @@ from ddt import ddt, data from qiskit import QuantumRegister, ClassicalRegister, QuantumCircuit, transpile -from qiskit.circuit import Parameter, Qubit, Clbit, Gate, ParameterVector +from qiskit.circuit import Parameter, Qubit, Clbit, Duration, Gate, ParameterVector from qiskit.circuit.classical import expr, types from qiskit.circuit.controlflow import CASE_DEFAULT from qiskit.circuit.library import PauliEvolutionGate @@ -721,15 +721,25 @@ def test_delay_statement(self): """Test that delay operations get output into valid OpenQASM 3.""" qreg = QuantumRegister(2, "qr") qc = QuantumCircuit(qreg) + s = qc.add_stretch("s") + t = qc.add_stretch("t") qc.delay(100, qreg[0], unit="ms") + qc.delay(expr.lift(Duration.ms(100)), qreg[0]) qc.delay(2, qreg[1], unit="ps") # "ps" is not a valid unit in OQ3, so we need to convert. + qc.delay(expr.div(s, 2.0), qreg[1]) + qc.delay(expr.add(expr.mul(s, expr.div(Duration.dt(1000), Duration.ns(200))), t), qreg[0]) expected_qasm = "\n".join( [ "OPENQASM 3.0;", "qubit[2] qr;", + "stretch s;", + "stretch t;", "delay[100ms] qr[0];", + "delay[100.0ms] qr[0];", "delay[2000ns] qr[1];", + "delay[s / 2.0] qr[1];", + "delay[s * (1000dt / 200.0ns) + t] qr[0];", "", ] ) @@ -1843,6 +1853,44 @@ def test_var_use(self): b = b & 8; c = ~b; e = 7.5; +""" + self.assertEqual(dumps(qc), expected) + + def test_qasm_stretch_example_1(self): + """Test an example from the OpenQASM docs.""" + qc = QuantumCircuit(5) + qc.barrier() + qc.cx(0, 1) + qc.u(pi / 4, 0, pi / 2, 2) + qc.cx(3, 4) + + a = qc.add_stretch("a") + b = qc.add_stretch("b") + c = qc.add_stretch("c") + + # Use the stretches as Delay duration. + qc.delay(a, [0, 1]) + qc.delay(b, 2) + qc.delay(c, [3, 4]) + qc.barrier() + + expected = """\ +OPENQASM 3.0; +include "stdgates.inc"; +qubit[5] q; +stretch a; +stretch b; +stretch c; +barrier q[0], q[1], q[2], q[3], q[4]; +cx q[0], q[1]; +U(pi/4, 0, pi/2) q[2]; +cx q[3], q[4]; +delay[a] q[0]; +delay[a] q[1]; +delay[b] q[2]; +delay[c] q[3]; +delay[c] q[4]; +barrier q[0], q[1], q[2], q[3], q[4]; """ self.assertEqual(dumps(qc), expected)