diff --git a/README.md b/README.md index a6a43d4..23d55c5 100644 --- a/README.md +++ b/README.md @@ -114,9 +114,6 @@ Examples: - **QUBO**: natural objective is the un-negated polynomial $x^\top Q x + c^\top x + b$. -Legacy `cost(x)` is kept as a deprecated compatibility score with old behavior: -`cost(x) = -energy(x)`. - --- ## Background @@ -253,16 +250,24 @@ qaoa = QAOA( ## Extract Results -Once `qaoa.optimize(depth=p)` is run, extract the expectation value, variance, and parameters for each depth $1\leq i \leq p$: +Once `qaoa.optimize(depth=p)` is run, extract the best energy, variance, +objective value, and parameters for each depth $1\leq i \leq p$: ```python -qaoa.get_Exp(depth=i) +qaoa.get_energy(depth=i) +qaoa.get_objective(depth=i) qaoa.get_Var(depth=i) qaoa.get_gamma(depth=i) qaoa.get_beta(depth=i) ``` -Additionally, for every optimizer call at each depth, the **angles, expectation value, variance, maximum cost, minimum cost, and number of shots** are stored in: +The legacy compatibility aliases `problem.cost()`, `problem.computeMinMaxCosts()`, +and `qaoa.get_Exp()` are not part of the API anymore. Use +`objective_value()`, `objective_bounds()`, `get_energy()`, and +`get_objective()` directly. + +Additionally, for every optimizer call at each depth, the optimizer history, +variance, best solutions, and shot counts are stored in: ```python qaoa.optimization_results[i] diff --git a/examples/ExactCover/CompareGroverXY.ipynb b/examples/ExactCover/CompareGroverXY.ipynb index b328607..ee4eaff 100644 --- a/examples/ExactCover/CompareGroverXY.ipynb +++ b/examples/ExactCover/CompareGroverXY.ipynb @@ -96,7 +96,7 @@ "\n", "opt_sol = ec_problem.brute_force_solve()\n", "print(f\"Optimal solution (brute force): {opt_sol}\")\n", - "print(f\"Cost: {ec_problem.cost(opt_sol):.6f}\")" + "print(f\"Cost: {ec_problem.objective_value(opt_sol):.6f}\")" ] }, { diff --git a/examples/ExactCover/ExactCover 6 3.ipynb b/examples/ExactCover/ExactCover 6 3.ipynb index a9a6222..55a1666 100644 --- a/examples/ExactCover/ExactCover 6 3.ipynb +++ b/examples/ExactCover/ExactCover 6 3.ipynb @@ -101,9 +101,9 @@ "print(\"solutions:\")\n", "costs=[]\n", "for s in [''.join(i) for i in itertools.product('01', repeat =nL)]:\n", - " costs.append(-qaoa.problem.cost(s))\n", + " costs.append(qaoa.problem.energy(s))\n", " if qaoa.problem.isFeasible(s):\n", - " print(s, -qaoa.problem.cost(s))" + " print(s, qaoa.problem.energy(s))" ] }, { @@ -216,7 +216,7 @@ } ], "source": [ - "min_cost, max_cost = qaoa.problem.computeMinMaxCosts()\n", + "min_cost, max_cost = qaoa.problem.objective_bounds()\n", "min_cost, max_cost" ] }, diff --git a/examples/ExactCover/data/exact_cover_path_problem_example.json b/examples/ExactCover/data/exact_cover_path_problem_example.json index 030929c..90342df 100644 --- a/examples/ExactCover/data/exact_cover_path_problem_example.json +++ b/examples/ExactCover/data/exact_cover_path_problem_example.json @@ -73,7 +73,8 @@ 0.0, 0.0 ], - "hamming_weight": 2 + "hamming_weight": 2, + "objective_sense": "minimize" }, "qaoa_params": { "cvar": 0.7, @@ -118,7 +119,9 @@ "00000011": 19, "01000010": 503 }, - "opt_time": 0.7518182500207331 + "opt_time": 0.7518182500207331, + "best_energy": null, + "best_objective": null }, "2": { "optimal_angles": [ @@ -157,7 +160,9 @@ "10000100": 224, "00000011": 150 }, - "opt_time": 0.31045849999645725 + "opt_time": 0.31045849999645725, + "best_energy": null, + "best_objective": null }, "3": { "optimal_angles": [ @@ -195,7 +200,9 @@ "01010000": 102, "00000110": 439 }, - "opt_time": 0.438680374994874 + "opt_time": 0.438680374994874, + "best_energy": null, + "best_objective": null }, "4": { "optimal_angles": [ @@ -238,7 +245,9 @@ "00010010": 33, "01100000": 3335 }, - "opt_time": 0.6514912919956259 + "opt_time": 0.6514912919956259, + "best_energy": null, + "best_objective": null }, "5": { "optimal_angles": [ @@ -283,7 +292,9 @@ "00011000": 103, "00000110": 146 }, - "opt_time": 0.9302357080159709 + "opt_time": 0.9302357080159709, + "best_energy": null, + "best_objective": null }, "6": { "optimal_angles": [ @@ -330,7 +341,9 @@ "00010010": 14, "01100000": 2922 }, - "opt_time": 1.1260944169771392 + "opt_time": 1.1260944169771392, + "best_energy": null, + "best_objective": null }, "7": { "optimal_angles": [ @@ -379,7 +392,9 @@ "00001001": 143, "00000110": 132 }, - "opt_time": 1.5074970000132453 + "opt_time": 1.5074970000132453, + "best_energy": null, + "best_objective": null }, "8": { "optimal_angles": [ @@ -430,7 +445,9 @@ "10000010": 56, "10000001": 17 }, - "opt_time": 1.9193396670161746 + "opt_time": 1.9193396670161746, + "best_energy": null, + "best_objective": null }, "9": { "optimal_angles": [ @@ -482,7 +499,9 @@ "00000011": 29, "00000110": 180 }, - "opt_time": 2.4477102090022527 + "opt_time": 2.4477102090022527, + "best_energy": null, + "best_objective": null }, "10": { "optimal_angles": [ @@ -537,7 +556,9 @@ "00100001": 5, "10010000": 4087 }, - "opt_time": 2.9181083750154357 + "opt_time": 2.9181083750154357, + "best_energy": null, + "best_objective": null } } }, @@ -551,5 +572,6 @@ "conda_env": "neqst-kongsberg-2025", "qaoa_repo_dir": "/Users/havahol/playground/quantum/QAOA/qaoa/utils", "qaoa_git_commit": "ff2d04cf2aa5069dcb58ba57ab7d2fc79c465513" - } + }, + "schema_version": 3 } diff --git a/examples/MaxCut/CVaR.ipynb b/examples/MaxCut/CVaR.ipynb index 4cd1c16..6b8dbd6 100644 --- a/examples/MaxCut/CVaR.ipynb +++ b/examples/MaxCut/CVaR.ipynb @@ -35,7 +35,7 @@ "id": "3d17ca29", "metadata": {}, "source": [ - "Create Barabási–Albert graph instance, more examples can be found here https://github.com/OpenQuantumComputing/data." + "Create Barab\u00e1si\u2013Albert graph instance, more examples can be found here https://github.com/OpenQuantumComputing/data." ] }, { @@ -55,11 +55,7 @@ "output_type": "display_data" } ], - "source": [ - "G = nx.read_gml(\"data/w_ba_n21_k4_0.gml\") # Load graph data for the 21-node, 4-regular graph (GML format) in examples/MaxCUt/data\n", - "nx.draw_networkx(G)\n", - "mincost = -25.23404480588015 # Precalculated minimum cost (for comparison)" - ] + "source": "G = nx.read_gml(\"data/w_ba_n21_k4_0.gml\") # Load graph data for the 21-node, 4-regular graph (GML format) in examples/MaxCUt/data\nnx.draw_networkx(G)\nmincost = 0 # worst feasible objective (no edges cut)\nmaxcost = 25.23404480588015 # precalculated optimal MaxCut value" }, { "cell_type": "markdown", @@ -222,29 +218,7 @@ "output_type": "display_data" } ], - "source": [ - "import matplotlib.pyplot as plt\n", - "\n", - "fig = plt.figure()\n", - "utils.plot_ApproximationRatio(\n", - " qaoa,\n", - " maxdepth,\n", - " mincost=mincost,\n", - " maxcost=0,\n", - " label=\"QAOA vanilla\",\n", - " style=\"o--b\",\n", - " fig=fig,\n", - ")\n", - "utils.plot_ApproximationRatio(\n", - " qaoa_CVaR,\n", - " maxdepth,\n", - " mincost=mincost,\n", - " maxcost=0,\n", - " label=\"QAOA CVaR\",\n", - " style=\"x--k\",\n", - " fig=fig,\n", - ")" - ] + "source": "import matplotlib.pyplot as plt\n\nfig = plt.figure()\nutils.plot_ApproximationRatio(\n qaoa,\n maxdepth,\n mincost=mincost,\n maxcost=maxcost,\n label=\"QAOA vanilla\",\n style=\"o--b\",\n fig=fig,\n)\nutils.plot_ApproximationRatio(\n qaoa_CVaR,\n maxdepth,\n mincost=mincost,\n maxcost=maxcost,\n label=\"QAOA CVaR\",\n style=\"x--k\",\n fig=fig,\n)" }, { "cell_type": "markdown", @@ -301,4 +275,4 @@ }, "nbformat": 4, "nbformat_minor": 5 -} +} \ No newline at end of file diff --git a/examples/MaxCut/ComparisonOptimizers.ipynb b/examples/MaxCut/ComparisonOptimizers.ipynb index fbbe53f..6855c06 100644 --- a/examples/MaxCut/ComparisonOptimizers.ipynb +++ b/examples/MaxCut/ComparisonOptimizers.ipynb @@ -37,7 +37,7 @@ "id": "0e9efef7", "metadata": {}, "source": [ - "Create Barabási–Albert graph instance, more examples can be found here https://github.com/OpenQuantumComputing/data" + "Create Barab\u00e1si\u2013Albert graph instance, more examples can be found here https://github.com/OpenQuantumComputing/data" ] }, { @@ -57,11 +57,7 @@ "output_type": "display_data" } ], - "source": [ - "G = nx.read_gml(\"data/w_ba_n10_k4_0.gml\") # Load graph data for the 10-node, 4-regular graph (GML format) in examples/MaxCut/data\n", - "nx.draw_networkx(G)\n", - "mincost = -8.657714089848158 # Precalculated" - ] + "source": "G = nx.read_gml(\"data/w_ba_n10_k4_0.gml\") # Load graph data for the 10-node, 4-regular graph (GML format) in examples/MaxCut/data\nnx.draw_networkx(G)\nmincost = 0 # worst feasible objective (no edges cut)\nmaxcost = 8.657714089848158 # precalculated optimal MaxCut value" }, { "cell_type": "markdown", @@ -257,21 +253,7 @@ "output_type": "display_data" } ], - "source": [ - "import matplotlib.pyplot as plt\n", - "\n", - "fig = plt.figure()\n", - "for key in optimizers:\n", - " utils.plot_ApproximationRatio(\n", - " qaoa[key],\n", - " maxdepth,\n", - " mincost=mincost,\n", - " maxcost=0,\n", - " label=key,\n", - " style=plotstyle[key] + \"-\",\n", - " fig=fig,\n", - " )" - ] + "source": "import matplotlib.pyplot as plt\n\nfig = plt.figure()\nfor key in optimizers:\n utils.plot_ApproximationRatio(\n qaoa[key],\n maxdepth,\n mincost=mincost,\n maxcost=maxcost,\n label=key,\n style=plotstyle[key] + \"-\",\n fig=fig,\n )" }, { "cell_type": "markdown", @@ -441,4 +423,4 @@ }, "nbformat": 4, "nbformat_minor": 5 -} +} \ No newline at end of file diff --git a/examples/MaxCut/ComparisonPostProcessing.ipynb b/examples/MaxCut/ComparisonPostProcessing.ipynb index e4671db..a1d965d 100644 --- a/examples/MaxCut/ComparisonPostProcessing.ipynb +++ b/examples/MaxCut/ComparisonPostProcessing.ipynb @@ -139,7 +139,7 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "Compare expectation value, with and without post-processing." + "Compare energy, with and without post-processing." ] }, { @@ -158,9 +158,9 @@ } ], "source": [ - "print(f\"Expected value at depth {maxdepth} without post_processing: {qaoa_post.get_Exp(maxdepth)}\")\n", - "print(f\"Expected value at depth {maxdepth} with post-processing: {qaoa_post.Exp_post_processed}\")\n", - "print(f\"Precalculated minimum cost: {mincost}\")" + "print(f\"Expected value at depth {maxdepth} without post_processing: {qaoa_post.get_energy(maxdepth)}\")\n", + "print(f\"Energy at depth {maxdepth} with post-processing: {qaoa_post.Energy_post_processed}\")\n", + "print(f\"Precalculated minimum objective value: {mincost}\")" ] }, { @@ -275,7 +275,7 @@ "plt.plot(x, [mincost]*maxdepth, label=\"True\")\n", "plt.legend()\n", "plt.xlabel(\"depth\")\n", - "plt.ylabel(\"expectation value\")\n", + "plt.ylabel(\"energy\")\n", "plt.show()" ] } diff --git a/examples/MaxCut/ConstrainedCase.ipynb b/examples/MaxCut/ConstrainedCase.ipynb index 6914c05..780a3b7 100644 --- a/examples/MaxCut/ConstrainedCase.ipynb +++ b/examples/MaxCut/ConstrainedCase.ipynb @@ -72,7 +72,7 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "Precomputed maximum value of the constrained cost function." + "Precomputed maximum value of the constrained objective function." ] }, { @@ -335,8 +335,8 @@ } ], "source": [ - "p = np.arange(1, len(np.array(qaoa.get_Exp())) + 1)\n", - "plt.plot(p, -np.array(qaoa.get_Exp()) / maxval, marker=\"o\")\n", + "p = np.arange(1, len(np.array(qaoa.get_energy())) + 1)\n", + "plt.plot(p, -np.array(qaoa.get_energy()) / maxval, marker=\"o\")\n", "plt.xlabel(\"depth\")\n", "plt.ylabel(\"Approx. ratio\")\n", "plt.title(\"LX-QAOA\")\n", diff --git a/examples/MaxCut/FixOneQubit.ipynb b/examples/MaxCut/FixOneQubit.ipynb index 7d5b824..c02b4a7 100644 --- a/examples/MaxCut/FixOneQubit.ipynb +++ b/examples/MaxCut/FixOneQubit.ipynb @@ -7,7 +7,7 @@ "source": [ "## Utilize flip symmetry to remove node with highest degree\n", "\n", - "The MaxKcut problem exhibits inherent symmetry concerning the flexibility of relabeling partitions. One approach is to assign the value 0 to a node in set A and 1 if it belongs to set B, or vice versa. This interchange does not alter the cost function associated with the partition. Therefore, we can deduce that the cost function remains invariant when the entire string is flipped.\n", + "The MaxKcut problem exhibits inherent symmetry concerning the flexibility of relabeling partitions. One approach is to assign the value 0 to a node in set A and 1 if it belongs to set B, or vice versa. This interchange does not alter the objective function associated with the partition. Therefore, we can deduce that the objective function remains invariant when the entire string is flipped.\n", "\n", "\n", "$C(x)=\\sum_{ij}w_{ij}x_{i}(1-x_{j})=\\sum_{ij}w_{ij}(1-x_{i})x_{j}$\n", @@ -16,7 +16,7 @@ "\n", "$H_{P}=\\sum_{ij}w_{ij}\\sigma_{i}^{z}\\sigma_{j}^{z}$\n", "\n", - "and it can be expressed considering the same symmetry: if one flips all the spins the energy cost should remain the same.\n", + "and it can be expressed considering the same symmetry: if one flips all the spins the energy should remain the same.\n", "\n", "The flip operator can be expressed as $M=\\otimes_{i} \\sigma_{i}^{x}$ and we can check that\n", "\n", @@ -136,7 +136,7 @@ " problem=problem,\n", " mixer=mixers.X()\n", " )\n", - " mincost, maxcost = problem.computeMinMaxCosts()\n", + " mincost, maxcost = problem.objective_bounds()\n", " print(mincost,maxcost)\n", " qaoa[fix_one_node].sample_cost_landscape(\n", " angles={\"gamma\": [0, np.pi / 2, 10], \"beta\": [0, np.pi / 2, 10]}\n", @@ -148,7 +148,7 @@ "id": "3ae9ce75-30ca-499d-b7cf-35fc1936a9e6", "metadata": {}, "source": [ - "The cost landscape is sampled using angles $\\gamma, \\beta$ $\\in$ $[0, \\frac{\\pi}{2}]$ on a $10\\times10$ grid." + "The energy landscape is sampled using angles $\\gamma, \\beta$ $\\in$ $[0, \\frac{\\pi}{2}]$ on a $10\\times10$ grid." ] }, { @@ -180,7 +180,7 @@ ], "source": [ "for fix_one_node in [False, True]:\n", - " _=utils.plot_E(qaoa[fix_one_node], title=\"Expectation value fixed node=\"+str(fix_one_node))" + " _=utils.plot_E(qaoa[fix_one_node], title=\"Energy fixed node=\"+str(fix_one_node))" ] }, { diff --git a/examples/MaxCut/MixerComparison.ipynb b/examples/MaxCut/MixerComparison.ipynb index f0b2c66..422f41d 100644 --- a/examples/MaxCut/MixerComparison.ipynb +++ b/examples/MaxCut/MixerComparison.ipynb @@ -26,7 +26,7 @@ "\n", "At depth p=1, a **2-D grid search** over (γ, β) is performed using the *vanilla (symmetric) subspace*: all gamma parameters for the layer are set equal to the grid γ value and all beta parameters to the grid β value. This gives the same warm-start landscape as vanilla QAOA regardless of how many parameters the ansatz has.\n", "\n", - "At depth p>1, the **interpolation heuristic (INTERP)** works well for vanilla QAOA but gives non-monotonic approximation ratios for free/orbit ansätze. Instead, free and orbit use a **layer-by-layer grid search** (`interpolate=False`): the best parameters from depth p−1 are *locked*, and a new 2-D grid search is performed over the new layer's parameters (again in the vanilla subspace). Because the grid includes (γ=0, β=0), which adds an identity layer and reproduces the depth-(p−1) result, the search guarantees `cost(p) ≤ cost(p−1)`, ensuring a **monotonically increasing approximation ratio**." + "At depth p>1, the **interpolation heuristic (INTERP)** works well for vanilla QAOA but gives non-monotonic approximation ratios for free/orbit ansätze. Instead, free and orbit use a **layer-by-layer grid search** (`interpolate=False`): the best parameters from depth p−1 are *locked*, and a new 2-D grid search is performed over the new layer's parameters (again in the vanilla subspace). Because the grid includes (γ=0, β=0), which adds an identity layer and reproduces the depth-(p−1) result, the search guarantees `energy(p) ≤ energy(p−1)`, ensuring a **monotonically increasing approximation ratio**." ] }, { @@ -129,7 +129,7 @@ "source": [ "### Optimal Max Cut\n", "\n", - "Brute-force the minimum QAOA cost (= maximum cut value) for reference." + "Brute-force the minimum QAOA energy (= maximum cut value) for reference." ] }, { @@ -139,7 +139,7 @@ "outputs": [], "source": [ "problem_ref = problems.MaxCut(G)\n", - "mincost, maxcost = problem_ref.computeMinMaxCosts()" + "mincost, maxcost = problem_ref.objective_bounds()" ] }, { @@ -274,7 +274,7 @@ "source": [ "### Energy Landscape at Depth 1 (Vanilla)\n", "\n", - "Sample the cost landscape over the (γ, β) grid for the vanilla instance. \n", + "Sample the energy landscape over the (γ, β) grid for the vanilla instance. \n", "The grid search uses the vanilla (symmetric) subspace, so the best grid point\n", "is a valid warm-start for all three ansätze." ] diff --git a/examples/MaxCut/OverlapInitialState.ipynb b/examples/MaxCut/OverlapInitialState.ipynb index 9a97631..8a0496d 100644 --- a/examples/MaxCut/OverlapInitialState.ipynb +++ b/examples/MaxCut/OverlapInitialState.ipynb @@ -14,15 +14,7 @@ "id": "29afd418", "metadata": {}, "outputs": [], - "source": [ - "import matplotlib.pyplot as plt\n", - "import networkx as nx\n", - "import numpy as np\n", - "\n", - "from qiskit import QuantumCircuit, QuantumRegister\n", - "\n", - "from qaoa import QAOA, problems, mixers, initialstates" - ] + "source": "import matplotlib.pyplot as plt\nimport networkx as nx\nimport numpy as np\n\nfrom qiskit import QuantumCircuit, QuantumRegister\n\nfrom qaoa import QAOA, problems, mixers, initialstates\nfrom qaoa.utils import compute_approx_ratio" }, { "cell_type": "markdown", @@ -113,7 +105,7 @@ "outputs": [], "source": [ "problem = problems.MaxCut(G)\n", - "mincost, maxcost = problem.computeMinMaxCosts()" + "mincost, maxcost = problem.objective_bounds()" ] }, { @@ -279,21 +271,7 @@ "output_type": "display_data" } ], - "source": [ - "for i in range(depth):\n", - " exp=[]\n", - " for j in range (numpoints):\n", - " # Normalized approximation ratio for a minimization objective.\n", - " # Here mincost is the optimal (most negative) value and maxcost the worst.\n", - " # This maps exp = maxcost → 0 (worst) and exp = mincost → 1 (optimal).\n", - " # Hence we use (maxcost - exp) / (maxcost - mincost).\n", - " exp.append((maxcost - qaoa[j].get_Exp()[i] )/(maxcost-mincost))\n", - " plt.plot(overlaps,exp,label=\"p=\"+str(i+1),marker='x')\n", - "\n", - "plt.xlabel(\"overlap\")\n", - "plt.ylabel(\"approx ratio\")\n", - "plt.legend()" - ] + "source": "for i in range(depth):\n exp=[]\n for j in range (numpoints):\n exp.append(compute_approx_ratio(\n qaoa[j].get_objective()[i], mincost, maxcost,\n problem.objective_sense,\n ))\n plt.plot(overlaps,exp,label=\"p=\"+str(i+1),marker='x')\n\nplt.xlabel(\"overlap\")\nplt.ylabel(\"approx ratio\")\nplt.legend()" }, { "cell_type": "code", @@ -322,21 +300,7 @@ "output_type": "display_data" } ], - "source": [ - "depths = range(1, depth+1)\n", - "\n", - "for j in range(numpoints):\n", - " exp = []\n", - " for i in range(depth):\n", - " # Normalized approximation ratio for a minimization objective\n", - " exp.append((maxcost - qaoa[j].get_Exp()[i]) / (maxcost - mincost))\n", - "\n", - " plt.plot(depths, exp, label=f\"overlap={overlaps[j]:.2f}\", marker='x')\n", - "\n", - "plt.xlabel(\"p (QAOA depth)\")\n", - "plt.ylabel(\"approx ratio\")\n", - "plt.legend()" - ] + "source": "depths = range(1, depth+1)\n\nfor j in range(numpoints):\n exp = []\n for i in range(depth):\n exp.append(compute_approx_ratio(\n qaoa[j].get_objective()[i], mincost, maxcost,\n problem.objective_sense,\n ))\n\n plt.plot(depths, exp, label=f\"overlap={overlaps[j]:.2f}\", marker='x')\n\nplt.xlabel(\"p (QAOA depth)\")\nplt.ylabel(\"approx ratio\")\nplt.legend()" } ], "metadata": { @@ -360,4 +324,4 @@ }, "nbformat": 4, "nbformat_minor": 5 -} +} \ No newline at end of file diff --git a/examples/MaxCut/WithFlip.ipynb b/examples/MaxCut/WithFlip.ipynb index 87b9080..79cc43b 100644 --- a/examples/MaxCut/WithFlip.ipynb +++ b/examples/MaxCut/WithFlip.ipynb @@ -43,11 +43,7 @@ "output_type": "display_data" } ], - "source": [ - "G = nx.read_gml(\"data/w_ba_n21_k4_0.gml\") # Load graph data for 21-node Barabasi-Albert graph with 4 edges per node\n", - "nx.draw_networkx(G)\n", - "mincost=-25.23404480588015 # Precalculated minimum cost (for comparison)" - ] + "source": "G = nx.read_gml(\"data/w_ba_n21_k4_0.gml\") # Load graph data for 21-node Barabasi-Albert graph with 4 edges per node\nnx.draw_networkx(G)\nmincost = 0 # worst feasible objective (no edges cut)\nmaxcost = 25.23404480588015 # precalculated optimal MaxCut value" }, { "cell_type": "markdown", @@ -158,29 +154,7 @@ "output_type": "display_data" } ], - "source": [ - "import matplotlib.pyplot as plt\n", - "\n", - "fig = plt.figure()\n", - "utils.plot_ApproximationRatio(\n", - " qaoa,\n", - " maxdepth,\n", - " mincost=-25.23404480588015,\n", - " maxcost=0,\n", - " label=\"QAOA vanilla\",\n", - " style=\"o--b\",\n", - " fig=fig,\n", - ")\n", - "utils.plot_ApproximationRatio(\n", - " qaoa_flip,\n", - " maxdepth,\n", - " mincost=-25.23404480588015,\n", - " maxcost=0,\n", - " label=\"QAOA flip\",\n", - " style=\"x--k\",\n", - " fig=fig,\n", - ")" - ] + "source": "import matplotlib.pyplot as plt\n\nfig = plt.figure()\nutils.plot_ApproximationRatio(\n qaoa,\n maxdepth,\n mincost=mincost,\n maxcost=maxcost,\n label=\"QAOA vanilla\",\n style=\"o--b\",\n fig=fig,\n)\nutils.plot_ApproximationRatio(\n qaoa_flip,\n maxdepth,\n mincost=mincost,\n maxcost=maxcost,\n label=\"QAOA flip\",\n style=\"x--k\",\n fig=fig,\n)" } ], "metadata": { @@ -204,4 +178,4 @@ }, "nbformat": 4, "nbformat_minor": 4 -} +} \ No newline at end of file diff --git a/examples/PortfolioOptimization/PortOpt.ipynb b/examples/PortfolioOptimization/PortOpt.ipynb index 5efb617..f785a3f 100644 --- a/examples/PortfolioOptimization/PortOpt.ipynb +++ b/examples/PortfolioOptimization/PortOpt.ipynb @@ -224,7 +224,7 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "Sample and plot the cost landscape for the different methods" + "Sample and plot the energy landscape for the different methods" ] }, { @@ -257,7 +257,7 @@ ], "source": [ "qaoa_penalty[1].sample_cost_landscape(angles={\"gamma\": [0, 2* np.pi, 10], \"beta\": [0, 2*np.pi, 10]})\n", - "fig=utils.plot_E(qaoa_penalty[1], title=\"Exp penalty method\")" + "fig=utils.plot_E(qaoa_penalty[1], title=\"Energy penalty method\")" ] }, { @@ -290,7 +290,7 @@ ], "source": [ "qaoa_penalty[0.1].sample_cost_landscape(angles={\"gamma\": [0, 2* np.pi, 10], \"beta\": [0, 2*np.pi, 10]})\n", - "fig=utils.plot_E(qaoa_penalty[0.1], title=\"Exp penalty method\")" + "fig=utils.plot_E(qaoa_penalty[0.1], title=\"Energy penalty method\")" ] }, { @@ -323,7 +323,7 @@ ], "source": [ "qaoa_XYmixer_ring[1].sample_cost_landscape(angles={\"gamma\": [0, 2* np.pi, 10], \"beta\": [0, 2*np.pi, 10]})\n", - "fig=utils.plot_E(qaoa_XYmixer_ring[1], title=\"Exp XY mixer ring\")" + "fig=utils.plot_E(qaoa_XYmixer_ring[1], title=\"Energy XY mixer ring\")" ] }, { @@ -356,7 +356,7 @@ ], "source": [ "qaoa_XYmixer_ring[0.1].sample_cost_landscape(angles={\"gamma\": [0, 2* np.pi, 10], \"beta\": [0, 2*np.pi, 10]})\n", - "fig=utils.plot_E(qaoa_XYmixer_ring[0.1], title=\"Exp XY mixer ring\")" + "fig=utils.plot_E(qaoa_XYmixer_ring[0.1], title=\"Energy XY mixer ring\")" ] }, { @@ -389,7 +389,7 @@ ], "source": [ "qaoa_Grover[1].sample_cost_landscape(angles={\"gamma\": [0, 2* np.pi, 10], \"beta\": [0, 2*np.pi, 10]})\n", - "fig=utils.plot_E(qaoa_Grover[1], title=\"Exp Grover mixer\")" + "fig=utils.plot_E(qaoa_Grover[1], title=\"Energy Grover mixer\")" ] }, { @@ -422,7 +422,7 @@ ], "source": [ "qaoa_Grover[0.1].sample_cost_landscape(angles={\"gamma\": [0, 2* np.pi, 10], \"beta\": [0, 2*np.pi, 10]})\n", - "fig=utils.plot_E(qaoa_Grover[0.1], title=\"Exp Grover mixer\")" + "fig=utils.plot_E(qaoa_Grover[0.1], title=\"Energy Grover mixer\")" ] }, { @@ -532,7 +532,7 @@ } ], "source": [ - "min_cost, max_cost = qaoa_Grover[1].problem.computeMinMaxCosts()\n", + "min_cost, max_cost = qaoa_Grover[1].problem.objective_bounds()\n", "min_cost, max_cost" ] }, diff --git a/examples/QUBO/ValidateCircuitQUBO.ipynb b/examples/QUBO/ValidateCircuitQUBO.ipynb index ea7979c..9496ea2 100644 --- a/examples/QUBO/ValidateCircuitQUBO.ipynb +++ b/examples/QUBO/ValidateCircuitQUBO.ipynb @@ -7,7 +7,7 @@ "source": [ "# Validating QUBO Circuit\n", "\n", - "Here, we aim to validate that the quantum circuit used to represent a generic Quadratic Unconstrained Binary Optimization (QUBO) problem is indeed equivalent to the corresponding cost function." + "Here, we aim to validate that the quantum circuit used to represent a generic Quadratic Unconstrained Binary Optimization (QUBO) problem is indeed equivalent to the corresponding energy function." ] }, { @@ -99,11 +99,11 @@ "n = 3\n", "for i in range(2**n):\n", " bs = bitstring(i, n)\n", - " print(bs, qubo_problem.cost(bs), qubo_problem_comb.cost(bs))\n", + " print(bs, qubo_problem.objective_value(bs), qubo_problem_comb.objective_value(bs))\n", "\n", "for i in range(2**2):\n", " bs = bitstring(i, 2)\n", - " print(bs, qubo_problem2.cost(bs), qubo_problem2_full.cost(bs))" + " print(bs, qubo_problem2.objective_value(bs), qubo_problem2_full.objective_value(bs))" ] }, { diff --git a/qaoa/problems/base_problem.py b/qaoa/problems/base_problem.py index d6582ba..0593ed7 100644 --- a/qaoa/problems/base_problem.py +++ b/qaoa/problems/base_problem.py @@ -1,7 +1,6 @@ from abc import ABC, abstractmethod from enum import Enum import itertools -import warnings from qaoa.utils import validation @@ -11,6 +10,10 @@ class ObjectiveSense(str, Enum): MAXIMIZE = "maximize" +# Sentinel used to detect when objective_sense is omitted entirely. +_SENSE_NOT_SET = object() + + class BaseProblem(ABC): """ Base class for defining optimization problems. @@ -39,15 +42,16 @@ class Problem(BaseProblem): Abstract subclass for defining specific optimization problems. This abstract subclass of `BaseProblem` is meant for defining concrete - optimization problems. Subclasses of `Problem` must implement the `cost` - and `create_circuit` methods to define the problem's cost function and - create the associated quantum circuit. + optimization problems. Subclasses of `Problem` must implement the + `objective_value` and `create_circuit` methods to define the natural + objective function and the associated quantum circuit. Attributes: circuit (QuantumCircuit): The quantum circuit associated with the problem. Methods: - cost(string): Abstract method to calculate the cost of a solution. + objective_value(string): Calculate the natural objective value of a + solution. create_circuit(): Abstract method to create the quantum circuit representing the problem. isFeasible(string): Checks if a given solution string is feasible. @@ -57,14 +61,14 @@ class Problem(BaseProblem): corresponds to the given cost function. Note: - Subclasses of `Problem` must provide implementations for the `cost` - and `create_circuit` methods. + Subclasses of `Problem` must provide implementations for the + `objective_value` and `create_circuit` methods. Example: ```python class MyProblem(Problem): - def cost(self, string): - # Define the cost calculation for the optimization problem. + def objective_value(self, string): + # Define the objective calculation for the optimization problem. ... def create_circuit(self): @@ -73,8 +77,13 @@ def create_circuit(self): ``` """ - def __init__(self, objective_sense: ObjectiveSense = ObjectiveSense.MINIMIZE) -> None: + def __init__(self, objective_sense=_SENSE_NOT_SET) -> None: super().__init__() + if objective_sense is _SENSE_NOT_SET: + raise ValueError( + "objective_sense must be provided explicitly when constructing a Problem. " + f"Pass one of: {[s.value for s in ObjectiveSense]}" + ) if not isinstance(objective_sense, ObjectiveSense): try: objective_sense = ObjectiveSense(objective_sense) @@ -85,26 +94,12 @@ def __init__(self, objective_sense: ObjectiveSense = ObjectiveSense.MINIMIZE) -> ) from exc self.objective_sense = objective_sense - def _has_objective_value_override(self) -> bool: - return self.__class__.objective_value is not Problem.objective_value - - def _has_legacy_cost_override(self) -> bool: - return self.__class__.cost is not Problem.cost - + @abstractmethod def objective_value(self, string): - if self._has_legacy_cost_override(): - warnings.warn( - "Implement objective_value() and objective_sense for custom problems. " - "Legacy cost() fallback is deprecated.", - DeprecationWarning, - stacklevel=2, - ) - return self.cost(string) - raise NotImplementedError("Subclasses must implement objective_value().") + """Return the natural objective value for a candidate solution bitstring.""" + raise NotImplementedError def energy(self, string): - if not self._has_objective_value_override() and self._has_legacy_cost_override(): - return -self.cost(string) value = self.objective_value(string) if self.objective_sense is ObjectiveSense.MINIMIZE: return value @@ -115,18 +110,6 @@ def objective_from_energy(self, energy): return energy return -energy - def score(self, string): - return -self.energy(string) - - def cost(self, string): - warnings.warn( - "cost() is deprecated and kept for backward compatibility. " - "Use objective_value() or energy(). cost(x) == -energy(x).", - DeprecationWarning, - stacklevel=2, - ) - return self.score(string) - @abstractmethod def create_circuit(self): """ @@ -164,17 +147,6 @@ def get_num_parameters(self): """ return 1 - def computeMinMaxCosts(self): - """ - Deprecated wrapper for objective_bounds(). Kept for backward compatibility. - """ - warnings.warn( - "computeMinMaxCosts() is deprecated. Use objective_bounds().", - DeprecationWarning, - stacklevel=2, - ) - return self.objective_bounds() - def objective_bounds(self): min_objective = float("inf") max_objective = float("-inf") @@ -203,7 +175,7 @@ def energy_bounds(self): def validate_circuit(self, t=1, flip=True, atol=1e-8, rtol=1e-8): """ - Exact check that the problem's circuit represents the problem's cost function. + Exact check that the problem's circuit represents the problem's energy function. This tests checks that the unitary operator represented by the quantum circuit is equal to the expected matrix with diagonal elements exp(-j*t*energy(e)), diff --git a/qaoa/problems/exactcover_problem.py b/qaoa/problems/exactcover_problem.py index 12af67f..b745739 100644 --- a/qaoa/problems/exactcover_problem.py +++ b/qaoa/problems/exactcover_problem.py @@ -22,7 +22,7 @@ class ExactCover(QUBO): scale_problem (bool): To scale the problem or not. Only implemented for problems with Hamming weight, and defaults therefore to false. Methods: - cost(): Calculates the cost of a given solution. + objective_value(): Calculates the natural objective value of a given solution. create_circuit(): Creates a parameterized circuit corresponding to the cost function. isFeasible(): Checks if a given bitstring represents a feasible solution to the problem. _exactCover(): Computes the penalty for a given solution vector x, measuring how far it is from being an exact cover. diff --git a/qaoa/problems/graph_problem.py b/qaoa/problems/graph_problem.py index 55026d6..ea9e75d 100644 --- a/qaoa/problems/graph_problem.py +++ b/qaoa/problems/graph_problem.py @@ -25,7 +25,7 @@ class GraphProblem(Problem): create_circuit(): Creates a circuit for the graph problem. same_color(str1, str2): Checks if two strings map to the same color. slice_string(string): Convert a binary string to a list of labels for each node. - cost(string): Creates a cost function for the given solution. + objective_value(string): Computes the natural objective value for the given solution. """ def __init__( @@ -42,8 +42,7 @@ def __init__( the circuit size. The highest-degree node is chosen to maximize the number of entangling gates eliminated (proportional to the degree of the fixed node). """ - super().__init__() - self.objective_sense = ObjectiveSense.MAXIMIZE + super().__init__(objective_sense=ObjectiveSense.MAXIMIZE) # fixes the highest-degree node (node n-1 after relabeling) to "color1" self.fix_one_node = fix_one_node diff --git a/qaoa/problems/maxkcut_one_hot_problem.py b/qaoa/problems/maxkcut_one_hot_problem.py index f17324e..43f5075 100644 --- a/qaoa/problems/maxkcut_one_hot_problem.py +++ b/qaoa/problems/maxkcut_one_hot_problem.py @@ -20,7 +20,7 @@ class MaxKCutOneHot(Problem): Methods: binstringToLabels(string): Converts a binary string in one-hot encoding to a string of color labels for each node. - cost(string): Computes the Max k-Cut cost for a given binary string representing a coloring. + objective_value(string): Computes the Max k-Cut objective for a given binary string representing a coloring. create_circuit(): Creates the parameterized quantum circuit corresponding to the Max k-Cut cost function using one-hot encoding. """ def __init__(self, G: nx.Graph, k_cuts: int) -> None: diff --git a/qaoa/problems/portfolio_problem.py b/qaoa/problems/portfolio_problem.py index fd8acf6..8ca3a10 100644 --- a/qaoa/problems/portfolio_problem.py +++ b/qaoa/problems/portfolio_problem.py @@ -21,7 +21,7 @@ class PortfolioOptimization(QUBO): N_qubits (int): Number of assets/qubits in the problem. Methods: - cost(string): Computes the portfolio cost of a given bitstring (problem-specific, includes penalty). + objective_value(string): Computes the portfolio objective value of a given bitstring. isFeasible(string): Checks if a given bitstring satisfies the budget constraint. __str2np(s): Converts a bitstring to a numpy array of integers. """ diff --git a/qaoa/problems/qubo_problem.py b/qaoa/problems/qubo_problem.py index c48ba52..db2edb1 100644 --- a/qaoa/problems/qubo_problem.py +++ b/qaoa/problems/qubo_problem.py @@ -29,7 +29,7 @@ class QUBO(Problem): QUBO_b (float): The scalar offset. Methods: - cost(string): Computes the cost of a given binary string according to the QUBO formulation. + objective_value(string): Computes the natural objective of a given binary string according to the QUBO formulation. create_circuit(): Creates a parametrized quantum circuit corresponding to the cost function of the QUBO problem. createParameterizedCostCircuitTril(): Creates a parameterized circuit of the triangularized QUBO problem. """ diff --git a/qaoa/qaoa.py b/qaoa/qaoa.py index bb56762..66544f8 100644 --- a/qaoa/qaoa.py +++ b/qaoa/qaoa.py @@ -1,5 +1,4 @@ import structlog -import warnings LOG = structlog.get_logger(file=__name__) @@ -35,24 +34,23 @@ class OptResult: Attributes: depth (int): The depth p of the optimization. angles (list): List of angles used in the optimization. - Exp (list): List of expected values. + energy_history (list): List of expected energies. Var (list): List of variances. - WorstCost (list): List of worst costs. - BestCost (list): List of best costs. + worst_energy_history (list): List of worst sampled energies. + best_energy_history (list): List of best sampled energies. BestSols (list): List of best solutions. shots (list): List of shots taken for each iteration. - index_Exp_min (int): Index of the minimum expected value. + best_energy_index (int): Index of the minimum expected energy. opt_time (float): Time used for optimization on given depth Methods: add_iteration(): Adds an iteration's results to the OptResult object. - compute_best_index(): Computes the index of the minimum expected value in `Exp`. - get_best_Exp(): Returns the best expected value. + compute_best_index(): Computes the index of the minimum expected energy. get_best_Var(): Returns the best variance. - get_best_angles(): Returns the best angles corresponding to the minimum expected value. + get_best_angles(): Returns the best angles corresponding to the minimum expected energy. num_fval(): Returns the number of function evaluations. num_shots(): Returns the total number of shots taken. - get_best_solution(): Returns the best solutions and their corresponding cost. + get_best_solution(): Returns the best solutions and their corresponding energy. """ def __init__(self, depth, problem): @@ -66,20 +64,17 @@ def __init__(self, depth, problem): self.problem = problem self.angles = [] - self.Exp = [] - self.energy_history = self.Exp + self.energy_history = [] self.objective_history = [] self.Var = [] - self.WorstCost = [] - self.BestCost = [] - self.best_energy = self.BestCost - self.worst_energy = self.WorstCost + self.worst_energy_history = [] + self.best_energy_history = [] self.best_objective = [] self.worst_objective = [] self.BestSols = [] self.shots = [] - self.index_Exp_min = -1 + self.best_energy_index = -1 self.opt_time = -1.0 def add_iteration(self, angles, stat, shots): @@ -93,13 +88,13 @@ def add_iteration(self, angles, stat, shots): """ self.angles.append(angles) cvar_energy = stat.get_CVaR() - self.Exp.append(cvar_energy) + self.energy_history.append(cvar_energy) self.objective_history.append(self.problem.objective_from_energy(cvar_energy)) self.Var.append(stat.get_Variance()) best_energy = stat.get_min() worst_energy = stat.get_max() - self.BestCost.append(best_energy) - self.WorstCost.append(worst_energy) + self.best_energy_history.append(best_energy) + self.worst_energy_history.append(worst_energy) self.best_objective.append(self.problem.objective_from_energy(best_energy)) self.worst_objective.append(self.problem.objective_from_energy(worst_energy)) self.BestSols.append(stat.get_min_sols()) @@ -107,44 +102,37 @@ def add_iteration(self, angles, stat, shots): def compute_best_index(self): """ - Computes the index of the minimum expected value in `Exp`. - This index is used to retrieve the best angles, expected value, and variance. + Computes the index of the minimum expected energy. + This index is used to retrieve the best angles, energy, and variance. """ - self.index_Exp_min = self.Exp.index(min(self.Exp)) - - def get_best_Exp(self): - """ - Returns: - float: The best expected value from `Exp`. - """ - return self.Exp[self.index_Exp_min] + self.best_energy_index = self.energy_history.index(min(self.energy_history)) def get_best_energy(self): - return self.get_best_Exp() + return self.energy_history[self.best_energy_index] def get_best_objective(self): - return self.objective_history[self.index_Exp_min] + return self.objective_history[self.best_energy_index] def get_best_Var(self): """ Returns: float: The best variance from `Var`. """ - return self.Var[self.index_Exp_min] + return self.Var[self.best_energy_index] def get_best_angles(self): """ Returns: - list: The best angles from `angles` at the index of the minimum expected value. + list: The best angles from `angles` at the index of the minimum expected energy. """ - return self.angles[self.index_Exp_min] + return self.angles[self.best_energy_index] def num_fval(self): """ Returns: - int: The number of function evaluations (the length of `Exp`). + int: The number of function evaluations (the length of `energy_history`). """ - return len(self.Exp) + return len(self.energy_history) def num_shots(self): """ @@ -155,15 +143,15 @@ def num_shots(self): def get_best_solution(self): """ - Iterates through the best solutions and returns the best cost and the corresponding solutions. + Iterates through the best solutions and returns the best energy and the corresponding solutions. Returns: tuple: A tuple containing - - list: The best solutions (bit-strings) that yield the best cost. - - float: The best cost found. + - list: The best solutions (bit-strings) that yield the best energy. + - float: The best energy found. """ - best_energy = np.min(self.BestCost) - iterations_with_best_cost = np.where(self.BestCost == best_energy)[0] + best_energy = np.min(self.best_energy_history) + iterations_with_best_cost = np.where(self.best_energy_history == best_energy)[0] all_best_sols = [] for i in iterations_with_best_cost: @@ -207,9 +195,9 @@ class QAOA: Main methods: - createParameterizedCircuit(): Creates the parameterized circuit for a given depth. - - sample_cost_landscape(): Samples the cost landscape for given angles at a specific depth. + - sample_cost_landscape(): Samples the energy landscape for given angles at a specific depth. - optimize(): Runs the optimization process for a given depth. - - get_Exp(): Returns the best expected value at a given depth. + - get_energy(): Returns the best expected energy at a given depth. - get_Var(): Returns the best variance at a given depth. - get_beta(): Returns the beta angles at a given depth. - get_gamma(): Returns the gamma angles at a given depth. @@ -314,14 +302,11 @@ def __init__( self.n_beta = 1 self.n_init = 0 - self.Exp_sampled_p1 = None self.Energy_sampled_p1 = None self.MinEnergy_sampled_p1 = None self.MaxEnergy_sampled_p1 = None self.landscape_p1_angles = {} self.Var_sampled_p1 = None - self.MaxCost_sampled_p1 = None - self.MinCost_sampled_p1 = None self.optimization_results = {} self.memory_lists = [] @@ -331,7 +316,6 @@ def __init__( self.bitflips = [] self.post = post - self.Exp_post_processed = None self.Energy_post_processed = None self.Var_post_processed = None self.samplecount_hists = {} @@ -349,47 +333,28 @@ def exp_landscape(self): def var_landscape(self): """ Returns: - float: The variance of the cost landscape at depth p = 1. + float: The variance of the energy landscape at depth p = 1. """ ### at depth p = 1 return self.Var_sampled_p1 - def get_Exp(self, depth=None): - """ - Deprecated alias for get_energy(). - - Args: - depth (int, optional): The depth at which to retrieve the expected value. - - Returns: - list/float: The best expected value(s) at the specified depth. - If depth is None, returns a list of best expected values for all depths up to the current depth. - If depth is specified, returns the best expected value at that depth. - """ - warnings.warn( - "get_Exp() is deprecated. Use get_energy().", - DeprecationWarning, - stacklevel=2, - ) - return self.get_energy(depth=depth) - def get_energy(self, depth=None): - if not depth: + if depth is None: ret = [] for i in range(1, self.current_depth + 1): ret.append(self.optimization_results[i].get_best_energy()) return ret - if depth > self.current_depth + 1: + if depth < 1 or depth > self.current_depth: raise ValueError return self.optimization_results[depth].get_best_energy() def get_objective(self, depth=None): - if not depth: + if depth is None: ret = [] for i in range(1, self.current_depth + 1): ret.append(self.optimization_results[i].get_best_objective()) return ret - if depth > self.current_depth + 1: + if depth < 1 or depth > self.current_depth: raise ValueError return self.optimization_results[depth].get_best_objective() @@ -567,7 +532,7 @@ def sample_cost_landscape( self, angles={"gamma": [0, 2 * np.pi, 20], "beta": [0, 2 * np.pi, 20]} ): """ - Evaluates the cost function (landscape) at depth p = 1 for a grid of angles. + Evaluates the energy landscape at depth p = 1 for a grid of angles. The grid search is performed in the *vanilla (symmetric) subspace*: for every ``(gamma, beta)`` grid point, **all** gamma parameters for that layer are set @@ -578,9 +543,9 @@ def sample_cost_landscape( and multi-angle ansätze. **Sequential mode**: If `sequential` is set to True, CVaR, variance and - max/min cost are calculated sequentially for each (gamma, beta) combination - and stored in `Exp_sampled_p1`, `Var_sampled_p1`, `MaxCost_sampled_p1`, - and `MinCost_sampled_p1`. + max/min energy are calculated sequentially for each (gamma, beta) + combination and stored in `Energy_sampled_p1`, `Var_sampled_p1`, + `MaxEnergy_sampled_p1`, and `MinEnergy_sampled_p1`. **Batch mode**: If `sequential` is set to False, all parameter binds are prepared and a single batched job is submitted to the backend. @@ -661,7 +626,6 @@ def sample_cost_landscape( self.Energy_sampled_p1 = np.array(expectations).reshape( angles["beta"][2], angles["gamma"][2] ) - self.Exp_sampled_p1 = self.Energy_sampled_p1 self.Var_sampled_p1 = np.array(variances).reshape( angles["beta"][2], angles["gamma"][2] ) @@ -671,8 +635,6 @@ def sample_cost_landscape( self.MinEnergy_sampled_p1 = np.array(min_energies).reshape( angles["beta"][2], angles["gamma"][2] ) - self.MaxCost_sampled_p1 = self.MinEnergy_sampled_p1 - self.MinCost_sampled_p1 = self.MaxEnergy_sampled_p1 logger.info("Done measurement") else: self.createParameterizedCircuit(depth) @@ -721,11 +683,15 @@ def measurementStatistics(self, job): # :return: expectation and variance, if the job is a list # """ """ - Processes the results of a job to extract measurement statistics -- CVar, variance and max/min costs. + Processes the results of a job to extract measurement statistics -- + CVar, variance and max/min energies. Extracts the job result and retrieves the counts of measurement outcomes (stored in `last_hist` for future reference), determining whether the result is a dictionary (single execution) or a list of dictionaries (batched executions with parameter sweeps). - - If the result is a list of dictionaries, it computes the measurement statistics for each set of counts, which are stored in `Exp_sampled_p1`, `Var_sampled_p1`, `MaxCost_sampled_p1`, and `MinCost_sampled_p1`. + - If the result is a list of dictionaries, it computes the measurement + statistics for each set of counts, which are stored in + `Energy_sampled_p1`, `Var_sampled_p1`, `MaxEnergy_sampled_p1`, and + `MinEnergy_sampled_p1`. - If the result is a single dictionary, it computes the measurement statistics for that single set of counts, storing the results in `stat`. The function also handles the memory of measurement outcomes and stores them in `memory_lists` if memory is enabled @@ -771,7 +737,6 @@ def measurementStatistics(self, job): self.Energy_sampled_p1 = np.array(expectations).reshape( angles["beta"][2], angles["gamma"][2] ) - self.Exp_sampled_p1 = self.Energy_sampled_p1 self.Var_sampled_p1 = np.array(variances).reshape( angles["beta"][2], angles["gamma"][2] ) @@ -781,8 +746,6 @@ def measurementStatistics(self, job): self.MinEnergy_sampled_p1 = np.array(min_energies).reshape( angles["beta"][2], angles["gamma"][2] ) - self.MaxCost_sampled_p1 = self.MinEnergy_sampled_p1 - self.MinCost_sampled_p1 = self.MaxEnergy_sampled_p1 else: for string in counts_list: # qiskit binary strings use little endian encoding, but our energy function expects big endian encoding. Therefore, we reverse the order @@ -841,10 +804,11 @@ def optimize( n_per_layer = n_gamma + n_beta start_time = time.perf_counter() if self.current_depth == 0: - if self.Exp_sampled_p1 is None: + if self.Energy_sampled_p1 is None: self.sample_cost_landscape(angles=angles) ind_Emin = np.unravel_index( - np.argmin(self.Exp_sampled_p1, axis=None), self.Exp_sampled_p1.shape + np.argmin(self.Energy_sampled_p1, axis=None), + self.Energy_sampled_p1.shape, ) # Build initial angles with init params (zeros) followed by first-layer params. # For multi-angle QAOA (n_gamma > 1 or n_beta > 1), the best vanilla angles @@ -865,7 +829,7 @@ def optimize( # Layer-by-layer grid search: lock previous layers at their # best angles and do a 2-D grid search over the new layer. # The grid includes (0, 0) which is equivalent to the depth - # p-1 circuit, so cost(p) ≤ cost(p-1) is guaranteed. + # p-1 circuit, so energy(p) ≤ energy(p-1) is guaranteed. angles0 = self._grid_search_layer(best_angles, angles) self.optimization_results[self.current_depth + 1] = OptResult( @@ -903,7 +867,6 @@ def optimize( samples = self.samplecount_hists[self.current_depth] post_processing(self, samples=samples, K=self.post) self.Energy_post_processed = self.stat.get_CVaR() - self.Exp_post_processed = self.Energy_post_processed self.Var_post_processed = self.stat.get_Variance() def local_opt(self, angles0): @@ -1077,7 +1040,7 @@ def interp(self, angles): return np.concatenate([init_part, result_layers.flatten()]) - def _eval_cost(self, angle_array): + def _eval_energy(self, angle_array): """ Evaluate the expected energy (CVaR) for a specific angle array without recording the result in ``optimization_results``. @@ -1121,7 +1084,8 @@ def _grid_search_layer(self, prev_angles, angles): Grid search over a single new layer's parameters with all previous layers locked at ``prev_angles``. - For each ``(gamma, beta)`` point on the 2-D grid the cost is evaluated + For each ``(gamma, beta)`` point on the 2-D grid the energy is + evaluated with layers ``1 … p-1`` fixed to ``prev_angles`` and the new layer ``p`` initialised in the *symmetric (vanilla) subspace*: all ``n_gamma`` gamma parameters are set equal to ``gamma`` and all @@ -1129,9 +1093,9 @@ def _grid_search_layer(self, prev_angles, angles): Because the grid always includes the point ``(0, 0)`` — which adds an identity layer and therefore reproduces the depth-``(p-1)`` circuit — - the best grid cost satisfies ``cost_grid ≤ cost(p-1)``. The + the best grid energy satisfies ``energy_grid ≤ energy(p-1)``. The subsequent full local optimisation can only improve on this, so - ``cost(p) ≤ cost(p-1)`` is guaranteed, giving a monotonically + ``energy(p) ≤ energy(p-1)`` is guaranteed, giving a monotonically increasing approximation ratio. Args: @@ -1165,7 +1129,7 @@ def _grid_search_layer(self, prev_angles, angles): f"{len(gamma_grid)}×{len(beta_grid)} points" ) - best_cost = np.inf + best_energy = np.inf best_angles = None for beta_val in beta_grid: @@ -1174,12 +1138,12 @@ def _grid_search_layer(self, prev_angles, angles): [gamma_val] * self.n_gamma + [beta_val] * self.n_beta ) candidate = np.concatenate([prev_angles, new_layer]) - cost = self._eval_cost(candidate) - if cost < best_cost: - best_cost = cost + energy = self._eval_energy(candidate) + if energy < best_energy: + best_energy = energy best_angles = candidate.copy() - logger.info(f"Layer grid search done, best energy: {best_cost:.6f}") + logger.info(f"Layer grid search done, best energy: {best_energy:.6f}") return best_angles def hist(self, angles, shots): diff --git a/qaoa/utils/__init__.py b/qaoa/utils/__init__.py index afee134..672d317 100644 --- a/qaoa/utils/__init__.py +++ b/qaoa/utils/__init__.py @@ -2,4 +2,4 @@ from .flip import BitFlip from .post import * from .graphutils import GraphHandler -from .plotroutines import plot_E, plot_Var, plot_angles, draw_colored_graph, plot_AllOptimalParameters, plot_optimalHitRatios, plot_feasibleHitRatios, printBestHistogramEntries, plotHitProbabilities, plotHitProbabilities_fromHist, plot_ApproximationRatio, plot_successprob +from .plotroutines import plot_E, plot_Var, plot_angles, draw_colored_graph, plot_AllOptimalParameters, plot_optimalHitRatios, plot_feasibleHitRatios, printBestHistogramEntries, plotHitProbabilities, plotHitProbabilities_fromHist, plot_ApproximationRatio, plot_successprob, compute_approx_ratio diff --git a/qaoa/utils/plotroutines.py b/qaoa/utils/plotroutines.py index f6382f1..e003044 100644 --- a/qaoa/utils/plotroutines.py +++ b/qaoa/utils/plotroutines.py @@ -9,6 +9,45 @@ from .statistic import Statistic +def compute_approx_ratio(value, min_objective, max_objective, sense): + """Compute the approximation ratio normalized to ``[0, 1]``. + + The ratio is defined so that it equals **1** at the optimal solution and + **0** at the worst feasible value, regardless of whether the problem is a + minimization or maximization problem. + + * **MAXIMIZE** – optimal is ``max_objective``: + ``ratio = (value - min_objective) / (max_objective - min_objective)`` + * **MINIMIZE** – optimal is ``min_objective``: + ``ratio = (max_objective - value) / (max_objective - min_objective)`` + + When ``min_objective`` and ``max_objective`` are equal (trivial landscape) + the function returns ``1.0``. + + Args: + value (float or np.ndarray): Objective value(s) to evaluate. + min_objective (float): Minimum (worst-for-max / best-for-min) objective. + max_objective (float): Maximum (best-for-max / worst-for-min) objective. + sense: Optimization sense – an :class:`~qaoa.problems.base_problem.ObjectiveSense` + instance or the strings ``"maximize"`` / ``"minimize"``. + + Returns: + float or np.ndarray: Approximation ratio in ``[0, 1]`` (clipping is + **not** applied; out-of-range values indicate inconsistent inputs). + """ + from qaoa.problems.base_problem import ObjectiveSense + + if np.isclose(max_objective, min_objective): + return np.ones_like(value, dtype=float) if hasattr(value, "__len__") else 1.0 + + sense_val = sense.value if isinstance(sense, ObjectiveSense) else str(sense) + span = max_objective - min_objective + if sense_val == "maximize": + return (value - min_objective) / span + else: + return (max_objective - value) / span + + def _np2str(npBitString): """Cast binary numpy arrays to bitstrings. @@ -108,8 +147,8 @@ def plot_ApproximationRatio( Args: qaoa_instance (QAOA): A QAOA instance that has been optimized. maxdepth (int): Maximum depth to plot. - mincost (float): Known minimum cost (for normalization). - maxcost (float): Known maximum cost (for normalization). + mincost (float): Known minimum objective value (for normalization). + maxcost (float): Known maximum objective value (for normalization). label (str): Legend label. style (str): Matplotlib line-style string. fig (matplotlib.figure.Figure, optional): Existing figure to draw on. @@ -130,12 +169,9 @@ def plot_ApproximationRatio( fig, ax = _get_fig_ax(fig) ax.hlines(1, 1, maxdepth, linestyles="solid", colors="black") - if np.isclose(maxcost, mincost): - appr_ratio = np.ones_like(exp) - elif qaoa_instance.problem.objective_sense.value == "maximize": - appr_ratio = (exp - mincost) / (maxcost - mincost) - else: - appr_ratio = (maxcost - exp) / (maxcost - mincost) + appr_ratio = compute_approx_ratio( + exp, mincost, maxcost, qaoa_instance.problem.objective_sense + ) ax.plot( np.arange(1, maxdepth + 1), @@ -200,11 +236,11 @@ def _apprrat_successprob(qaoa_instance, depth, shots=10**4): for string in hist: if qaoa_instance.problem.isFeasible(string): - cost = qaoa_instance.problem.objective_value(string) + energy = qaoa_instance.problem.energy(string) counts += hist[string] - stat.add_sample(cost, hist[string], string) + stat.add_sample(energy, hist[string], string) - return stat.get_CVaR(), counts / shots + return qaoa_instance.problem.objective_from_energy(stat.get_CVaR()), counts / shots # Keep the old private name as an alias for internal backward compatibility. __apprrat_successprob = _apprrat_successprob @@ -564,4 +600,3 @@ def prob_hit_ones(p, n): ax.legend() ax.grid(True, which="both", ls="--") return fig, ax - diff --git a/qaoa/utils/qaoaIO.py b/qaoa/utils/qaoaIO.py index cc9dc7e..68ff346 100644 --- a/qaoa/utils/qaoaIO.py +++ b/qaoa/utils/qaoaIO.py @@ -3,7 +3,7 @@ import json import numpy as np from dataclasses import dataclass, asdict, field -from typing import List, Dict, Type +from typing import List, Dict, Type, Optional from enum import Enum import os import subprocess @@ -12,6 +12,8 @@ # ---------- Utility functions ---------- +CURRENT_SCHEMA_VERSION = 3 + def _numpy_to_list(obj): """Recursively convert numpy arrays and Enums to JSON-serializable types.""" if isinstance(obj, np.ndarray): @@ -123,8 +125,8 @@ class DepthResult: optimal_angles: List[float] histogram: Dict[str, int] opt_time: float # runtime in seconds - best_energy: float | None = None - best_objective: float | None = None + best_energy: Optional[float] = None + best_objective: Optional[float] = None @dataclass @@ -143,7 +145,7 @@ class QAOAResult: problem: ProblemData qaoa_params: QAOAParameters metadata: Dict[str, str] = field(default_factory=dict) - schema_version: int = 2 + schema_version: int = CURRENT_SCHEMA_VERSION def __post_init__(self): """Automatically populate metadata if not provided.""" @@ -169,10 +171,18 @@ def load(cls, filename: str) -> "QAOAResult": with open(filename, "r") as f: data = json.load(f) + schema_version = data.get("schema_version") + if schema_version != CURRENT_SCHEMA_VERSION: + raise ValueError( + "Unsupported qaoaIO schema version. " + f"Expected {CURRENT_SCHEMA_VERSION}, got {schema_version!r}." + ) + if "objective_sense" not in data["problem"]: + raise ValueError("Serialized problem is missing required objective_sense.") + # Rebuild problem instance from its dict problem = ProblemData.from_dict(data["problem"]) - schema_version = data.get("schema_version", 1) depths_data = data["qaoa_params"]["depths"] depths = {int(k): DepthResult(**v) for k, v in depths_data.items()} @@ -272,7 +282,11 @@ def from_qaoa(cls, qaoa: QAOA, depths = depths ) - return cls(problem=problem_data, qaoa_params=qaoa_params, schema_version=2) + return cls( + problem=problem_data, + qaoa_params=qaoa_params, + schema_version=CURRENT_SCHEMA_VERSION, + ) # TODO: Implement # def generate_qaoa_object(self) -> "QAOA" diff --git a/unittests/test_maxkcut_binary_problem.py b/unittests/test_maxkcut_binary_problem.py index 4119ea3..93c1259 100644 --- a/unittests/test_maxkcut_binary_problem.py +++ b/unittests/test_maxkcut_binary_problem.py @@ -98,10 +98,10 @@ def test_MaxKCutBinaryPowerTwo(self): if binary_str_i == binary_str_j: self.assertTrue(np.isclose(inner_product, -1)) - self.assertTrue(np.isclose(problem.cost(bitstring), 0)) + self.assertTrue(np.isclose(problem.objective_value(bitstring), 0)) else: self.assertTrue(np.isclose(inner_product, 1)) - self.assertTrue(np.isclose(problem.cost(bitstring), 1)) + self.assertTrue(np.isclose(problem.objective_value(bitstring), 1)) def test_MaxKCutBinaryPowerTwo_PauliBasisequalDiffusion(self): # This tests if the Hamiltonians of "method=PauliBasis" is equal to "method=Diffusion" @@ -234,10 +234,10 @@ def test_MaxKCutBinaryFullH(self): if (binary_str_i == binary_str_j) or samecolor: self.assertTrue(np.isclose(inner_product, -1)) - self.assertTrue(np.isclose(problem.cost(bitstring), 0)) + self.assertTrue(np.isclose(problem.objective_value(bitstring), 0)) else: self.assertTrue(np.isclose(inner_product, 1)) - self.assertTrue(np.isclose(problem.cost(bitstring), 1)) + self.assertTrue(np.isclose(problem.objective_value(bitstring), 1)) def test_MaxKCutBinaryPowerTwo_PauliBasisequalDiffusion(self): # This tests if the Hamiltonians of "method=PauliBasis" is equal to "method=Diffusion" is equal to "method="PowerOfTwo" diff --git a/unittests/test_maxkcut_one_hot_problem.py b/unittests/test_maxkcut_one_hot_problem.py index 8f4e24d..80f8e05 100644 --- a/unittests/test_maxkcut_one_hot_problem.py +++ b/unittests/test_maxkcut_one_hot_problem.py @@ -52,29 +52,29 @@ def test_binstringToLabels_k3(self): msg = f"string: {binstring}, expected: {expected}, computed: {computed}" self.assertEqual(expected, computed, msg) - def test_cost_k2_barbell(self): + def test_objective_k2_barbell(self): """ Test that the cost funciton in MaxKCutBinaryOneHot is correct for k = 2 with the barbell graph. """ prob = MaxKCutOneHot(self.barbell, 2) strings = {"0101": 0, "1001": 1, "0110": 1, "1010": 0} for string, expected in strings.items(): - computed = prob.cost(string[::-1]) + computed = prob.objective_value(string[::-1]) msg = f"string: {string}, expected: {expected}, computed: {computed}" self.assertEqual(expected, computed, msg) - def test_cost_k2_three_node_graph(self): + def test_objective_k2_three_node_graph(self): """ Test that the cost funciton in MaxKCutBinaryOntHot is correct for k = 2 with three-node graph. """ prob = MaxKCutOneHot(self.three_node_graph, 2) strings = {"010101": 0, "100110": 2, "011010": 1, "101010": 0} for string, expected in strings.items(): - computed = prob.cost(string[::-1]) + computed = prob.objective_value(string[::-1]) msg = f"string: {string}, expected: {expected}, computed: {computed}" self.assertEqual(expected, computed, msg) - def test_cost_k3_three_node_graph(self): + def test_objective_k3_three_node_graph(self): """ Test that the cost funciton in MaxKCutBinaryOntHot is correct for k = 3 with three-node graph. """ @@ -88,11 +88,11 @@ def test_cost_k3_three_node_graph(self): "100100100": 0, } for string, expected in strings.items(): - computed = prob.cost(string[::-1]) + computed = prob.objective_value(string[::-1]) msg = f"string: {string}, expected: {expected}, computed: {computed}" self.assertEqual(expected, computed, msg) - def test_cost_k6_three_node_graph(self): + def test_objective_k6_three_node_graph(self): """ Test that the cost funciton in MaxKCutBinaryOntHot is correct for k = 3 with three-node graph. """ @@ -104,7 +104,7 @@ def test_cost_k6_three_node_graph(self): "000001100000010000": 2, } for string, expected in strings.items(): - computed = prob.cost(string[::-1]) + computed = prob.objective_value(string[::-1]) msg = f"string: {string}, expected: {expected}, computed: {computed}" self.assertEqual(expected, computed, msg) diff --git a/unittests/test_multiangle_gridsearch.py b/unittests/test_multiangle_gridsearch.py index 53db97c..c3afca7 100644 --- a/unittests/test_multiangle_gridsearch.py +++ b/unittests/test_multiangle_gridsearch.py @@ -29,7 +29,7 @@ def test_vanilla_grid_search_sequential(self): qaoa.sample_cost_landscape(angles=self.angles) - self.assertEqual(qaoa.Exp_sampled_p1.shape, (5, 5)) + self.assertEqual(qaoa.Energy_sampled_p1.shape, (5, 5)) def test_vanilla_grid_search_batch(self): """Vanilla QAOA (1 gamma, 1 beta) should complete grid search in batch mode.""" @@ -46,7 +46,7 @@ def test_vanilla_grid_search_batch(self): qaoa.sample_cost_landscape(angles=self.angles) - self.assertEqual(qaoa.Exp_sampled_p1.shape, (5, 5)) + self.assertEqual(qaoa.Energy_sampled_p1.shape, (5, 5)) def test_multiangle_grid_search_sequential(self): """Multi-angle QAOA should complete 2D grid search (not 20^n) in sequential mode.""" @@ -65,7 +65,7 @@ def test_multiangle_grid_search_sequential(self): qaoa.sample_cost_landscape(angles=self.angles) # Result should still be 5x5 grid regardless of parameter count - self.assertEqual(qaoa.Exp_sampled_p1.shape, (5, 5)) + self.assertEqual(qaoa.Energy_sampled_p1.shape, (5, 5)) def test_multiangle_grid_search_batch(self): """Multi-angle QAOA should complete 2D grid search (not 20^n) in batch mode.""" @@ -84,7 +84,7 @@ def test_multiangle_grid_search_batch(self): qaoa.sample_cost_landscape(angles=self.angles) # Result should still be 5x5 grid - self.assertEqual(qaoa.Exp_sampled_p1.shape, (5, 5)) + self.assertEqual(qaoa.Energy_sampled_p1.shape, (5, 5)) def test_vanilla_warmstart_initialization(self): """Multi-angle optimize should broadcast vanilla angles to all params.""" @@ -109,7 +109,7 @@ def test_layer_grid_search_depth2(self): Layer-by-layer grid search (interpolate=False) at depth 2 should: 1. Complete without error. 2. Store results at depth 2. - 3. Yield cost(2) ≤ cost(1) (monotonic) because the grid includes (0,0). + 3. Yield energy(2) ≤ energy(1) (monotonic) because the grid includes (0,0). """ G = nx.path_graph(3) problem = problems.MaxCut(G) @@ -129,11 +129,10 @@ def test_layer_grid_search_depth2(self): self.assertIn(1, qaoa.optimization_results) self.assertIn(2, qaoa.optimization_results) - cost1 = qaoa.get_Exp(depth=1) - cost2 = qaoa.get_Exp(depth=2) - # cost is negative (we minimise), so a better result is more negative. - # Monotonicity: cost(p) ≤ cost(p-1) ↔ get_Exp(2) ≤ get_Exp(1). - self.assertLessEqual(cost2, cost1 + 1e-6) # small tolerance for shot noise + energy1 = qaoa.get_energy(depth=1) + energy2 = qaoa.get_energy(depth=2) + # Energy is minimized, so a better result is more negative for MaxCut. + self.assertLessEqual(energy2, energy1 + 1e-6) # small tolerance for shot noise def test_layer_grid_search_multiangle(self): """Layer grid search should work for free (multi-angle) ansatz.""" diff --git a/unittests/test_objective_sense_migration.py b/unittests/test_objective_sense_migration.py index 4f0bcba..e837a4f 100644 --- a/unittests/test_objective_sense_migration.py +++ b/unittests/test_objective_sense_migration.py @@ -8,7 +8,7 @@ from qaoa import QAOA, initialstates, mixers, problems from qaoa.problems.base_problem import ObjectiveSense, Problem -from qaoa.utils import BitFlip, Statistic, qaoaIO +from qaoa.utils import BitFlip, Statistic, qaoaIO, compute_approx_ratio class MinToyProblem(Problem): @@ -64,8 +64,10 @@ def test_energy_objective_invariants(self): pmax = MaxToyProblem() self.assertEqual(pmin.energy("10"), pmin.objective_value("10")) self.assertEqual(pmax.energy("10"), -pmax.objective_value("10")) - self.assertEqual(pmin.cost("10"), -pmin.energy("10")) - self.assertEqual(pmax.cost("10"), -pmax.energy("10")) + + def test_removed_compatibility_apis(self): + self.assertFalse(hasattr(MinToyProblem(), "cost")) + self.assertFalse(hasattr(MinToyProblem(), "computeMinMaxCosts")) def test_maxcut_objective_and_energy(self): G = nx.Graph() @@ -150,7 +152,7 @@ def test_objective_and_energy_bounds(self): self.assertEqual(pmin.optimal_objective(), 0.0) self.assertEqual(pmax.optimal_objective(), 2.0) - def test_qaoa_get_exp_alias_and_energy(self): + def test_qaoa_energy_and_objective(self): from qiskit_aer import AerSimulator G = nx.path_graph(3) @@ -164,7 +166,7 @@ def test_qaoa_get_exp_alias_and_energy(self): shots=128, ) q.optimize(depth=1, angles={"gamma": [0, np.pi, 3], "beta": [0, np.pi, 3]}) - self.assertAlmostEqual(q.get_Exp(depth=1), q.get_energy(depth=1)) + self.assertFalse(hasattr(q, "get_Exp")) self.assertAlmostEqual( q.get_objective(depth=1), -q.get_energy(depth=1) ) @@ -190,10 +192,38 @@ def test_serialization_roundtrip_preserves_objective_sense(self): with tempfile.NamedTemporaryFile(suffix=".json") as fp: result.save(fp.name) loaded = qaoaIO.QAOAResult.load(fp.name) - self.assertEqual(loaded.schema_version, 2) + self.assertEqual(loaded.schema_version, 3) self.assertEqual(loaded.problem.objective_sense, "minimize") + def test_legacy_serialization_is_rejected(self): + legacy_payload = { + "problem": { + "problem_type": "ExactCover", + "columns": [[1, 0], [0, 1]], + "weights": [1.0, 2.0], + "solution": [1, 1], + "hamming_weight": 1, + }, + "qaoa_params": { + "cvar": 0.5, + "init_method": "PLUS", + "mixer_method": "X", + "backend": "sim", + "optimizer": "COBYLA", + "N_qubits": 2, + "depths": {"1": {"optimal_angles": [0.1, 0.2], "histogram": {"00": 1}, "opt_time": 0.01}}, + }, + } + with tempfile.NamedTemporaryFile(mode="w", suffix=".json") as fp: + import json + + json.dump(legacy_payload, fp) + fp.flush() + with self.assertRaises(ValueError): + qaoaIO.QAOAResult.load(fp.name) + def test_approximation_ratio_mapping_formula(self): + """Legacy inline-formula tests – kept for regression.""" best, worst = 10.0, 2.0 value_best, value_worst = 10.0, 2.0 ratio_max_best = (value_best - worst) / (best - worst) @@ -207,3 +237,46 @@ def test_approximation_ratio_mapping_formula(self): ratio_min_worst = (worst - value_worst) / (worst - best) self.assertAlmostEqual(ratio_min_best, 1.0) self.assertAlmostEqual(ratio_min_worst, 0.0) + + def test_compute_approx_ratio_maximize(self): + # best value = max_objective (10), worst = min_objective (2) + self.assertAlmostEqual(compute_approx_ratio(10.0, 2.0, 10.0, ObjectiveSense.MAXIMIZE), 1.0) + self.assertAlmostEqual(compute_approx_ratio(2.0, 2.0, 10.0, ObjectiveSense.MAXIMIZE), 0.0) + self.assertAlmostEqual(compute_approx_ratio(6.0, 2.0, 10.0, ObjectiveSense.MAXIMIZE), 0.5) + # also accepts string sense + self.assertAlmostEqual(compute_approx_ratio(10.0, 2.0, 10.0, "maximize"), 1.0) + + def test_compute_approx_ratio_minimize(self): + # best value = min_objective (2), worst = max_objective (10) + self.assertAlmostEqual(compute_approx_ratio(2.0, 2.0, 10.0, ObjectiveSense.MINIMIZE), 1.0) + self.assertAlmostEqual(compute_approx_ratio(10.0, 2.0, 10.0, ObjectiveSense.MINIMIZE), 0.0) + self.assertAlmostEqual(compute_approx_ratio(6.0, 2.0, 10.0, ObjectiveSense.MINIMIZE), 0.5) + # also accepts string sense + self.assertAlmostEqual(compute_approx_ratio(2.0, 2.0, 10.0, "minimize"), 1.0) + + def test_compute_approx_ratio_trivial_landscape(self): + self.assertAlmostEqual(compute_approx_ratio(5.0, 5.0, 5.0, ObjectiveSense.MAXIMIZE), 1.0) + self.assertAlmostEqual(compute_approx_ratio(5.0, 5.0, 5.0, ObjectiveSense.MINIMIZE), 1.0) + + def test_compute_approx_ratio_array(self): + values = np.array([2.0, 6.0, 10.0]) + expected_max = np.array([0.0, 0.5, 1.0]) + expected_min = np.array([1.0, 0.5, 0.0]) + np.testing.assert_allclose( + compute_approx_ratio(values, 2.0, 10.0, ObjectiveSense.MAXIMIZE), expected_max + ) + np.testing.assert_allclose( + compute_approx_ratio(values, 2.0, 10.0, ObjectiveSense.MINIMIZE), expected_min + ) + + def test_missing_sense_raises(self): + """Problem.__init__ must raise ValueError when objective_sense is omitted.""" + with self.assertRaises(ValueError) as ctx: + # Anonymous concrete Problem subclass – no sense provided. + class _NakedProblem(Problem): + def objective_value(self, s): + return 0.0 + def create_circuit(self): + pass + _NakedProblem() + self.assertIn("objective_sense", str(ctx.exception)) diff --git a/unittests/test_problems.py b/unittests/test_problems.py index 38729fb..b0952eb 100644 --- a/unittests/test_problems.py +++ b/unittests/test_problems.py @@ -2,10 +2,10 @@ Unit tests for problem classes. Covers: -- ExactCover: cost, isFeasible, create_circuit -- QUBO: cost, create_circuit, lower-triangular -- PortfolioOptimization: cost, isFeasible, penalty behavior -- MaxKCutOneHot: binstringToLabels, cost, create_circuit +- ExactCover: objective/energy, isFeasible, create_circuit +- QUBO: objective/energy, create_circuit, lower-triangular +- PortfolioOptimization: objective/energy, isFeasible, penalty behavior +- MaxKCutOneHot: binstringToLabels, objective, create_circuit """ import math @@ -27,23 +27,23 @@ def _make_columns(self): [0, 0, 1], ]) - def test_exact_cover_cost_feasible(self): + def test_exact_cover_objective_feasible(self): from qaoa.problems import ExactCover # Exact cover: subsets 0 and 2 cover all elements exactly once # columns[:,0] = {0,1}, columns[:,2] = {2,3} columns = self._make_columns() problem = ExactCover(columns) # "101": use subsets 0 and 2 → covers elements {0,1} and {2,3} exactly once - cost = problem.cost("101") - self.assertAlmostEqual(cost, 0.0) + objective = problem.objective_value("101") + self.assertAlmostEqual(objective, 0.0) - def test_exact_cover_cost_infeasible(self): + def test_exact_cover_objective_infeasible(self): from qaoa.problems import ExactCover columns = self._make_columns() problem = ExactCover(columns) # "000": no subsets selected → elements uncovered → penalty > 0 - cost = problem.cost("000") - self.assertLess(cost, 0.0) + objective = problem.objective_value("000") + self.assertGreater(objective, 0.0) def test_exact_cover_is_feasible_true(self): from qaoa.problems import ExactCover @@ -78,9 +78,8 @@ def test_exact_cover_with_weights(self): columns = self._make_columns() weights = np.array([1.0, 1.0, 1.0]) problem = ExactCover(columns, weights=weights) - # Feasible solution "101" should have non-negative cost (weights shift it) - cost = problem.cost("101") - self.assertIsInstance(cost, (float, np.floating)) + objective = problem.objective_value("101") + self.assertIsInstance(objective, (float, np.floating)) class TestQUBO(unittest.TestCase): @@ -92,35 +91,28 @@ def _make_simple_qubo(self): Q = np.diag([1.0, -1.0]) return QUBO(Q=Q) - def test_qubo_cost_zeros(self): + def test_qubo_objective_zeros(self): problem = self._make_simple_qubo() - # x = [0, 0] → cost = 0 - self.assertAlmostEqual(problem.cost("00"), 0.0) + self.assertAlmostEqual(problem.objective_value("00"), 0.0) - def test_qubo_cost_minimizer(self): + def test_qubo_objective_minimizer(self): problem = self._make_simple_qubo() - # x = [0, 1] → cost = -(-1) = 1... wait, we want min x0 - x1 - # QUBO.cost returns -(x^T Q x) because we maximize - # Q = diag(1, -1), so x^T Q x = x0 - x1 - # cost("01") = -(0 - 1) = 1 (best: we want minimum cost = most negative) - cost_01 = problem.cost("01") - cost_10 = problem.cost("10") - cost_11 = problem.cost("11") - cost_00 = problem.cost("00") - # The QUBO is negated for maximization: QUBO.cost = -(x^T Q x) - # "01": -(0 - 1) = 1, "10": -(1 - 0) = -1, "11": -(1 - 1) = 0 - self.assertAlmostEqual(cost_01, 1.0) - self.assertAlmostEqual(cost_10, -1.0) - self.assertAlmostEqual(cost_00, 0.0) + objective_01 = problem.objective_value("01") + objective_10 = problem.objective_value("10") + objective_11 = problem.objective_value("11") + objective_00 = problem.objective_value("00") + self.assertAlmostEqual(objective_01, -1.0) + self.assertAlmostEqual(objective_10, 1.0) + self.assertAlmostEqual(objective_11, 0.0) + self.assertAlmostEqual(objective_00, 0.0) def test_qubo_with_linear_terms(self): from qaoa.problems import QUBO Q = np.diag([0.0, 0.0]) c = np.array([1.0, 0.0]) problem = QUBO(Q=Q, c=c) - # cost = -(c^T x) => cost("10") = -1, cost("01") = 0 - self.assertAlmostEqual(problem.cost("10"), -1.0) - self.assertAlmostEqual(problem.cost("01"), 0.0) + self.assertAlmostEqual(problem.objective_value("10"), 1.0) + self.assertAlmostEqual(problem.objective_value("01"), 0.0) def test_qubo_circuit_parametrized(self): problem = self._make_simple_qubo() @@ -169,30 +161,29 @@ def test_portfolio_infeasible(self): self.assertFalse(problem.isFeasible("00")) self.assertFalse(problem.isFeasible("11")) - def test_portfolio_cost_is_float(self): + def test_portfolio_objective_is_float(self): problem = self._make_portfolio() - cost = problem.cost("01") - self.assertIsInstance(cost, (float, np.floating)) + objective = problem.objective_value("01") + self.assertIsInstance(objective, (float, np.floating)) - def test_portfolio_cost_penalized(self): + def test_portfolio_objective_penalized(self): problem = self._make_portfolio() - # cost() should return a float for any bitstring - cost = problem.cost("01") - self.assertIsInstance(cost, (float, np.floating)) + objective = problem.objective_value("01") + self.assertIsInstance(objective, (float, np.floating)) - def test_portfolio_penalty_lowers_infeasible_cost(self): + def test_portfolio_penalty_raises_infeasible_objective(self): from qaoa.problems import PortfolioOptimization cov = np.array([[1.0, 0.2], [0.2, 1.0]]) exp_ret = np.array([0.1, 0.3]) # Without penalty: infeasible "11" gets no budget penalty problem_no_pen = PortfolioOptimization(risk=0.5, budget=1, cov_matrix=cov, exp_return=exp_ret, penalty=0) - # With penalty: infeasible "11" is penalized → lower (more negative) cost + # With penalty: infeasible "11" is penalized → higher objective value problem_with_pen = PortfolioOptimization(risk=0.5, budget=1, cov_matrix=cov, exp_return=exp_ret, penalty=2.0) - cost_no_pen = problem_no_pen.cost("11") - cost_with_pen = problem_with_pen.cost("11") - self.assertLess(cost_with_pen, cost_no_pen) + objective_no_pen = problem_no_pen.objective_value("11") + objective_with_pen = problem_with_pen.objective_value("11") + self.assertGreater(objective_with_pen, objective_no_pen) def test_portfolio_circuit_created(self): problem = self._make_portfolio() @@ -224,21 +215,21 @@ def test_maxkcut_one_hot_invalid_k(self): with self.assertRaises(ValueError): MaxKCutOneHot(G, k_cuts=9) - def test_maxkcut_one_hot_cost_cut(self): + def test_maxkcut_one_hot_objective_cut(self): from qaoa.problems import MaxKCutOneHot G = self._make_graph() problem = MaxKCutOneHot(G, k_cuts=2) # nodes in different colors → cut: "1001" means node0=color0, node1=color1 - cost = problem.cost("1001") - self.assertGreater(cost, 0.0) + objective = problem.objective_value("1001") + self.assertGreater(objective, 0.0) - def test_maxkcut_one_hot_cost_no_cut(self): + def test_maxkcut_one_hot_objective_no_cut(self): from qaoa.problems import MaxKCutOneHot G = self._make_graph() problem = MaxKCutOneHot(G, k_cuts=2) # nodes same color → no cut: "1010" means both=color0 - cost = problem.cost("1010") - self.assertAlmostEqual(cost, 0.0) + objective = problem.objective_value("1010") + self.assertAlmostEqual(objective, 0.0) def test_maxkcut_one_hot_binstring_to_labels(self): from qaoa.problems import MaxKCutOneHot diff --git a/unittests/test_qaoa_core.py b/unittests/test_qaoa_core.py index c34e274..f549227 100644 --- a/unittests/test_qaoa_core.py +++ b/unittests/test_qaoa_core.py @@ -52,7 +52,7 @@ def test_cvar_half(self): cvar=0.5, ) qaoa.optimize(depth=1, angles=angles) - exp = qaoa.get_Exp(depth=1) + exp = qaoa.get_energy(depth=1) # CVaR result should be a valid negative number for MaxCut self.assertIsInstance(exp, float) self.assertLess(exp, 0.0) @@ -79,7 +79,7 @@ def test_spsa_optimizer(self): shots=256, ) qaoa.optimize(depth=1, angles=angles) - exp = qaoa.get_Exp(depth=1) + exp = qaoa.get_energy(depth=1) self.assertIsInstance(exp, float) @@ -105,12 +105,12 @@ def test_flip_enabled(self): qaoa.optimize(depth=1, angles=angles) # bitflips list should have one entry after depth 1 self.assertEqual(len(qaoa.bitflips), 1) - exp = qaoa.get_Exp(depth=1) + exp = qaoa.get_energy(depth=1) self.assertIsInstance(exp, float) class TestQAOAPostProcessing(unittest.TestCase): - """QAOA with post-processing should compute post-processed expectation.""" + """QAOA with post-processing should compute post-processed energy.""" def test_post_processing_enabled(self): from qaoa import QAOA @@ -129,9 +129,9 @@ def test_post_processing_enabled(self): post=5, # 5 rounds of bit-flip boosting ) qaoa.optimize(depth=1, angles=angles) - # Post-processed expectation should be set - self.assertIsNotNone(qaoa.Exp_post_processed) - self.assertIsInstance(qaoa.Exp_post_processed, float) + # Post-processed energy should be set + self.assertIsNotNone(qaoa.Energy_post_processed) + self.assertIsInstance(qaoa.Energy_post_processed, float) class TestQAOAGetOptimalSolutions(unittest.TestCase): @@ -220,10 +220,10 @@ def test_exact_cover_optimize(self): shots=256, ) qaoa.optimize(depth=1, angles=angles) - exp = qaoa.get_Exp(depth=1) + exp = qaoa.get_energy(depth=1) self.assertIsInstance(exp, float) - def test_exact_cover_feasible_solution_has_zero_cost(self): + def test_exact_cover_feasible_solution_has_zero_objective(self): from qaoa.problems import ExactCover columns = np.array([ @@ -234,11 +234,11 @@ def test_exact_cover_feasible_solution_has_zero_cost(self): problem = ExactCover(columns) # No single subset covers all elements exactly once in this 3×3 problem # but all 3 subsets together cover each element twice (not exact), so - # we verify the cost function penalizes infeasible strings - cost_111 = problem.cost("111") - cost_000 = problem.cost("000") - self.assertLessEqual(cost_111, 0.0) - self.assertLessEqual(cost_000, 0.0) + # we verify the objective penalizes infeasible strings + objective_111 = problem.objective_value("111") + objective_000 = problem.objective_value("000") + self.assertGreaterEqual(objective_111, 0.0) + self.assertGreaterEqual(objective_000, 0.0) class TestQAOAPortfolioEndToEnd(unittest.TestCase): @@ -264,7 +264,7 @@ def test_portfolio_optimize(self): shots=256, ) qaoa.optimize(depth=1, angles=angles) - exp = qaoa.get_Exp(depth=1) + exp = qaoa.get_energy(depth=1) self.assertIsInstance(exp, float) @@ -289,8 +289,8 @@ def test_orbit_optimize_depth2(self): ) qaoa.optimize(depth=2, angles=angles) self.assertEqual(qaoa.current_depth, 2) - exp1 = qaoa.get_Exp(depth=1) - exp2 = qaoa.get_Exp(depth=2) + exp1 = qaoa.get_energy(depth=1) + exp2 = qaoa.get_energy(depth=2) # Both depths should produce valid floats self.assertIsInstance(exp1, float) self.assertIsInstance(exp2, float) @@ -319,7 +319,7 @@ def test_landscape_shape(self): self.assertEqual(qaoa.var_landscape().shape, (4, 4)) def test_landscape_values_are_negative(self): - """Landscape values should be ≤ 0 for MaxCut (all costs are ≤ 0).""" + """Landscape values should be ≤ 0 for MaxCut energies.""" from qaoa import QAOA from qaoa import problems, mixers, initialstates @@ -335,7 +335,7 @@ def test_landscape_values_are_negative(self): shots=128, ) qaoa.sample_cost_landscape(angles=angles) - # All expected values should be ≤ 0 + # All expected energies should be ≤ 0 self.assertTrue(np.all(qaoa.exp_landscape() <= 0.0)) def test_interp_from_depth1_to_depth2(self): @@ -359,10 +359,10 @@ def test_interp_from_depth1_to_depth2(self): self.assertIn(2, qaoa.optimization_results) -class TestQAOAMultidepthGetExp(unittest.TestCase): - """QAOA.get_Exp() with no depth argument should return a list.""" +class TestQAOAMultidepthGetEnergy(unittest.TestCase): + """QAOA.get_energy() with no depth argument should return a list.""" - def test_get_exp_all_depths(self): + def test_get_energy_all_depths(self): from qaoa import QAOA from qaoa import problems, mixers, initialstates @@ -378,9 +378,9 @@ def test_get_exp_all_depths(self): shots=128, ) qaoa.optimize(depth=2, angles=angles) - all_exps = qaoa.get_Exp() - self.assertIsInstance(all_exps, list) - self.assertEqual(len(all_exps), 2) + all_energies = qaoa.get_energy() + self.assertIsInstance(all_energies, list) + self.assertEqual(len(all_energies), 2) class TestQAOAHistMethod(unittest.TestCase): diff --git a/unittests/test_qiskit2_compatibility.py b/unittests/test_qiskit2_compatibility.py index 298404a..269de8b 100644 --- a/unittests/test_qiskit2_compatibility.py +++ b/unittests/test_qiskit2_compatibility.py @@ -113,8 +113,8 @@ def test_maxcut_optimize_depth1(self): ) qaoa.optimize(depth=1, angles=self.angles) - best_exp = qaoa.get_Exp(depth=1) - # For a path graph with 3 nodes the optimum is -2 (cost stored negated). + best_exp = qaoa.get_energy(depth=1) + # For a path graph with 3 nodes the optimum energy is -2. # A good QAOA should achieve at least -1 (approximation ratio >= 0.5). self.assertLessEqual(best_exp, -1.0) @@ -184,7 +184,7 @@ def test_qnspsa_optimizer(self): ) # Should not raise ValueError about BaseSamplerV2 qaoa_inst.optimize(depth=1, angles=self.angles) - best_exp = qaoa_inst.get_Exp(depth=1) + best_exp = qaoa_inst.get_energy(depth=1) self.assertIsInstance(best_exp, float)