Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
32 changes: 32 additions & 0 deletions quil-py/quil/program/__init__.pyi
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,9 @@ class Program:
def memory_regions(self, memory_regions: Dict[str, MemoryRegion]): ...
@property
def declarations(self) -> Dict[str, Declaration]: ...
def freeze(self) -> FrozenProgram:
"""Return a copy of this `Program` as an immutable `FrozenProgram`"""

def dagger(self) -> "Program":
"""Creates a new conjugate transpose of the ``Program`` by reversing the order of gate instructions and applying the DAGGER modifier to each.

Expand Down Expand Up @@ -165,6 +168,35 @@ class Program:
.. _control flow graph: https://en.wikipedia.org/wiki/Control-flow_graph
"""

@final
class FrozenProgram:
"""This class is an immutable version of the `Program` class. Once an instance of this class is created, its contents cannot be modified.

Immutability is ensured by not providing any methods that could alter the instance, either in Python or through underlying Rust bindings.
"""

def __new__(cls, program: Program) -> "FrozenProgram":
"""Create a new instance of a `FrozenProgram` using a `Program`."""

@staticmethod
def from_quil(quil: str) -> "FrozenProgram":
"""Create a new instance of a `FrozenProgram` using a Quil string. Raises an error if the Quil program is not valid."""

def to_quil(self) -> str:
"""Attempt to convert the instruction to a valid Quil string.

Raises an exception if the instruction can't be converted to valid Quil.
"""
...
def to_quil_or_debug(self) -> str:
"""Convert the instruction to a Quil string.

If any part of the instruction can't be converted to valid Quil, it will be printed in a human-readable debug format.
"""
...
def as_mutable_program(self) -> Program:
"""Return a copy of this `FrozenProgram` as a mutable `Program`."""

class BasicBlock:
def __new__(cls, instance: "BasicBlock") -> Self:
"""Create a new instance of a `BasicBlock` (or a subclass) using an existing instance."""
Expand Down
59 changes: 58 additions & 1 deletion quil-py/src/program/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ use std::{

use indexmap::IndexMap;
use numpy::{PyArray2, ToPyArray};
use pyo3::types::PyTuple;
use quil_rs::{
instruction::{Instruction, QubitPlaceholder, TargetPlaceholder, Waveform},
program::{
Expand Down Expand Up @@ -347,6 +348,10 @@ impl PyProgram {
.resolve_placeholders_with_custom_resolvers(rs_target_resolver, rs_qubit_resolver);
}

pub fn freeze(&self) -> FrozenProgram {
FrozenProgram::new(self.clone())
}

pub fn __add__(&self, py: Python<'_>, rhs: Self) -> PyResult<Self> {
let new = self.as_inner().clone() + rhs.as_inner().clone();
new.to_python(py)
Expand All @@ -373,6 +378,58 @@ impl PyProgram {
}
}

#[pyclass]
#[derive(Debug, Clone, PartialEq)]
pub struct FrozenProgram {
program: PyProgram,
}
impl_eq!(FrozenProgram);
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Question: The __richcmp__ implementation this macro generates will only compare two FrozenPrograms with equivalent inner Programs as equal.

What about the case where a FrozenProgram is compared to a Program. If the inner program of a FrozenProgram instance is equivalent to an instance of Program, should they evaluate as equivalent?

Choose a reason for hiding this comment

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

I'm flexible. If converting a Program to a FrozenProgram is a light operation (it encapsulates the Program and exposes a limited immutable interface but does not deeply copy the program) then at the level of quil-py I would adhere to its Rust heritage and force the user to write "if frozen_program == FrozenProgram(program):".

How this is net exposed in pyquil is a separate question.

Choose a reason for hiding this comment

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

Strike that ill-informed comment of mine.

I can see it takes the clone. Of course it needs to as the user has a mutable reference to it already.

Let me think about the performance implications of typical workflows here and get back to you.


#[pymethods]
impl FrozenProgram {
#[new]
fn new(program: PyProgram) -> Self {
Self { program }
}

#[staticmethod]
fn from_quil(quil: &str) -> PyResult<Self> {
Ok(Self {
program: PyProgram(
Program::from_str(quil).map_err(|e| PyValueError::new_err(e.to_string()))?,
),
})
}

fn to_quil(&self) -> PyResult<String> {
self.program.to_quil().map_err(|e| {
PyValueError::new_err(format!("Program could not be converted to valid quil: {e}"))
})
}

fn to_quil_or_debug(&self) -> String {
self.program.to_quil_or_debug()
}

fn as_mutable_program(&self) -> PyProgram {
self.program.clone()
}

fn __reduce__<'py>(&'py self, py: Python<'py>) -> PyResult<&'py PyAny> {
let callable = py.get_type::<Self>().getattr("from_quil")?;
let args = pyo3::types::PyTuple::new(py, &[self.to_quil()?.into_py(py)]);
Ok(PyTuple::new(py, [callable, args]))
}

fn __repr__(&self) -> String {
format!("{self:?}")
}

fn __str__(&self) -> String {
self.to_quil_or_debug()
}
}

create_init_submodule! {
classes: [ PyFrameSet, PyProgram, PyCalibrationSet, PyMemoryRegion, PyBasicBlock, PyControlFlowGraph, PyScheduleSeconds, PyScheduleSecondsItem, PyTimeSpanSeconds ],
classes: [ PyFrameSet, PyProgram, PyCalibrationSet, PyMemoryRegion, PyBasicBlock, PyControlFlowGraph, PyScheduleSeconds, PyScheduleSecondsItem, PyTimeSpanSeconds, FrozenProgram ],
}
11 changes: 10 additions & 1 deletion quil-py/test/program/test_program.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
from syrupy.assertion import SnapshotAssertion

from quil.instructions import Gate, Instruction, Jump, Qubit, QubitPlaceholder, Target, TargetPlaceholder
from quil.program import Program
from quil.program import Program, FrozenProgram


def test_pickle():
Expand Down Expand Up @@ -186,3 +186,12 @@ def test_filter_instructions(snapshot: SnapshotAssertion):
program = Program.parse(input)
program_without_quil_t = program.filter_instructions(lambda instruction: not instruction.is_quil_t())
assert program_without_quil_t.to_quil() == snapshot


class TestFrozenProgram:
def test_freeze_program(self):
program = Program.parse("H 0")
frozen = program.freeze()
assert isinstance(frozen, FrozenProgram)
assert frozen.to_quil() == program.to_quil()
assert frozen.as_mutable_program() == program