Beispiel #1
0
    def solve_dp(self, items, total_charge_diff, pos_total_charge, max_sum,
                 blowup):

        solutionTime = -perf_counter()
        # lower and upper capacity limits
        upper = round(blowup * (pos_total_charge + total_charge_diff))
        lower = max(0, round(blowup * (pos_total_charge - total_charge_diff)))

        # check if feasible solutions may exist
        reachable = round(blowup * max_sum)
        if upper < 0 or lower > reachable:
            # sum of min weights over all sets is larger than the upper bound
            # or sum of max weights over all sets is smaller than the lower bound
            raise AssignmentError('Could not solve DP problem. Please retry'
                                  ' with a SimpleCharger')

        # init DP and traceback tables
        dp = [0] + [-float('inf')] * upper
        tb = [[] for _ in range(upper + 1)]

        # DP
        # iterate over all sets
        for items_l in items:
            # iterate over all capacities
            for d in range(upper, -1, -1):
                try:
                    # find max. profit with capacity i over all items j in set k
                    idx, dp[d] = max(((item[0], dp[d - item[1]] + item[2])
                                      for item in items_l if d - item[1] >= 0),
                                     key=lambda x: x[1])
                    # copy old traceback indices and add new index to traceback
                    if dp[d] >= 0:
                        tb[d] = tb[d - items_l[idx][1]] + [idx]
                except ValueError:
                    dp[d] = -float('inf')

        # find max profit
        max_pos, max_val = max(enumerate(dp[lower:upper + 1]),
                               key=lambda x: x[1])

        solutionTime += perf_counter()

        if max_val == -float('inf'):
            raise AssignmentError('Could not solve DP problem. Please retry'
                                  ' with a SimpleCharger')

        solution = tb[lower + max_pos]

        return solution, solutionTime
Beispiel #2
0
    def solve_dp_c(self, charge_dists, total_charge, total_charge_diff):

        import charge.c.dp as dp

        num_sets = len(charge_dists)
        num_items = sum(
            len(charges) for (_, (charges, _)) in charge_dists.items())

        weights = dp.new_doublea(num_items)
        profits = dp.new_doublea(num_items)
        sets = dp.new_ushorta(num_sets)
        solution = dp.new_ushorta(num_sets)

        offset = 0
        pos_total = total_charge
        for k, (atom, (charges,
                       frequencies)) in enumerate(charge_dists.items()):
            dp.ushorta_setitem(sets, k, len(charges))
            for i, (charge, frequency) in enumerate(zip(charges, frequencies)):
                dp.doublea_setitem(weights, offset + i, charge)
                dp.doublea_setitem(profits, offset + i, frequency)
            pos_total -= min(charges)
            offset += len(charges)

        solutionTime = -perf_counter()

        profit = dp.solve_dp(weights, profits, sets, num_items, num_sets,
                             self.__rounding_digits, total_charge,
                             total_charge_diff, solution)

        solutionTime += perf_counter()

        dp_solution = list()
        if profit >= 0:
            for k in range(num_sets):
                i = dp.ushorta_getitem(solution, k)
                dp_solution.append((k, i))

        dp.delete_doublea(weights)
        dp.delete_doublea(profits)
        dp.delete_ushorta(sets)
        dp.delete_ushorta(solution)

        if profit < 0:
            raise AssignmentError('Could not solve DP problem. Please retry'
                                  ' with a SimpleCharger')

        return dp_solution, solutionTime, num_items, (pos_total +
                                                      total_charge_diff)
Beispiel #3
0
    def _handle_error(self, no_vals: List[Atom], shells: List[int]) -> None:
        """Raises an AssignmentError if no_vals is not empty.

        Args:
            no_vals: List of atoms for which no charges could be found.
            shells: List of tried shell sizes.

        Raises:
            AssignmentError: If no_vals is not empty.
        """
        if len(no_vals) > 0:
            err = 'Could not find charges for atoms {0}.'.format(', '.join(
                map(str, no_vals)))
            if not 0 in shells:
                err += ' Please retry with a smaller "shell" parameter.'
            raise AssignmentError(err)
Beispiel #4
0
    def solve_dp_c(
        self, charge_dists: Dict[Atom, Tuple[ChargeList, WeightList, str]],
        total_charge: float, total_charge_diff: float
    ) -> Tuple[List[Tuple[int, int]], float, int, float]:
        """Solves the knapsack problem with dynamic programming implemented in C.

        Args:
            charge_dists: Charge distributions for the atoms, obtained \
                    by a Collector.
            total_charge: The total charge of the molecule.
            total_charge_diff: Maximum allowed deviation from the total charge

        Returns:
            Tuple of solution, solution time, the number of items and the scaled capacity.
        """

        import charge.c.dp as dp

        num_sets = len(charge_dists)
        num_items = sum(
            len(charges) for (_, (charges, _, _)) in charge_dists.items())

        weights = dp.new_doublea(num_items)
        profits = dp.new_doublea(num_items)
        sets = dp.new_ushorta(num_sets)
        solution = dp.new_ushorta(num_sets)

        offset = 0
        pos_total = total_charge
        for k, (atom, (charges, frequencies,
                       _)) in enumerate(charge_dists.items()):
            dp.ushorta_setitem(sets, k, len(charges))
            for i, (charge, frequency) in enumerate(zip(charges, frequencies)):
                dp.doublea_setitem(weights, offset + i, charge)
                dp.doublea_setitem(profits, offset + i, frequency)
            pos_total -= min(charges)
            offset += len(charges)

        solutionTime = -perf_counter()

        profit = dp.solve_dp(weights, profits, sets, num_items, num_sets,
                             self._rounding_digits, total_charge,
                             total_charge_diff, solution)

        solutionTime += perf_counter()

        dp_solution = list()
        if profit >= 0:
            for k in range(num_sets):
                i = dp.ushorta_getitem(solution, k)
                dp_solution.append((k, i))

        dp.delete_doublea(weights)
        dp.delete_doublea(profits)
        dp.delete_ushorta(sets)
        dp.delete_ushorta(solution)

        if profit < 0:
            raise AssignmentError(
                'Could not solve DP problem. Please retry with a SimpleCharger'
            )

        return dp_solution, solutionTime, num_items, (pos_total +
                                                      total_charge_diff)
Beispiel #5
0
    def solve_dp(items: List[List[Tuple[int, int,
                                        float]]], total_charge_diff: float,
                 pos_total_charge: float, max_sum: float, blowup: int):
        """Solves the knapsack problem with dynamic programming implemented in Python.

        Args:
            items: Knapsack items.
            total_charge_diff: Maximum allowed deviation from the total charge.
            pos_total_charge: Transformed total charge.
            max_sum: Sum of all maximal charges per item class.
            blowup: Blowup factor.

        Returns:
            Tuple of solution and solution time.
        """

        solutionTime = -perf_counter()
        # lower and upper capacity limits
        upper = round(blowup * (pos_total_charge + total_charge_diff))
        lower = max(0, round(blowup * (pos_total_charge - total_charge_diff)))

        # check if feasible solutions may exist
        reachable = round(blowup * max_sum)
        if upper < 0 or lower > reachable:
            # sum of min weights over all sets is larger than the upper bound
            # or sum of max weights over all sets is smaller than the lower bound
            raise AssignmentError(
                'Could not solve DP problem. Please retry with a SimpleCharger'
            )

        # init DP and traceback tables
        dp = [0] + [-float('inf')] * upper
        tb = [[] for _ in range(upper + 1)]

        # DP
        # iterate over all sets
        for items_l in items:
            # iterate over all capacities
            for d in range(upper, -1, -1):
                try:
                    # find max. profit with capacity i over all items j in set k
                    idx, dp[d] = max(((item[0], dp[d - item[1]] + item[2])
                                      for item in items_l if d - item[1] >= 0),
                                     key=lambda x: x[1])
                    # copy old traceback indices and add new index to traceback
                    if dp[d] >= 0:
                        tb[d] = tb[d - items_l[idx][1]] + [idx]
                except ValueError:
                    dp[d] = -float('inf')

        # find max profit
        max_pos, max_val = max(enumerate(dp[lower:upper + 1]),
                               key=lambda x: x[1])

        solutionTime += perf_counter()

        if max_val == -float('inf'):
            raise AssignmentError(
                'Could not solve DP problem. Please retry with a SimpleCharger'
            )

        solution = tb[lower + max_pos]

        return solution, solutionTime
Beispiel #6
0
    def solve_partial_charges(
            self,
            graph: nx.Graph,
            charge_dists: Dict[Atom, Tuple[ChargeList, WeightList, str]],
            total_charge: int,
            total_charge_diff: float = DEFAULT_TOTAL_CHARGE_DIFF,
            **kwargs) -> None:
        """Assign charges to the atoms in a graph.

        Modify a graph by adding additional attributes describing the \
        atoms' charges and scores. In particular, each atom will get \
        a 'partial_charge' attribute with the partial charge, and a \
        'score' attribute giving a degree of certainty for that charge.

        This solver formulates a relaxed version of the \
        epsilon-Equivalence-Multiple Choice Knapsack Problem as an \
        Linear Programming problem and then uses a generic LP solver \
        from the pulp library to produce optimised charges.

        Atoms with isomorphic neighborhoods are considered equal and we \
        assign the same charge to all atoms in an equivalence class.

        Args:
            graph: The molecule graph to solve charges for.
            charge_dists: Charge distributions for the atoms, obtained \
                    by a Collector, plus the atom's canonical key.
            total_charge: The total charge of the molecule.
            total_charge_diff: Maximum allowed deviation from the total charge
        """

        atom_idx = dict()
        idx = list()
        # weights = partial charges
        weights = dict()
        # profits = frequencies
        profits = dict()

        pos_total = total_charge
        for k, (atom, (charges, frequencies,
                       _)) in enumerate(charge_dists.items()):
            atom_idx[k] = atom
            idx.append(list(zip(itertools.repeat(k), range(len(charges)))))
            weights[k] = charges
            profits[k] = frequencies
            pos_total -= min(charges)

        x = LpVariable.dicts('x',
                             itertools.chain.from_iterable(idx),
                             lowBound=0,
                             upBound=1)

        charging_problem = LpProblem("Atomic Charging Problem", LpMaximize)

        # maximize profits
        charging_problem += sum([
            profits[k][i] * x[(k, i)]
            for k, i in itertools.chain.from_iterable(idx)
        ])

        # select exactly one item per set
        for indices in idx:
            charging_problem += sum([x[(k, i)] for k, i in indices]) == 1

        # total charge difference
        charging_problem +=\
            sum([weights[k][i] * x[(k, i)] for k, i in itertools.chain.from_iterable(idx)]) - total_charge\
            <= total_charge_diff
        charging_problem +=\
            sum([weights[k][i] * x[(k, i)] for k, i in itertools.chain.from_iterable(idx)]) - total_charge\
            >= -total_charge_diff

        # identical neighborhood charge conditions
        neighborhoodclasses = self.compute_atom_neighborhood_classes(
            atom_idx, charge_dists)
        for neighborhood_class in neighborhoodclasses:
            i = neighborhood_class[0]
            for j in neighborhood_class[1::]:
                for (_, k) in idx[i]:
                    charging_problem += x[(i, k)] - x[(
                        j, k
                    )] == 0  # weight k from atom i is selected as partial charge <=> weight k of atom j is selected as partial charge

        solutionTime = -perf_counter()
        try:
            charging_problem.solve(solver=self._solver)
        except:
            raise AssignmentError(
                'Could not solve ILP problem. Please retry with a SimpleCharger'
            )
        solutionTime += perf_counter()

        if not charging_problem.status == LpStatusOptimal:
            raise AssignmentError(
                'Could not solve ILP problem. Please retry with a SimpleCharger'
            )

        solution = []
        profit = 0
        charge = 0
        for k, i in itertools.chain.from_iterable(idx):
            if x[(k, i)].value() != 0.0:
                if 'partial_charge' in graph.nodes[
                        atom_idx[k]] and 'score' in graph.nodes[atom_idx[k]]:
                    graph.nodes[atom_idx[k]][
                        'partial_charge'] += weights[k][i] * x[(k, i)].value()
                    graph.nodes[atom_idx[k]]['score'] += profits[k][i] * x[
                        (k, i)].value()
                else:
                    graph.nodes[atom_idx[k]][
                        'partial_charge'] = weights[k][i] * x[(k, i)].value()
                    graph.nodes[atom_idx[k]]['score'] = profits[k][i] * x[
                        (k, i)].value()

                solution.append(i)
        for node in graph.nodes:
            profit += graph.nodes[node]['score']
            charge += graph.nodes[node]['partial_charge']

        graph.graph['total_charge'] = round(charge, self._rounding_digits)
        graph.graph['score'] = profit
        graph.graph['time'] = solutionTime
        graph.graph['items'] = len(x)
        graph.graph['scaled_capacity'] = pos_total + total_charge_diff
        graph.graph['neighborhoods'] = [[atom_idx[k] for k in i]
                                        for i in neighborhoodclasses]
Beispiel #7
0
    def solve_partial_charges(
            self,
            graph: nx.Graph,
            charge_dists: Dict[Atom, Tuple[ChargeList, WeightList, str]],
            total_charge: int,
            total_charge_diff: float = DEFAULT_TOTAL_CHARGE_DIFF,
            **kwargs) -> None:
        """Assign charges to the atoms in a graph.

        Modify a graph by adding additional attributes describing the \
        atoms' charges and scores. In particular, each atom will get \
        a 'partial_charge' attribute with the partial charge, and a \
        'score' attribute giving a degree of certainty for that charge.

        This solver formulates the epsilon-Multiple Choice Knapsack \
        Problem as an Integer Linear Programming problem and then uses \
        a generic ILP solver from the pulp library to produce optimised \
        charges.

        Args:
            graph: The molecule graph to solve charges for.
            charge_dists: Charge distributions for the atoms, obtained \
                    by a Collector, plus the atom's canonical key.
            total_charge: The total charge of the molecule
            total_charge_diff: Maximum allowed deviation from the total charge
        """

        atom_idx = dict()
        idx = list()
        # weights = partial charges
        weights = dict()
        # profits = frequencies
        profits = dict()
        pos_total = total_charge
        for k, (atom, (charges, frequencies,
                       _)) in enumerate(charge_dists.items()):
            atom_idx[k] = atom
            idx.append(list(zip(itertools.repeat(k), range(len(charges)))))
            weights[k] = charges
            profits[k] = frequencies
            pos_total -= min(charges)

        x = LpVariable.dicts('x',
                             itertools.chain.from_iterable(idx),
                             lowBound=0,
                             upBound=1,
                             cat=LpInteger)

        charging_problem = LpProblem("Atomic Charging Problem", LpMaximize)

        # maximize profits
        charging_problem += sum([
            profits[k][i] * x[(k, i)]
            for k, i in itertools.chain.from_iterable(idx)
        ])

        # select exactly one item per set
        for indices in idx:
            charging_problem += sum([x[(k, i)] for k, i in indices]) == 1

        # total charge difference
        charging_problem +=\
            sum([weights[k][i] * x[(k, i)] for k, i in itertools.chain.from_iterable(idx)]) - total_charge\
            <= total_charge_diff
        charging_problem +=\
            sum([weights[k][i] * x[(k, i)] for k, i in itertools.chain.from_iterable(idx)]) - total_charge\
            >= -total_charge_diff

        solutionTime = -perf_counter()
        try:
            charging_problem.solve(solver=self._solver)
        except:
            raise AssignmentError(
                'Could not solve ILP problem. Please retry with a SimpleCharger'
            )
        solutionTime += perf_counter()

        if not charging_problem.status == LpStatusOptimal:
            raise AssignmentError(
                'Could not solve ILP problem. Please retry  with a SimpleCharger'
            )

        solution = []
        profit = 0
        charge = 0
        for k, i in itertools.chain.from_iterable(idx):
            if x[(k, i)].value() == 1.0:
                graph.nodes[atom_idx[k]]['partial_charge'] = weights[k][i]
                graph.nodes[atom_idx[k]]['score'] = profits[k][i]
                solution.append(i)
                profit += profits[k][i]
                charge += graph.nodes[atom_idx[k]]['partial_charge']

        graph.graph['total_charge'] = round(charge, self._rounding_digits)
        graph.graph['score'] = profit
        graph.graph['time'] = solutionTime
        graph.graph['items'] = len(x)
        graph.graph['scaled_capacity'] = pos_total + total_charge_diff
Beispiel #8
0
    def solve_partial_charges(
            self,
            graph: nx.Graph,
            charge_dists: Dict[Atom, Tuple[ChargeList, WeightList]],
            total_charge: int,
            keydict: Dict[Atom, str] = None,
            total_charge_diff: float = DEFAULT_TOTAL_CHARGE_DIFF,
            **kwargs) -> None:
        """Assign charges to the atoms in a graph.

        Modify a graph by adding additional attributes describing the \
        atoms' charges and scores. In particular, each atom will get \
        a 'partial_charge' attribute with the partial charge, and a \
        'score' attribute giving a degree of certainty for that charge.

        This solver uses Dynamic Programming to solve the \
        epsilon-Multiple Choice Knapsack Problem. This is the Python \
        version of the algorithm, see CDPSolver for a faster \
        implementation.

        Args:
            graph: The molecule graph to solve charges for.
            charge_dists: Charge distributions for the atoms, obtained \
                    by a Collector.
            total_charge: The total charge of the molecule.
            total_charge_diff: Maximum allowed deviation from the total charge
        """

        blowup = 10**self.__rounding_digits
        deflate = 10**(-self.__rounding_digits)

        atom_idx = dict()
        idx = list()
        # item = (index, weight, profit)
        items = list()
        # min weights
        w_min = dict()

        solutionTime = -perf_counter()

        # transform weights to non-negative integers
        pos_total_charge = total_charge
        max_sum = 0
        for k, (atom, (charges,
                       frequencies)) in enumerate(charge_dists.items()):
            atom_idx[k] = atom
            idx.append(zip(itertools.repeat(k), range(len(charges))))
            w_min[k] = min(charges)
            max_sum += max(charges) - w_min[k]
            items.append(
                list(
                    zip(range(len(charges)), [
                        round(blowup * (charge - w_min[k]))
                        for charge in charges
                    ], frequencies)))
            pos_total_charge -= w_min[k]

        # lower and upper capacity limits
        upper = round(blowup * (pos_total_charge + total_charge_diff))
        lower = max(0, round(blowup * (pos_total_charge - total_charge_diff)))

        # check if feasible solutions may exist
        reachable = round(blowup * max_sum)
        if upper < 0 or lower > reachable:
            # sum of min weights over all sets is larger than the upper bound
            # or sum of max weights over all sets is smaller than the lower bound
            raise AssignmentError('Could not solve DP problem. Please retry'
                                  ' with a SimpleCharger')

        # init DP and traceback tables
        dp = [0] + [-float('inf')] * upper
        tb = [[] for _ in range(upper + 1)]

        # DP
        # iterate over all sets
        for items_l in items:
            # iterate over all capacities
            for d in range(upper, -1, -1):
                try:
                    # find max. profit with capacity i over all items j in set k
                    idx, dp[d] = max(((item[0], dp[d - item[1]] + item[2])
                                      for item in items_l if d - item[1] >= 0),
                                     key=lambda x: x[1])
                    # copy old traceback indices and add new index to traceback
                    if dp[d] >= 0:
                        tb[d] = tb[d - items_l[idx][1]] + [idx]
                except ValueError:
                    dp[d] = -float('inf')

        # find max profit
        max_pos, max_val = max(enumerate(dp[lower:upper + 1]),
                               key=lambda x: x[1])

        solutionTime += perf_counter()

        if max_val == -float('inf'):
            raise AssignmentError('Could not solve DP problem. Please retry'
                                  ' with a SimpleCharger')

        solution = tb[lower + max_pos]

        charge = 0
        score = 0
        for i, j in enumerate(solution):
            graph.node[atom_idx[i]]['partial_charge'] = charge_dists[
                atom_idx[i]][0][j]
            graph.node[atom_idx[i]]['score'] = charge_dists[atom_idx[i]][1][j]
            charge += graph.node[atom_idx[i]]['partial_charge']
            score += graph.node[atom_idx[i]]['score']

        graph.graph['total_charge'] = round(charge, self.__rounding_digits)
        graph.graph['score'] = score
        graph.graph['time'] = solutionTime
        graph.graph['items'] = sum(len(i) for i in items)
        graph.graph['scaled_capacity'] = pos_total_charge + total_charge_diff