Source code for QHyper.converter

# This work was supported by the EuroHPC PL infrastructure funded at the
# Smart Growth Operational Programme (2014-2020), Measure 4.2
# under the grant agreement no. POIR.04.02.00-00-D014/20-00


"""This module contains the Converter class with static methods for converting Problem objects to different formats.

To use different solvers, but with the same Problem representation, some form 
of conversion is needed. This module provides methods for converting Problem
objects to ConstrainedQuadraticModel, DiscreteQuadraticModel, and QUBO formats.

.. autosummary:: 
    :toctree: generated
    
    Converter

"""


from typing import cast

import dimod
import re
import warnings
from dimod import ConstrainedQuadraticModel, DiscreteQuadraticModel
from QHyper.polynomial import Polynomial
from QHyper.constraint import (
    Constraint, SLACKS_LOG_2, UNBALANCED_PENALIZATION, Operator)
from QHyper.problems.base import Problem
import numpy as np


class ProblemWarning(Warning):
    pass


[docs] class Converter: @staticmethod def calc_slack_coefficients(constant: int) -> list[int]: num_slack = int(np.floor(np.log2(constant))) slack_coefficients = [2**j for j in range(num_slack)] if constant - 2**num_slack >= 0: slack_coefficients.append(constant - 2**num_slack + 1) return slack_coefficients @staticmethod def use_slacks(const: int | float, label: str) -> Polynomial: if const <= 0 or not int(const) == const: raise ValueError("Const must be a positive integer") const = int(const) slack_coefficients = Converter.calc_slack_coefficients(const) return Polynomial( {(f"{label}_{i}",): v for i, v in enumerate(slack_coefficients)} ) @staticmethod def apply_slacks( constraint: Constraint, weight: list[float] ) -> Polynomial: if len(weight) != 1: raise ValueError("Weight must be a list of length 1") rhs_without_const, rhs_const = constraint.rhs.separate_const() lhs = constraint.lhs - rhs_without_const slacks = Converter.use_slacks(rhs_const, constraint.label) return weight[0] * (lhs + slacks - rhs_const) ** 2 @staticmethod def use_unbalanced_penalization( constraint: Constraint, weight: list[float] ) -> Polynomial: lhs = constraint.lhs - constraint.rhs return weight[0]*lhs + weight[1]*lhs**2 @staticmethod def assign_penalty_weights_to_constraints( constraints_weights: list[float], constraints: list[Constraint] ) -> list[tuple[list[float], Constraint]]: weights_constraints_list = [] idx = 0 group_to_weight: dict[int, list[float]] = {} for constraint in constraints: if constraint.method_for_inequalities == UNBALANCED_PENALIZATION: if constraint.group == -1: weights = constraints_weights[idx: idx + 2] idx += 2 elif constraint.group in group_to_weight: weights = group_to_weight[constraint.group] else: weights = constraints_weights[idx: idx + 2] group_to_weight[constraint.group] = weights idx += 2 weights_constraints_list.append( (weights, constraint) ) else: if constraint.group == -1: weights = [constraints_weights[idx]] idx += 1 elif constraint.group in group_to_weight: weights = group_to_weight[constraint.group] else: weights = [constraints_weights[idx]] group_to_weight[constraint.group] = weights idx += 1 weights_constraints_list.append((weights, constraint)) return weights_constraints_list @staticmethod def create_qubo(problem: Problem, penalty_weights: list[float]) -> Polynomial: of_weight = penalty_weights[0] if len(penalty_weights) else 1 result = float(of_weight) * problem.objective_function constraints_penalty_weights = penalty_weights[1:] for weight, constraint in Converter.assign_penalty_weights_to_constraints( constraints_penalty_weights, problem.constraints ): if constraint.operator == Operator.EQ: result += float(weight[0]) * ( constraint.lhs - constraint.rhs) ** 2 continue lhs = constraint.lhs - constraint.rhs if constraint.operator == Operator.GE: lhs = -lhs if constraint.method_for_inequalities == SLACKS_LOG_2: result += Converter.apply_slacks(constraint, weight) elif (constraint.method_for_inequalities == UNBALANCED_PENALIZATION): result += Converter.use_unbalanced_penalization( constraint, weight ) return result @staticmethod def to_cqm(problem: Problem) -> ConstrainedQuadraticModel: binary_polynomial = dimod.BinaryPolynomial( problem.objective_function.terms, dimod.BINARY ) cqm = dimod.make_quadratic_cqm(binary_polynomial) variables = problem.objective_function.get_variables() for constraint in problem.constraints: variables.update(constraint.lhs.get_variables()) for variable in variables: cqm.add_variable(dimod.BINARY, str(variable)) for i, constraint in enumerate(problem.constraints): lhs = [tuple([*key, value]) for key, value in constraint.lhs.terms.items()] cqm.add_constraint(lhs, constraint.operator.value, label=i) return cqm @staticmethod def to_dimod_qubo(problem: Problem, lagrange_multiplier: float = 10 ) -> tuple[dict[tuple[str, ...], float], float]: cqm = Converter.to_cqm(problem) bqm, _ = dimod.cqm_to_bqm( cqm, lagrange_multiplier=lagrange_multiplier) return cast(tuple[dict[tuple[str, ...], float], float], bqm.to_qubo()) # (qubo, offset)
[docs] @staticmethod def to_dqm(problem: Problem, cases: int = 1) -> DiscreteQuadraticModel: """ Convert problem to DQM format. Attributes ---------- problem : Problem The problem to be solved. Objective funtion variables should be written in the format <str><int> (e.g. x10, yz1). cases: int, default 1 Number of variable cases (values) 1 is denoting binary variable. """ def binary_to_discrete(v: str) -> str: for i in range(len(v)): if v[i].isdigit(): break prefix = v[:i] numeric_part = v[i:] discrete_id = int(numeric_part) // cases new_v = prefix + str(discrete_id) return new_v def extract_number(element) -> int: match = re.search(r'(\d+)', element) prefix = element[:match.start()] number = int(match.group(1)) return (prefix, number) if problem.constraints: warnings.warn( "Defined problem has constraints. DQM does not support" " constraints, it only supports objective functions!", ProblemWarning ) pattern = re.compile(r'^[a-zA-Z]+\d+$') for variable in problem.objective_function.get_variables(): if not pattern.match(variable): raise ValueError( f"Objective funtion variable '{variable}'" "should be written in the format <str><int> (e.g. x10, yz1)." ) dqm = dimod.DiscreteQuadraticModel() objective_function_variables = sorted( problem.objective_function.get_variables(), key=extract_number) variables = [ binary_to_discrete(str(v)) for v in objective_function_variables[:: cases] ] cases_offset = cases == 1 for variable in variables: if variable not in dqm.variables: dqm.add_variable(cases + cases_offset, variable) for vars, bias in problem.objective_function.terms.items(): s_i, *s_j = vars x_i = binary_to_discrete(s_i) xi_idx: int = cast(int, dqm.variables.index(x_i)) if s_j: x_j = binary_to_discrete(*s_j) xj_idx: int = cast(int, dqm.variables.index(x_j)) dqm.set_quadratic( dqm.variables[xi_idx], dqm.variables[xj_idx], {(case, case): bias for case in range(cases + cases_offset)}, ) else: dqm.set_linear( dqm.variables[xi_idx], [bias] * (cases + cases_offset), ) return dqm