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:
- pyQuil is the original Python SDK, the reference implementation for interacting with QCS.
- The Rust SDK is on crates.io.
- 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:
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
executable: TheExecutableto set the parameter on.name: The name of the memory region to read out of. Must match a QuilDECLAREstatement exactly.
Safety
executablemust be the result ofexecutable_from_quilnamemust be a valid, non-NULL, nul-terminated string. It must also live untilexecutableis 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
executable: TheExecutableto set the parameter on.name: The name of the memory region to set a value for.index: The index into the memory region where the value should be set.value: The value to set forname[index].
Safety
executablemust be the result ofexecutable_from_quilnamemust be a valid, non-NULL, nul-terminated string. It must also live untilexecutableis 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
thetawas a larger vector (e.g.DECLARE theta REAL[2]) then you would set the other indexes likeset_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
executable: TheExecutableto set the parameter on.shots: The number of times to run the executable for each execution.
Safety
executablemust be the result ofexecutable_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
- You must provide the return value from this function to
free_execution_resultonce 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
executable: AnExecutable
Errors
This program will return the Error variant of ExecutionResult with a human-readable description of the error. Some common errors:
- QVM was not running or not reachable.
- A syntax error in the provided Quil
program. - There was no data to read (improper or missing
read_fromoption).
execute_on_qpu
Execute an Executable on a specified QPU.
Definition
struct ExecutionResult *execute_on_qpu(struct Executable *executable, char *qpu_id);
Safety
- In order to run this function safely, you must provide the return value from this function to
free_execution_resultonce you're done with it. - The input
qpu_idmust 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
executable: TheExecutableto execute.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:
- QCS could not be authenticated with due to missing / invalid configuration.
- Authenticated user has no active reservation for the requested
qpu_id. - A syntax error in the provided Quil when calling
executable_from_quil. - There was no data to read (improper or missing
read_fromoption).
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
execution_duration_microsecondsis how long the program took to execute on the QPU. This is always 0 for QVM executions.handleis theResultHandlethat can be passed toget_datato getRegisterDatacontaining 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
handleis thehandleattribute ofExecutionData.nameis the name of a register you want the data from. It should correspond to a call toread_from(or the default of"ro") and a QuilDECLAREinstruction.
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
number_of_shotsis the outer dimension of the 2D array of data. This should always be equal to the parameter provided towrap_in_shots(or 1 if not called).shot_lengthis the inner dimension of the data array, corresponding to the dimension of the declared memory. For example, declaringBITin Quil will result in ashot_lengthof 1, but declaringBIT[2]in Quil will result in ashot_lengthof 2.datais aDataTypewhich contains the actual results as measured from the requested register. This is a 2D array with outer dimension ofnumber_of_shotsand inner dimension ofshot_length. The type of this data depends on the type of the declared memory. Thetagfield tells you which type of data is contained within, thenbyteorrealis 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);