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

Issue:
Python: Add method for getting version info from quilc



Discussion / hints:

Fix patch:
diff --git a/Cargo.lock b/Cargo.lock
index a6d4e0aa8..7928d1476 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -2,6 +2,15 @@
 # It is not intended for manual editing.
 version = 3
 
+[[package]]
+name = "aho-corasick"
+version = "0.7.19"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b4f55bd91a0978cbfd91c457a164bab8b4001c833b7f323132c0a4e1922dd44e"
+dependencies = [
+ "memchr",
+]
+
 [[package]]
 name = "anyhow"
 version = "1.0.65"
@@ -1296,6 +1305,7 @@ dependencies = [
  "qcs-api-client-grpc",
  "qcs-api-client-openapi",
  "quil-rs",
+ "regex",
  "reqwest",
  "rmp-serde",
  "serde",
@@ -1477,6 +1487,8 @@ version = "1.6.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "4c4eb3267174b8c6c2f654116623910a0fef09c4753f8dd83db29c48a0df988b"
 dependencies = [
+ "aho-corasick",
+ "memchr",
  "regex-syntax",
 ]
 
diff --git a/crates/lib/Cargo.toml b/crates/lib/Cargo.toml
index a22cdfc54..7255ecbd4 100644
--- a/crates/lib/Cargo.toml
+++ b/crates/lib/Cargo.toml
@@ -47,3 +47,4 @@ simple_logger = { version = "2.3.0", default-features = false }
 tempfile = "3.3.0"
 tokio = { version = "1.21.2", features = ["macros", "rt-multi-thread"] }
 warp = "0.3.3"
+regex = "1.6.0"
diff --git a/crates/lib/src/api.rs b/crates/lib/src/api.rs
index e62ab9490..c41b8a026 100644
--- a/crates/lib/src/api.rs
+++ b/crates/lib/src/api.rs
@@ -24,16 +24,27 @@ use crate::qpu::{
 };
 
 /// Uses quilc to convert a Quil program to native Quil
+/// TODO: Add `+ Send + Sync` to the error type once quil-rs supports it:
+/// <https://github.com/rigetti/quil-rs/issues/122>
+/// <https://github.com/rigetti/qcs-sdk-rust/issues/210>
 pub fn compile(
     quil: &str,
     target: TargetDevice,
     client: &Qcs,
-) -> Result<String, Box<dyn std::error::Error>> {
+) -> Result<String, Box<dyn std::error::Error + 'static>> {
     quilc::compile_program(quil, target, client)
         .map_err(Into::into)
         .map(|p| p.to_string(true))
 }
 
+/// Gets the quilc version
+/// TODO: Add `+ Send + Sync` to the error type once quil-rs supports it:
+/// <https://github.com/rigetti/quil-rs/issues/122>
+/// <https://github.com/rigetti/qcs-sdk-rust/issues/210>
+pub fn get_quilc_version(client: &Qcs) -> Result<String, Box<dyn std::error::Error + 'static>> {
+    quilc::get_version_info(client).map_err(Into::into)
+}
+
 /// Collection of errors that can result from rewriting arithmetic.
 #[derive(thiserror::Error, Debug)]
 pub enum RewriteArithmeticError {
diff --git a/crates/lib/src/qpu/quilc/mod.rs b/crates/lib/src/qpu/quilc/mod.rs
index 957484784..3bb59f03b 100644
--- a/crates/lib/src/qpu/quilc/mod.rs
+++ b/crates/lib/src/qpu/quilc/mod.rs
@@ -41,7 +41,7 @@ pub(crate) fn compile_program(
     let request = rpcq::RPCRequest::new("quil_to_native_quil", &params);
     let rpcq_client = rpcq::Client::new(endpoint)
         .map_err(|source| Error::from_quilc_error(endpoint.into(), source))?;
-    match rpcq_client.run_request::<_, QuilcResponse>(&request) {
+    match rpcq_client.run_request::<_, QuilcCompileProgramResponse>(&request) {
         Ok(response) => response
             .quil
             .parse::<quil_rs::Program>()
@@ -50,6 +50,19 @@ pub(crate) fn compile_program(
     }
 }
 
+pub(crate) fn get_version_info(client: &Qcs) -> Result<String, Error> {
+    let config = client.get_config();
+    let endpoint = config.quilc_url();
+    let binding: HashMap<String, String> = HashMap::new();
+    let request = rpcq::RPCRequest::new("get_version_info", &binding);
+    let rpcq_client = rpcq::Client::new(endpoint)
+        .map_err(|source| Error::from_quilc_error(endpoint.into(), source))?;
+    match rpcq_client.run_request::<_, QuilcVersionResponse>(&request) {
+        Ok(response) => Ok(response.quilc),
+        Err(source) => Err(Error::from_quilc_error(endpoint.into(), source)),
+    }
+}
+
 /// All of the errors that can occur within this module.
 #[derive(Debug, thiserror::Error)]
 pub enum Error {
@@ -77,10 +90,15 @@ impl Error {
 }
 
 #[derive(Clone, Deserialize, Debug, Eq, PartialEq, Ord, PartialOrd)]
-struct QuilcResponse {
+struct QuilcCompileProgramResponse {
     pub quil: String,
 }
 
+#[derive(Clone, Deserialize, Debug, Eq, PartialEq, Ord, PartialOrd)]
+struct QuilcVersionResponse {
+    pub quilc: String,
+}
+
 /// The top level params that get passed to quilc
 #[derive(Serialize, Debug, Clone, PartialEq)]
 struct QuilcParams {
@@ -138,6 +156,7 @@ impl TryFrom<InstructionSetArchitecture> for TargetDevice {
 mod tests {
     use super::*;
     use qcs_api_client_openapi::models::InstructionSetArchitecture;
+    use regex::Regex;
     use std::fs::File;
 
     const EXPECTED_H0_OUTPUT: &str = "MEASURE 0\n";
@@ -194,4 +213,12 @@ MEASURE 1 ro[1]
             assert_eq!(shot[0], shot[1]);
         }
     }
+
+    #[tokio::test]
+    async fn get_version_info_from_quilc() {
+        let client = Qcs::load().await.unwrap_or_default();
+        let version = get_version_info(&client).expect("Should get version info from quilc");
+        let semver_re = Regex::new(r"^([0-9]+)\.([0-9]+)\.([0-9]+)$").unwrap();
+        assert!(semver_re.is_match(&version));
+    }
 }
diff --git a/crates/python/qcs_sdk.pyi b/crates/python/qcs_sdk.pyi
index 7da16277a..9071c81cf 100644
--- a/crates/python/qcs_sdk.pyi
+++ b/crates/python/qcs_sdk.pyi
@@ -157,3 +157,9 @@ async def retrieve_results(
         An Awaitable that resolves to a dictionary describing the results of the execution and its duration (see `ExecutionResults`).
     """
     ...
+
+async def get_quilc_version() -> str:
+    """
+    Returns the version number of the running quilc server.
+    """
+    ...
diff --git a/crates/python/src/lib.rs b/crates/python/src/lib.rs
index 0d3f8b93b..cc0a478d2 100644
--- a/crates/python/src/lib.rs
+++ b/crates/python/src/lib.rs
@@ -122,6 +122,18 @@ fn retrieve_results(
     })
 }
 
+#[pyfunction]
+fn get_quilc_version(py: Python<'_>) -> PyResult<&PyAny> {
+    pyo3_asyncio::tokio::future_into_py(py, async move {
+        let client = Qcs::load()
+            .await
+            .map_err(|e| InvalidConfigError::new_err(e.to_string()))?;
+        let version = api::get_quilc_version(&client)
+            .map_err(|e| CompilationError::new_err(e.to_string()))?;
+        Ok(version)
+    })
+}
+
 #[pymodule]
 fn qcs_sdk(_py: Python<'_>, m: &PyModule) -> PyResult<()> {
     m.add_function(wrap_pyfunction!(compile, m)?)?;
@@ -130,5 +142,6 @@ fn qcs_sdk(_py: Python<'_>, m: &PyModule) -> PyResult<()> {
     m.add_function(wrap_pyfunction!(submit, m)?)?;
     m.add_function(wrap_pyfunction!(retrieve_results, m)?)?;
     m.add_function(wrap_pyfunction!(build_patch_values, m)?)?;
+    m.add_function(wrap_pyfunction!(get_quilc_version, m)?)?;
     Ok(())
 }


Test patch:
diff --git a/crates/python/tests/conftest.py b/crates/python/tests/conftest.py
index ce3ce3798..4614bc506 100644
--- a/crates/python/tests/conftest.py
+++ b/crates/python/tests/conftest.py
@@ -1,6 +1,17 @@
+import os
+
 import pytest
 
 
+TEST_CONFIG_DIR = os.path.join(
+    os.path.dirname(os.path.realpath(__file__)), "./qcs_config"
+)
+TEST_QCS_SETTINGS_PATH = os.path.join(TEST_CONFIG_DIR, "settings.toml")
+TEST_QCS_SECRETS_PATH = os.path.join(TEST_CONFIG_DIR, "secrets.toml")
+os.environ["QCS_SETTINGS_FILE_PATH"] = TEST_QCS_SETTINGS_PATH
+os.environ["QCS_SECRETS_FILE_PATH"] = TEST_QCS_SECRETS_PATH
+
+
 @pytest.fixture
 def device_2q() -> str:
     import json
diff --git a/crates/python/tests/qcs_config/secrets.toml b/crates/python/tests/qcs_config/secrets.toml
new file mode 100644
index 000000000..12c409e9f
--- /dev/null
+++ b/crates/python/tests/qcs_config/secrets.toml
@@ -0,0 +1,9 @@
+[credentials]
+
+  [credentials.default]
+
+    [credentials.default.token_payload]
+      access_token = "access"
+      id_token = ""
+      refresh_token = "refresh"
+      token_type = "Bearer"
diff --git a/crates/python/tests/qcs_config/settings.toml b/crates/python/tests/qcs_config/settings.toml
new file mode 100644
index 000000000..e2e9d3f77
--- /dev/null
+++ b/crates/python/tests/qcs_config/settings.toml
@@ -0,0 +1,15 @@
+default_profile_name = "default"
+
+[auth_servers]
+
+  [auth_servers.default]
+    client_id = ""
+    issuer = "https://rigetticomputing.okta.com/oauth2/ausmoidj5qavd4v310h7"
+
+[profiles]
+
+  [profiles.default]
+    api_url = "https://api.qcs.rigetti.com"
+    auth_server_name = "default"
+    credentials_name = "default"
+
diff --git a/crates/python/tests/test_api.py b/crates/python/tests/test_api.py
index efa798656..7abefc608 100644
--- a/crates/python/tests/test_api.py
+++ b/crates/python/tests/test_api.py
@@ -1,11 +1,12 @@
 from unittest.mock import patch
 import pytest
 
+import re
+
 import qcs_sdk
 
 
 @pytest.mark.asyncio
-@pytest.mark.skip
 async def test_compile(native_bitflip_program: str, device_2q: str):
     await qcs_sdk.compile(native_bitflip_program, device_2q)
 
@@ -30,7 +31,7 @@ def test_rewrite_arithmetic():
     rewritten_arithmetic = qcs_sdk.rewrite_arithmetic(native_quil)
     expected = {
         "program": "DECLARE __SUBST REAL[1]\nRX(__SUBST[0]) 0\n",
-        "recalculation_table": ['((2*theta[0])/6.283185307179586)'],
+        "recalculation_table": ["((2*theta[0])/6.283185307179586)"],
     }
     assert rewritten_arithmetic == expected
 
@@ -40,4 +41,10 @@ def test_build_patch_values():
     recalculation_table = ["0.5*theta[0]"]
     expected = {"theta": [2.0], "__SUBST": [1.0]}
     patch_values = qcs_sdk.build_patch_values(recalculation_table, memory)
-    assert patch_values == expected
\ No newline at end of file
+    assert patch_values == expected
+
+
+@pytest.mark.asyncio
+async def test_get_quilc_version():
+    version = await qcs_sdk.get_quilc_version()
+    assert re.match(r"^([0-9]+)\.([0-9]+)\.([0-9]+)$", version)