Source code for qubolite.preprocessing

from functools import partial

import igraph  as ig
import numpy   as np
import portion as P

from . import qubo
from .bounds import (
    lb_roof_dual,
    lb_negative_parameters,
    ub_local_descent,
    ub_sample)

from ._heuristics import MatrixOrder, HEURISTICS
from ._misc       import get_random_state
from .assignment  import partial_assignment
from .solving     import solution_t

################################################################################
# Dynamic Range Compression                                                    #
################################################################################


def _get_random_index_pair(matrix_order, npr):
    row_indices, column_indices = np.where(np.invert(np.isclose(matrix_order.matrix, 0)))
    try:
        random_index = npr.integers(row_indices.shape[0])
        i, j = row_indices[random_index], column_indices[random_index]
    except ValueError:
        i, j = 0, 0
    return i, j

def _compute_change(matrix_order, npr, heuristic=None, decision='heuristic', **bound_params):
    if decision == 'random':
        i, j = _get_random_index_pair(matrix_order, npr)
        change = _compute_index_change(matrix_order, i, j, heuristic=heuristic, **bound_params)
    elif decision == 'heuristic':
        order_indices = matrix_order.dynamic_range_impact()
        indices = matrix_order.to_matrix_indices(order_indices, matrix_order.matrix.shape[0])
        changes = [_compute_index_change(matrix_order, x[0], x[1],
                                         heuristic=heuristic,
                                         **bound_params) for x in indices]
        drs = [_dynamic_range_change(x[0], x[1], changes[index],
                                     matrix_order) for index, x in enumerate(indices)]
        if np.any(drs):
            index = np.argmax(drs)
            i, j = indices[index]
            change = changes[index]
        else:
            i, j = _get_random_index_pair(matrix_order, npr)
            change = _compute_index_change(matrix_order, i, j,
                                           heuristic=heuristic,
                                           **bound_params)
    else:
        raise NotImplementedError
    return i, j, change

def _compute_pre_opt_bounds(Q, i, j, **kwargs):
    lower_bound = {
        'roof_dual': lb_roof_dual,
        'min_sum': lb_negative_parameters
    }[kwargs.get('lower_bound', 'roof_dual')]
    upper_bound = {
        'local_descent': ub_local_descent,
        'sample': ub_sample
    }[kwargs.get('upper_bound', 'local_descent')]
    lower_bound = partial(lower_bound, **kwargs.get('lower_bound_kwargs', {}))
    upper_bound = partial(upper_bound, **kwargs.get('upper_bound_kwargs', {}))
    change_diff = kwargs.get('change_diff', 1e-08)
    Q = qubo(Q)
    if i != j:
        # Define sub-qubos
        Q_00, c_00, _ = Q.clamp({i: 0, j: 0})
        Q_01, c_01, _ = Q.clamp({i: 0, j: 1})
        Q_10, c_10, _ = Q.clamp({i: 1, j: 0})
        Q_11, c_11, _ = Q.clamp({i: 1, j: 1})
        # compute bounds
        upper_00 = upper_bound(Q_00) + c_00
        upper_01 = upper_bound(Q_01) + c_01
        upper_10 = upper_bound(Q_10) + c_10
        lower_11 = lower_bound(Q_11) + c_11
        upper_or = min(upper_00, upper_01, upper_10)

        lower_00 = lower_bound(Q_00) + c_00
        lower_01 = lower_bound(Q_01) + c_01
        lower_10 = lower_bound(Q_10) + c_10
        upper_11 = upper_bound(Q_11) + c_11
        lower_or = min(lower_00, lower_01, lower_10)

        suboptimal = lower_11 > min(upper_00, upper_01, upper_10)
        optimal = upper_11 < min(lower_00, lower_01, lower_10)
        upper_bound = float('inf') if suboptimal else lower_or - upper_11 - change_diff
        lower_bound = -float('inf') if optimal else upper_or - lower_11 + change_diff
    else:
        # Define sub-qubos
        Q_0, c_0, _ = Q.clamp({i: 0})
        Q_1, c_1, _ = Q.clamp({i: 1})
        # Compute bounds
        upper_0 = upper_bound(Q_0) + c_0
        lower_1 = lower_bound(Q_1) + c_1

        lower_0 = lower_bound(Q_0) + c_0
        upper_1 = upper_bound(Q_1) + c_1
        suboptimal = lower_1 > upper_0
        optimal = upper_1 < lower_0
        upper_bound = float("inf") if suboptimal else lower_0 - upper_1 - change_diff
        lower_bound = -float("inf") if optimal else upper_0 - lower_1 + change_diff
    return lower_bound, upper_bound

def _compute_pre_opt_bounds_all(Q, **kwargs):
    indices = np.triu_indices(Q.shape[0])
    bounds = np.zeros((len(indices[0]), 2))
    for index, index_pair in enumerate(zip(indices[0], indices[1])):
        i, j = index_pair[0], index_pair[1]
        res = _compute_pre_opt_bounds(Q, i=i, j=j, **kwargs)
        if isinstance(res[0], qubo):
            print(f'Optimal configuration is found, clamped QUBO should be returned!')
            # return res
        else:
            bounds[index, 0] = res[0]
            bounds[index, 1] = res[1]
    return bounds

def _dynamic_range_change(i, j, change, matrix_order):
    old_dynamic_range = matrix_order.dynamic_range
    matrix = matrix_order.update_entry(i, j, change, True)
    new_dynamic_range = qubo(matrix).dynamic_range()
    dynamic_range_diff = old_dynamic_range - new_dynamic_range
    return dynamic_range_diff

def _check_to_next_increase(matrix_order, change, i, j):
    current_entry = matrix_order.matrix[i, j]
    new_entry = current_entry + change
    lower_index = np.searchsorted(matrix_order.unique, new_entry, side='right')
    lower_entry = matrix_order.unique[lower_index - 1]
    min_dis = matrix_order.min_distance
    lower_interval = P.open(lower_entry - min_dis, lower_entry + min_dis)
    try:
        upper_entry = matrix_order.unique[lower_index]
        upper_interval = P.open(upper_entry - min_dis, upper_entry + min_dis)
        forbidden_interval = lower_interval | upper_interval
    except IndexError:
        forbidden_interval = lower_interval
    possible_interval = P.openclosed(-P.inf, new_entry)
    difference = possible_interval.difference(forbidden_interval)
    difference = difference | P.singleton(lower_entry)
    return difference.upper - current_entry

def _check_to_next_decrease(matrix_order, change, i, j):
    current_entry = matrix_order.matrix[i, j]
    new_entry = current_entry + change
    upper_index = np.searchsorted(matrix_order.unique, new_entry, side='left')
    upper_entry = matrix_order.unique[upper_index]
    min_dis = matrix_order.min_distance
    upper_interval = P.open(upper_entry - min_dis, upper_entry + min_dis)
    try:
        lower_entry = matrix_order.unique[upper_index - 1]
        lower_interval = P.open(lower_entry - min_dis, lower_entry + min_dis)
        forbidden_interval = lower_interval | upper_interval
    except IndexError:
        forbidden_interval = upper_interval
    possible_interval = P.openclosed(new_entry, P.inf)
    difference = possible_interval.difference(forbidden_interval)
    difference = difference | P.singleton(upper_entry)
    return difference.lower - current_entry

def _compute_index_change(matrix_order, i, j, heuristic=None, **kwargs):
    # Decide whether to increase or decrease
    increase = heuristic.decide_increase(matrix_order, i, j)
    # Bounds on changes based on reducing the dynamic range
    dyn_range_change = heuristic.compute_change(matrix_order, i, j, increase)
    # Bounds on changes based on preserving the optimum
    if increase:
        _, pre_opt_change = _compute_pre_opt_bounds(matrix_order.matrix, i, j, **kwargs)
    else:
        pre_opt_change, _ = _compute_pre_opt_bounds(matrix_order.matrix, i, j, **kwargs)
    set_to_zero = heuristic.set_to_zero()
    if increase:
        change = min(pre_opt_change, dyn_range_change)
        if change < 0 or np.isclose(change, 0):
            change = 0
        elif 0 > matrix_order.matrix[i, j] > - change and set_to_zero:
            change = - matrix_order.matrix[i, j]
        else:
            change = _check_to_next_increase(matrix_order, change, i, j)
    else:
        change = max(pre_opt_change, dyn_range_change)
        if change > 0 or np.isclose(change, 0):
            change = 0
        elif 0 < matrix_order.matrix[i, j] < - change and set_to_zero:
            change = - matrix_order.matrix[i, j]
        else:
            change = _check_to_next_decrease(matrix_order, change, i, j)
    return change

[docs]def reduce_dynamic_range( Q: qubo, iterations=100, heuristic='greedy0', random_state=None, decision='heuristic', callback=None, **kwargs): """Iterative procedure for reducing the dynammic range of a given QUBO, while preserving an optimum, described in `Mücke et al. (2023) <http://arxiv.org/abs/2307.02195>`__. For this, at every step we choose a specific QUBO weight and change it according to some heuristic. Args: Q (qubolite.qubo): QUBO iterations (int, optional): Number of iterations. Defaults to 100. heuristic (str, optional): Used heuristic for computing weight change. Possible heuristics are 'greedy0', 'greedy' and 'order'. Defaults to 'greedy0'. random_state (optional): A numerical or lexical seed, or a NumPy random generator. Defaults to None. decision (str, optional): Method for deciding which QUBO weight to change next. Possibilities are 'random' and 'heuristic'. Defaults to 'heuristic'. callback (optional): Callback function which obtains the following inputs after each step: i (int), j (int) , change (float), current matrix order (MatrixOrder), current iteration (int). Defaults to None. **kwargs (optional): Keyword arguments for determining the upper and lower bound computations of the optimal QUBO value. Keyword Args: change_diff (float): Distance to optimum for avoiding numerical madness. Defaults to 1e-8. upper_bound (str): Method for upper bound, possibilities are 'local_descent' and 'sample'. Defaults to 'local_descent'. lower_bound (str): Method for lower bound, possibilities are 'roof_dual' and 'min_sum'. Defaults to 'roof_dual'. upper_bound_kwargs (dict): Additional keyword arguments for upper bound method. lower_bound_kwargs (dict): Additional keyword arguments for lower bound method. Returns: qubolite.qubo: Compressed QUBO with reduced dynamic range. """ try: heuristic = HEURISTICS[heuristic] except KeyError: raise ValueError(f'Unknown heuristic "{heuristic}", available are "greedy0", "greedy" and "order"') npr = get_random_state(random_state) Q_copy = Q.copy() matrix_order = MatrixOrder(Q_copy.m) stop_update = False matrix_order.matrix = np.round(matrix_order.matrix, decimals=8) for it in range(iterations): if not stop_update: i, j, change = _compute_change(matrix_order, heuristic=heuristic, npr=npr, decision=decision, **kwargs) stop_update = matrix_order.update_entry(i, j, change) if callback is not None: callback(i, j, change, matrix_order, it) else: break return qubo(matrix_order.matrix)
################################################################################ # QPRO+ Algorithm # ################################################################################ def _calculate_Dplus_and_Dminus(Q: np.ndarray): """Calculates a bound for each variable of the possible impact of the variable. Args: Q (np.ndarray): Array that contains the QUBO Returns: np.ndarray: A 2d array, where the first column contains the positive bounds and the second column the negative bounds """ Q_plus = np.multiply(Q, Q > 0) np.fill_diagonal(Q_plus, 0) row_sums_plus = np.sum(Q_plus, axis = 0) coulumn_sums_plus = np.sum(Q_plus, axis = 1) d_plus = row_sums_plus + coulumn_sums_plus Q_minus = np.multiply(Q, Q < 0) np.fill_diagonal(Q_minus, 0) row_sums_minus = np.sum(Q_minus, axis = 0) coulumn_sums_minus = np.sum(Q_minus, axis = 1) d_minus = row_sums_minus + coulumn_sums_minus return np.vstack((d_minus, d_plus)) def _reduceQ(Q: np.ndarray, assignment: tuple, D_list: np.ndarray, indices: list): """Given an assignment, updates the Q matrix so that the QUBO stays equivalent but the assigned variable can be removed. Args: Q (np.ndarray): containing the QUBO assignment (tuple): first element is the variabe index, second elment is the assignment, i.e. 0 or 1 D_list (np.ndarray): containing bounds as calculated by calculate_Dplus_and_Dminus Returns: np.ndarray: updated Q np.ndarray: updated D_list list: updated indices """ #does change input D_list #value is assumed to be either 0 or 1 i = assignment[0] value = assignment[1] if value == 1: Q[indices, indices] += Q[indices, i] + Q[i, indices] D_list = _D_list_remove(Q, D_list, i) indices.remove(i) # drop node i return Q, D_list, indices def _D_list_remove(Q: np.ndarray, D_list: np.ndarray, i: int): """Removes influence of variable i in D_list. Used in reduceQ2_5 and reduceQ2_6 Args: Q (np.ndarray): containing the QUBO D_list (np.ndarray): containing bounds as calculated by calculate_Dplus_and_Dminus i (int): variable whose values are to be removed Returns: np.ndarray: updated D_list """ d_ij = Q[:, i] + Q[i, :] d_plus = d_ij > 0 d_minus = d_ij < 0 D_list[1, d_plus] -= d_ij[d_plus] D_list[0, d_minus] -= d_ij[d_minus] return D_list def _D_list_correct_i( new_i_row_column: np.ndarray, D_list: np.ndarray, i: int, h:int, indices: list): """Corrects the entry of the i-th variable in D_list in reduceQ2_5 and reduceQ2_6 Args: new_i_row_column (np.ndarray): containing the updated row and column of variable i D_list (np.ndarray): containing bounds as calculated by calculate_Dplus_and_Dminus i (int): variable whose value is corrected h (int): variable that is removed by 2.5 or 2.6 indices (list): of variables that have not been assigned yet Returns: np.ndarray: updated D_list """ #add new elements d_hj new_i_row_column[i] = 0 new_i_row_column[h] = 0 positive = new_i_row_column > 0 negative = new_i_row_column < 0 D_list[1, positive] += new_i_row_column[positive] D_list[0, negative] += new_i_row_column[negative] #fix D_i^+ and D_i^- D_list[1, i] = np.sum(new_i_row_column[indices][positive[indices]]) D_list[0, i] = np.sum(new_i_row_column[indices][negative[indices]]) return D_list def _reduceQ2_5(Q: np.ndarray, assignment: tuple, D_list: np.ndarray, indices: list): """ Implements updates according to rule 2.5, i.e. assumes x_h = 1 - x_i and updates QUBO accordingly Args: Q (np.ndarray): containing the QUBO assignment (tuple): first element is the variabe h, second elment is the variable h D_list (np.ndarray): containing bounds as calculated by calculate_Dplus_and_Dminus indices (list): of variables that have not been assigned yet Returns: np.ndarray: updated Q np.ndarray: updated D_list list: updated indices """ #x_h = 1 - x_i i = assignment[1] h = assignment[0] c_i = Q[i,i] c_h = Q[h,h] #D_list update #remove h and i from all calculations D_list = _D_list_remove(Q, D_list, i) D_list = _D_list_remove(Q, D_list, h) new_i_row_column = (Q[:, i] + Q[i, :]) - (Q[:, h] + Q[h, :]) Q[:i, i] = new_i_row_column[:i] Q[i, i+1:] = new_i_row_column[i+1:] Q[indices, indices] += (Q[indices, h] + Q[h, indices]) Q[i,i] = c_i - c_h #add new elements d_hj and fix D_i^+ and D_i^- D_list = _D_list_correct_i(new_i_row_column, D_list, i, h, indices) #remove variable x_h from matrix by deleting row and column h indices.remove(h) return Q, D_list, indices def _reduceQ2_6( Q: np.ndarray, assignment: tuple, D_list: np.ndarray, indices:list): """ Implements updates according to rule 2.6, i.e. assumes x_h = x_i and updates QUBO accordingly Args; Q (np.ndarray): containing the QUBO assignment (tuple): first element is the variabe h, second elment is the variable h D_list (np.ndarray): containing bounds as calculated by calculate_Dplus_and_Dminus indices (list): variables that have not been assigned yet Returns: np.ndarray: updated Q np.ndarray: updated D_list np.ndarray: updated indices """ #x_h = x_i i = assignment[1] h = assignment[0] c_i = Q[i,i] c_h = Q[h,h] d_hi = Q[h, i] #D_list update #remove h and i from all calculations D_list = _D_list_remove(Q, D_list, i) D_list = _D_list_remove(Q, D_list, h) #calculate new x_i values as d_ij + d_hj new_i_row_column = (Q[:, i] + Q[i, :]) + (Q[:, h] + Q[h, :]) Q[:i, i] = new_i_row_column[:i] Q[i, i+1:] = new_i_row_column[i+1:] Q[i,i] = c_i + c_h + d_hi #add new elements d_hj and fix D_i^+ and D_i^- D_list = _D_list_correct_i(new_i_row_column, D_list, i, h, indices) #remove variable x_h from matrix by deleting row and column h indices.remove(h) return Q, D_list, indices def _assign_1( Qmatrix: np.ndarray, D_list: np.ndarray, indices: list, assignments: dict, last_assignment: int, c_0: int, i: int, c_i: int): assignments["x_" + str(i)] = 1 #store what assignment is being made last_assignment = i Qmatrix, D_list, indices = _reduceQ(Qmatrix, (i, 1), D_list, indices) c_0 += c_i return Qmatrix, D_list, indices, assignments, last_assignment, c_0 def _assign_0( Qmatrix: np.ndarray, D_list: np.ndarray, indices: list, assignments: dict, last_assignment: int, i: int): assignments["x_" + str(i)] = 0 #store what assignment is beeing made last_assignment = i Qmatrix, D_list, indices = _reduceQ(Qmatrix, (i, 0), D_list, indices) return Qmatrix, D_list, indices, assignments, last_assignment def _apply_rule2_5( Qmatrix: np.ndarray, D_list: np.ndarray, indices: list, assignments: dict, last_assignment: int, c_0: int, i: int, h: int, c_h: int): assignments["x_" + str(i)] = f'[!{h}]' #store what assignment is being made assignments["x_" + str(h)] = f'[!{i}]' #store what assignment is being made last_assignment = i+1 Qmatrix, D_list, indices = _reduceQ2_5(Qmatrix, (h, i), D_list, indices) c_0 += c_h return Qmatrix, D_list, indices, assignments, last_assignment, c_0 def _apply_rule2_6( Qmatrix: np.ndarray, D_list: np.ndarray, indices: list, assignments: dict, last_assignment: int, i: int, h: int): assignments["x_" + str(i)] = f'[{h}]' #store what assignment is being made assignments["x_" + str(h)] = f'[{i}]' #store what assignment is being made last_assignment = i+1 Qmatrix, D_list, indices = _reduceQ2_6(Qmatrix, (h, i), D_list, indices) return Qmatrix, D_list, indices, assignments, last_assignment
[docs]def qpro_plus(Q: qubo): """Implements the routine applying rules described in `Glover et al. (2018) <https://www.sciencedirect.com/science/article/pii/S0377221717307567>`__ for reducing the QUBO size by applying logical implications. Args: Q (qubo): QUBO instance to be reduced. Returns: Instance of :class:`assignment.partial_assignment` representing the reduction. See example. Example: >>> import qubolite as ql >>> Q = ql.qubo.random(32, density=0.2, random_state='example') >>> PA = ql.preprocessing.qpro_plus(Q) >>> print(f'{PA.num_fixed} variables were eliminated!') 9 variables were eliminated! >>> Q_reduced, offset = PA.apply(Q) >>> Q_reduced.n 23 >>> x = ql.bitvec.from_string('10011101011011110001011') >>> Q_reduced(x)+offset -0.5215481745331401 >>> Q(PA.expand(x)) -0.5215481745331385 """ #assumes QUBO to be upper triangluar #paper assumes maximazation hence we need to flip the sign for minimization m = -Q.m assignments = dict() # dict of assignments hList = list() c_0 = 0 # offset to restore original energy function #calculate D_i^+ and D_i^- for each variable D_list = _calculate_Dplus_and_Dminus(m) last_assignment = 0 #index of the most recent variable that has been assigned #variables that have not been assigned yet indices = list(range(Q.n)) while (last_assignment != -1): last_assignment = -1 #apply rules hList.clear() for i in indices.copy(): #apply rule 1.0 and 2.0 c_i = m[i,i] if c_i + D_list[0, i] >= 0: #rule 1.0 #x_i = 1 m, D_list, indices, _, last_assignment, c_0 = _assign_1( m, D_list, indices, assignments, last_assignment, c_0, i, c_i) elif c_i + D_list[1, i] <= 0: #rule 2.0 #x_i = 0 (m, D_list, indices, _, last_assignment) = _assign_0( m, D_list, indices, assignments, last_assignment, i) else: for h in hList: #define variables: d_ih = m[h, i] c_i = m[i, i] c_h = m[h, h] if c_h + D_list[0, h] >= 0: #rule 1.0 #x_h = 1 m, D_list, indices, _, last_assignment, c_0 = _assign_1( m, D_list, indices, assignments, last_assignment, c_0, h, c_h) # drop node h hList.remove(h) elif c_h + D_list[1, h] <= 0: #rule 2.0 #x_h = 0 m, D_list, indices, _, last_assignment = _assign_0( m, D_list, indices, assignments, last_assignment, h) # drop node h hList.remove(h) #rule 3.1 elif d_ih >= 0 and c_i + c_h - d_ih + D_list[1, i] + D_list[1, h] <= 0: #set x_i = x_h = 0 m, D_list, indices, _, last_assignment = _assign_0( m, D_list, indices, assignments, last_assignment, i) break #rule 3.2 elif d_ih < 0 and -c_i + c_h + d_ih - D_list[0, i] + D_list[1, h] <= 0: #set x_i = 1, x_h = 0 m, D_list, indices, _, last_assignment, c_0 = _assign_1( m, D_list, indices, assignments, last_assignment, c_0, i, c_i) break #rule 3.3 elif d_ih < 0 and -c_h + c_i + d_ih - D_list[0, h] + D_list[1, i] <= 0: #set x_i = 0, x_h = 1 m, D_list, indices, _, last_assignment = _assign_0( m, D_list, indices, assignments, last_assignment, i) break #rule 3.4 elif d_ih >= 0 and -c_i - c_h - d_ih - D_list[0, h] - D_list[0, i] <= 0: #set x_i = 1, x_h = 1 m, D_list, indices, _, last_assignment, c_0 = _assign_1( m, D_list, indices, assignments, last_assignment, c_0, i, c_i) break #rule 2.5 elif (d_ih < 0 and (c_i - d_ih + D_list[0][i] >= 0 or c_h - d_ih + D_list[0][h] >= 0) and (c_i + d_ih + D_list[1][i] <= 0 or c_h + d_ih + D_list[1][h]) <= 0): # x_h = 1 - x_i m, D_list, indices, _, last_assignment, c_0 = _apply_rule2_5( m, D_list, indices, assignments, last_assignment, c_0, i, h, c_h) # drop node h hList.remove(h) #rule 2.6 elif (d_ih > 0 and (c_i-d_ih+D_list[1][i] <= 0 or c_h+d_ih+D_list[0][h] >= 0) and (c_i+d_ih+D_list[0][i] >= 0 or c_h-d_ih+D_list[1][h]) <= 0): # x_h = x_i m, D_list, indices, _, last_assignment = _apply_rule2_6( m, D_list, indices, assignments, last_assignment, i, h) # drop node h hList.remove(h) if last_assignment != i: # this means x_i could not be assigned hList.append(i) assignment_pattern = ''.join([str(assignments.get(f'x_{i}', '*')) for i in range(Q.n)]) return partial_assignment.from_expression(assignment_pattern)
# reduced qubo: qubo(-m[np.ix_(indices, indices)]) # offset: -c_0 ################################################################################ # Graph Transformation # ################################################################################ """ def try_solve_polynomial(Q: qubo): if 'quadratic_nonpositive' in Q.properties: q = np.triu(Q.m, 1) c = np.diag(Q.m) s, t = Q.n, Q.n+1 # source and target indices r = q.sum(0) + q.sum(1) edges = np.r_[ np.stack(np.where(~np.isclose(q, 0))).T, # (i, j) np.c_[np.full(Q.n, s), np.arange(Q.n)], # (s, j) np.c_[np.arange(Q.n), np.full(Q.n, t)]] # (i, t) weights = np.r_[ -q[np.where(~np.isclose(q, 0))], # (i, j) np.minimum(0, -r-c), # (s, j) np.minimum(0, r+c)] # (i, t) #print(f'Q:\n{q}\nc: {c}\nr: {r}\nedges:\n{edges}\nweights:\n{weights}') G = ig.Graph(n=Q.n+2, edges=edges, edge_attrs={'w': weights}) # note the "-" cut = G.st_mincut(source=s, target=t, capacity='w') #print(cut.partition) ones = cut.partition[0][:-1] # omit s at the end x = np.zeros(Q.n) x[ones] = 1 raise NotImplementedError('Not working') return solution_t(x, Q(x)) # XXX doesn't yield same result as brute force... return None """