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);
}