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