def generate_ising(graph, feasible_configurations, decision_variables, linear_energy_ranges, quadratic_energy_ranges, smt_solver_name): """Generates the Ising model that induces the given feasible configurations. Args: graph (nx.Graph): The target graph on which the Ising model is to be built. feasible_configurations (dict): The set of feasible configurations of the decision variables. The key is a feasible configuration as a tuple of spins, the values are the associated energy. decision_variables (list/tuple): Which variables in the graph are assigned as decision variables. linear_energy_ranges (dict, optional): A dict of the form {v: (min, max, ...} where min and max are the range of values allowed to v. quadratic_energy_ranges (dict): A dict of the form {(u, v): (min, max), ...} where min and max are the range of values allowed to (u, v). smt_solver_name (str/None): The name of the smt solver. Must be a solver available to pysmt. If None, uses the pysmt default. Returns: tuple: A 4-tuple contiaing: dict: The linear biases of the Ising problem. dict: The quadratic biases of the Ising problem. float: The ground energy of the Ising problem. float: The classical energy gap between ground and the first excited state. Raises: ImpossiblePenaltyModel: If the penalty model cannot be built. Normally due to a non-zero infeasible gap. """ # we need to build a Table. The table encodes all of the information used by the smt solver table = Table(graph, decision_variables, linear_energy_ranges, quadratic_energy_ranges) # iterate over every possible configuration of the decision variables. for config in itertools.product((-1, 1), repeat=len(decision_variables)): # determine the spin associated with each varaible in decision variables. spins = dict(zip(decision_variables, config)) if config in feasible_configurations: # if the configuration is feasible, we require that the mininum energy over all # possible aux variable settings be exactly its target energy (given by the value) table.set_energy(spins, feasible_configurations[config]) else: # if the configuration is infeasible, we simply want its minimum energy over all # possible aux variable settings to be an upper bound on the classical gap. table.set_energy_upperbound(spins) # now we just need to get a solver with Solver(smt_solver_name) as solver: # add all of the assertions from the table to the solver for assertion in table.assertions: solver.add_assertion(assertion) # check if the model is feasible at all. if solver.solve(): # we want to increase the gap until we have found the max classical gap gmin = 0 gmax = sum(max(abs(r) for r in linear_energy_ranges[v]) for v in graph) gmax += sum(max(abs(r) for r in quadratic_energy_ranges[(u, v)]) for (u, v) in graph.edges) # 2 is a good target gap g = 2. while abs(gmax - gmin) >= .01: solver.push() gap_assertion = table.gap_bound_assertion(g) solver.add_assertion(gap_assertion) if solver.solve(): model = solver.get_model() gmin = float(model.get_py_value(table.gap).limit_denominator()) else: solver.pop() gmax = g g = min(gmin + .1, (gmax + gmin) / 2) else: raise pm.ImpossiblePenaltyModel("Model cannot be built") # finally we need to convert our values back into python floats. # we use limit_denominator to deal with some of the rounding # issues. theta = table.theta linear = {v: float(model.get_py_value(bias).limit_denominator()) for v, bias in iteritems(theta.linear)} quadratic = {(u, v): float(model.get_py_value(bias).limit_denominator()) for (u, v), bias in iteritems(theta.quadratic)} ground_energy = -float(model.get_py_value(theta.offset).limit_denominator()) classical_gap = float(model.get_py_value(table.gap).limit_denominator()) return linear, quadratic, ground_energy, classical_gap
def _generate_ising(graph, table, decision, min_classical_gap, linear_energy_ranges, quadratic_energy_ranges): if not table: # if there are no feasible configurations then the gap is 0 and the model is empty h = {v: 0.0 for v in graph.nodes} J = {edge: 0.0 for edge in graph.edges} offset = 0.0 gap = 0.0 return h, J, offset, gap, {} auxiliary = [v for v in graph if v not in decision] variables = decision + auxiliary solver = pywraplp.Solver('SolveIntegerProblem', pywraplp.Solver.CBC_MIXED_INTEGER_PROGRAMMING) h = {v: solver.NumVar(linear_energy_ranges[v][0], linear_energy_ranges[v][1], 'h_%s' % v) for v in graph.nodes} J = {} for u, v in graph.edges: if (u, v) in quadratic_energy_ranges: low, high = quadratic_energy_ranges[(u, v)] else: low, high = quadratic_energy_ranges[(v, u)] J[(u, v)] = solver.NumVar(low, high, 'J_%s,%s' % (u, v)) offset = solver.NumVar(-solver.infinity(), solver.infinity(), 'offset') gap = solver.NumVar(min_classical_gap, solver.infinity(), 'classical_gap') # Let x, a be the decision, auxiliary variables respectively # Let E(x, a) be the energy of x and a # Let F be the feasible configurations of x # Let g be the classical gap # Let a*(x) be argmin_a E(x, a) - the config of aux variables that minimizes the energy with x fixed # We want: # E(x, a) >= target_energy forall x in F, forall a # E(x, a) - g >= highest_target_energy forall x not in F, forall a highest_target_energy = max(table.values()) if isinstance(table, dict) else 0 for config in itertools.product((-1, 1), repeat=len(variables)): spins = dict(zip(variables, config)) decision_config = tuple(spins[v] for v in decision) target_energy = table.get(decision_config, highest_target_energy) # the E(x, a) term coefficients = {bias: spins[v] for v, bias in h.items()} coefficients.update({bias: spins[u] * spins[v] for (u, v), bias in J.items()}) coefficients[offset] = 1 if decision_config not in table: # we want energy greater than gap for decision configs not in feasible coefficients[gap] = -1 const = solver.Constraint(target_energy, solver.infinity()) for var, coef in coefficients.items(): const.SetCoefficient(var, coef) if not auxiliary: # We have no auxiliary variables. We want: # E(x) <= target_energy forall x in F for decision_config, target_energy in table.items(): spins = dict(zip(decision, decision_config)) # the E(x, a) term coefficients = {bias: spins[v] for v, bias in h.items()} coefficients.update({bias: spins[u] * spins[v] for (u, v), bias in J.items()}) coefficients[offset] = 1 const = solver.Constraint(-solver.infinity(), target_energy) for var, coef in coefficients.items(): const.SetCoefficient(var, coef) else: # We have auxiliary variables. So that each feasible config has at least one ground we want: # E(x, a) - 100*|| a - a*(x) || <= target_energy forall x in F, forall a # we need a*(x) forall x in F a_star = {config: {v: solver.IntVar(0, 1, 'a*(%s)_%s' % (config, v)) for v in auxiliary} for config in table} for decision_config, target_energy in table.items(): for aux_config in itertools.product((-1, 1), repeat=len(variables) - len(decision)): spins = dict(zip(variables, decision_config+aux_config)) ub = target_energy # the E(x, a) term coefficients = {bias: spins[v] for v, bias in h.items()} coefficients.update({bias: spins[u] * spins[v] for (u, v), bias in J.items()}) coefficients[offset] = 1 # # the -100*|| a - a*(x) || term for v in auxiliary: # we don't have absolute value, so we check what a is and order the subtraction accordingly if spins[v] == -1: # a*(x)_v - a_v coefficients[a_star[decision_config][v]] = -200 else: # a_v - a*(x)_v assert spins[v] == 1 # sanity check coefficients[a_star[decision_config][v]] = +200 ub += 200 const = solver.Constraint(-solver.infinity(), ub) for var, coef in coefficients.items(): const.SetCoefficient(var, coef) # without loss of generality we can fix the auxiliary variables associated with # one of the feasible configurations. Do so randomly. for var in next(iter(a_star.values())).values(): val = random.randint(0, 1) const = solver.Constraint(val, val) # equality constraint const.SetCoefficient(var, 1) if auxiliary or len(table) != 2**len(decision): objective = solver.Objective() objective.SetCoefficient(gap, 1) objective.SetMaximization() _inf_gap = False else: _inf_gap = True # run solver result_status = solver.Solve() if result_status not in [solver.OPTIMAL, solver.FEASIBLE]: raise pm.ImpossiblePenaltyModel("No solution was found") # read everything back into floats h = {v: bias.solution_value() for v, bias in h.items()} J = {(u, v): bias.solution_value() for (u, v), bias in J.items()} offset = offset.solution_value() gap = float('inf') if _inf_gap else gap.solution_value() if not gap: raise pm.ImpossiblePenaltyModel("No positive gap can be found for the given model") if auxiliary: aux_configs = {config: {v: val.solution_value()*2 - 1 for v, val in a_star[config].items()} for config in table} else: aux_configs = {config: dict() for config in table} return h, J, offset, gap, aux_configs