Skip to content

Commit

Permalink
feat: cache program properties (#1661)
Browse files Browse the repository at this point in the history
  • Loading branch information
MarquessV committed Sep 18, 2023
1 parent 001c8df commit 5f061e5
Show file tree
Hide file tree
Showing 2 changed files with 53 additions and 7 deletions.
44 changes: 39 additions & 5 deletions pyquil/quil.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
import types
from collections import defaultdict
from copy import deepcopy
import functools
from typing import (
Any,
Callable,
Expand Down Expand Up @@ -104,6 +105,30 @@
RetType = TypeVar("RetType")


def _invalidates_cached_properties(func: Callable[..., RetType]) -> Callable[..., RetType]:
"""A decorator that will check a class for any cached properties and clear them.
This should be used on any `Program` method that changes the internal state of the program
so that the next call to proprety rebuilds the cache.
"""

@functools.wraps(func)
def wrapper(self: "Program", *args: Any, **kwargs: Any) -> RetType:
result = func(self, *args, **kwargs)
cls = type(self)
cached = {
attr
for attr in list(self.__dict__.keys())
if (descriptor := getattr(cls, attr, None))
if isinstance(descriptor, functools.cached_property)
}
for attr in cached:
del self.__dict__[attr]
return result

return wrapper


class Program:
"""
A list of pyQuil instructions that comprise a quantum program.
Expand All @@ -124,36 +149,40 @@ def __init__(self, *instructions: InstructionDesignator):

self.native_quil_metadata: Optional[NativeQuilMetadata] = None

@property
# The following properties are cached on the first call and won't be re-built unless cleared.
# Any method that mutates the state program should use the `@_invalidates_cached_properties`
# decorator to clear these caches.

@functools.cached_property
def calibrations(self) -> List[DefCalibration]:
"""A list of Quil-T calibration definitions."""
return [DefCalibration._from_rs_calibration(cal) for cal in self._program.calibrations.calibrations]

@property
@functools.cached_property
def measure_calibrations(self) -> List[DefMeasureCalibration]:
"""A list of measure calibrations"""
return [
DefMeasureCalibration._from_rs_measure_calibration_definition(cal)
for cal in self._program.calibrations.measure_calibrations
]

@property
@functools.cached_property
def waveforms(self) -> Dict[str, DefWaveform]:
"""A mapping from waveform names to their corresponding definitions."""
return {
name: DefWaveform._from_rs_waveform_definition(quil_rs.WaveformDefinition(name, waveform))
for name, waveform in self._program.waveforms.items()
}

@property
@functools.cached_property
def frames(self) -> Dict[Frame, DefFrame]:
"""A mapping from Quil-T frames to their definitions."""
return {
Frame._from_rs_frame_identifier(frame): DefFrame._from_rs_attribute_values(frame, attributes)
for frame, attributes in self._program.frames.get_all_frames().items()
}

@property
@functools.cached_property
def declarations(self) -> Dict[str, Declare]:
"""A mapping from declared region names to their declarations."""
return {name: Declare._from_rs_declaration(inst) for name, inst in self._program.declarations.items()}
Expand Down Expand Up @@ -206,6 +235,7 @@ def instructions(self, instructions: List[AbstractInstruction]) -> None:
new_program.inst(instructions)
self._program = new_program._program

@_invalidates_cached_properties
def inst(self, *instructions: Union[InstructionDesignator, RSProgram]) -> "Program":
"""
Mutates the Program object by appending new instructions.
Expand Down Expand Up @@ -268,6 +298,7 @@ def inst(self, *instructions: Union[InstructionDesignator, RSProgram]) -> "Progr

return self

@_invalidates_cached_properties
def resolve_placeholders(self) -> None:
"""
Resolve all label and qubit placeholders in the program using a default resolver that will generate
Expand Down Expand Up @@ -309,12 +340,14 @@ def rs_label_resolver(placeholder: quil_rs.TargetPlaceholder) -> Optional[str]:
target_resolver=rs_label_resolver, qubit_resolver=rs_qubit_resolver
)

@_invalidates_cached_properties
def resolve_qubit_placeholders(self) -> None:
"""
Resolve all qubit placeholders in the program.
"""
self._program.resolve_placeholders_with_custom_resolvers(target_resolver=lambda _: None)

@_invalidates_cached_properties
def resolve_qubit_placeholders_with_mapping(self, qubit_mapping: Dict[QubitPlaceholder, int]) -> None:
"""
Resolve all qubit placeholders in the program using a mapping of ``QubitPlaceholder``\\s to
Expand All @@ -331,6 +364,7 @@ def label_resolver(_: quil_rs.TargetPlaceholder) -> None:
qubit_resolver=qubit_resolver, target_resolver=label_resolver
)

@_invalidates_cached_properties
def resolve_label_placeholders(self) -> None:
"""
Resolve all label placeholders in the program.
Expand Down
16 changes: 14 additions & 2 deletions test/unit/test_quil.py
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,6 @@
merge_with_pauli_noise,
address_qubits,
get_classical_addresses_from_program,
Pragma,
validate_supported_quil,
)
from pyquil.quilatom import Frame, MemoryReference, Parameter, QubitPlaceholder, Sub, quil_cos, quil_sin
Expand All @@ -91,7 +90,6 @@
DefMeasureCalibration,
DefPermutationGate,
)
from test.unit.utils import parse_equals


def test_gate(snapshot):
Expand Down Expand Up @@ -1137,3 +1135,17 @@ def test_copy_everything_except_instructions():
assert len(program.instructions) == 0 # the purpose of copy_everything_except_instructions()
assert len(program.declarations) == 0 # this is a view on the instructions member; must be consistent


def test_cached_frames():
frames = [
DefFrame(Frame([Qubit(0)], "frame0"), center_frequency=432.0),
DefFrame(Frame([Qubit(1)], "frame1"), sample_rate=44100.0),
]

p = Program(frames[0])
program_frames = p.frames
assert program_frames == {frames[0].frame: frames[0]}

p.inst(frames[1])
program_frames = p.frames
assert program_frames == {frames[0].frame: frames[0], frames[1].frame: frames[1]}

0 comments on commit 5f061e5

Please sign in to comment.