Skip to the content.

Getting Started with FSharp.Azure.Quantum

Welcome to FSharp.Azure.Quantum - an F# library for quantum optimization using quantum algorithms (QAOA, VQE, QFT) with automatic backend selection (local simulation or Azure Quantum cloud). This guide will help you get up and running in 5 minutes.

Installation

NuGet Package: https://www.nuget.org/packages/FSharp.Azure.Quantum

Via NuGet Package Manager

dotnet add package FSharp.Azure.Quantum

Via Package Manager Console

Install-Package FSharp.Azure.Quantum

Via .fsproj File

<ItemGroup>
  <PackageReference Include="FSharp.Azure.Quantum" />
</ItemGroup>

Note: Omitting the version will install the latest stable version. To specify a particular version, add --version X.Y.Z (CLI) or Version="X.Y.Z" (XML).

Prerequisites

Quick Start: Your First Quantum Optimization

Let’s solve a simple Graph Coloring Problem using the quantum-first API:

open System.Threading
open FSharp.Azure.Quantum
open FSharp.Azure.Quantum.GraphColoring

// Define a graph coloring problem (register allocation)
let problem = graphColoring {
    node "R1" ["R2"; "R3"]
    node "R2" ["R1"; "R4"]
    node "R3" ["R1"; "R4"]
    node "R4" ["R2"; "R3"]
    colors ["EAX"; "EBX"; "ECX"; "EDX"]   // required: the builder rejects a problem without colors
}

// Solve using QAOA quantum algorithm (LocalBackend simulation)
task {
    match! GraphColoring.solveAsync problem 4 None CancellationToken.None with  // None = LocalBackend (default)
    | Ok solution ->
        printfn "Colors used: %d" solution.ColorsUsed
        solution.Assignments 
        |> Map.iter (fun node color -> printfn "%s → %s" node color)
    | Error err -> 
        printfn "Error: %s" err.Message
}

Samples are task blocks: await them in an application, or end a script with |> Async.AwaitTask |> Async.RunSynchronously.

Output (QAOA is probabilistic, so the exact assignment can differ between runs):

Colors used: 2
R1 → EAX
R2 → EBX
R3 → EBX
R4 → EAX

What happens:

  1. Computation expression builds graph coloring problem
  2. GraphColoring.solveAsync encodes problem as QUBO
  3. QAOA quantum algorithm builds optimization circuit
  4. LocalBackend simulates quantum circuit (memory-derived width, free); this problem needs 4 nodes × 4 colors = 16 qubits
  5. Returns color assignments with validation

Key Concepts

Use high-level builders to solve problems with quantum algorithms directly:

// MaxCut problem (graph partitioning)
let vertices = ["A"; "B"; "C"; "D"]
let edges = [
    ("A", "B", 1.0)
    ("B", "C", 2.0)
    ("C", "D", 1.0)
    ("D", "A", 1.0)
]

let maxCutProblem = MaxCut.createProblem vertices edges

// Solve with QAOA on LocalBackend (simulation)
task {
    match! MaxCut.solveAsync maxCutProblem None CancellationToken.None with
    | Ok solution ->
        printfn "Cut Value: %.2f" solution.CutValue
        printfn "Partition S: %A" solution.PartitionS
        printfn "Partition T: %A" solution.PartitionT
    | Error err -> printfn "Error: %s" err.Message
}

2. HybridSolver - Optional Routing for Variable-Sized Problems

The HybridSolver solves small problems with classical heuristics and routes large ones to quantum when you supply a backend:

open FSharp.Azure.Quantum.Classical

// TSP with 5 cities
let distances = array2D [
    [0.0; 2.0; 9.0; 10.0; 7.0]
    [1.0; 0.0; 6.0;  4.0; 3.0]
    [15.0; 7.0; 0.0;  8.0; 3.0]
    [6.0; 3.0; 12.0;  0.0; 11.0]
    [10.0; 4.0; 8.0;  5.0; 0.0]
]

// 5 cities is below the advisor's thresholds, so this runs the classical solver
task {
    match! HybridSolver.solveTspAsync distances None None None CancellationToken.None with
    | Ok solution ->
        printfn "Method: %A" solution.Method          // Classical or Quantum
        printfn "Tour: %A" solution.Result.Tour
        printfn "Length: %.2f" solution.Result.TourLength
        printfn "Reasoning: %s" solution.Reasoning    // Explains routing decision
    | Error err -> printfn "Error: %s" err.Message
}

How HybridSolver Decides: Decision Flow

┌─────────────────────────────────────┐
│    HybridSolver.solveTspAsync()     │
└──────────────┬──────────────────────┘
               │
               ▼
       ┌───────────────┐
       │ forceMethod?  │
       └───┬───────┬───┘
           │       │
      Yes  │       │ None (auto)
           │       │
           ▼       ▼
    ┌──────────┐  ┌──────────────────┐
    │ Classical│  │ QuantumAdvisor   │
    │    or    │  │  - Size          │
    │ Quantum  │  │  - Estimated     │
    │ (forced) │  │    speedup       │
    └──────────┘  └────────┬─────────┘
                           │
                  ┌────────┴────────┐
                  ▼                 ▼
             Size < 50          Size ≥ 50
              │                     │
              ▼                     ▼
        ┌──────────┐     ┌─────────────────────┐
        │Classical │     │ Backend supplied and │
        │          │     │ cost within budget?  │
        └──────────┘     └──────┬─────────┬─────┘
                            Yes │         │ No
                                ▼         ▼
                         ┌──────────┐ ┌──────────┐
                         │ Quantum  │ │Classical │
                         │(QAOA on  │ │          │
                         │backend)  │ │          │
                         └──────────┘ └──────────┘

Key Decision Factors (with QuantumAdvisor.defaultThresholds):

solution.Reasoning always says which branch was taken.

3. Problem Builders - Type-Safe Problem Specification

Each problem type has a builder: a computation expression (graphColoring { ... }, scheduledTask { ... }) or createProblem helpers that take plain tuples:

open System.Threading
// Knapsack problem (resource allocation)
let items = [
    ("Laptop", 2.0, 1000.0)   // (id, weight, value)
    ("Tablet", 1.0, 500.0)
    ("Phone", 0.5, 300.0)
]
let knapsackProblem = Knapsack.createProblem items 3.0  // capacity = 3.0

// Solve with QAOA quantum algorithm
task {
    match! Knapsack.solveAsync knapsackProblem None CancellationToken.None with
    | Ok solution ->
        printfn "Total Value: %.2f" solution.TotalValue
        printfn "Total Weight: %.2f" solution.TotalWeight
        solution.SelectedItems |> List.iter (fun item -> printfn "  - %s" item.Id)
    | Error err -> printfn "Error: %s" err.Message
}

When to Use Direct Quantum API vs HybridSolver:

Scenario Recommendation Reason
Learning quantum algorithms Direct API Every run goes through a quantum backend
Fixed problem size that fits the backend Direct API Simple, predictable behavior
Variable problem size HybridSolver Small problems are solved classically
Prototyping Direct API LocalBackend is fast enough at small widths
Problems too large for any backend HybridSolver (classical path) No qubit limit on the classical heuristics

Common Pitfalls & How to Avoid Them

❌ Pitfall 1: Non-Square Distance Matrix

// ❌ WRONG: 3 cities but 2x3 matrix
let wrong = array2D [
    [0.0; 1.0; 2.0]
    [1.0; 0.0; 3.0]
]
// HybridSolver.solveTspAsync returns
// Error (ValidationError "Distance matrix must be square (got 2x3 dimensions)")

✅ Fix: Ensure rows = columns = number of cities

// ✅ CORRECT: 3 cities, 3x3 matrix
let correct = array2D [
    [0.0; 1.0; 2.0]
    [1.0; 0.0; 3.0]
    [2.0; 3.0; 0.0]
]

❌ Pitfall 2: Asymmetric Distance Matrix

// ❌ WRONG: Distance from A→B ≠ B→A
let asymmetric = array2D [
    [0.0; 10.0]
    [5.0; 0.0]   // 10 ≠ 5
]
// No error or warning: the classical TSP solver (2-opt) assumes symmetric
// distances, so its tours on this matrix are not meaningful.
// QuantumTspSolver charges distances.[from, to] for every step, the return
// to the first city included, and so accepts one-way distances.

✅ Fix: Make matrix symmetric

// ✅ CORRECT: Distance A↔B is same both ways
let symmetric = array2D [
    [0.0; 10.0]
    [10.0; 0.0]
]

❌ Pitfall 3: Ignoring Result Type

// ❌ WRONG: Not handling errors (would cause compiler error)
// let! solution = HybridSolver.solveTspAsync distances None None None CancellationToken.None
// printfn "%A" solution.Result  // Compiler error! 'solution' is Result<T,E>

✅ Fix: Always pattern match on Result

open System.Threading
// ✅ CORRECT: Proper error handling
task {
    match! HybridSolver.solveTspAsync distances None None None CancellationToken.None with
    | Ok solution -> 
        printfn "Success: %A" solution.Result
    | Error err -> 
        eprintfn "Failed: %s" err.Message
}

❌ Pitfall 4: Budget Constraints Too Tight

// ❌ WRONG: Budget smaller than minimum asset price
let assets = [("AAPL", 0.12, 0.18, 150.0)]
let constraints: PortfolioSolver.Constraints = { Budget = 100.0; MinHolding = 0.0; MaxHolding = 1000.0 }
// Error: "Budget (100) is insufficient to purchase any asset (minimum price: 150)"

✅ Fix: Ensure budget ≥ cheapest asset price

// ✅ CORRECT: Budget can buy at least one share
let constraintsFixed: PortfolioSolver.Constraints = { Budget = 500.0; MinHolding = 0.0; MaxHolding = 500.0 }

❌ Pitfall 5: Treating Errors as Strings

Solvers return QuantumResult<'T>, which is Result<'T, QuantumError>. The error is a union (ValidationError, OperationError, BackendError, …), not a string.

// ❌ WRONG: 'err' is a QuantumError, so %s and err.Contains do not compile
// | Error err -> printfn "Error: %s" err

✅ Fix: Use err.Message for text, or match on the case

open FSharp.Azure.Quantum.Core

task {
    match! HybridSolver.solveTspAsync wrong None None None CancellationToken.None with
    | Ok _ -> ()
    | Error (QuantumError.ValidationError (field, reason)) -> eprintfn "Invalid %s: %s" field reason
    | Error err -> eprintfn "Failed (%s): %s" err.Category err.Message
}

Complete Error Handling Examples

Robust TSP Solving with Recovery

open FSharp.Azure.Quantum.Core

let solveTspRobust (distances: float[,]) (cancellationToken: CancellationToken) =
    task {
        // Validate input
        let n = distances.GetLength(0)
        if n <> distances.GetLength(1) then
            return Error (QuantumError.ValidationError ("distances", "Distance matrix must be square"))
        elif n < 2 then
            return Error (QuantumError.ValidationError ("distances", "Need at least 2 cities"))
        else
            // Try solving with automatic routing
            match! HybridSolver.solveTspAsync distances None None None cancellationToken with
            | Ok solution ->
                printfn "✓ Success using %A solver" solution.Method
                printfn "  Tour: %A" solution.Result.Tour
                printfn "  Length: %.2f" solution.Result.TourLength
                printfn "  Time: %.2f ms" solution.ElapsedMs
                return Ok solution
                
            | Error err ->
                // Log error and return error (no classical fallback in this example)
                eprintfn "⚠ HybridSolver failed: %s" err.Message
                return Error err
    }

// Usage
let distancesRobust = array2D [[0.0; 10.0]; [10.0; 0.0]]
task {
    match! solveTspRobust distancesRobust CancellationToken.None with
    | Ok _ -> printfn "Problem solved!"
    | Error err -> eprintfn "Could not solve: %s" err.Message
}

Portfolio Optimization with Validation

open System.Threading
let solvePortfolioSafely
    (assets: (string * float * float * float) list)
    (budget: float)
    (cancellationToken: CancellationToken) =
    task {
        // Validate assets
        let invalidAssets = 
            assets 
            |> List.filter (fun (symbol, ret, risk, price) -> 
                price <= 0.0 || risk < 0.0)
        
        if not (List.isEmpty invalidAssets) then
            return Error (QuantumError.ValidationError ("assets", $"Invalid assets: %A{invalidAssets}"))
        elif budget <= 0.0 then
            return Error (QuantumError.ValidationError ("budget", $"Budget must be positive: {budget}"))
        else
            let constraints: PortfolioSolver.Constraints = {
                Budget = budget
                MinHolding = 0.0
                MaxHolding = budget * 0.5  // Max 50% in any asset
            }
            
            // Create asset records
            let assetRecords: PortfolioSolver.Asset list = 
                assets 
                |> List.map (fun (symbol, ret, risk, price) ->
                    { Symbol = symbol; ExpectedReturn = ret; Risk = risk; Price = price })
            
            // Validate budget constraint
            match PortfolioSolver.validateBudgetConstraint assetRecords constraints with
            | validation when not validation.IsValid ->
                return Error (QuantumError.ValidationError ("constraints", String.concat "; " validation.Messages))
            | _ ->
                // Solve
                match! HybridSolver.solvePortfolioAsync assetRecords constraints None None None cancellationToken with
                | Ok solution ->
                    printfn "✓ Portfolio optimized using %A" solution.Method
                    printfn "  Total Value: $%.2f" solution.Result.TotalValue
                    printfn "  Expected Return: %.2f%%" (solution.Result.ExpectedReturn * 100.0)
                    printfn "  Risk: %.2f" solution.Result.Risk
                    printfn "  Sharpe Ratio: %.2f" solution.Result.SharpeRatio
                    return Ok solution
                | Error err ->
                    return Error err
    }

// Usage with error recovery
let assets: (string * float * float * float) list = [
    ("AAPL", 0.12, 0.18, 150.0)
    ("MSFT", 0.10, 0.15, 300.0)
]

task {
    match! solvePortfolioSafely assets 10000.0 CancellationToken.None with
    | Ok solution -> 
        // Process successful result
        solution.Result.Allocations 
        |> List.iter (fun a -> printfn "  %s: $%.2f" a.Asset.Symbol a.Value)
    | Error err -> 
        // Handle failure gracefully
        eprintfn "Portfolio optimization failed: %s" err.Message
        eprintfn "Try: Increase budget or reduce constraints"
}

HybridSolver.solvePortfolioAsync treats the assets as independent (risk = sqrt(Σ (wᵢσᵢ)²)). When you have the covariance of the returns, HybridSolver.solvePortfolioWithCovarianceAsync assets covariance constraints None None None None cancellationToken validates it (square, one row per asset, symmetric, positive semidefinite, otherwise a ValidationError) and both solver paths report risk as sqrt(wᵀΣw).

Handling Budget Limits

The budget argument (USD) is a cost guard: when the advisor recommends quantum but the estimated cost exceeds the budget, HybridSolver runs the classical solver instead and says so in Reasoning. Exceeding the budget is not an error.

let solveTspWithBudget (distances: float[,]) (maxBudget: float) (cancellationToken: CancellationToken) =
    task {
        printfn "Solving with budget=$%.2f" maxBudget
        
        match! HybridSolver.solveTspAsync distances (Some maxBudget) None None cancellationToken with
        | Ok solution when solution.Method = HybridSolver.Classical ->
            // Classical was used (small problem, no backend, or over budget)
            printfn "✓ Classical solver used: %s" solution.Reasoning
            return Ok solution
            
        | Ok solution ->
            printfn "✓ Quantum solver used: %s" solution.Reasoning
            return Ok solution
            
        | Error (QuantumError.ValidationError (field, reason)) ->
            eprintfn "✗ Invalid input (%s): %s" field reason
            return Error (QuantumError.ValidationError (field, reason))
            
        | Error err ->
            eprintfn "✗ Solver error: %s" err.Message
            return Error err
    }

// Usage: $5 budget
let result = solveTspWithBudget distances 5.0 CancellationToken.None

The timeout argument of the HybridSolver functions is accepted but not currently used; there is no solver timeout.

Quantum TSP with Parameter Optimization

FSharp.Azure.Quantum provides automatic QAOA parameter optimization - a variational quantum-classical loop that finds optimal circuit parameters for your specific problem:

open System.Threading
open FSharp.Azure.Quantum.Quantum.QuantumTspSolver
open FSharp.Azure.Quantum.Backends

// Create distance matrix for 3-city TSP (3² = 9 qubits)
let distances = array2D [
    [ 0.0; 1.0; 2.0 ]
    [ 1.0; 0.0; 1.5 ]
    [ 2.0; 1.5; 0.0 ]
]

let backend = LocalBackendFactory.createUnified()

// solveAsync returns a Task<QuantumResult<_>>
let runTsp config (cancellationToken: CancellationToken) =
    solveAsync backend distances config cancellationToken

// Option 1: Use default configuration (optimization enabled)
task {
    match! runTsp defaultConfig CancellationToken.None with
    | Ok solution ->
        printfn "Best tour: %A" solution.Tour
        printfn "Tour length: %.2f" solution.TourLength
        printfn "Layer parameters (gamma, beta): %A" solution.LayerParameters // one pair per layer
        printfn "Optimization converged: %A" solution.OptimizationConverged    // bool option
        printfn "Valid tours among the shots: %A" (solution.Sampling |> Option.map (fun s -> s.Valid))
    | Error err -> printfn "Error: %s" err.Message                             // e.g. no valid tour in 1000 shots
}

// Option 2: Custom configuration for fine-tuning
let customConfig = {
    NumLayers = 2                    // QAOA layers (p)
    OptimizationShots = 100          // Samples per step when the backend has no state vector
    FinalShots = 1000                // High shots for accurate final result
    EnableOptimization = true        // Enable variational loop
    InitialParameters = (0.5, 0.5)   // (gamma, beta) of every layer when optimization is disabled
    MaxOptimizationIterations = 1000 // Cap the variational loop
}
let result = runTsp customConfig CancellationToken.None

// Option 3: No variational loop at all — one single-layer circuit at the initial parameters.
// Use this when the backend is expensive (e.g. topological), since every
// optimizer iteration is a full circuit execution.
let fastResult = runTsp fastConfig CancellationToken.None

QuantumTspSolver.solveAsync is the only entry point: a fixed shot count without optimization, or the default configuration on a LocalBackend, are both expressed through its QuantumTspConfig argument.

What the solver returns

The QUBO has one variable per (city, time slot) pair and charges every step of the closed tour, the return to the first city included, at distances.[from, to]. Its constraint penalty is twice the largest distance, so the lowest-energy bitstrings are the shortest tours at any distance scale.

A measurement is a tour only when it is a permutation matrix: one city in every time slot and one slot for every city. Other measurements are dropped; nothing is repaired or filled in. solution.Sampling gives the count of valid measurements (Valid) and of those that are the returned tour (Hits); the rotations and, for symmetric distances, the two directions of one cycle count as the same tour. The returned tour is the shortest one measured, which need not be the optimum.

When no measurement is a tour, the result is an Error that names the number of shots. Raise FinalShots or NumLayers; the solver does not fall back to a classical tour. See Measured tour rates for what to expect on the local simulator.

Measured tour rates

Per-shot probabilities on the local simulator, computed from the state vector of the final circuit (two instances per size; “uniform” is a random bitstring):

Cities (qubits) Configuration Valid tour Uniform Optimal tour Uniform
3 (9) fastConfig (1 layer, fixed angles) 7–9% 1.2% 6–7% 0.6–1.2%
3 (9) defaultConfig (2 optimised layers) 15–23% 1.2% 15–17% 0.6–1.2%
4 (16) fastConfig 2.0–2.1% 0.037% 0.8–0.9% 0.012%
4 (16) defaultConfig 4.1–4.6% 0.037% 1.7–2.1% 0.012%
4 (16) defaultConfig with NumLayers = 3 (one instance) 14% 0.037% 6.6% 0.012%

At 4 cities the default 1000 final shots hold about 45 valid tours, and the chance that none of them is optimal is negligible; with 200 shots it is 1.5–3.5%. A run with 50 shots has no valid tour about one time in ten. The default optimisation takes one to two minutes at 4 cities (16 qubits) and several minutes with three layers; 3 cities take under a second.

Only n! of the 2^(n²) bitstrings are tours, so the share of valid shots falls quickly with the number of cities. These figures say nothing about instances wider than the simulator can run.

How QAOA Parameter Optimization Works

Variational Quantum-Classical Loop:

  1. Classical optimizer proposes QAOA parameters (gamma, beta) for every layer
  2. Quantum backend executes the QAOA circuit with those parameters
  3. Score the parameters - the expected QUBO energy of the circuit: exact from the amplitudes on a state-vector backend, otherwise the mean over OptimizationShots samples
  4. Optimizer updates parameters based on gradient-free Nelder-Mead simplex method
  5. Repeat until convergence or until MaxOptimizationIterations is reached
  6. Final execution uses optimized parameters with high shots for accurate result

Benefits:

Configuration Guidelines:

Performance: every optimizer iteration executes the circuit once (with OptimizationShots shots on hardware), so the extra cost is iterations × OptimizationShots. Measure on your own problem before relying on the variational loop on a paid backend.

For more details, see:

Next Steps

Need Help?

Authentication (for Cloud Quantum Backends)

When using cloud quantum backends (IonQ, Rigetti, Quantinuum, Atom Computing, IQM via Azure Quantum), you’ll need Azure credentials. Cloud backends take an authenticated HttpClient and your workspace URL:

open FSharp.Azure.Quantum.Core
open FSharp.Azure.Quantum.Backends.CloudBackends

// Uses DefaultAzureCredential: Azure CLI (az login), Managed Identity, environment variables, etc.
let credential = Authentication.CredentialProviders.createDefaultCredential ()
let httpClient = Authentication.createAuthenticatedClient credential

let workspaceUrl =
    "https://eastus.quantum.azure.com/subscriptions/<sub>/resourceGroups/<rg>/providers/Microsoft.Quantum/workspaces/<ws>"

// httpClient, workspace URL, target, shots
let ionq = CloudBackendFactory.createIonQ httpClient workspaceUrl "ionq.simulator" 1000

For quota and provider queries, FSharp.Azure.Quantum.Backends.AzureQuantumWorkspace.createDefault takes the subscription ID, resource group, workspace name and location.

Note: LocalBackend (default) works without Azure credentials - suited to development, testing, and small problems that fit the simulator width.


Ready to optimize! Continue with the API Reference for detailed documentation.