Skip to content

Commit 7a1cf37

Browse files
authored
feat!: add named measurements (#479)
Adds an optional `!name` to `MEASURE` and `DEFCAL MEASURE and updates the corresponding calibration logic. As written, the change uses a careful Python signature so that the constructor changes are not actually a breaking change for `quil` users. This additionally required implementing `__getnewargs_ex__`. Finally, these changes exposed a small bug in the linter script, wherein it missed `pyo3_stub_gen` usages when written with a full path. --------- Co-authored-by: Alexander Saites <[email protected]> Closes: #478 BREAKING CHANGES: `quil-rs` users will need to modify `Measurement` and `MeasureCalibrationIdentifier` struct and constructor usage. `quil` users are unaffected.
1 parent a672934 commit 7a1cf37

16 files changed

+404
-27
lines changed

quil-rs/python/quil/instructions.pyi

Lines changed: 21 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1200,6 +1200,11 @@ class MeasureCalibrationDefinition:
12001200
@instructions.setter
12011201
def instructions(self, value: builtins.list[Instruction]) -> None: ...
12021202
@property
1203+
def name(self) -> typing.Optional[builtins.str]:
1204+
r"""
1205+
The Quil-T name of the measurement that this measure calibration definition is for, if any.
1206+
"""
1207+
@property
12031208
def qubit(self) -> Qubit:
12041209
r"""
12051210
The qubit that this measure calibration definition is for.
@@ -1222,6 +1227,16 @@ class MeasureCalibrationIdentifier:
12221227
A unique identifier for a measurement calibration definition within a program
12231228
"""
12241229
@property
1230+
def name(self) -> typing.Optional[builtins.str]:
1231+
r"""
1232+
The Quil-T name of the measurement, if any.
1233+
"""
1234+
@name.setter
1235+
def name(self, value: typing.Optional[builtins.str]) -> None:
1236+
r"""
1237+
The Quil-T name of the measurement, if any.
1238+
"""
1239+
@property
12251240
def qubit(self) -> Qubit:
12261241
r"""
12271242
The qubit which is being measured.
@@ -1248,21 +1263,23 @@ class MeasureCalibrationIdentifier:
12481263
If this is missing, this is a calibration for a measurement for effect.
12491264
"""
12501265
def __eq__(self, other:builtins.object) -> builtins.bool: ...
1251-
def __getnewargs__(self) -> tuple[Qubit, typing.Optional[builtins.str]]: ...
1252-
def __new__(cls, qubit:Qubit, target:typing.Optional[builtins.str]) -> MeasureCalibrationIdentifier: ...
1266+
def __getnewargs_ex__(self) -> tuple[tuple[Qubit, str | None], dict[str, str | None]]: ...
1267+
def __new__(cls, qubit:Qubit, target:typing.Optional[builtins.str], *, name:typing.Optional[builtins.str]=None) -> MeasureCalibrationIdentifier: ...
12531268
def __repr__(self) -> builtins.str: ...
12541269
def to_quil(self) -> builtins.str: ...
12551270
def to_quil_or_debug(self) -> builtins.str: ...
12561271

12571272
class Measurement:
1273+
@property
1274+
def name(self) -> typing.Optional[builtins.str]: ...
12581275
@property
12591276
def qubit(self) -> Qubit: ...
12601277
@property
12611278
def target(self) -> typing.Optional[MemoryReference]: ...
12621279
def __eq__(self, other:builtins.object) -> builtins.bool: ...
1263-
def __getnewargs__(self) -> tuple[Qubit, typing.Optional[MemoryReference]]: ...
1280+
def __getnewargs_ex__(self) -> tuple[tuple[Qubit, MemoryReference | None], dict[str, str | None]]: ...
12641281
def __hash__(self) -> builtins.int: ...
1265-
def __new__(cls, qubit:Qubit, target:typing.Optional[MemoryReference]) -> Measurement: ...
1282+
def __new__(cls, qubit:Qubit, target:typing.Optional[MemoryReference], *, name:typing.Optional[builtins.str]=None) -> Measurement: ...
12661283
def __repr__(self) -> builtins.str: ...
12671284
def to_quil(self) -> builtins.str: ...
12681285
def to_quil_or_debug(self) -> builtins.str: ...

quil-rs/scripts/pyo3_linter/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -347,7 +347,7 @@ def _cfg(regex: str, cond: str = r"[^,]+") -> str:
347347
)
348348
STUB_GEN_RE = re.compile(
349349
_cfg(
350-
r"gen_stub_py(class(?:(?:_complex)?_enum)?|methods|function)(?:\((\s*?[^)]+)\))?"
350+
r"(?:(?:pyo3_stub_gen::)?derive::)?gen_stub_py(class(?:(?:_complex)?_enum)?|methods|function)(?:\((\s*?[^)]+)\))?"
351351
)
352352
)
353353

quil-rs/src/instruction/calibration.rs

Lines changed: 94 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -262,6 +262,9 @@ impl Quil for MeasureCalibrationDefinition {
262262
pyo3::pyclass(module = "quil.instructions", eq, get_all, set_all, subclass)
263263
)]
264264
pub struct MeasureCalibrationIdentifier {
265+
/// The Quil-T name of the measurement, if any.
266+
pub name: Option<String>,
267+
265268
/// The qubit which is being measured.
266269
pub qubit: Qubit,
267270

@@ -272,18 +275,27 @@ pub struct MeasureCalibrationIdentifier {
272275
pub target: Option<String>,
273276
}
274277

275-
pickleable_new! {
276-
impl MeasureCalibrationIdentifier {
277-
pub fn new(qubit: Qubit, target: Option<String>);
278+
impl MeasureCalibrationIdentifier {
279+
pub const fn new(name: Option<String>, qubit: Qubit, target: Option<String>) -> Self {
280+
Self {
281+
name,
282+
qubit,
283+
target,
284+
}
278285
}
279286
}
280287

281288
impl CalibrationSignature for MeasureCalibrationIdentifier {
282-
type Signature<'a> = (&'a Qubit, Option<&'a str>);
289+
type Signature<'a> = (Option<&'a str>, &'a Qubit, Option<&'a str>);
283290

284291
fn signature(&self) -> Self::Signature<'_> {
285-
let Self { qubit, target } = self;
286-
(qubit, target.as_deref())
292+
let Self {
293+
name,
294+
qubit,
295+
target,
296+
} = self;
297+
298+
(name.as_deref(), qubit, target.as_deref())
287299
}
288300

289301
fn has_signature(&self, signature: &Self::Signature<'_>) -> bool {
@@ -297,9 +309,17 @@ impl Quil for MeasureCalibrationIdentifier {
297309
f: &mut impl std::fmt::Write,
298310
fall_back_to_debug: bool,
299311
) -> crate::quil::ToQuilResult<()> {
300-
let Self { qubit, target } = self;
312+
let Self {
313+
name,
314+
qubit,
315+
target,
316+
} = self;
301317

302-
write!(f, "DEFCAL MEASURE ")?;
318+
write!(f, "DEFCAL MEASURE")?;
319+
if let Some(name) = name {
320+
write!(f, "!{name}")?;
321+
}
322+
write!(f, " ")?;
303323
qubit.write(f, fall_back_to_debug)?;
304324
if let Some(target) = target {
305325
write!(f, " {target}")?;
@@ -324,6 +344,23 @@ mod test_measure_calibration_definition {
324344
"With Fixed Qubit",
325345
MeasureCalibrationDefinition {
326346
identifier: MeasureCalibrationIdentifier {
347+
name: None,
348+
qubit: Qubit::Fixed(0),
349+
target: Some("theta".to_string()),
350+
},
351+
instructions: vec![Instruction::Gate(Gate {
352+
name: "X".to_string(),
353+
parameters: vec![Expression::Variable("theta".to_string())],
354+
qubits: vec![Qubit::Fixed(0)],
355+
modifiers: vec![],
356+
357+
})]},
358+
)]
359+
#[case(
360+
"Named With Fixed Qubit",
361+
MeasureCalibrationDefinition {
362+
identifier: MeasureCalibrationIdentifier {
363+
name: Some("midcircuit".to_string()),
327364
qubit: Qubit::Fixed(0),
328365
target: Some("theta".to_string()),
329366
},
@@ -339,6 +376,23 @@ mod test_measure_calibration_definition {
339376
"Effect With Fixed Qubit",
340377
MeasureCalibrationDefinition {
341378
identifier: MeasureCalibrationIdentifier {
379+
name: None,
380+
qubit: Qubit::Fixed(0),
381+
target: None,
382+
},
383+
instructions: vec![Instruction::Gate(Gate {
384+
name: "X".to_string(),
385+
parameters: vec![Expression::PiConstant()],
386+
qubits: vec![Qubit::Fixed(0)],
387+
modifiers: vec![],
388+
389+
})]},
390+
)]
391+
#[case(
392+
"Named Effect With Fixed Qubit",
393+
MeasureCalibrationDefinition {
394+
identifier: MeasureCalibrationIdentifier {
395+
name: Some("midcircuit".to_string()),
342396
qubit: Qubit::Fixed(0),
343397
target: None,
344398
},
@@ -354,6 +408,22 @@ mod test_measure_calibration_definition {
354408
"With Variable Qubit",
355409
MeasureCalibrationDefinition {
356410
identifier: MeasureCalibrationIdentifier {
411+
name: None,
412+
qubit: Qubit::Variable("q".to_string()),
413+
target: Some("theta".to_string()),
414+
},
415+
instructions: vec![Instruction::Gate(Gate {
416+
name: "X".to_string(),
417+
parameters: vec![Expression::Variable("theta".to_string())],
418+
qubits: vec![Qubit::Variable("q".to_string())],
419+
modifiers: vec![],
420+
})]},
421+
)]
422+
#[case(
423+
"Named With Variable Qubit",
424+
MeasureCalibrationDefinition {
425+
identifier: MeasureCalibrationIdentifier {
426+
name: Some("midcircuit".to_string()),
357427
qubit: Qubit::Variable("q".to_string()),
358428
target: Some("theta".to_string()),
359429
},
@@ -368,6 +438,22 @@ mod test_measure_calibration_definition {
368438
"Effect Variable Qubit",
369439
MeasureCalibrationDefinition {
370440
identifier: MeasureCalibrationIdentifier {
441+
name: None,
442+
qubit: Qubit::Variable("q".to_string()),
443+
target: None,
444+
},
445+
instructions: vec![Instruction::Gate(Gate {
446+
name: "X".to_string(),
447+
parameters: vec![Expression::PiConstant()],
448+
qubits: vec![Qubit::Variable("q".to_string())],
449+
modifiers: vec![],
450+
})]},
451+
)]
452+
#[case(
453+
"Named Effect Variable Qubit",
454+
MeasureCalibrationDefinition {
455+
identifier: MeasureCalibrationIdentifier {
456+
name: Some("midcircuit".to_string()),
371457
qubit: Qubit::Variable("q".to_string()),
372458
target: None,
373459
},

quil-rs/src/instruction/measurement.rs

Lines changed: 22 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
#[cfg(feature = "stubs")]
22
use pyo3_stub_gen::derive::gen_stub_pyclass;
33

4-
use crate::{pickleable_new, quil::Quil};
4+
use crate::quil::Quil;
55

66
use super::{MemoryReference, Qubit};
77

@@ -12,13 +12,18 @@ use super::{MemoryReference, Qubit};
1212
pyo3::pyclass(module = "quil.instructions", eq, frozen, hash, get_all, subclass)
1313
)]
1414
pub struct Measurement {
15+
pub name: Option<String>,
1516
pub qubit: Qubit,
1617
pub target: Option<MemoryReference>,
1718
}
1819

19-
pickleable_new! {
20-
impl Measurement {
21-
pub fn new(qubit: Qubit, target: Option<MemoryReference>);
20+
impl Measurement {
21+
pub const fn new(name: Option<String>, qubit: Qubit, target: Option<MemoryReference>) -> Self {
22+
Self {
23+
name,
24+
qubit,
25+
target,
26+
}
2227
}
2328
}
2429

@@ -28,9 +33,19 @@ impl Quil for Measurement {
2833
writer: &mut impl std::fmt::Write,
2934
fall_back_to_debug: bool,
3035
) -> Result<(), crate::quil::ToQuilError> {
31-
write!(writer, "MEASURE ")?;
32-
self.qubit.write(writer, fall_back_to_debug)?;
33-
if let Some(target) = &self.target {
36+
let Self {
37+
name,
38+
qubit,
39+
target,
40+
} = self;
41+
42+
write!(writer, "MEASURE")?;
43+
if let Some(name) = name {
44+
write!(writer, "!{name}")?;
45+
}
46+
write!(writer, " ")?;
47+
qubit.write(writer, fall_back_to_debug)?;
48+
if let Some(target) = target {
3449
write!(writer, " ")?;
3550
target.write(writer, fall_back_to_debug)?;
3651
}

quil-rs/src/instruction/quilpy.rs

Lines changed: 70 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,10 @@
11
use num_complex::Complex64;
22
use numpy::{PyArray2, ToPyArray};
33
use paste::paste;
4-
use pyo3::{prelude::*, types::PyTuple};
4+
use pyo3::{
5+
prelude::*,
6+
types::{IntoPyDict as _, PyDict, PyTuple},
7+
};
58

69
use super::*;
710
use crate::{
@@ -512,6 +515,12 @@ impl GateSpecification {
512515
#[cfg_attr(feature = "stubs", gen_stub_pymethods)]
513516
#[pymethods]
514517
impl MeasureCalibrationDefinition {
518+
/// The Quil-T name of the measurement that this measure calibration definition is for, if any.
519+
#[getter]
520+
fn name(&self) -> Option<&str> {
521+
self.identifier.name.as_deref()
522+
}
523+
515524
/// The qubit that this measure calibration definition is for.
516525
#[getter]
517526
fn qubit(&self) -> Qubit {
@@ -526,6 +535,66 @@ impl MeasureCalibrationDefinition {
526535
}
527536
}
528537

538+
// We don't use [`pickleable_new!`] here because we're separating Rust's
539+
// [`MeasureCalibrationIdentifier::new`] and Python's `MeasureCalibrationIdentifier.new`.
540+
#[cfg_attr(not(feature = "stubs"), optipy::strip_pyo3(only_stubs))]
541+
#[cfg_attr(feature = "stubs", gen_stub_pymethods)]
542+
#[pymethods]
543+
impl MeasureCalibrationIdentifier {
544+
// Note that the Python argument order is not the same as the Rust argument order for
545+
// [`Self::new`], and that this function requires keywords on the Python side! Make sure
546+
// `__getnewargs_ex__` is consistent with `__new__`!
547+
#[pyo3(signature = (qubit, target, *, name = None))]
548+
#[new]
549+
fn __new__(qubit: Qubit, target: Option<String>, name: Option<String>) -> Self {
550+
Self::new(name, qubit, target)
551+
}
552+
553+
#[gen_stub(override_return_type(
554+
type_repr = "tuple[tuple[Qubit, str | None], dict[str, str | None]]"
555+
))]
556+
fn __getnewargs_ex__<'py>(&self, py: Python<'py>) -> PyResult<Bound<'py, PyTuple>> {
557+
let Self {
558+
name,
559+
qubit,
560+
target,
561+
} = self;
562+
let positional: Bound<'py, PyTuple> = (qubit.clone(), target.clone()).into_pyobject(py)?;
563+
let keyword: Bound<'py, PyDict> = [("name", name)].into_py_dict(py)?;
564+
(positional, keyword).into_pyobject(py)
565+
}
566+
}
567+
568+
// We don't use [`pickleable_new!`] here because we're separating Rust's [`Measurement::new`] and
569+
// Python's `Measurement.new`.
570+
#[cfg_attr(not(feature = "stubs"), optipy::strip_pyo3(only_stubs))]
571+
#[cfg_attr(feature = "stubs", gen_stub_pymethods)]
572+
#[pymethods]
573+
impl Measurement {
574+
// Note that the Python argument order is not the same as the Rust argument order for
575+
// [`Self::new`], and that this function requires keywords on the Python side! Make sure
576+
// `__getnewargs_ex__` is consistent with `__new__`!
577+
#[pyo3(signature = (qubit, target, *, name = None))]
578+
#[new]
579+
fn __new__(qubit: Qubit, target: Option<MemoryReference>, name: Option<String>) -> Self {
580+
Self::new(name, qubit, target)
581+
}
582+
583+
#[gen_stub(override_return_type(
584+
type_repr = "tuple[tuple[Qubit, MemoryReference | None], dict[str, str | None]]"
585+
))]
586+
fn __getnewargs_ex__<'py>(&self, py: Python<'py>) -> PyResult<Bound<'py, PyTuple>> {
587+
let Self {
588+
name,
589+
qubit,
590+
target,
591+
} = self;
592+
let positional: Bound<'py, PyTuple> = (qubit.clone(), target.clone()).into_pyobject(py)?;
593+
let keyword: Bound<'py, PyDict> = [("name", name)].into_py_dict(py)?;
594+
(positional, keyword).into_pyobject(py)
595+
}
596+
}
597+
529598
#[cfg_attr(feature = "stubs", gen_stub_pymethods)]
530599
#[pymethods]
531600
impl Sharing {
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
---
2+
source: quil-rs/src/instruction/calibration.rs
3+
expression: measure_cal_def.to_quil_or_debug()
4+
---
5+
DEFCAL MEASURE!midcircuit q:
6+
X(pi) q
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
---
2+
source: quil-rs/src/instruction/calibration.rs
3+
expression: measure_cal_def.to_quil_or_debug()
4+
---
5+
DEFCAL MEASURE!midcircuit 0:
6+
X(pi) 0
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
---
2+
source: quil-rs/src/instruction/calibration.rs
3+
expression: measure_cal_def.to_quil_or_debug()
4+
---
5+
DEFCAL MEASURE!midcircuit 0 theta:
6+
X(%theta) 0
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
---
2+
source: quil-rs/src/instruction/calibration.rs
3+
expression: measure_cal_def.to_quil_or_debug()
4+
---
5+
DEFCAL MEASURE!midcircuit q theta:
6+
X(%theta) q

0 commit comments

Comments
 (0)