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():