Skip to main content
IBM Quantum Platform

Resolva o problema da segmentação de mercado com o Parity Twine Optimizer da ParityQC

Estimativa de tempo de execução: 10 segundos em um processador Nighthawk r2. (OBSERVAÇÃO: Trata-se apenas de uma estimativa. (O tempo de execução pode variar.)


Resultados do aprendizado


Segundo plano

Este tutorial mostra como resolver o problema de divisão de mercado usando o Otimizador Parity Twine da ParityQC.

A instância do problema foi obtida da QOBLIB — Quantum Optimization Benchmarking Library.

Problema de divisão do mercado

O problema da divisão de mercado é um problema de alocação de recursos do mundo real, da classe NP-difícil, e tornou-se uma referência para algoritmos de otimização quântica. Isso representa um desafio logístico de grande importância: como dividir um cenário complexo de clientes e produtos em territórios gerenciáveis e equilibrados.

O objetivo é dividir os mercados d nn em duas regiões de vendas equilibradas, de modo que cada região receba exatamente metade da demanda total pelos produtos d mm. A solução consiste na configuração específica que permite obter a distribuição mais uniforme possível da demanda por produtos, possibilitando que uma empresa implemente uma estratégia de logística e alocação de pessoal que mantenha o equilíbrio entre ambas as regiões, minimizando riscos como a escassez localizada de produtos ou o excesso de estoque nos armazéns.

À medida que o número de mercados e produtos aumenta, o número de permutações possíveis cresce exponencialmente, tornando difícil encontrar a melhor divisão por meio de buscas exaustivas tradicionais.

Formulação matemática

Seja AA uma matriz de tipo m×nm \times n que representa a demanda por produtos nos diversos mercados, em que AijA_{ij} é a demanda pelo produto ii no mercado jj.

Define-se um vetor de atribuição binário, x=[x1,x2,…,xn]T∈{0,1}nx = [x_1, x_2, \dots, x_n]^T \in \{0, 1\}^n, em que:

  • xj=1x_j = 1 atribui o mercado jj à Região A.
  • xj=0x_j = 0 atribui o mercado jj à Região B.

Seja d=[d1,d2,…,dm]Td = [d_1, d_2, \dots, d_m]^T o vetor de demanda total para cada produto, calculado como d=A⋅1d = A \cdot \mathbf{1}. O volume de vendas alvo por região para o produto ii é exatamente di2\frac{d_i}{2}.

A restrição de otimização ou de viabilidade exige que o total de vendas alocado à Região A corresponda exatamente à metade da demanda total de cada produto:

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

Na prática, como raramente é possível realizar uma divisão exata, o problema é formulado de modo a minimizar a violação ao quadrado da restrição (a função de custo):

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.

Ao expandir isso, obtém-se uma forma equivalente a um problema de otimização binária quadrática sem restrições (QUBO).

Após a resolução, o vetor de solução xx determina a qual região o mercado é atribuído. Essa é a configuração que permite a distribuição mais equilibrada possível da demanda pelo produto.


Requisitos

Antes de iniciar este tutorial, certifique-se de que os seguintes itens estejam instalados:

  • Qiskit Functions Catalog IBM Cliente (pip install qiskit-ibm-catalog)
  • Componente adicional do Qiskit: Optimization Mapper (pip install qiskit_addon_opt_mapper)
  • NumPy (pip install numpy)

Você também precisa de permissão para acessar a função “ ParityQC ” do Twine Optimizer. Para solicitar acesso, preencha este formulário.


Instalação

(Este código pressupõe que você já tenha salvo sua conta no seu ambiente local.)

Primeiro, importe todos os pacotes necessários para este tutorial.

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

Carregue o Parity Twine Optimizer do catálogo “ Qiskit Functions ”:

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

Etapa 1: Definir o problema como uma função-objetivo

Obtenha uma instância do problema de divisão de mercado da QOBLIB — Biblioteca de Referência de Otimização Quântica — da seguinte maneira.

A função load_market_split_problem recupera um problema específico da QOBLIB e o converte em um problema 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)

A função load_market_split_problem requer as seguintes funções de análise para recuperar e processar os dados do problema de divisão de mercado da 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)

Uma vez definido, load_marketsplit_problem pode ser usado para carregar uma instância específica do problema a partir da biblioteca:

ms_instance = "ms_04_050_001"

ms_problem = load_market_split_problem(ms_instance)

Etapa 2: Converter para o formato JSON

Na primeira etapa, você obteve a forma QUBO do problema. Agora, converta-o para o formato JSON para a função do otimizador:

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

A instância QUBO do problema “Market Split” foi convertida para o formato JSON da seguinte forma:

json_ms_problem = optimization_problem_to_json(ms_problem)

Etapa 3: Resolva o problema usando o Otimizador de Cordas de Paridade

Agora que você obteve o problema de divisão de mercado e o converteu para o formato correto, pode encontrar uma solução usando o Twine Optimizer e um backend do IBM® de sua escolha.

Para executar a função, escolha um dispositivo backend adequado; por exemplo, ibm_phoenix.

Você pode usar options para ter um controle adicional (opcional) sobre o envio:

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

onde shots é um número inteiro que especifica o número de execuções do circuito, postprocessing_level determina se o pós-processamento é aplicado ao resultado, transpile_onlyespecifica se o problema é apenas transpilado para um circuito (e não resolvido), e job_tags é o rótulo usado para identificar o trabalho no site IBM Quantum® Platform.

Execute o otimizador:

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}")

Verifique o status do trabalho:

# Monitor the job status
function_job.status()

Recuperar resultados:

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

result

O resultado tem a seguinte forma:

{
    '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},
        },
    }
}

onde o dicionário solution corresponde aos qubits definidos no problema e fornece seus valores de spin otimizados. metadata fornece informações sobre a transpilação (contagem de portas de dois qubits/profundidade, portas utilizadas, qubits ativos) e vários tempos de execução.

No contexto do problema da divisão de mercados, a sequência de bits da solução representa um vetor de atribuição binário utilizado para dividir os mercados em duas regiões distintas. Um valor igual a 1 atribui esse mercado específico à Região A, enquanto um valor igual a 0 o atribui à Região B. Para a solução ótima, a combinação equilibra a divisão, o que significa que ambas as regiões recebem exatamente metade da demanda total da empresa por cada produto.


Próximas etapas

Recomendações
Esta página foi útil?
Relate um bug, erro de digitação ou solicite conteúdo no GitHub.