Conversation
|
I ran into an unexpected issue while reviewing this PR. Please try again later. |
|
|
Like the other PR where most of the CI tests broke due to me, when I pull in the upstream fix and rerun the tests locally, I get these failures: |
1673201 to
0817702
Compare
63309bd to
9bbef47
Compare
5e86cbb to
3ba6d11
Compare
asaites
left a comment
There was a problem hiding this comment.
This looks good to me, with the same expectations about the upstream merge (note that there are a couple small fixes there you may want to bring in if you want to test this directly against, say, pyQuil).
Speaking of pyQuil, I noted in my review, I think this will have to be a breaking change for pyQuil once it lands, unless there's a sensible "default" qubit value to supply for measure calibration definitions. My guess is that the parameter really always was required, so I'm also fine with the "yes, it's technically a breaking change, but if it does break for you, it's because your code was wrong and we unfortunately didn't catch it due to our own bug".
| /// The qubit which is being measured. | ||
| pub qubit: Qubit, |
There was a problem hiding this comment.
Since the Qubit is no longer optional, I don't think there's a way for this to not be a breaking change for PyQuil, as I see no obvious "default" qubit to supply.
And based on what I've read of your proposed changes to the Quil spec, I'm guessing it probably should always have been required? I don't see a way to make sense of "measure this qubit" without knowing which one.
There was a problem hiding this comment.
Yeah, this is just a bug – somebody had a thinko, and instead of "required qubit and optional memory location" implemented "optional qubit and required memory location", which as you say makes no sense – and isn't even compatible with MEASURE, which correctly takes a required qubit and an optional memory location. And if you look at the parser, you'll see some very strange code which could for instance result in a DEFCAL MEASURE that (a) measures no qubits, but (b) puts the result into the memory location named "0".
/// Parse the contents of a `DEFCAL MEASURE` instruction, following the `MEASURE` token.
pub(crate) fn parse_defcal_measure<'a>(
input: ParserInput<'a>,
) -> InternalParserResult<'a, Instruction> {
let (input, params) = pair(parse_qubit, opt(token!(Identifier(v))))(input)?;
let (qubit, destination) = match params {
(qubit, Some(destination)) => (Some(qubit), destination),
(destination, None) => (None, destination.to_quil_or_debug()),
};
// ...
}I wonder if we should move this fix back with the CalibrationDefinition name change, if you think that'll help things go more smoothly? My thinking was that MeasureCalibrationDefinition had to change anyway, to accommodate the new !name, but if you think it would make for a softer landing I could pull this fix out.
There was a problem hiding this comment.
I wonder if we should move this fix back with the CalibrationDefinition name change, if you think that'll help things go more smoothly?
I'm inclined to say "yes" specifically to the change for making qubit required and parameter optional. That part has to be a breaking change for pyQuil, but the rest we could work-around; I've detailed some of that below and am interested in your thoughts with respect to how well quil-rs, quil, and pyQuil should match with respect to constructor signatures.
My thought is about the effect on pyQuil users specifically, avoiding 2 major version bumps back-to-back. We should be able to prevent that by simply not releasing a new pyQuil right away, though I think we need to change the version specifier for quil in its dependency list to prevent it from trying to grab the newer release before we're ready (given that this project is currently pre-1.0, that probably should have always been the case).
pyQuil doesn't actually have its own notion of a MeasureCalibrationIdentifier -- it just has DefMeasureCalibration (corresponding to MeasureCalibrationDefinition), with (roughly) this constructor:
class DefMeasureCalibration(quil_rs.MeasureCalibrationDefinition, AbstractInstruction):
def __new__(
cls,
qubit: Qubit | FormalArgument | None,
memory_reference: MemoryReference,
instrs: list[AbstractInstruction],
) -> Self:
"""Initialize a new measure calibration definition."""
rs_qubit = None if not qubit else _convert_to_rs_qubit(qubit)
ident = quil_rs.MeasureCalibrationIdentifier(rs_qubit, str(memory_reference))
return super().__new__(cls, ident, _convert_to_rs_instructions(instrs))So the breaking change is here:
class DefMeasureCalibration(quil_rs.MeasureCalibrationDefinition, AbstractInstruction):
def __new__(
cls,
- qubit: Qubit | FormalArgument | None,
+ qubit: Qubit | FormalArgument,
memory_reference: MemoryReference,
instrs: list[AbstractInstruction],
) -> Self:
"""Initialize a new measure calibration definition."""
- rs_qubit = None if not qubit else _convert_to_rs_qubit(qubit)
+ rs_qubit = _convert_to_rs_qubit(qubit)
ident = quil_rs.MeasureCalibrationIdentifier(rs_qubit, str(memory_reference))
return super().__new__(cls, ident, _convert_to_rs_instructions(instrs))And then to add support for named measurements, we could make this non-breaking change (assuming the constructor signature for MeasureCalibrationIdentifier you've proposed in this PR):
class DefMeasureCalibration(quil_rs.MeasureCalibrationDefinition, AbstractInstruction):
def __new__(
cls,
qubit: Qubit | FormalArgument,
memory_reference: MemoryReference,
instrs: list[AbstractInstruction],
+ name: str | None = None,
) -> Self:
"""Initialize a new measure calibration definition."""
rs_qubit = _convert_to_rs_qubit(qubit)
- ident = quil_rs.MeasureCalibrationIdentifier(rs_qubit, str(memory_reference))
+ ident = quil_rs.MeasureCalibrationIdentifier(name, rs_qubit, str(memory_reference))
return super().__new__(cls, ident, _convert_to_rs_instructions(instrs))As a point of interest, we could essentially make the same sort of transition within quil to avoid two breaking changes. First we'd make this breaking change:
pickleable_new! {
impl MeasureCalibrationIdentifier {
- pub fn new(qubit: Option<Qubit>, parameter: String);
+ pub fn new(qubit: Qubit, target: Option<String>);
}
}With that, this wouldn't be a breaking change:
pickleable_new! {
impl MeasureCalibrationIdentifier {
- pub fn new(qubit: Qubit, target: Option<String>);
+ #[pyo3(signature = (qubit, target, name=None))]
+ pub new(qubit: Qubit, target: Option<String>, name: Option<String>);
}
}Of course, this requires we add the name parameter to the end in order to give it a default value.
We could have a separate constructor specifically for quil, if you really wanted the Rust constructor's parameter order to match their order within the Quil language construct. It would also be possible (though more complex) to write out an overloaded signature and sort out the details of which version you meant ourselves, but that wouldn't be my recommendation in the face of the above options.
There was a problem hiding this comment.
Two separate thoughts:
-
Re the existing
DEFCAL MEASUREbug: sounds like, due to the way Python works, the breaking change if any will actually be pretty small. I'll move it back along with the other change regardless, though, it'll be nice to be uniform. -
I thought about this issue with defaults, and I'm torn. On the Rust side, I think putting the name first is a win – you have to write
Noneanyway, so you might as well put it in the logical order. On the Python side… I don't know! I think there's also a good argument for
def __init__(self, qubit: Qubit, target: Optional[String], *, name: Optional[String] = None):
...for this and for MEASURE, since that lets you create the simple case simply. Perhaps the move is to leave this a breaking change in quil-py, and have the pyQuil wrapper do the default-parameter dance?
There was a problem hiding this comment.
I included the DEFCAL MEASURE fix in #482. The discussion about point 2 can continue though!
There was a problem hiding this comment.
Perhaps the move is to leave this a breaking change in quil-py, and have the pyQuil wrapper do the default-parameter dance?
In this particular case, it's moot: pyQuil doesn't have its own MeasureCalibrationIdentifier, and its signature for DefMeasureCalibration already diverges from MeasureCalibrationDefinition (to say nothing of the name difference). Handling the parameter ordering is no burden for this instance, and making qubit required is a breaking change no matter what.
In the bigger picture, we want Python users to have an ergonomic experience working with quil-rs. It's been fuzzy whether that responsibility falls on quil or should be handled in a higher-level wrapper like pyQuil. Right now, pyQuil's subclasses mostly exist to support things like "accept a Python number where an Expression.Number is expected". That's the kind of ergonomics we want, but having all those extra classes has proven itself a significant maintenance burden. Moving forward, our goal should be getting rid of those subclasses and having pyQuil manipulate quil's constructs directly when appropriate.
Doing that initially will, of course, be a major breaking change. Then, without the layer in between, quil will be more responsible for avoiding breaking changes. It might sound like that gets a lot harder without the extra layer, but the reality is that quil-py is precisely the compatibility layer between quil and quil-rs! Then the question becomes how far we want to let the Python version of things diverge from their Rust counterparts, as for example having a constructor with different parameter ordering. I don't know offhand how heavy a burden divergence there will create, but my instinct is that we should minimize differences whenever possible.
So with respect to adding the new constructor parameter (necessarily a breaking change for quil-rs), the options are:
- Share the constructor with
quil, with the newnameparameter at the beginning of the list, making it a breaking change forquil(and potentiallypyQuil, depending on when it lands). - Share the constructor with
quil, but add the newnameparameter to the end of the list, potentially at the cost of making it less ergonomic for Rust users or those very familiar with Quil. Use thepyo3signature to avoid a breaking change forquil. - Add a new constructor specifically for
quil, using thepyo3signature to avoid a breaking change, and deal with the consequences the divergence may entail.
Putting it in these terms makes me think (2) is the right way forward. Do you think I'm on the right track?
There was a problem hiding this comment.
I think that’s convincing, yeah! I’d amend it very slightly: make the gate name parameter last but named-only, as in my snippet above. This gives us multiple virtues:
- There are no Python calls that look identical to Rust calls but have different meanings;
- If you want to write things in the same order, you can, as long as you name every parameter.
I can make that change to MEASURE and DEFCAL MEASURE.
Merge pull request #482 from rigetti/478-backport-breaking-changes This PR backports the following breaking changes from #479, as they're generally useful for #462 and we want to minimize the number of breaking change releases. The changes are as follows 1. Deleting the unused-but-public `quil_rs::program::MemoryAccess` type. 2. Renaming the `Calibration` type to `CalibrationDefinition` for consistency, along with some updates to associated code. 3. Fixing the `MeasureCalibrationDefinition` type to correctly have a *required* qubit and an *optional* memory address, rather than the other way around. 4. Regenerating all the test files – not technically a breaking change, but very noisy. However, it gets things into a consistent state and deletes some `.snap` files that no longer correspond to anything, so it needed to happen.
3a7972c to
aa6ceb0
Compare
aa6ceb0 to
61d655b
Compare
554d902 to
d827767
Compare
This PR adds named measurements (proposal: quil-lang/quil#90, spec PR: quil-lang/quil#97) to quil-rs: we can now write
I went with the above syntax, using a
!, since there were no obviously winning syntax candidates and people dislikedMEASURE(midcircuit)andMEASURE[midcircuit].The corresponding support for calibration expansion is also added, along with tests.
Closes #478.