Source code for QHyper.problems.knapsack

# 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


"""
.. currentmodule:: QHyper.problems.knapsack
"""

import random
import sympy
import numpy as np
from collections import namedtuple

from QHyper.polynomial import Polynomial
from QHyper.problems.base import Problem
from QHyper.constraint import Constraint

Item = namedtuple('Item', "weight value")


class Knapsack:
    """Knapsack class

    Attributes
    ----------
    max_weight: int
        maximum weight of an item
    max_item_value: int
        maximum value of an item
    items: list[Item]
        list of items
    """

    def __init__(
        self,
        max_weight: int,
        max_item_value: int,
        items_amount: int,
        item_weights: list[int],
        item_values: list[int]
    ) -> None:
        """
        Parameters
        ----------
        max_weight: int
            maximum weight of an item
        max_item_value: int
            maximum value of an item
        items_amount: int
            items amount, used only for random knapsack
        items: list[tuple[int, int]]
            set items in knapsack
        """
        self.items: list[Item] = []
        self.max_weight: int = max_weight
        self.max_item_value: int = max_item_value
        if item_weights and item_values:
            if len(item_weights) != len(item_values):
                raise ValueError(
                    "Weights and values must have the same length")
            self.set_knapsack(item_weights, item_values)
        else:
            if items_amount < 1:
                raise ValueError(
                    "Cannot create knapsack with less than one item")
            self.generate_knapsack(items_amount)

    def generate_knapsack(self, items_amount: int) -> None:
        for _ in range(items_amount):
            self.items.append(Item(
                random.randint(1, self.max_weight),
                random.randint(1, self.max_item_value)
            ))

    def set_knapsack(self, weights: list[int], values: list[int]
                     ) -> None:
        self.items = [Item(weight, value)
                      for weight, value in zip(weights, values)]

    def __len__(self) -> int:
        return len(self.items)


[docs] class KnapsackProblem(Problem): """Objective function and constraints for the knapsack problem Parameters ---------- max_weight: int maximum weight of an item max_item_value: int, default 10 maximum value of an item items_amount: int, optional items amount, used only for random knapsack. If not provided, then item_weights and item_values must be specified item_weights: list[int], optional list of items weights item_values: list[int], optional list of items values Attributes ---------- objective_function : Polynomial objective function in SymPy syntax wrapped in Expression class constraints : list[Polynomial] list of constraints in SymPy syntax wrapped in Expression class knapsack: :py:class:`Knapsack` Knapsack instance """ def __init__( self, max_weight: int, max_item_value: int = 10, items_amount: int = 1, item_weights: list[int] = [], item_values: list[int] = [] ) -> None: self.knapsack = Knapsack(max_weight, max_item_value, items_amount, item_weights, item_values) self.variables: tuple[sympy.Symbol] = sympy.symbols(' '.join( [f'x{i}' for i in range(len(self.knapsack) + self.knapsack.max_weight)] )) self._set_objective_function() self._set_constraints() def _set_objective_function(self) -> None: """ Create the objective function items on defined in SymPy syntax """ equation = Polynomial(0) for i in range(len(self.knapsack.items)): equation += Polynomial({(f"x{i}", ): self.knapsack.items[i].value}) equation = -equation self.objective_function = equation def _set_constraints(self) -> None: """ Create constraints defined in SymPy syntax """ self.constraints: list[Constraint] = [] equation = Polynomial(1) for i in range(self.knapsack.max_weight): equation -= Polynomial({(f'x{i+len(self.knapsack)}',): 1}) self.constraints.append(Constraint(equation)) equation = Polynomial(0) for i in range(self.knapsack.max_weight): equation += Polynomial({(f'x{i+len(self.knapsack)}', ): (i+1)}) for i in range(len(self.knapsack.items)): equation -= Polynomial({(f"x{i}",): self.knapsack.items[i].weight}) self.constraints.append(Constraint(equation))
[docs] def get_score(self, result: np.record, penalty: float = 0) -> float: """Returns score for the provided numpy recor Parameters ---------- result : np.record Outcome as a numpy record with variables as keys and their values. Dtype is list of tuples with variable name and its value (0 or 1) and tuple ('probability', <float>). penalty : float, default 0 Penalty for the constraint violation Returns ------- float Returns negated sum of value of picked items or 0 if knapsack isn't correct """ sum = 0 weight = 0 for i, item in enumerate(self.knapsack.items): if result[f'x{i}'] == 1: sum += item.value weight += item.weight if weight > self.knapsack.max_weight: return penalty for i in range(self.knapsack.max_weight): if result[f'x{i + len(self.knapsack)}'] == 1 and i + 1 != weight: return penalty if weight != 0 and result[f'x{weight + len(self.knapsack) - 1}'] != 1: return penalty return -sum