Repository: rigetti/pyquil (python)

Issue:
Make `QAMExecutionResult` serializable
Due to using non-picklable members from `qcs-sdk-python`, `QAMExecutionResult` is not itself serializable. We should add a way to pickle it, either by supporting pickling upstream or implementing it for `QAMExecutionResult` specifically. 


Discussion:
This would also be useful for `pyquil.api._qpu.QPUExecuteResponse` so that programs can be submitted and retrieved in separate programs.
Yes this would be great. Also, instead of the program data/results disappearing after it's been retrieved by the user (as happens now, from what i understand), it would be great, if it stays available for some (even short) time after, say a day or two. That would allow users to handle potential intermittent retrieval/network/whatever issues (e.g. users tries to retrieve job data, rigetti server sends it over and marks as retrieved, removing relevant data, then something happens along the way and users does not see it... they then can't try to retrieve again, as server thinks that's been done and potentially got rid of it).

Patch:
diff --git a/pyquil/api/__init__.py b/pyquil/api/__init__.py
--- a/pyquil/api/__init__.py
+++ b/pyquil/api/__init__.py
@@ -30,7 +30,7 @@
     QVMCompiler,
 )
 from pyquil.api._qam import QAM, MemoryMap, QAMExecutionResult
-from pyquil.api._qpu import QPU
+from pyquil.api._qpu import QPU, QPUExecuteResponse
 from pyquil.api._quantum_computer import (
     QuantumComputer,
     get_qc,
@@ -59,6 +59,7 @@
     "MemoryMap",
     "QAMExecutionResult",
     "QPU",
+    "QPUExecuteResponse",
     "QuantumComputer",
     "get_qc",
     "list_quantum_computers",
diff --git a/pyquil/control_flow_graph.py b/pyquil/control_flow_graph.py
--- a/pyquil/control_flow_graph.py
+++ b/pyquil/control_flow_graph.py
@@ -5,7 +5,11 @@
 from quil import program as quil_rs
 from typing_extensions import Self, override
 
-from pyquil.quilbase import AbstractInstruction, _convert_to_py_instruction, _convert_to_py_instructions
+from pyquil.quilbase import (
+    AbstractInstruction,
+    _convert_to_py_instruction,
+    _convert_to_py_instructions,
+)
 
 
 class BasicBlock(quil_rs.BasicBlock):
diff --git a/pyquil/quilbase.py b/pyquil/quilbase.py
--- a/pyquil/quilbase.py
+++ b/pyquil/quilbase.py
@@ -1068,12 +1068,13 @@ class LogicalBinaryOp(quil_rs.BinaryLogic, AbstractInstruction):
 
     def __new__(cls, left: MemoryReference, right: Union[MemoryReference, int]) -> Self:
         """Initialize the operands of the binary logical instruction."""
-        operands = cls._to_rs_binary_operands(left, right)
-        return super().__new__(cls, cls.op, operands)
+        destination = left._to_rs_memory_reference()
+        source = cls._to_rs_binary_operand(right)
+        return super().__new__(cls, cls.op, destination, source)
 
     @classmethod
     def _from_rs_binary_logic(cls, binary_logic: quil_rs.BinaryLogic) -> "LogicalBinaryOp":
-        return super().__new__(cls, binary_logic.operator, binary_logic.operands)
+        return super().__new__(cls, binary_logic.operator, binary_logic.destination, binary_logic.source)
 
     @staticmethod
     def _to_rs_binary_operand(operand: Union[MemoryReference, int]) -> quil_rs.BinaryOperand:
@@ -1081,12 +1082,6 @@ def _to_rs_binary_operand(operand: Union[MemoryReference, int]) -> quil_rs.Binar
             return quil_rs.BinaryOperand.from_memory_reference(operand._to_rs_memory_reference())
         return quil_rs.BinaryOperand.from_literal_integer(operand)
 
-    @staticmethod
-    def _to_rs_binary_operands(left: MemoryReference, right: Union[MemoryReference, int]) -> quil_rs.BinaryOperands:
-        left_operand = left._to_rs_memory_reference()
-        right_operand = LogicalBinaryOp._to_rs_binary_operand(right)
-        return quil_rs.BinaryOperands(left_operand, right_operand)
-
     @staticmethod
     def _to_py_binary_operand(operand: quil_rs.BinaryOperand) -> Union[MemoryReference, int]:
         if operand.is_literal_integer():
@@ -1096,24 +1091,22 @@ def _to_py_binary_operand(operand: quil_rs.BinaryOperand) -> Union[MemoryReferen
     @property
     def left(self) -> MemoryReference:
         """The left hand side of the binary expression."""
-        return MemoryReference._from_rs_memory_reference(super().operands.memory_reference)
+        return MemoryReference._from_rs_memory_reference(super().destination)
 
     @left.setter
     def left(self, left: MemoryReference) -> None:
-        operands = super().operands
-        operands.memory_reference = left._to_rs_memory_reference()
-        quil_rs.BinaryLogic.operands.__set__(self, operands)  # type: ignore[attr-defined]
+        destination = left._to_rs_memory_reference()
+        quil_rs.BinaryLogic.destination.__set__(self, destination)  # type: ignore[attr-defined]
 
     @property
     def right(self) -> Union[MemoryReference, int]:
         """The right hand side of the binary expression."""
-        return self._to_py_binary_operand(super().operands.operand)
+        return self._to_py_binary_operand(super().source)
 
     @right.setter
     def right(self, right: Union[MemoryReference, int]) -> None:
-        operands = super().operands
-        operands.operand = self._to_rs_binary_operand(right)
-        quil_rs.BinaryLogic.operands.__set__(self, operands)  # type: ignore[attr-defined]
+        source = self._to_rs_binary_operand(right)
+        quil_rs.BinaryLogic.source.__set__(self, source)  # type: ignore[attr-defined]
 
     def out(self) -> str:
         """Return the instruction as a valid Quil string. Raises an error if the instruction contains placeholders."""
@@ -1156,9 +1149,8 @@ class ArithmeticBinaryOp(quil_rs.Arithmetic, AbstractInstruction):
 
     def __new__(cls, left: MemoryReference, right: Union[MemoryReference, int, float]) -> Self:
         """Initialize the operands of the binary arithmetic instruction."""
-        left_operand = quil_rs.ArithmeticOperand.from_memory_reference(left._to_rs_memory_reference())
         right_operand = _to_rs_arithmetic_operand(right)
-        return super().__new__(cls, cls.op, left_operand, right_operand)
+        return super().__new__(cls, cls.op, left._to_rs_memory_reference(), right_operand)
 
     @classmethod
     def _from_rs_arithmetic(cls, arithmetic: quil_rs.Arithmetic) -> "ArithmeticBinaryOp":
@@ -1167,12 +1159,12 @@ def _from_rs_arithmetic(cls, arithmetic: quil_rs.Arithmetic) -> "ArithmeticBinar
     @property
     def left(self) -> MemoryReference:
         """The left hand side of the binary expression."""
-        return MemoryReference._from_rs_memory_reference(super().destination.to_memory_reference())
+        return MemoryReference._from_rs_memory_reference(super().destination)
 
     @left.setter
     def left(self, left: MemoryReference) -> None:
         quil_rs.Arithmetic.destination.__set__(  # type: ignore[attr-defined]
-            self, quil_rs.ArithmeticOperand.from_memory_reference(left._to_rs_memory_reference())
+            self, left._to_rs_memory_reference()
         )
 
     @property
@@ -1493,12 +1485,16 @@ def __new__(
         right: Union[MemoryReference, int, float],
     ) -> "ClassicalComparison":
         """Initialize a new comparison instruction."""
-        operands = (target._to_rs_memory_reference(), left._to_rs_memory_reference(), cls._to_comparison_operand(right))
-        return super().__new__(cls, cls.op, operands)
+        rs_target, rs_left, rs_right = (
+            target._to_rs_memory_reference(),
+            left._to_rs_memory_reference(),
+            cls._to_comparison_operand(right),
+        )
+        return super().__new__(cls, cls.op, rs_target, rs_left, rs_right)
 
     @classmethod
     def _from_rs_comparison(cls, comparison: quil_rs.Comparison) -> Self:
-        return super().__new__(cls, comparison.operator, comparison.operands)
+        return super().__new__(cls, comparison.operator, comparison.destination, comparison.lhs, comparison.rhs)
 
     @staticmethod
     def _to_comparison_operand(operand: Union[MemoryReference, int, float]) -> quil_rs.ComparisonOperand:
@@ -1522,35 +1518,29 @@ def _to_py_operand(operand: quil_rs.ComparisonOperand) -> Union[MemoryReference,
     @property
     def target(self) -> MemoryReference:
         """The target of the comparison."""
-        return MemoryReference._from_rs_memory_reference(super().operands[0])
+        return MemoryReference._from_rs_memory_reference(super().destination)
 
     @target.setter
     def target(self, target: MemoryReference) -> None:
-        operands = list(super().operands)
-        operands[0] = target._to_rs_memory_reference()
-        quil_rs.Comparison.operands.__set__(self, tuple(operands))  # type: ignore
+        quil_rs.Comparison.destination.__set__(self, target._to_rs_memory_reference())  # type: ignore
 
     @property
     def left(self) -> MemoryReference:
         """The left hand side of the comparison."""
-        return MemoryReference._from_rs_memory_reference(super().operands[1])
+        return MemoryReference._from_rs_memory_reference(super().lhs)
 
     @left.setter
     def left(self, left: MemoryReference) -> None:
-        operands = list(super().operands)
-        operands[1] = left._to_rs_memory_reference()
-        quil_rs.Comparison.operands.__set__(self, tuple(operands))  # type: ignore
+        quil_rs.Comparison.lhs.__set__(self, left._to_rs_memory_reference())  # type: ignore
 
     @property
     def right(self) -> Union[MemoryReference, int, float]:
         """The right hand side of the comparison."""
-        return self._to_py_operand(super().operands[2])
+        return self._to_py_operand(super().rhs)
 
     @right.setter
     def right(self, right: MemoryReference) -> None:
-        operands = list(super().operands)
-        operands[2] = self._to_comparison_operand(right)
-        quil_rs.Comparison.operands.__set__(self, tuple(operands))  # type: ignore
+        quil_rs.Comparison.rhs.__set__(self, quil_rs.ComparisonOperand(right._to_rs_memory_reference()))  # type: ignore
 
     def out(self) -> str:
         """Return the instruction as a valid Quil string."""


Test patch:
diff --git a/test/unit/__snapshots__/test_quilbase.ambr b/test/unit/__snapshots__/test_quilbase.ambr
--- a/test/unit/__snapshots__/test_quilbase.ambr
+++ b/test/unit/__snapshots__/test_quilbase.ambr
@@ -11,6 +11,18 @@
 # name: TestArithmeticBinaryOp.test_out[SUB-left1-1]
   'SUB b[1] 1'
 # ---
+# name: TestArithmeticBinaryOp.test_pickle[ADD-left0-right0]
+  Arithmetic { operator: Add, destination: MemoryReference { name: "a", index: 0 }, source: MemoryReference(MemoryReference { name: "b", index: 0 }) }
+# ---
+# name: TestArithmeticBinaryOp.test_pickle[DIV-left3-4.2]
+  Arithmetic { operator: Divide, destination: MemoryReference { name: "c", index: 2 }, source: LiteralReal(4.2) }
+# ---
+# name: TestArithmeticBinaryOp.test_pickle[MUL-left2-1.0]
+  Arithmetic { operator: Multiply, destination: MemoryReference { name: "c", index: 2 }, source: LiteralInteger(1) }
+# ---
+# name: TestArithmeticBinaryOp.test_pickle[SUB-left1-1]
+  Arithmetic { operator: Subtract, destination: MemoryReference { name: "b", index: 1 }, source: LiteralInteger(1) }
+# ---
 # name: TestCapture.test_out[Blocking]
   'CAPTURE 123 q "FRAMEX" WAVEFORMY ro[0]'
 # ---
@@ -20,6 +32,15 @@
 # name: TestCapture.test_out[TemplateWaveform]
   'NONBLOCKING CAPTURE 123 q "FRAMEX" flat(duration: 2.5, iq: 1+2.0i) ro[0]'
 # ---
+# name: TestCapture.test_pickle[Blocking]
+  Capture { blocking: true, frame: FrameIdentifier { name: "FRAMEX", qubits: [Fixed(123), Variable("q")] }, memory_reference: MemoryReference { name: "ro", index: 0 }, waveform: WaveformInvocation { name: "WAVEFORMY", parameters: {} } }
+# ---
+# name: TestCapture.test_pickle[NonBlocking]
+  Capture { blocking: false, frame: FrameIdentifier { name: "FRAMEX", qubits: [Fixed(123), Variable("q")] }, memory_reference: MemoryReference { name: "ro", index: 0 }, waveform: WaveformInvocation { name: "WAVEFORMY", parameters: {} } }
+# ---
+# name: TestCapture.test_pickle[TemplateWaveform]
+  Capture { blocking: false, frame: FrameIdentifier { name: "FRAMEX", qubits: [Fixed(123), Variable("q")] }, memory_reference: MemoryReference { name: "ro", index: 0 }, waveform: WaveformInvocation { name: "flat", parameters: {"duration": Number(Complex { re: 2.5, im: 0.0 }), "iq": Infix(InfixExpression { left: Number(Complex { re: 1.0, im: 0.0 }), operator: Plus, right: Number(Complex { re: 0.0, im: 2.0 }) })} } }
+# ---
 # name: TestClassicalComparison.test_out[EQ-target0-left0-right0]
   'EQ t[0] y[0] z[0]'
 # ---
@@ -175,12 +196,6 @@
   DEFCIRCUIT NiftyCircuit(%theta) a:
   	DECLARE ro BIT[1]
   	MEASURE a ro[0]
-  	DEFGATE ParameterizedGate(%theta) AS MATRIX:
-  		%theta, 0, 0, 0
-  		0, %theta, 0, 0
-  		0, 0, %theta, 0
-  		0, 0, 0, %theta
-  	
   
   '''
 # ---
@@ -189,6 +204,8 @@
 # ---
 # name: TestDefFrame.test_out[Frame-Only].1
   set({
+    '\tDIRECTION: "direction"',
+    '\tINITIAL-FREQUENCY: 0',
   })
 # ---
 # name: TestDefFrame.test_out[With-Optionals]
@@ -209,6 +226,8 @@
 # ---
 # name: TestDefFrame.test_str[Frame-Only].1
   set({
+    '\tDIRECTION: "direction"',
+    '\tINITIAL-FREQUENCY: 0',
   })
 # ---
 # name: TestDefFrame.test_str[With-Optionals]
@@ -272,6 +291,18 @@
   
   '''
 # ---
+# name: TestDefGate.test_pickle[MixedTypes]
+  GateDefinition { name: "MixedTypes", parameters: ["X"], specification: Matrix([[Number(Complex { re: 0.0, im: 0.0 }), FunctionCall(FunctionCallExpression { function: Sine, expression: Variable("X") })], [Number(Complex { re: 0.0, im: 0.0 }), Number(Complex { re: 0.0, im: 0.0 })]]) }
+# ---
+# name: TestDefGate.test_pickle[No-Params]
+  GateDefinition { name: "NoParamGate", parameters: [], specification: Matrix([[Number(Complex { re: 1.0, im: 0.0 }), Number(Complex { re: 0.0, im: 0.0 }), Number(Complex { re: 0.0, im: 0.0 }), Number(Complex { re: 0.0, im: 0.0 })], [Number(Complex { re: 0.0, im: 0.0 }), Number(Complex { re: 1.0, im: 0.0 }), Number(Complex { re: 0.0, im: 0.0 }), Number(Complex { re: 0.0, im: 0.0 })], [Number(Complex { re: 0.0, im: 0.0 }), Number(Complex { re: 0.0, im: 0.0 }), Number(Complex { re: 1.0, im: 0.0 }), Number(Complex { re: 0.0, im: 0.0 })], [Number(Complex { re: 0.0, im: 0.0 }), Number(Complex { re: 0.0, im: 0.0 }), Number(Complex { re: 0.0, im: 0.0 }), Number(Complex { re: 1.0, im: 0.0 })]]) }
+# ---
+# name: TestDefGate.test_pickle[ParameterlessExpression]
+  GateDefinition { name: "ParameterlessExpressions", parameters: [], specification: Matrix([[Number(Complex { re: 1.0, im: 0.0 }), Number(Complex { re: 1.2246467991473532e-16, im: 0.0 })], [Number(Complex { re: 1.2246467991473532e-16, im: 0.0 }), Prefix(PrefixExpression { operator: Minus, expression: Number(Complex { re: 1.0, im: 0.0 }) })]]) }
+# ---
+# name: TestDefGate.test_pickle[Params]
+  GateDefinition { name: "ParameterizedGate", parameters: ["X"], specification: Matrix([[FunctionCall(FunctionCallExpression { function: Cosine, expression: Variable("X") }), Number(Complex { re: 0.0, im: 0.0 }), Number(Complex { re: 0.0, im: 0.0 }), Number(Complex { re: 0.0, im: 0.0 })], [Number(Complex { re: 0.0, im: 0.0 }), FunctionCall(FunctionCallExpression { function: Cosine, expression: Variable("X") }), Number(Complex { re: 0.0, im: 0.0 }), Number(Complex { re: 0.0, im: 0.0 })], [Number(Complex { re: 0.0, im: 0.0 }), Number(Complex { re: 0.0, im: 0.0 }), FunctionCall(FunctionCallExpression { function: Cosine, expression: Variable("X") }), Number(Complex { re: 0.0, im: 0.0 })], [Number(Complex { re: 0.0, im: 0.0 }), Number(Complex { re: 0.0, im: 0.0 }), Number(Complex { re: 0.0, im: 0.0 }), FunctionCall(FunctionCallExpression { function: Cosine, expression: Variable("X") })]]) }
+# ---
 # name: TestDefGate.test_str[MixedTypes]
   '''
   DEFGATE MixedTypes(%X) AS MATRIX:
@@ -406,12 +437,6 @@
   
   '''
 # ---
-# name: TestDefWaveform.test_out[No-Params-Entries]
-  '''
-  DEFWAVEFORM Wavey:
-  	
-  '''
-# ---
 # name: TestDefWaveform.test_out[With-Param]
   '''
   DEFWAVEFORM Wavey(%x):
@@ -424,6 +449,12 @@
   	1+2.0i, %x, 3*%y
   '''
 # ---
+# name: TestDefWaveform.test_pickle[With-Param]
+  WaveformDefinition { name: "Wavey", definition: Waveform { matrix: [Variable("x")], parameters: ["x"] } }
+# ---
+# name: TestDefWaveform.test_pickle[With-Params-Complex]
+  WaveformDefinition { name: "Wavey", definition: Waveform { matrix: [Infix(InfixExpression { left: Number(Complex { re: 1.0, im: 0.0 }), operator: Plus, right: Number(Complex { re: 0.0, im: 2.0 }) }), Variable("x"), Infix(InfixExpression { left: Number(Complex { re: 3.0, im: 0.0 }), operator: Star, right: Variable("y") })], parameters: ["x", "y"] } }
+# ---
 # name: TestDelayFrames.test_out[frames0-5.0]
   'DELAY 0 "frame" 5'
 # ---
@@ -598,6 +629,30 @@
 # name: TestPulse.test_out[NonBlocking]
   'NONBLOCKING PULSE 123 q "FRAMEX" WAVEFORMY'
 # ---
+# name: TestPulse.test_pickle[Blocking]
+  Pulse { blocking: true, frame: FrameIdentifier { name: "FRAMEX", qubits: [Fixed(123), Variable("q")] }, waveform: WaveformInvocation { name: "WAVEFORMY", parameters: {} } }
+# ---
+# name: TestPulse.test_pickle[BoxcarAveragerKernel]
+  Pulse { blocking: false, frame: FrameIdentifier { name: "FRAMEX", qubits: [Fixed(123), Variable("q")] }, waveform: WaveformInvocation { name: "boxcar_kernel", parameters: {"duration": Number(Complex { re: 2.5, im: 0.0 }), "scale": Number(Complex { re: 1.0, im: 0.0 })} } }
+# ---
+# name: TestPulse.test_pickle[DragGaussianWaveform]
+  Pulse { blocking: false, frame: FrameIdentifier { name: "FRAMEX", qubits: [Fixed(123), Variable("q")] }, waveform: WaveformInvocation { name: "drag_gaussian", parameters: {"alpha": Number(Complex { re: 1.0, im: 0.0 }), "anh": Number(Complex { re: 0.1, im: 0.0 }), "duration": Number(Complex { re: 2.5, im: 0.0 }), "fwhm": Number(Complex { re: 1.0, im: 0.0 }), "t0": Number(Complex { re: 1.0, im: 0.0 })} } }
+# ---
+# name: TestPulse.test_pickle[ErfSquareWaveform]
+  Pulse { blocking: false, frame: FrameIdentifier { name: "FRAMEX", qubits: [Fixed(123), Variable("q")] }, waveform: WaveformInvocation { name: "erf_square", parameters: {"duration": Number(Complex { re: 2.5, im: 0.0 }), "pad_left": Number(Complex { re: 1.0, im: 0.0 }), "pad_right": Number(Complex { re: 0.1, im: 0.0 }), "risetime": Number(Complex { re: 1.0, im: 0.0 }), "scale": Number(Complex { re: 1.0, im: 0.0 })} } }
+# ---
+# name: TestPulse.test_pickle[FlatWaveform]
+  Pulse { blocking: false, frame: FrameIdentifier { name: "FRAMEX", qubits: [Fixed(123), Variable("q")] }, waveform: WaveformInvocation { name: "flat", parameters: {"duration": Number(Complex { re: 2.5, im: 0.0 }), "iq": Infix(InfixExpression { left: Number(Complex { re: 1.0, im: 0.0 }), operator: Plus, right: Number(Complex { re: 0.0, im: 2.0 }) })} } }
+# ---
+# name: TestPulse.test_pickle[GaussianWaveform]
+  Pulse { blocking: false, frame: FrameIdentifier { name: "FRAMEX", qubits: [Fixed(123), Variable("q")] }, waveform: WaveformInvocation { name: "gaussian", parameters: {"duration": Number(Complex { re: 2.5, im: 0.0 }), "fwhm": Number(Complex { re: 1.0, im: 0.0 }), "phase": Number(Complex { re: 0.1, im: 0.0 }), "t0": Number(Complex { re: 1.0, im: 0.0 })} } }
+# ---
+# name: TestPulse.test_pickle[HrmGaussianWaveform]
+  Pulse { blocking: false, frame: FrameIdentifier { name: "FRAMEX", qubits: [Fixed(123), Variable("q")] }, waveform: WaveformInvocation { name: "hrm_gaussian", parameters: {"alpha": Number(Complex { re: 1.0, im: 0.0 }), "anh": Number(Complex { re: 0.1, im: 0.0 }), "duration": Number(Complex { re: 2.5, im: 0.0 }), "fwhm": Number(Complex { re: 1.0, im: 0.0 }), "second_order_hrm_coeff": Number(Complex { re: 0.5, im: 0.0 }), "t0": Number(Complex { re: 1.0, im: 0.0 })} } }
+# ---
+# name: TestPulse.test_pickle[NonBlocking]
+  Pulse { blocking: false, frame: FrameIdentifier { name: "FRAMEX", qubits: [Fixed(123), Variable("q")] }, waveform: WaveformInvocation { name: "WAVEFORMY", parameters: {} } }
+# ---
 # name: TestRawCapture.test_out[Blocking]
   'RAW-CAPTURE 123 q "FRAMEX" 0.5 ro[0]'
 # ---
diff --git a/test/unit/test_qpu.py b/test/unit/test_qpu.py
--- a/test/unit/test_qpu.py
+++ b/test/unit/test_qpu.py
@@ -1,12 +1,22 @@
+import pickle
+from typing import Any
 from unittest.mock import MagicMock, patch
 
 import numpy as np
 import pytest
-from qcs_sdk.qpu import MemoryValues
+from qcs_sdk import ExecutionData, ResultData
+from qcs_sdk.qpu import MemoryValues, QPUResultData, ReadoutValues
 from qcs_sdk.qpu.api import ExecutionResult, ExecutionResults, Register
 from rpcq.messages import ParameterSpec
 
-from pyquil.api import ConnectionStrategy, ExecutionOptions, ExecutionOptionsBuilder, RegisterMatrixConversionError
+from pyquil.api import (
+    ConnectionStrategy,
+    ExecutionOptions,
+    ExecutionOptionsBuilder,
+    QAMExecutionResult,
+    QPUExecuteResponse,
+    RegisterMatrixConversionError,
+)
 from pyquil.api._abstract_compiler import EncryptedProgram
 from pyquil.api._qpu import QPU
 from pyquil.quil import Program
@@ -198,3 +208,37 @@ def test_submit_with_options(
             client=qpu._client_configuration,
             execution_options=execution_options,
         )
+
+
+@pytest.mark.parametrize(
+    "input",
+    [
+        (
+            QAMExecutionResult(
+                executable=mock_encrypted_program,
+                data=ExecutionData(
+                    result_data=ResultData.from_qpu(
+                        QPUResultData(
+                            mappings={"ro[0]": "q0", "ro[1]": "q1"},
+                            readout_values={
+                                "q0": ReadoutValues.from_integer([1, 1]),
+                                "q1": ReadoutValues.from_real([1.1, 1.2]),
+                                "q2": ReadoutValues.from_complex([complex(3, 4), complex(2.35, 4.21)]),
+                            },
+                            memory_values={"int": MemoryValues([2, 3, 4]), "real": MemoryValues([5.0, 6.0, 7.0])},
+                        )
+                    )
+                ),
+            )
+        ),
+        (
+            QPUExecuteResponse(
+                job_id="some-job-id", _executable=mock_encrypted_program, execution_options=ExecutionOptions.default()
+            )
+        ),
+    ],
+)
+def test_pickle_execute_responses(input: Any):
+    pickled_response = pickle.dumps(input)
+    unpickled_response = pickle.loads(pickled_response)
+    assert unpickled_response == input
diff --git a/test/unit/test_quilbase.py b/test/unit/test_quilbase.py
--- a/test/unit/test_quilbase.py
+++ b/test/unit/test_quilbase.py
@@ -1,4 +1,5 @@
 import copy
+import pickle
 from collections.abc import Iterable
 from math import pi
 from numbers import Complex, Number
@@ -187,6 +188,11 @@ def test_compile(self, program: Program, compiler: QPUCompiler):
         except Exception as e:
             raise AssertionError(f"Failed to compile the program: \n{program}") from e
 
+    def test_pickle(self, gate: Gate):
+        pickled = pickle.dumps(gate)
+        unpickled = pickle.loads(pickled)
+        assert unpickled == gate
+
 
 @pytest.mark.parametrize(
     ("name", "matrix", "parameters"),
@@ -261,6 +267,11 @@ def test_copy(self, def_gate: DefGate):
         assert isinstance(copy.copy(def_gate), DefGate)
         assert isinstance(copy.deepcopy(def_gate), DefGate)
 
+    def test_pickle(self, def_gate: DefGate, snapshot: SnapshotAssertion):
+        pickled = pickle.dumps(def_gate)
+        unpickled = pickle.loads(pickled)
+        assert unpickled == snapshot
+
 
 @pytest.mark.parametrize(
     ("name", "permutation"),
@@ -430,6 +441,11 @@ def test_convert(self, calibration: DefCalibration):
         rs_calibration = _convert_to_rs_instruction(calibration)
         assert calibration == _convert_to_py_instruction(rs_calibration)
 
+    def test_pickle(self, calibration: DefCalibration):
+        pickled = pickle.dumps(calibration)
+        unpickled = pickle.loads(pickled)
+        assert unpickled == calibration
+
 
 @pytest.mark.parametrize(
     ("qubit", "memory_reference", "instrs"),
@@ -468,6 +484,11 @@ def test_convert(self, measure_calibration: DefMeasureCalibration):
         rs_measure_calibration = _convert_to_rs_instruction(measure_calibration)
         assert measure_calibration == _convert_to_py_instruction(rs_measure_calibration)
 
+    def test_pickle(self, measure_calibration: DefMeasureCalibration):
+        pickled = pickle.dumps(measure_calibration)
+        unpickled = pickle.loads(pickled)
+        assert unpickled == measure_calibration
+
 
 @pytest.mark.parametrize(
     ("qubit", "classical_reg"),
@@ -511,11 +532,16 @@ def test_convert(self, measurement: Measurement):
         rs_measurement = _convert_to_rs_instruction(measurement)
         assert measurement == _convert_to_py_instruction(rs_measurement)
 
+    def test_pickle(self, measurement: Measurement):
+        pickled = pickle.dumps(measurement)
+        unpickled = pickle.loads(pickled)
+        assert unpickled == measurement
+
 
 @pytest.mark.parametrize(
     ("frame", "direction", "initial_frequency", "hardware_object", "sample_rate", "center_frequency", "channel_delay"),
     [
-        (Frame([Qubit(0)], "frame"), None, None, None, None, None, None),
+        (Frame([Qubit(0)], "frame"), "direction", 0.0, None, None, None, None),
         (Frame([Qubit(1)], "frame"), "direction", 1.39, "hardware_object", 44.1, 440.0, 0.0),
     ],
     ids=("Frame-Only", "With-Optionals"),
@@ -603,6 +629,12 @@ def test_convert(self, def_frame: DefFrame):
         rs_def_frame = _convert_to_rs_instruction(def_frame)
         assert def_frame == _convert_to_py_instruction(rs_def_frame)
 
+    def test_pickle(self, def_frame: DefFrame):
+        print(def_frame.to_quil())
+        pickled = pickle.dumps(def_frame)
+        unpickled = pickle.loads(pickled)
+        assert unpickled == def_frame
+
 
 @pytest.mark.parametrize(
     ("name", "memory_type", "memory_size", "shared_region", "offsets"),
@@ -673,6 +705,11 @@ def test_convert(self, declare: Declare):
         rs_declare = _convert_to_rs_instruction(declare)
         assert declare == _convert_to_py_instruction(rs_declare)
 
+    def test_pickle(self, declare: Declare):
+        pickled = pickle.dumps(declare)
+        unpickled = pickle.loads(pickled)
+        assert unpickled == declare
+
 
 @pytest.mark.parametrize(
     ("command", "args", "freeform_string"),
@@ -718,6 +755,11 @@ def test_convert(self, pragma: Pragma):
         rs_pragma = _convert_to_rs_instruction(pragma)
         assert pragma == _convert_to_py_instruction(rs_pragma)
 
+    def test_pickle(self, pragma: Pragma):
+        pickled = pickle.dumps(pragma)
+        unpickled = pickle.loads(pickled)
+        assert unpickled == pragma
+
 
 @pytest.mark.parametrize(
     ("qubit"),
@@ -765,6 +807,11 @@ def test_convert(self, reset_qubit: Reset):
         rs_reset_qubit = _convert_to_rs_instruction(reset_qubit)
         assert reset_qubit == _convert_to_py_instruction(rs_reset_qubit)
 
+    def test_pickle(self, reset_qubit: Reset):
+        pickled = pickle.dumps(reset_qubit)
+        unpickled = pickle.loads(pickled)
+        assert unpickled == reset_qubit
+
 
 @pytest.mark.parametrize(
     ("frames", "duration"),
@@ -798,6 +845,11 @@ def test_convert(self, delay_frames: DelayFrames):
         rs_delay_frames = _convert_to_rs_instruction(delay_frames)
         assert delay_frames == _convert_to_py_instruction(rs_delay_frames)
 
+    def test_pickle(self, delay_frames: DelayFrames):
+        pickled = pickle.dumps(delay_frames)
+        unpickled = pickle.loads(pickled)
+        assert unpickled == delay_frames
+
 
 @pytest.mark.parametrize(
     ("qubits", "duration"),
@@ -833,6 +885,11 @@ def test_convert(self, delay_qubits: DelayQubits):
         rs_delay_qubits = _convert_to_rs_instruction(delay_qubits)
         assert delay_qubits == _convert_to_py_instruction(rs_delay_qubits)
 
+    def test_pickle(self, delay_qubits: DelayQubits):
+        pickled = pickle.dumps(delay_qubits)
+        unpickled = pickle.loads(pickled)
+        assert unpickled == delay_qubits
+
 
 @pytest.mark.parametrize(
     ("qubits"),
@@ -863,6 +920,11 @@ def test_convert(self, fence: Fence):
         rs_fence = _convert_to_rs_instruction(fence)
         assert fence == _convert_to_py_instruction(rs_fence)
 
+    def test_pickle(self, fence: Fence):
+        pickled = pickle.dumps(fence)
+        unpickled = pickle.loads(pickled)
+        assert unpickled == fence
+
 
 def test_fence_all():
     fa = FenceAll()
@@ -873,7 +935,6 @@ def test_fence_all():
 @pytest.mark.parametrize(
     ("name", "parameters", "entries"),
     [
-        ("Wavey", [], []),
         ("Wavey", [Parameter("x")], [Parameter("x")]),
         (
             "Wavey",
@@ -881,7 +942,7 @@ def test_fence_all():
             [complex(1.0, 2.0), Parameter("x"), Mul(complex(3.0, 0.0), Parameter("y"))],
         ),
     ],
-    ids=("No-Params-Entries", "With-Param", "With-Params-Complex"),
+    ids=("With-Param", "With-Params-Complex"),
 )
 class TestDefWaveform:
     @pytest.fixture
@@ -914,6 +975,12 @@ def test_convert(self, def_waveform: DefWaveform):
         rs_def_waveform = _convert_to_rs_instruction(def_waveform)
         assert def_waveform == _convert_to_py_instruction(rs_def_waveform)
 
+    def test_pickle(self, def_waveform: DefWaveform, snapshot: SnapshotAssertion):
+        print(def_waveform.to_quil())
+        pickled = pickle.dumps(def_waveform)
+        unpickled = pickle.loads(pickled)
+        assert unpickled == snapshot
+
 
 @pytest.mark.parametrize(
     ("name", "parameters", "qubit_variables", "instructions"),
@@ -926,7 +993,6 @@ def test_convert(self, def_waveform: DefWaveform):
             [
                 Declare("ro", "BIT", 1),
                 Measurement(FormalArgument("a"), MemoryReference("ro")),
-                DefGate("ParameterizedGate", np.diag([Parameter("theta")] * 4), [Parameter("theta")]),
             ],
         ),
     ],
@@ -974,6 +1040,12 @@ def test_convert(self, def_circuit: DefCircuit):
         rs_def_circuit = _convert_to_rs_instruction(def_circuit)
         assert def_circuit == _convert_to_py_instruction(rs_def_circuit)
 
+    def test_pickle(self, def_circuit: DefCircuit):
+        print(def_circuit.to_quil())
+        pickled = pickle.dumps(def_circuit)
+        unpickled = pickle.loads(pickled)
+        assert unpickled == def_circuit
+
 
 @pytest.mark.parametrize(
     ("frame", "kernel", "memory_region", "nonblocking"),
@@ -1035,6 +1107,11 @@ def test_convert(self, capture: Capture):
         rs_capture = _convert_to_rs_instruction(capture)
         assert capture == _convert_to_py_instruction(rs_capture)
 
+    def test_pickle(self, capture: Capture, snapshot: SnapshotAssertion):
+        pickled = pickle.dumps(capture)
+        unpickled = pickle.loads(pickled)
+        assert unpickled == snapshot
+
 
 @pytest.mark.parametrize(
     ("frame", "waveform", "nonblocking"),
@@ -1125,6 +1202,11 @@ def test_convert(self, pulse: Pulse):
         rs_pulse = _convert_to_rs_instruction(pulse)
         assert pulse == _convert_to_py_instruction(rs_pulse)
 
+    def test_pickle(self, pulse: Pulse, snapshot: SnapshotAssertion):
+        pickled = pickle.dumps(pulse)
+        unpickled = pickle.loads(pickled)
+        assert unpickled == snapshot
+
 
 @pytest.mark.parametrize(
     ("frame", "duration", "memory_region", "nonblocking"),
@@ -1186,6 +1268,11 @@ def test_convert(self, raw_capture: RawCapture):
         rs_raw_capture = _convert_to_rs_instruction(raw_capture)
         assert raw_capture == _convert_to_py_instruction(rs_raw_capture)
 
+    def test_pickle(self, raw_capture: RawCapture):
+        pickled = pickle.dumps(raw_capture)
+        unpickled = pickle.loads(pickled)
+        assert unpickled == raw_capture
+
 
 @pytest.mark.parametrize(
     ("frame", "expression"),
@@ -1260,32 +1347,37 @@ def test_convert(self, frame_mutation_instructions):
 )
 class TestSwapPhases:
     @pytest.fixture
-    def swap_phase(self, frame_a, frame_b):
+    def swap_phases(self, frame_a, frame_b):
         return SwapPhases(frame_a, frame_b)
 
-    def test_out(self, swap_phase: SwapPhases, snapshot: SnapshotAssertion):
-        assert swap_phase.out() == snapshot
+    def test_out(self, swap_phases: SwapPhases, snapshot: SnapshotAssertion):
+        assert swap_phases.out() == snapshot
 
-    def test_frames(self, swap_phase: SwapPhases, frame_a: Frame, frame_b: Frame):
-        assert swap_phase.frameA == frame_a
-        assert swap_phase.frameB == frame_b
-        swap_phase.frameA = Frame([Qubit(123)], "NEW-FRAME")
-        swap_phase.frameB = Frame([Qubit(123)], "NEW-FRAME")
-        assert swap_phase.frameA == Frame([Qubit(123)], "NEW-FRAME")
-        assert swap_phase.frameB == Frame([Qubit(123)], "NEW-FRAME")
+    def test_frames(self, swap_phases: SwapPhases, frame_a: Frame, frame_b: Frame):
+        assert swap_phases.frameA == frame_a
+        assert swap_phases.frameB == frame_b
+        swap_phases.frameA = Frame([Qubit(123)], "NEW-FRAME")
+        swap_phases.frameB = Frame([Qubit(123)], "NEW-FRAME")
+        assert swap_phases.frameA == Frame([Qubit(123)], "NEW-FRAME")
+        assert swap_phases.frameB == Frame([Qubit(123)], "NEW-FRAME")
 
-    def test_get_qubits(self, swap_phase: SwapPhases, frame_a: Frame, frame_b: Frame):
+    def test_get_qubits(self, swap_phases: SwapPhases, frame_a: Frame, frame_b: Frame):
         expected_qubits = set(frame_a.qubits + frame_b.qubits)
-        assert swap_phase.get_qubits() == set([q.index for q in expected_qubits if isinstance(q, Qubit)])
-        assert swap_phase.get_qubits(False) == expected_qubits
+        assert swap_phases.get_qubits() == set([q.index for q in expected_qubits if isinstance(q, Qubit)])
+        assert swap_phases.get_qubits(False) == expected_qubits
+
+    def test_copy(self, swap_phases: SwapPhases):
+        assert isinstance(copy.copy(swap_phases), SwapPhases)
+        assert isinstance(copy.deepcopy(swap_phases), SwapPhases)
 
-    def test_copy(self, swap_phase: SwapPhases):
-        assert isinstance(copy.copy(swap_phase), SwapPhases)
-        assert isinstance(copy.deepcopy(swap_phase), SwapPhases)
+    def test_convert(self, swap_phases: SwapPhases):
+        rs_swap_phase = _convert_to_rs_instruction(swap_phases)
+        assert swap_phases == _convert_to_py_instruction(rs_swap_phase)
 
-    def test_convert(self, swap_phase: SwapPhases):
-        rs_swap_phase = _convert_to_rs_instruction(swap_phase)
-        assert swap_phase == _convert_to_py_instruction(rs_swap_phase)
+    def test_pickle(self, swap_phases: SwapPhases):
+        pickled = pickle.dumps(swap_phases)
+        unpickled = pickle.loads(pickled)
+        assert unpickled == swap_phases
 
 
 @pytest.mark.parametrize(
@@ -1322,6 +1414,11 @@ def test_convert(self, move: ClassicalMove):
         rs_classical_move = _convert_to_rs_instruction(move)
         assert move == _convert_to_py_instruction(rs_classical_move)
 
+    def test_pickle(self, move: ClassicalMove):
+        pickled = pickle.dumps(move)
+        unpickled = pickle.loads(pickled)
+        assert unpickled == move
+
 
 @pytest.mark.parametrize(
     ("left", "right"),
@@ -1353,6 +1450,11 @@ def test_convert(self, exchange: ClassicalExchange):
         rs_classical_exchange = _convert_to_rs_instruction(exchange)
         assert exchange == _convert_to_py_instruction(rs_classical_exchange)
 
+    def test_pickle(self, exchange: ClassicalExchange):
+        pickled = pickle.dumps(exchange)
+        unpickled = pickle.loads(pickled)
+        assert unpickled == exchange
+
 
 @pytest.mark.parametrize(
     ("left", "right"),
@@ -1384,6 +1486,11 @@ def test_convert(self, convert: ClassicalConvert):
         rs_classical_convert = _convert_to_rs_instruction(convert)
         assert convert == _convert_to_py_instruction(rs_classical_convert)
 
+    def test_pickle(self, convert: ClassicalConvert):
+        pickled = pickle.dumps(convert)
+        unpickled = pickle.loads(pickled)
+        assert unpickled == convert
+
 
 @pytest.mark.parametrize(
     ("target", "left", "right"),
@@ -1420,6 +1527,11 @@ def test_convert(self, load: ClassicalLoad):
         rs_classical_load = _convert_to_rs_instruction(load)
         assert load == _convert_to_py_instruction(rs_classical_load)
 
+    def test_pickle(self, load: ClassicalLoad):
+        pickled = pickle.dumps(load)
+        unpickled = pickle.loads(pickled)
+        assert unpickled == load
+
 
 @pytest.mark.parametrize(
     ("target", "left", "right"),
@@ -1460,6 +1572,11 @@ def test_convert(self, store: ClassicalStore):
         rs_classical_store = _convert_to_rs_instruction(store)
         assert store == _convert_to_py_instruction(rs_classical_store)
 
+    def test_pickle(self, store: ClassicalStore):
+        pickled = pickle.dumps(store)
+        unpickled = pickle.loads(pickled)
+        assert unpickled == store
+
 
 @pytest.mark.parametrize(
     ("op", "target", "left", "right"),
@@ -1512,6 +1629,11 @@ def test_convert(self, comparison: ClassicalComparison):
         rs_classical_comparison = _convert_to_rs_instruction(comparison)
         assert comparison == _convert_to_py_instruction(rs_classical_comparison)
 
+    def test_pickle(self, comparison: ClassicalComparison):
+        pickled = pickle.dumps(comparison)
+        unpickled = pickle.loads(pickled)
+        assert unpickled == comparison
+
 
 @pytest.mark.parametrize(
     ("op", "target"),
@@ -1544,6 +1666,11 @@ def test_convert(self, unary: UnaryClassicalInstruction):
         rs_classical_unary = _convert_to_rs_instruction(unary)
         assert unary == _convert_to_py_instruction(rs_classical_unary)
 
+    def test_pickle(self, unary: UnaryClassicalInstruction):
+        pickled = pickle.dumps(unary)
+        unpickled = pickle.loads(pickled)
+        assert unpickled == unary
+
 
 @pytest.mark.parametrize(
     ("op", "left", "right"),
@@ -1595,6 +1722,11 @@ def test_convert(self, arithmetic: UnaryClassicalInstruction):
         rs_classical_arithmetic = _convert_to_rs_instruction(arithmetic)
         assert arithmetic == _convert_to_py_instruction(rs_classical_arithmetic)
 
+    def test_pickle(self, arithmetic: UnaryClassicalInstruction, snapshot: SnapshotAssertion):
+        pickled = pickle.dumps(arithmetic)
+        unpickled = pickle.loads(pickled)
+        assert unpickled == snapshot
+
 
 @pytest.mark.parametrize(
     ("op", "left", "right"),
@@ -1634,6 +1766,11 @@ def test_convert(self, logical: LogicalBinaryOp):
         rs_classical_logical = _convert_to_rs_instruction(logical)
         assert logical == _convert_to_py_instruction(rs_classical_logical)
 
+    def test_pickle(self, logical: LogicalBinaryOp):
+        pickled = pickle.dumps(logical)
+        unpickled = pickle.loads(pickled)
+        assert unpickled == logical
+
 
 def test_include():
     include = Include("my-cool-file.quil")