-
Notifications
You must be signed in to change notification settings - Fork 10
feat!: defgate as sequence #460
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
Conversation
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Other comments (7)
-
quil-rs/src/parser/common.rs (274-274)
There's an issue with the error message in `parse_argument_qubit`. The code is using `stringify!($expected_variant)` but there's no macro parameter named `$expected_variant` defined in this context. This will cause a compilation error.
expected_token!(input, other_token, "an identifier".to_owned()) - quil-rs/src/instruction/gate_sequence.rs (87-87) The call to `Gate::new()` uses `unwrap()` which could cause a panic if the gate creation fails. Consider handling this error case explicitly.
- quil-rs/src/program/mod.rs (343-343) There's a FIXME comment about dropping expanded gate definitions. Consider addressing this before finalizing the PR to ensure proper cleanup of gate definitions that have been expanded.
- quil-rs/src/program/defgate_sequence_expansion.rs (117-117) The `gate_sequence_from_instruction` function takes a mutable reference to `seen` but doesn't modify it. Consider changing the parameter to an immutable reference for clarity.
- quil-rs/src/program/defgate_sequence_expansion.rs (80-82) In the recursive calls to expansion functions, you're cloning `seen` and then passing a mutable reference to the clone. This approach is inconsistent with how `seen` is used in the parent function. Consider either passing the original `seen` (if you want changes to propagate up) or using the clone directly without passing it as mutable (if you want isolation).
- quil-rs/src/instruction/gate_sequence.rs (25-25) There's a typo in the error variant name: `GateQubitArugment` should be `GateQubitArgument`.
- quil-rs/src/program/defgate_sequence_expansion.rs (130-130) There's a typo in the variable name - it has an equals sign attached: `source=`. This should be `source =`.
💡 To request another review, post a new comment with "/windsurf-review".
antalsz
left a comment
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The biggest thing I find lacking from this PR is guidance on how it's going to be entered. Can you write up an example in a doc comment somewhere showing how you'd perform full program expansion, soup to nuts?
The other thing I find surprising is that there's no "just expand everything" option. There are two almost-identical expansion operations which return completely disjoint source map types, when I'd expect you'd mostly just want to do all the expansion at once. It feels like it would be possible to unify those operations and provide an expand function, which would avoid the need to merge source maps.
The problem, of course, is that it's ambiguous which operation should "win" if there are conflicting DEFGATE AS SEQUENCE and DEFCALs. I can see four approaches:
- Error if a single instruction could be expanded with both a
SEQUENCEand aDEFCAL. - Provide a way of configuring what should happen if there's an ambiguity: prefer the
SEQUENCE, prefer theDEFCAL, or error. - As the former, but per-gate.
- Provide exactly one resolution b behavior – probably "prefer the
SEQUENCE". Document it.
Those are arranged in order of preference.
The motivation for this work is to define a cycle's calibration and unitary definition within the same program. In this context, calibration and gate definition expansion are orthogonal to each other:
Gate sequences could also be used as a convenience function for describing a program compactly: Note that the order of desired expansion is reversed here. In the first example, we want to expand calibrations and not expand gate sequences. In this example, we must expand the gate sequence definition before calibrations. There is no presumption to make on behalf of the user here, so if they want to expand everything, they should programmatically do just that. I can certainly add a documentation example with these examples. |
6bdae28 to
8ce18b0
Compare
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Apologies for the delay, but the resulting changes look great – I have some very minor comments, but then I think we're good to merge! 🚀
quil-py/example.py
Outdated
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This file appears to be duplicated later in a Rust and a Python doc comment. Perhaps it should just live in those doc comments? On the Rust side, it's possible to include a file in a doc comment, but I don't know if it is or not in Python. A note to the developers to keep things in sync would also be nice, I think – again, this is the sort of thing that #462 will help with.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'll make the comment and take this file out.
I didn't intend to version this file, but the reason I wrote it in the first place was because I couldn't get the doctests to actually run through pytest:
_______________________________________________________________________ ERROR collecting quil/validation/identifier.py ________________________________________________________________________
.venv/lib/python3.11/site-packages/_pytest/runner.py:344: in from_call
result: TResult | None = func()
^^^^^^
.venv/lib/python3.11/site-packages/_pytest/runner.py:389: in collect
return list(collector.collect())
^^^^^^^^^^^^^^^^^^^^^^^^^
.venv/lib/python3.11/site-packages/_pytest/doctest.py:566: in collect
module = self.obj
^^^^^^^^
.venv/lib/python3.11/site-packages/_pytest/python.py:280: in obj
self._obj = obj = self._getobj()
^^^^^^^^^^^^^^
.venv/lib/python3.11/site-packages/_pytest/python.py:551: in _getobj
return importtestmodule(self.path, self.config)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
.venv/lib/python3.11/site-packages/_pytest/python.py:498: in importtestmodule
mod = import_path(
.venv/lib/python3.11/site-packages/_pytest/pathlib.py:595: in import_path
module_file = mod.__file__
^^^^^^^^^^^^
E AttributeError: module 'quil.validation.identifier' has no attribute '__file__'
=================================================================================== short test summary info ===================================================================================
ERROR quil/validation/identifier.py - AttributeError: module 'quil.validation.identifier' has no attribute '__file__'I believe this is a fundamental issue with stubs and extension modules, but it's been a while since I looked into it.
ef4b0ab to
ee6f91e
Compare
asaites
left a comment
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thank you for your hard work on this! And my apologies it's taken me so long to review it.
The bones here are good! I particularly like the idea of uniting the source maps, but there are some rough spots to the API that would benefit from some massaging for long-term maintainability. I need a bit more time to digest some aspects of the code, and I still intend to help rebase this on the merged crates (which should help simplify some aspects). I didn't want to make you wait longer, though, so I'm submitting this initial review.
| @final | ||
| class GateSignature: | ||
| """A signature for a gate definition; this does not include the gate definition content. | ||
| To get a signature from a definition, use `GateDefinition.signature`. | ||
| """ | ||
| def __new__(cls, name: str, gate_parameters: List[str], qubit_parameters: List[str], gate_type: GateType) -> Self: ... | ||
|
|
||
| @final | ||
| class DefGateSequence: | ||
| """A sequence of gates that make up a defined gate (i.e. with `DEFGATE ... AS SEQUENCE`).""" | ||
|
|
||
| def __new__(cls, qubits: List[str], gates: List[Gate]) -> Self: | ||
| """Creates a new `DefGateSequence` with the given qubits and gates. | ||
| :param qubits: A list of qubit names that the gates in the sequence will act on. | ||
| :param gates: A list of `Gate` objects that make up the sequence. Each gate must reference | ||
| qubits in the `qubits` list by name. They may not specify a fixed qubit. | ||
| """ | ||
| ... | ||
|
|
||
| @property | ||
| def qubits(self) -> List[str]: | ||
| """Returns the list of qubit variable names in the gate signature.""" | ||
| ... | ||
|
|
||
| @property | ||
| def gates(self) -> List[Gate]: | ||
| """Returns the list of `Gate` objects that make up the sequence.""" | ||
| ... | ||
|
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
When this branch gets updated to be compatible with the merged quil-py/quil-rs crates, these files will be generated automatically from the Rust source, so this documentation will need to move to their Rust counterparts (with quil-rs specifically, not wrappers for them in quil-py).
07c11d5 to
e3d5005
Compare
e3d5005 to
043e9e2
Compare
asaites
left a comment
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I've made some suggestions based on the changes, and implemented them here. I think there's still room for improvement on the implementation details of the defgate_sequence_expansion, particularly regarding the allocation of Vecs that likely could be done by passing along a target vec to fill -- but if you're happy with the API, then we can address that if it becomes a performance problem in the future.
quil-rs/src/instruction/gate.rs
Outdated
| } | ||
| } | ||
| GateSpecification::Sequence(sequence) => { | ||
| for gate in sequence.gates.iter() { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Nit/clippy suggestion:
| for gate in sequence.gates.iter() { | |
| for gate in &sequence.gates { |
| let gate_parameters = gate | ||
| .parameters | ||
| .iter() | ||
| .cloned() |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
These don't actually need to be cloned().
| .cloned() |
quil-rs/src/instruction/mod.rs
Outdated
| f, | ||
| parameters | ||
| .iter() | ||
| .map(|p| p.as_ref()) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Nit:
| .map(|p| p.as_ref()) | |
| .map(AsRef::as_ref) |
| gate_expansion_stack: &mut IndexSet<String>, | ||
| ) -> Result<Vec<Instruction>, DefGateSequenceExpansionError> { | ||
| let mut target_instructions = vec![]; | ||
| for source_instruction in source_instructions.iter() { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
| for source_instruction in source_instructions.iter() { | |
| for source_instruction in source_instructions { |
| where | ||
| TargetIndex: SourceMapIndexable<QueryIndex>, | ||
| { | ||
| self.entries |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Obviously this was here before, but while we're making this change, I think this would be a lot cleaner as the (equivalent) filter followed by map:
self.entries
.iter()
.filter(|&entry| entry.target_location().contains(target_index))
.map(SourceMapEntry::source_location)
.collect()| fn gate_sequence_from_instruction( | ||
| &self, | ||
| instruction: &Instruction, | ||
| gate_expansion_stack: &mut IndexSet<String>, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
| gate_expansion_stack: &mut IndexSet<String>, | |
| gate_expansion_stack: &IndexSet<String>, |
| let mut gate_expansion_stack = gate_expansion_stack.clone(); | ||
| gate_expansion_stack.insert(gate_sequence_signature.name().to_string()); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Since you're constructing a new one here and then dropping it at the end of the block, it doesn't actually need to be a &mut reference parameter to the function. That said, it probably makes more sense to keep it that way, but avoid the clone here. In order to do that, you'd need to check the result of this insert and (if it's true), you'd pop the name back off the stack once the recursive call finishes. But since that might be easy to mess up, I'd actually recommend making a little wrapper and giving it a method that manages that for the caller:
struct ExpansionStack(IndexSet<String>);
impl ExpansionStack {
fn new() -> Self {
Self(IndexSet::new())
}
/// Check if the name is in the stack and, if so, return an error.
fn check(&self, name: impl AsRef<str>) -> Result<(), DefGateSequenceExpansionError> {
if self.0.contains(name.as_ref()) {
let cycle = self.0.iter().cloned().collect();
Err(DefGateSequenceExpansionError::CyclicSequenceGateDefinition(cycle))
} else {
Ok(())
}
}
/// Execute a closure with an gate added to the stack.
fn with_gate_sequence<F, R>(&mut self, name: String, f: F) -> R
where
F: FnOnce(&mut Self) -> R,
{
let must_pop = self.0.insert(name);
let result = f(self);
if must_pop {
self.0.pop();
}
result
}
}Then these signatures would accept &mut ExpansionStack and use something like this:
let mut recursive_source_map = SourceMap::default();
let recursive_target_gate_instructions = gate_expansion_stack.with_gate_sequence(
gate_sequence_signature.name().to_string(),
|gate_expansion_stack| self.expand_with_source_map_impl(
&target_gate_instructions,
&mut recursive_source_map,
gate_expansion_stack,
)
)?;
quil-rs/src/program/mod.rs
Outdated
| .iter() | ||
| .filter(|(name, _)| !filter(name)) | ||
| { | ||
| for (gate_name, (j, _)) in gate_sequence_definitions.iter() { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
| for (gate_name, (j, _)) in gate_sequence_definitions.iter() { | |
| for (gate_name, (j, _)) in &gate_sequence_definitions { |
quil-rs/src/program/quilpy.rs
Outdated
| #[derive(Debug)] | ||
| #[cfg_attr(feature = "stubs", gen_stub_pyclass)] | ||
| #[pyclass(module = "quil.program", frozen)] | ||
| pub struct DefGateExpansionFilter { | ||
| filter: Py<PyFunction>, | ||
| on_error: Py<PyFunction>, | ||
| } | ||
|
|
||
| #[pymethods] | ||
| impl DefGateExpansionFilter { | ||
| #[new] | ||
| #[pyo3(signature = (/, filter, on_error))] | ||
| fn new(filter: Py<PyFunction>, on_error: Py<PyFunction>) -> Self { | ||
| Self { filter, on_error } | ||
| } | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Although this works, I recommend we just accept a single Callable for filtering, and not have any an on_error of any sort. Here's my reasoning:
- The
filter_instructionsmethod already works this way, and from an API perspective, it'd be nicer for these to have the same form. Changing the existing one is a more impactful than having this match it. - For a similar reason, we should probably call this
predicaterather thanfilter; that's also a little nicer sincefilteris a Python builtin function. - Since this function comes from the user, if there's an error while calling it, it's almost certainly a bug in their application and something they should address.
- If they do have a reason to pass a filter that might return an invalid value, they have other options for handling it.
- Calling
on_errorcould cause apanicjust as well. - If our code fails for reasons unrelated to calling their function, we certainly want a noisy crash.
So my recommendation is to just accept a predicate argument and panic on mistakes.
* style: use same predicate semantics across API Currently `filter_instructions` uses the name "predicate", an ordinary Python `Callable`, and bubbles errors as `PanicException`s. This edits the new defgate functions to use the same naming and semantics. It also introduces a `call_predicate` function to simplify the common logic associated with calling those predicates, and refactors the relevant to code to make use of it. * style: refactor defgate seq expander with stack * style: apply clippy suggestions * chore: regenerate stubs * style: remove line * fix: self import for python 3.10 --------- Co-authored-by: Alexander Saites <[email protected]>
Closes #447
New Features
Refactor
Program::expand_calibrations_with_source_mapto returnSourceMap<InstructionIndex, InstructionTarget<CalibrationExpansion>>.enum InstructionTargetShimto cover all known instruction targets and returnInstructionSourceMap = SourceMap<std::ops::Range<usize>, InstructionTargetSim>.Notes on approach
There's a lot of ways to slice this pie, but the goals I decided on were to:
quil-pythat can be extended to support Python constructed source maps without a breaking change (or at least none other than handling an additionalInstructionTargetShimvariant.quil-pythat is forward compatible with passes that excise instructions or operate on multiple source instructions (accomplished by using aSourceIndextype ofstd::ops::Range<usize>).