diff --git a/examples/qaoa_max_k_col_subgraph.ipynb b/examples/qaoa_max_k_col_subgraph.ipynb new file mode 100644 index 0000000..0e0f029 --- /dev/null +++ b/examples/qaoa_max_k_col_subgraph.ipynb @@ -0,0 +1,1311 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "id": "0542209c-a0f5-4d4b-ade1-23fd8c3a2298", + "metadata": { + "tags": [] + }, + "source": [ + "# Quantum Alternating Operator Ansatz (QAOA) on the Max-κ-Colorable Subgraph problem\n", + "\n", + "In this example, we implement QAOA for the Max-κ-Colorable Subgraph problem (also known as Max-κ-Cut).\n", + "\n", + "Reference:\n", + "\n", + "- Stuart Hadfield, Zhihui Wang, Bryan O'Gorman, Eleanor G. Rieffel,Davide Venturelli and Rupak Biswas. From the Quantum Approximate Optimization Algorithm to a Quantum Alternating Operator Ansatz. Algorithms 12.2 (2019), p.34.\n", + "\n", + "The code is also available as a standalone repo at https://github.com/Vuenc/QAOA-Mixers, which additionally includes code to run experiments in an automated fashion." + ] + }, + { + "cell_type": "code", + "execution_count": 1, + "id": "8939a85c-b9cd-443b-b816-f440529e2511", + "metadata": {}, + "outputs": [ + { + "name": "stderr", + "output_type": "stream", + "text": [ + "┌ Info: Precompiling Qaintellect [0a7fc590-1c99-4bf9-b5a7-8c116fa99d16]\n", + "└ @ Base loading.jl:1342\n", + "WARNING: Method definition pattern_uncall(Type{var\"#s423\"} where var\"#s423\"<:(RBNF.Token{T} where T), Function, Any, Any, Any) in module RBNF at /home/vuenc/.julia/packages/MLStyle/hjd8l/src/Record.jl:91 overwritten in module Qaintessent on the same line (check for duplicate calls to `include`).\n", + " ** incremental compilation may be fatally broken for this module **\n", + "\n" + ] + } + ], + "source": [ + "# Importing the required libraries\n", + "using Qaintessent\n", + "using Qaintessent.MaxKColSubgraphQAOA\n", + "using Qaintellect\n", + "using LinearAlgebra\n", + "using Flux\n", + "using Plots\n", + "\n", + "# Importing some example-specific utility functions for printing/plotting the output distribution\n", + "include(\"qaoa_max_k_col_subgraph_utility.jl\");" + ] + }, + { + "cell_type": "markdown", + "id": "09fbc22a-418e-4789-aa30-80e386d08060", + "metadata": {}, + "source": [ + "## Quantum Alternating Operator Ansatz in [Hadfield et al. 2019]\n", + "QAOA is an approach to approximately solve combinatorial optimization problems. Potential solutions are represented by quantum basis states. A QAOA circuit maps an initial state to an output state which is a superposition of potential solutions. Parameter optimization is performed on the circuit gates with the goal of increasing the probabilities of good solutions in the output superposition, and finally a good solution can be obtained by sampling from the output state.\n", + "\n", + "\"\"\n", + "\n", + "The QAOA setup in the aforementioned paper assumes that some initial state undergoes a series of parametric gates: *phase separation gates* and *mixer gates*.\n", + "The phase separation gates encode the optimization problem at hand. The mixer gates ensure that amplitudes are mixed between potential solutions. The phase separation gates have parameters $\\gamma_i$ and the mixer gates have parameters $\\beta_i$, and the output superposition for some parameter vectors $\\boldsymbol{\\beta}, \\boldsymbol{\\gamma}$ is denoted $| \\boldsymbol{\\beta}, \\boldsymbol{\\gamma} \\rangle$. The picture shows measurements at the end of the circuit, but in our case no measurements are needed, since we work in a simulation and have full access to the quantum state.\n", + "\n", + "\n", + "## The Max-κ-Colorable Subgraph problem\n", + "A graph $G$ on $n$ vertices and a number of colors $\\kappa$ are given.\n", + "The goal is to find a coloring and subgraph with a maximum number of edges such that no two adjacent vertices in the subgraph have the same color.\n", + "In other words, the goal is to find an (improper) coloring of the graph that minimizes the number of \"invalid\" edges.\n", + "\n", + "### QAOA Mapping\n", + "- Encoding: Potential solutions, i.e. colorings, are encoded in a one-hot fashion. Each vertex has $\\kappa$ bits associated with it, and the $i$-th of its bits is 1 iff the vertex has color $i$.\n", + "\n", + "\"\"\n", + "\n", + "- Phase separator: $U_P(\\gamma) = \\exp(-i\\gamma H_P)$, where the Hamiltonian $H_P$ encodes the objective function. If $x$ is a coloring and $f(x)$ its number of valid edges, it holds that\n", + "$$H_P |x\\rangle = (\\kappa m - 4 f(x) )|x\\rangle$$\n", + "- Mixer: Different mixers are possible.\n", + " 1. *r-Nearby-Values mixer* $U_{r-NV}(\\beta)$,\n", + " 2. *Parity Ring mixer* $U_{\\text{parity}}(\\beta)$,\n", + " 3. *Partition mixer* $U_{\\mathcal{P}-r-NV}(\\beta)$ which generalizes the Parity Ring mixer.\n", + " \n" + ] + }, + { + "cell_type": "markdown", + "id": "59cd9be7-0b47-42a5-bd0f-46f6502e7051", + "metadata": {}, + "source": [ + "## Running QAOA on Max-κ-Colorable Subgraph\n", + "The necessary gates are already implemented: The mixer gates are implemented in `src/qaoa/mixer_gates.jl`. The phase separator gates are implemented in `src/qaoa/phase_separator_gates.jl`. Backward passes for both types of gates are implemented in `src/qaoa/qaoa_gradients.jl`.\n", + "\n", + "Next, we create the function `max_κ_colorable_subgraph_circuit` which creates a QAOA circuit out of these gates on a given input graph." + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "id": "0414b38a-2264-4f48-9cc5-5ca85dc9cfe4", + "metadata": {}, + "outputs": [], + "source": [ + "function max_κ_colorable_subgraph_circuit(γs::Vector{Float64}, βs::Matrix{Float64},\n", + " graph::Graph, κ::Integer, mixer_type::Type{M}, mixer_params::Vector{<:Any}\n", + " ) where {M<:Union{RNearbyValuesMixerGate, ParityRingMixerGate, PartitionMixerGate}}\n", + " size(βs) == (graph.n, length(γs)) || throw(ArgumentError(\"γs, βs have incorrect dimensions.\"))\n", + " N = graph.n * κ\n", + "\n", + " # Create the circuit gates (multiple stages of phase separation gate and mixer gates)\n", + " gates::Vector{CircuitGate} = []\n", + " for (γ, βs_column) ∈ zip(γs, eachcol(βs))\n", + " # Add the phase separation gate\n", + " push!(gates, CircuitGate(Tuple(1:N), MaxKColSubgraphPhaseSeparationGate(γ, κ, graph)))\n", + "\n", + " # Add the mixer, consisting of a partial mixer gate for each vertex\n", + " for (vertex, β) ∈ zip(1:graph.n, βs_column)\n", + " if mixer_type == RNearbyValuesMixerGate\n", + " gate = RNearbyValuesMixerGate(β, mixer_params[1], κ)\n", + " elseif mixer_type == ParityRingMixerGate\n", + " gate = ParityRingMixerGate(β, κ)\n", + " elseif mixer_type == PartitionMixerGate\n", + " gate = PartitionMixerGate(β, κ, mixer_params[1])\n", + " end\n", + " push!(gates, CircuitGate(Tuple(((vertex - 1) * κ + 1):(vertex * κ)), gate))\n", + " end\n", + " end\n", + "\n", + " # One-hot encoding: one qubit for each node/color combination\n", + " Circuit{N}(gates)\n", + "end;" + ] + }, + { + "cell_type": "markdown", + "id": "e735a86a-deaa-4b23-981f-1a9043cdfbe9", + "metadata": {}, + "source": [ + "Let's try it:" + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "id": "38d4783c-f343-4a96-967e-3cdeecba48ab", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "\n", + " 6 ——□—————□—————□—————□—————□—————□—————□—————□———\n", + " | | | | | | | | \n", + " 5 ——□—————□—————□—————□—————□—————□—————□—————□———\n", + " | | | | \n", + " 4 ——□—————□—————□—————□—————□—————□—————□—————□———\n", + " | | | | | | | | \n", + " 3 ——□—————□—————□—————□—————□—————□—————□—————□———\n", + " | | | | \n", + " 2 ——□—————□—————□—————□—————□—————□—————□—————□———\n", + " | | | | | | | | \n", + " 1 ——□—————□—————□—————□—————□—————□—————□—————□———\n" + ] + }, + "execution_count": 3, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "n = 3 # number of nodes\n", + "κ = 2 # number of colors\n", + "p = 4 # circuit depth\n", + "testGraph = Graph(n, [(1,2)]) # graph with one edge\n", + "\n", + "initial_γs = randn(p) # p phase separators => p parameters\n", + "initial_βs = randn((n, p)) # p * n mixer gates, one per layer and vertex = p * n parameters\n", + "\n", + "mixerType = RNearbyValuesMixerGate\n", + "mixerParams = [1] # r = 1 in r-NV gate\n", + "\n", + "circ_test = max_κ_colorable_subgraph_circuit(initial_γs, initial_βs, testGraph, κ, RNearbyValuesMixerGate, mixerParams)" + ] + }, + { + "cell_type": "markdown", + "id": "11cc397f-4683-4c8d-8af1-0140f54ace8a", + "metadata": {}, + "source": [ + "You can see a QAOA circuit: The gates that act on all 6 qubits are the phase separators. The gates that act on only two qubits at a time are the mixers (a mixer mixes the colors of a single vertex, and since $\\kappa = 2$, each vertex is represented by 2 qubits). There are four layers of phase separators/mixers, since $p = 4$." + ] + }, + { + "cell_type": "markdown", + "id": "e7858a50-2620-4c3f-8ef8-69b018be9132", + "metadata": {}, + "source": [ + "We also need to create the initial state, which here should give all vertices the first color. `ψ_from_coloring` creates a one-hot encoded state corresponding to a coloring, and `ψ_initial` creates the initial state using the canonic initial coloring." + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "id": "2e34774d-44be-437c-82b9-3c9669d217fb", + "metadata": {}, + "outputs": [], + "source": [ + "# Create a state ψ that corresponds to a given coloring\n", + "function ψ_from_coloring(n::Int, κ::Int, colors::Vector{Int})::Vector{ComplexF64}\n", + " (n > 0 && κ > 0) || throw(DomainError(\"Parameters n and κ must be positive integers.\"))\n", + " colors ⊆ 1:κ || throw(ArgumentError(\"Parameter `colors` may only contain colors in the range 1:$(κ).\"))\n", + "\n", + " # Create ψ with |1> entries in the indices corresponding to the given colors, |0> elsewhere\n", + " ψ = kron((color == colors[vertex] ? [0.0im, 1] : [1, 0.0im] for vertex ∈ 1:n for color ∈ 1:κ)...)\n", + " ψ\n", + "end\n", + "\n", + "# Create the initial state (all vertices are assigned the first color)\n", + "function ψ_initial(n::Integer, κ::Integer)::Vector{ComplexF64}\n", + " ψ_from_coloring(n, κ, repeat([1], n))\n", + "end;" + ] + }, + { + "cell_type": "markdown", + "id": "585c1e79-c05a-47df-bae5-3ee3a89981b2", + "metadata": {}, + "source": [ + "Finally, we need a function that runs the optimization. The `optimize_qaoa` function creates a QAOA circuit with random initialization for a given input graph and number of colors and a given circuit depth $p$. It then runs the optimization for a specified number of rounds using the ADAM optimizer and a specified learning rate. Optionally, it can log detailed results (see the `QAOALogger` struct in `experiment-runner.jl` in the `QAOAMixers` repo for an example)." + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "id": "6ad58e00-869f-4cba-a437-5ecb128878a8", + "metadata": {}, + "outputs": [], + "source": [ + "function optimize_qaoa(graph::Graph, κ::Int; p::Union{Int, Nothing}=nothing, training_rounds::Int=10,\n", + " learning_rate::Float64=0.005, circ_in::Union{Circuit{N}, Nothing}=nothing, init_stddev=0.1,\n", + " mixer_type::Type{M}=RNearbyValuesMixerGate, mixer_params::Vector{<:Any}=[1],\n", + " logger::Any=nothing) where {N, M<:Union{RNearbyValuesMixerGate, ParityRingMixerGate, PartitionMixerGate}}\n", + " \n", + " (isnothing(circ_in) ⊻ isnothing(p)) ||\n", + " throw(ArgumentError(\"Must specify exactly one of the parameters `circ_in` and `p`.\"))\n", + "\n", + " (κ > 0 && (isnothing(p) || p > 0) && training_rounds > 0) ||\n", + " throw(DomainError(\"Parameters `κ`, `p` and `training_rounds` must be positive integers.\"))\n", + "\n", + " if isnothing(circ_in)\n", + " # Initialize circuit and wavefunction\n", + " (initial_γs, initial_βs) = (randn(p) * init_stddev, randn((graph.n, p)) * init_stddev)\n", + " \n", + " circ = max_κ_colorable_subgraph_circuit(initial_γs, initial_βs, graph, κ, mixer_type, mixer_params)\n", + " else\n", + " N == κ * graph.n || throw(ArgumentError(\"Circuit `circ_in` has wrong dimensions.\"))\n", + " circ = circ_in\n", + " end\n", + " ψ = ψ_initial(graph.n, κ)\n", + " H_P_diag = diag(max_k_col_subgraph_phase_separation_hamiltonian(graph, κ)) # can't have `Diagonal` matrix type here (error in backprop)\n", + " \n", + " # Undo the transform of HP to the objective function: f(x) |-> κm - 4 f(x). See text after Eq. (17).\n", + " objective_transform(x) = (κ*length(graph.edges) - x)/4\n", + "\n", + " # Set up optimization with Flux\n", + " params = Flux.params(circ)\n", + " data = repeat([()], training_rounds) # empty input data for `training_rounds` rounds of training\n", + " optimizer = ADAM(learning_rate)\n", + " round = 1\n", + " expectation() = begin # evaluate expectation to be minimized and print it\n", + " ψ_out = apply(ψ, circ.moments)\n", + " objective = objective_transform(real(ψ_out' * (H_P_diag .* ψ_out)))\n", + " println(\"Training, round $(round): average objective = $(objective)\")\n", + " if !isnothing(logger)\n", + " log_qaoa(logger, round, ψ_out, objective, params)\n", + " end\n", + " round += 1\n", + " return -objective\n", + " end\n", + "\n", + " # Perform training\n", + " Flux.train!(expectation, params, data, optimizer) #, cb=Flux.throttle(print_expectation, 1))\n", + " return circ\n", + "end;" + ] + }, + { + "cell_type": "markdown", + "id": "22eb3b45-e85d-4418-8c35-f1a48265bdd1", + "metadata": {}, + "source": [ + "## Running Experiments\n", + "\n", + "### Experiment 1\n", + "First, let's run the QAOA optimization on a very small graph $(n = 3)$ with two colors ($\\kappa = 2$).\n", + "\n", + "In this example, it's clear that $G$ is 2-colorable. The algorithm should be able to approach an objective function value of 2.0 (2 valid edges). Since the example is so small, the optimization should run through quickly." + ] + }, + { + "cell_type": "code", + "execution_count": 6, + "id": "5c93d720-f4bc-4a21-9ed2-decd3e7caf4c", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Training, round 1: average objective = 0.5980859885186314\n", + "Training, round 2: average objective = 0.7704099325632594\n", + "Training, round 3: average objective = 0.9376987727429621\n", + "Training, round 4: average objective = 1.0745095791278172\n", + "Training, round 5: average objective = 1.1653043533151544\n", + "Training, round 6: average objective = 1.223352835940797\n", + "Training, round 7: average objective = 1.2843664085833453\n", + "Training, round 8: average objective = 1.365886486092841\n", + "Training, round 9: average objective = 1.4661949457256298\n", + "Training, round 10: average objective = 1.5760099959152722\n", + "Training, round 11: average objective = 1.6833413028912028\n", + "Training, round 12: average objective = 1.7757823699960618\n", + "Training, round 13: average objective = 1.842675846624324\n", + "Training, round 14: average objective = 1.8780587902910388\n", + "Training, round 15: average objective = 1.883644637898063\n", + "Training, round 16: average objective = 1.8691070752834913\n", + "Training, round 17: average objective = 1.8476729402284238\n", + "Training, round 18: average objective = 1.830160123078123\n", + "Training, round 19: average objective = 1.8222408766472404\n", + "Training, round 20: average objective = 1.825176789555552\n", + "Training, round 21: average objective = 1.8375845983799508\n", + "Training, round 22: average objective = 1.856780500444334\n", + "Training, round 23: average objective = 1.87955078473738\n", + "Training, round 24: average objective = 1.902656766624304\n", + "Training, round 25: average objective = 1.9233223726895543\n", + "Training, round 26: average objective = 1.9396981054577893\n", + "Training, round 27: average objective = 1.9511135514170648\n", + "Training, round 28: average objective = 1.957973583326912\n", + "Training, round 29: average objective = 1.961352752659178\n", + "Training, round 30: average objective = 1.9625083777645804\n" + ] + }, + { + "data": { + "text/plain": [ + "\n", + " 6 ——□—————□—————□—————□—————□—————□—————□—————□—————□—————□———\n", + " | | | | | | | | | | \n", + " 5 ——□—————□—————□—————□—————□—————□—————□—————□—————□—————□———\n", + " | | | | | \n", + " 4 ——□—————□—————□—————□—————□—————□—————□—————□—————□—————□———\n", + " | | | | | | | | | | \n", + " 3 ——□—————□—————□—————□—————□—————□—————□—————□—————□—————□———\n", + " | | | | | \n", + " 2 ——□—————□—————□—————□—————□—————□—————□—————□—————□—————□———\n", + " | | | | | | | | | | \n", + " 1 ——□—————□—————□—————□—————□—————□—————□—————□—————□—————□———\n" + ] + }, + "execution_count": 6, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "exampleGraph1 = Graph(3, [(1,2), (2,3)])\n", + "κ = 2 # number of colors\n", + "p = 5 # number of phase separator/mixer pairs in the circuit (depth)\n", + "\n", + "circ_opt1 = optimize_qaoa(exampleGraph1, κ, p=p, training_rounds=30, mixer_type=RNearbyValuesMixerGate)" + ] + }, + { + "cell_type": "markdown", + "id": "38431985-28a4-4d88-b22f-e57ec6867385", + "metadata": {}, + "source": [ + "You should see the expected objective function approach the optimum 2.0. We can use the optimized circuit and plot the probabilities of the individual solutions in its output superposition:" + ] + }, + { + "cell_type": "code", + "execution_count": 7, + "id": "e8dc9804-cbd7-4e93-96b5-f3ef2f203e8a", + "metadata": {}, + "outputs": [ + { + "data": { + "image/svg+xml": [ + "\n", + "\n", + "\n", + " \n", + " \n", + " \n", + "\n", + "\n", + "\n", + " \n", + " \n", + " \n", + "\n", + "\n", + "\n", + " \n", + " \n", + " \n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n" + ] + }, + "execution_count": 7, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "plot_circuit_output_distribution(circ_opt1, ψ_initial(3, 2))" + ] + }, + { + "cell_type": "markdown", + "id": "daf0c74c-d23f-4f4f-9726-9d601ff32d77", + "metadata": {}, + "source": [ + "The plot should show high probability mass on one or both of the states $|011001\\rangle$ and $|100110\\rangle$, which are the two optimal solutions. We can also print out the probabilities of the individual solutions:" + ] + }, + { + "cell_type": "code", + "execution_count": 8, + "id": "f5218644-1883-48c8-b5f4-2a8dc7b12461", + "metadata": { + "tags": [] + }, + "outputs": [ + { + "data": { + "text/plain": [ + "8-element Vector{Tuple{Vector{Int64}, Int64, Float64}}:\n", + " ([2, 1, 2], 2, 0.9658643183793165)\n", + " ([1, 1, 2], 1, 0.014951992917530497)\n", + " ([1, 2, 2], 1, 0.008682032156874284)\n", + " ([2, 1, 1], 1, 0.004763787126430834)\n", + " ([2, 2, 2], 0, 0.003764181236909984)\n", + " ([2, 2, 1], 1, 0.001369773047610789)\n", + " ([1, 2, 1], 2, 0.0005182406529906287)\n", + " ([1, 1, 1], 0, 8.567448233657023e-5)" + ] + }, + "execution_count": 8, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "# Prints a sorted list of output colorings.\n", + "# - first element is the coloring\n", + "# - second element is the objective function value for this coloring\n", + "# - third element is its probability in the output superposition\n", + "output_colorings_distribution_scored(circ_opt1, exampleGraph1)" + ] + }, + { + "cell_type": "markdown", + "id": "8e1dacc7-3dd8-4a30-8dca-6d404f965204", + "metadata": {}, + "source": [ + "### Experiment 2\n", + "Next, we'll run the QAOA optimization on a slightly bigger graph $(n = 5)$ with two colors ($\\kappa = 2$). This gives $5 \\cdot 2 = 10$ qubits, which is already close to the maximum number of qubits we can handle without running out of memory.\n", + "\n", + "\"\"\n", + "\n", + "This example is more interesting, because the graph is not 2-colorable. It can be made 2-colorable by removing one edge, so the maximum objective function value is 5.0. The algorithm should be able to approach this optimum, however it might take more iterations than before. Also the example takes a while to run because of the increased number of qubits." + ] + }, + { + "cell_type": "code", + "execution_count": 9, + "id": "9e849656-8e57-42a5-a591-cec212421d7e", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Training, round 1: average objective = 0.8596878350500474\n", + "Training, round 2: average objective = 1.2429881881573963\n", + "Training, round 3: average objective = 1.667428432726421\n", + "Training, round 4: average objective = 2.0865175743804114\n", + "Training, round 5: average objective = 2.4637513807495113\n", + "Training, round 6: average objective = 2.770649504659332\n", + "Training, round 7: average objective = 2.983176606226205\n", + "Training, round 8: average objective = 3.1003903435197935\n", + "Training, round 9: average objective = 3.1526425183720606\n", + "Training, round 10: average objective = 3.1783643730539235\n", + "Training, round 11: average objective = 3.204192299462844\n", + "Training, round 12: average objective = 3.2399611981647345\n", + "Training, round 13: average objective = 3.2830169416761\n", + "Training, round 14: average objective = 3.325569128755278\n", + "Training, round 15: average objective = 3.361044554814974\n", + "Training, round 16: average objective = 3.3879710309652493\n", + "Training, round 17: average objective = 3.4103779169030344\n", + "Training, round 18: average objective = 3.4345935519970405\n", + "Training, round 19: average objective = 3.465395545468241\n", + "Training, round 20: average objective = 3.504477153943348\n", + "Training, round 21: average objective = 3.550859167547561\n", + "Training, round 22: average objective = 3.601915622492066\n", + "Training, round 23: average objective = 3.654395554223762\n", + "Training, round 24: average objective = 3.7053144384266736\n", + "Training, round 25: average objective = 3.752684281240512\n", + "Training, round 26: average objective = 3.795986105479465\n", + "Training, round 27: average objective = 3.8362531142795406\n", + "Training, round 28: average objective = 3.8757216940442003\n", + "Training, round 29: average objective = 3.9171689685913047\n", + "Training, round 30: average objective = 3.9631455313650683\n" + ] + }, + { + "data": { + "text/plain": [ + "\n", + " 10 ——□—————□—————□—————□—————□—————□—————□—————□—————□—————□———\n", + " | | | | | | | | | | \n", + " 9 ——□—————□—————□—————□—————□—————□—————□—————□—————□—————□———\n", + " | | | | | \n", + " 8 ——□—————□—————□—————□—————□—————□—————□—————□—————□—————□———\n", + " | | | | | | | | | | \n", + " 7 ——□—————□—————□—————□—————□—————□—————□—————□—————□—————□———\n", + " | | | | | \n", + " 6 ——□—————□—————□—————□—————□—————□—————□—————□—————□—————□———\n", + " | | | | | | | | | | \n", + " 5 ——□—————□—————□—————□—————□—————□—————□—————□—————□—————□———\n", + " | | | | | \n", + " 4 ——□—————□—————□—————□—————□—————□—————□—————□—————□—————□———\n", + " | | | | | | | | | | \n", + " 3 ——□—————□—————□—————□—————□—————□—————□—————□—————□—————□———\n", + " | | | | | \n", + " 2 ——□—————□—————□—————□—————□—————□—————□—————□—————□—————□———\n", + " | | | | | | | | | | \n", + " 1 ——□—————□—————□—————□—————□—————□—————□—————□—————□—————□———\n" + ] + }, + "execution_count": 9, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "exampleGraph2 = Graph(5, [(1,2), (2,3), (3,4), (4,5), (5,1), (1,3)]);\n", + "κ = 2 # number of colors\n", + "p = 5 # number of phase separator/mixer pairs in the circuit (depth)\n", + "\n", + "# This might take a while...\n", + "circ_opt2 = optimize_qaoa(exampleGraph2, κ, p=p, training_rounds=30, mixer_type=RNearbyValuesMixerGate)" + ] + }, + { + "cell_type": "markdown", + "id": "ac2dad5f-bce8-40a2-adc5-d1295701f995", + "metadata": {}, + "source": [ + "Let's look at the output distribution:" + ] + }, + { + "cell_type": "code", + "execution_count": 12, + "id": "ccde11be-1978-43b3-8547-d8936a2fabf8", + "metadata": {}, + "outputs": [ + { + "data": { + "image/svg+xml": [ + "\n", + "\n", + "\n", + " \n", + " \n", + " \n", + "\n", + "\n", + "\n", + " \n", + " \n", + " \n", + "\n", + "\n", + "\n", + " \n", + " \n", + " \n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n" + ] + }, + "execution_count": 12, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "plot_circuit_output_distribution(circ_opt2, ψ_initial(5, 2))" + ] + }, + { + "cell_type": "code", + "execution_count": 11, + "id": "b04c584e-d588-4f6f-86e0-92fb8836d060", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "32-element Vector{Tuple{Vector{Int64}, Int64, Float64}}:\n", + " ([2, 1, 1, 2, 1], 5, 0.325520180005122)\n", + " ([2, 2, 1, 2, 1], 5, 0.10233284302571378)\n", + " ([2, 1, 2, 1, 1], 4, 0.06495808562081633)\n", + " ([2, 1, 1, 1, 2], 3, 0.05441697592930108)\n", + " ([2, 1, 2, 1, 2], 4, 0.05393344536093922)\n", + " ([1, 1, 1, 1, 2], 2, 0.04995270328585539)\n", + " ([2, 1, 2, 2, 1], 4, 0.049510500511390776)\n", + " ([1, 1, 2, 1, 2], 5, 0.04513377797798233)\n", + " ([2, 2, 1, 1, 2], 3, 0.040445022211986446)\n", + " ([1, 1, 2, 1, 1], 3, 0.03798965987117928)\n", + " ([1, 2, 1, 2, 1], 4, 0.02527330673930269)\n", + " ([1, 1, 1, 2, 1], 2, 0.021886580550562917)\n", + " ([2, 1, 1, 2, 2], 3, 0.01863631816552771)\n", + " ⋮\n", + " ([2, 2, 1, 1, 1], 3, 0.005722850816101823)\n", + " ([2, 2, 2, 1, 2], 2, 0.004938218665173299)\n", + " ([2, 2, 2, 1, 1], 2, 0.004622677034715244)\n", + " ([2, 2, 1, 2, 2], 3, 0.003998552284911806)\n", + " ([1, 2, 1, 1, 1], 2, 0.003280965654082029)\n", + " ([1, 1, 2, 2, 1], 3, 0.0031566115183896664)\n", + " ([1, 2, 2, 1, 2], 5, 0.0017402000113088901)\n", + " ([1, 2, 2, 1, 1], 3, 0.001708693060965)\n", + " ([1, 2, 1, 2, 2], 4, 0.0010040138475498502)\n", + " ([1, 1, 1, 2, 2], 2, 0.00012565502681213174)\n", + " ([2, 2, 2, 2, 2], 0, 8.876058116919295e-5)\n", + " ([1, 2, 2, 2, 2], 3, 1.617302209324228e-5)" + ] + }, + "execution_count": 11, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "output_colorings_distribution_scored(circ_opt2, exampleGraph2)" + ] + }, + { + "cell_type": "markdown", + "id": "09fbef8d-3d11-4565-a93e-7e7a9bfe5c9d", + "metadata": {}, + "source": [ + "You should see that high probability mass is put on one or more colorings with objective function value 5." + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Julia 1.6.0", + "language": "julia", + "name": "julia-1.6" + }, + "language_info": { + "file_extension": ".jl", + "mimetype": "application/julia", + "name": "julia", + "version": "1.6.2" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/examples/qaoa_max_k_col_subgraph_utility.jl b/examples/qaoa_max_k_col_subgraph_utility.jl new file mode 100644 index 0000000..1b90a12 --- /dev/null +++ b/examples/qaoa_max_k_col_subgraph_utility.jl @@ -0,0 +1,79 @@ +# Utility function to count the properly colored edges in a graph coloring (i.e. endpoints have different colors) +function properly_colored_edges(graph::Graph, coloring::Vector{Int}) + length(coloring) == graph.n || throw(ArgumentError("Length of coloring must equal the number of vertices.")) + return count(coloring[a] != coloring[b] for (a, b) ∈ graph.edges) +end + +# Utility function to compute the probabilities of the outcomes represented by a wavefunction ψ, sorted by descending probability +function wavefunction_distribution(ψ::Vector{ComplexF64}; as_bitstrings::Bool = true, + include_zero = false)::Union{Vector{Tuple{Int, Float64}}, Vector{Tuple{Vector{Int}, Float64}}} + distribution = [(i-1, abs(amplitude)^2) for (i, amplitude) ∈ enumerate(ψ) if abs(amplitude) > 0 || include_zero] + if as_bitstrings + N = Int(log2(length(ψ))) + distribution = [(digits(i, base=2, pad=N) |> reverse, p) for (i, p) ∈ distribution] + end + + return sort(distribution, by=(t -> -t[2])) +end + +# Utility function to compute the probabilities of the outcome wavefunction of a circuit applied to ψ, sorted by descending probability +function output_distribution(circ::Circuit, ψ::Vector{ComplexF64}; as_bitstrings::Bool = true, + include_zero = false)::Union{Vector{Tuple{Int, Float64}}, Vector{Tuple{Vector{Int}, Float64}}} + ψ_out = apply(ψ, circ.moments) + return wavefunction_distribution(ψ_out, as_bitstrings=as_bitstrings, include_zero=include_zero) +end + +# Utility function to compute the probabilities of the output colorings of a circuit applied to ψ, sorted by descending probability +function output_colorings_distribution(circ::Circuit{N}, n::Int; + include_zero = false)::Vector{Tuple{Dict{Int, Vector{Int}}, Float64}} where {N} + κ = Int(N / n) + ψ = ψ_initial(n, κ) + ψ_out = apply(ψ, circ.moments) + distribution = wavefunction_distribution(ψ_out, as_bitstrings=false, include_zero=include_zero) + + return [(decode_basis_state(state, n, κ), p) for (state, p) ∈ distribution] +end + +# Utility function to compute the probabilities of the output colorings of a circuit applied to ψ, sorted by descending probability +function output_colorings_distribution_scored(circ::Circuit{N}, graph::Graph; + include_zero = false)::Vector{Tuple{Vector{Int}, Int, Float64}} where {N} + dist = output_colorings_distribution(circ, graph.n, include_zero = include_zero) + + coloring_dict_to_list(dict) = [length(dict[i]) == 1 ? dict[i][1] : + throw(ArgumentError("At least one vertex has not one unique color.")) for i ∈ 1:graph.n] + + return [(coloring_dict_to_list(coloring), properly_colored_edges(graph, coloring_dict_to_list(coloring)), p) for (coloring, p) ∈ dist] +end + +# Decodes the coloring represented by a single computational basis state, represented as integer +function decode_basis_state(basis_state::Int, n::Int, κ::Int)::Dict{Int, Vector{Int}} + (n > 0 && κ > 0) || throw(DomainError("Parameters n and κ must be positive integers.")) + + N = n * κ + bits = digits(basis_state, base=2, pad=N) |> reverse + vertex_bits = vertex -> bits[((vertex - 1) * κ + 1):(vertex * κ)] + colors_by_vertex = Dict([(v, findall(!iszero, vertex_bits(v))) for v ∈ 1:n]) + + all(!isnothing, colors_by_vertex) || throw(ArgumentError("The state `basis_state` has at least one vertex without a color.")) + + return colors_by_vertex +end + +# Prototypical log function. Implementations should have the @Zygote.nograd attribute and use +# a data structure as first argument which can save log data. params is ::Zygote.Params. +function log_qaoa(logger::T, round::Int, ψ_out::Vector{ComplexF64}, objective::Float64, params) where {T} + throw(ArgumentError("The type `$(T)` does not implement the `log_qaoa` + function and is not a valid logger.")) +end + +function plot_circuit_output_distribution(circ::Circuit, ψ) + data = output_distribution(circ, ψ, as_bitstrings=true, include_zero=false) + sort!(data, by=d -> d[1]) + bar(data .|> (d -> "|$(join(d[1]))⟩"), + data .|> (d -> d[2]), + ylims=(0, 1), xrotation = 60, color="#ff8080", x_ticks=:all, + bottom_margin=5Plots.mm, + xtickfontsize=5, legend=:topleft, + label = "outcome probabilities" + ) +end \ No newline at end of file diff --git a/src/flux_integration.jl b/src/flux_integration.jl index 2fa5806..8290eef 100644 --- a/src/flux_integration.jl +++ b/src/flux_integration.jl @@ -1,3 +1,4 @@ +using Qaintessent.MaxKColSubgraphQAOA # let Flux discover the trainable parameters @@ -15,6 +16,12 @@ Flux.@functor Moment Flux.@functor MeasurementOperator Flux.@functor Circuit +# Definitions for the Max-k-col. subgraph QAOA example +Flux.@functor ParityRingMixerGate +Flux.@functor RNearbyValuesMixerGate +Flux.@functor PartitionMixerGate +Flux.@functor MaxKColSubgraphPhaseSeparationGate + function collect_gradients(cx::Zygote.Context, q, dq) # special cases: circuit gate chain