[Stretch] Add stretch variable support to QuantumCircuit.#13852
[Stretch] Add stretch variable support to QuantumCircuit.#13852jakelishman merged 170 commits intoQiskit:mainfrom
stretch variable support to QuantumCircuit.#13852Conversation
This reverts commit e2b3221.
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.
jakelishman
left a comment
There was a problem hiding this comment.
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?
| 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", | ||
| )); | ||
| } |
There was a problem hiding this comment.
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?
| var: uuid.UUID | ||
| """A :class:`~uuid.UUID` to uniquely identify this stretch.""" | ||
| name: str | None | ||
| """The name of the stretch variable.""" |
There was a problem hiding this comment.
Two minor points:
- would
varbe better named asuid?Varoriginally called its inner fieldvarbecause the inner object was aClbitorClassicalRegister, then the field got co-opted to mean something more like "l-value ID".Stretchisn't wrapping anything else, so we might be better jumping straight touid-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.) namemust always be set and be astrforStretch, I think?name: Nonewas for implicitVarnodes that wrapClbit, etc.
| 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) |
There was a problem hiding this comment.
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.
| # Use the stretches as Delay duration. | ||
| qc.delay(a, [0, 1]) | ||
| qc.delay(b, 2) | ||
| qc.delay(c, [3, 4]) | ||
| qc.barrier() |
There was a problem hiding this comment.
Strictly I think this PR doesn't add this bit, right?
There was a problem hiding this comment.
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>`__ |
There was a problem hiding this comment.
| `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>`__ |
| 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. |
There was a problem hiding this comment.
It probably won't be able to be used by box in time for 2.0, so likely best drop that line.
| copied = qc.copy_empty_like("copy") | ||
| self.assertEqual(copied.name, "copy") | ||
|
|
||
| # pylint: disable=invalid-name |
There was a problem hiding this comment.
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.
| 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())) | ||
|
|
There was a problem hiding this comment.
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
left a comment
There was a problem hiding this comment.
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).
| qc1 = QuantumCircuit(captures=[a, b, c]) | ||
| self.assertEqual(qc1, QuantumCircuit(captures=[a, b, c])) | ||
| self.assertNotEqual(qc1, QuantumCircuit(captures=[c, b, a])) |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
Added to my list, I'll sort it out in #13853
There was a problem hiding this comment.
If there's no time, it can wait til post 2.0 - it's a valid bugfix.
jakelishman
left a comment
There was a problem hiding this comment.
Re-approving, assuming I didn't bork the merge conflict. (Just trying to get this in the queue before I got to bed.)
aad2200 to
7634b3e
Compare
jakelishman
left a comment
There was a problem hiding this comment.
Thanks for the QPY docs, and for pointing out I'd missed them haha!
Summary
Adds support for adding new
stretchvariables to a circuit viaQuantumCircuit::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 newstretchvariables, but this can be added in the future.Details and comments
Based on #13850. See the readable diff here.Stretch. It's type is alwaysDuration, and it is always a constant expression.QuantumCircuit"input" variables.DelayandBoxstart using them in their duration expressions.To-do