Skip to content

feat!: add named measurements#479

Merged
asaites merged 8 commits intomainfrom
478-named-measurements
Oct 10, 2025
Merged

feat!: add named measurements#479
asaites merged 8 commits intomainfrom
478-named-measurements

Conversation

@antalsz
Copy link
Contributor

@antalsz antalsz commented Sep 19, 2025

This PR adds named measurements (proposal: quil-lang/quil#90, spec PR: quil-lang/quil#97) to quil-rs: we can now write

MEASURE!midcircuit 0 ro

DEFCAL MEASURE!midcircuit 0 addr:
    ...

I went with the above syntax, using a !, since there were no obviously winning syntax candidates and people disliked MEASURE(midcircuit) and MEASURE[midcircuit].

The corresponding support for calibration expansion is also added, along with tests.

Closes #478.

@windsurf-bot
Copy link

windsurf-bot bot commented Sep 19, 2025

I ran into an unexpected issue while reviewing this PR. Please try again later.

@github-actions
Copy link

github-actions bot commented Sep 19, 2025

PR Preview Action v1.6.2
Preview removed because the pull request was closed.
2025-10-10 19:01 UTC

@asaites
Copy link
Contributor

asaites commented Sep 19, 2025

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:

failures:
    program::calibration::tests::test_expansion::case_08
    waveform::templates::tests::into_iq_values::case_01
    waveform::templates::tests::into_iq_values::case_02
    waveform::templates::tests::into_iq_values::case_04
    waveform::templates::tests::into_iq_values::case_05
    waveform::templates::tests::into_iq_values::case_07
    waveform::templates::tests::into_iq_values::case_08
    waveform::templates::tests::into_iq_values::case_09
    waveform::templates::tests::into_iq_values::case_10
    waveform::templates::tests::into_iq_values::case_11
    waveform::templates::tests::into_iq_values::case_12
    waveform::templates::tests::into_iq_values::case_13
    waveform::templates::tests::into_iq_values::case_14
    waveform::templates::tests::into_iq_values::case_15
    waveform::templates::tests::into_iq_values::case_16

@antalsz antalsz force-pushed the 476-memory-dependencies-remember-writes branch from 1673201 to 0817702 Compare September 19, 2025 23:52
@antalsz antalsz force-pushed the 478-named-measurements branch from 63309bd to 9bbef47 Compare September 19, 2025 23:53
@antalsz
Copy link
Contributor Author

antalsz commented Sep 20, 2025

@asaites Yep – merged in the fix from #477

@antalsz antalsz force-pushed the 478-named-measurements branch from 5e86cbb to 3ba6d11 Compare September 20, 2025 00:07
Copy link
Contributor

@asaites asaites left a comment

Choose a reason for hiding this comment

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

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".

Comment on lines +268 to +269
/// The qubit which is being measured.
pub qubit: Qubit,
Copy link
Contributor

Choose a reason for hiding this comment

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

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.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

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.

Copy link
Contributor

@asaites asaites Sep 25, 2025

Choose a reason for hiding this comment

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

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.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Two separate thoughts:

  1. Re the existing DEFCAL MEASURE bug: 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.

  2. 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 None anyway, 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?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I included the DEFCAL MEASURE fix in #482. The discussion about point 2 can continue though!

Copy link
Contributor

Choose a reason for hiding this comment

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

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:

  1. Share the constructor with quil, with the new name parameter at the beginning of the list, making it a breaking change for quil (and potentially pyQuil, depending on when it lands).
  2. Share the constructor with quil, but add the new name parameter 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 the pyo3 signature to avoid a breaking change for quil.
  3. Add a new constructor specifically for quil, using the pyo3 signature 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?

Copy link
Contributor Author

@antalsz antalsz Sep 26, 2025

Choose a reason for hiding this comment

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

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.

antalsz added a commit that referenced this pull request Sep 26, 2025
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.
@asaites asaites force-pushed the 476-memory-dependencies-remember-writes branch 2 times, most recently from 3a7972c to aa6ceb0 Compare October 9, 2025 14:00
@antalsz antalsz force-pushed the 476-memory-dependencies-remember-writes branch from aa6ceb0 to 61d655b Compare October 9, 2025 20:42
Base automatically changed from 476-memory-dependencies-remember-writes to main October 9, 2025 21:22
@antalsz antalsz force-pushed the 478-named-measurements branch from 554d902 to d827767 Compare October 9, 2025 21:35
@asaites asaites merged commit 7a1cf37 into main Oct 10, 2025
19 checks passed
@asaites asaites deleted the 478-named-measurements branch October 10, 2025 19:00
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Provide an initial implementation of named measurements (quil-lang/quil#90) to shake out design issues

2 participants