Coverage for moptipyapps / binpacking2d / objectives / bin_count_and_last_empty.py: 63%

30 statements  

« prev     ^ index     » next       coverage.py v7.13.0, created at 2025-12-11 04:40 +0000

1""" 

2An objective function indirectly minimizing the number of bins in packings. 

3 

4This objective function first computes the number of bins used. Let's call it 

5`n_bins`. We know the total number of items, 

6:attr:`~moptipyapps.binpacking2d.instance.Instance.n_items`, as 

7well (because this is also the number of rows in the packing). 

8Now we return `(n_items * (n_bins - 1)) + number_of_items_in_last_bin`, 

9where `number_of_items_in_last_bin` is, well, the number of items in the 

10very last bin. 

11 

12The idea behind this is: If one of two packings has the smaller number of 

13bins, then this one will always have the smaller objective value. If two 

14packings have the same number of bins, but one has fewer items in the very 

15last bin, then that one is better. With this mechanism, we drive the search 

16towards "emptying" the last bin. If the number of items in the last bin would 

17reach `0`, that last bin would disappear - and we have one bin less. 

18""" 

19from typing import Final 

20 

21import numba # type: ignore 

22import numpy as np 

23from pycommons.math.int_math import ceil_div 

24 

25from moptipyapps.binpacking2d.objectives.bin_count import BinCount 

26from moptipyapps.binpacking2d.packing import IDX_BIN 

27 

28 

29@numba.njit(cache=True, inline="always", fastmath=True, boundscheck=False) 

30def bin_count_and_last_empty(y: np.ndarray) -> int: 

31 """ 

32 Compute the number of bins and the emptiness of the last bin. 

33 

34 We compute the total number of bins minus 1 and multiply it with the 

35 number of items. We then add the number of items in the last bin. 

36 

37 :param y: the packing 

38 :return: the objective value 

39 

40 >>> bin_count_and_last_empty(np.array([[1, 1, 10, 10, 20, 20], 

41 ... [1, 1, 30, 30, 40, 40], 

42 ... [1, 1, 20, 20, 30, 30]], int)) 

43 3 

44 >>> bin_count_and_last_empty(np.array([[1, 1, 10, 10, 20, 20], 

45 ... [1, 2, 30, 30, 40, 40], # bin 2! 

46 ... [1, 1, 20, 20, 30, 30]], int)) 

47 4 

48 >>> bin_count_and_last_empty(np.array([[1, 2, 10, 10, 20, 20], # bin 2! 

49 ... [1, 2, 30, 30, 40, 40], # bin 2! 

50 ... [1, 1, 20, 20, 30, 30]], int)) 

51 5 

52 >>> bin_count_and_last_empty(np.array([[1, 3, 10, 10, 20, 20], # bin 3! 

53 ... [1, 2, 30, 30, 40, 40], # bin 2! 

54 ... [1, 1, 20, 20, 30, 30]], int)) 

55 7 

56 """ 

57 current_bin: int = -1 # the current idea of what the last bin is 

58 current_size: int = -1 # the number of items already in that bin 

59 n_items: Final[int] = len(y) # the number of rows in the matrix 

60 

61 for i in range(n_items): # iterate over all packed items 

62 bin_idx: int = int(y[i, IDX_BIN]) # get the bin index of the item 

63 if bin_idx > current_bin: # it's a new biggest bin = new last bin? 

64 current_size = 1 # then there is 1 object in it for now 

65 current_bin = bin_idx # and we remember it 

66 elif bin_idx == current_bin: # did item go into the current last bin? 

67 current_size += 1 # then increase size 

68 return (n_items * (current_bin - 1)) + current_size # return objective 

69 

70 

71class BinCountAndLastEmpty(BinCount): 

72 """Compute the number of bins and the emptiness of the last one.""" 

73 

74 def evaluate(self, x) -> int: 

75 """ 

76 Evaluate the objective function. 

77 

78 :param x: the solution 

79 :return: the bin size and emptyness factor 

80 """ 

81 return bin_count_and_last_empty(x) 

82 

83 def lower_bound(self) -> int: 

84 """ 

85 Get the lower bound of the number of bins and emptiness objective. 

86 

87 We know from the instance (:attr:`~moptipyapps.binpacking2d\ 

88.instance.Instance.lower_bound_bins`) that we require at least as many bins 

89 such that they can accommodate the total area of all items together. 

90 Let's call this number `lb`. Now if `lb` is one, then all objects could 

91 be in the first bin, in which case the objective value would equal to 

92 :attr:`~moptipyapps.binpacking2d.instance.Instance.n_items`, 

93 i.e., the total number of items in the first = last bin. 

94 If it is `lb=2`, then we know that we will need at least two bins. The 

95 best case would be that `n_items - 1` items are in the first bin and 

96 one is in the last bin. This means that we would get `1 * n_items + 1` 

97 as objective value. If we have `lb=3` bins, then we could have 

98 `n_items - 1` items distributed over the first two bins with one item 

99 left over in the last bin, i.e., would get `(2 * n_items) + 1`. And so 

100 on. 

101 

102 :return: `max(n_items, (lb - 1) * n_items + 1)` 

103 

104 >>> from moptipyapps.binpacking2d.instance import Instance 

105 >>> ins = Instance("a", 100, 50, [[10, 5, 1], [3, 3, 1], [5, 5, 1]]) 

106 >>> ins.n_items 

107 3 

108 >>> ins.lower_bound_bins 

109 1 

110 >>> BinCountAndLastEmpty(ins).lower_bound() 

111 3 

112 

113 >>> ins = Instance("b", 10, 50, [[10, 5, 10], [3, 3, 1], [5, 5, 1]]) 

114 >>> ins.n_items 

115 12 

116 >>> ins.lower_bound_bins 

117 2 

118 >>> BinCountAndLastEmpty(ins).lower_bound() 

119 13 

120 

121 >>> ins = Instance("c", 10, 50, [[10, 5, 20], [30, 3, 10], [5, 5, 1]]) 

122 >>> ins.n_items 

123 31 

124 >>> ins.lower_bound_bins 

125 4 

126 >>> BinCountAndLastEmpty(ins).lower_bound() 

127 94 

128 """ 

129 return max(self._instance.n_items, 

130 ((self._instance.lower_bound_bins - 1) 

131 * self._instance.n_items) + 1) 

132 

133 def upper_bound(self) -> int: 

134 """ 

135 Get the upper bound of the number of bins plus emptiness. 

136 

137 :return: the number of items in the instance to the square 

138 

139 >>> from moptipyapps.binpacking2d.instance import Instance 

140 >>> ins = Instance("a", 100, 50, [[10, 5, 1], [3, 3, 1], [5, 5, 1]]) 

141 >>> ins.n_items 

142 3 

143 >>> BinCountAndLastEmpty(ins).upper_bound() 

144 9 

145 

146 >>> ins = Instance("b", 10, 50, [[10, 5, 10], [3, 3, 1], [5, 5, 1]]) 

147 >>> ins.n_items 

148 12 

149 >>> BinCountAndLastEmpty(ins).upper_bound() 

150 144 

151 

152 >>> ins = Instance("c", 10, 50, [[10, 5, 20], [30, 3, 10], [5, 5, 1]]) 

153 >>> ins.n_items 

154 31 

155 >>> BinCountAndLastEmpty(ins).upper_bound() 

156 961 

157 """ 

158 return self._instance.n_items * self._instance.n_items 

159 

160 def to_bin_count(self, z: int) -> int: 

161 """ 

162 Convert an objective value to a bin count. 

163 

164 :param z: the objective value 

165 :return: the bin count 

166 """ 

167 return ceil_div(z, self._instance.n_items) 

168 

169 def __str__(self) -> str: 

170 """ 

171 Get the name of the bins objective function. 

172 

173 :return: `binCountAndLastEmpty` 

174 :retval "binCountAndLastEmpty": always 

175 """ 

176 return "binCountAndLastEmpty"