From f82689f83f28c32a88af1fb6ead8777985408e6a Mon Sep 17 00:00:00 2001 From: Kevin Hartman Date: Thu, 6 Mar 2025 01:50:44 -0500 Subject: [PATCH] [Stretch] Add `stretch` variable support to `QuantumCircuit`. (#13852) * WIP * Add try_const to lift. * Try multiple singletons, new one for const. * Revert "Try multiple singletons, new one for const." This reverts commit e2b32212533a52b30fecb700ed36a7f9e93a19a6. * Remove Bool singleton test. * Add const handling for stores, fix test bugs. * Fix formatting. * Remove Duration and Stretch for now. * Cleanup, fix const bug in index. * Fix ordering issue for types with differing const-ness. Types that have some natural order no longer have an ordering when one of them is strictly greater but has an incompatible const-ness (i.e. when the greater type is const but the other type is not). * Fix QPY serialization. We need to reject types with const=True in QPY until it supports them. For now, I've also made the Index and shift operator constructors lift their RHS to the same const-ness as the target to make it less likely that existing users of expr run into issues when serializing to older QPY versions. * Make expr.Lift default to non-const. This is probably a better default in general, since we don't really have much use for const except for timing stuff. * Revert to old test_expr_constructors.py. * Make binary_logical lift independent again. Since we're going for using a Cast node when const-ness differs, this will be fine. * Update tests, handle a few edge cases. * Fix docstring. * Remove now redundant arg from tests. * Add const testing for ordering. * Add const tests for shifts. * Add release note. * Add const store tests. * Address lint, minor cleanup. * Add Float type to classical expressions. * Allow DANGEROUS conversion from Float to Bool. I wasn't going to have this, but since we have DANGEROUS Float => Int, and we have Int => Bool, I think this makes the most sense. * Test Float ordering. * Improve error messages for using Float with logical operators. * Float tests for constructors. * Add release note. * Add Duration and Stretch classical types. A Stretch can always represent a Duration (it's just an expression without any unresolved stretch variables, in this case), so we allow implicit conversion from Duration => Stretch. The reason for a separate Duration type is to support things like Duration / Duration => Float. This is not valid for stretches in OpenQASM (to my knowledge). * Add Duration type to qiskit.circuit. Also adds support to expr.lift to create a value expression of type types.Duration from an instance of qiskit.circuit.Duration. * Block Stretch from use in binary relations. * Add type ordering tests for Duration and Stretch. Also improves testing for other types. * Test expr constructors for Duration and Stretch. * Fix lint. * Implement operators +, -, *, /. * Add expression tests for arithmetic ops. * Implement stretch support for circuit. * Initial support for writing QASM3. * Reject const vars in add_var and add_input. Also removes the assumption that a const-type can never be an l-value in favor of just restricting l-values with const types from being added to circuits for now. We will (in a separate PR) add support for adding stretch variables to circuits, which are const. However, we may track those differently, or at least not report them as variable when users query the circuit for variables. * Implement QPY support for const-typed expressions. * Remove invalid test. This one I'd added thinking I ought to block store from using a const var target. But since I figured it's better to just restrict adding vars to the circuit that are const (and leave the decision of whether or not a const var can be an l-value till later), this test no longer makes sense. * Update QPY version 14 desc. * Fix lint. * Add serialization testing. * Test pre-v14 QPY rejects const-typed exprs. * QASM export for floats. * QPY support for floats. * Fix lint. * Settle on Duration circuit core type. * QPY serialization for durations and stretches. * Add QPY testing. I can't really test Stretch yet since we can't add them to circuits until a later PR. * QASM support for stretch and duration. The best I can do to test these right now (before Delay is made to accept timing expressions) is to use them in comparison operations (will be in a follow up commit). * Fix lint. * Add arithmetic operators to QASM. * QPY testing for arithmetic operations. * QASM testing for arithmetic operations. * Update tests for blocked const vars. We decided to punt on the idea of assigning const-typed variables, so we don't support declaring stretch expressions via add_var or the 'declarations' argument to QuantumCircuit. We also don't allow const 'inputs'. * Only test declare stretch QASM; fix lint. * Special case for stretch in add_uninitialized_var. At the moment, we track stretches as variables, but these will eventually make their way here during circuit copy etc., so we need to support them even though they're const. * Remove outdated docstring comment. * Remove outdated comment. * Don't use match since we still support Python 3.9. * Block const stores. * Fix enum match. * Revert visitors.py. * Address review comments. * Improve type docs. * Revert QPY, since the old format can support constexprs. By making const-ness a property of expressions, we don't need any special serialization in QPY. That's because we assume that all `Var` expressions are non-const, and all `Value` expressions are const. And the const-ness of any expression is defined by the const-ness of its operands, e.g. when QPY reconstructs a binary operand, the constructed expression's `const` attribute gets set to `True` if both of the operands are `const`, which ultimately flows bottom-up from the `Var` and `Value` leaf nodes. * Move const-ness from Type to Expr. * Revert QPY testing, no longer needed. * Add explicit validation of const expr. * Revert stuff I didn't need to touch. * Update release note. * A few finishing touches. * Fix-up after merge. * Fix-ups after merge. * Fix lint. * Fix comment and release note. * Fixes after merge. * Fix test. * Fix lint. * Special-case Var const-ness for Stretch type. This feels like a bit of a hack, but the idea is to override a Var to report itself as a constant expression only in the case that its type is Stretch. I would argue that it's not quite as hack-y as it appears, since Stretch is probably the only kind of constant we'll ever allow in Qiskit without an in-line initializer. If ever we want to support declaring other kinds of constants (e.g. Uint), we'll probably want to introduce a `expr.Const` type whose constructor requires a const initializer expression. * Address review comments. * Update release note. * Update docs. * Add release notes and doc link. * Address review comments. * Remove Stretch type. * Remove a few more mentions of the binned stretch type. * Add docstring for Duration. * Remove Stretch type stuff. * WIP * Track stretch variables throughout circuits. * Update QASM exporter. * Fix existing tests and found bugs. * Fix format. * Simplify structural eq visitor. * Add num_identifiers. * Support QPY. * Implement QASM visit for stretch expr. * Track stretches through DAGCircuit. * Add visit_stretch to classical resource map. * Fix lint. * Address review comments. * Remove unused import. * Represent stretch with StretchDeclaration in QASM AST. Previously, we used StretchType within a ClassicalDeclaration, but this wasn't the best fit. * Remove visitor short circuit. * Address review comments. * Address review comments. * Support division by Uint. * Add release note. * Fix lint. * Add missing DAG stretch plumbing. * Fix QPY serialization bug. * Fix lint. * Add qpy test. * Support remapping for stretch variables in compose. * Fix qpy compat test. * Add negative test for QPY stretch expr. * Add more circuit testing. * Add circuit equality testing for stretch. * Refer to 'stretches' instead of 'stretch variables'. * Remove | None for Stretch.name * Address review comments. * Update QPY desc. * Fix num_identifiers. * Fix merge for control flow tests. --- crates/circuit/src/converters.rs | 10 + crates/circuit/src/dag_circuit.rs | 231 +++++++++- qiskit/circuit/_classical_resource_map.py | 5 +- qiskit/circuit/classical/expr/__init__.py | 16 +- qiskit/circuit/classical/expr/expr.py | 71 +++ qiskit/circuit/classical/expr/visitors.py | 73 +++ qiskit/circuit/controlflow/_builder_utils.py | 4 +- qiskit/circuit/controlflow/builder.py | 122 ++++- qiskit/circuit/controlflow/control_flow.py | 10 + qiskit/circuit/quantumcircuit.py | 423 ++++++++++++++++-- qiskit/converters/dag_to_circuit.py | 4 +- qiskit/qasm3/ast.py | 17 +- qiskit/qasm3/exporter.py | 16 +- qiskit/qasm3/printer.py | 13 +- qiskit/qpy/__init__.py | 33 +- qiskit/qpy/binary_io/circuits.py | 15 +- qiskit/qpy/binary_io/value.py | 43 +- qiskit/qpy/formats.py | 4 + qiskit/qpy/type_keys.py | 3 + .../transpiler/passes/layout/apply_layout.py | 4 + .../transpiler/passes/layout/sabre_layout.py | 4 + .../contract_idle_wires_in_control_flow.py | 4 +- .../stretch-variables-076070c3f57cfa09.yaml | 43 ++ .../circuit/classical/test_expr_helpers.py | 3 +- .../circuit/test_circuit_load_from_qpy.py | 11 + .../python/circuit/test_circuit_operations.py | 39 +- test/python/circuit/test_circuit_vars.py | 166 ++++++- test/python/circuit/test_compose.py | 65 ++- test/python/circuit/test_control_flow.py | 83 +++- test/python/compiler/test_transpiler.py | 1 + test/python/qasm3/test_export.py | 2 + test/qpy_compat/test_qpy.py | 17 +- 32 files changed, 1440 insertions(+), 115 deletions(-) create mode 100644 releasenotes/notes/stretch-variables-076070c3f57cfa09.yaml diff --git a/crates/circuit/src/converters.rs b/crates/circuit/src/converters.rs index 2952bbac2acc..17d3e9d3cf89 100644 --- a/crates/circuit/src/converters.rs +++ b/crates/circuit/src/converters.rs @@ -32,6 +32,8 @@ pub struct QuantumCircuitData<'py> { pub input_vars: Vec>, pub captured_vars: Vec>, pub declared_vars: Vec>, + pub captured_stretches: Vec>, + pub declared_stretches: Vec>, } impl<'py> FromPyObject<'py> for QuantumCircuitData<'py> { @@ -63,6 +65,14 @@ impl<'py> FromPyObject<'py> for QuantumCircuitData<'py> { .call_method0(intern!(py, "iter_declared_vars"))? .try_iter()? .collect::>>()?, + captured_stretches: ob + .call_method0(intern!(py, "iter_captured_stretches"))? + .try_iter()? + .collect::>>()?, + declared_stretches: ob + .call_method0(intern!(py, "iter_declared_stretches"))? + .try_iter()? + .collect::>>()?, }) } } diff --git a/crates/circuit/src/dag_circuit.rs b/crates/circuit/src/dag_circuit.rs index 022b512eb4bf..e30093debcc9 100644 --- a/crates/circuit/src/dag_circuit.rs +++ b/crates/circuit/src/dag_circuit.rs @@ -237,6 +237,9 @@ pub struct DAGCircuit { control_flow_module: PyControlFlowModule, vars_info: HashMap, vars_by_type: [Py; 3], + + captured_stretches: IndexMap, RandomState>, + declared_stretches: IndexMap, RandomState>, } #[derive(Clone, Debug)] @@ -391,6 +394,8 @@ impl DAGCircuit { PySet::empty(py)?.unbind(), PySet::empty(py)?.unbind(), ], + captured_stretches: IndexMap::default(), + declared_stretches: IndexMap::default(), }) } @@ -1497,6 +1502,12 @@ impl DAGCircuit { { target_dag.add_var(py, &var, DAGVarType::Declare)?; } + for stretch in self.captured_stretches.values() { + target_dag.add_captured_stretch(py, stretch.bind(py))?; + } + for stretch in self.declared_stretches.values() { + target_dag.add_declared_stretch(stretch.bind(py))?; + } } else if vars_mode == "captures" { for var in self.vars_by_type[DAGVarType::Input as usize] .bind(py) @@ -1516,6 +1527,12 @@ impl DAGCircuit { { target_dag.add_var(py, &var, DAGVarType::Capture)?; } + for stretch in self.captured_stretches.values() { + target_dag.add_captured_stretch(py, stretch.bind(py))?; + } + for stretch in self.declared_stretches.values() { + target_dag.add_captured_stretch(py, stretch.bind(py))?; + } } else if vars_mode != "drop" { return Err(PyValueError::new_err(format!( "unknown vars_mode: '{}'", @@ -1813,9 +1830,9 @@ impl DAGCircuit { dag.add_input_var(py, &var?)?; } if inline_captures { - for var in other.iter_captured_vars(py)?.bind(py) { + for var in other.iter_captures(py)?.bind(py) { let var = var?; - if !dag.has_var(&var)? { + if !dag.has_identifier(&var)? { return Err(DAGCircuitError::new_err(format!("Variable '{}' to be inlined is not in the base DAG. If you wanted it to be automatically added, use `inline_captures=False`.", var))); } } @@ -1823,10 +1840,16 @@ impl DAGCircuit { for var in other.iter_captured_vars(py)?.bind(py) { dag.add_captured_var(py, &var?)?; } + for stretch in other.iter_captured_stretches(py)?.bind(py) { + dag.add_captured_stretch(py, &stretch?)?; + } } for var in other.iter_declared_vars(py)?.bind(py) { dag.add_declared_var(py, &var?)?; } + for var in other.iter_declared_stretches(py)?.bind(py) { + dag.add_declared_stretch(&var?)?; + } let variable_mapper = PyVariableMapper::new( py, @@ -2244,6 +2267,32 @@ impl DAGCircuit { } } + if self.captured_stretches.len() != other.captured_stretches.len() + || self.declared_stretches.len() != other.declared_stretches.len() + { + return Ok(false); + } + + for (our_stretch, their_stretch) in self + .captured_stretches + .values() + .zip(other.captured_stretches.values()) + { + if !our_stretch.bind(py).eq(their_stretch)? { + return Ok(false); + } + } + + for (our_stretch, their_stretch) in self + .declared_stretches + .values() + .zip(other.declared_stretches.values()) + { + if !our_stretch.bind(py).eq(their_stretch)? { + return Ok(false); + } + } + let self_bit_indices = { let indices = self .qubits @@ -4270,6 +4319,47 @@ impl DAGCircuit { Ok(()) } + /// Add a captured stretch to the circuit. + /// + /// Args: + /// var: the stretch to add. + fn add_captured_stretch(&mut self, py: Python, var: &Bound) -> PyResult<()> { + if !self.vars_by_type[DAGVarType::Input as usize] + .bind(py) + .is_empty() + { + return Err(DAGCircuitError::new_err( + "cannot add captures to a circuit with inputs", + )); + } + let var_name: String = var.getattr("name")?.extract::()?; + if self.vars_info.contains_key(&var_name) { + return Err(DAGCircuitError::new_err( + "cannot add stretch as its name shadows an existing identifier", + )); + } + if let Some(previous) = self.declared_stretches.get(&var_name) { + if var.eq(previous)? { + return Err(DAGCircuitError::new_err("already present in the circuit")); + } + return Err(DAGCircuitError::new_err( + "cannot add stretch as its name shadows an existing identifier", + )); + } + if let Some(previous) = self.captured_stretches.get(&var_name) { + if var.eq(previous)? { + return Err(DAGCircuitError::new_err("already present in the circuit")); + } + return Err(DAGCircuitError::new_err( + "cannot add stretch as its name shadows an existing identifier", + )); + } + + self.captured_stretches + .insert(var_name, var.clone().unbind()); + Ok(()) + } + /// Add a declared local variable to the circuit. /// /// Args: @@ -4279,6 +4369,39 @@ impl DAGCircuit { Ok(()) } + /// Add a declared stretch to the circuit. + /// + /// Args: + /// var: the stretch to add. + fn add_declared_stretch(&mut self, var: &Bound) -> PyResult<()> { + let var_name: String = var.getattr("name")?.extract::()?; + if self.vars_info.contains_key(&var_name) { + return Err(DAGCircuitError::new_err( + "cannot add stretch as its name shadows an existing identifier", + )); + } + if let Some(previous) = self.declared_stretches.get(&var_name) { + if var.eq(previous)? { + return Err(DAGCircuitError::new_err("already present in the circuit")); + } + return Err(DAGCircuitError::new_err( + "cannot add stretch as its name shadows an existing identifier", + )); + } + if let Some(previous) = self.captured_stretches.get(&var_name) { + if var.eq(previous)? { + return Err(DAGCircuitError::new_err("already present in the circuit")); + } + return Err(DAGCircuitError::new_err( + "cannot add stretch as its name shadows an existing identifier", + )); + } + + self.declared_stretches + .insert(var_name, var.clone().unbind()); + Ok(()) + } + /// Total number of classical variables tracked by the circuit. #[getter] fn num_vars(&self) -> usize { @@ -4325,6 +4448,36 @@ impl DAGCircuit { } } + /// Is this stretch in the DAG? + /// + /// Args: + /// var: the stretch or name to check. + fn has_stretch(&self, var: &Bound) -> PyResult { + match var.extract::() { + Ok(name) => Ok(self.declared_stretches.contains_key(&name) + || self.captured_stretches.contains_key(&name)), + Err(_) => { + let raw_name = var.getattr("name")?; + let var_name: String = raw_name.extract()?; + if let Some(stretch) = self.declared_stretches.get(&var_name) { + return Ok(stretch.is(var)); + } + if let Some(stretch) = self.captured_stretches.get(&var_name) { + return Ok(stretch.is(var)); + } + Ok(false) + } + } + } + + /// Is this identifier in the DAG? + /// + /// Args: + /// var: the identifier or name to check. + fn has_identifier(&self, var: &Bound) -> PyResult { + Ok(self.has_var(var)? || self.has_stretch(var)?) + } + /// Iterable over the input classical variables tracked by the circuit. fn iter_input_vars(&self, py: Python) -> PyResult> { Ok(self.vars_by_type[DAGVarType::Input as usize] @@ -4345,6 +4498,29 @@ impl DAGCircuit { .unbind()) } + /// Iterable over the captured stretches tracked by the circuit. + fn iter_captured_stretches(&self, py: Python) -> PyResult> { + Ok(PyList::new(py, self.captured_stretches.values())? + .into_any() + .try_iter()? + .unbind()) + } + + /// Iterable over all captured identifiers tracked by the circuit. + fn iter_captures(&self, py: Python) -> PyResult> { + let out_set = PySet::empty(py)?; + for var in self.vars_by_type[DAGVarType::Capture as usize] + .bind(py) + .iter() + { + out_set.add(var)?; + } + for stretch in self.captured_stretches.values() { + out_set.add(stretch)?; + } + Ok(out_set.into_any().try_iter()?.unbind()) + } + /// Iterable over the declared classical variables tracked by the circuit. fn iter_declared_vars(&self, py: Python) -> PyResult> { Ok(self.vars_by_type[DAGVarType::Declare as usize] @@ -4355,6 +4531,14 @@ impl DAGCircuit { .unbind()) } + /// Iterable over the declared stretches tracked by the circuit. + fn iter_declared_stretches(&self, py: Python) -> PyResult> { + Ok(PyList::new(py, self.declared_stretches.values())? + .into_any() + .try_iter()? + .unbind()) + } + /// Iterable over all the classical variables tracked by the circuit. fn iter_vars(&self, py: Python) -> PyResult> { let out_set = PySet::empty(py)?; @@ -4366,6 +4550,18 @@ impl DAGCircuit { Ok(out_set.into_any().try_iter()?.unbind()) } + /// Iterable over all the stretches tracked by the circuit. + fn iter_stretches(&self, py: Python) -> PyResult> { + let out_set = PySet::empty(py)?; + for s in self.captured_stretches.values() { + out_set.add(s)?; + } + for s in self.declared_stretches.values() { + out_set.add(s)?; + } + Ok(out_set.into_any().try_iter()?.unbind()) + } + fn _has_edge(&self, source: usize, target: usize) -> bool { self.dag .contains_edge(NodeIndex::new(source), NodeIndex::new(target)) @@ -5794,7 +5990,14 @@ impl DAGCircuit { return Err(DAGCircuitError::new_err("already present in the circuit")); } return Err(DAGCircuitError::new_err( - "cannot add var as its name shadows an existing var", + "cannot add var as its name shadows an existing identifier", + )); + } + if self.declared_stretches.contains_key(&var_name) + || self.captured_stretches.contains_key(&var_name) + { + return Err(DAGCircuitError::new_err( + "cannot add var as its name shadows an existing identifier", )); } @@ -5915,6 +6118,8 @@ impl DAGCircuit { PySet::empty(py)?.unbind(), PySet::empty(py)?.unbind(), ], + captured_stretches: IndexMap::default(), + declared_stretches: IndexMap::default(), }) } @@ -6411,6 +6616,24 @@ impl DAGCircuit { new_dag.add_var(py, var, DAGVarType::Capture)?; } + new_dag + .captured_stretches + .reserve(qc.captured_stretches.len()); + + for var in qc.captured_stretches { + let name: String = var.getattr("name")?.extract::()?; + new_dag.captured_stretches.insert(name, var.unbind()); + } + + new_dag + .declared_stretches + .reserve(qc.declared_stretches.len()); + + for var in qc.declared_stretches { + let name: String = var.getattr("name")?.extract::()?; + new_dag.declared_stretches.insert(name, var.unbind()); + } + // Add all the registers if let Some(qregs) = qc.qregs { for qreg in qregs.iter() { @@ -6460,6 +6683,8 @@ impl DAGCircuit { input_vars: Vec::new(), captured_vars: Vec::new(), declared_vars: Vec::new(), + captured_stretches: Vec::new(), + declared_stretches: Vec::new(), }; Self::from_circuit(py, circ, copy_op, None, None) } diff --git a/qiskit/circuit/_classical_resource_map.py b/qiskit/circuit/_classical_resource_map.py index ba42f15cddc9..66ebc4653bf2 100644 --- a/qiskit/circuit/_classical_resource_map.py +++ b/qiskit/circuit/_classical_resource_map.py @@ -43,7 +43,7 @@ def __init__( self, target_cregs: typing.Iterable[ClassicalRegister], bit_map: typing.Mapping[Bit, Bit], - var_map: typing.Mapping[expr.Var, expr.Var] | None = None, + var_map: typing.Mapping[expr.Var | expr.Stretch, expr.Var | expr.Stretch] | None = None, *, add_register: typing.Callable[[ClassicalRegister], None] | None = None, ): @@ -132,6 +132,9 @@ def visit_var(self, node, /): return expr.Var(self._map_register(node.var), node.type) return self.var_map.get(node, node) + def visit_stretch(self, node, /): + return self.var_map.get(node, node) + def visit_value(self, node, /): return expr.Value(node.value, node.type) diff --git a/qiskit/circuit/classical/expr/__init__.py b/qiskit/circuit/classical/expr/__init__.py index 24fdb924b051..e01470e496d7 100644 --- a/qiskit/circuit/classical/expr/__init__.py +++ b/qiskit/circuit/classical/expr/__init__.py @@ -54,6 +54,11 @@ .. autoclass:: Value +Stretch variables for use in duration expressions are represented by the :class:`Stretch` node. + +.. autoclass:: Stretch + :members: var, name, new + The operations traditionally associated with pre-, post- or infix operators in programming are represented by the :class:`Unary` and :class:`Binary` nodes as appropriate. These each take an operation type code, which are exposed as enumerations inside each class as :class:`Unary.Op` @@ -174,6 +179,11 @@ .. autofunction:: iter_vars +To iterator over all variables including stretch variables, the iterator method +:func:`iter_identifiers` is provided. + +.. autofunction:: iter_identifiers + Two expressions can be compared for direct structural equality by using the built-in Python ``==`` operator. In general, though, one might want to compare two expressions slightly more semantically, allowing that the :class:`Var` nodes inside them are bound to different memory-location descriptions @@ -196,8 +206,10 @@ "Unary", "Binary", "Index", + "Stretch", "ExprVisitor", "iter_vars", + "iter_identifiers", "structurally_equivalent", "is_lvalue", "lift", @@ -225,8 +237,8 @@ "lift_legacy_condition", ] -from .expr import Expr, Var, Value, Cast, Unary, Binary, Index -from .visitors import ExprVisitor, iter_vars, structurally_equivalent, is_lvalue +from .expr import Expr, Var, Value, Cast, Unary, Binary, Index, Stretch +from .visitors import ExprVisitor, iter_vars, iter_identifiers, structurally_equivalent, is_lvalue from .constructors import ( lift, cast, diff --git a/qiskit/circuit/classical/expr/expr.py b/qiskit/circuit/classical/expr/expr.py index d2f8a20fb091..fc407fdff744 100644 --- a/qiskit/circuit/classical/expr/expr.py +++ b/qiskit/circuit/classical/expr/expr.py @@ -22,10 +22,12 @@ __all__ = [ "Expr", "Var", + "Stretch", "Value", "Cast", "Unary", "Binary", + "Index", ] import abc @@ -203,6 +205,75 @@ def __deepcopy__(self, memo): return self +@typing.final +class Stretch(Expr): + """A stretch variable. + + In general, construction of stretch variables for use in programs should use :meth:`Stretch.new` + or :meth:`.QuantumCircuit.add_stretch`. + """ + + __slots__ = ( + "var", + "name", + ) + + var: uuid.UUID + """A :class:`~uuid.UUID` to uniquely identify this stretch.""" + name: str + """The name of the stretch variable.""" + + def __init__( + self, + var: uuid.UUID, + name: str, + ): + super().__setattr__("type", types.Duration()) + super().__setattr__("const", True) + super().__setattr__("var", var) + super().__setattr__("name", name) + + @classmethod + def new(cls, name: str) -> typing.Self: + """Generate a new named stretch variable.""" + return cls(uuid.uuid4(), name) + + def accept(self, visitor, /): + return visitor.visit_stretch(self) + + def __setattr__(self, key, value): + if hasattr(self, key): + raise AttributeError(f"'Stretch' object attribute '{key}' is read-only") + raise AttributeError(f"'Stretch' object has no attribute '{key}'") + + def __hash__(self): + return hash((self.var, self.name)) + + def __eq__(self, other): + return isinstance(other, Stretch) and self.var == other.var and self.name == other.name + + def __repr__(self): + return f"Stretch({self.var}, {self.name})" + + def __getstate__(self): + return (self.var, self.name) + + def __setstate__(self, state): + var, name = state + super().__setattr__("type", types.Duration()) + super().__setattr__("const", True) + super().__setattr__("var", var) + super().__setattr__("name", name) + + def __copy__(self): + # I am immutable... + return self + + def __deepcopy__(self, memo): + # ... as are all my constituent parts. + return self + + @typing.final class Value(Expr): """A single scalar value.""" diff --git a/qiskit/circuit/classical/expr/visitors.py b/qiskit/circuit/classical/expr/visitors.py index 65f744587662..fe4d9bef95dd 100644 --- a/qiskit/circuit/classical/expr/visitors.py +++ b/qiskit/circuit/classical/expr/visitors.py @@ -17,7 +17,9 @@ __all__ = [ "ExprVisitor", "iter_vars", + "iter_identifiers", "structurally_equivalent", + "is_lvalue", ] import typing @@ -43,6 +45,9 @@ def visit_generic(self, node: expr.Expr, /) -> _T_co: # pragma: no cover def visit_var(self, node: expr.Var, /) -> _T_co: # pragma: no cover return self.visit_generic(node) + def visit_stretch(self, node: expr.Stretch, /) -> _T_co: # pragma: no cover + return self.visit_generic(node) + def visit_value(self, node: expr.Value, /) -> _T_co: # pragma: no cover return self.visit_generic(node) @@ -65,6 +70,36 @@ class _VarWalkerImpl(ExprVisitor[typing.Iterable[expr.Var]]): def visit_var(self, node, /): yield node + def visit_stretch(self, node, /): + yield from () + + def visit_value(self, node, /): + yield from () + + def visit_unary(self, node, /): + yield from node.operand.accept(self) + + def visit_binary(self, node, /): + yield from node.left.accept(self) + yield from node.right.accept(self) + + def visit_cast(self, node, /): + yield from node.operand.accept(self) + + def visit_index(self, node, /): + yield from node.target.accept(self) + yield from node.index.accept(self) + + +class _IdentWalkerImpl(ExprVisitor[typing.Iterable[typing.Union[expr.Var, expr.Stretch]]]): + __slots__ = () + + def visit_var(self, node, /): + yield node + + def visit_stretch(self, node, /): + yield node + def visit_value(self, node, /): yield from () @@ -84,6 +119,7 @@ def visit_index(self, node, /): _VAR_WALKER = _VarWalkerImpl() +_IDENT_WALKER = _IdentWalkerImpl() def iter_vars(node: expr.Expr) -> typing.Iterator[expr.Var]: @@ -102,10 +138,39 @@ def iter_vars(node: expr.Expr) -> typing.Iterator[expr.Var]: for node in expr.iter_vars(expr.bit_and(expr.bit_not(cr1), cr2)): if isinstance(node.var, ClassicalRegister): print(node.var.name) + + .. seealso:: + :func:`iter_identifiers` + Get an iterator over all identifier nodes in the expression, including + both :class:`~.expr.Var` and :class:`~.expr.Stretch` nodes. """ yield from node.accept(_VAR_WALKER) +def iter_identifiers(node: expr.Expr) -> typing.Iterator[typing.Union[expr.Var, expr.Stretch]]: + """Get an iterator over the :class:`~.expr.Var` and :class:`~.expr.Stretch` + nodes referenced at any level in the given :class:`~.expr.Expr`. + + Examples: + Print out the name of each :class:`.ClassicalRegister` encountered:: + + from qiskit.circuit import ClassicalRegister + from qiskit.circuit.classical import expr + + cr1 = ClassicalRegister(3, "a") + cr2 = ClassicalRegister(3, "b") + + for node in expr.iter_vars(expr.bit_and(expr.bit_not(cr1), cr2)): + if isinstance(node.var, ClassicalRegister): + print(node.var.name) + + .. seealso:: + :func:`iter_vars` + Get an iterator over just the :class:`~.expr.Var` nodes in the expression. + """ + yield from node.accept(_IDENT_WALKER) + + class _StructuralEquivalenceImpl(ExprVisitor[bool]): # The strategy here is to continue to do regular double dispatch through the visitor format, # since we simply exit out with a ``False`` as soon as the structure of the two trees isn't the @@ -134,6 +199,11 @@ def visit_var(self, node, /): other_var = self.other.var return self_var == other_var + def visit_stretch(self, node, /): + if self.other.__class__ is not node.__class__: + return False + return node.var == self.other.var + def visit_value(self, node, /): return ( node.__class__ is self.other.__class__ @@ -240,6 +310,9 @@ class _IsLValueImpl(ExprVisitor[bool]): def visit_var(self, node, /): return True + def visit_stretch(self, node, /): + return False + def visit_value(self, node, /): return False diff --git a/qiskit/circuit/controlflow/_builder_utils.py b/qiskit/circuit/controlflow/_builder_utils.py index e80910aac3ed..cc2594c7e01d 100644 --- a/qiskit/circuit/controlflow/_builder_utils.py +++ b/qiskit/circuit/controlflow/_builder_utils.py @@ -180,10 +180,12 @@ def _unify_circuit_resources_rebuild( # pylint: disable=invalid-name # (it's t *circuit.cregs, global_phase=circuit.global_phase, inputs=circuit.iter_input_vars(), - captures=circuit.iter_captured_vars(), + captures=circuit.iter_captures(), ) for var in circuit.iter_declared_vars(): out.add_uninitialized_var(var) + for stretch in circuit.iter_declared_stretches(): + out.add_stretch(stretch) for instruction in circuit.data: out._append(instruction) out_circuits.append(out) diff --git a/qiskit/circuit/controlflow/builder.py b/qiskit/circuit/controlflow/builder.py index ca5c58504a24..e9f3174ff6fc 100644 --- a/qiskit/circuit/controlflow/builder.py +++ b/qiskit/circuit/controlflow/builder.py @@ -121,6 +121,18 @@ def add_uninitialized_var(self, var: expr.Var): redefines an existing name. """ + @abc.abstractmethod + def add_stretch(self, stretch: expr.Stretch): + """Add a stretch to the circuit scope. + + Args: + stretch: the stretch to add, if valid. + + Raises: + CircuitError: if the stretch cannot be added, such as because it invalidly shadows or + redefines an existing name. + """ + @abc.abstractmethod def remove_var(self, var: expr.Var): """Remove a variable from the locals of this scope. @@ -133,6 +145,18 @@ def remove_var(self, var: expr.Var): :meth:`add_uninitialized_var` call. """ + @abc.abstractmethod + def remove_stretch(self, stretch: expr.Stretch): + """Remove a stretch from the locals of this scope. + + This is only called in the case that an exception occurred while initializing the stretch, + and is not exposed to users. + + Args: + stretch: the stretch to remove. It can be assumed that this was already the subject of an + :meth:`add_stretch` call. + """ + @abc.abstractmethod def use_var(self, var: expr.Var): """Called for every standalone classical real-time variable being used by some circuit @@ -145,13 +169,21 @@ def use_var(self, var: expr.Var): Args: var: the variable to validate. - Returns: - the same variable. - Raises: CircuitError: if the variable is not valid for this scope. """ + @abc.abstractmethod + def use_stretch(self, stretch: expr.Stretch): + """Called for every stretch being used by some circuit instruction. + + Args: + stretch: the stretch to validate. + + Raises: + CircuitError: if the stretch is not valid for this scope. + """ + @abc.abstractmethod def get_var(self, name: str) -> Optional[expr.Var]: """Get the variable (if any) in scope with the given name. @@ -166,6 +198,20 @@ def get_var(self, name: str) -> Optional[expr.Var]: the variable if it is found, otherwise ``None``. """ + @abc.abstractmethod + def get_stretch(self, name: str) -> Optional[expr.Stretch]: + """Get the stretch (if any) in scope with the given name. + + This should call up to the parent scope if in a control-flow builder scope, in case the + stretch exists in an outer scope. + + Args: + name: the name of the symbol to lookup. + + Returns: + the stretch if it is found, otherwise ``None``. + """ + @abc.abstractmethod def use_qubit(self, qubit: Qubit): """Called to mark that a :class:`~.circuit.Qubit` should be considered "used" by this scope, @@ -317,6 +363,8 @@ class ControlFlowBuilderBlock(CircuitScopeInterface): "_forbidden_message", "_vars_local", "_vars_capture", + "_stretches_local", + "_stretches_capture", ) def __init__( @@ -359,6 +407,8 @@ def __init__( self.global_phase = 0.0 self._vars_local = {} self._vars_capture = {} + self._stretches_local = {} + self._stretches_capture = {} self._allow_jumps = allow_jumps self._parent = parent self._built = False @@ -448,25 +498,60 @@ def add_uninitialized_var(self, var: expr.Var): raise CircuitError("Cannot add resources after the scope has been built.") # We can shadow a name if it was declared in an outer scope, but only if we haven't already # captured it ourselves yet. + if (previous := self._stretches_local.get(var.name)) is not None: + raise CircuitError(f"cannot add '{var}' as its name shadows the existing '{previous}'") if (previous := self._vars_local.get(var.name)) is not None: if previous == var: raise CircuitError(f"'{var}' is already present in the scope") raise CircuitError(f"cannot add '{var}' as its name shadows the existing '{previous}'") - if var.name in self._vars_capture: + if var.name in self._vars_capture or var.name in self._stretches_capture: raise CircuitError(f"cannot add '{var}' as its name shadows the existing '{previous}'") self._vars_local[var.name] = var + def add_stretch(self, stretch: expr.Stretch): + if self._built: + raise CircuitError("Cannot add resources after the scope has been built.") + # We can shadow a name if it was declared in an outer scope, but only if we haven't already + # captured it ourselves yet. + if (previous := self._vars_local.get(stretch.name)) is not None: + raise CircuitError( + f"cannot add '{stretch}' as its name shadows the existing '{previous}'" + ) + if (previous := self._stretches_local.get(stretch.name)) is not None: + if previous == stretch: + raise CircuitError(f"'{stretch}' is already present in the scope") + raise CircuitError( + f"cannot add '{stretch}' as its name shadows the existing '{previous}'" + ) + if stretch.name in self._vars_capture or stretch.name in self._stretches_capture: + raise CircuitError( + f"cannot add '{stretch}' as its name shadows the existing '{previous}'" + ) + self._stretches_local[stretch.name] = stretch + def remove_var(self, var: expr.Var): if self._built: raise RuntimeError("exception handler 'remove_var' called after scope built") self._vars_local.pop(var.name) + def remove_stretch(self, stretch: expr.Stretch): + if self._built: + raise RuntimeError("exception handler 'remove_stretch' called after scope built") + self._stretches_local.pop(stretch.name) + def get_var(self, name: str): 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 (out := self._stretches_local.get(name)) is not None: + return out + return self._parent.get_stretch(name) + def use_var(self, var: expr.Var): + if (local := self._stretches_local.get(var.name)) is not None: + raise CircuitError(f"cannot use '{var}' which is shadowed by the local '{local}'") if (local := self._vars_local.get(var.name)) is not None: if local == var: return @@ -478,6 +563,20 @@ def use_var(self, var: expr.Var): self._parent.use_var(var) self._vars_capture[var.name] = var + def use_stretch(self, stretch: expr.Stretch): + if (local := self._vars_local.get(stretch.name)) is not None: + raise CircuitError(f"cannot use '{stretch}' which is shadowed by the local '{local}'") + if (local := self._stretches_local.get(stretch.name)) is not None: + if local == stretch: + return + raise CircuitError(f"cannot use '{stretch}' which is shadowed by the local '{local}'") + if self._stretches_capture.get(stretch.name) == stretch: + return + if self._parent.get_stretch(stretch.name) != stretch: + raise CircuitError(f"cannot close over '{stretch}', which is not in scope") + self._parent.use_stretch(stretch) + self._stretches_capture[stretch.name] = stretch + def use_qubit(self, qubit: Qubit): self._instructions.add_qubit(qubit, strict=False) @@ -485,10 +584,18 @@ def iter_local_vars(self): """Iterator over the variables currently declared in this scope.""" return self._vars_local.values() + def iter_local_stretches(self): + """Iterator over the stretches currently declared in this scope.""" + return self._stretches_local.values() + def iter_captured_vars(self): """Iterator over the variables currently captured in this scope.""" return self._vars_capture.values() + def iter_captured_stretches(self): + """Iterator over the stretches currently captured in this scope.""" + return self._stretches_capture.values() + def peek(self) -> CircuitInstruction: """Get the value of the most recent instruction tuple in this scope.""" if not self._instructions: @@ -589,13 +696,16 @@ def build( self._instructions.clbits, *self.registers, global_phase=self.global_phase, - captures=self._vars_capture.values(), + captures=itertools.chain(self._vars_capture.values(), self._stretches_capture.values()), ) for var in self._vars_local.values(): # The requisite `Store` instruction to initialise the variable will have been appended # into the instructions. out.add_uninitialized_var(var) + for var in self._stretches_local.values(): + out.add_stretch(var) + # Maps placeholder index to the newly concrete instruction. placeholder_to_concrete = {} @@ -669,6 +779,8 @@ def copy(self) -> "ControlFlowBuilderBlock": out.global_phase = self.global_phase out._vars_local = self._vars_local.copy() out._vars_capture = self._vars_capture.copy() + out._stretches_local = self._stretches_local.copy() + out._stretches_capture = self._stretches_capture.copy() out._parent = self._parent out._allow_jumps = self._allow_jumps out._forbidden_message = self._forbidden_message diff --git a/qiskit/circuit/controlflow/control_flow.py b/qiskit/circuit/controlflow/control_flow.py index 2085f760ebcd..74e610910e7c 100644 --- a/qiskit/circuit/controlflow/control_flow.py +++ b/qiskit/circuit/controlflow/control_flow.py @@ -82,3 +82,13 @@ def iter_captured_vars(self) -> typing.Iterable[expr.Var]: if var not in seen: seen.add(var) yield var + + def iter_captured_stretches(self) -> typing.Iterable[expr.Stretch]: + """Get an iterator over the unique captured stretch variables in all blocks of this + construct.""" + seen = set() + for block in self.blocks: + for stretch in block.iter_captured_stretches(): + if stretch not in seen: + seen.add(stretch) + yield stretch diff --git a/qiskit/circuit/quantumcircuit.py b/qiskit/circuit/quantumcircuit.py index d6c258456364..d77da4b25712 100644 --- a/qiskit/circuit/quantumcircuit.py +++ b/qiskit/circuit/quantumcircuit.py @@ -18,6 +18,7 @@ import collections.abc import copy as _copy + import itertools import multiprocessing import typing @@ -956,7 +957,7 @@ def __init__( global_phase: ParameterValueType = 0, metadata: dict | None = None, inputs: Iterable[expr.Var] = (), - captures: Iterable[expr.Var] = (), + captures: Iterable[expr.Var | expr.Stretch] = (), declarations: Mapping[expr.Var, expr.Expr] | Iterable[Tuple[expr.Var, expr.Expr]] = (), ): """ @@ -1004,7 +1005,7 @@ def __init__( :meth:`QuantumCircuit.add_input`. The variables given in this argument will be passed directly to :meth:`add_input`. A circuit cannot have both ``inputs`` and ``captures``. - captures: any variables that that this circuit scope should capture from a containing + captures: any variables that this circuit scope should capture from a containing scope. The variables given here will be passed directly to :meth:`add_capture`. A circuit cannot have both ``inputs`` and ``captures``. declarations: any variables that this circuit should declare and initialize immediately. @@ -1098,6 +1099,8 @@ def __init__( self._vars_input: dict[str, expr.Var] = {} self._vars_capture: dict[str, expr.Var] = {} self._vars_local: dict[str, expr.Var] = {} + self._stretches_capture: dict[str, expr.Stretch] = {} + self._stretches_local: dict[str, expr.Stretch] = {} for input_ in inputs: self.add_input(input_) for capture in captures: @@ -1698,7 +1701,9 @@ def compose( wrap: bool = False, *, copy: bool = True, - var_remap: Mapping[str | expr.Var, str | expr.Var] | None = None, + var_remap: ( + Mapping[str | expr.Var | expr.Stretch, str | expr.Var | expr.Stretch] | None + ) = None, inline_captures: bool = False, ) -> Optional["QuantumCircuit"]: """Apply the instructions from one circuit onto specified qubits and/or clbits on another. @@ -1742,23 +1747,25 @@ def compose( the base circuit, in order to avoid unnecessary copies; in this case, it is not valid to use ``other`` afterward, and some instructions may have been mutated in place. - var_remap (Mapping): mapping to use to rewrite :class:`.expr.Var` nodes in ``other`` as - they are inlined into ``self``. This can be used to avoid naming conflicts. - - Both keys and values can be given as strings or direct :class:`.expr.Var` instances. - If a key is a string, it matches any :class:`~.expr.Var` with the same name. If a - value is a string, whenever a new key matches a it, a new :class:`~.expr.Var` is - created with the correct type. If a value is a :class:`~.expr.Var`, its - :class:`~.expr.Expr.type` must exactly match that of the variable it is replacing. - inline_captures (bool): if ``True``, then all "captured" :class:`~.expr.Var` nodes in - the ``other`` :class:`.QuantumCircuit` are assumed to refer to variables already + var_remap (Mapping): mapping to use to rewrite :class:`.expr.Var` and + :class:`.expr.Stretch` nodes in ``other`` as they are inlined into ``self``. + This can be used to avoid naming conflicts. + + Both keys and values can be given as strings or direct identifier instances. + If a key is a string, it matches any :class:`~.expr.Var` or :class:`~.expr.Stretch` + with the same name. If a value is a string, whenever a new key matches it, a new + :class:`~.expr.Var` or :class:`~.expr.Stretch` is created with the correct type. + If a value is a :class:`~.expr.Var`, its :class:`~.expr.Expr.type` must exactly + match that of the variable it is replacing. + inline_captures (bool): if ``True``, then all "captured" identifier nodes in + the ``other`` :class:`.QuantumCircuit` are assumed to refer to identifiers already declared in ``self`` (as any input/capture/local type), and the uses in ``other`` - will apply to the existing variables. If you want to build up a layer for an + will apply to the existing identifiers. If you want to build up a layer for an existing circuit to use with :meth:`compose`, you might find the ``vars_mode="captures"`` argument to :meth:`copy_empty_like` useful. Any remapping in ``vars_remap`` occurs before evaluating this variable inlining. - If this is ``False`` (the default), then all variables in ``other`` will be required + If this is ``False`` (the default), then all identifiers in ``other`` will be required to be distinct from those in ``self``, and new declarations will be made for them. wrap (bool): If True, wraps the other circuit into a gate (or instruction, depending on whether it contains only unitary instructions) before composing it onto self. @@ -1827,20 +1834,36 @@ def compose( # instructions. We cache all replacement lookups for a) speed and b) to ensure that # the same variable _always_ maps to the same replacement even if it's used in different # places in the recursion tree (such as being a captured variable). - def replace_var(var: expr.Var, cache: Mapping[expr.Var, expr.Var]) -> expr.Var: + def replace_var( + var: Union[expr.Var, expr.Stretch], cache: Mapping[expr.Var, expr.Var] + ) -> Union[expr.Var | expr.Stretch]: # This is closing over an argument to `compose`. nonlocal var_remap if out := cache.get(var): return out if (replacement := var_remap.get(var)) or (replacement := var_remap.get(var.name)): - if isinstance(replacement, str): - replacement = expr.Var.new(replacement, var.type) - if replacement.type != var.type: - raise CircuitError( - f"mismatched types in replacement for '{var.name}':" - f" '{var.type}' cannot become '{replacement.type}'" - ) + if isinstance(var, expr.Var): + if isinstance(replacement, expr.Stretch): + raise CircuitError( + "mismatched identifier kinds in replacement:" + f" '{var}' cannot become '{replacement}'" + ) + if isinstance(replacement, str): + replacement = expr.Var.new(replacement, var.type) + if replacement.type != var.type: + raise CircuitError( + f"mismatched types in replacement for '{var.name}':" + f" '{var.type}' cannot become '{replacement.type}'" + ) + else: + if isinstance(replacement, expr.Var): + raise CircuitError( + "mismatched identifier kind in replacement:" + f" '{var}' cannot become '{replacement}'" + ) + if isinstance(replacement, str): + replacement = expr.Stretch.new(replacement) else: replacement = var cache[var] = replacement @@ -1939,9 +1962,9 @@ def copy_with_remapping( for var in source.iter_input_vars(): dest.add_input(replace_var(var, var_map)) if inline_captures: - for var in source.iter_captured_vars(): + for var in source.iter_captures(): replacement = replace_var(var, var_map) - if not dest.has_var(replace_var(var, var_map)): + if not dest.has_identifier(replace_var(var, var_map)): if var is replacement: raise CircuitError( f"Variable '{var}' to be inlined is not in the base circuit." @@ -1953,10 +1976,12 @@ def copy_with_remapping( " base circuit. Is the replacement correct?" ) else: - for var in source.iter_captured_vars(): + for var in source.iter_captures(): dest.add_capture(replace_var(var, var_map)) for var in source.iter_declared_vars(): dest.add_uninitialized_var(replace_var(var, var_map)) + for stretch in source.iter_declared_stretches(): + dest.add_stretch(replace_var(stretch, var_map)) def recurse_block(block): # Recurse the remapping into a control-flow block. Note that this doesn't remap the @@ -1967,6 +1992,8 @@ def recurse_block(block): new_block._vars_input = {} new_block._vars_capture = {} new_block._vars_local = {} + new_block._stretches_capture = {} + new_block._stretches_local = {} # For the recursion, we never want to inline captured variables because we're not # copying onto a base that has variables. copy_with_remapping(block, new_block, bit_map, var_map, inline_captures=False) @@ -2132,6 +2159,22 @@ def num_vars(self) -> int: This is the length of the :meth:`iter_vars` iterable.""" return self.num_input_vars + self.num_captured_vars + self.num_declared_vars + @property + def num_stretches(self) -> int: + """The number of stretches in the circuit. + + This is the length of the :meth:`iter_stretches` iterable.""" + return self.num_captured_stretches + self.num_declared_stretches + + @property + def num_identifiers(self) -> int: + """The number of real-time classical variables and stretches in + the circuit. + + This is equal to :meth:`num_vars` + :meth:`num_stretches`. + """ + return self.num_vars + self.num_stretches + @property def num_input_vars(self) -> int: """The number of real-time classical variables in the circuit marked as circuit inputs. @@ -2149,6 +2192,15 @@ def num_captured_vars(self) -> int: :attr:`num_input_vars` must be zero.""" return len(self._vars_capture) + @property + def num_captured_stretches(self) -> int: + """The number of stretches in the circuit marked as captured from an + enclosing scope. + + This is the length of the :meth:`iter_captured_stretches` iterable. If this is non-zero, + :attr:`num_input_vars` must be zero.""" + return len(self._stretches_capture) + @property def num_declared_vars(self) -> int: """The number of real-time classical variables in the circuit that are declared by this @@ -2157,6 +2209,14 @@ def num_declared_vars(self) -> int: This is the length of the :meth:`iter_declared_vars` iterable.""" return len(self._vars_local) + @property + def num_declared_stretches(self) -> int: + """The number of stretches in the circuit that are declared by this + circuit scope, excluding captures. + + This is the length of the :meth:`iter_declared_stretches` iterable.""" + return len(self._stretches_local) + def iter_vars(self) -> typing.Iterable[expr.Var]: """Get an iterable over all real-time classical variables in scope within this circuit. @@ -2169,6 +2229,18 @@ def iter_vars(self) -> typing.Iterable[expr.Var]: self._vars_input.values(), self._vars_capture.values(), self._vars_local.values() ) + def iter_stretches(self) -> typing.Iterable[expr.Stretch]: + """Get an iterable over all stretches in scope within this circuit. + + This method will iterate over all stretches in scope. For more fine-grained iterators, see + :meth:`iter_declared_stretches` and :meth:`iter_captured_stretches`.""" + if self._control_flow_scopes: + builder = self._control_flow_scopes[-1] + return itertools.chain( + builder.iter_captured_stretches(), builder.iter_local_stretches() + ) + return itertools.chain(self._stretches_capture.values(), self._stretches_local.values()) + def iter_declared_vars(self) -> typing.Iterable[expr.Var]: """Get an iterable over all real-time classical variables that are declared with automatic storage duration in this scope. This excludes input variables (see :meth:`iter_input_vars`) @@ -2177,6 +2249,13 @@ def iter_declared_vars(self) -> typing.Iterable[expr.Var]: return self._control_flow_scopes[-1].iter_local_vars() return self._vars_local.values() + def iter_declared_stretches(self) -> typing.Iterable[expr.Stretch]: + """Get an iterable over all stretches that are declared in this scope. + This excludes captured stretches (see :meth:`iter_captured_stretches`).""" + if self._control_flow_scopes: + return self._control_flow_scopes[-1].iter_local_stretches() + return self._stretches_local.values() + def iter_input_vars(self) -> typing.Iterable[expr.Var]: """Get an iterable over all real-time classical variables that are declared as inputs to this circuit scope. This excludes locally declared variables (see @@ -2185,6 +2264,18 @@ def iter_input_vars(self) -> typing.Iterable[expr.Var]: return () return self._vars_input.values() + def iter_captures(self) -> typing.Iterable[typing.Union[expr.Var, expr.Stretch]]: + """Get an iterable over all identifiers are captured by this circuit scope from a + containing scope. This excludes input variables (see :meth:`iter_input_vars`) + and locally declared variables and stretches (see :meth:`iter_declared_vars` + and :meth:`iter_declared_stretches`).""" + if self._control_flow_scopes: + return itertools.chain( + self._control_flow_scopes[-1].iter_captured_vars(), + self._control_flow_scopes[-1].iter_captured_stretches(), + ) + return itertools.chain(self._vars_capture.values(), self._stretches_capture.values()) + def iter_captured_vars(self) -> typing.Iterable[expr.Var]: """Get an iterable over all real-time classical variables that are captured by this circuit scope from a containing scope. This excludes input variables (see :meth:`iter_input_vars`) @@ -2193,6 +2284,14 @@ def iter_captured_vars(self) -> typing.Iterable[expr.Var]: return self._control_flow_scopes[-1].iter_captured_vars() return self._vars_capture.values() + def iter_captured_stretches(self) -> typing.Iterable[expr.Stretch]: + """Get an iterable over stretches that are captured by this circuit + scope from a containing scope. This excludes locally declared stretches + (see :meth:`iter_declared_stretches`).""" + if self._control_flow_scopes: + return self._control_flow_scopes[-1].iter_captured_stretches() + return self._stretches_capture.values() + def __and__(self, rhs: "QuantumCircuit") -> "QuantumCircuit": """Overload & to implement self.compose.""" return self.compose(rhs) @@ -2366,9 +2465,9 @@ def append( if bad_captures := { var for var in itertools.chain.from_iterable( - block.iter_captured_vars() for block in operation.blocks + block.iter_captures() for block in operation.blocks ) - if not self.has_var(var) + if not self.has_identifier(var) }: raise CircuitError( f"Control-flow op attempts to capture '{bad_captures}'" @@ -2633,6 +2732,144 @@ def has_var(self, name_or_var: str | expr.Var, /) -> bool: return self.get_var(name_or_var, None) is not None return self.get_var(name_or_var.name, None) == name_or_var + @typing.overload + def get_stretch(self, name: str, default: T) -> Union[expr.Stretch, T]: ... + + # The builtin `types` module has `EllipsisType`, but only from 3.10+! + @typing.overload + def get_stretch(self, name: str, default: type(...) = ...) -> expr.Stretch: ... + + def get_stretch(self, name: str, default: typing.Any = ...): + """Retrieve a stretch that is accessible in this circuit scope by name. + + Args: + name: the name of the stretch to retrieve. + default: if given, this value will be returned if the variable is not present. If it + is not given, a :exc:`KeyError` is raised instead. + + Returns: + The corresponding stretch. + + Raises: + KeyError: if no default is given, but the variable does not exist. + + Examples: + Retrieve a stretch by name from a circuit:: + + from qiskit.circuit import QuantumCircuit + + # Create a circuit and create a variable in it. + qc = QuantumCircuit() + my_stretch = qc.add_stretch("my_stretch") + + # We can use 'my_stretch' as a variable, but let's say we've lost the Python object and + # need to retrieve it. + my_stretch_again = qc.get_stretch("my_stretch") + + assert my_stretch is my_stretch_again + + Get a variable from a circuit by name, returning some default if it is not present:: + + assert qc.get_stretch("my_stretch", None) is my_stretch + assert qc.get_stretch("unknown_stretch", None) is None + """ + if (out := self._current_scope().get_stretch(name)) is not None: + return out + if default is Ellipsis: + raise KeyError(f"no stretch named '{name}' is present") + return default + + def has_stretch(self, name_or_stretch: str | expr.Stretch, /) -> bool: + """Check whether a stretch is accessible in this scope. + + Args: + name_or_stretch: the stretch, or name of a stretch to check. If this is a + :class:`.expr.Stretch` node, the stretch must be exactly the given one for this + function to return ``True``. + + Returns: + whether a matching stretch is accessible. + + See also: + :meth:`QuantumCircuit.get_stretch` + Retrieve the :class:`.expr.Stretch` instance from this circuit by name. + """ + if isinstance(name_or_stretch, str): + return self.get_stretch(name_or_stretch, None) is not None + return self.get_stretch(name_or_stretch.name, None) == name_or_stretch + + @typing.overload + def get_identifier(self, name: str, default: T) -> Union[expr.Var | expr.Stretch, T]: ... + + # The builtin `types` module has `EllipsisType`, but only from 3.10+! + @typing.overload + def get_identifier( + self, name: str, default: type(...) = ... + ) -> Union[expr.Var, expr.Stretch]: ... + + # We use a _literal_ `Ellipsis` as the marker value to leave `None` available as a default. + def get_identifier(self, name: str, default: typing.Any = ...): + """Retrieve an identifier that is accessible in this circuit scope by name. + + This currently includes both real-time classical variables and stretches. + + Args: + name: the name of the identifier to retrieve. + default: if given, this value will be returned if the variable is not present. If it + is not given, a :exc:`KeyError` is raised instead. + + Returns: + The corresponding variable. + + Raises: + KeyError: if no default is given, but the identifier does not exist. + + See also: + :meth:`get_var` + Gets an identifier known to be a :class:`.expr.Var` instance. + :meth:`get_stretch` + Gets an identifier known to be a :class:`.expr.Stretch` instance. + :meth:`get_parameter` + A similar method, but for :class:`.Parameter` compile-time parameters instead of + :class:`.expr.Var` run-time variables. + """ + if (out := self._current_scope().get_var(name)) is not None: + return out + if (out := self._current_scope().get_stretch(name)) is not None: + return out + if default is Ellipsis: + raise KeyError(f"no identifier named '{name}' is present") + return default + + def has_identifier(self, name_or_ident: str | expr.Var | expr.Stretch, /) -> bool: + """Check whether an identifier is accessible in this scope. + + Args: + name_or_ident: the instance, or name of the identifier to check. If this is a + :class:`.expr.Var` or :class:`.expr.Stretch` node, the matched instance must + be exactly the given one for this function to return ``True``. + + Returns: + whether a matching identifier is accessible. + + See also: + :meth:`QuantumCircuit.get_identifier` + Retrieve the :class:`.expr.Var` or :class:`.expr.Stretch` instance from this + circuit by name. + :meth:`QuantumCircuit.has_var` + The same as this method, but ignoring anything that isn't a + run-time :class:`expr.Var` variable. + :meth:`QuantumCircuit.has_stretch` + The same as this method, but ignoring anything that isn't a + run-time :class:`expr.Stretch` variable. + :meth:`QuantumCircuit.has_parameter` + A similar method to this, but for compile-time :class:`.Parameter`\\ s instead of + run-time :class:`.expr.Var` variables. + """ + if isinstance(name_or_ident, str): + return self.get_identifier(name_or_ident, None) is not None + return self.get_identifier(name_or_ident.name, None) == name_or_ident + def _prepare_new_var( self, name_or_var: str | expr.Var, type_: types.Type | None, / ) -> expr.Var: @@ -2657,12 +2894,63 @@ def _prepare_new_var( # The `var` is guaranteed to have a name because we already excluded the cases where it's # wrapping a bit/register. - if (previous := self.get_var(var.name, default=None)) is not None: + if (previous := self.get_identifier(var.name, default=None)) is not None: if previous == var: raise CircuitError(f"'{var}' is already present in the circuit") raise CircuitError(f"cannot add '{var}' as its name shadows the existing '{previous}'") return var + def _prepare_new_stretch(self, name_or_stretch: str | expr.Stretch, /) -> expr.Stretch: + """The common logic for preparing and validating a new :class:`~.expr.Stretch` for the circuit. + + Returns the validated stretch, which is guaranteed to be safe to add to the circuit.""" + if isinstance(name_or_stretch, str): + stretch = expr.Stretch.new(name_or_stretch) + else: + stretch = name_or_stretch + + if (previous := self.get_identifier(stretch.name, default=None)) is not None: + if previous == stretch: + raise CircuitError(f"'{stretch}' is already present in the circuit") + raise CircuitError( + f"cannot add '{stretch}' as its name shadows the existing '{previous}'" + ) + return stretch + + def add_stretch(self, name_or_stretch: str | expr.Stretch) -> expr.Stretch: + """Declares a new stretch scoped to this circuit. + + Args: + name_or_stretch: either a string of the stretch name, or an existing instance of + :class:`~.expr.Stretch` to re-use. Stretches cannot shadow names that are already in + use within the circuit. + + Returns: + The created stretch. If a :class:`~.expr.Stretch` instance was given, the exact same + object will be returned. + + Raises: + CircuitError: if the stretch cannot be created due to shadowing an existing + identifier. + + Examples: + Define and use a new stretch given just a name:: + + from qiskit.circuit import QuantumCircuit, Duration + from qiskit.circuit.classical import expr + + qc = QuantumCircuit(2) + my_stretch = qc.add_stretch("my_stretch") + + qc.delay(expr.add(Duration.dt(200), my_stretch), 1) + """ + if isinstance(name_or_stretch, str): + stretch = expr.Stretch.new(name_or_stretch) + else: + stretch = name_or_stretch + self._current_scope().add_stretch(stretch) + return stretch + def add_var(self, name_or_var: str | expr.Var, /, initial: typing.Any) -> expr.Var: """Add a classical variable with automatic storage and scope to this circuit. @@ -2688,7 +2976,7 @@ def add_var(self, name_or_var: str | expr.Var, /, initial: typing.Any) -> expr.V object will be returned. Raises: - CircuitError: if the variable cannot be created due to shadowing an existing variable. + CircuitError: if the variable cannot be created due to shadowing an existing identifier. Examples: Define a new variable given just a name and an initializer expression:: @@ -2792,37 +3080,51 @@ def add_uninitialized_var(self, var: expr.Var, /): raise CircuitError("cannot add a variable wrapping a bit or register to a circuit") self._builder_api.add_uninitialized_var(var) - def add_capture(self, var: expr.Var): - """Add a variable to the circuit that it should capture from a scope it will be contained - within. + @typing.overload + def add_capture(self, var: expr.Var): ... + + @typing.overload + def add_capture(self, stretch: expr.Stretch): ... + + def add_capture(self, var): + """Add an identifier to the circuit that it should capture from a scope it will + be contained within. - This method requires a :class:`~.expr.Var` node to enforce that you've got a handle to one, - because you will need to declare the same variable using the same object into the outer - circuit. + This method requires a :class:`~.expr.Var` or :class:`~.expr.Stretch` node to enforce that + you've got a handle to an identifier, because you will need to declare the same identifier + using the same object in the outer circuit. This is a low-level method, which is only really useful if you are manually constructing control-flow operations. You typically will not need to call this method, assuming you are using the builder interface for control-flow scopes (``with`` context-manager statements for :meth:`if_test` and the other scoping constructs). The builder interface will - automatically make the inner scopes closures on your behalf by capturing any variables that - are used within them. + automatically make the inner scopes closures on your behalf by capturing any identifiers + that are used within them. Args: - var: the variable to capture from an enclosing scope. + var (Union[expr.Var, expr.Stretch]): the variable or stretch to capture from an + enclosing scope. Raises: - CircuitError: if the variable cannot be created due to shadowing an existing variable. + CircuitError: if the identifier cannot be created due to shadowing an existing + identifier. """ if self._control_flow_scopes: # Allow manual capturing. Not sure why it'd be useful, but there's a clear expected # behavior here. - self._control_flow_scopes[-1].use_var(var) + if isinstance(var, expr.Stretch): + self._control_flow_scopes[-1].use_stretch(var) + else: + self._control_flow_scopes[-1].use_var(var) return if self._vars_input: raise CircuitError( "circuits with input variables cannot be enclosed, so cannot be closures" ) - self._vars_capture[var.name] = self._prepare_new_var(var, None) + if isinstance(var, expr.Stretch): + self._stretches_capture[var.name] = self._prepare_new_stretch(var) + else: + self._vars_capture[var.name] = self._prepare_new_var(var, None) @typing.overload def add_input(self, name_or_var: str, type_: types.Type, /) -> expr.Var: ... @@ -2850,7 +3152,7 @@ def add_input( # pylint: disable=missing-raises-doc """ if self._control_flow_scopes: raise CircuitError("cannot add an input variable in a control-flow scope") - if self._vars_capture: + if self._vars_capture or self._stretches_capture: raise CircuitError("circuits to be enclosed with captures cannot have input variables") if isinstance(name_or_var, expr.Var) and type_ is not None: raise ValueError("cannot give an explicit type with an existing Var") @@ -6838,9 +7140,16 @@ def add_uninitialized_var(self, var): var = self.circuit._prepare_new_var(var, None) self.circuit._vars_local[var.name] = var + def add_stretch(self, stretch): + stretch = self.circuit._prepare_new_stretch(stretch) + self.circuit._stretches_local[stretch.name] = stretch + def remove_var(self, var): self.circuit._vars_local.pop(var.name) + def remove_stretch(self, stretch): + self.circuit._stretches_local.pop(stretch.name) + def get_var(self, name): if (out := self.circuit._vars_local.get(name)) is not None: return out @@ -6848,10 +7157,19 @@ def get_var(self, name): return out return self.circuit._vars_input.get(name) + def get_stretch(self, name): + if (out := self.circuit._stretches_local.get(name)) is not None: + return out + return self.circuit._stretches_capture.get(name) + def use_var(self, var): if self.get_var(var.name) != var: raise CircuitError(f"'{var}' is not present in this circuit") + def use_stretch(self, stretch): + if self.get_stretch(stretch.name) != stretch: + raise CircuitError(f"'{stretch}' is not present in this circuit") + def use_qubit(self, qubit): # Since the qubit is guaranteed valid, there's nothing for us to do. pass @@ -6861,11 +7179,16 @@ def _validate_expr(circuit_scope: CircuitScopeInterface, node: expr.Expr) -> exp # This takes the `circuit_scope` object as an argument rather than being a circuit method and # inferring it because we may want to call this several times, and we almost invariably already # need the interface implementation for something else anyway. - for var in set(expr.iter_vars(node)): - if var.standalone: - circuit_scope.use_var(var) + # If we're not in a capturing scope (i.e. we're in the root scope), then the + # `use_{var,stretch}` calls are no-ops. + for ident in set(expr.iter_identifiers(node)): + if isinstance(ident, expr.Stretch): + circuit_scope.use_stretch(ident) else: - circuit_scope.resolve_classical_resource(var.var) + if ident.standalone: + circuit_scope.use_var(ident) + else: + circuit_scope.resolve_classical_resource(ident.var) return node @@ -6951,14 +7274,20 @@ def _copy_metadata(original, cpy, vars_mode): cpy._vars_local = original._vars_local.copy() cpy._vars_input = original._vars_input.copy() cpy._vars_capture = original._vars_capture.copy() + cpy._stretches_local = original._stretches_local.copy() + cpy._stretches_capture = original._stretches_capture.copy() elif vars_mode == "captures": cpy._vars_local = {} cpy._vars_input = {} cpy._vars_capture = {var.name: var for var in original.iter_vars()} + cpy._stretches_local = {} + cpy._stretches_capture = {stretch.name: stretch for stretch in original.iter_stretches()} elif vars_mode == "drop": cpy._vars_local = {} cpy._vars_input = {} cpy._vars_capture = {} + cpy._stretches_local = {} + cpy._stretches_capture = {} else: # pragma: no cover raise ValueError(f"unknown vars_mode: '{vars_mode}'") diff --git a/qiskit/converters/dag_to_circuit.py b/qiskit/converters/dag_to_circuit.py index 88b53ce73a78..2fb5e7a6c677 100644 --- a/qiskit/converters/dag_to_circuit.py +++ b/qiskit/converters/dag_to_circuit.py @@ -66,10 +66,12 @@ def dag_to_circuit(dag, copy_operations=True): name=name, global_phase=dag.global_phase, inputs=dag.iter_input_vars(), - captures=dag.iter_captured_vars(), + captures=dag.iter_captures(), ) for var in dag.iter_declared_vars(): circuit.add_uninitialized_var(var) + for stretch in dag.iter_declared_stretches(): + circuit.add_stretch(stretch) circuit.metadata = dag.metadata or {} circuit._data = circuit_data diff --git a/qiskit/qasm3/ast.py b/qiskit/qasm3/ast.py index 61e2b0009e23..24a6d149b5d6 100644 --- a/qiskit/qasm3/ast.py +++ b/qiskit/qasm3/ast.py @@ -181,12 +181,6 @@ class DurationType(ClassicalType): __slots__ = () -class StretchType(ClassicalType): - """Type information for a stretch.""" - - __slots__ = () - - class BitArrayType(ClassicalType): """Type information for a sized number of classical bits.""" @@ -414,6 +408,17 @@ def __init__(self, type_: ClassicalType, identifier: Identifier, initializer=Non self.initializer = initializer +class StretchDeclaration(Statement): + """Declaration of a stretch variable, optionally with a lower bound + expression.""" + + __slots__ = ("identifier", "bound") + + def __init__(self, identifier: Identifier, bound=None): + self.identifier = identifier + self.bound = bound + + class AssignmentStatement(Statement): """Assignment of an expression to an l-value.""" diff --git a/qiskit/qasm3/exporter.py b/qiskit/qasm3/exporter.py index 59873d6140bb..4969b7c2c898 100644 --- a/qiskit/qasm3/exporter.py +++ b/qiskit/qasm3/exporter.py @@ -61,7 +61,6 @@ from .exceptions import QASM3ExporterError from .printer import BasicPrinter - # Reserved keywords that gates and variables cannot be named. It is possible that some of these # _could_ be accepted as variable names by OpenQASM 3 parsers, but it's safer for us to just be very # conservative. @@ -627,7 +626,7 @@ def _lookup_bit(self, bit) -> ast.Identifier: def build_program(self): """Builds a Program""" circuit = self.scope.circuit - if circuit.num_captured_vars: + if circuit.num_captured_vars or circuit.num_captured_stretches: raise QASM3ExporterError( "cannot export an inner scope with captured variables as a top-level program" ) @@ -959,6 +958,14 @@ def build_current_scope(self) -> List[ast.Statement]: ) for var in self.scope.circuit.iter_declared_vars() ] + + for stretch in self.scope.circuit.iter_declared_stretches(): + statements.append( + ast.StretchDeclaration( + self.symbols.register_variable(stretch.name, stretch, allow_rename=True), + ) + ) + for instruction in self.scope.circuit.data: if isinstance(instruction.operation, ControlFlowOp): if isinstance(instruction.operation, ForLoopOp): @@ -1280,7 +1287,7 @@ class _ExprBuilder(expr.ExprVisitor[ast.Expression]): __slots__ = ("lookup",) # This is a very simple, non-contextual converter. As the type system expands, we may well end - # up with some places where Terra's abstract type system needs to be lowered to OQ3 rather than + # up with some places where Qiskit's abstract type system needs to be lowered to OQ3 rather than # mapping 100% directly, which might need a more contextual visitor. def __init__(self, lookup): @@ -1289,6 +1296,9 @@ def __init__(self, lookup): def visit_var(self, node, /): return self.lookup(node) if node.standalone else self.lookup(node.var) + def visit_stretch(self, node, /): + return self.lookup(node) + # pylint: disable=too-many-return-statements def visit_value(self, node, /): if node.type.kind is types.Bool: diff --git a/qiskit/qasm3/printer.py b/qiskit/qasm3/printer.py index d8730476926e..6554e052ef21 100644 --- a/qiskit/qasm3/printer.py +++ b/qiskit/qasm3/printer.py @@ -217,9 +217,6 @@ def _visit_BoolType(self, _node: ast.BoolType) -> None: def _visit_DurationType(self, _node: ast.DurationType) -> None: self.stream.write("duration") - def _visit_StretchType(self, _node: ast.StretchType) -> None: - self.stream.write("stretch") - def _visit_IntType(self, node: ast.IntType) -> None: self.stream.write("int") if node.size is not None: @@ -369,6 +366,16 @@ def _visit_ClassicalDeclaration(self, node: ast.ClassicalDeclaration) -> None: self.visit(node.initializer) self._end_statement() + def _visit_StretchDeclaration(self, node: ast.StretchDeclaration) -> None: + self._start_line() + self.stream.write("stretch") + self.stream.write(" ") + self.visit(node.identifier) + if node.bound is not None: + self.stream.write(" = ") + self.visit(node.bound) + self._end_statement() + def _visit_AssignmentStatement(self, node: ast.AssignmentStatement) -> None: self._start_line() self.visit(node.lvalue) diff --git a/qiskit/qpy/__init__.py b/qiskit/qpy/__init__.py index dd58f2b9df41..6d7ae90547f9 100644 --- a/qiskit/qpy/__init__.py +++ b/qiskit/qpy/__init__.py @@ -382,8 +382,9 @@ def open(*args): Version 14 ---------- -Version 14 adds a new core DURATION type, as well as support for additional :class:`~.types.Type` -classes. +Version 14 adds a new core DURATION type, support for additional :class:`~.types.Type` +classes :class:`~.types.Float` and :class:`~.types.Duration`, and a new expression +node type :class:`~.expr.Stretch`. DURATION ~~~~~~~~ @@ -406,6 +407,34 @@ def open(*args): ============================== ========= ========================================================= +Changes to EXPR_VAR_DECLARATION +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +The ``EXPR_VAR_DECLARATION`` type is now used to represent both :class:`~.expr.Var` standalone +variables and :class:`~.expr.Stretch` identifiers. To support this change, the usage type code has +two new possible entries, in addition to the existing ones: + +========= ========================================================================================= +Type code Meaning +========= ========================================================================================= +``A`` A ``capture`` stretch to the circuit. + +``O`` A locally declared stretch to the circuit. + +========= ========================================================================================= + +Changes to EXPRESSION +--------------------- + +The EXPRESSION type code has a new possible entry, ``s``, corresponding to :class:`.expr.Stretch` +nodes. + +======================= ========= ====================================================== ======== +Qiskit class Type code Payload Children +======================= ========= ====================================================== ======== +:class:`~.expr.Stretch` ``s`` One ``unsigned short var_index`` 0 +======================= ========= ====================================================== ======== + Changes to EXPR_TYPE ~~~~~~~~~~~~~~~~~~~~ diff --git a/qiskit/qpy/binary_io/circuits.py b/qiskit/qpy/binary_io/circuits.py index f1ea9de43e25..2b3aa112517e 100644 --- a/qiskit/qpy/binary_io/circuits.py +++ b/qiskit/qpy/binary_io/circuits.py @@ -13,7 +13,7 @@ # pylint: disable=invalid-name """Binary IO for circuit objects.""" - +import itertools from collections import defaultdict import io import json @@ -1237,7 +1237,7 @@ def write_circuit( metadata_size=metadata_size, num_registers=num_registers, num_instructions=num_instructions, - num_vars=circuit.num_vars, + num_vars=circuit.num_identifiers, ) header = struct.pack(formats.CIRCUIT_HEADER_V12_PACK, *header_raw) file_obj.write(header) @@ -1338,7 +1338,7 @@ def read_circuit(file_obj, version, metadata_deserializer=None, use_symengine=Fa num_clbits = header["num_clbits"] num_registers = header["num_registers"] num_instructions = header["num_instructions"] - num_vars = header.get("num_vars", 0) + num_identifiers = header.get("num_vars", 0) # `out_registers` is two "name: register" maps segregated by type for the rest of QPY, and # `all_registers` is the complete ordered list used to construct the `QuantumCircuit`. out_registers = {"q": {}, "c": {}} @@ -1395,7 +1395,7 @@ def read_circuit(file_obj, version, metadata_deserializer=None, use_symengine=Fa "q": [Qubit() for _ in out_bits["q"]], "c": [Clbit() for _ in out_bits["c"]], } - var_segments, standalone_var_indices = value.read_standalone_vars(file_obj, num_vars) + var_segments, standalone_var_indices = value.read_standalone_vars(file_obj, num_identifiers) circ = QuantumCircuit( out_bits["q"], out_bits["c"], @@ -1404,10 +1404,15 @@ def read_circuit(file_obj, version, metadata_deserializer=None, use_symengine=Fa global_phase=global_phase, metadata=metadata, inputs=var_segments[type_keys.ExprVarDeclaration.INPUT], - captures=var_segments[type_keys.ExprVarDeclaration.CAPTURE], + captures=itertools.chain( + var_segments[type_keys.ExprVarDeclaration.CAPTURE], + var_segments[type_keys.ExprVarDeclaration.STRETCH_CAPTURE], + ), ) for declaration in var_segments[type_keys.ExprVarDeclaration.LOCAL]: circ.add_uninitialized_var(declaration) + for stretch in var_segments[type_keys.ExprVarDeclaration.STRETCH_LOCAL]: + circ.add_stretch(stretch) custom_operations = _read_custom_operations(file_obj, version, vectors) for _instruction in range(num_instructions): _read_instruction( diff --git a/qiskit/qpy/binary_io/value.py b/qiskit/qpy/binary_io/value.py index ef9c5d1dab9b..f7ef0a9514ed 100644 --- a/qiskit/qpy/binary_io/value.py +++ b/qiskit/qpy/binary_io/value.py @@ -299,6 +299,16 @@ def visit_var(self, node, /): else: raise exceptions.QpyError(f"unhandled Var object '{node.var}'") + def visit_stretch(self, node, /): + self.file_obj.write(type_keys.Expression.STRETCH) + self._write_expr_type(node.type) + self.file_obj.write( + struct.pack( + formats.EXPRESSION_STRETCH_PACK, + *formats.EXPRESSION_STRETCH(self.standalone_var_indices[node]), + ) + ) + def visit_value(self, node, /): self.file_obj.write(type_keys.Expression.VALUE) self._write_expr_type(node.type) @@ -720,6 +730,13 @@ def _read_expr( name = file_obj.read(payload.reg_name_size).decode(common.ENCODE) return expr.Var(cregs[name], type_) raise exceptions.QpyError("Invalid classical-expression Var key '{var_type_key}'") + if type_key == type_keys.Expression.STRETCH: + payload = formats.EXPRESSION_STRETCH._make( + struct.unpack( + formats.EXPRESSION_STRETCH_PACK, file_obj.read(formats.EXPRESSION_STRETCH_SIZE) + ) + ) + return standalone_vars[payload.var_index] if type_key == type_keys.Expression.VALUE: value_type_key = file_obj.read(formats.EXPR_VALUE_DISCRIMINATOR_SIZE) if value_type_key == type_keys.ExprValue.BOOL: @@ -850,6 +867,8 @@ def read_standalone_vars(file_obj, num_vars): type_keys.ExprVarDeclaration.INPUT: [], type_keys.ExprVarDeclaration.CAPTURE: [], type_keys.ExprVarDeclaration.LOCAL: [], + type_keys.ExprVarDeclaration.STRETCH_CAPTURE: [], + type_keys.ExprVarDeclaration.STRETCH_LOCAL: [], } var_order = [] for _ in range(num_vars): @@ -861,7 +880,13 @@ def read_standalone_vars(file_obj, num_vars): ) type_ = _read_expr_type(file_obj) name = file_obj.read(data.name_size).decode(common.ENCODE) - var = expr.Var(uuid.UUID(bytes=data.uuid_bytes), type_, name=name) + if data.usage in { + type_keys.ExprVarDeclaration.STRETCH_CAPTURE, + type_keys.ExprVarDeclaration.STRETCH_LOCAL, + }: + var = expr.Stretch(uuid.UUID(bytes=data.uuid_bytes), name) + else: + var = expr.Var(uuid.UUID(bytes=data.uuid_bytes), type_, name=name) read_vars[data.usage].append(var) var_order.append(var) return read_vars, var_order @@ -888,8 +913,8 @@ def write_standalone_vars(file_obj, circuit, version): version (int): the QPY target version. Returns: - dict[expr.Var, int]: a mapping of the variables written to the index that they were written - at. + dict[expr.Var | expr.Stretch, int]: a mapping of the variables written to the + index that they were written at. """ index = 0 out = {} @@ -905,6 +930,18 @@ def write_standalone_vars(file_obj, circuit, version): _write_standalone_var(file_obj, var, type_keys.ExprVarDeclaration.LOCAL, version) out[var] = index index += 1 + if version < 14 and circuit.num_stretches: + raise exceptions.UnsupportedFeatureForVersion( + "circuits containing stretch variables", required=14, target=version + ) + for var in circuit.iter_captured_stretches(): + _write_standalone_var(file_obj, var, type_keys.ExprVarDeclaration.STRETCH_CAPTURE, version) + out[var] = index + index += 1 + for var in circuit.iter_declared_stretches(): + _write_standalone_var(file_obj, var, type_keys.ExprVarDeclaration.STRETCH_LOCAL, version) + out[var] = index + index += 1 return out diff --git a/qiskit/qpy/formats.py b/qiskit/qpy/formats.py index e25c6a1afbfe..41f0246fc9db 100644 --- a/qiskit/qpy/formats.py +++ b/qiskit/qpy/formats.py @@ -357,6 +357,10 @@ EXPRESSION_BINARY_PACK = "!B" EXPRESSION_BINARY_SIZE = struct.calcsize(EXPRESSION_BINARY_PACK) +EXPRESSION_STRETCH = namedtuple("EXPRESSION_STRETCH", ["var_index"]) +EXPRESSION_STRETCH_PACK = "!H" +EXPRESSION_STRETCH_SIZE = struct.calcsize(EXPRESSION_STRETCH_PACK) + # EXPR_TYPE diff --git a/qiskit/qpy/type_keys.py b/qiskit/qpy/type_keys.py index b356bbe1a247..7c99124dbafd 100644 --- a/qiskit/qpy/type_keys.py +++ b/qiskit/qpy/type_keys.py @@ -247,6 +247,7 @@ class Expression(TypeKeyBase): """Type keys for the ``EXPRESSION`` QPY item.""" VAR = b"x" + STRETCH = b"s" VALUE = b"v" CAST = b"c" UNARY = b"u" @@ -273,6 +274,8 @@ class ExprVarDeclaration(TypeKeyBase): INPUT = b"I" CAPTURE = b"C" LOCAL = b"L" + STRETCH_CAPTURE = b"A" + STRETCH_LOCAL = b"O" @classmethod def assign(cls, obj): diff --git a/qiskit/transpiler/passes/layout/apply_layout.py b/qiskit/transpiler/passes/layout/apply_layout.py index 7bd4ab714d83..c0181328601e 100644 --- a/qiskit/transpiler/passes/layout/apply_layout.py +++ b/qiskit/transpiler/passes/layout/apply_layout.py @@ -67,6 +67,10 @@ def run(self, dag): new_dag.add_captured_var(var) for var in dag.iter_declared_vars(): new_dag.add_declared_var(var) + for stretch in dag.iter_captured_stretches(): + new_dag.add_captured_stretch(stretch) + for stretch in dag.iter_declared_stretches(): + new_dag.add_declared_stretch(stretch) new_dag.metadata = dag.metadata new_dag.add_clbits(dag.clbits) for creg in dag.cregs.values(): diff --git a/qiskit/transpiler/passes/layout/sabre_layout.py b/qiskit/transpiler/passes/layout/sabre_layout.py index 4e23c86a9529..1cbe508fba15 100644 --- a/qiskit/transpiler/passes/layout/sabre_layout.py +++ b/qiskit/transpiler/passes/layout/sabre_layout.py @@ -315,6 +315,10 @@ def run(self, dag): mapped_dag.add_captured_var(var) for var in dag.iter_declared_vars(): mapped_dag.add_declared_var(var) + for stretch in dag.iter_captured_stretches(): + mapped_dag.add_captured_stretch(stretch) + for stretch in dag.iter_declared_stretches(): + mapped_dag.add_declared_stretch(stretch) mapped_dag.global_phase = dag.global_phase self.property_set["original_qubit_indices"] = { bit: index for index, bit in enumerate(dag.qubits) diff --git a/qiskit/transpiler/passes/optimization/contract_idle_wires_in_control_flow.py b/qiskit/transpiler/passes/optimization/contract_idle_wires_in_control_flow.py index 940319ce5fc3..c53ba14badc6 100644 --- a/qiskit/transpiler/passes/optimization/contract_idle_wires_in_control_flow.py +++ b/qiskit/transpiler/passes/optimization/contract_idle_wires_in_control_flow.py @@ -77,7 +77,7 @@ def contract(block): name=block.name, global_phase=block.global_phase, metadata=block.metadata, - captures=block.iter_captured_vars(), + captures=block.iter_captures(), ) out.add_bits( [ @@ -92,6 +92,8 @@ def contract(block): # Control-flow ops can only have captures and locals, and we already added the captures. for var in block.iter_declared_vars(): out.add_uninitialized_var(var) + for stretch in block.iter_declared_stretches(): + out.add_stretch(stretch) for inner in block: out._append(inner) return out diff --git a/releasenotes/notes/stretch-variables-076070c3f57cfa09.yaml b/releasenotes/notes/stretch-variables-076070c3f57cfa09.yaml new file mode 100644 index 000000000000..8501100c3309 --- /dev/null +++ b/releasenotes/notes/stretch-variables-076070c3f57cfa09.yaml @@ -0,0 +1,43 @@ +--- +prelude: > + This release adds support for ``stretch`` variables to :class:`.QuantumCircuit` + which are used to express relationships between instruction durations. For + example, in order to ensure a sequence of gates between two barriers will be + left-aligned, whatever their actual durations may be, we can do the following:: + + from qiskit import QuantumCircuit + from numpy import pi + + 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() + + For additional context and examples, refer to the + `OpenQASM 3 language specification. `__ + +features_circuits: + - | + A new expression node :class:`~.expr.Stretch` has been added to the classical expression + system to represent ``stretch`` variables. To create a new ``stretch` variable, you can + use :meth:`.QuantumCircuit.add_stretch`. The resulting expression is a constant + expression of type :class:`~.types.Duration`, which can currently be used as the ``duration`` + argument of a :meth:`~.QuantumCircuit.delay`. + + The :class:`~.expr.Stretch` expression is most similar to the existing :class:`~.expr.Var` + expression used to represent classical variables in a circuit, except it is constant and + is always of type :class:`~.types.Duration`. It can be used in other expressions (e.g. + you can multiply it by a numeric constant) and :class:`.QuantumCircuit` provides full + scoping support for it (e.g. it can be captured by or declared within a control flow + scope). diff --git a/test/python/circuit/classical/test_expr_helpers.py b/test/python/circuit/classical/test_expr_helpers.py index 5264e55a52da..6bddd81194f4 100644 --- a/test/python/circuit/classical/test_expr_helpers.py +++ b/test/python/circuit/classical/test_expr_helpers.py @@ -15,7 +15,7 @@ import copy import ddt -from qiskit.circuit import Clbit, ClassicalRegister +from qiskit.circuit import Clbit, ClassicalRegister, Duration from qiskit.circuit.classical import expr, types from test import QiskitTestCase # pylint: disable=wrong-import-order @@ -32,6 +32,7 @@ class TestStructurallyEquivalent(QiskitTestCase): expr.logic_and(expr.less(2, ClassicalRegister(3, "a")), expr.lift(Clbit())), expr.shift_left(expr.shift_right(255, 3), 3), expr.index(expr.Var.new("a", types.Uint(8)), 0), + expr.greater(expr.Stretch.new("a"), Duration.dt(100)), ) def test_equivalent_to_self(self, node): self.assertTrue(expr.structurally_equivalent(node, node)) diff --git a/test/python/circuit/test_circuit_load_from_qpy.py b/test/python/circuit/test_circuit_load_from_qpy.py index a08d425514dc..0bb44a7bcf82 100644 --- a/test/python/circuit/test_circuit_load_from_qpy.py +++ b/test/python/circuit/test_circuit_load_from_qpy.py @@ -1971,6 +1971,17 @@ def test_pre_v14_rejects_duration_typed_expr(self, version): ): dump(qc, fptr, version=version) + @ddt.idata(range(QPY_COMPATIBILITY_VERSION, 14)) + def test_pre_v14_rejects_stretch_expr(self, version): + """Test that dumping to older QPY versions rejects duration-typed expressions.""" + qc = QuantumCircuit() + qc.add_stretch("a") + with ( + io.BytesIO() as fptr, + self.assertRaisesRegex(UnsupportedFeatureForVersion, "version 14 is required.*stretch"), + ): + dump(qc, fptr, version=version) + class TestSymengineLoadFromQPY(QiskitTestCase): """Test use of symengine in qpy set of methods.""" diff --git a/test/python/circuit/test_circuit_operations.py b/test/python/circuit/test_circuit_operations.py index 4b73805d75f9..771c00ff6986 100644 --- a/test/python/circuit/test_circuit_operations.py +++ b/test/python/circuit/test_circuit_operations.py @@ -10,6 +10,7 @@ # copyright notice, and modified files need to carry a notice indicating # that they have been altered from the originals. +# pylint: disable=invalid-name """Test Qiskit's QuantumCircuit class.""" import copy @@ -417,11 +418,15 @@ def test_copy_variables(self): b = expr.Var.new("b", types.Uint(8)) c = expr.Var.new("c", types.Bool()) d = expr.Var.new("d", types.Uint(8)) + e = expr.Stretch.new("e") + f = expr.Stretch.new("f") qc = QuantumCircuit(inputs=[a], declarations=[(c, expr.lift(False))]) + qc.add_stretch(e) copied = qc.copy() self.assertEqual({a}, set(copied.iter_input_vars())) self.assertEqual({c}, set(copied.iter_declared_vars())) + self.assertEqual({e}, set(copied.iter_declared_stretches())) self.assertEqual( [instruction.operation for instruction in qc], [instruction.operation for instruction in copied.data], @@ -430,10 +435,13 @@ def test_copy_variables(self): # Check that the original circuit is not mutated. copied.add_input(b) copied.add_var(d, 0xFF) + copied.add_stretch(f) self.assertEqual({a, b}, set(copied.iter_input_vars())) self.assertEqual({c, d}, set(copied.iter_declared_vars())) + self.assertEqual({e, f}, set(copied.iter_declared_stretches())) self.assertEqual({a}, set(qc.iter_input_vars())) self.assertEqual({c}, set(qc.iter_declared_vars())) + self.assertEqual({e}, set(qc.iter_declared_stretches())) qc = QuantumCircuit(captures=[b], declarations=[(a, expr.lift(False)), (c, a)]) copied = qc.copy() @@ -446,9 +454,15 @@ def test_copy_variables(self): # Check that the original circuit is not mutated. copied.add_capture(d) + copied.add_stretch(f) self.assertEqual({b, d}, set(copied.iter_captured_vars())) + self.assertEqual({a, c}, set(copied.iter_declared_vars())) + self.assertEqual({f}, set(copied.iter_declared_stretches())) self.assertEqual({b}, set(qc.iter_captured_vars())) + self.assertEqual({a, c}, set(qc.iter_declared_vars())) + self.assertEqual(set(), set(qc.iter_declared_stretches())) + # pylint: disable=invalid-name def test_copy_empty_variables(self): """Test that an empty copy of circuits including variables copies them across, but does not initialise them.""" @@ -466,10 +480,13 @@ def test_copy_empty_variables(self): # Check that the original circuit is not mutated. copied.add_input(b) copied.add_var(d, 0xFF) + e = copied.add_stretch("e") self.assertEqual({a, b}, set(copied.iter_input_vars())) self.assertEqual({c, d}, set(copied.iter_declared_vars())) + self.assertEqual({e}, set(copied.iter_declared_stretches())) self.assertEqual({a}, set(qc.iter_input_vars())) self.assertEqual({c}, set(qc.iter_declared_vars())) + self.assertEqual(set(), set(qc.iter_declared_stretches())) qc = QuantumCircuit(captures=[b], declarations=[(a, expr.lift(False)), (c, a)]) copied = qc.copy_empty_like() @@ -479,9 +496,13 @@ def test_copy_empty_variables(self): # Check that the original circuit is not mutated. copied.add_capture(d) + copied.add_capture(e) self.assertEqual({b, d}, set(copied.iter_captured_vars())) + self.assertEqual({e}, set(copied.iter_captured_stretches())) self.assertEqual({b}, set(qc.iter_captured_vars())) + self.assertEqual(set(), set(qc.iter_captured_stretches())) + # pylint: disable=invalid-name def test_copy_empty_variables_alike(self): """Test that an empty copy of circuits including variables copies them across, but does not initialise them. This is the same as the default, just spelled explicitly.""" @@ -489,6 +510,7 @@ def test_copy_empty_variables_alike(self): b = expr.Var.new("b", types.Uint(8)) c = expr.Var.new("c", types.Bool()) d = expr.Var.new("d", types.Uint(8)) + e = expr.Stretch.new("e") qc = QuantumCircuit(inputs=[a], declarations=[(c, expr.lift(False))]) copied = qc.copy_empty_like(vars_mode="alike") @@ -499,10 +521,13 @@ def test_copy_empty_variables_alike(self): # Check that the original circuit is not mutated. copied.add_input(b) copied.add_var(d, 0xFF) + copied.add_stretch(e) self.assertEqual({a, b}, set(copied.iter_input_vars())) self.assertEqual({c, d}, set(copied.iter_declared_vars())) + self.assertEqual({e}, set(copied.iter_declared_stretches())) self.assertEqual({a}, set(qc.iter_input_vars())) self.assertEqual({c}, set(qc.iter_declared_vars())) + self.assertEqual(set(), set(qc.iter_declared_stretches())) qc = QuantumCircuit(captures=[b], declarations=[(a, expr.lift(False)), (c, a)]) copied = qc.copy_empty_like(vars_mode="alike") @@ -512,9 +537,12 @@ def test_copy_empty_variables_alike(self): # Check that the original circuit is not mutated. copied.add_capture(d) + copied.add_capture(e) self.assertEqual({b, d}, set(copied.iter_captured_vars())) + self.assertEqual({e}, set(copied.iter_captured_stretches())) self.assertEqual({b}, set(qc.iter_captured_vars())) + # pylint: disable=invalid-name def test_copy_empty_variables_to_captures(self): """``vars_mode="captures"`` should convert all variables to captures.""" a = expr.Var.new("a", types.Bool()) @@ -523,15 +551,20 @@ def test_copy_empty_variables_to_captures(self): d = expr.Var.new("d", types.Uint(8)) qc = QuantumCircuit(inputs=[a, b], declarations=[(c, expr.lift(False))]) + e = qc.add_stretch("e") copied = qc.copy_empty_like(vars_mode="captures") self.assertEqual({a, b, c}, set(copied.iter_captured_vars())) + self.assertEqual({e}, set(copied.iter_captured_stretches())) self.assertEqual({a, b, c}, set(copied.iter_vars())) + self.assertEqual({e}, set(copied.iter_stretches())) self.assertEqual([], list(copied.data)) - qc = QuantumCircuit(captures=[c, d]) + qc = QuantumCircuit(captures=[c, d, e]) copied = qc.copy_empty_like(vars_mode="captures") self.assertEqual({c, d}, set(copied.iter_captured_vars())) + self.assertEqual({e}, set(copied.iter_captured_stretches())) self.assertEqual({c, d}, set(copied.iter_vars())) + self.assertEqual({e}, set(copied.iter_stretches())) self.assertEqual([], list(copied.data)) def test_copy_empty_variables_drop(self): @@ -539,10 +572,12 @@ def test_copy_empty_variables_drop(self): a = expr.Var.new("a", types.Bool()) b = expr.Var.new("b", types.Uint(8)) c = expr.Var.new("c", types.Bool()) + d = expr.Stretch.new("s") - qc = QuantumCircuit(inputs=[a, b], declarations=[(c, expr.lift(False))]) + qc = QuantumCircuit(captures=[a, b, d], declarations=[(c, expr.lift(False))]) copied = qc.copy_empty_like(vars_mode="drop") self.assertEqual(set(), set(copied.iter_vars())) + self.assertEqual(set(), set(copied.iter_stretches())) self.assertEqual([], list(copied.data)) def test_copy_empty_like_parametric_phase(self): diff --git a/test/python/circuit/test_circuit_vars.py b/test/python/circuit/test_circuit_vars.py index f6916dcb72db..411529cba7ee 100644 --- a/test/python/circuit/test_circuit_vars.py +++ b/test/python/circuit/test_circuit_vars.py @@ -12,6 +12,8 @@ # pylint: disable=missing-module-docstring,missing-class-docstring,missing-function-docstring +import itertools + from test import QiskitTestCase from qiskit.circuit import QuantumCircuit, CircuitError, Clbit, ClassicalRegister, Store @@ -23,7 +25,10 @@ class TestCircuitVars(QiskitTestCase): tested in the suites of the specific methods.""" def test_initialise_inputs(self): - vars_ = [expr.Var.new("a", types.Bool()), expr.Var.new("b", types.Uint(16))] + vars_ = [ + expr.Var.new("a", types.Bool()), + expr.Var.new("b", types.Uint(16)), + ] qc = QuantumCircuit(inputs=vars_) self.assertEqual(set(vars_), set(qc.iter_vars())) self.assertEqual(qc.num_vars, len(vars_)) @@ -32,13 +37,23 @@ def test_initialise_inputs(self): self.assertEqual(qc.num_declared_vars, 0) def test_initialise_captures(self): - vars_ = [expr.Var.new("a", types.Bool()), expr.Var.new("b", types.Uint(16))] - qc = QuantumCircuit(captures=vars_) + vars_ = [ + expr.Var.new("a", types.Bool()), + expr.Var.new("b", types.Uint(16)), + ] + stretches_ = [ + expr.Stretch.new("c"), + ] + qc = QuantumCircuit(captures=itertools.chain(vars_, stretches_)) self.assertEqual(set(vars_), set(qc.iter_vars())) + self.assertEqual(set(stretches_), set(qc.iter_stretches())) self.assertEqual(qc.num_vars, len(vars_)) + self.assertEqual(qc.num_stretches, len(stretches_)) self.assertEqual(qc.num_input_vars, 0) self.assertEqual(qc.num_captured_vars, len(vars_)) + self.assertEqual(qc.num_captured_stretches, len(stretches_)) self.assertEqual(qc.num_declared_vars, 0) + self.assertEqual(qc.num_declared_stretches, 0) def test_initialise_declarations_iterable(self): vars_ = [ @@ -56,7 +71,10 @@ def test_initialise_declarations_iterable(self): (instruction.operation.name, instruction.operation.lvalue, instruction.operation.rvalue) for instruction in qc.data ] - self.assertEqual(operations, [("store", lvalue, rvalue) for lvalue, rvalue in vars_]) + self.assertEqual( + operations, + [("store", lvalue, rvalue) for lvalue, rvalue in vars_], + ) def test_initialise_declarations_mapping(self): # Dictionary iteration order is guaranteed to be insertion order. @@ -147,6 +165,12 @@ def test_add_var_returns_good_var(self): self.assertEqual(b.name, "b") self.assertEqual(b.type, types.Uint(8)) + def test_add_stretch_returns_good_var(self): + qc = QuantumCircuit() + a = qc.add_stretch("a") + self.assertEqual(a.name, "a") + self.assertEqual(a.type, types.Duration()) + def test_add_var_returns_input(self): """Test that the `Var` returned by `add_var` is the same as the input if `Var`.""" a = expr.Var.new("a", types.Bool()) @@ -154,6 +178,34 @@ def test_add_var_returns_input(self): a_other = qc.add_var(a, expr.lift(True)) self.assertIs(a, a_other) + def test_add_stretch_returns_input(self): + a = expr.Stretch.new("a") + qc = QuantumCircuit() + a_other = qc.add_stretch(a) + self.assertIs(a, a_other) + + def test_stretch_circuit_equality(self): + a = expr.Stretch.new("a") + b = expr.Stretch.new("b") + c = expr.Stretch.new("c") + + qc1 = QuantumCircuit(captures=[a, b, c]) + self.assertEqual(qc1, QuantumCircuit(captures=[a, b, c])) + self.assertNotEqual(qc1, QuantumCircuit(captures=[c, b, a])) + + qc2 = QuantumCircuit(captures=[a]) + qc2.add_stretch(b) + qc2.add_stretch(c) + self.assertNotEqual(qc1, qc2) + + qc1 = qc2.copy() + self.assertEqual(qc1, qc2) + + qc2 = QuantumCircuit(captures=[a]) + qc2.add_stretch(c) + qc2.add_stretch(b) + self.assertNotEqual(qc1, qc2) + def test_add_input_returns_good_var(self): qc = QuantumCircuit() a = qc.add_input("a", types.Bool()) @@ -174,18 +226,30 @@ def test_add_input_returns_input(self): def test_cannot_have_both_inputs_and_captures(self): a = expr.Var.new("a", types.Bool()) b = expr.Var.new("b", types.Bool()) + c = expr.Stretch.new("c") with self.assertRaisesRegex(CircuitError, "circuits with input.*cannot be closures"): QuantumCircuit(inputs=[a], captures=[b]) + with self.assertRaisesRegex(CircuitError, "circuits with input.*cannot be closures"): + QuantumCircuit(inputs=[a], captures=[c]) + qc = QuantumCircuit(inputs=[a]) with self.assertRaisesRegex(CircuitError, "circuits with input.*cannot be closures"): qc.add_capture(b) + qc = QuantumCircuit(inputs=[a]) + with self.assertRaisesRegex(CircuitError, "circuits with input.*cannot be closures"): + qc.add_capture(c) + qc = QuantumCircuit(captures=[a]) with self.assertRaisesRegex(CircuitError, "circuits to be enclosed.*cannot have input"): qc.add_input(b) + qc = QuantumCircuit(captures=[c]) + with self.assertRaisesRegex(CircuitError, "circuits to be enclosed.*cannot have input"): + qc.add_input(b) + def test_cannot_add_cyclic_declaration(self): a = expr.Var.new("a", types.Bool()) with self.assertRaisesRegex(CircuitError, "not present in this circuit"): @@ -214,12 +278,15 @@ def test_initialise_inputs_equal_to_add_input(self): def test_initialise_captures_equal_to_add_capture(self): a = expr.Var.new("a", types.Bool()) b = expr.Var.new("b", types.Uint(16)) + c = expr.Stretch.new("c") - qc_init = QuantumCircuit(captures=[a, b]) + qc_init = QuantumCircuit(captures=[a, b, c]) qc_manual = QuantumCircuit() qc_manual.add_capture(a) qc_manual.add_capture(b) + qc_manual.add_capture(c) self.assertEqual(list(qc_init.iter_vars()), list(qc_manual.iter_vars())) + self.assertEqual(list(qc_init.iter_stretches()), list(qc_manual.iter_stretches())) def test_initialise_declarations_equal_to_add_var(self): a = expr.Var.new("a", types.Bool()) @@ -281,6 +348,20 @@ def test_cannot_shadow_vars(self): with self.assertRaisesRegex(CircuitError, "already present"): QuantumCircuit(captures=[a], declarations=[(a, a_init)]) + def test_cannot_shadow_stretches(self): + """Test that exact duplicate ``Stretch`` nodes within different combinations of the inputs are + detected and rejected.""" + a = expr.Stretch.new("a") + with self.assertRaisesRegex(CircuitError, "already present"): + QuantumCircuit(captures=[a, a]) + qc = QuantumCircuit(captures=[a]) + with self.assertRaisesRegex(CircuitError, "already present"): + qc.add_stretch(a) + qc = QuantumCircuit() + with self.assertRaisesRegex(CircuitError, "already present"): + qc.add_stretch(a) + qc.add_stretch(a) + def test_cannot_shadow_names(self): """Test that exact duplicate ``Var`` nodes within different combinations of the inputs are detected and rejected.""" @@ -331,6 +412,26 @@ def test_cannot_shadow_names(self): with self.assertRaisesRegex(CircuitError, "its name shadows"): qc.add_var("a", expr.lift(0xFF)) + a_stretch1 = expr.Stretch.new("a") + a_stretch2 = expr.Stretch.new("a") + with self.assertRaisesRegex(CircuitError, "its name shadows"): + QuantumCircuit(captures=[a_stretch1, a_stretch2]) + with self.assertRaisesRegex(CircuitError, "its name shadows"): + QuantumCircuit(captures=[a_stretch1, a_bool1]) + with self.assertRaisesRegex(CircuitError, "its name shadows"): + QuantumCircuit(captures=[a_bool1, a_stretch1]) + with self.assertRaisesRegex(CircuitError, "its name shadows"): + QuantumCircuit(captures=[a_stretch1], declarations=[(a_bool1, a_bool_init)]) + qc = QuantumCircuit(declarations=[(a_bool1, a_bool_init)]) + with self.assertRaisesRegex(CircuitError, "its name shadows"): + qc.add_stretch(a_stretch1) + with self.assertRaisesRegex(CircuitError, "its name shadows"): + qc.add_stretch("a") + qc = QuantumCircuit() + qc.add_stretch("a") + with self.assertRaisesRegex(CircuitError, "its name shadows"): + qc.add_var("a", expr.lift(True)) + def test_cannot_add_vars_wrapping_clbits(self): a = expr.Var(Clbit(), types.Bool()) with self.assertRaisesRegex(CircuitError, "cannot add variables that wrap"): @@ -379,10 +480,29 @@ def test_get_var_success(self): self.assertIs(qc.get_var("a"), a) self.assertIs(qc.get_var("b"), b) - qc = QuantumCircuit(declarations={a: expr.lift(True), b: expr.Value(0xFF, types.Uint(8))}) + qc = QuantumCircuit( + inputs=[], + declarations={ + a: expr.lift(True), + b: expr.Value(0xFF, types.Uint(8)), + }, + ) self.assertIs(qc.get_var("a"), a) self.assertIs(qc.get_var("b"), b) + def test_get_stretch_success(self): + a = expr.Stretch.new("a") + b = expr.Stretch.new("b") + + qc = QuantumCircuit(captures=[a]) + qc.add_stretch(b) + self.assertIs(qc.get_stretch("a"), a) + self.assertIs(qc.get_stretch("b"), b) + + qc = QuantumCircuit(captures=[a, b]) + self.assertIs(qc.get_stretch("a"), a) + self.assertIs(qc.get_stretch("b"), b) + def test_get_var_missing(self): qc = QuantumCircuit() with self.assertRaises(KeyError): @@ -393,6 +513,16 @@ def test_get_var_missing(self): with self.assertRaises(KeyError): qc.get_var("b") + def test_get_stretch_missing(self): + qc = QuantumCircuit() + with self.assertRaises(KeyError): + qc.get_stretch("a") + + a = expr.Stretch.new("a") + qc.add_capture(a) + with self.assertRaises(KeyError): + qc.get_stretch("b") + def test_get_var_default(self): qc = QuantumCircuit() self.assertIs(qc.get_var("a", None), None) @@ -400,8 +530,18 @@ def test_get_var_default(self): missing = "default" a = expr.Var.new("a", types.Bool()) qc.add_input(a) - self.assertIs(qc.get_var("b", missing), missing) - self.assertIs(qc.get_var("b", a), a) + self.assertIs(qc.get_var("c", missing), missing) + self.assertIs(qc.get_var("c", a), a) + + def test_get_stretch_default(self): + qc = QuantumCircuit() + self.assertIs(qc.get_stretch("a", None), None) + + missing = "default" + a = expr.Stretch.new("a") + qc.add_stretch(a) + self.assertIs(qc.get_stretch("c", missing), missing) + self.assertIs(qc.get_stretch("c", a), a) def test_has_var(self): a = expr.Var.new("a", types.Bool()) @@ -416,3 +556,13 @@ def test_has_var(self): # When giving an `Var`, the match must be exact, not just the name. self.assertFalse(QuantumCircuit(inputs=[a]).has_var(expr.Var.new("a", types.Uint(8)))) self.assertFalse(QuantumCircuit(inputs=[a]).has_var(expr.Var.new("a", types.Bool()))) + self.assertFalse(QuantumCircuit(inputs=[a]).has_var(expr.Var.new("a", types.Float()))) + + def test_has_stretch(self): + a = expr.Stretch.new("a") + self.assertFalse(QuantumCircuit().has_stretch("a")) + self.assertTrue(QuantumCircuit(captures=[a]).has_stretch("a")) + self.assertTrue(QuantumCircuit(captures=[a]).has_stretch(a)) + + # When giving an `Stretch`, the match must be exact, not just the name. + self.assertFalse(QuantumCircuit(captures=[a]).has_stretch(expr.Stretch.new("a"))) diff --git a/test/python/circuit/test_compose.py b/test/python/circuit/test_compose.py index a0e3e5ee4eff..2a849154e670 100644 --- a/test/python/circuit/test_compose.py +++ b/test/python/circuit/test_compose.py @@ -845,22 +845,49 @@ def test_var_remap_to_avoid_collisions(self): self.assertEqual([a1, c], list(out.iter_input_vars())) self.assertEqual([a1, c], list(out.iter_vars())) + def test_stretch_remap_to_avoid_collisions(self): + """We can use `var_remap` to avoid a stretch collision.""" + a1 = expr.Stretch.new("a") + a2 = expr.Stretch.new("a") + b = expr.Stretch.new("b") + + base = QuantumCircuit(captures=[a1]) + other = QuantumCircuit(captures=[a2]) + + out = base.compose(other, var_remap={a2: b}) + self.assertEqual([a1, b], list(out.iter_captured_stretches())) + self.assertEqual([a1, b], list(out.iter_stretches())) + + out = base.compose(other, var_remap={"a": b}) + self.assertEqual([a1, b], list(out.iter_captured_stretches())) + self.assertEqual([a1, b], list(out.iter_stretches())) + + out = base.compose(other, var_remap={"a": "c"}) + self.assertTrue(out.has_stretch("c")) + c = out.get_stretch("c") + self.assertEqual(c.name, "c") + self.assertEqual([a1, c], list(out.iter_captured_stretches())) + self.assertEqual([a1, c], list(out.iter_stretches())) + def test_simple_inline_captures(self): """We should be able to inline captures onto other variables.""" a = expr.Var.new("a", types.Bool()) b = expr.Var.new("b", types.Bool()) c = expr.Var.new("c", types.Uint(8)) + d = expr.Stretch.new("d") base = QuantumCircuit(inputs=[a, b]) base.add_var(c, 255) + base.add_stretch(d) base.store(a, expr.logic_or(a, b)) - other = QuantumCircuit(captures=[a, b, c]) + other = QuantumCircuit(captures=[a, b, c, d]) other.store(c, 254) other.store(b, expr.logic_or(a, b)) new = base.compose(other, inline_captures=True) expected = QuantumCircuit(inputs=[a, b]) expected.add_var(c, 255) + expected.add_stretch(d) expected.store(a, expr.logic_or(a, b)) expected.store(c, 254) expected.store(b, expr.logic_or(a, b)) @@ -920,10 +947,13 @@ def test_cannot_mix_inputs_and_captures(self): """The rules about mixing `input` and `capture` vars should still apply.""" a = expr.Var.new("a", types.Bool()) b = expr.Var.new("b", types.Uint(8)) + c = expr.Stretch.new("c") with self.assertRaisesRegex(CircuitError, "circuits with input variables cannot be"): QuantumCircuit(inputs=[a]).compose(QuantumCircuit(captures=[b])) with self.assertRaisesRegex(CircuitError, "circuits to be enclosed with captures cannot"): QuantumCircuit(captures=[a]).compose(QuantumCircuit(inputs=[b])) + with self.assertRaisesRegex(CircuitError, "circuits with input variables cannot be"): + QuantumCircuit(inputs=[a]).compose(QuantumCircuit(captures=[c])) def test_reject_var_naming_collision(self): """We can't have multiple vars with the same name.""" @@ -932,14 +962,23 @@ def test_reject_var_naming_collision(self): b = expr.Var.new("b", types.Bool()) self.assertNotEqual(a1, a2) + s1 = expr.Stretch.new("s") + s2 = expr.Stretch.new("s") + t = expr.Stretch.new("t") + self.assertNotEqual(s1, s2) + with self.assertRaisesRegex(CircuitError, "cannot add.*shadows"): QuantumCircuit(inputs=[a1]).compose(QuantumCircuit(inputs=[a2])) + with self.assertRaisesRegex(CircuitError, "cannot add.*shadows"): + QuantumCircuit(captures=[s1]).compose(QuantumCircuit(captures=[s2])) with self.assertRaisesRegex(CircuitError, "cannot add.*shadows"): QuantumCircuit(captures=[a1]).compose(QuantumCircuit(declarations=[(a2, False)])) with self.assertRaisesRegex(CircuitError, "cannot add.*shadows"): QuantumCircuit(declarations=[(a1, True)]).compose( QuantumCircuit(inputs=[b]), var_remap={b: a2} ) + with self.assertRaisesRegex(CircuitError, "cannot add.*shadows"): + QuantumCircuit(captures=[s1]).compose(QuantumCircuit(captures=[t]), var_remap={t: s2}) def test_reject_remap_var_to_bad_type(self): """Can't map a var to a different type.""" @@ -952,6 +991,17 @@ def test_reject_remap_var_to_bad_type(self): with self.assertRaisesRegex(CircuitError, "mismatched types"): QuantumCircuit().compose(qc, var_remap={b: a}) + def test_reject_remap_identifier_to_wrong_kind(self): + """Can't map a var to stretch or stretch to var.""" + a = expr.Var.new("a", types.Bool()) + b = expr.Stretch.new("b") + qc = QuantumCircuit(inputs=[a]) + with self.assertRaisesRegex(CircuitError, "mismatched identifier"): + QuantumCircuit().compose(qc, var_remap={a: b}) + qc = QuantumCircuit(captures=[b]) + with self.assertRaisesRegex(CircuitError, "mismatched identifier"): + QuantumCircuit().compose(qc, var_remap={b: a}) + def test_reject_inlining_missing_var(self): """Can't inline a var that doesn't exist.""" a = expr.Var.new("a", types.Bool()) @@ -965,6 +1015,19 @@ def test_reject_inlining_missing_var(self): with self.assertRaisesRegex(CircuitError, "Replacement '.*' for variable '.*' is not in"): QuantumCircuit(inputs=[a]).compose(qc, var_remap={a: b}, inline_captures=True) + def test_reject_inlining_missing_stretch(self): + """Can't inline a var that doesn't exist.""" + a = expr.Stretch.new("a") + b = expr.Stretch.new("b") + qc = QuantumCircuit(captures=[a]) + with self.assertRaisesRegex(CircuitError, "Variable '.*' to be inlined is not in the base"): + QuantumCircuit().compose(qc, inline_captures=True) + + # 'a' _would_ be present, except we also say to remap it before attempting the inline. + qc = QuantumCircuit(captures=[a]) + with self.assertRaisesRegex(CircuitError, "Replacement '.*' for variable '.*' is not in"): + QuantumCircuit().compose(qc, var_remap={a: b}, inline_captures=True) + if __name__ == "__main__": unittest.main() diff --git a/test/python/circuit/test_control_flow.py b/test/python/circuit/test_control_flow.py index 01693d9787ac..2f97429ff63b 100644 --- a/test/python/circuit/test_control_flow.py +++ b/test/python/circuit/test_control_flow.py @@ -1084,80 +1084,83 @@ def test_can_add_op_with_captures_of_captures(self): """Test circuit methods can capture captured variables.""" outer = QuantumCircuit(1, 1) a = expr.Var.new("a", types.Bool()) + b = expr.Stretch.new("b") outer.add_capture(a) + outer.add_capture(b) - inner = QuantumCircuit(1, 1, captures=[a]) + inner = QuantumCircuit(1, 1, captures=[a, b]) outer.if_test((outer.clbits[0], False), inner.copy(), [0], [0]) added = outer.data[-1].operation self.assertEqual(added.name, "if_else") - self.assertEqual(set(added.blocks[0].iter_captured_vars()), {a}) + self.assertEqual(set(added.blocks[0].iter_captures()), {a, b}) outer.if_else((outer.clbits[0], False), inner.copy(), inner.copy(), [0], [0]) added = outer.data[-1].operation self.assertEqual(added.name, "if_else") - self.assertEqual(set(added.blocks[0].iter_captured_vars()), {a}) - self.assertEqual(set(added.blocks[1].iter_captured_vars()), {a}) + self.assertEqual(set(added.blocks[0].iter_captures()), {a, b}) + self.assertEqual(set(added.blocks[1].iter_captures()), {a, b}) outer.while_loop((outer.clbits[0], False), inner.copy(), [0], [0]) added = outer.data[-1].operation self.assertEqual(added.name, "while_loop") - self.assertEqual(set(added.blocks[0].iter_captured_vars()), {a}) + self.assertEqual(set(added.blocks[0].iter_captures()), {a, b}) outer.for_loop(range(3), None, inner.copy(), [0], [0]) added = outer.data[-1].operation self.assertEqual(added.name, "for_loop") - self.assertEqual(set(added.blocks[0].iter_captured_vars()), {a}) + self.assertEqual(set(added.blocks[0].iter_captures()), {a, b}) outer.switch(outer.clbits[0], [(False, inner.copy()), (True, inner.copy())], [0], [0]) added = outer.data[-1].operation self.assertEqual(added.name, "switch_case") - self.assertEqual(set(added.blocks[0].iter_captured_vars()), {a}) - self.assertEqual(set(added.blocks[1].iter_captured_vars()), {a}) + self.assertEqual(set(added.blocks[0].iter_captures()), {a, b}) + self.assertEqual(set(added.blocks[1].iter_captures()), {a, b}) outer.box(inner.copy(), [0], [0]) added = outer.data[-1].operation self.assertEqual(added.name, "box") - self.assertEqual(set(added.blocks[0].iter_captured_vars()), {a}) + self.assertEqual(set(added.blocks[0].iter_captures()), {a, b}) def test_can_add_op_with_captures_of_locals(self): """Test circuit methods can capture declared variables.""" outer = QuantumCircuit(1, 1) a = outer.add_var("a", expr.lift(True)) + b = outer.add_stretch("b") - inner = QuantumCircuit(1, 1, captures=[a]) + inner = QuantumCircuit(1, 1, captures=[a, b]) outer.if_test((outer.clbits[0], False), inner.copy(), [0], [0]) added = outer.data[-1].operation self.assertEqual(added.name, "if_else") - self.assertEqual(set(added.blocks[0].iter_captured_vars()), {a}) + self.assertEqual(set(added.blocks[0].iter_captures()), {a, b}) outer.if_else((outer.clbits[0], False), inner.copy(), inner.copy(), [0], [0]) added = outer.data[-1].operation self.assertEqual(added.name, "if_else") - self.assertEqual(set(added.blocks[0].iter_captured_vars()), {a}) - self.assertEqual(set(added.blocks[1].iter_captured_vars()), {a}) + self.assertEqual(set(added.blocks[0].iter_captures()), {a, b}) + self.assertEqual(set(added.blocks[1].iter_captures()), {a, b}) outer.while_loop((outer.clbits[0], False), inner.copy(), [0], [0]) added = outer.data[-1].operation self.assertEqual(added.name, "while_loop") - self.assertEqual(set(added.blocks[0].iter_captured_vars()), {a}) + self.assertEqual(set(added.blocks[0].iter_captures()), {a, b}) outer.for_loop(range(3), None, inner.copy(), [0], [0]) added = outer.data[-1].operation self.assertEqual(added.name, "for_loop") - self.assertEqual(set(added.blocks[0].iter_captured_vars()), {a}) + self.assertEqual(set(added.blocks[0].iter_captures()), {a, b}) outer.switch(outer.clbits[0], [(False, inner.copy()), (True, inner.copy())], [0], [0]) added = outer.data[-1].operation self.assertEqual(added.name, "switch_case") - self.assertEqual(set(added.blocks[0].iter_captured_vars()), {a}) - self.assertEqual(set(added.blocks[1].iter_captured_vars()), {a}) + self.assertEqual(set(added.blocks[0].iter_captures()), {a, b}) + self.assertEqual(set(added.blocks[1].iter_captures()), {a, b}) outer.box(inner.copy(), [0], [0]) added = outer.data[-1].operation self.assertEqual(added.name, "box") - self.assertEqual(set(added.blocks[0].iter_captured_vars()), {a}) + self.assertEqual(set(added.blocks[0].iter_captures()), {a, b}) def test_cannot_capture_unknown_variables_methods(self): """Control-flow operations should not be able to capture variables that don't exist in the @@ -1167,6 +1170,25 @@ def test_cannot_capture_unknown_variables_methods(self): a = expr.Var.new("a", types.Bool()) inner = QuantumCircuit(1, 1, captures=[a]) + with self.assertRaisesRegex(CircuitError, "not in this circuit"): + outer.if_test((outer.clbits[0], False), inner.copy(), [0], [0]) + with self.assertRaisesRegex(CircuitError, "not in this circuit"): + outer.if_else((outer.clbits[0], False), inner.copy(), inner.copy(), [0], [0]) + with self.assertRaisesRegex(CircuitError, "not in this circuit"): + outer.while_loop((outer.clbits[0], False), inner.copy(), [0], [0]) + with self.assertRaisesRegex(CircuitError, "not in this circuit"): + outer.for_loop(range(3), None, inner.copy(), [0], [0]) + with self.assertRaisesRegex(CircuitError, "not in this circuit"): + outer.switch(outer.clbits[0], [(False, inner.copy()), (True, inner.copy())], [0], [0]) + + def test_cannot_capture_unknown_stretches_methods(self): + """Control-flow operations should not be able to capture stretches that don't exist in the + outer circuit.""" + outer = QuantumCircuit(1, 1) + + a = expr.Stretch.new("b") + inner = QuantumCircuit(1, 1, captures=[a]) + with self.assertRaisesRegex(CircuitError, "not in this circuit"): outer.if_test((outer.clbits[0], False), inner.copy(), [0], [0]) with self.assertRaisesRegex(CircuitError, "not in this circuit"): @@ -1204,3 +1226,28 @@ def test_cannot_capture_unknown_variables_append(self): ) with self.assertRaisesRegex(CircuitError, "not in this circuit"): outer.append(BoxOp(inner.copy()), [0], [0]) + + def test_cannot_capture_unknown_stretches_append(self): + """Control-flow operations should not be able to capture stretches that don't exist in the + outer circuit.""" + outer = QuantumCircuit(1, 1) + + a = expr.Stretch.new("a") + inner = QuantumCircuit(1, 1, captures=[a]) + + with self.assertRaisesRegex(CircuitError, "not in this circuit"): + outer.append(IfElseOp((outer.clbits[0], False), inner.copy(), None), [0], [0]) + with self.assertRaisesRegex(CircuitError, "not in this circuit"): + outer.append(IfElseOp((outer.clbits[0], False), inner.copy(), inner.copy()), [0], [0]) + with self.assertRaisesRegex(CircuitError, "not in this circuit"): + outer.append(WhileLoopOp((outer.clbits[0], False), inner.copy()), [0], [0]) + with self.assertRaisesRegex(CircuitError, "not in this circuit"): + outer.append(ForLoopOp(range(3), None, inner.copy()), [0], [0]) + with self.assertRaisesRegex(CircuitError, "not in this circuit"): + outer.append( + SwitchCaseOp(outer.clbits[0], [(False, inner.copy()), (True, inner.copy())]), + [0], + [0], + ) + with self.assertRaisesRegex(CircuitError, "not in this circuit"): + outer.append(BoxOp(inner.copy()), [0], [0]) diff --git a/test/python/compiler/test_transpiler.py b/test/python/compiler/test_transpiler.py index 7aa98bd00d62..04926d79cd05 100644 --- a/test/python/compiler/test_transpiler.py +++ b/test/python/compiler/test_transpiler.py @@ -2133,6 +2133,7 @@ def _standalone_var_circuit(self): qc = QuantumCircuit(5, 5, inputs=[a]) qc.add_var(b, 12) + qc.add_stretch("d") qc.h(0) qc.cx(0, 1) qc.measure([0, 1], [0, 1]) diff --git a/test/python/qasm3/test_export.py b/test/python/qasm3/test_export.py index 4f15c1521abd..84a61dacd2a1 100644 --- a/test/python/qasm3/test_export.py +++ b/test/python/qasm3/test_export.py @@ -1828,6 +1828,7 @@ def test_var_use(self): # All inputs should come first, regardless of declaration order. qc.add_input("d", types.Bool()) qc.add_var("e", expr.lift(7.5)) + qc.add_stretch("f") expected = """\ OPENQASM 3.0; @@ -1837,6 +1838,7 @@ def test_var_use(self): input bool d; uint[8] c; float[64] e; +stretch f; a = !a; b = b & 8; c = ~b; diff --git a/test/qpy_compat/test_qpy.py b/test/qpy_compat/test_qpy.py index 6c6c7157f6c0..ec4502109fd0 100755 --- a/test/qpy_compat/test_qpy.py +++ b/test/qpy_compat/test_qpy.py @@ -15,6 +15,7 @@ """Test cases to verify qpy backwards compatibility.""" import argparse + import itertools import random import re @@ -847,7 +848,8 @@ def generate_replay_with_expression_substitutions(): def generate_v14_expr(): """Circuits that contain expressions and types new in QPY v14.""" - from qiskit.circuit.classical import expr, types + import uuid + from qiskit.circuit.classical import expr from qiskit.circuit import Duration float_expr = QuantumCircuit(name="float_expr") @@ -884,7 +886,18 @@ def generate_v14_expr(): ): pass - return [float_expr, duration_expr, math_expr] + stretch_expr = QuantumCircuit(name="stretch_expr") + s = expr.Stretch(uuid.UUID(bytes=b"hello, qpy world", version=4), "a") + stretch = stretch_expr.add_stretch(s) + with stretch_expr.if_test(expr.equal(stretch, Duration.dt(100))): + pass + + return [ + float_expr, + duration_expr, + math_expr, + stretch_expr, + ] def generate_box():