Repository: rigetti/qcs-sdk-rust (language: python)

Issue:
Forward `native_quil_metadata` from `quilc`
When compiling a program to native quil with `quilc`, the response can optionally contain metadata with helpful details about the result of the compilation. We only return the compiled program right now, but we should also return this metadata.


Discussion / hints:

Fix patch:
diff --git a/Cargo.toml b/Cargo.toml
index 651287240..e31b8c449 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -21,7 +21,7 @@ numpy = "0.17"
 pyo3 = { version = "0.17", features = ["extension-module"] }
 pyo3-asyncio = { version = "0.17", features = ["tokio-runtime"] }
 pyo3-build-config = { version = "0.17" }
-rigetti-pyo3 = { version = "0.1.0-rc.4", features = [
+rigetti-pyo3 = { version = "0.1.0-rc.7", features = [
     "extension-module",
     "complex",
 ] }
diff --git a/crates/lib/src/compiler/quilc.rs b/crates/lib/src/compiler/quilc.rs
index 71ca1822f..2ae747c62 100644
--- a/crates/lib/src/compiler/quilc.rs
+++ b/crates/lib/src/compiler/quilc.rs
@@ -2,6 +2,7 @@
 
 use std::collections::HashMap;
 use std::convert::TryFrom;
+use std::str::FromStr;
 
 use quil_rs::program::{Program, ProgramError};
 use serde::{Deserialize, Serialize};
@@ -42,7 +43,7 @@ pub fn compile_program(
     isa: TargetDevice,
     client: &Qcs,
     options: CompilerOpts,
-) -> Result<Program, Error> {
+) -> Result<CompilationResult, Error> {
     #[cfg(feature = "tracing")]
     tracing::debug!(compiler_options=?options, "compiling quil program with quilc",);
 
@@ -53,12 +54,24 @@ pub fn compile_program(
         rpcq::RPCRequest::new("quil_to_native_quil", &params).with_timeout(options.timeout);
     let rpcq_client = rpcq::Client::new(endpoint)
         .map_err(|source| Error::from_quilc_error(endpoint.into(), source))?;
-    match rpcq_client.run_request::<_, QuilcCompileProgramResponse>(&request) {
-        Ok(response) => response.quil.parse::<Program>().map_err(Error::Parse),
+    match rpcq_client.run_request::<_, QuilToNativeQuilResponse>(&request) {
+        Ok(response) => Ok(CompilationResult {
+            program: Program::from_str(&response.quil).map_err(Error::Parse)?,
+            native_quil_metadata: response.metadata,
+        }),
         Err(source) => Err(Error::from_quilc_error(endpoint.into(), source)),
     }
 }
 
+/// The result of compiling a Quil program to native quil with `quilc`
+#[derive(Clone, Debug, PartialEq)]
+pub struct CompilationResult {
+    /// The compiled program
+    pub program: Program,
+    /// Metadata about the compiled program
+    pub native_quil_metadata: Option<NativeQuilMetadata>,
+}
+
 /// A set of options that determine the behavior of compiling programs with quilc
 #[derive(Clone, Copy, Debug)]
 pub struct CompilerOpts {
@@ -288,9 +301,35 @@ impl Error {
     }
 }
 
-#[derive(Clone, Deserialize, Debug, Eq, PartialEq, Ord, PartialOrd)]
-struct QuilcCompileProgramResponse {
+/// The response from quilc for a `quil_to_native_quil` request.
+#[derive(Clone, Deserialize, Debug, PartialEq, PartialOrd)]
+struct QuilToNativeQuilResponse {
+    /// The compiled program
     quil: String,
+    /// Metadata about the compiled program
+    metadata: Option<NativeQuilMetadata>,
+}
+
+/// Metadata about a program compiled to native quil.
+#[derive(Clone, Deserialize, Debug, PartialEq, PartialOrd)]
+pub struct NativeQuilMetadata {
+    /// Output qubit index relabeling due to SWAP insertion.
+    pub final_rewiring: Vec<u64>,
+    /// Maximum number of successive gates in the native Quil program.
+    pub gate_depth: Option<u64>,
+    /// Total number of gates in the native Quil program.
+    pub gate_volume: Option<u64>,
+    /// Maximum number of two-qubit gates in the native Quil program.
+    pub multiqubit_gate_depth: Option<u64>,
+    /// Rough estimate of native quil program length in seconds.
+    pub program_duration: Option<f64>,
+    /// Rough estimate of fidelity of the native Quil program.
+    pub program_fidelity: Option<f64>,
+    /// Total number of swaps in the native Quil program.
+    pub topological_swaps: Option<u64>,
+    /// The estimated runtime of the program on a Rigetti QPU, in milliseconds. Available only for
+    /// protoquil compliant programs.
+    pub qpu_runtime_estimation: Option<f64>,
 }
 
 #[derive(Clone, Deserialize, Debug, Eq, PartialEq, Ord, PartialOrd)]
@@ -381,7 +420,7 @@ mod tests {
             CompilerOpts::default(),
         )
         .expect("Could not compile");
-        assert_eq!(output.to_string(true), EXPECTED_H0_OUTPUT);
+        assert_eq!(output.program.to_string(true), EXPECTED_H0_OUTPUT);
     }
 
     const BELL_STATE: &str = r##"DECLARE ro BIT[2]
@@ -403,7 +442,7 @@ MEASURE 1 ro[1]
             CompilerOpts::default(),
         )
         .expect("Could not compile");
-        let mut results = crate::qvm::Execution::new(&output.to_string(true))
+        let mut results = crate::qvm::Execution::new(&output.program.to_string(true))
             .unwrap()
             .run(
                 10,
diff --git a/crates/lib/src/qpu/execution.rs b/crates/lib/src/qpu/execution.rs
index 494479b70..54289b2c6 100644
--- a/crates/lib/src/qpu/execution.rs
+++ b/crates/lib/src/qpu/execution.rs
@@ -24,7 +24,7 @@ use super::rewrite_arithmetic::RewrittenProgram;
 use super::translation::EncryptedTranslationResult;
 use super::QpuResultData;
 use super::{get_isa, GetIsaError};
-use crate::compiler::quilc::{self, CompilerOpts, TargetDevice};
+use crate::compiler::quilc::{self, CompilationResult, CompilerOpts, TargetDevice};
 
 /// Contains all the info needed for a single run of an [`crate::Executable`] against a QPU. Can be
 /// updated with fresh parameters in order to re-run the same program against the same QPU with the
@@ -147,7 +147,8 @@ impl<'a> Execution<'a> {
                     task_name: "quilc",
                     source,
                 })
-            })??
+            })?
+            .map(|CompilationResult { program, .. }| program)?
         } else {
             #[cfg(feature = "tracing")]
             trace!("Skipping conversion to Native Quil");
@@ -163,7 +164,10 @@ impl<'a> Execution<'a> {
     }
 
     /// Translate the execution's quil program for it's given quantum processor.
-    pub(crate) async fn translate(&mut self, options: Option<TranslationOptions>) -> Result<EncryptedTranslationResult, Error> {
+    pub(crate) async fn translate(
+        &mut self,
+        options: Option<TranslationOptions>,
+    ) -> Result<EncryptedTranslationResult, Error> {
         let encrpyted_translation_result = translate(
             self.quantum_processor_id.as_ref(),
             &self.program.to_string().0,
@@ -176,12 +180,17 @@ impl<'a> Execution<'a> {
     }
 
     /// Run on a real QPU and wait for the results.
-    pub(crate) async fn submit(&mut self, params: &Parameters, translation_options: Option<TranslationOptions>) -> Result<JobHandle<'a>, Error> {
+    pub(crate) async fn submit(
+        &mut self,
+        params: &Parameters,
+        translation_options: Option<TranslationOptions>,
+    ) -> Result<JobHandle<'a>, Error> {
         #[cfg(feature = "tracing")]
         tracing::debug!(quantum_processor_id=%self.quantum_processor_id, "submitting job to QPU");
 
         let job_target = JobTarget::QuantumProcessorId(self.quantum_processor_id.to_string());
-        self.submit_to_target(params, job_target, translation_options).await
+        self.submit_to_target(params, job_target, translation_options)
+            .await
     }
 
     /// Run on specific QCS endpoint and wait for the results.
@@ -195,7 +204,8 @@ impl<'a> Execution<'a> {
         S: Into<Cow<'a, str>>,
     {
         let job_target = JobTarget::EndpointId(endpoint_id.into().to_string());
-        self.submit_to_target(params, job_target, translation_options).await
+        self.submit_to_target(params, job_target, translation_options)
+            .await
     }
 
     async fn submit_to_target(
@@ -204,7 +214,8 @@ impl<'a> Execution<'a> {
         job_target: JobTarget,
         translation_options: Option<TranslationOptions>,
     ) -> Result<JobHandle<'a>, Error> {
-        let EncryptedTranslationResult { job, readout_map } = self.translate(translation_options).await?;
+        let EncryptedTranslationResult { job, readout_map } =
+            self.translate(translation_options).await?;
 
         let patch_values = self
             .get_substitutions(params)
@@ -249,9 +260,7 @@ impl<'a> Execution<'a> {
                 job_handle.readout_map(),
                 &response.readout_values,
             )),
-            duration: Some(response
-                .execution_duration_microseconds)
-                .map(Duration::from_micros),
+            duration: Some(response.execution_duration_microseconds).map(Duration::from_micros),
         })
     }
 
diff --git a/crates/python/poetry.lock b/crates/python/poetry.lock
index 851218764..c673565fc 100644
--- a/crates/python/poetry.lock
+++ b/crates/python/poetry.lock
@@ -82,6 +82,17 @@ files = [
     {file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"},
 ]
 
+[[package]]
+name = "colored"
+version = "1.4.4"
+description = "Simple library for color and formatting to terminal"
+category = "dev"
+optional = false
+python-versions = "*"
+files = [
+    {file = "colored-1.4.4.tar.gz", hash = "sha256:04ff4d4dd514274fe3b99a21bb52fb96f2688c01e93fba7bef37221e7cb56ce0"},
+]
+
 [[package]]
 name = "exceptiongroup"
 version = "1.1.0"
@@ -286,6 +297,22 @@ pytest = ">=6.1.0"
 [package.extras]
 testing = ["coverage (>=6.2)", "flaky (>=3.5.0)", "hypothesis (>=5.7.1)", "mypy (>=0.931)", "pytest-trio (>=0.7.0)"]
 
+[[package]]
+name = "syrupy"
+version = "3.0.6"
+description = "Pytest Snapshot Test Utility"
+category = "dev"
+optional = false
+python-versions = ">=3.7,<4"
+files = [
+    {file = "syrupy-3.0.6-py3-none-any.whl", hash = "sha256:9c18e22264026b34239bcc87ab7cc8d893eb17236ea7dae634217ea4f22a848d"},
+    {file = "syrupy-3.0.6.tar.gz", hash = "sha256:583aa5ca691305c27902c3e29a1ce9da50ff9ab5f184c54b1dc124a16e4a6cf4"},
+]
+
+[package.dependencies]
+colored = ">=1.3.92,<2.0.0"
+pytest = ">=5.1.0,<8.0.0"
+
 [[package]]
 name = "tomli"
 version = "2.0.1"
@@ -313,4 +340,4 @@ files = [
 [metadata]
 lock-version = "2.0"
 python-versions = "^3.8"
-content-hash = "226706998c75145ffb0124fc12c73c08935e14c1872e1b0bd37f08c1c3e69cb5"
+content-hash = "8d3949abebf19c6aeb6e121cabd530fd4b827d5d6242a571e063cbc68fff1d8f"
diff --git a/crates/python/pyproject.toml b/crates/python/pyproject.toml
index f550694d2..2ee3235c7 100644
--- a/crates/python/pyproject.toml
+++ b/crates/python/pyproject.toml
@@ -49,6 +49,7 @@ maturin = "^0.13.2"
 pytest = "^7.1.3"
 pytest-asyncio = "^0.19.0"
 black = "^22.8.0"
+syrupy = "^3.0.6"
 
 [tool.black]
 line-length = 120
diff --git a/crates/python/qcs_sdk/compiler/quilc.pyi b/crates/python/qcs_sdk/compiler/quilc.pyi
index fb9ed4fff..2efeb22a0 100644
--- a/crates/python/qcs_sdk/compiler/quilc.pyi
+++ b/crates/python/qcs_sdk/compiler/quilc.pyi
@@ -6,7 +6,6 @@ from ..qpu.client import QCSClient
 DEFAULT_COMPILER_TIMEOUT: float
 """Number of seconds to wait before timing out."""
 
-
 class QuilcError(RuntimeError):
     """
     A number of errors that can occur when compiling with Quilc:
@@ -15,8 +14,8 @@ class QuilcError(RuntimeError):
     - Quilc returned an error during compilation.
     - The program was not parsed correctly.
     """
-    ...
 
+    ...
 
 @final
 class CompilerOpts:
@@ -28,11 +27,9 @@ class CompilerOpts:
         timeout: Optional[float] = DEFAULT_COMPILER_TIMEOUT,
         protoquil: Optional[bool] = None,
     ) -> "CompilerOpts": ...
-
     @staticmethod
     def default() -> "CompilerOpts": ...
 
-
 @final
 class TargetDevice:
     """
@@ -45,14 +42,13 @@ class TargetDevice:
     ) -> "TargetDevice":
         """
         Create a ``TargetDevice`` based on an ``InstructionSetArchitecture``.
-        
+
         :param isa: ``InstructionSetArchitecture`` that describes the target device.
 
         :raises QuilcError: If the ``InstructionSetArchitecture`` cannot be converted
         into a format that Quilc understands.
         """
         ...
-
     @staticmethod
     def from_json(
         value: str,
@@ -66,7 +62,6 @@ class TargetDevice:
         """
         ...
 
-
 @final
 class PauliTerm:
     """Pauli Term used for ConjugatePauliByCliffordRequest."""
@@ -77,14 +72,12 @@ class PauliTerm:
         indices: List[int],
         symbols: List[str],
     ) -> "PauliTerm": ...
-
     @property
     def indices(self) -> List[int]:
         """Qubit indices onto which the factors of the Pauli Term are applied."""
         ...
     @indices.setter
     def indices(self, value: List[int]): ...
-
     @property
     def symbols(self) -> List[str]:
         """Ordered factors of the Pauli Term."""
@@ -92,7 +85,6 @@ class PauliTerm:
     @symbols.setter
     def symbols(self, value: List[str]): ...
 
-
 @final
 class ConjugateByCliffordRequest:
     """Request to conjugate a Pauli Term by a Clifford element."""
@@ -103,14 +95,12 @@ class ConjugateByCliffordRequest:
         pauli: PauliTerm,
         clifford: str,
     ) -> "ConjugateByCliffordRequest": ...
-
     @property
     def pauli(self) -> PauliTerm:
         """Pauli Term to conjugate."""
         ...
     @pauli.setter
     def pauli(self, value: PauliTerm): ...
-
     @property
     def clifford(self) -> str:
         """Clifford element."""
@@ -118,7 +108,6 @@ class ConjugateByCliffordRequest:
     @clifford.setter
     def clifford(self, value: str): ...
 
-
 @final
 class ConjugatePauliByCliffordResponse:
     """Pauli Term conjugated by a Clifford element."""
@@ -127,13 +116,11 @@ class ConjugatePauliByCliffordResponse:
     def phase(self) -> int:
         """Encoded global phase factor on the emitted Pauli."""
         ...
-
     @property
     def pauli(self) -> str:
         """Description of the encoded Pauli."""
         ...
 
-
 @final
 class RandomizedBenchmarkingRequest:
     """Request to generate a randomized benchmarking sequence."""
@@ -147,35 +134,30 @@ class RandomizedBenchmarkingRequest:
         seed: Optional[int] = None,
         interleaver: Optional[str] = None,
     ) -> "RandomizedBenchmarkingRequest": ...
-
     @property
     def depth(self) -> int:
         """Depth of the benchmarking sequence."""
         ...
     @depth.setter
     def depth(self, value: int): ...
-
     @property
     def qubits(self) -> int:
         """Number of qubits involved in the benchmarking sequence. Limit 2."""
         ...
     @qubits.setter
     def qubits(self, value: int): ...
-
     @property
     def gateset(self) -> List[str]:
         """List of Quil programs, each describing a Clifford."""
         ...
     @gateset.setter
     def gateset(self, value: List[str]): ...
-
     @property
     def seed(self) -> Optional[int]:
         """PRNG seed. Set this to guarantee repeatable results."""
         ...
     @seed.setter
     def seed(self, value: Optional[int]): ...
-
     @property
     def interleaver(self) -> Optional[str]:
         """Fixed Clifford, specified as a Quil string, to interleave through an RB sequence."""
@@ -183,7 +165,6 @@ class RandomizedBenchmarkingRequest:
     @interleaver.setter
     def interleaver(self, value: Optional[str]): ...
 
-
 @final
 class GenerateRandomizedBenchmarkingSequenceResponse:
     """Randomly generated benchmarking sequence response."""
@@ -193,14 +174,12 @@ class GenerateRandomizedBenchmarkingSequenceResponse:
         """List of Cliffords, each expressed as a list of generator indices."""
         ...
 
-
-
 def compile_program(
     quil: str,
     target: TargetDevice,
     client: Optional[QCSClient] = ...,
     options: Optional[CompilerOpts] = ...,
-) -> str:
+) -> CompilationResult:
     """
     Compile a quil program for a target device.
 
@@ -213,13 +192,12 @@ def compile_program(
     """
     ...
 
-
 async def compile_program_async(
     quil: str,
     target: TargetDevice,
     client: Optional[QCSClient] = ...,
     options: Optional[CompilerOpts] = ...,
-) -> str:
+) -> CompilationResult:
     """
     Compile a quil program for a target device.
     (async analog of ``compile_program``)
@@ -233,6 +211,56 @@ async def compile_program_async(
     """
     ...
 
+@final
+class CompilationResult:
+    @property
+    def program(self) -> str:
+        """The compiled program."""
+        ...
+    @property
+    def native_quil_metadata(self) -> Optional[NativeQuilMetadata]:
+        """Metadata about the compiled program."""
+        ...
+
+@final
+class NativeQuilMetadata:
+    """
+    Metadata about a program compiled to native quil.
+    """
+
+    @property
+    def final_rewiring(self) -> List[int]:
+        """
+        Output qubit index relabeling due to SWAP insertion.
+        """
+        ...
+    @property
+    def gate_depth(self) -> Optional[int]:
+        """Total number of gates in the native Quil program."""
+        ...
+    @property
+    def gate_volume(self) -> Optional[int]:
+        """Total number of gates in the native Quil program."""
+        ...
+    @property
+    def multiqubit_gate_depth(self) -> Optional[int]:
+        """Maximum number of two-qubit gates in the native Quil program."""
+        ...
+    @property
+    def program_duration(self) -> Optional[float]:
+        """Rough estimate of native quil program length in seconds."""
+        ...
+    @property
+    def program_fidelity(self) -> Optional[float]:
+        """Rough estimate of the fidelity of the native quil program."""
+    @property
+    def topological_swaps(self) -> Optional[int]:
+        """Total number of swaps in the native Quil program."""
+        ...
+    @property
+    def qpu_runtime_estimation(self) -> Optional[float]:
+        """The estimated runtime of the program on a Rigetti QPU, in milliseconds. Available only for protoquil compliant programs."""
+        ...
 
 def get_version_info(
     client: Optional[QCSClient] = ...,
@@ -246,7 +274,6 @@ def get_version_info(
     """
     ...
 
-
 async def get_version_info_async(
     client: Optional[QCSClient] = ...,
 ) -> str:
@@ -260,7 +287,6 @@ async def get_version_info_async(
     """
     ...
 
-
 def conjugate_pauli_by_clifford(
     request: ConjugateByCliffordRequest,
     client: Optional[QCSClient] = ...,
@@ -268,7 +294,7 @@ def conjugate_pauli_by_clifford(
     """
     Given a circuit that consists only of elements of the Clifford group, return its action on a PauliTerm.
     In particular, for Clifford C, and Pauli P, this returns the PauliTerm representing CPC^{\dagger}.
-    
+
     :param request: Pauli Term conjugation request.
     :param client: Optional client configuration. If ``None``, a default one is created.
 
@@ -276,8 +302,7 @@ def conjugate_pauli_by_clifford(
     """
     ...
 
-
-def conjugate_pauli_by_clifford_async(
+async def conjugate_pauli_by_clifford_async(
     request: ConjugateByCliffordRequest,
     client: Optional[QCSClient] = ...,
 ) -> ConjugatePauliByCliffordResponse:
@@ -285,7 +310,7 @@ def conjugate_pauli_by_clifford_async(
     Given a circuit that consists only of elements of the Clifford group, return its action on a PauliTerm.
     In particular, for Clifford C, and Pauli P, this returns the PauliTerm representing CPC^{\dagger}.
     (async analog of ``conjugate_pauli_by_clifford``)
-    
+
     :param request: Pauli Term conjugation request.
     :param client: Optional client configuration. If ``None``, a default one is created.
 
@@ -293,7 +318,6 @@ def conjugate_pauli_by_clifford_async(
     """
     ...
 
-
 def generate_randomized_benchmarking_sequence(
     request: RandomizedBenchmarkingRequest,
     client: Optional[QCSClient] = ...,
@@ -310,7 +334,7 @@ def generate_randomized_benchmarking_sequence(
     gateset) is provided, then the sequence instead takes the form
 
         C_1 G C_2 G ... C_(depth-1) G C_inv .
-    
+
     :param request: Pauli Term conjugation request.
     :param client: Optional client configuration. If ``None``, a default one is created.
 
@@ -318,8 +342,7 @@ def generate_randomized_benchmarking_sequence(
     """
     ...
 
-
-def generate_randomized_benchmarking_sequence_async(
+async def generate_randomized_benchmarking_sequence_async(
     request: RandomizedBenchmarkingRequest,
     client: Optional[QCSClient] = ...,
 ) -> GenerateRandomizedBenchmarkingSequenceResponse:
@@ -336,9 +359,9 @@ def generate_randomized_benchmarking_sequence_async(
 
         C_1 G C_2 G ... C_(depth-1) G C_inv .
     (async analog of ``generate_randomized_benchmarking_sequence``)
-    
+
     :param request: Pauli Term conjugation request.
     :param client: Optional client configuration. If ``None``, a default one is created.
 
     :raises QuilcError: If the is a failure connecting to Quilc or if the request is malformed.
-    """
\ No newline at end of file
+    """
diff --git a/crates/python/src/compiler/quilc.rs b/crates/python/src/compiler/quilc.rs
index 158166311..8bae7ca6c 100644
--- a/crates/python/src/compiler/quilc.rs
+++ b/crates/python/src/compiler/quilc.rs
@@ -1,17 +1,19 @@
 use pyo3::{
     exceptions::{PyRuntimeError, PyValueError},
-    pyfunction, pymethods,
-    types::{PyInt, PyString},
-    Py, PyResult,
+    pyfunction, pymethods, Py, PyResult,
 };
 use qcs::compiler::quilc::{
     CompilerOpts, ConjugateByCliffordRequest, ConjugatePauliByCliffordResponse,
-    GenerateRandomizedBenchmarkingSequenceResponse, PauliTerm, RandomizedBenchmarkingRequest,
-    TargetDevice, DEFAULT_COMPILER_TIMEOUT,
+    GenerateRandomizedBenchmarkingSequenceResponse, NativeQuilMetadata, PauliTerm,
+    RandomizedBenchmarkingRequest, TargetDevice, DEFAULT_COMPILER_TIMEOUT,
 };
 use qcs_api_client_openapi::models::InstructionSetArchitecture;
 use rigetti_pyo3::{
     create_init_submodule, py_wrap_data_struct, py_wrap_error, py_wrap_struct, py_wrap_type,
+    pyo3::{
+        pyclass,
+        types::{PyFloat, PyInt, PyString},
+    },
     wrap_error, ToPythonError,
 };
 
@@ -22,6 +24,8 @@ use crate::qpu::isa::PyInstructionSetArchitecture;
 create_init_submodule! {
     classes: [
         PyCompilerOpts,
+        PyCompilationResult,
+        PyNativeQuilMetadata,
         PyTargetDevice,
         PyPauliTerm,
         PyConjugateByCliffordRequest,
@@ -103,16 +107,42 @@ py_function_sync_async! {
         target: PyTargetDevice,
         client: Option<PyQcsClient>,
         options: Option<PyCompilerOpts>,
-    ) -> PyResult<String> {
+    ) -> PyResult<PyCompilationResult> {
         let client = PyQcsClient::get_or_create_client(client).await?;
         let options = options.unwrap_or_default();
         qcs::compiler::quilc::compile_program(&quil, target.into(), &client, options.into())
             .map_err(RustQuilcError::from)
             .map_err(RustQuilcError::to_py_err)
-            .map(|p| p.to_string(true))
+            .map(|result| PyCompilationResult {
+                program: result.program.to_string(true),
+                native_quil_metadata: result.native_quil_metadata.map(PyNativeQuilMetadata)
+            })
+
     }
 }
 
+py_wrap_data_struct! {
+    PyNativeQuilMetadata(NativeQuilMetadata) as "NativeQuilMetadata" {
+        final_rewiring: Vec<u64> => Vec<Py<PyInt>>,
+        gate_depth: Option<u64> => Option<Py<PyInt>>,
+        gate_volume: Option<u64> => Option<Py<PyInt>>,
+        multiqubit_gate_depth: Option<u64> => Option<Py<PyInt>>,
+        program_duration: Option<f64> => Option<Py<PyFloat>>,
+        program_fidelity: Option<f64> => Option<Py<PyFloat>>,
+        topological_swaps: Option<u64> => Option<Py<PyInt>>,
+        qpu_runtime_estimation: Option<f64> => Option<Py<PyFloat>>
+
+    }
+}
+
+#[pyclass(name = "CompilationResult")]
+pub struct PyCompilationResult {
+    #[pyo3(get)]
+    program: String,
+    #[pyo3(get)]
+    native_quil_metadata: Option<PyNativeQuilMetadata>,
+}
+
 py_function_sync_async! {
     #[pyfunction(client = "None")]
     async fn get_version_info(


Test patch:
diff --git a/crates/python/tests/compiler/__snapshots__/test_quilc.ambr b/crates/python/tests/compiler/__snapshots__/test_quilc.ambr
new file mode 100644
index 000000000..8f5737510
--- /dev/null
+++ b/crates/python/tests/compiler/__snapshots__/test_quilc.ambr
@@ -0,0 +1,181 @@
+# name: test_compile_program
+  '''
+  DECLARE ro BIT[2]
+  RZ(((-pi)/2)) 1
+  RX((pi/2)) 1
+  RZ((pi/2)) 1
+  RZ(pi) 0
+  RX((pi/2)) 0
+  RZ(pi) 0
+  RX(((-pi)/2)) 0
+  CZ 1 0
+  RZ(pi) 0
+  RZ(((-3)*(pi/2))) 1
+  RX((pi/2)) 1
+  RZ((3*(pi/2))) 1
+  MEASURE 0 ro[0]
+  MEASURE 1 ro[1]
+  
+  '''
+# ---
+# name: test_compile_program.1
+  NativeQuilMetadata(
+    final_rewiring=list([
+      0,
+      1,
+      2,
+      3,
+      4,
+      5,
+      6,
+      7,
+      8,
+      9,
+      10,
+      11,
+      12,
+      13,
+      14,
+      15,
+      16,
+      17,
+      18,
+      19,
+      20,
+      21,
+      22,
+      23,
+      24,
+      25,
+      26,
+      27,
+      28,
+      29,
+      30,
+      31,
+      32,
+      33,
+      34,
+      35,
+      36,
+      37,
+      38,
+      39,
+      40,
+      41,
+      42,
+      43,
+      44,
+      45,
+      46,
+      47,
+      48,
+      49,
+      50,
+      51,
+      52,
+      53,
+      54,
+      55,
+      56,
+      57,
+      58,
+      59,
+      60,
+      61,
+      62,
+      63,
+      64,
+      65,
+      66,
+      67,
+      68,
+      69,
+      70,
+      71,
+      72,
+      73,
+      74,
+      75,
+      76,
+      77,
+      78,
+      79,
+      80,
+      81,
+      82,
+      83,
+      84,
+      85,
+      86,
+      87,
+      88,
+      89,
+      90,
+      91,
+      92,
+      93,
+      94,
+      95,
+      96,
+      97,
+      98,
+      99,
+      100,
+      101,
+      102,
+      103,
+      104,
+      105,
+      106,
+      107,
+      108,
+      109,
+      110,
+      111,
+      112,
+      113,
+      114,
+      115,
+      116,
+      117,
+      118,
+      119,
+      120,
+      121,
+      122,
+      123,
+      124,
+      125,
+      126,
+      127,
+      128,
+      129,
+      130,
+      131,
+      132,
+      133,
+      134,
+      135,
+      136,
+      137,
+      138,
+      139,
+      140,
+      141,
+      142,
+      143,
+      144,
+      145,
+      146,
+      147,
+    ]),
+    gate_depth=8,
+    gate_volume=12,
+    multiqubit_gate_depth=1,
+    program_duration=350.03999999999996,
+    program_fidelity=0.947021779021503,
+    qpu_runtime_estimation=201.5111541748047,
+    topological_swaps=0,
+  )
+# ---
diff --git a/crates/python/tests/compiler/test_quilc.py b/crates/python/tests/compiler/test_quilc.py
index dba23ada3..e94b6642f 100644
--- a/crates/python/tests/compiler/test_quilc.py
+++ b/crates/python/tests/compiler/test_quilc.py
@@ -1,4 +1,5 @@
 import pytest
+from syrupy.assertion import SnapshotAssertion
 
 from qcs_sdk.qpu.isa import InstructionSetArchitecture
 from qcs_sdk.compiler.quilc import (
@@ -21,6 +22,7 @@
     generate_randomized_benchmarking_sequence_async,
 )
 
+
 @pytest.fixture
 def target_device(aspen_m_3_isa: InstructionSetArchitecture) -> TargetDevice:
     return TargetDevice.from_isa(aspen_m_3_isa)
@@ -39,8 +41,9 @@ def test_compiler_opts():
 def test_target_device_error(aspen_m_3_isa: InstructionSetArchitecture):
     """TargetDevice cannot be built when ISA lacks ``randomized_benchmark_simultaneous_1q`` benchmark."""
     isa = InstructionSetArchitecture.from_raw(aspen_m_3_isa.json())
-    isa.benchmarks=[
-        benchmark for benchmark in isa.benchmarks
+    isa.benchmarks = [
+        benchmark
+        for benchmark in isa.benchmarks
         if benchmark.name != "randomized_benchmark_simultaneous_1q"
     ]
     with pytest.raises(QuilcError):
@@ -50,10 +53,12 @@ def test_target_device_error(aspen_m_3_isa: InstructionSetArchitecture):
 def test_compile_program(
     bell_program: str,
     target_device: TargetDevice,
+    snapshot: SnapshotAssertion,
 ):
     """A simple program should compile successfully."""
-    compiled_program = compile_program(bell_program, target_device)
-    assert compiled_program
+    result = compile_program(bell_program, target_device)
+    assert result.program == snapshot
+    assert result.native_quil_metadata == snapshot
 
 
 @pytest.mark.asyncio
@@ -82,8 +87,7 @@ async def test_get_version_info_async():
 def test_conjugate_pauli_by_clifford():
     """Pauli should be conjugated by clifford."""
     request = ConjugateByCliffordRequest(
-        pauli=PauliTerm(indices=[0], symbols=["X"]),
-        clifford="H 0"
+        pauli=PauliTerm(indices=[0], symbols=["X"]), clifford="H 0"
     )
     response = conjugate_pauli_by_clifford(request)
     assert type(response) == ConjugatePauliByCliffordResponse
@@ -95,8 +99,7 @@ def test_conjugate_pauli_by_clifford():
 async def test_conjugate_pauli_by_clifford_async():
     """Pauli should be conjugated by clifford."""
     request = ConjugateByCliffordRequest(
-        pauli=PauliTerm(indices=[0], symbols=["X"]),
-        clifford="H 0"
+        pauli=PauliTerm(indices=[0], symbols=["X"]), clifford="H 0"
     )
     response = await conjugate_pauli_by_clifford_async(request)
     assert type(response) == ConjugatePauliByCliffordResponse
@@ -130,4 +133,4 @@ async def test_generate_randomized_benchmark_sequence_async():
     )
     response = await generate_randomized_benchmarking_sequence_async(request)
     assert type(response) == GenerateRandomizedBenchmarkingSequenceResponse
-    assert response.sequence == [[1, 0], [0, 1, 0, 1], [1, 0]]
\ No newline at end of file
+    assert response.sequence == [[1, 0], [0, 1, 0, 1], [1, 0]]