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 ExecutionResult
s 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
: TheExecutable
to set the parameter on.name
: The name of the memory region to read out of. Must match a QuilDECLARE
statement exactly.
Safety
executable
must be the result ofexecutable_from_quil
name
must be a valid, non-NULL, nul-terminated string. It must also live untilexecutable
is 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
: TheExecutable
to 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
executable
must be the result ofexecutable_from_quil
name
must be a valid, non-NULL, nul-terminated string. It must also live untilexecutable
is 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 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
: TheExecutable
to set the parameter on.shots
: The number of times to run the executable for each execution.
Safety
executable
must 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_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
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_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
- 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. - 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
executable
: TheExecutable
to 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_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
execution_duration_microseconds
is how long the program took to execute on the QPU. This is always 0 for QVM executions.handle
is theResultHandle
that can be passed toget_data
to getRegisterData
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
handle
is thehandle
attribute ofExecutionData
.name
is 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 QuilDECLARE
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
number_of_shots
is 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_length
is the inner dimension of the data array, corresponding to the dimension of the declared memory. For example, declaringBIT
in Quil will result in ashot_length
of 1, but declaringBIT[2]
in Quil will result in ashot_length
of 2.data
is aDataType
which contains the actual results as measured from the requested register. This is a 2D array with outer dimension ofnumber_of_shots
and inner dimension ofshot_length
. The type of this data depends on the type of the declared memory. Thetag
field tells you which type of data is contained within, thenbyte
orreal
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);