Coverage for moptipyapps / ttp / game_plan_space.py: 84%

61 statements  

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

1""" 

2Here we provide a :class:`~moptipy.api.space.Space` of bin game plans. 

3 

4The bin game plans object is defined in module 

5:mod:`~moptipyapps.ttp.game_plan`. Basically, it is a 

6two-dimensional numpy array holding, for each day (or time slot) for each team 

7the opposing team. 

8""" 

9from typing import Final 

10 

11import numpy as np 

12from moptipy.api.space import Space 

13from moptipy.utils.logger import CSV_SEPARATOR, KeyValueLogSection 

14from pycommons.types import type_error 

15 

16from moptipyapps.ttp.game_plan import GamePlan 

17from moptipyapps.ttp.instance import Instance 

18from moptipyapps.utils.shared import SCOPE_INSTANCE 

19 

20 

21class GamePlanSpace(Space): 

22 """An implementation of the `Space` API of for game plans.""" 

23 

24 def __init__(self, instance: Instance) -> None: 

25 """ 

26 Create a 2D packing space. 

27 

28 :param instance: the 2d bin packing instance 

29 

30 >>> inst = Instance.from_resource("circ4") 

31 >>> space = GamePlanSpace(inst) 

32 >>> space.instance is inst 

33 True 

34 """ 

35 if not isinstance(instance, Instance): 

36 raise type_error(instance, "instance", Instance) 

37 #: The instance to which the packings apply. 

38 self.instance: Final[Instance] = instance 

39 self.copy = np.copyto # type: ignore 

40 self.to_str = GamePlan.__str__ # type: ignore 

41 

42 def create(self) -> GamePlan: 

43 """ 

44 Create a game plan without assigning items to locations. 

45 

46 :return: the (empty, uninitialized) packing object 

47 

48 >>> inst = Instance.from_resource("circ8") 

49 >>> space = GamePlanSpace(inst) 

50 >>> x = space.create() 

51 >>> print(inst.rounds) 

52 2 

53 >>> print(inst.n_cities) 

54 8 

55 >>> x.shape 

56 (14, 8) 

57 >>> x.instance is inst 

58 True 

59 >>> type(x) 

60 <class 'moptipyapps.ttp.game_plan.GamePlan'> 

61 """ 

62 return GamePlan(self.instance) 

63 

64 def is_equal(self, x1: GamePlan, x2: GamePlan) -> bool: 

65 """ 

66 Check if two bin game plans have the same contents. 

67 

68 :param x1: the first game plan 

69 :param x2: the second game plan 

70 :return: `True` if both game plans are for the same instance and have 

71 the same structure 

72 

73 >>> inst = Instance.from_resource("circ4") 

74 >>> space = GamePlanSpace(inst) 

75 >>> y1 = space.create() 

76 >>> y1.fill(0) 

77 >>> y2 = space.create() 

78 >>> y2.fill(0) 

79 >>> space.is_equal(y1, y2) 

80 True 

81 >>> y1 is y2 

82 False 

83 >>> y1[0, 0] = 1 

84 >>> space.is_equal(y1, y2) 

85 False 

86 """ 

87 return (x1.instance is x2.instance) and np.array_equal(x1, x2) 

88 

89 def from_str(self, text: str) -> GamePlan: 

90 """ 

91 Convert a string to a packing. 

92 

93 :param text: the string 

94 :return: the packing 

95 

96 >>> inst = Instance.from_resource("circ6") 

97 >>> space = GamePlanSpace(inst) 

98 >>> y1 = space.create() 

99 >>> y1.fill(0) 

100 >>> y2 = space.from_str(space.to_str(y1)) 

101 >>> space.is_equal(y1, y2) 

102 True 

103 >>> y1 is y2 

104 False 

105 """ 

106 if not isinstance(text, str): 

107 raise type_error(text, "packing text", str) 

108 

109 # we only want the very first line 

110 text = text.lstrip() 

111 lb: int = text.find("\n") 

112 if lb > 0: 

113 text = text[:lb].rstrip() 

114 

115 x: Final[GamePlan] = self.create() 

116 np.copyto(x, np.fromstring(text, dtype=x.dtype, sep=CSV_SEPARATOR) 

117 .reshape(x.shape)) 

118 self.validate(x) 

119 return x 

120 

121 def validate(self, x: GamePlan) -> None: 

122 """ 

123 Check if a game plan is an instance of the right object. 

124 

125 This method performs a superficial feasibility check, as in the TTP, 

126 we try to find feasible game plans and may have infeasible ones. All 

127 we check here is that the object is of the right type and dimensions 

128 and that it does not contain some out-of-bounds value. 

129 

130 :param x: the game plan 

131 :raises TypeError: if any component of the game plan is of the wrong 

132 type 

133 :raises ValueError: if the game plan is not feasible 

134 """ 

135 if not isinstance(x, GamePlan): 

136 raise type_error(x, "x", GamePlan) 

137 inst: Final[Instance] = self.instance 

138 if inst is not x.instance: 

139 raise ValueError( 

140 f"x.instance must be {inst} but is {x.instance}.") 

141 if inst.game_plan_dtype is not x.dtype: 

142 raise ValueError(f"inst.game_plan_dtype = {inst.game_plan_dtype}" 

143 f" but x.dtype={x.dtype}") 

144 

145 n: Final[int] = inst.n_cities # the number of teams 

146 # each team plays every other team 'rounds' times 

147 n_days: Final[int] = (n - 1) * inst.rounds 

148 

149 needed_shape: Final[tuple[int, int]] = (n_days, n) 

150 if x.shape != needed_shape: 

151 raise ValueError(f"x.shape={x.shape}, but must be {needed_shape}.") 

152 min_id: Final[int] = -n 

153 

154 for i in range(n_days): 

155 for j in range(n): 

156 v = x[i, j] 

157 if not (min_id <= v <= n): 

158 raise ValueError(f"value {v} at x[{i}, {j}] should be in " 

159 f"{min_id}...{n}, but is not.") 

160 

161 def n_points(self) -> int: 

162 """ 

163 Get the number of game plans. 

164 

165 The values in a game plan go from `-n..n`, including zero, and we have 

166 `days*n` values. This gives `(2n + 1) ** (days * n)`, where `days` 

167 equals `(n - 1) * rounds` and `rounds` is the number of rounds. In 

168 total, this gives `(2n + 1) ** ((n - 1) * rounds * n)`. 

169 

170 :return: the number of possible game plans 

171 

172 >>> space = GamePlanSpace(Instance.from_resource("circ6")) 

173 >>> print((2 * 6 + 1) ** ((6 - 1) * 2 * 6)) 

174 6864377172744689378196133203444067624537070830997366604446306636401 

175 >>> space.n_points() 

176 6864377172744689378196133203444067624537070830997366604446306636401 

177 >>> space = GamePlanSpace(Instance.from_resource("circ4")) 

178 >>> space.n_points() 

179 79766443076872509863361 

180 >>> print((2 * 4 + 1) ** ((4 - 1) * 2 * 4)) 

181 79766443076872509863361 

182 """ 

183 inst: Final[Instance] = self.instance 

184 n: Final[int] = inst.n_cities 

185 n_days: Final[int] = (n - 1) * inst.rounds 

186 total_values: Final[int] = 2 * n + 1 

187 return total_values ** (n_days * n) 

188 

189 def __str__(self) -> str: 

190 """ 

191 Get the name of the game plan space. 

192 

193 :return: the name, simply `gp_` + the instance name 

194 

195 >>> print(GamePlanSpace(Instance.from_resource("bra24"))) 

196 gp_bra24 

197 """ 

198 return f"gp_{self.instance}" 

199 

200 def log_parameters_to(self, logger: KeyValueLogSection) -> None: 

201 """ 

202 Log the parameters of the space to the given logger. 

203 

204 :param logger: the logger for the parameters 

205 """ 

206 super().log_parameters_to(logger) 

207 with logger.scope(SCOPE_INSTANCE) as kv: 

208 self.instance.log_parameters_to(kv)