Skip to main content
IBM Quantum Platform

ParityQC の「Parity Twine Optimizer」で、市場の分断問題を解決しましょう

推定実行時間:Nighthawk r2 プロセッサ上で10秒。 (注:これはあくまで目安です。 (実行時間は異なる場合があります。)


学習成果

  • QOBLIB(Quantum Optimization Benchmarking Library )から「Market Split」問題を取得し、フォーマットを整えてください。
  • Parity Twine Optimizer をセットアップして使用し、マーケット・スプリットの事例を解決します。
  • Parity Twine Optimizer のオプションの選び方や、どのような結果が出力されるかについて学びましょう。

背景

このチュートリアルでは、 ParityQC の「Parity Twine Optimizer」を使用して、マーケット・スプリット問題を解く方法について解説します。

この問題のインスタンスは、QOBLIB(量子最適化ベンチマークライブラリ) から取得したものです。

市場の分割問題

マーケット・スプリット問題は、現実世界のNP困難な資源配分問題であり、量子最適化アルゴリズムのベンチマークとなっている。 これは、極めて重要な物流上の課題である。すなわち、顧客や製品から成る複雑な構造を、管理しやすく、かつ均等な販売エリアに分割する方法である。

目標は、 nn の市場を2つの均等な販売地域に分割し、各地域が mm 製品の総需要のちょうど半分を受け持つようにすることです。 この解決策とは、製品需要を可能な限り均等に分散させるための具体的な構成であり、これにより企業は両地域でバランスが取れた物流および人員配置戦略を実施し、 特定の地域での製品不足や倉庫の過剰在庫といったリスクを最小限に抑えることができる。

市場や製品の数が増えるにつれて、考えられる組み合わせの数は指数関数的に増加するため、従来の網羅的探索では最適な分割を見つけることが困難になります。

数学的定式化

AA を、各市場における製品の需要を表す m×nm \times n の行列とする。ここで、 AijA_{ij} は、市場 jj における製品 ii の需要を表す。

2値の割り当てベクトル x=[x1,x2,…,xn]T∈{0,1}nx = [x_1, x_2, \dots, x_n]^T \in \{0, 1\}^n は、以下のように定義される。ここで:

  • xj=1x_j = 1 市場 jj を地域Aに割り当てます。
  • xj=0x_j = 0 市場 jj を地域Bに割り当てます。

d=[d1,d2,…,dm]Td = [d_1, d_2, \dots, d_m]^T を各製品の総需要ベクトルとし、 d=A⋅1d = A \cdot \mathbf{1} として計算する。製品 ii の地域ごとの目標販売量は、 di2\frac{d_i}{2} となる。

最適化または実現可能性の制約条件として、地域Aに割り当てられる総売上高は、すべての製品について、総需要の半分と完全に一致しなければならない

Ax=12A1=b.A x = \frac{1}{2} A \mathbf{1} = b.

実際には、正確な除算が可能なことはめったにないため、この問題は、制約違反の二乗和(コスト関数)を最小化するよう定式化される:

min⁡x∥Ax−b∥2=∑i=1m(∑j=1nAijxj−b)2.\min_{x} \left\Vert{} A x - b \right\Vert{}^2 = \sum_{i=1}^{m} \left( \sum_{j=1}^{n} A_{ij} x_j - b\right)^2.

これを展開すると、制約なしの二次バイナリ最適化(QUBO)問題に相当する形式が得られる。

解が得られた際、解ベクトル xx によって、市場がどの領域に割り当てられるかが決定されます。 これは、製品の需要を可能な限り均等に分散させるための構成です。


要件

このチュートリアルを始める前に、以下のものがインストールされていることを確認してください:

  • Qiskit Functions Catalog IBM クライアント (pip install qiskit-ibm-catalog)
  • Qiskit アドオン「Optimization Mapper」(pip install qiskit_addon_opt_mapper)
  • NumPy (pip install numpy)

また、 ParityQC のTwine Optimizer関数にアクセスするには、権限が必要です。 アクセスを希望される場合は、このフォームにご記入ください。


セットアップ

(このコードは、 アカウントがすでにローカル環境に保存されていることを前提としています。)

まず、このチュートリアルに必要なすべてのパッケージをインポートします。

import tempfile

from collections.abc import Callable
from pathlib import Path

import numpy as np
import requests

from qiskit_addon_opt_mapper import OptimizationProblem
from qiskit_addon_opt_mapper.converters import OptimizationProblemToQubo
from qiskit_ibm_catalog import QiskitFunctionsCatalog

Qiskit Functions カタログからParity Twine Optimizerを読み込みます:

catalog = QiskitFunctionsCatalog(channel="ibm_quantum_platform")
function = catalog.load("parityqc/parity-twine-optimizer")

ステップ 1:問題を目的関数として定式化する

QOBLIB(量子最適化ベンチマークライブラリ) から、市場分割問題のインスタンスを次のように取得します。

この関 load_market_split_problem 数は、QOBLIBから指定された問題を取得し、それをQUBO問題に変換します。

def load_market_split_problem(instance_name: str) -> OptimizationProblem:
    """Load and formulate a market split optimization problem from an QOBLIB instance.

    The QOBLIB library can be found here:
    https://github.com/ZIB-AOPT/QOBLIB.

    Args:
        instance_name: Name of the market split instance to load as specified by the .dat file
            in the QOBLIB repo.

    Returns:
        The output OptimizationProblem containing the loaded market split problem.
    """

    problem_matrix, problem_vector = fetch_and_parse(
        instance_name, "01-marketsplit", parse_marketsplit_dat
    )

    # Create optimization problem
    optimization_problem = OptimizationProblem(instance_name)

    # Add binary variables (one for each market)
    optimization_problem.binary_var_list(problem_matrix.shape[1])

    # Add equality constraints (one for each product)
    for idx, rhs in enumerate(problem_vector):
        optimization_problem.linear_constraint(
            problem_matrix[idx, :], sense="==", rhs=rhs
        )

    # Convert to QUBO with penalty parameter
    return OptimizationProblemToQubo(penalty=1).convert(optimization_problem)

この関数 load_market_split_problem では、QOBLIBから市場分割問題のデータを取得・処理するために、以下のパーサー関数が必要です。

def fetch_and_parse(instance_name: str, problem: str, parse_func: Callable):
    """Generic function to fetch and parse data from QOBLIB repository.

    Args:
        instance_name: Name of the instance to fetch.
        problem: Category of the problem (e.g., '01-marketsplit', '07-independentset').
        parse_func: Function used to parse the downloaded file
            (e.g., parse_marketsplit_dat, parse_gph_file).

    Returns:
        Result of `parse_func` - either (np.ndarray, np.ndarray) for marketsplit
        or nx.Graph for MIS.
    """
    base_url = (
        "https://raw.githubusercontent.com/ZIB-AOPT/QOBLIB/refs/heads/main/"
    )
    url = (
        base_url
        + problem
        + "/instances/"
        + instance_name
        + (".dat" if problem == "01-marketsplit" else ".gph")
    )

    try:
        response = requests.get(url, timeout=30)
        response.raise_for_status()

        with tempfile.NamedTemporaryFile(
            mode="w",
            suffix=".dat" if problem == "01-marketsplit" else ".gph",
            delete=False,
            encoding="utf-8",
        ) as temp_file:
            temp_file.write(response.text)
            temp_file_path = temp_file.name

        try:
            return parse_func(temp_file_path)
        finally:
            Path(temp_file_path).unlink(missing_ok=True)

    except requests.RequestException as e:
        print(f"Error fetching data from repository: {e}")
    except (ValueError, OSError) as e:
        print(f"Error processing data: {e}")
        return None


def parse_marketsplit_dat(filename: str) -> tuple[np.ndarray, np.ndarray]:
    """Parse a market split problem from a .dat file format.

    Args:
        filename: Path to the .dat file.

    Returns:
        Tuple of (A, b) where:
            - A: (m, n) array of coefficients.
            - b: (m,) array of target values.

    Raises:
        ValueError: If file format is invalid or file is empty.
    """
    with Path(filename).open(encoding="utf-8") as f:
        lines = [
            line.strip()
            for line in f
            if line.strip() and not line.startswith("#")
        ]

    if not lines:
        raise ValueError("Empty or invalid .dat file")

    # First line: m n (number of products and markets)
    try:
        m, n = map(int, lines[0].split())
    except (ValueError, IndexError) as e:
        raise ValueError(
            "Invalid file format: first line must contain 'm n' integers"
        ) from e

    if len(lines) < m + 1:
        raise ValueError(
            f"File contains {len(lines)} lines but expected {m + 1} lines"
        )

    # Next m lines: each row of A followed by corresponding element of b
    mat_a = []
    vec_b = []

    for i in range(1, m + 1):
        try:
            values = list(map(int, lines[i].split()))
        except ValueError as e:
            raise ValueError(f"Invalid integer values in line {i + 1}") from e

        if len(values) != n + 1:
            raise ValueError(
                f"Line {i + 1} contains {len(values)} values but expected {n + 1}"
            )

        mat_a.append(values[:-1])  # First n values: product sales per market
        vec_b.append(values[-1])  # Last value: target sales for this product

    return np.array(mat_a), np.array(vec_b)

定義が完了すると、これを使用してライブラリから特定の問題インスタンスを読み込む load_marketsplit_problem ことができます:

ms_instance = "ms_04_050_001"

ms_problem = load_market_split_problem(ms_instance)

ステップ2:JSON形式に変換する

最初のステップで、この問題のQUBO形式を導き出しました。 それでは、オプティマイザ関数用にこれをJSON形式に変換しましょう:

def optimization_problem_to_json(
    problem: OptimizationProblem,
) -> dict[str, float]:
    """
    Converts an unconstrained quadratic OptimizationProblem in terms of binary or spin variables
    to the JSON input format of the Parity Twine Qiskit Function.

    Args:
        problem: The optimization problem to convert to JSON.

    Returns:
        The JSON input format of the given problem.
    """
    ising, constant = problem.to_ising()
    output = {"()": float(constant)}
    for op, coefficient in zip(ising.paulis, ising.coeffs, strict=True):
        # Invert the label strings because Qiskit has opposite convention
        qubits = tuple(
            num
            for num, pauli in enumerate(op.to_label()[::-1])
            if pauli == "Z"
        )
        output[str(qubits)] = float(coefficient)
    return output

「Market Split」問題のQUBOインスタンスは、次のようにJSON形式に変換されました:

json_ms_problem = optimization_problem_to_json(ms_problem)

ステップ3:Parity Twine Optimizer を使用して問題を解く

市場分割問題を特定し、正しい形式に変換できたので、Twine Optimizerと選択した IBM® バックエンドを使用して解を求めることができます。

この関数を実行するには、適切なバックエンドデバイスを選択してください。例えば、 ibm_phoenix。

送信をさらに細かく制御したい options 場合は(任意)、以下を使用できます:

options = {
    "shots": 100000,
    "postprocessing_level": 1,
    "transpile_only": False,
    "job_tags": ["market_split"],
}

ここで、 は回路の実行回数を指定する整数 shots であり、 は結果に後処理を適用するかどうかを決定 postprocessing_level し、 transpile_onlyは問題が回路へのトランスパイルのみ(解かれることはない)であるかどうかを指定し、 は IBM Quantum® Platform 上でジョブを識別するために使用されるラベル job_tags である。

オプティマイザーを実行します:

function_job = function.run(
    problem=json_ms_problem,
    variable_type="spin",
    backend_name="ibm_phoenix",
    options=options,
)
print(f"Job ID: {function_job.job_id}")

ジョブのステータスを確認する:

# Monitor the job status
function_job.status()

結果を取得する:

# Retrieve the job result if the status is DONE
result = function_job.result()

result

結果は次のような形式になります:

{
    'solution': {'0': 1, '1': -1, '10': 1, ... },
    'objective_value':  1.0,
    'solution_bitstring': '010000011101111011001001110010',
    'metadata': {
        'circuit_metrics': {
            'depth': 309,
            'gate_count': 3880,
            'two_qubit_gate_depth': 116,
            'two_qubit_gate_count': 899,
            'num_qubits': 30,
            'operations': {'sx': 1244, 'rz': 1227, 'cz': 899, 'delay': 473, 'measure': 30, 'x': 7},
        },
        'solver_info': {
            'variable_mapping': {'0': 0, '1': 1, '10': 2, ... },
            'bitstring_distributions': {
                'before_postprocessing': {'011101110010110111001110011000': 1, ...},
                'after_postprocessing': {'011011110000110101001111011000': 1, ...}
            },
            'best_parameters': {
                'beta': [-0.18054534155552715],
                'gamma': [1.4141236348317905]
            }
        },
        'resource_usage': {
            'RUNNING: MAPPING': {'CPU_TIME': 172.936},
            'RUNNING: OPTIMIZING_FOR_HARDWARE': {'CPU_TIME': 0.272},
            'RUNNING: WAITING_FOR_QPU': {'CPU_TIME': 7.798},
            'RUNNING: EXECUTING_QPU': {'QPU_TIME': 30.0},
            'RUNNING: POST_PROCESSING': {'CPU_TIME': 31.613},
        },
    }
}

ここで、辞書 solution は問題で定義された量子ビットに対応し、それらの最適化されたスピン値を示す。 metadata トランスピレーションに関する情報(2量子ビットゲートの数/深さ、使用されたゲート、アクティブな量子ビット)や、さまざまな実行時間を示しています。

市場の分割問題において、解となるビット文字列は、市場を2つの独立した領域に分割するために用いられる2進割り当てベクトルを表す。 値が1の場合、その 特定の市場は地域Aに割り当てられ、値が0の場合は地域Bに割り当てられます。最適解では、この組み合わせによって割り当てのバランスが取れ、つまり各製品について、両地域が会社の総需要の ちょうど半分ずつを受け取ることになります。


次のステップ

推奨事項
このページは役に立ちましたか?
バグや誤字の報告、またはコンテンツの要求はGitHubで行ってください。