Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
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
62 changes: 62 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,67 @@
# Changelog

## 5.0.0-rc.2 (2025-10-06)

### Breaking Changes

#### update to new quil-rs

### Features

#### add e2e marker

#### improve program instruction iteration

#### update quil to rc3

#### use SCREAMING_SNAKE_CASE for quil enums

### Fixes

#### update packages with detected vulnerabilities (#1834)

#### clarify what printing a compiled program's output looks like for different targets (#1833)

#### Frame.duration should return a float, not Expression

## 5.0.0-rc.1 (2025-09-30)

### Breaking Changes

#### update to new quil-rs

### Features

#### add e2e marker

#### improve program instruction iteration

#### update quil to rc3

### Fixes

#### update packages with detected vulnerabilities (#1834)

#### clarify what printing a compiled program's output looks like for different targets (#1833)

## 5.0.0-rc.0 (2025-09-30)

### Breaking Changes

#### update to new quil-rs

### Features

#### add e2e marker

#### improve program instruction iteration

### Fixes

#### update packages with detected vulnerabilities (#1834)

#### clarify what printing a compiled program's output looks like for different targets (#1833)

## 4.16.3-rc.1 (2025-09-10)

### Fixes
Expand Down
2,784 changes: 1,674 additions & 1,110 deletions poetry.lock

Large diffs are not rendered by default.

10 changes: 5 additions & 5 deletions pyproject.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[tool.poetry]
name = "pyquil"
version = "4.16.3-rc.1"
version = "5.0.0-rc.2"
description = "A Python library for creating Quantum Instruction Language (Quil) programs."
authors = ["Rigetti Computing <[email protected]>"]
readme = "README.md"
Expand All @@ -10,24 +10,24 @@ license = "Apache-2.0"
classifiers = [
"Development Status :: 5 - Production/Stable",
"License :: OSI Approved :: Apache Software License",
"Programming Language :: Python :: 3.9",
"Programming Language :: Python :: 3.10",
"Programming Language :: Python :: 3.11",
"Programming Language :: Python :: 3.12",
"Programming Language :: Python :: 3.13",
"Operating System :: OS Independent",
]
keywords = ["quantum", "quil", "programming", "hybrid"]
packages = [{ include = "pyquil" }]
exclude = ["pyquil/conftest.py"]

[tool.poetry.dependencies]
python = "^3.9,<4"
python = "^3.10,<4"
numpy = ">=1.26,<3"
scipy = "^1.11"
rpcq = "^3.11.0"
networkx = ">=2.5"
qcs-sdk-python = ">=0.20.1"
quil = ">=0.15.3"
qcs-sdk-python = "=0.21.*"
quil = "^0.33.0rc4"
packaging = ">=23.1"
deprecated = "^1.2.14"
types-deprecated = "^1.2.9.3"
Expand Down
8 changes: 5 additions & 3 deletions pyquil/control_flow_graph.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,15 +26,17 @@ def _from_rs(cls, block: quil_rs.BasicBlock) -> Self:
return super().__new__(cls, block)

@override
@property
def instructions(self) -> list[AbstractInstruction]: # type: ignore[override]
return _convert_to_py_instructions(super().instructions())
return _convert_to_py_instructions(super().instructions)

@override
@property
def terminator(self) -> Optional[AbstractInstruction]: # type: ignore[override]
inst = super().terminator()
inst = super().terminator
if inst is None:
return None
return _convert_to_py_instruction(super().terminator())
return _convert_to_py_instruction(inst)


class ControlFlowGraph(quil_rs.ControlFlowGraph):
Expand Down
141 changes: 74 additions & 67 deletions pyquil/quil.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
from collections import defaultdict
from collections.abc import Generator, Iterable, Iterator, Sequence
from copy import deepcopy
from itertools import chain
from typing import (
Any,
Callable,
Expand Down Expand Up @@ -190,7 +191,7 @@ def copy_everything_except_instructions(self) -> "Program":
self.calibrations,
self.defined_gates,
self.measure_calibrations,
[quil_rs.Instruction.from_pragma(pragma) for pragma in self._program.pragma_extern_map.values()],
[quil_rs.Instruction.Pragma(pragma) for pragma in self._program.pragma_extern_map.values()],
)
if self.native_quil_metadata is not None:
new_prog.native_quil_metadata = deepcopy(self.native_quil_metadata)
Expand All @@ -215,7 +216,7 @@ def defined_gates(self) -> list[DefGate]:
@property
def instructions(self) -> list[AbstractInstruction]:
"""Fill in any placeholders and return a list of quil AbstractInstructions."""
return list(self.declarations.values()) + _convert_to_py_instructions(self._program.body_instructions)
return list(self)

@instructions.setter
def instructions(self, instructions: list[AbstractInstruction]) -> None:
Expand Down Expand Up @@ -299,7 +300,7 @@ def inst(self, *instructions: Union[InstructionDesignator, RSProgram]) -> "Progr
self._program += instruction
else:
try:
instruction = quil_rs.Instruction(instruction) # type: ignore
instruction = _convert_to_rs_instruction(instruction)
self.inst(instruction)
except ValueError as e:
raise ValueError(f"Invalid instruction: {instruction}, type: {type(instruction)}") from e
Expand Down Expand Up @@ -423,59 +424,63 @@ def _add_instruction(self, instruction: quil_rs.Instruction) -> None:
For backwards compatibility, it also prevents duplicate calibration, measurement, and gate definitions from
being added. Users of ``Program`` should use ``inst`` or ``Program`` addition instead.
"""
if instruction.is_calibration_definition():
defcal = instruction.to_calibration_definition()
idx, existing_calibration = next(
(
(i, existing_calibration)
for i, existing_calibration in enumerate(self._program.calibrations.calibrations)
if defcal.name == existing_calibration.name
and defcal.parameters == existing_calibration.parameters
and defcal.qubits == existing_calibration.qubits
),
(0, None),
)
if existing_calibration is None:
self._program.add_instruction(instruction)

elif (
existing_calibration.instructions != defcal.instructions
or existing_calibration.modifiers != defcal.modifiers
):
warnings.warn(f"Redefining calibration {defcal.name}", stacklevel=2)
current_calibrations = self._program.calibrations
new_calibrations = CalibrationSet(
current_calibrations.calibrations[:idx] + [defcal] + current_calibrations.calibrations[idx + 1 :],
current_calibrations.measure_calibrations,
match instruction:
case quil_rs.Instruction.CalibrationDefinition(defcal):
idx, existing_calibration = next(
(
(i, existing_calibration)
for i, existing_calibration in enumerate(self._program.calibrations.calibrations)
if defcal.name == existing_calibration.name
and defcal.parameters == existing_calibration.parameters
and defcal.qubits == existing_calibration.qubits
),
(0, None),
)
self._program.calibrations = new_calibrations
elif instruction.is_measure_calibration_definition():
defmeasure = instruction.to_measure_calibration_definition()
idx, existing_measure_calibration = next(
(
(i, existing_measure_calibration)
for i, existing_measure_calibration in enumerate(self._program.calibrations.measure_calibrations)
if existing_measure_calibration.parameter == defmeasure.parameter
and existing_measure_calibration.qubit == defmeasure.qubit
),
(0, None),
)
if existing_measure_calibration is None:
self._program.add_instruction(instruction)

else:
warnings.warn(f"Redefining DefMeasureCalibration {instruction}", stacklevel=2)
current_calibrations = self._program.calibrations
new_calibrations = CalibrationSet(
current_calibrations.calibrations,
current_calibrations.measure_calibrations[:idx]
+ [defmeasure]
+ current_calibrations.measure_calibrations[idx + 1 :],
if existing_calibration is None:
self._program.add_instruction(instruction)

elif (
existing_calibration.instructions != defcal.instructions
or existing_calibration.modifiers != defcal.modifiers
):
warnings.warn(f"Redefining calibration {defcal.name}", stacklevel=2)
current_calibrations = self._program.calibrations
new_calibrations = CalibrationSet(
current_calibrations.calibrations[:idx]
+ [defcal]
+ current_calibrations.calibrations[idx + 1 :],
current_calibrations.measure_calibrations,
)
self._program.calibrations = new_calibrations

case quil_rs.Instruction.MeasureCalibrationDefinition(defmeasure):
idx, existing_measure_calibration = next(
(
(i, existing_measure_calibration)
for i, existing_measure_calibration in enumerate(
self._program.calibrations.measure_calibrations
)
if existing_measure_calibration.target == defmeasure.target
and existing_measure_calibration.qubit == defmeasure.qubit
),
(0, None),
)
if existing_measure_calibration is None:
self._program.add_instruction(instruction)

self._program.calibrations = new_calibrations
else:
self._program.add_instruction(instruction)
else:
warnings.warn(f"Redefining DefMeasureCalibration {instruction}", stacklevel=2)
current_calibrations = self._program.calibrations
new_calibrations = CalibrationSet(
current_calibrations.calibrations,
current_calibrations.measure_calibrations[:idx]
+ [defmeasure]
+ current_calibrations.measure_calibrations[idx + 1 :],
)

self._program.calibrations = new_calibrations
case _:
self._program.add_instruction(instruction)

def filter_instructions(self, predicate: Callable[[AbstractInstruction], bool]) -> "Program":
"""Return a new ``Program`` containing only the instructions for which ``predicate`` returns ``True``.
Expand Down Expand Up @@ -825,14 +830,14 @@ def get_qubits(self, indices: bool = True) -> Union[set[QubitDesignator], set[in
"""
if indices:
return self.get_qubit_indices()
return set(_convert_to_py_qubits(self._program.get_used_qubits()))
return set(_convert_to_py_qubits(self._program.used_qubits))

def get_qubit_indices(self) -> set[int]:
"""Return the index of each qubit used in the program.

Will raise an exception if any of the qubits are placeholders.
"""
return {q.to_fixed() for q in self._program.get_used_qubits()}
return {q._0 for q in self._program.used_qubits}

def match_calibrations(self, instr: AbstractInstruction) -> Optional[CalibrationMatch]:
"""Attempt to match a calibration to the provided instruction.
Expand All @@ -853,16 +858,13 @@ def match_calibrations(self, instr: AbstractInstruction) -> Optional[Calibration
if not isinstance(instr, (Gate, Measurement)):
return None

instruction = _convert_to_rs_instruction(instr)
if instruction.is_gate():
gate = instruction.to_gate()
gate_match = self._program.calibrations.get_match_for_gate(gate)
return _convert_to_calibration_match(gate, gate_match)

if instruction.is_measurement():
measurement = instruction.to_measurement()
measure_match = self._program.calibrations.get_match_for_measurement(measurement)
return _convert_to_calibration_match(measurement, measure_match)
match _convert_to_rs_instruction(instr):
case quil_rs.Instruction.Gate(gate):
gate_match = self._program.calibrations.get_match_for_gate(gate)
return _convert_to_calibration_match(gate, gate_match)
case quil_rs.Instruction.Measurement(measurement):
measure_match = self._program.calibrations.get_match_for_measurement(measurement)
return _convert_to_calibration_match(measurement, measure_match)

return None

Expand Down Expand Up @@ -954,7 +956,12 @@ def __getitem__(self, index: Union[slice, int]) -> Union[AbstractInstruction, "P

def __iter__(self) -> Iterator[AbstractInstruction]:
"""Iterate through a program's instructions, e.g. [a for a in Program(X(0))]."""
return self.instructions.__iter__()
return iter(
chain(
(Declare._from_rs_declaration(instr) for instr in self._program.declarations.values()),
(_convert_to_py_instruction(instr) for instr in self._program.body_instructions),
)
)

def __eq__(self, other: object) -> bool:
"""Check if two programs are equal."""
Expand All @@ -964,7 +971,7 @@ def __eq__(self, other: object) -> bool:

def __len__(self) -> int:
"""Get the number of instructions in the program."""
return len(self.instructions)
return len(self._program.declarations) + len(self._program.body_instructions)

def __hash__(self) -> int:
"""Hash the program."""
Expand Down
Loading
Loading