# Maestro Tutorial

This tutorial demonstrates how to use the Maestro library to simulate quantum circuits in C++.

## Introduction

Maestro provides a unified C interface to various quantum simulation backends. You can define circuits using QASM or a JSON format and execute them on the most appropriate simulator.

## Basic Usage

The core workflow involves:

1. Initializing the Maestro library.
2. Creating a simulator instance.
3. Defining a circuit (QASM string).
4. Executing the circuit.
5. Parsing the results.
6. Cleaning up.

### Step-by-Step Example

Below is a complete C++ example. You can also find this in `examples/basic_simulation.cpp`.

```cpp
#include <iostream>
#include <string>
#include <vector>
#include "maestrolib/Interface.h"

// Helper to parse the JSON result (using a simple string search for demonstration)
// In a real app, use a JSON library like nlohmann/json or boost::json
void PrintResults(const char* jsonResult) {
    if (!jsonResult) {
        std::cout << "No results returned." << std::endl;
        return;
    }
    std::cout << "Simulation Results: " << jsonResult << std::endl;
}

int main() {
    // 1. Initialize Maestro
    // Get the singleton instance of the Maestro engine
    void* maestro = GetMaestroObject();
    if (!maestro) {
        std::cerr << "Failed to initialize Maestro." << std::endl;
        return 1;
    }

    // 2. Create a Simulator
    // Create a simple simulator for a small number of qubits.
    // This returns a handle (ID) to the simulator.
    unsigned long int simHandle = CreateSimpleSimulator(2);
    if (simHandle == 0) {
        std::cerr << "Failed to create simulator." << std::endl;
        return 1;
    }

    std::cout << "Simulator created with handle: " << simHandle << std::endl;

    // 3. Define a Circuit
    // We'll use a simple Bell State circuit in OpenQASM 2.0 format.
    const char* qasmCircuit =
        "OPENQASM 2.0;\n"
        "include \"qelib1.inc\";\n"
        "qreg q[2];\n"
        "creg c[2];\n"
        "h q[0];\n"
        "cx q[0], q[1];\n"
        "measure q -> c;\n";

    // 4. Configure Execution
    // Configuration is passed as a JSON string.
    // Here we request 1024 shots.
    const char* config = "{\"shots\": 1024}";

    // 5. Execute the Circuit
    // SimpleExecute takes the simulator handle, circuit string, and config.
    // It returns a JSON string with the results.
    char* result = SimpleExecute(simHandle, qasmCircuit, config);

    // 6. Process Results
    PrintResults(result);

    // 7. Cleanup
    // Free the result string memory
    FreeResult(result);

    // Destroy the simulator instance
    DestroySimpleSimulator(simHandle);

    return 0;
}
```

## Compiling the Example

To compile this example, you need to link against the `maestro` library.

Assuming you have built Maestro in the `build` directory:

```bash
g++ -std=c++17 -o maestro_example example.cpp \
    -I. \
    -L./build/lib -lmaestro \
    -Wl,-rpath,./build/lib
```

*Note: You may need to adjust include paths (`-I`) depending on where `maestrolib/Interface.h` is located relative to your source file.*

## Advanced Usage

### Manual Simulator Control

Instead of `SimpleExecute`, you can manually control the simulator state. See `examples/advanced_simulation.cpp` for a complete runnable example.

```cpp
// Create a specific simulator type (e.g., Statevector)
// See Simulators::SimulatorType enum for values (mapped to int)
// 0: Statevector, 1: MPS, etc. (Check source for exact mapping)
unsigned long int simHandle = CreateSimulator(0, 0);
void* sim = GetSimulator(simHandle);

// Apply gates directly
ApplyH(sim, 0);           // H on qubit 0
ApplyCX(sim, 0, 1);       // CX 0 -> 1

// Measure
unsigned long long int outcomes = MeasureNoCollapse(sim);
std::cout << "Measurement outcome: " << outcomes << std::endl;

// Clean up
DestroySimulator(simHandle);
```

### Configuration Options

The `jsonConfig` string in `SimpleExecute` supports various keys:

- `shots`: Number of execution shots (integer).
- `matrix_product_state_max_bond_dimension`: Max bond dimension for MPS (string/int).
- `matrix_product_state_truncation_threshold`: Truncation threshold for MPS (string/double).
- `mps_sample_measure_algorithm`: Algorithm for MPS sampling (string).

```json
{
  "shots": 1000,
  "matrix_product_state_max_bond_dimension": "100"
}
```
### Expectation Values

Maestro allows calculating the expectation values of observables without needing to perform manual sampling. This is particularly useful for Variational Quantum Algorithms.

```cpp
const char* observables = "ZZ;XX;YY";
char* result = SimpleEstimate(simHandle, qasmCircuit, observables, config);
// Result contains "expectation_values" array
```

### GPU and QuEST Backends (C++)

Maestro supports **GPU** and **QuEST** as dynamically-loaded simulation backends. On Linux, `GetMaestroObject()` automatically attempts to load the GPU library. You can also initialise these backends explicitly via the `SimulatorsFactory`.

> **Note:** The GPU backend is **not included** in the open-source version of Maestro. Contact [Qoro Quantum](https://qoroquantum.de) for access.

#### Initialisation

```cpp
#include "Simulators/Factory.h"

// GPU (Linux only) — automatically called by GetMaestroObject()
#ifdef __linux__
bool gpuReady = Simulators::SimulatorsFactory::InitGpuLibrary();
#endif

// QuEST (all platforms)
bool questReady = Simulators::SimulatorsFactory::InitQuestLibrary();
```

#### Using GPU via the C Interface

The `SimulatorType` and `SimulationType` enums are mapped to `int` values in the C interface. Use `RemoveAllOptimizationSimulatorsAndAdd` to switch the backend of an existing `SimpleSimulator` handle:

```cpp
#include "maestrolib/Interface.h"
#include "Simulators/State.h"  // for enum definitions

void* maestro = GetMaestroObject();
unsigned long int simHandle = CreateSimpleSimulator(2);

// Switch to GPU + Statevector
RemoveAllOptimizationSimulatorsAndAdd(
    simHandle,
    static_cast<int>(Simulators::SimulatorType::kGpuSim),
    static_cast<int>(Simulators::SimulationType::kStatevector)
);

const char* qasm =
    "OPENQASM 2.0;\n"
    "include \"qelib1.inc\";\n"
    "qreg q[2];\n"
    "creg c[2];\n"
    "h q[0];\n"
    "cx q[0], q[1];\n"
    "measure q -> c;\n";

char* result = SimpleExecute(simHandle, qasm, "{\"shots\": 1024}");
PrintResults(result);

FreeResult(result);
DestroySimpleSimulator(simHandle);
```

#### Using QuEST via the C Interface

```cpp
#include "maestrolib/Interface.h"
#include "Simulators/Factory.h"
#include "Simulators/State.h"

void* maestro = GetMaestroObject();

// Initialise QuEST (required before first use)
Simulators::SimulatorsFactory::InitQuestLibrary();

unsigned long int simHandle = CreateSimpleSimulator(2);

// Switch to QuEST + Statevector (QuEST only supports Statevector)
RemoveAllOptimizationSimulatorsAndAdd(
    simHandle,
    static_cast<int>(Simulators::SimulatorType::kQuestSim),
    static_cast<int>(Simulators::SimulationType::kStatevector)
);

const char* qasm =
    "OPENQASM 2.0;\n"
    "include \"qelib1.inc\";\n"
    "qreg q[2];\n"
    "creg c[2];\n"
    "h q[0];\n"
    "cx q[0], q[1];\n"
    "measure q -> c;\n";

char* result = SimpleExecute(simHandle, qasm, "{\"shots\": 1024}");
PrintResults(result);

FreeResult(result);
DestroySimpleSimulator(simHandle);
```

#### Supported Simulation Types

| SimulationType | GPU | QuEST |
|----------------|-----|-------|
| `kStatevector` | ✅ | ✅ |
| `kMatrixProductState` | ✅ | ❌ |
| `kTensorNetwork` | ✅ | ❌ |
| `kPauliPropagator` | ✅ | ❌ |
| `kStabilizer` | ❌ | ❌ |
| `kExtendedStabilizer` | ❌ | ❌ |

## Python

Maestro provides Python bindings for ease of use, allowing you to integrate its high-performance simulation capabilities into your Python-based quantum workflows.

### Installation

Install directly from PyPI (pre-built wheels for Linux and macOS):

```bash
pip install qoro-maestro
```

Or build from source from the root of the Maestro repository:

```bash
pip install .
```

This will compile the C++ core and install the `maestro` Python package.

### Convenience API

The easiest way to use Maestro in Python is through the `simple_execute` and `simple_estimate` functions.

```python
import maestro

qasm_circuit = """
OPENQASM 2.0;
include "qelib1.inc";
qreg q[2];
h q[0];
cx q[0], q[1];
"""

# 1. Execute and get counts
# You can specify simulator_type and simulation_type if needed
result = maestro.simple_execute(qasm_circuit, shots=1024)
print(f"Simulator: {result['simulator']}")  # int (SimulatorType enum value)
print(f"Method: {result['method']}")          # int (SimulationType enum value)
print(f"Counts: {result['counts']}")

# 2. Estimate expectation values
observables = "ZZ;XX;YY"
estimate = maestro.simple_estimate(qasm_circuit, observables)
print(f"Expectation Values: {estimate['expectation_values']}")
```

### QuantumCircuit Model

The `QuantumCircuit` class provides a Pythonic, object-oriented API for building
and executing quantum circuits directly in Python — no QASM strings required.
This is the **recommended** approach for most Python workflows.

#### Creating a Circuit

```python
import maestro

# Access the QuantumCircuit class from the circuits submodule
QuantumCircuit = maestro.circuits.QuantumCircuit

qc = QuantumCircuit()
```

#### Adding Gates

The `QuantumCircuit` supports a comprehensive gate set. Qubits are referenced
by integer index (0-based) and are automatically allocated as needed.

**Single-Qubit Gates (Non-Parametric)**

| Method       | Gate             | Description                  |
|--------------|------------------|------------------------------|
| `qc.x(q)`    | Pauli-X          | Bit flip                     |
| `qc.y(q)`    | Pauli-Y          | Bit + phase flip             |
| `qc.z(q)`    | Pauli-Z          | Phase flip                   |
| `qc.h(q)`    | Hadamard         | Creates superposition        |
| `qc.s(q)`    | S Gate           | π/2 phase                    |
| `qc.sdg(q)`  | S† Gate          | −π/2 phase                   |
| `qc.t(q)`    | T Gate           | π/4 phase                    |
| `qc.tdg(q)`  | T† Gate          | −π/4 phase                   |
| `qc.sx(q)`   | √X Gate          | Square root of X             |
| `qc.sxdg(q)` | √X† Gate        | Adjoint of √X                |
| `qc.k(q)`    | K Gate           | K gate                       |

**Single-Qubit Gates (Parametric)**

| Method                       | Gate    | Parameters          |
|------------------------------|---------|---------------------|
| `qc.p(q, λ)`                | Phase   | `λ` (lambda)        |
| `qc.rx(q, θ)`               | Rx      | `θ` (theta)         |
| `qc.ry(q, θ)`               | Ry      | `θ` (theta)         |
| `qc.rz(q, θ)`               | Rz      | `θ` (theta)         |
| `qc.u(q, θ, φ, λ)`         | U       | `θ`, `φ`, `λ`       |

**Two-Qubit Gates**

| Method              | Gate    | Description                     |
|---------------------|---------|----------------------------------|
| `qc.cx(c, t)`      | CNOT    | Controlled-X (control, target)   |
| `qc.cy(c, t)`      | CY      | Controlled-Y                     |
| `qc.cz(c, t)`      | CZ      | Controlled-Z                     |
| `qc.ch(c, t)`      | CH      | Controlled-Hadamard              |
| `qc.csx(c, t)`     | CSX     | Controlled-√X                    |
| `qc.csxdg(c, t)`   | CSX†    | Controlled-√X†                   |
| `qc.swap(a, b)`    | SWAP    | Swaps two qubits                 |

**Controlled Parametric Gates**

| Method                          | Gate | Parameters                    |
|---------------------------------|------|-------------------------------|
| `qc.cp(c, t, λ)`               | CP   | `λ` (lambda)                  |
| `qc.crx(c, t, θ)`              | CRx  | `θ` (theta)                   |
| `qc.cry(c, t, θ)`              | CRy  | `θ` (theta)                   |
| `qc.crz(c, t, θ)`              | CRz  | `θ` (theta)                   |
| `qc.cu(c, t, θ, φ, λ, γ)`     | CU   | `θ`, `φ`, `λ`, `γ`            |

**Three-Qubit Gates**

| Method                    | Gate     | Description                              |
|---------------------------|----------|------------------------------------------|
| `qc.ccx(c1, c2, t)`      | Toffoli  | Controlled-Controlled-X (CCX)            |
| `qc.cswap(c, a, b)`      | Fredkin  | Controlled-SWAP                          |

#### Measurements

Add measurements by providing a list of `(qubit_index, classical_bit_index)` pairs:

```python
qc = QuantumCircuit()
qc.h(0)
qc.cx(0, 1)

# Measure qubit 0 → classical bit 0, qubit 1 → classical bit 1
qc.measure([(0, 0), (1, 1)])
```

You can also remap the classical bit assignments. For example, to store the
result of qubit 0 in classical bit 1 and vice versa:

```python
qc.measure([(0, 1), (1, 0)])
```

#### Executing a Circuit

The `execute` method runs the circuit for a given number of shots and returns
a dictionary with measurement counts:

```python
qc = QuantumCircuit()
qc.h(0)
qc.cx(0, 1)
qc.measure([(0, 0), (1, 1)])

result = qc.execute(shots=1024)

print(result["counts"])        # e.g. {"00": 512, "11": 512}
print(result["time_taken"])    # Execution time in seconds
print(result["simulator"])     # Simulator type used (int)
print(result["method"])        # Simulation method used (int)
```

**Execution Parameters:**

| Parameter                    | Type   | Default                              | Description                            |
|------------------------------|--------|--------------------------------------|----------------------------------------|
| `simulator_type`             | enum   | `SimulatorType.QCSim`                | Simulator backend                      |
| `simulation_type`            | enum   | `SimulationType.Statevector`         | Simulation method                      |
| `shots`                      | int    | `1024`                               | Number of measurement shots            |
| `max_bond_dimension`         | int    | `2`                                  | Max bond dimension (MPS only)          |
| `singular_value_threshold`   | float  | `1e-8`                               | Truncation threshold (MPS only)        |

**Example with MPS backend:**

```python
result = qc.execute(
    simulator_type=maestro.SimulatorType.QCSim,
    simulation_type=maestro.SimulationType.MatrixProductState,
    max_bond_dimension=64,
    singular_value_threshold=1e-6,
    shots=2048
)
```

#### Estimating Expectation Values

The `estimate` method computes expectation values of Pauli observables without
sampling. This is ideal for variational algorithms and Hamiltonian simulation.

Observables can be specified as either a semicolon-separated string or a list of
strings. Each observable is a Pauli string (e.g., `"ZZ"`, `"XII"`, `"IYZ"`).

```python
qc = QuantumCircuit()
qc.h(0)
qc.cx(0, 1)
# Bell state: (|00⟩ + |11⟩) / √2

# Using a list of Pauli strings
result = qc.estimate(observables=["XX", "ZZ", "IZ"])
print(result["expectation_values"])  # [1.0, 1.0, 0.0]

# Using a semicolon-separated string
result = qc.estimate(observables="XX;ZZ;IZ")
print(result["expectation_values"])  # [1.0, 1.0, 0.0]
```

**Estimation Parameters:**

| Parameter                    | Type          | Default                              | Description                            |
|------------------------------|---------------|--------------------------------------|----------------------------------------|
| `observables`                | str or list   | *(required)*                         | Pauli observables to measure           |
| `simulator_type`             | enum          | `SimulatorType.QCSim`                | Simulator backend                      |
| `simulation_type`            | enum          | `SimulationType.Statevector`         | Simulation method                      |
| `max_bond_dimension`         | int           | `2`                                  | Max bond dimension (MPS only)          |
| `singular_value_threshold`   | float         | `1e-8`                               | Truncation threshold (MPS only)        |

> **Note:** The `estimate` method does not require measurements to be added to
> the circuit. The number of qubits is automatically inferred from the circuit
> operations and observable lengths.

#### Full Example: Bell State

```python
import maestro

QuantumCircuit = maestro.circuits.QuantumCircuit

# Build a Bell state circuit
qc = QuantumCircuit()
qc.h(0)
qc.cx(0, 1)

# --- Sampling ---
qc.measure([(0, 0), (1, 1)])
result = qc.execute(shots=1000)
print("Counts:", result["counts"])
# Expected: ~{"00": 500, "11": 500}

# --- Expectation Values ---
qc_est = QuantumCircuit()
qc_est.h(0)
qc_est.cx(0, 1)

est = qc_est.estimate(observables=["XX", "ZZ", "ZI", "IZ"])
print("⟨XX⟩ =", est["expectation_values"][0])  # 1.0
print("⟨ZZ⟩ =", est["expectation_values"][1])  # 1.0
print("⟨ZI⟩ =", est["expectation_values"][2])  # 0.0
print("⟨IZ⟩ =", est["expectation_values"][3])  # 0.0
```

### Manual Control API

For more granular control over the simulation lifecycle, you can use the `Maestro` class directly.

```python
from maestro import Maestro, SimulatorType, SimulationType

# Initialize Maestro
m = Maestro()

# Create a simulator handle
# Defaults to QCSim and MatrixProductState
sim_handle = m.create_simulator(SimulatorType.QCSim, SimulationType.Statevector)

# Get the raw simulator object
sim = m.get_simulator(sim_handle)

# Cleanup
m.destroy_simulator(sim_handle)
```

> **Note:** The `Maestro` class provides handle-based lifecycle management.
> For most Python workflows, the `QuantumCircuit` API or the `simple_execute` /
> `simple_estimate` convenience functions are the recommended approach.

### Probability Access

Get the full probability distribution after executing a circuit (without sampling):

```python
import maestro

QuantumCircuit = maestro.circuits.QuantumCircuit

qc = QuantumCircuit()
qc.h(0)
qc.cx(0, 1)

probs = maestro.get_probabilities(qc)
print(probs)  # [0.5, 0.0, 0.0, 0.5] for a Bell state
```

### Examples

You can find several complete examples in the `examples/` directory:

- `examples/python_example_1.py`: Basic simulation and sampling.
- `examples/python_example_2.py`: Advanced simulation with manual gate application.
- `examples/python_example_3.py`: Working with expectation values and observables.

---

## QuEST and GPU Execution (Dynamic Backends)

Maestro supports **QuEST** and **GPU** simulation backends as **optional, dynamically-loaded libraries**. Unlike the built-in CPU backends (QCSim, MPS, etc.) which are always available, these backends are packaged as separate shared libraries (`libcomposer_quest.so`, `libcomposer_gpu_simulators.so`) and loaded at runtime only when explicitly requested.

This design means:

- The core `maestro` package works without QuEST or GPU support installed.
- You must **initialize** a dynamic backend before using it.
- If the shared library is not found on the system, initialization will gracefully return `False` rather than crashing.

### Initializing Dynamic Backends

Before using QuEST or GPU simulators, you **must** call the corresponding initialization function. This loads the shared library into memory and registers the backend with Maestro's simulator factory.

```python
import maestro

# --- QuEST Backend ---
quest_ok = maestro.init_quest()
print(f"QuEST initialized: {quest_ok}")  # True if library found and loaded

# --- GPU Backend ---
gpu_ok = maestro.init_gpu()
print(f"GPU initialized: {gpu_ok}")  # True if library found and loaded
```

> **Note:** Initialization only needs to be done **once** per process. Subsequent calls are safe but redundant.

### Checking Availability

You can check whether a dynamic backend has been successfully loaded at any point:

```python
import maestro

print(f"QuEST available: {maestro.is_quest_available()}")
print(f"GPU available:   {maestro.is_gpu_available()}")
```

These return `False` if `init_quest()` or `init_gpu()` has not been called, or if the initialization failed (e.g., shared library not found).

### Running Circuits with QuEST

QuEST provides an alternative statevector simulation engine. Once initialized, you can select it via `SimulatorType.QuestSim`. QuEST also natively supports **MPI-distributed statevector** simulation, enabling execution across multiple nodes for larger qubit counts.

> **Limitation:** QuEST only supports the **Statevector** simulation type. Attempting to use it with MPS, Stabilizer, or other simulation types will raise an error.

#### Using the QuantumCircuit API

```python
import maestro

# Initialize QuEST (required before first use)
if not maestro.init_quest():
    raise RuntimeError("QuEST library not available")

QuantumCircuit = maestro.circuits.QuantumCircuit

# Build a circuit
qc = QuantumCircuit()
qc.h(0)
qc.cx(0, 1)
qc.measure([(0, 0), (1, 1)])

# Execute on QuEST
result = qc.execute(
    simulator_type=maestro.SimulatorType.QuestSim,
    simulation_type=maestro.SimulationType.Statevector,
    shots=1024
)

print(f"Counts: {result['counts']}")
print(f"Time:   {result['time_taken']:.4f}s")
```

#### Using the Convenience API

```python
import maestro

maestro.init_quest()

qasm_circuit = """
OPENQASM 2.0;
include "qelib1.inc";
qreg q[2];
h q[0];
cx q[0], q[1];
"""

# Execute
result = maestro.simple_execute(
    qasm_circuit,
    simulator_type=maestro.SimulatorType.QuestSim,
    shots=1024
)
print(f"Counts: {result['counts']}")

# Estimate expectation values
estimate = maestro.simple_estimate(
    qasm_circuit,
    observables="ZZ;XX",
    simulator_type=maestro.SimulatorType.QuestSim
)
print(f"Expectation values: {estimate['expectation_values']}")
```

### Running Circuits on GPU

The GPU backend provides CUDA-accelerated simulation. Once initialized, select it via `SimulatorType.Gpu`.

> **Note:** The GPU backend is **not included** in the open-source version of Maestro. Contact [Qoro Quantum](https://qoroquantum.de) for access.

The GPU backend supports multiple simulation types:

| Simulation Type     | GPU Support |
|---------------------|-------------|
| Statevector         | ✅          |
| MatrixProductState  | ✅          |
| TensorNetwork       | ✅          |
| PauliPropagator     | ✅          |
| Stabilizer          | ❌          |
| ExtendedStabilizer  | ❌          |

```python
import maestro

# Initialize GPU (required before first use)
if not maestro.init_gpu():
    raise RuntimeError("GPU library not available — is CUDA installed?")

QuantumCircuit = maestro.circuits.QuantumCircuit

qc = QuantumCircuit()
qc.h(0)
qc.cx(0, 1)
qc.measure([(0, 0), (1, 1)])

# Execute on GPU with statevector
result = qc.execute(
    simulator_type=maestro.SimulatorType.Gpu,
    simulation_type=maestro.SimulationType.Statevector,
    shots=2048
)
print(f"GPU Counts: {result['counts']}")

# Execute on GPU with MPS
result_mps = qc.execute(
    simulator_type=maestro.SimulatorType.Gpu,
    simulation_type=maestro.SimulationType.MatrixProductState,
    max_bond_dimension=64,
    shots=2048
)
print(f"GPU MPS Counts: {result_mps['counts']}")
```

### Defensive Initialization Pattern

For scripts that should work regardless of backend availability, use a guard pattern:

```python
import maestro

QuantumCircuit = maestro.circuits.QuantumCircuit

qc = QuantumCircuit()
qc.h(0)
qc.cx(0, 1)
qc.measure([(0, 0), (1, 1)])

# Try QuEST, fall back to default CPU
if maestro.init_quest():
    result = qc.execute(simulator_type=maestro.SimulatorType.QuestSim)
    print("Ran on QuEST")
elif maestro.init_gpu():
    result = qc.execute(simulator_type=maestro.SimulatorType.Gpu)
    print("Ran on GPU")
else:
    result = qc.execute()  # Default: QCSim Statevector
    print("Ran on CPU (QCSim)")

print(f"Counts: {result['counts']}")
```

### API Reference

| Function                    | Returns | Description                                                  |
|-----------------------------|---------|--------------------------------------------------------------|
| `maestro.init_quest()`      | `bool`  | Load the QuEST shared library. Returns `True` on success.    |
| `maestro.is_quest_available()` | `bool` | Check if QuEST has been loaded and is ready to use.         |
| `maestro.init_gpu()`        | `bool`  | Load the GPU shared library. Returns `True` on success.      |
| `maestro.is_gpu_available()`| `bool`  | Check if the GPU backend has been loaded and is ready.        |

| Simulator Type               | Enum Value                        | Dynamic? | Backend Library                   |
|-------------------------------|-----------------------------------|----------|-----------------------------------|
| QCSim (default CPU)           | `SimulatorType.QCSim`             | No       | Built-in                          |
| Composite QCSim               | `SimulatorType.CompositeQCSim`    | No       | Built-in                          |
| QuEST                         | `SimulatorType.QuestSim`          | **Yes**  | `libcomposer_quest.so`            |
| GPU                           | `SimulatorType.Gpu`               | **Yes**  | `libcomposer_gpu_simulators.so`   |

### Building with Dynamic Backend Support

The QuEST and GPU shared libraries are **not** part of the default `pip install qoro-maestro` package. To use them:

- **QuEST:** Build the QuEST integration library and ensure `libcomposer_quest.so` (or `.dylib` on macOS) is on your library path.
- **GPU:** The GPU backend is not included in the open-source release. Contact [Qoro Quantum](https://qoroquantum.de) for access to the GPU libraries.

See `INSTALL.md` for detailed build instructions.
