Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Stretch] Add stretch variable support to QuantumCircuit. #13852

Merged
merged 170 commits into from
Mar 6, 2025

Conversation

kevinhartman
Copy link
Contributor

@kevinhartman kevinhartman commented Feb 16, 2025

Summary

Adds support for adding new stretch variables to a circuit via QuantumCircuit::add_stretch. The returned variable follows the same scoping rules as other classical circuit variables. For now, we don't support introducing a lower bound when declaring new stretch variables, but this can be added in the future.

Details and comments

Based on #13850. See the readable diff here.

  • Stretch variables are represented with a new expression node, Stretch. It's type is always Duration, and it is always a constant expression.
  • Stretch variables are tracked separately from classical run-time variables.
  • Stretches are not allowed as QuantumCircuit "input" variables.
  • We can now declare stretch variables and have those be emitted to QASM, but we won't be able to do anything with them until Delay and Box start using them in their duration expressions.

To-do

  • Release note.

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).
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.
This is probably a better default in general, since we
don't really have much use for const except for timing
stuff.
Since we're going for using a Cast node when const-ness
differs, this will be fine.
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.
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).
Also adds support to expr.lift to create a value expression of
type types.Duration from an instance of qiskit.circuit.Duration.
@kevinhartman kevinhartman added Changelog: New Feature Include in the "Added" section of the changelog mod: circuit Related to the core of the `QuantumCircuit` class or the circuit library and removed on hold Can not fix yet labels Mar 5, 2025
Copy link
Member

@jakelishman jakelishman left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for all this work - I know quite a lot of it is tedious duplication of Var-based stuff.

I'm not convinced I'd have spotted if there's stretch handling missing from anywhere, but I think that's par for the course for reviews like this.

One more point I didn't spot in the review (but maybe overlooked): could you add a test that QuantumCircuit equality requires the stretches to be declared in the same order?

Comment on lines 4331 to 4351
if self.vars_info.contains_key(&var_name) {
return Err(DAGCircuitError::new_err(
"cannot add var as its name shadows an existing var",
));
}
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 var as its name shadows an existing var",
));
}
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 var as its name shadows an existing var",
));
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think the "input var" should be referred to as stretch, and the "existing var" would now be an "existing identifier"? Should add_var have its terminology in the error messages updated too?

Comment on lines 221 to 224
var: uuid.UUID
"""A :class:`~uuid.UUID` to uniquely identify this stretch."""
name: str | None
"""The name of the stretch variable."""
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Two minor points:

  • would var be better named as uid? Var originally called its inner field var because the inner object was a Clbit or ClassicalRegister, then the field got co-opted to mean something more like "l-value ID". Stretch isn't wrapping anything else, so we might be better jumping straight to uid-based naming? (Strictly, we don't need the uid to be universally unique, just unique among stretches, so we might not want to hard commit to saying it's universally unique.)
  • name must always be set and be a str for Stretch, I think? name: None was for implicit Var nodes that wrap Clbit, etc.

Comment on lines +258 to +266
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)
Copy link
Member

@jakelishman jakelishman Mar 5, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Potentially we should just swap to a __getnewargs__ form, but I'm assuming that's pre-existing based on how Var was written? Let's do both at once (in a different PR post 2.0), if so.

Comment on lines +21 to +25
# Use the stretches as Delay duration.
qc.delay(a, [0, 1])
qc.delay(b, 2)
qc.delay(c, [3, 4])
qc.barrier()
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Strictly I think this PR doesn't add this bit, right?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Correct. It'll come in the next PR.

qc.barrier()

For additional context and examples, refer to the
`QASM language specification. <https://openqasm.com/language/delays.html#duration-and-stretch-types>`__
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
`QASM language specification. <https://openqasm.com/language/delays.html#duration-and-stretch-types>`__
`OpenQASM 3 language specification. <https://openqasm.com/language/delays.html#duration-and-stretch-types>`__

Comment on lines 34 to 36
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` or :meth:`~.QuantumCircuit.box` instruction.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It probably won't be able to be used by box in time for 2.0, so likely best drop that line.

@@ -411,17 +411,22 @@ def test_copy_empty_like_circuit(self):
copied = qc.copy_empty_like("copy")
self.assertEqual(copied.name, "copy")

# pylint: disable=invalid-name
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this comment was supposed to go inside the function so it scopes to only that, but I also don't care because I don't like this rule haha.

Comment on lines 437 to 444
copied.add_var(d, 0xFF)
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({e}, set(qc.iter_declared_stretches()))

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

iirc, the spirit of this test is to copied.add_stretch(f) and to assert that {e, f} is in copied, while qc is still {e} (to catch the case of a shared reference to a Python object).

That said, we then seem to repeat functionally the same test another 20ish times, so probably not a big deal lol

jakelishman
jakelishman previously approved these changes Mar 6, 2025
Copy link
Member

@jakelishman jakelishman left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm fine leaving Stretch.var as var and not changing to uid - if nothing else it helps duck-typing over Stretch and Var, and they are used in similar contexts sometimes.

Last comment is about the equality assertions, but it's fine - let's just leave it til a follow up. At the moment we'll false negative some assertions, but we false negative loads of things in QuantumCircuit comparisons, so it's not the biggest deal ever (might matter for control-flow builders one day, so we ought to fix it, but still).

Comment on lines +192 to +194
qc1 = QuantumCircuit(captures=[a, b, c])
self.assertEqual(qc1, QuantumCircuit(captures=[a, b, c]))
self.assertNotEqual(qc1, QuantumCircuit(captures=[c, b, a]))
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actually this particular one should compare equal in both cases - captures are orderless, it's just declarations for which the order matters (because captures don't introduce a new degree of freedom).

So in this test they should both be equal, but

qc1 = QuantumCircuit()
qc1.add_stretch(a)
qc1.add_stretch(b)

qc2 = QuantumCircuit()
qc2.add_stretch(b)
qc2.add_stretch(a)

would have qc1 != qc2.

That said, at this point I'm fine leaving this til a follow-up anyway, though let's note to do it.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added to my list, I'll sort it out in #13853

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If there's no time, it can wait til post 2.0 - it's a valid bugfix.

jakelishman
jakelishman previously approved these changes Mar 6, 2025
Copy link
Member

@jakelishman jakelishman left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Re-approving, assuming I didn't bork the merge conflict. (Just trying to get this in the queue before I got to bed.)

jakelishman
jakelishman previously approved these changes Mar 6, 2025
Copy link
Member

@jakelishman jakelishman left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for the QPY docs, and for pointing out I'd missed them haha!

@jakelishman jakelishman enabled auto-merge March 6, 2025 01:08
Copy link
Contributor

@ElePT ElePT left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM!

@jakelishman jakelishman added this pull request to the merge queue Mar 6, 2025
Merged via the queue into Qiskit:main with commit f82689f Mar 6, 2025
20 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Changelog: New Feature Include in the "Added" section of the changelog mod: circuit Related to the core of the `QuantumCircuit` class or the circuit library priority: high
Projects
None yet
Development

Successfully merging this pull request may close these issues.

6 participants