Intro

Welcome to the QCS SDK book πŸ‘‹. This SDK is all about running quantum programs on Rigetti's QCS Platform. There are three different SDKs today:

  1. pyQuil is the original Python SDK, the reference implementation for interacting with QCS.
  2. The Rust SDK is on crates.io.
  3. The C SDK is a thin wrapper around the Rust SDK that exposes the public API through C FFI. That documentation is right here!

Installation

C SDK

Download Artifacts

To avoid building from source, you can download these things:

  1. The dynamic library for your platform (if it's available on GitHub)
  2. The libqcs.h header file

Build from Source

Check out the GitHub Repo README for instructions.

Services

Full usage of this library requires quilc and qvm to be available on local webservers. The easiest way to do this is by using this docker-compose file and running docker-compose up -d. This will run the required services on their default ports in the background.

Getting Started from C

Basics

The point of libqcs is to allow you to run a quil program on QCS. The process generally looks like this:

graph TD
    classDef clickable color:#00b5ad,stroke:#00b5ad,fill:#e8e8e9
    classDef default stroke:#0d0d36,fill:#e8e8e9
    
    subgraph Setup
        quil["Quil (string)"] --> exe_fn{{executable_from_quil}} --> Executable
        click exe_fn "/api/executable_from_quil.html"
        click Executable "/api/executable.html"
        Executable -.-> config((Apply Options)) -.-> Executable
        Executable --After all executions--> free_executable{{free_executable}}
        click free_executable "/api/free_executable.html"
        
        class exefn,Executable clickable
    end
    style Setup fill:#cfcfd7,stroke:#0d0d36

    Executable ==Run multiple times==> qvm_or_qpu{QVM or QPU?}
    
    subgraph Execute
        qvm_or_qpu --QVM--> execute_on_qvm{{execute_on_qvm}}
        click execute_on_qvm "/api/execute_on_qvm.html"
        qvm_or_qpu --QPU--> execute_on_qpu{{execute_on_qpu}}
        click execute_on_qpu "/api/execute_on_qpu.html"
        
        execute_on_qvm --> ExecutionResult
        execute_on_qpu --> ExecutionResult
        click ExecutionResult "/api/execution_result.html"
        ExecutionResult --> check_for_errors((Check for Errors))
        --> ExecutionData
        --> get_data{{get_data}}
        --> RegisterData
        click get_data "/api/get_data.html"
        click ExecutionData "/api/execution_data.html"
        click RegisterData "/api/register_data.html"
        ExecutionResult --When done with all data--> free_program_result{{free_execution_result}}
        click free_program_result "/api/free_execution_result.html"
        
    end
    style Execute fill:#cfcfd7,stroke:#0d0d36
    

You pass the string of a Quil program into executable_from_quil. You get back an Executable. Then, configure the Executable with any options. Finally, use either execute_on_qvm (for simulating results) or execute_on_qpu (for running against a real Quantum Computer) to get a ExecutionResult.

Once you have an ExecutionResult, check it for any errors, read the data out of it using get_data, then free it once you're done with this execution using free_execution_result.

You may execute a single Executable multiple times with varying options (e.g. different parameters) for the same Quil program. Once you're done with that Executable, call free_executable to avoid leaking memory.

There are several options that can be applied to an Executable to achieve different effects:

An Example

Let's walk through an example by reviewing some code used to test this library:

Step 1: Include libqcs.h

#include "../libqcs.h"

Step 2: Define a Quil Program

In this case, we have a constant program, but you could also write one dynamically at runtime.

char *BELL_STATE_PROGRAM =
        "DECLARE ro BIT[2]\n"
        "H 0\n"
        "CNOT 0 1\n"
        "MEASURE 0 ro[0]\n"
        "MEASURE 1 ro[1]\n";

Step 3: Run the Program

Here we create an Executable using executable_from_quil, then wrap it in 3 "shots" using wrap_in_shots. The number of shots is the number of times that the QPU will run a program per execution. We measured to memory called "ro"; that is the default, so we don't need to call read_from. execute_on_qvm runs the Executable on the locally-running QVM (simulated quantum computer). The return value is an ExecutionResult which contains either the resulting data or any error messages.

    unsigned int shots = 3;
    Executable *exe = executable_from_quil(BELL_STATE_PROGRAM);
    wrap_in_shots(exe, shots);
    ExecutionResult *result = execute_on_qvm(exe);

If we want to run on a real QPU, we swap out the function and add a parameter specifying which QPU to run against:

    ExecutionResult result = execute_on_qpu(exe, "Aspen-11");

Step 4: Handle Errors

If something goes wrong, ExecutionResult will be the Error variant. This field contains a human-readable description of the error.

    if (result->tag == ExecutionResult_Error) {
        return fail(
                TEST_NAME,
                result->error,
                exe,
                result
        );
    }

For the sake of our test cases, we have defined a function called fail which frees the memory of result and exe for us. Make sure to always free all ExecutionResults using free_execution_result and any Executable using free_executable.

Step 5: Process Results

If there were no errors, then you can safely read your results out of the requested registers using get_data which returns a RegisterData! In this case, we know a successful response will be a Byte variant since we read out of BIT memory.

    const RegisterData *ro = get_data(result->success.handle, "ro");
    if (ro == NULL) {
        return fail(
                TEST_NAME,
                "ro register was not in result",
                exe,
                result
        );
    }
    if (ro->data.tag != DataType_Byte) {
        char message[50];
        sprintf(message, "Expected type Byte, got tag  %d", ro->data.tag);
        return fail(
                TEST_NAME,
                message,
                exe,
                result
        );
    }
    for (int shot = 0; shot < ro->number_of_shots; shot++) {
        // In our case, we measured two entangled qubits, so we expect their values to be equal.
        int bit_0 = ro->data.byte[shot][0];
        int bit_1 = ro->data.byte[shot][1];
        if (bit_0 != bit_1) {
            char message[50];
            sprintf(
                    message,
                    "in shot %d, got |%d%d",
                    shot,
                    bit_0,
                    bit_1
            );
            return fail(
                    TEST_NAME,
                    message,
                    exe,
                    result
            );
        }
    }

data.byte is a 2D array of bytes. There is an array representing the requested register per shot. In this case, there are 2 bits to read and three shots, so the data looks something like this:

[[0, 0],[1, 1],[0, 0]]

ro->number_of_shots contains the outer dimension (3). For this test we know there are exactly 2 slots in that register, but we could read that inner dimension dynamically with ro->shot_length.

Step 6: Free the Memory

You must call free_executable and free_execution_result to deallocate Executable and ExecutionResult safely:

    free_executable(exe);
    if (executionResult != NULL) {
        free_execution_result(executionResult);
    }

All Together

Here's what the full integration test looks like from our test suite:

#include <math.h>
#include <stdbool.h>
#include <stdio.h>
#include <string.h>
#include "../libqcs.h"

char *BELL_STATE_PROGRAM =
        "DECLARE ro BIT[2]\n"
        "H 0\n"
        "CNOT 0 1\n"
        "MEASURE 0 ro[0]\n"
        "MEASURE 1 ro[1]\n";

bool fail(
        const char *testName,
        char *message,
        Executable *exe,
        ExecutionResult *executionResult
) {
    printf("❌ %s failed: %s\n", testName, message);
    free_executable(exe);
    if (executionResult != NULL) {
        free_execution_result(executionResult);
    }

    return false;
}

bool succeed(const char *testName, Executable *exe, ExecutionResult *executionResult) {
    printf("βœ… %s succeeded.\n", testName);
    free_executable(exe);
    if (executionResult != NULL) {
        free_execution_result(executionResult);
    }
    return true;
}

bool test_bell_state() {
    const char *TEST_NAME = "test_bell_state";

    unsigned int shots = 3;
    Executable *exe = executable_from_quil(BELL_STATE_PROGRAM);
    wrap_in_shots(exe, shots);
    ExecutionResult *result = execute_on_qvm(exe);

    if (result->tag == ExecutionResult_Error) {
        return fail(
                TEST_NAME,
                result->error,
                exe,
                result
        );
    }

    const RegisterData *ro = get_data(result->success.handle, "ro");
    if (ro == NULL) {
        return fail(
                TEST_NAME,
                "ro register was not in result",
                exe,
                result
        );
    }

    if (ro->data.tag != DataType_Byte) {
        char message[50];
        sprintf(message, "Expected type Byte, got tag  %d", ro->data.tag);
        return fail(
                TEST_NAME,
                message,
                exe,
                result
        );
    }

    if (ro->number_of_shots != shots) {
        char message[50];
        sprintf(message, "Response number of shots was %d, expected %d", ro->number_of_shots, shots);
        return fail(
                TEST_NAME,
                message,
                exe,
                result
        );
    }

    if (ro->shot_length != 2) {
        char message[50];
        sprintf(message, "expected shot_length of 2, got %d", ro->shot_length);
        return fail(
                TEST_NAME,
                message,
                exe,
                result
        );
    }

    for (int shot = 0; shot < ro->number_of_shots; shot++) {
        // In our case, we measured two entangled qubits, so we expect their values to be equal.
        int bit_0 = ro->data.byte[shot][0];
        int bit_1 = ro->data.byte[shot][1];
        if (bit_0 != bit_1) {
            char message[50];
            sprintf(
                    message,
                    "in shot %d, got |%d%d",
                    shot,
                    bit_0,
                    bit_1
            );
            return fail(
                    TEST_NAME,
                    message,
                    exe,
                    result
            );
        }
    }

    return succeed(TEST_NAME, exe, result);
}

executable_from_quil

Create an Executable from a string containing a Quil program. Be sure to free this Executable using free_executable once all executions are complete.

Definition

struct Executable *executable_from_quil(char *quil);

Executable

An intentionally opaque struct used internally by the SDK to track state and cache various stages of compilation for better performance. Created by executable_from_quil.

Definition

typedef struct Executable Executable;

See Also

read_from

Set the memory location to read out of for an Executable. If not set, the Executable assumes a default of "ro". You can call this function multiple times to read from multiple registers. The first time you call the function, the default of "ro" is not longer relevant.

Definition

void read_from(struct Executable *executable, char *name);

Arguments

  1. executable: The Executable to set the parameter on.
  2. name: The name of the memory region to read out of. Must match a Quil DECLARE statement exactly.

Safety

  1. executable must be the result of executable_from_quil
  2. name must be a valid, non-NULL, nul-terminated string. It must also live until executableis freed.

Example

With a program like this one:

    char *REAL_MEMORY_PROGRAM =
        "DECLARE first REAL[1]\n"
        "DECLARE second OCTET[1]\n"
        "MOVE first[0] 3.141\n"
        "MOVE second[0] 2\n";

We've declared a region called first and another called secondβ€”both of which we'd like to read out of. Since we are not using a single register called "ro" (the default), we need to specify where to read from.

    Executable *exe = executable_from_quil(REAL_MEMORY_PROGRAM);
    read_from(exe, "first");
    read_from(exe, "second");
    ExecutionResult *result = execute_on_qvm(exe);

set_param

Set the value of a parameter on an Executable for parametric execution.

Definition

void set_param(struct Executable *executable, char *name, unsigned int index, double value);

Arguments

  1. executable: The Executable to set the parameter on.
  2. name: The name of the memory region to set a value for.
  3. index: The index into the memory region where the value should be set.
  4. value: The value to set for name[index].

Safety

  1. executable must be the result of executable_from_quil
  2. name must be a valid, non-NULL, nul-terminated string. It must also live until executableis freed.

Example

Adapted from pyQuil

With a program like this one:

    char *PARAMETRIZED_PROGRAM =
        "DECLARE ro BIT\n"
        "DECLARE theta REAL\n"

        "RX(pi / 2) 0\n"
        "RZ(theta) 0\n"
        "RX(-pi / 2) 0\n"

        "MEASURE 0 ro[0]\n";

We've declared a region called theta that is used as a parameter. This parameter must be injected for any executions. You might have a loop which looks like this, where you execute on multiple different parameters:

    for (int step = 0; step < STEPS; step++) {
        theta = step * step_size;
        set_param(exe, "theta", 0, theta);

        ExecutionResult *result = execute_on_qvm(exe);
        
        // Do things with result...

        free_execution_result(result);
    }

In that example, exe is an Executable which would have been previously allocated using executable_from_quil.

Make sure to free each ExecutionResult in the loop to avoid leaking memory!

If theta was a larger vector (e.g. DECLARE theta REAL[2]) then you would set the other indexes like set_param(exe, "theta", index, another_value).

wrap_in_shots

Set the Executable to run multiple times per execution on the QPU. If this option is not set, the Executable will be run one time per execution.

Definition

void wrap_in_shots(struct Executable *executable, unsigned short shots);

Arguments

  1. executable: The Executable to set the parameter on.
  2. shots: The number of times to run the executable for each execution.

Safety

  1. executable must be the result of executable_from_quil

Example

Take a simple bell state program:

    char *BELL_STATE_PROGRAM =
        "DECLARE ro BIT[2]\n"
        "H 0\n"
        "CNOT 0 1\n"
        "MEASURE 0 ro[0]\n"
        "MEASURE 1 ro[1]\n";

If we run the program like this:

    unsigned int shots = 3;
    Executable *exe = executable_from_quil(BELL_STATE_PROGRAM);
    wrap_in_shots(exe, shots);
    ExecutionResult *result = execute_on_qvm(exe);

Then the program will be executed 3 times (per the number of shots set). The resulting Byte data of one execution will be a 2D array with an outer dimension of 3 (number of shots) and an inner dimension of 2 (amount of memory locations read per run).

execute_on_qvm

Run an Executable against a locally-running simulator.

Definition

struct ExecutionResult *execute_on_qvm(struct Executable *executable);

Safety

  1. You must provide the return value from this function to free_execution_result once you're done with it.

Usage

In order to execute, QVM must be running at http://localhost:5000 (unless you've specified a different endpoint in config).

Arguments

  1. executable: An Executable

Errors

This program will return the Error variant of ExecutionResult with a human-readable description of the error. Some common errors:

  1. QVM was not running or not reachable.
  2. A syntax error in the provided Quil program.
  3. There was no data to read (improper or missing read_from option).

execute_on_qpu

Execute an Executable on a specified QPU.

Definition

struct ExecutionResult *execute_on_qpu(struct Executable *executable, char *qpu_id);

Safety

  1. In order to run this function safely, you must provide the return value from this function to free_execution_result once you're done with it.
  2. The input qpu_id must be a valid, nul-terminated, non-null string which remains constant for the duration of this function.

Usage

In order to execute, you must have an active reservation for the QPU you're targeting as well as valid, configured QCS credentials.

Configuration

Valid settings and secrets must be set either in ~/.qcs or by setting the OS environment variables QCS_SECRETS_FILE_PATH and QCS_SETTINGS_FILE_PATH for secrets and settings respectively. QCS_PROFILE_NAME can also be used to choose a different profile in those configuration files.

Arguments

  1. executable: The Executable to execute.
  2. qpu_id: the ID of the QPU to run on (e.g. "Aspen-11")

Errors

This program will return the Error variant of ExecutionResult with a human-readable description of the error. Some common errors:

  1. QCS could not be authenticated with due to missing / invalid configuration.
  2. Authenticated user has no active reservation for the requested qpu_id.
  3. A syntax error in the provided Quil when calling executable_from_quil.
  4. There was no data to read (improper or missing read_from option).

ExecutionResult

An ExecutionResult is the struct you receive from a call to either execute_on_qvm or execute_on_qpu. This struct is implemented as a tagged union, describing the possible outcomes of program execution.

SAFETY

You must pass this struct to free_execution_result when you're done with it to avoid leaking the memory.

Any RegisterData which was retrieved from this result will be freed when this is called.

Definition

typedef struct ExecutionResult {
    ExecutionResult_Tag tag;
    union {
        struct {
            struct ExecutionData success;
        };
        struct {
            char *error;
        };
    };
} ExecutionResult;

Variants

There are multiple variants of ExecutionResult. The tag attribute determines which is in use via an enum:

typedef enum ExecutionResult_Tag {
    ExecutionResult_Success,
    ExecutionResult_Error,
} ExecutionResult_Tag;

The Error variant indicates that execution failed. The Success variant is populated in the case of a successful run, and contains an ExecutionData.

Error

If something goes wrong, tag will be ExecutionResult_Error, indicating it is the Error variant. This variant is a human-readable string of the error that occurred.

Error Example

Here, result.error is that string:

    unsigned int shots = 3;
    Executable *exe = executable_from_quil(BELL_STATE_PROGRAM);
    wrap_in_shots(exe, shots);
    ExecutionResult *result = execute_on_qvm(exe);

    if (result->tag == ExecutionResult_Error) {
        return fail(
                TEST_NAME,
                result->error,
                exe,
                result
        );
    }

Success

If there is not an error, tag will instead be ExecutionResult_Success. The success attribute is an ExecutionData.

Handle Example

    const RegisterData *ro = get_data(result->success.handle, "ro");
    if (ro == NULL) {
        return fail(
                TEST_NAME,
                "ro register was not in result",
                exe,
                result
        );
    }

ExecutionData

ExecutionData contains a ResultHandle to the RegisterData containing the results of the program, as well as some metadata about the execution itself. You get an ExecutionData from the Success variant of an ExecutionResult.

Definition

typedef struct ExecutionData {
    unsigned long execution_duration_microseconds;
    struct ResultHandle *handle;
} ExecutionData;

Safety

The memory for any ExecutionData will be freed when calling free_execution_result for the corresponding ExecutionResult. Make sure not to free the ExecutionResult until after you're done with the data.

Attributes

  1. execution_duration_microseconds is how long the program took to execute on the QPU. This is always 0 for QVM executions.
  2. handle is the ResultHandle that can be passed to get_data to get RegisterData containing the results of the program.

ResultHandle

ResultHandle is an opaque struct managed by the SDK which allows you to dynamically read out results with get_data. You will only ever have a pointer to this struct accessed from within an ExecutionResult if it is the Success variant.

Definition

typedef struct ResultHandle ResultHandle;

get_data

get_data is how you retrieve the actual results (in a RegisterData) from the ResultHandle within the ExecutionData from the Success variant of an ExecutionResult. The register name provided to this function should match one that you provided to read_from (or "ro" if left at the default).

Definition

const struct RegisterData *get_data(const struct ResultHandle *handle, const char *name);

Safety

All inputs must be non-null. name must be a nul-terminated string. handle must come from the result of a non-error call to execute_on_qvm or execute_on_qpu. If there are no results matching the provided name then the return value will be NULL, make sure to check this return value.

Arguments

  1. handle is the handle attribute of ExecutionData.
  2. name is the name of a register you want the data from. It should correspond to a call to read_from (or the default of "ro") and a Quil DECLARE instruction.

Returns

A RegisterData if there was data for the requested register, or NULL if not.

Example

    Executable *exe = executable_from_quil(REAL_MEMORY_PROGRAM);
    read_from(exe, "first");
    read_from(exe, "second");
    ExecutionResult *result = execute_on_qvm(exe);

    const RegisterData *first = get_data(result->success.handle, "first");
    const RegisterData *second = get_data(result->success.handle, "second");

Our ExecutionResult named result is a Success variant, so we can access the ExecutionData via result->success. We can then call get_data with that ExecutionData's handle attribute and the name of a register we want to retrieve.

RegisterData

RegisterData contains the actual results of an execution for a partiuclar register (e.g., "ro"). You get a pointer to one by calling get_data.

Definition

typedef struct RegisterData {
    unsigned short number_of_shots;
    unsigned short shot_length;
    struct DataType data;
} RegisterData;

typedef struct DataType {
    DataType_Tag tag;
    union {
        struct {
            char **byte;
        };
        struct {
            double **real;
        };
    };
} DataType;

typedef enum DataType_Tag {
    DataType_Byte,
    DataType_Real,
} DataType_Tag;

Safety

The memory for any RegisterData will be freed when calling free_execution_result for the corresponding ExecutionResult. Make sure not to free the ExecutionResult until after you're done with the data.

Attributes

  1. number_of_shots is the outer dimension of the 2D array of data. This should always be equal to the parameter provided to wrap_in_shots (or 1 if not called).
  2. shot_length is the inner dimension of the data array, corresponding to the dimension of the declared memory. For example, declaring BIT in Quil will result in a shot_length of 1, but declaring BIT[2] in Quil will result in a shot_length of 2.
  3. data is a DataType which contains the actual results as measured from the requested register. This is a 2D array with outer dimension of number_of_shots and inner dimension of shot_length. The type of this data depends on the type of the declared memory. The tag field tells you which type of data is contained within, then byte or real is the 2D array.

Variants

The type of data will depend on how the memory was declared in Quil.

Byte

The result of reading from a BIT or OCTET register is the Byte variant. data.tag will be DataType_Byte, and data.byte will be populated.

Real

The result of reading from a REAL register is the Real variant. data.tag will be DataType_Real, and data.real will be populated.

Example

Here we declare both REAL and OCTET registers which will correspond to Real and Byte variants.

char *REAL_MEMORY_PROGRAM =
        "DECLARE first REAL[1]\n"
        "DECLARE second OCTET[1]\n"
        "MOVE first[0] 3.141\n"
        "MOVE second[0] 2\n";

bool test_real_data_type() {
    const char *TEST_NAME = "test_real_data_type";

    Executable *exe = executable_from_quil(REAL_MEMORY_PROGRAM);
    read_from(exe, "first");
    read_from(exe, "second");
    ExecutionResult *result = execute_on_qvm(exe);

    if (result->tag == ExecutionResult_Error) {
        return fail(
                TEST_NAME,
                result->error,
                exe,
                result
        );
    }

    const RegisterData *first = get_data(result->success.handle, "first");
    const RegisterData *second = get_data(result->success.handle, "second");

    if (first == NULL || first->data.tag != DataType_Real) {
        return fail(
                TEST_NAME,
                "first register did not contain real data",
                exe,
                result
        );
    }
    if (second == NULL || second->data.tag != DataType_Byte) {
        return fail(
                TEST_NAME,
                "second register did not contain byte data",
                exe,
                result
        );
    }

    if (first->data.real[0][0] != 3.141) {
        char message[50];
        sprintf(
                message,
                "Found %f in first, expected 3.141",
                first->data.real[0][0]
        );
        return fail(
                TEST_NAME,
                message,
                exe,
                result
        );
    }
    if (second->data.byte[0][0] != 2) {
        char message[50];
        sprintf(
                message,
                "Found %d in first, expected 2",
                second->data.byte[0][0]
        );
        return fail(
                TEST_NAME,
                message,
                exe,
                result
        );
    }

    return succeed(TEST_NAME, exe, result);
}

free_execution_result

free_execution_result is a memory deallocation function which must be called for any ExecutionResult to avoid leakage.

Safety

Only call this function with the result of execute_on_qpu or execute_on_qvm as it expects memory that was originally allocated by Rust.

Definition

void free_execution_result(struct ExecutionResult *result);

free_executable

free_executable is a memory deallocation function which must be called for any Executable to avoid leakage.

Safety

Only call this function with the result of executable_from_quil as it expects memory that was originally allocated by Rust.

Definition

void free_executable(struct Executable *executable);