{
  "cells": [
    {
      "cell_type": "markdown",
      "id": "11ea9666",
      "metadata": {},
      "source": [
        "---\n",
        "title: Optimization Solver by Q-CTRL Fire Opal\n",
        "description: Start solving utility-scale optimization problems with the Fire Opal Optimization Solver by Q-CTRL\n",
        "---\n",
        "\n",
        "{/* cspell:ignore Sachdeva */}\n",
        "\n"
      ]
    },
    {
      "cell_type": "markdown",
      "id": "dde95705",
      "metadata": {},
      "source": [
        "# Optimization Solver by Q-CTRL Fire Opal\n",
        "\n",
        "*See the [API reference](/docs/functions/q-ctrl-optimization-solver/api-reference)*\n",
        "\n",
        "<Admonition type=\"note\">\n",
        "  Quantum Functions are a beta feature available only to IBM Quantum® Premium Plan, Flex Plan, and On-Prem (through the IBM Quantum Platform API) Plan users. Beta features are subject to change.\n",
        "</Admonition>\n",
        "\n"
      ]
    },
    {
      "cell_type": "markdown",
      "id": "40c2085c",
      "metadata": {
        "tags": [
          "version-info"
        ]
      },
      "source": [
        "{/*\n",
        "  DO NOT EDIT THIS CELL!!!\n",
        "  This cell's content is generated automatically by a script. Anything you add\n",
        "  here will be removed next time the notebook is run. To add new content, create\n",
        "  a new cell before or after this one.\n",
        "  */}\n",
        "\n",
        "<Accordion>\n",
        "  <AccordionItem title=\"Package versions\">\n",
        "    The code on this page was developed using the following requirements.\n",
        "    We recommend using these versions or newer.\n",
        "\n",
        "    ```\n",
        "    qiskit-ibm-runtime~=0.47.0\n",
        "    sympy~=1.14.0\n",
        "    ```\n",
        "  </AccordionItem>\n",
        "</Accordion>\n",
        "\n"
      ]
    },
    {
      "cell_type": "markdown",
      "id": "e30c7881",
      "metadata": {},
      "source": [
        "## Overview\n",
        "\n",
        "With the Fire Opal Optimization Solver, you can solve utility-scale optimization problems on quantum hardware without requiring quantum expertise. Simply input the high-level problem definition, and the Solver takes care of the rest. The entire workflow is noise-aware and leverages [Fire Opal's Performance Management](/docs/functions/q-ctrl-performance-management) under the hood. The Solver consistently delivers accurate solutions to classically challenging problems, even at full-device scale on the largest IBM® QPUs.\n",
        "\n",
        "The Solver is flexible and can be used to solve combinatorial optimization problems defined as objective functions or arbitrary graphs. Problems do not have to be mapped to device topology. Both unconstrained and constrained problems are solvable, with constraints enforced as hard Hamming-weight-1 constraints rather than penalty terms. The examples included in this guide demonstrate how to solve an unconstrained and a constrained utility-scale optimization problem using different Solver input types. The first example involves a max-cut problem defined on a 156-node, 3-Regular graph, while the second example tackles a 50-node graph partitioning problem defined by a cost function.\n",
        "\n",
        "To get access to the Optimization Solver, [contact Q-CTRL](https://form.typeform.com/to/uOAVDnGg?typeform-source=q-ctrl.com).\n",
        "\n"
      ]
    },
    {
      "cell_type": "markdown",
      "id": "5f761442",
      "metadata": {},
      "source": [
        "## Function description\n",
        "\n",
        "The Solver fully optimizes and automates the entire algorithm, from error suppression at the hardware level to efficient problem mapping and closed-loop classical optimization. Behind the scenes, the Solver's pipeline reduces errors at every stage, enabling the enhanced performance required to meaningfully scale. The underlying workflow is inspired by the Quantum Approximate Optimization Algorithm (QAOA), which is a hybrid quantum-classical algorithm. For a detailed summary of the full Optimization Solver workflow, refer to [the published manuscript](https://arxiv.org/abs/2406.01743).\n",
        "\n",
        "![Visualization of the Optimization Solver workflow](https://eu-de.quantum.cloud.ibm.com/docs/images/guides/qctrl-optimization/solver_workflow.svg)\n",
        "\n",
        "To solve a generic problem with the Optimization Solver:\n",
        "\n",
        "1. Define your problem as an objective function, a graph, or `SparsePauliOp` spin chain.\n",
        "2. Connect to the function through the Quantum Functions Catalog.\n",
        "3. Run the problem with the Solver and retrieve results.\n",
        "\n"
      ]
    },
    {
      "cell_type": "markdown",
      "id": "d20af236",
      "metadata": {},
      "source": [
        "### Accepted problem formats\n",
        "\n",
        "* Polynomial expression representation of an objective function. Ideally created in Python with an existing SymPy Poly object and formatted into a string using [sympy.srepr](https://docs.sympy.org/latest/tutorials/intro-tutorial/printing.html#srepr).\n",
        "* Graph representation of a specific problem type. The graph should be created using the networkx library in Python. It should then converted to a string by using the networkx function [`nx.readwrite.json_graph.adjacency_data`](https://networkx.org/documentation/stable/reference/readwrite/generated/networkx.readwrite.json_graph.adjacency_data.html).\n",
        "* Spin chain representation of a specific problem. The spin chain should be represented as a `SparsePauliOp` object; see the [documentation](/docs/api/qiskit/qiskit.quantum_info.SparsePauliOp) for more details.\n",
        "\n",
        "<Admonition type=\"note\" title=\"Does this function support all IBM backends?\">\n",
        "  If you want to use a backend that this function does not currently support, [reach out to Q-CTRL](https://form.typeform.com/to/iuujEAEI?typeform-source=q-ctrl.com) to add support.\n",
        "</Admonition>\n",
        "\n",
        "To access results from previous runs, see [how to retrieve previous quantum function jobs](/docs/ibm-quantum-platform/functions-get-started#list-previously-run-functions-jobs).\n",
        "\n"
      ]
    },
    {
      "cell_type": "markdown",
      "id": "1165f4f0",
      "metadata": {},
      "source": [
        "## Benchmarks\n",
        "\n",
        "<Admonition type=\"caution\" title=\"Disclaimer\">\n",
        "  Performance can depend on both the problem instance and subsequent processing steps. In some cases, classical samples and quantum-generated samples might achieve similar final solution quality after equivalent post-processing. Evaluation should therefore consider the complete optimization workflow.\n",
        "</Admonition>\n",
        "\n",
        "[Published benchmarking results](https://arxiv.org/abs/2406.01743) show that the Solver successfully solves problems with over 120 qubits, even outperforming previously published results on quantum annealing and trapped-ion devices. The following benchmark metrics provide a rough indication of the accuracy and scaling of problem types based on a few examples. Actual metrics may differ based on various problem features, such as the number of terms in the objective function (density) and their locality, number of variables, and polynomial order.\n",
        "\n",
        "The \"Number of qubits\" indicated is not a hard limitation but represents rough thresholds where you can expect extremely consistent solution accuracy. Larger problem sizes have been successfully solved, and testing beyond these limits is encouraged.\n",
        "\n",
        "Arbitrary qubit connectivity is supported across all problem types.\n",
        "\n",
        "| Problem type                              | Number of qubits | Example                                          | Accuracy | Total time (s) | Runtime usage (s) | Number of iterations |\n",
        "| ----------------------------------------- | ---------------- | ------------------------------------------------ | -------- | -------------- | ----------------- | -------------------- |\n",
        "| Sparsely-connected quadratic problems     | 156              | 3-regular max-cut                                | 100%     | 1764           | 293               | 16                   |\n",
        "| Higher-order binary optimization          | 156              | Ising spin-glass model                           | 100%     | 1461           | 272               | 16                   |\n",
        "| Densely-connected quadratic problems      | 50               | Fully-connected max-cut                          | 100%     | 1758           | 268               | 12                   |\n",
        "| Constrained problem with hard constraints | 50               | Weighted graph partitioning with 8% edge density | 100%     | 1074           | 215               | 10                   |\n",
        "\n"
      ]
    },
    {
      "cell_type": "markdown",
      "id": "73390a19",
      "metadata": {},
      "source": [
        "## Get started\n",
        "\n",
        "First, authenticate using your [IBM Quantum API key](http://quantum.cloud.ibm.com/). Then, select the quantum function as follows. (This snippet assumes you've already [saved your account](/docs/ibm-quantum-platform/functions-get-started#install-qiskit-functions-catalog-client) to your local environment.)\n",
        "\n"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": null,
      "id": "95a715d2",
      "metadata": {},
      "outputs": [],
      "source": [
        "from qiskit_ibm_catalog import FunctionsCatalog\n",
        "\n",
        "catalog = FunctionsCatalog(channel=\"ibm_quantum_platform\")"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": 2,
      "id": "8acf84f6",
      "metadata": {},
      "outputs": [],
      "source": [
        "# Access Function\n",
        "solver = catalog.load(\"q-ctrl/optimization-solver\")"
      ]
    },
    {
      "cell_type": "markdown",
      "id": "f0bbccd8",
      "metadata": {},
      "source": [
        "## Function sizes\n",
        "\n",
        "Every job runs at one size: `S`, `M`, or `L`. If the size is too small, the job is rejected. If it is larger than needed, the job runs on a bigger compute profile than the work requires. See [Function sizes](/docs/managed-workflows/managed-workflow-compute-profiles#best-practice-match-sizes-to-problem-sizes) for how sizes work in general.\n",
        "\n",
        "Pass the size to `solver.run()` with the `function_size` argument. For the Optimization Solver, the size depends on the number of binary decision variables and the interaction density of the problem.\n",
        "\n",
        "Interaction density is the fraction of possible variable pairs that appear together in at least one term of the objective function or constraints. Each distinct pair is counted once. For sizing, equality constraints contribute interactions between every pair of variables they contain. Continue to pass hard constraints directly through the `constraint` input, as shown in the constrained optimization example.\n",
        "\n",
        "| Size | Binary decision variables | Interaction density | Rule                                                                                   |\n",
        "| ---- | ------------------------- | ------------------- | -------------------------------------------------------------------------------------- |\n",
        "| `S`  | ≤ 50                      | ≤ 10%               | Both metrics must satisfy the Small limits.                                            |\n",
        "| `M`  | ≤ 100                     | ≤ 25%               | At least one metric exceeds its Small limit, but both remain within the Medium limits. |\n",
        "| `L`  | > 100                     | > 25%               | Either the variable count or interaction density exceeds its Medium limit.             |\n",
        "\n",
        "Both the variable count and the interaction density must satisfy the selected size. For example, a problem with 40 variables and 30% interaction density requires `\"L\"`.\n",
        "\n",
        "For graph problems, count the binary decision variables in the optimization model. Depending on the formulation, a single graph node can require more than one variable.\n",
        "\n"
      ]
    },
    {
      "cell_type": "markdown",
      "id": "3055fed4",
      "metadata": {},
      "source": [
        "### Calculate the size before submitting\n",
        "\n",
        "Use the following helper to calculate the size from the number of binary decision variables and the number of distinct interacting variable pairs, including the pairs that hard constraints introduce. It returns `\"S\"`, `\"M\"`, or `\"L\"`, and no job is submitted. The examples that follow show how to count the variables and pairs for a graph problem and for a constrained problem.\n",
        "\n"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": 5,
      "id": "3d6701fd",
      "metadata": {},
      "outputs": [],
      "source": [
        "def calculate_function_size(num_variables, num_interactions):\n",
        "    possible_pairs = num_variables * (num_variables - 1) // 2\n",
        "    density = num_interactions / possible_pairs if possible_pairs else 0\n",
        "\n",
        "    if num_variables <= 50 and density <= 0.10:\n",
        "        return \"S\"\n",
        "    if num_variables <= 100 and density <= 0.25:\n",
        "        return \"M\"\n",
        "    return \"L\""
      ]
    },
    {
      "cell_type": "markdown",
      "id": "qctrl-os-sizes-outro",
      "metadata": {},
      "source": [
        "If the size is too small, the job fails with error `4600`, and the message names the size to use instead. If it is larger than needed, the job runs normally and records a `4601` advisory event. See [Function sizes](/docs/managed-workflows/managed-workflow-compute-profiles#best-practice-match-sizes-to-problem-sizes) to learn more.\n",
        "\n"
      ]
    },
    {
      "cell_type": "markdown",
      "id": "e8837f5f",
      "metadata": {},
      "source": [
        "## Example: Unconstrained optimization\n",
        "\n",
        "Run the [maximum cut](https://en.wikipedia.org/wiki/Maximum_cut) (max-cut) problem. The following example demonstrates the Solver's capabilities on a 100-node, 3-regular unweighted graph max-cut problem, but you can also solve weighted graph problems.\n",
        "\n"
      ]
    },
    {
      "cell_type": "markdown",
      "id": "a699235b",
      "metadata": {},
      "source": [
        "In addition to `qiskit-ibm-catalog`, you will also use the following packages to run this example: `networkx` and `numpy`. You can install these packages by uncommenting the following cell if you are running this example in a notebook using the IPython kernel.\n",
        "\n"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": 3,
      "id": "a74fe6a9",
      "metadata": {},
      "outputs": [],
      "source": [
        "# %pip install networkx numpy"
      ]
    },
    {
      "cell_type": "markdown",
      "id": "a41c4a0d",
      "metadata": {},
      "source": [
        "### 1. Define the problem\n",
        "\n",
        "You can run a max-cut problem by defining a graph problem and specifying `problem_type='maxcut'`.\n",
        "\n"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": 1,
      "id": "d56e1440",
      "metadata": {},
      "outputs": [],
      "source": [
        "import networkx as nx\n",
        "import numpy as np\n",
        "\n",
        "# Generate a random graph with 100 nodes\n",
        "maxcut_graph = nx.random_regular_graph(d=3, n=100, seed=8)"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": 7,
      "id": "0a7255e1",
      "metadata": {},
      "outputs": [
        {
          "data": {
            "text/plain": [
              "<Image src=\"/docs/images/guides/q-ctrl-optimization-solver/extracted-outputs/0a7255e1-0.avif\" alt=\"Output of the previous code cell\" />"
            ]
          },
          "metadata": {},
          "output_type": "display_data"
        }
      ],
      "source": [
        "# Optionally, visualize the graph\n",
        "nx.draw_networkx(\n",
        "    maxcut_graph, nx.kamada_kawai_layout(maxcut_graph), node_size=100\n",
        ")"
      ]
    },
    {
      "cell_type": "markdown",
      "id": "e42a8d55",
      "metadata": {},
      "source": [
        "The Solver accepts a string as the problem definition input.\n",
        "\n"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": 3,
      "id": "2546e3e5",
      "metadata": {},
      "outputs": [],
      "source": [
        "# Convert graph to string\n",
        "problem_as_str = nx.readwrite.json_graph.adjacency_data(maxcut_graph)"
      ]
    },
    {
      "cell_type": "markdown",
      "id": "0ab6f24f",
      "metadata": {},
      "source": [
        "### 2. Run the problem\n",
        "\n",
        "When using the graph-based input method, specify the problem type. Calculate the function size from `maxcut_graph`, then pass the returned value to `solver.run` through `function_size`.\n",
        "\n"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": 11,
      "id": "ac1267f2",
      "metadata": {},
      "outputs": [
        {
          "name": "stdout",
          "output_type": "stream",
          "text": [
            "Workload size: M\n"
          ]
        }
      ],
      "source": [
        "# Calculate the workload size\n",
        "maxcut_size = calculate_function_size(\n",
        "    maxcut_graph.number_of_nodes(),\n",
        "    maxcut_graph.number_of_edges(),\n",
        ")\n",
        "print(f\"Workload size: {maxcut_size}\")"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": null,
      "id": "16c66d64",
      "metadata": {},
      "outputs": [],
      "source": [
        "# Solve the problem\n",
        "maxcut_job = solver.run(\n",
        "    problem=problem_as_str,\n",
        "    problem_type=\"maxcut\",\n",
        "    backend_name=backend_name,  # E.g. \"ibm_fez\"\n",
        "    function_size=maxcut_size,\n",
        ")"
      ]
    },
    {
      "cell_type": "markdown",
      "id": "48069093",
      "metadata": {},
      "source": [
        "Check your quantum function workload's [status](/docs/ibm-quantum-platform/functions-get-started#check-job-status) or return [results](/docs/ibm-quantum-platform/functions-get-started#retrieve-results) as follows:\n",
        "\n"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": 17,
      "id": "856fe992",
      "metadata": {},
      "outputs": [
        {
          "name": "stdout",
          "output_type": "stream",
          "text": [
            "eb364ea7-b69d-48f4-a1ce-1b22f2290534\n",
            "RUNNING\n"
          ]
        }
      ],
      "source": [
        "# Print the ID so you can use it later, if necessary\n",
        "print(maxcut_job.job_id)\n",
        "\n",
        "# Get job status\n",
        "print(maxcut_job.status())"
      ]
    },
    {
      "cell_type": "markdown",
      "id": "74f48eab",
      "metadata": {},
      "source": [
        "### 3. Retrieve the result\n",
        "\n",
        "Retrieve the optimal cut value from the results dictionary.\n",
        "\n",
        "<Admonition type=\"note\">\n",
        "  The mapping of the variables to the bitstring may have changed. The output dictionary contains a `variables_to_bitstring_index_map` sub-dictionary, which helps to verify the ordering.\n",
        "</Admonition>\n",
        "\n"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": 18,
      "id": "6b571411",
      "metadata": {},
      "outputs": [
        {
          "name": "stdout",
          "output_type": "stream",
          "text": [
            "Optimal cut value: 137.0\n"
          ]
        }
      ],
      "source": [
        "# Poll for results\n",
        "maxcut_result = maxcut_job.result()\n",
        "\n",
        "# Take the absolute value of the solution since the cost function is minimized\n",
        "qctrl_maxcut = abs(maxcut_result[\"solution_bitstring_cost\"])\n",
        "\n",
        "# Print the optimal cut value found by the Optimization Solver\n",
        "print(f\"Optimal cut value: {qctrl_maxcut}\")"
      ]
    },
    {
      "cell_type": "markdown",
      "id": "508e29b0",
      "metadata": {},
      "source": [
        "You can verify the accuracy of the result by solving the problem classically with open-source solvers like [PuLP](https://coin-or.github.io/pulp/) if the graph is not densely connected. High density problems may require advanced classical solvers to validate the solution.\n",
        "\n"
      ]
    },
    {
      "cell_type": "markdown",
      "id": "e2817b13",
      "metadata": {},
      "source": [
        "## Example: Constrained optimization\n",
        "\n",
        "The prior max-cut example is a common quadratic unconstrained binary optimization problem. Q-CTRL's Optimization Solver can also solve constrained optimization problems by passing hard constraints directly to the Solver through the `constraint` input, instead of encoding them as penalty terms in the objective function. The Solver currently supports Hamming-weight-1 constraints: each constraint specifies a group of variables where exactly one variable must equal 1 and the rest must equal 0.\n",
        "\n",
        "The following example demonstrates how to construct a cost function and a set of hard constraints for a constrained optimization problem, [graph partitioning](https://en.wikipedia.org/wiki/Graph_partition), by assigning every node in a graph to exactly one of several groups while minimizing the total weight of edges whose endpoints land in the same group.\n",
        "\n"
      ]
    },
    {
      "cell_type": "markdown",
      "id": "a9fbc0e9",
      "metadata": {},
      "source": [
        "In addition to the `qiskit-ibm-catalog` and `qiskit` packages, you will also use the following packages to run this example: `numpy`, `networkx`, and `sympy`. You can install these packages by uncommenting the following cell if you are running this example in a notebook using the IPython kernel.\n",
        "\n"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": 11,
      "id": "c6428fd0",
      "metadata": {},
      "outputs": [],
      "source": [
        "# %pip install numpy networkx sympy"
      ]
    },
    {
      "cell_type": "markdown",
      "id": "98d3bfd3",
      "metadata": {},
      "source": [
        "### 1. Define the problem\n",
        "\n",
        "Define a random graph partitioning problem by generating a graph with randomly weighted nodes.\n",
        "\n"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": 19,
      "id": "c2ce65e3",
      "metadata": {},
      "outputs": [
        {
          "data": {
            "text/plain": [
              "<Image src=\"/docs/images/guides/q-ctrl-optimization-solver/extracted-outputs/c2ce65e3-0.avif\" alt=\"Output of the previous code cell\" />"
            ]
          },
          "metadata": {},
          "output_type": "display_data"
        }
      ],
      "source": [
        "import networkx as nx\n",
        "from sympy import Symbol, Poly, srepr\n",
        "\n",
        "# To change the weights, change the seed to any integer.\n",
        "rng_seed = 18\n",
        "_rng = np.random.default_rng(rng_seed)\n",
        "node_count = 50\n",
        "edge_probability = 0.08\n",
        "graph = nx.erdos_renyi_graph(\n",
        "    node_count, edge_probability, seed=rng_seed, directed=False\n",
        ")\n",
        "\n",
        "# add node weights\n",
        "min_weight = -1.0\n",
        "max_weight = 1.0\n",
        "for i in graph.nodes:\n",
        "    weight = (max_weight - min_weight) * _rng.random() + min_weight\n",
        "    graph.add_node(i, weight=weight)\n",
        "\n",
        "# Optionally, visualize the graph\n",
        "nx.draw_networkx(graph, nx.kamada_kawai_layout(graph), node_size=200)"
      ]
    },
    {
      "cell_type": "markdown",
      "id": "89c91472",
      "metadata": {},
      "source": [
        "A standard optimization model for weighted graph partitioning can be formulated as follows. Split the nodes of the graph into three groups $g \\in \\{0, 1, 2\\}$, and let $n_{i,g} = 1$ if node $i$ is assigned to group $g$, and $n_{i,g} = 0$ otherwise. The goal is to minimize the total weight of edges whose endpoints are assigned to the same group, where the weight of an edge $(i,j)$ is the combined weight of its two endpoints, $\\omega_{i,j} = \\omega_i + \\omega_j$:\n",
        "\n",
        "$\\textbf{Minimize}\\qquad y = \\sum_{(i,j)\\in E} \\omega_{i,j} \\sum_{g} n_{i,g}\\, n_{j,g}$\n",
        "\n"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": 27,
      "id": "91586ee9",
      "metadata": {},
      "outputs": [],
      "source": [
        "# Construct the cost function.\n",
        "group_count = 3\n",
        "variables = [\n",
        "    Symbol(f\"n[{i},{g}]\")\n",
        "    for i in range(node_count)\n",
        "    for g in range(group_count)\n",
        "]\n",
        "node_group_var = {\n",
        "    (i, g): variables[i * group_count + g]\n",
        "    for i in range(node_count)\n",
        "    for g in range(group_count)\n",
        "}\n",
        "cost_function = Poly(0, *variables)\n",
        "\n",
        "for i, j in graph.edges():\n",
        "    edge_weight = graph.nodes[i][\"weight\"] + graph.nodes[j][\"weight\"]\n",
        "    for g in range(group_count):\n",
        "        cost_function += (\n",
        "            edge_weight * node_group_var[(i, g)] * node_group_var[(j, g)]\n",
        "        )"
      ]
    },
    {
      "cell_type": "markdown",
      "id": "9d01684b",
      "metadata": {},
      "source": [
        "Every node must be assigned to exactly one of the three groups. This is a Hamming-weight-1 constraint: for every node $i$, exactly one of $n_{i,0}, n_{i,1}, n_{i,2}$ must equal 1, and the rest must equal 0:\n",
        "\n",
        "$n_{i,0} + n_{i,1} + n_{i,2} = 1 \\texttt{ for all } i \\in V$\n",
        "\n",
        "Rather than encoding this requirement as a penalty term in the cost function, pass it directly to the Solver as a hard constraint using the `constraint` input.\n",
        "\n"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": 28,
      "id": "1ff1ffcc",
      "metadata": {},
      "outputs": [
        {
          "name": "stdout",
          "output_type": "stream",
          "text": [
            "Problem constraints: {\"('n[0,0]', 'n[0,1]', 'n[0,2]')\": 1, \"('n[1,0]', 'n[1,1]', 'n[1,2]')\": 1, \"('n[2,0]', 'n[2,1]', 'n[2,2]')\": 1, \"('n[3,0]', 'n[3,1]', 'n[3,2]')\": 1, \"('n[4,0]', 'n[4,1]', 'n[4,2]')\": 1, \"('n[5,0]', 'n[5,1]', 'n[5,2]')\": 1, \"('n[6,0]', 'n[6,1]', 'n[6,2]')\": 1, \"('n[7,0]', 'n[7,1]', 'n[7,2]')\": 1, \"('n[8,0]', 'n[8,1]', 'n[8,2]')\": 1, \"('n[9,0]', 'n[9,1]', 'n[9,2]')\": 1, \"('n[10,0]', 'n[10,1]', 'n[10,2]')\": 1, \"('n[11,0]', 'n[11,1]', 'n[11,2]')\": 1, \"('n[12,0]', 'n[12,1]', 'n[12,2]')\": 1, \"('n[13,0]', 'n[13,1]', 'n[13,2]')\": 1, \"('n[14,0]', 'n[14,1]', 'n[14,2]')\": 1, \"('n[15,0]', 'n[15,1]', 'n[15,2]')\": 1, \"('n[16,0]', 'n[16,1]', 'n[16,2]')\": 1, \"('n[17,0]', 'n[17,1]', 'n[17,2]')\": 1, \"('n[18,0]', 'n[18,1]', 'n[18,2]')\": 1, \"('n[19,0]', 'n[19,1]', 'n[19,2]')\": 1, \"('n[20,0]', 'n[20,1]', 'n[20,2]')\": 1, \"('n[21,0]', 'n[21,1]', 'n[21,2]')\": 1, \"('n[22,0]', 'n[22,1]', 'n[22,2]')\": 1, \"('n[23,0]', 'n[23,1]', 'n[23,2]')\": 1, \"('n[24,0]', 'n[24,1]', 'n[24,2]')\": 1, \"('n[25,0]', 'n[25,1]', 'n[25,2]')\": 1, \"('n[26,0]', 'n[26,1]', 'n[26,2]')\": 1, \"('n[27,0]', 'n[27,1]', 'n[27,2]')\": 1, \"('n[28,0]', 'n[28,1]', 'n[28,2]')\": 1, \"('n[29,0]', 'n[29,1]', 'n[29,2]')\": 1, \"('n[30,0]', 'n[30,1]', 'n[30,2]')\": 1, \"('n[31,0]', 'n[31,1]', 'n[31,2]')\": 1, \"('n[32,0]', 'n[32,1]', 'n[32,2]')\": 1, \"('n[33,0]', 'n[33,1]', 'n[33,2]')\": 1, \"('n[34,0]', 'n[34,1]', 'n[34,2]')\": 1, \"('n[35,0]', 'n[35,1]', 'n[35,2]')\": 1, \"('n[36,0]', 'n[36,1]', 'n[36,2]')\": 1, \"('n[37,0]', 'n[37,1]', 'n[37,2]')\": 1, \"('n[38,0]', 'n[38,1]', 'n[38,2]')\": 1, \"('n[39,0]', 'n[39,1]', 'n[39,2]')\": 1, \"('n[40,0]', 'n[40,1]', 'n[40,2]')\": 1, \"('n[41,0]', 'n[41,1]', 'n[41,2]')\": 1, \"('n[42,0]', 'n[42,1]', 'n[42,2]')\": 1, \"('n[43,0]', 'n[43,1]', 'n[43,2]')\": 1, \"('n[44,0]', 'n[44,1]', 'n[44,2]')\": 1, \"('n[45,0]', 'n[45,1]', 'n[45,2]')\": 1, \"('n[46,0]', 'n[46,1]', 'n[46,2]')\": 1, \"('n[47,0]', 'n[47,1]', 'n[47,2]')\": 1, \"('n[48,0]', 'n[48,1]', 'n[48,2]')\": 1, \"('n[49,0]', 'n[49,1]', 'n[49,2]')\": 1}\n"
          ]
        }
      ],
      "source": [
        "# Build the hard constraint: exactly one group per node.\n",
        "constraint_dict = {\n",
        "    str(tuple(f\"n[{i},{g}]\" for g in range(group_count))): 1\n",
        "    for i in range(node_count)\n",
        "}\n",
        "print(f\"Problem constraints: {constraint_dict}\")"
      ]
    },
    {
      "cell_type": "markdown",
      "id": "753e5514",
      "metadata": {},
      "source": [
        "<Admonition type=\"note\" title=\"Partially constrained problems\">\n",
        "  You do not need to add every variable to `constraint`. Any variable left out of the dictionary remains unconstrained, so you can mix hard-constrained groups of variables with free variables in the same problem.\n",
        "</Admonition>\n",
        "\n"
      ]
    },
    {
      "cell_type": "markdown",
      "id": "9fd0ba9e",
      "metadata": {},
      "source": [
        "### 2. Run the problem\n",
        "\n",
        "Calculate the function size using the binary decision variables and interactions from the objective function and constraints. This formulation uses one variable per node and group. Pass the calculated size to `solver.run` through `function_size`.\n",
        "\n"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": 22,
      "id": "0080c2cf",
      "metadata": {},
      "outputs": [
        {
          "name": "stdout",
          "output_type": "stream",
          "text": [
            "Workload size: L\n"
          ]
        }
      ],
      "source": [
        "num_variables = node_count * group_count\n",
        "objective_pairs = graph.number_of_edges() * group_count\n",
        "constraint_pairs = node_count * group_count * (group_count - 1) // 2\n",
        "\n",
        "partition_size = calculate_function_size(\n",
        "    num_variables,\n",
        "    objective_pairs + constraint_pairs,\n",
        ")\n",
        "\n",
        "print(f\"Workload size: {partition_size}\")"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": 20,
      "id": "f380b56d",
      "metadata": {},
      "outputs": [],
      "source": [
        "# Solve the problem\n",
        "partition_job = solver.run(\n",
        "    problem=srepr(cost_function),\n",
        "    constraint=constraint_dict,\n",
        "    backend_name=\"ibm_marrakesh\",  # E.g. \"ibm_marrakesh\"\n",
        "    function_size=partition_size,\n",
        ")"
      ]
    },
    {
      "cell_type": "markdown",
      "id": "d154c226",
      "metadata": {},
      "source": [
        "Check your quantum function workload's [status](/docs/ibm-quantum-platform/functions-get-started#check-job-status) or return [results](/docs/ibm-quantum-platform/functions-get-started#retrieve-results) as follows:\n",
        "\n"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": 21,
      "id": "6de77b14",
      "metadata": {},
      "outputs": [
        {
          "name": "stdout",
          "output_type": "stream",
          "text": [
            "b8085944-f313-444e-be39-ea61b1b47ebd\n",
            "QUEUED\n"
          ]
        }
      ],
      "source": [
        "# Print the ID so you can use it later, if necessary\n",
        "print(partition_job.job_id)\n",
        "\n",
        "# Get job status\n",
        "print(partition_job.status())"
      ]
    },
    {
      "cell_type": "markdown",
      "id": "8a95618e",
      "metadata": {},
      "source": [
        "### 3. Get the result\n",
        "\n",
        "Retrieve the solution and analyze the results. The solution cost represents the total weight of edges whose endpoints ended up in the same group, so a lower cost indicates a better partitioning of the graph.\n",
        "\n"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": null,
      "id": "a924bf93",
      "metadata": {},
      "outputs": [
        {
          "name": "stdout",
          "output_type": "stream",
          "text": [
            "Total weight of same-group edges: -36.5539\n",
            "Solution bitstring: 100100100100100001100100100100100100100100100100100001010100010100100100100010001001100100100001100001100001010001001010100100100100100010100100100100\n"
          ]
        }
      ],
      "source": [
        "partition_result = partition_job.result()\n",
        "qctrl_cost = partition_result[\"solution_bitstring_cost\"]\n",
        "solution_bitstring = partition_result[\"solution_bitstring\"]\n",
        "\n",
        "# Print results\n",
        "print(f\"Total weight of same-group edges: {qctrl_cost}\")\n",
        "print(f\"Solution bitstring: {solution_bitstring}\")"
      ]
    },
    {
      "cell_type": "markdown",
      "id": "e9ec2e67",
      "metadata": {},
      "source": [
        "## Get support\n",
        "\n",
        "For any questions or issues, [reach out to Q-CTRL](https://form.typeform.com/to/iuujEAEI?typeform-source=q-ctrl.com).\n",
        "\n"
      ]
    },
    {
      "cell_type": "markdown",
      "id": "993aa226c6ec9f5e",
      "metadata": {},
      "source": [
        "## Changelog\n",
        "\n",
        "* 2026-08-10: Added support for hard (Hamming weight 1) constraints via the `constraint` input, and updated the constrained optimization example to use them.\n",
        "* 2026-02-11: We now have support for `ibm_miami`\n",
        "\n"
      ]
    },
    {
      "cell_type": "markdown",
      "id": "5a6a25c8",
      "metadata": {},
      "source": [
        "## Next steps\n",
        "\n",
        "<Admonition type=\"tip\" title=\"Recommendations\">\n",
        "  * Request access to [Q-CTRL Optimization Solver](/functions?id=q-ctrl-optimization-solver).\n",
        "  * Visit the [API reference](/docs/functions/q-ctrl-optimization-solver/api-reference) for this quantum function.\n",
        "  * Try the [Solve higher-order binary optimization problems with Q-CTRL's Optimization Solver](/docs/tutorials/solve-higher-order-binary-optimization-problems-with-q-ctrls-optimization-solver) tutorial.\n",
        "  * Review [Sachdeva, N., et al. (2024).  Quantum optimization using a 127-qubit gate-model IBM quantum computer can outperform quantum annealers for nontrivial binary optimization problems. arXiv preprint arXiv:2406.01743](https://arxiv.org/abs/2406.01743).\n",
        "  * Review [Loco, D., et al. (2026).  Practical protein-pocket hydration-site prediction for drug discovery on a quantum computer. arXiv preprint arXiv:2512.08390](https://arxiv.org/abs/2512.08390).\n",
        "  * Review the [Mazda](https://q-ctrl.com/case-study/tackling-a-costly-bottleneck-in-automotive-design) case study.\n",
        "  * Review the [Network Rail](https://q-ctrl.com/case-study/accelerating-the-schedule-for-quantum-enhanced-rail) case study.\n",
        "  * Review the [Australian Army](https://q-ctrl.com/case-study/improving-army-logistics-with-quantum-computing) case study.\n",
        "  * Review the [Transport for New South Wales](https://q-ctrl.com/case-study/delivering-quantum-computing-for-faster-commuting) case study.\n",
        "</Admonition>\n",
        "\n"
      ]
    },
    {
      "cell_type": "markdown",
      "metadata": {},
      "id": "a1b8767d",
      "source": "© IBM Corp., 2017-2026"
    }
  ],
  "metadata": {
    "kernelspec": {
      "display_name": "Python 3",
      "language": "python",
      "name": "python3"
    },
    "language_info": {
      "codemirror_mode": {
        "name": "ipython",
        "version": 3
      },
      "file_extension": ".py",
      "mimetype": "text/x-python",
      "name": "python",
      "nbconvert_exporter": "python",
      "pygments_lexer": "ipython3",
      "version": "3"
    }
  },
  "nbformat": 4,
  "nbformat_minor": 5
}