Skip to content

Commit

Permalink
[Stretch] Add stretch variable support to QuantumCircuit. (#13852)
Browse files Browse the repository at this point in the history
* WIP

* Add try_const to lift.

* Try multiple singletons, new one for const.

* Revert "Try multiple singletons, new one for const."

This reverts commit e2b3221.

* Remove Bool singleton test.

* Add const handling for stores, fix test bugs.

* Fix formatting.

* Remove Duration and Stretch for now.

* Cleanup, fix const bug in index.

* Fix ordering issue for types with differing const-ness.

Types that have some natural order no longer have an ordering
when one of them is strictly greater but has an incompatible
const-ness (i.e. when the greater type is const but the other
type is not).

* Fix QPY serialization.

We need to reject types with const=True in QPY until it supports them.

For now, I've also made the Index and shift operator constructors
lift their RHS to the same const-ness as the target to make it
less likely that existing users of expr run into issues when
serializing to older QPY versions.

* Make expr.Lift default to non-const.

This is probably a better default in general, since we
don't really have much use for const except for timing
stuff.

* Revert to old test_expr_constructors.py.

* Make binary_logical lift independent again.

Since we're going for using a Cast node when const-ness
differs, this will be fine.

* Update tests, handle a few edge cases.

* Fix docstring.

* Remove now redundant arg from tests.

* Add const testing for ordering.

* Add const tests for shifts.

* Add release note.

* Add const store tests.

* Address lint, minor cleanup.

* Add Float type to classical expressions.

* Allow DANGEROUS conversion from Float to Bool.

I wasn't going to have this, but since we have DANGEROUS
Float => Int, and we have Int => Bool, I think this makes the
most sense.

* Test Float ordering.

* Improve error messages for using Float with logical operators.

* Float tests for constructors.

* Add release note.

* Add Duration and Stretch classical types.

A Stretch can always represent a Duration (it's just an expression
without any unresolved stretch variables, in this case), so we
allow implicit conversion from Duration => Stretch.

The reason for a separate Duration type is to support things like
Duration / Duration => Float. This is not valid for stretches in
OpenQASM (to my knowledge).

* Add Duration type to qiskit.circuit.
Also adds support to expr.lift to create a value expression of
type types.Duration from an instance of qiskit.circuit.Duration.

* Block Stretch from use in binary relations.

* Add type ordering tests for Duration and Stretch.

Also improves testing for other types.

* Test expr constructors for Duration and Stretch.

* Fix lint.

* Implement operators +, -, *, /.

* Add expression tests for arithmetic ops.

* Implement stretch support for circuit.

* Initial support for writing QASM3.

* Reject const vars in add_var and add_input.

Also removes the assumption that a const-type can never be an l-value
in favor of just restricting l-values with const types from being
added to circuits for now.

We will (in a separate PR) add support for adding stretch variables
to circuits, which are const. However, we may track those
differently, or at least not report them as variable when users
query the circuit for variables.

* Implement QPY support for const-typed expressions.

* Remove invalid test.

This one I'd added thinking I ought to block store from using
a const var target. But since I figured it's better to just
restrict adding vars to the circuit that are const (and leave
the decision of whether or not a const var can be an l-value
till later), this test no longer makes sense.

* Update QPY version 14 desc.

* Fix lint.

* Add serialization testing.

* Test pre-v14 QPY rejects const-typed exprs.

* QASM export for floats.

* QPY support for floats.

* Fix lint.

* Settle on Duration circuit core type.

* QPY serialization for durations and stretches.

* Add QPY testing.

I can't really test Stretch yet since we can't add them to circuits
until a later PR.

* QASM support for stretch and duration.

The best I can do to test these right now (before Delay
is made to accept timing expressions) is to use them in
comparison operations (will be in a follow up commit).

* Fix lint.

* Add arithmetic operators to QASM.

* QPY testing for arithmetic operations.

* QASM testing for arithmetic operations.

* Update tests for blocked const vars.

We decided to punt on the idea of assigning const-typed variables,
so we don't support declaring stretch expressions via add_var or
the 'declarations' argument to QuantumCircuit. We also don't
allow const 'inputs'.

* Only test declare stretch QASM; fix lint.

* Special case for stretch in add_uninitialized_var.

At the moment, we track stretches as variables, but these
will eventually make their way here during circuit copy
etc., so we need to support them even though they're
const.

* Remove outdated docstring comment.

* Remove outdated comment.

* Don't use match since we still support Python 3.9.

* Block const stores.

* Fix enum match.

* Revert visitors.py.

* Address review comments.

* Improve type docs.

* Revert QPY, since the old format can support constexprs.

By making const-ness a property of expressions, we don't need
any special serialization in QPY. That's because we assume that
all `Var` expressions are non-const, and all `Value` expressions
are const. And the const-ness of any expression is defined by
the const-ness of its operands, e.g. when QPY reconstructs a
binary operand, the constructed expression's `const` attribute
gets set to `True` if both of the operands are `const`, which
ultimately flows bottom-up from the `Var` and `Value` leaf nodes.

* Move const-ness from Type to Expr.

* Revert QPY testing, no longer needed.

* Add explicit validation of const expr.

* Revert stuff I didn't need to touch.

* Update release note.

* A few finishing touches.

* Fix-up after merge.

* Fix-ups after merge.

* Fix lint.

* Fix comment and release note.

* Fixes after merge.

* Fix test.

* Fix lint.

* Special-case Var const-ness for Stretch type.

This feels like a bit of a hack, but the idea is to override a
Var to report itself as a constant expression only in
the case that its type is Stretch. I would argue that it's not
quite as hack-y as it appears, since Stretch is probably the
only kind of constant we'll ever allow in Qiskit without an
in-line initializer. If ever we want to support declaring
other kinds of constants (e.g. Uint), we'll probably want to
introduce a `expr.Const` type whose constructor requires a
const initializer expression.

* Address review comments.

* Update release note.

* Update docs.

* Add release notes and doc link.

* Address review comments.

* Remove Stretch type.

* Remove a few more mentions of the binned stretch type.

* Add docstring for Duration.

* Remove Stretch type stuff.

* WIP

* Track stretch variables throughout circuits.

* Update QASM exporter.

* Fix existing tests and found bugs.

* Fix format.

* Simplify structural eq visitor.

* Add num_identifiers.

* Support QPY.

* Implement QASM visit for stretch expr.

* Track stretches through DAGCircuit.

* Add visit_stretch to classical resource map.

* Fix lint.

* Address review comments.

* Remove unused import.

* Represent stretch with StretchDeclaration in QASM AST.

Previously, we used StretchType within a ClassicalDeclaration,
but this wasn't the best fit.

* Remove visitor short circuit.

* Address review comments.

* Address review comments.

* Support division by Uint.

* Add release note.

* Fix lint.

* Add missing DAG stretch plumbing.

* Fix QPY serialization bug.

* Fix lint.

* Add qpy test.

* Support remapping for stretch variables in compose.

* Fix qpy compat test.

* Add negative test for QPY stretch expr.

* Add more circuit testing.

* Add circuit equality testing for stretch.

* Refer to 'stretches' instead of 'stretch variables'.

* Remove | None for Stretch.name

* Address review comments.

* Update QPY desc.

* Fix num_identifiers.

* Fix merge for control flow tests.
  • Loading branch information
kevinhartman authored Mar 6, 2025
1 parent fdd5e96 commit f82689f
Show file tree
Hide file tree
Showing 32 changed files with 1,440 additions and 115 deletions.
10 changes: 10 additions & 0 deletions crates/circuit/src/converters.rs
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,8 @@ pub struct QuantumCircuitData<'py> {
pub input_vars: Vec<Bound<'py, PyAny>>,
pub captured_vars: Vec<Bound<'py, PyAny>>,
pub declared_vars: Vec<Bound<'py, PyAny>>,
pub captured_stretches: Vec<Bound<'py, PyAny>>,
pub declared_stretches: Vec<Bound<'py, PyAny>>,
}

impl<'py> FromPyObject<'py> for QuantumCircuitData<'py> {
Expand Down Expand Up @@ -63,6 +65,14 @@ impl<'py> FromPyObject<'py> for QuantumCircuitData<'py> {
.call_method0(intern!(py, "iter_declared_vars"))?
.try_iter()?
.collect::<PyResult<Vec<_>>>()?,
captured_stretches: ob
.call_method0(intern!(py, "iter_captured_stretches"))?
.try_iter()?
.collect::<PyResult<Vec<_>>>()?,
declared_stretches: ob
.call_method0(intern!(py, "iter_declared_stretches"))?
.try_iter()?
.collect::<PyResult<Vec<_>>>()?,
})
}
}
Expand Down
231 changes: 228 additions & 3 deletions crates/circuit/src/dag_circuit.rs
Original file line number Diff line number Diff line change
Expand Up @@ -237,6 +237,9 @@ pub struct DAGCircuit {
control_flow_module: PyControlFlowModule,
vars_info: HashMap<String, DAGVarInfo>,
vars_by_type: [Py<PySet>; 3],

captured_stretches: IndexMap<String, Py<PyAny>, RandomState>,
declared_stretches: IndexMap<String, Py<PyAny>, RandomState>,
}

#[derive(Clone, Debug)]
Expand Down Expand Up @@ -391,6 +394,8 @@ impl DAGCircuit {
PySet::empty(py)?.unbind(),
PySet::empty(py)?.unbind(),
],
captured_stretches: IndexMap::default(),
declared_stretches: IndexMap::default(),
})
}

Expand Down Expand Up @@ -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)
Expand All @@ -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: '{}'",
Expand Down Expand Up @@ -1813,20 +1830,26 @@ 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)));
}
}
} else {
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,
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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<PyAny>) -> 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::<String>()?;
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:
Expand All @@ -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<PyAny>) -> PyResult<()> {
let var_name: String = var.getattr("name")?.extract::<String>()?;
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 {
Expand Down Expand Up @@ -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<PyAny>) -> PyResult<bool> {
match var.extract::<String>() {
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<PyAny>) -> PyResult<bool> {
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<Py<PyIterator>> {
Ok(self.vars_by_type[DAGVarType::Input as usize]
Expand All @@ -4345,6 +4498,29 @@ impl DAGCircuit {
.unbind())
}

/// Iterable over the captured stretches tracked by the circuit.
fn iter_captured_stretches(&self, py: Python) -> PyResult<Py<PyIterator>> {
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<Py<PyIterator>> {
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<Py<PyIterator>> {
Ok(self.vars_by_type[DAGVarType::Declare as usize]
Expand All @@ -4355,6 +4531,14 @@ impl DAGCircuit {
.unbind())
}

/// Iterable over the declared stretches tracked by the circuit.
fn iter_declared_stretches(&self, py: Python) -> PyResult<Py<PyIterator>> {
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<Py<PyIterator>> {
let out_set = PySet::empty(py)?;
Expand All @@ -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<Py<PyIterator>> {
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))
Expand Down Expand Up @@ -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",
));
}

Expand Down Expand Up @@ -5915,6 +6118,8 @@ impl DAGCircuit {
PySet::empty(py)?.unbind(),
PySet::empty(py)?.unbind(),
],
captured_stretches: IndexMap::default(),
declared_stretches: IndexMap::default(),
})
}

Expand Down Expand Up @@ -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::<String>()?;
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::<String>()?;
new_dag.declared_stretches.insert(name, var.unbind());
}

// Add all the registers
if let Some(qregs) = qc.qregs {
for qreg in qregs.iter() {
Expand Down Expand Up @@ -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)
}
Expand Down
5 changes: 4 additions & 1 deletion qiskit/circuit/_classical_resource_map.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
):
Expand Down Expand Up @@ -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)

Expand Down
Loading

0 comments on commit f82689f

Please sign in to comment.