Skip to content

Commit f82689f

Browse files
authored
[Stretch] Add stretch variable support to QuantumCircuit. (#13852)
* WIP * Add try_const to lift. * Try multiple singletons, new one for const. * Revert "Try multiple singletons, new one for const." This reverts commit 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.
1 parent fdd5e96 commit f82689f

32 files changed

Lines changed: 1440 additions & 115 deletions

crates/circuit/src/converters.rs

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,8 @@ pub struct QuantumCircuitData<'py> {
3232
pub input_vars: Vec<Bound<'py, PyAny>>,
3333
pub captured_vars: Vec<Bound<'py, PyAny>>,
3434
pub declared_vars: Vec<Bound<'py, PyAny>>,
35+
pub captured_stretches: Vec<Bound<'py, PyAny>>,
36+
pub declared_stretches: Vec<Bound<'py, PyAny>>,
3537
}
3638

3739
impl<'py> FromPyObject<'py> for QuantumCircuitData<'py> {
@@ -63,6 +65,14 @@ impl<'py> FromPyObject<'py> for QuantumCircuitData<'py> {
6365
.call_method0(intern!(py, "iter_declared_vars"))?
6466
.try_iter()?
6567
.collect::<PyResult<Vec<_>>>()?,
68+
captured_stretches: ob
69+
.call_method0(intern!(py, "iter_captured_stretches"))?
70+
.try_iter()?
71+
.collect::<PyResult<Vec<_>>>()?,
72+
declared_stretches: ob
73+
.call_method0(intern!(py, "iter_declared_stretches"))?
74+
.try_iter()?
75+
.collect::<PyResult<Vec<_>>>()?,
6676
})
6777
}
6878
}

crates/circuit/src/dag_circuit.rs

Lines changed: 228 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -237,6 +237,9 @@ pub struct DAGCircuit {
237237
control_flow_module: PyControlFlowModule,
238238
vars_info: HashMap<String, DAGVarInfo>,
239239
vars_by_type: [Py<PySet>; 3],
240+
241+
captured_stretches: IndexMap<String, Py<PyAny>, RandomState>,
242+
declared_stretches: IndexMap<String, Py<PyAny>, RandomState>,
240243
}
241244

242245
#[derive(Clone, Debug)]
@@ -391,6 +394,8 @@ impl DAGCircuit {
391394
PySet::empty(py)?.unbind(),
392395
PySet::empty(py)?.unbind(),
393396
],
397+
captured_stretches: IndexMap::default(),
398+
declared_stretches: IndexMap::default(),
394399
})
395400
}
396401

@@ -1497,6 +1502,12 @@ impl DAGCircuit {
14971502
{
14981503
target_dag.add_var(py, &var, DAGVarType::Declare)?;
14991504
}
1505+
for stretch in self.captured_stretches.values() {
1506+
target_dag.add_captured_stretch(py, stretch.bind(py))?;
1507+
}
1508+
for stretch in self.declared_stretches.values() {
1509+
target_dag.add_declared_stretch(stretch.bind(py))?;
1510+
}
15001511
} else if vars_mode == "captures" {
15011512
for var in self.vars_by_type[DAGVarType::Input as usize]
15021513
.bind(py)
@@ -1516,6 +1527,12 @@ impl DAGCircuit {
15161527
{
15171528
target_dag.add_var(py, &var, DAGVarType::Capture)?;
15181529
}
1530+
for stretch in self.captured_stretches.values() {
1531+
target_dag.add_captured_stretch(py, stretch.bind(py))?;
1532+
}
1533+
for stretch in self.declared_stretches.values() {
1534+
target_dag.add_captured_stretch(py, stretch.bind(py))?;
1535+
}
15191536
} else if vars_mode != "drop" {
15201537
return Err(PyValueError::new_err(format!(
15211538
"unknown vars_mode: '{}'",
@@ -1813,20 +1830,26 @@ impl DAGCircuit {
18131830
dag.add_input_var(py, &var?)?;
18141831
}
18151832
if inline_captures {
1816-
for var in other.iter_captured_vars(py)?.bind(py) {
1833+
for var in other.iter_captures(py)?.bind(py) {
18171834
let var = var?;
1818-
if !dag.has_var(&var)? {
1835+
if !dag.has_identifier(&var)? {
18191836
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)));
18201837
}
18211838
}
18221839
} else {
18231840
for var in other.iter_captured_vars(py)?.bind(py) {
18241841
dag.add_captured_var(py, &var?)?;
18251842
}
1843+
for stretch in other.iter_captured_stretches(py)?.bind(py) {
1844+
dag.add_captured_stretch(py, &stretch?)?;
1845+
}
18261846
}
18271847
for var in other.iter_declared_vars(py)?.bind(py) {
18281848
dag.add_declared_var(py, &var?)?;
18291849
}
1850+
for var in other.iter_declared_stretches(py)?.bind(py) {
1851+
dag.add_declared_stretch(&var?)?;
1852+
}
18301853

18311854
let variable_mapper = PyVariableMapper::new(
18321855
py,
@@ -2244,6 +2267,32 @@ impl DAGCircuit {
22442267
}
22452268
}
22462269

2270+
if self.captured_stretches.len() != other.captured_stretches.len()
2271+
|| self.declared_stretches.len() != other.declared_stretches.len()
2272+
{
2273+
return Ok(false);
2274+
}
2275+
2276+
for (our_stretch, their_stretch) in self
2277+
.captured_stretches
2278+
.values()
2279+
.zip(other.captured_stretches.values())
2280+
{
2281+
if !our_stretch.bind(py).eq(their_stretch)? {
2282+
return Ok(false);
2283+
}
2284+
}
2285+
2286+
for (our_stretch, their_stretch) in self
2287+
.declared_stretches
2288+
.values()
2289+
.zip(other.declared_stretches.values())
2290+
{
2291+
if !our_stretch.bind(py).eq(their_stretch)? {
2292+
return Ok(false);
2293+
}
2294+
}
2295+
22472296
let self_bit_indices = {
22482297
let indices = self
22492298
.qubits
@@ -4270,6 +4319,47 @@ impl DAGCircuit {
42704319
Ok(())
42714320
}
42724321

4322+
/// Add a captured stretch to the circuit.
4323+
///
4324+
/// Args:
4325+
/// var: the stretch to add.
4326+
fn add_captured_stretch(&mut self, py: Python, var: &Bound<PyAny>) -> PyResult<()> {
4327+
if !self.vars_by_type[DAGVarType::Input as usize]
4328+
.bind(py)
4329+
.is_empty()
4330+
{
4331+
return Err(DAGCircuitError::new_err(
4332+
"cannot add captures to a circuit with inputs",
4333+
));
4334+
}
4335+
let var_name: String = var.getattr("name")?.extract::<String>()?;
4336+
if self.vars_info.contains_key(&var_name) {
4337+
return Err(DAGCircuitError::new_err(
4338+
"cannot add stretch as its name shadows an existing identifier",
4339+
));
4340+
}
4341+
if let Some(previous) = self.declared_stretches.get(&var_name) {
4342+
if var.eq(previous)? {
4343+
return Err(DAGCircuitError::new_err("already present in the circuit"));
4344+
}
4345+
return Err(DAGCircuitError::new_err(
4346+
"cannot add stretch as its name shadows an existing identifier",
4347+
));
4348+
}
4349+
if let Some(previous) = self.captured_stretches.get(&var_name) {
4350+
if var.eq(previous)? {
4351+
return Err(DAGCircuitError::new_err("already present in the circuit"));
4352+
}
4353+
return Err(DAGCircuitError::new_err(
4354+
"cannot add stretch as its name shadows an existing identifier",
4355+
));
4356+
}
4357+
4358+
self.captured_stretches
4359+
.insert(var_name, var.clone().unbind());
4360+
Ok(())
4361+
}
4362+
42734363
/// Add a declared local variable to the circuit.
42744364
///
42754365
/// Args:
@@ -4279,6 +4369,39 @@ impl DAGCircuit {
42794369
Ok(())
42804370
}
42814371

4372+
/// Add a declared stretch to the circuit.
4373+
///
4374+
/// Args:
4375+
/// var: the stretch to add.
4376+
fn add_declared_stretch(&mut self, var: &Bound<PyAny>) -> PyResult<()> {
4377+
let var_name: String = var.getattr("name")?.extract::<String>()?;
4378+
if self.vars_info.contains_key(&var_name) {
4379+
return Err(DAGCircuitError::new_err(
4380+
"cannot add stretch as its name shadows an existing identifier",
4381+
));
4382+
}
4383+
if let Some(previous) = self.declared_stretches.get(&var_name) {
4384+
if var.eq(previous)? {
4385+
return Err(DAGCircuitError::new_err("already present in the circuit"));
4386+
}
4387+
return Err(DAGCircuitError::new_err(
4388+
"cannot add stretch as its name shadows an existing identifier",
4389+
));
4390+
}
4391+
if let Some(previous) = self.captured_stretches.get(&var_name) {
4392+
if var.eq(previous)? {
4393+
return Err(DAGCircuitError::new_err("already present in the circuit"));
4394+
}
4395+
return Err(DAGCircuitError::new_err(
4396+
"cannot add stretch as its name shadows an existing identifier",
4397+
));
4398+
}
4399+
4400+
self.declared_stretches
4401+
.insert(var_name, var.clone().unbind());
4402+
Ok(())
4403+
}
4404+
42824405
/// Total number of classical variables tracked by the circuit.
42834406
#[getter]
42844407
fn num_vars(&self) -> usize {
@@ -4325,6 +4448,36 @@ impl DAGCircuit {
43254448
}
43264449
}
43274450

4451+
/// Is this stretch in the DAG?
4452+
///
4453+
/// Args:
4454+
/// var: the stretch or name to check.
4455+
fn has_stretch(&self, var: &Bound<PyAny>) -> PyResult<bool> {
4456+
match var.extract::<String>() {
4457+
Ok(name) => Ok(self.declared_stretches.contains_key(&name)
4458+
|| self.captured_stretches.contains_key(&name)),
4459+
Err(_) => {
4460+
let raw_name = var.getattr("name")?;
4461+
let var_name: String = raw_name.extract()?;
4462+
if let Some(stretch) = self.declared_stretches.get(&var_name) {
4463+
return Ok(stretch.is(var));
4464+
}
4465+
if let Some(stretch) = self.captured_stretches.get(&var_name) {
4466+
return Ok(stretch.is(var));
4467+
}
4468+
Ok(false)
4469+
}
4470+
}
4471+
}
4472+
4473+
/// Is this identifier in the DAG?
4474+
///
4475+
/// Args:
4476+
/// var: the identifier or name to check.
4477+
fn has_identifier(&self, var: &Bound<PyAny>) -> PyResult<bool> {
4478+
Ok(self.has_var(var)? || self.has_stretch(var)?)
4479+
}
4480+
43284481
/// Iterable over the input classical variables tracked by the circuit.
43294482
fn iter_input_vars(&self, py: Python) -> PyResult<Py<PyIterator>> {
43304483
Ok(self.vars_by_type[DAGVarType::Input as usize]
@@ -4345,6 +4498,29 @@ impl DAGCircuit {
43454498
.unbind())
43464499
}
43474500

4501+
/// Iterable over the captured stretches tracked by the circuit.
4502+
fn iter_captured_stretches(&self, py: Python) -> PyResult<Py<PyIterator>> {
4503+
Ok(PyList::new(py, self.captured_stretches.values())?
4504+
.into_any()
4505+
.try_iter()?
4506+
.unbind())
4507+
}
4508+
4509+
/// Iterable over all captured identifiers tracked by the circuit.
4510+
fn iter_captures(&self, py: Python) -> PyResult<Py<PyIterator>> {
4511+
let out_set = PySet::empty(py)?;
4512+
for var in self.vars_by_type[DAGVarType::Capture as usize]
4513+
.bind(py)
4514+
.iter()
4515+
{
4516+
out_set.add(var)?;
4517+
}
4518+
for stretch in self.captured_stretches.values() {
4519+
out_set.add(stretch)?;
4520+
}
4521+
Ok(out_set.into_any().try_iter()?.unbind())
4522+
}
4523+
43484524
/// Iterable over the declared classical variables tracked by the circuit.
43494525
fn iter_declared_vars(&self, py: Python) -> PyResult<Py<PyIterator>> {
43504526
Ok(self.vars_by_type[DAGVarType::Declare as usize]
@@ -4355,6 +4531,14 @@ impl DAGCircuit {
43554531
.unbind())
43564532
}
43574533

4534+
/// Iterable over the declared stretches tracked by the circuit.
4535+
fn iter_declared_stretches(&self, py: Python) -> PyResult<Py<PyIterator>> {
4536+
Ok(PyList::new(py, self.declared_stretches.values())?
4537+
.into_any()
4538+
.try_iter()?
4539+
.unbind())
4540+
}
4541+
43584542
/// Iterable over all the classical variables tracked by the circuit.
43594543
fn iter_vars(&self, py: Python) -> PyResult<Py<PyIterator>> {
43604544
let out_set = PySet::empty(py)?;
@@ -4366,6 +4550,18 @@ impl DAGCircuit {
43664550
Ok(out_set.into_any().try_iter()?.unbind())
43674551
}
43684552

4553+
/// Iterable over all the stretches tracked by the circuit.
4554+
fn iter_stretches(&self, py: Python) -> PyResult<Py<PyIterator>> {
4555+
let out_set = PySet::empty(py)?;
4556+
for s in self.captured_stretches.values() {
4557+
out_set.add(s)?;
4558+
}
4559+
for s in self.declared_stretches.values() {
4560+
out_set.add(s)?;
4561+
}
4562+
Ok(out_set.into_any().try_iter()?.unbind())
4563+
}
4564+
43694565
fn _has_edge(&self, source: usize, target: usize) -> bool {
43704566
self.dag
43714567
.contains_edge(NodeIndex::new(source), NodeIndex::new(target))
@@ -5794,7 +5990,14 @@ impl DAGCircuit {
57945990
return Err(DAGCircuitError::new_err("already present in the circuit"));
57955991
}
57965992
return Err(DAGCircuitError::new_err(
5797-
"cannot add var as its name shadows an existing var",
5993+
"cannot add var as its name shadows an existing identifier",
5994+
));
5995+
}
5996+
if self.declared_stretches.contains_key(&var_name)
5997+
|| self.captured_stretches.contains_key(&var_name)
5998+
{
5999+
return Err(DAGCircuitError::new_err(
6000+
"cannot add var as its name shadows an existing identifier",
57986001
));
57996002
}
58006003

@@ -5915,6 +6118,8 @@ impl DAGCircuit {
59156118
PySet::empty(py)?.unbind(),
59166119
PySet::empty(py)?.unbind(),
59176120
],
6121+
captured_stretches: IndexMap::default(),
6122+
declared_stretches: IndexMap::default(),
59186123
})
59196124
}
59206125

@@ -6411,6 +6616,24 @@ impl DAGCircuit {
64116616
new_dag.add_var(py, var, DAGVarType::Capture)?;
64126617
}
64136618

6619+
new_dag
6620+
.captured_stretches
6621+
.reserve(qc.captured_stretches.len());
6622+
6623+
for var in qc.captured_stretches {
6624+
let name: String = var.getattr("name")?.extract::<String>()?;
6625+
new_dag.captured_stretches.insert(name, var.unbind());
6626+
}
6627+
6628+
new_dag
6629+
.declared_stretches
6630+
.reserve(qc.declared_stretches.len());
6631+
6632+
for var in qc.declared_stretches {
6633+
let name: String = var.getattr("name")?.extract::<String>()?;
6634+
new_dag.declared_stretches.insert(name, var.unbind());
6635+
}
6636+
64146637
// Add all the registers
64156638
if let Some(qregs) = qc.qregs {
64166639
for qreg in qregs.iter() {
@@ -6460,6 +6683,8 @@ impl DAGCircuit {
64606683
input_vars: Vec::new(),
64616684
captured_vars: Vec::new(),
64626685
declared_vars: Vec::new(),
6686+
captured_stretches: Vec::new(),
6687+
declared_stretches: Vec::new(),
64636688
};
64646689
Self::from_circuit(py, circ, copy_op, None, None)
64656690
}

qiskit/circuit/_classical_resource_map.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,7 @@ def __init__(
4343
self,
4444
target_cregs: typing.Iterable[ClassicalRegister],
4545
bit_map: typing.Mapping[Bit, Bit],
46-
var_map: typing.Mapping[expr.Var, expr.Var] | None = None,
46+
var_map: typing.Mapping[expr.Var | expr.Stretch, expr.Var | expr.Stretch] | None = None,
4747
*,
4848
add_register: typing.Callable[[ClassicalRegister], None] | None = None,
4949
):
@@ -132,6 +132,9 @@ def visit_var(self, node, /):
132132
return expr.Var(self._map_register(node.var), node.type)
133133
return self.var_map.get(node, node)
134134

135+
def visit_stretch(self, node, /):
136+
return self.var_map.get(node, node)
137+
135138
def visit_value(self, node, /):
136139
return expr.Value(node.value, node.type)
137140

0 commit comments

Comments
 (0)