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
« 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.
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
11import numpy as np
12from moptipy.api.space import Space
13from moptipy.utils.logger import CSV_SEPARATOR, KeyValueLogSection
14from pycommons.types import type_error
16from moptipyapps.ttp.game_plan import GamePlan
17from moptipyapps.ttp.instance import Instance
18from moptipyapps.utils.shared import SCOPE_INSTANCE
21class GamePlanSpace(Space):
22 """An implementation of the `Space` API of for game plans."""
24 def __init__(self, instance: Instance) -> None:
25 """
26 Create a 2D packing space.
28 :param instance: the 2d bin packing instance
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
42 def create(self) -> GamePlan:
43 """
44 Create a game plan without assigning items to locations.
46 :return: the (empty, uninitialized) packing object
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)
64 def is_equal(self, x1: GamePlan, x2: GamePlan) -> bool:
65 """
66 Check if two bin game plans have the same contents.
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
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)
89 def from_str(self, text: str) -> GamePlan:
90 """
91 Convert a string to a packing.
93 :param text: the string
94 :return: the packing
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)
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()
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
121 def validate(self, x: GamePlan) -> None:
122 """
123 Check if a game plan is an instance of the right object.
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.
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}")
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
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
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.")
161 def n_points(self) -> int:
162 """
163 Get the number of game plans.
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)`.
170 :return: the number of possible game plans
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)
189 def __str__(self) -> str:
190 """
191 Get the name of the game plan space.
193 :return: the name, simply `gp_` + the instance name
195 >>> print(GamePlanSpace(Instance.from_resource("bra24")))
196 gp_bra24
197 """
198 return f"gp_{self.instance}"
200 def log_parameters_to(self, logger: KeyValueLogSection) -> None:
201 """
202 Log the parameters of the space to the given logger.
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)