Coverage for moptipyapps / ttp / instance.py: 81%

202 statements  

« prev     ^ index     » next       coverage.py v7.13.2, created at 2026-01-28 08:47 +0000

1""" 

2An instance of the Traveling Tournament Problem (TTP). 

3 

4The Traveling Tournament Problem (TTP) describes the logistics of a sports 

5league. In this league, `n` teams compete. In each time slot, each team plays 

6against one other team. In each game, one team plays at home and one team 

7plays away with the other team. In each round, every team plays once against 

8every other team. The league may have multiple :attr:`~moptipyapps.ttp.\ 

9instance.Instance.rounds`. If there are two rounds, then each team plays 

10against each other team once at home and once abroad. If a team plays at home 

11(or abroad) several times in a row, this is called a "streak". There are 

12minimum and maximum streak length constraints defined, for both at home and 

13abroad. Additionally, if team A plays a team B in one time slot, then the 

14exact inverse game cannot take place in the next time slot. A minimum 

15number of games must take place in between for separation. There can also be 

16a maximum separation length. 

17 

18David Van Bulck of the Sports Scheduling Research group, part of the 

19Faculty of Economics and Business Administration at Ghent University, Belgium, 

20maintains "RobinX: An XML-driven Classification for Round-Robin Sports 

21Timetabling", a set of benchmark data instances and results of the TTP. 

22We provide some of these instances as resources here. You can also download 

23them directly at <https://robinxval.ugent.be/RobinX/travelRepo.php>. Also, 

24see <https://robinxval.ugent.be/> for more information. 

25 

261. David Van Bulck. Minimum Travel Objective Repository. *RobinX: An 

27 XML-driven Classification for Round-Robin Sports Timetabling.* Faculty of 

28 Economics and Business Administration at Ghent University, Belgium. 

29 https://robinxval.ugent.be/ 

302. Kelly Easton, George L. Nemhauser, and Michael K. Trick. The Traveling 

31 Tournament Problem Description and Benchmarks. In *Principles and Practice 

32 of Constraint Programming (CP'01),* November 26 - December 1, 2001, Paphos, 

33 Cyprus, pages 580-584, Berlin/Heidelberg, Germany: Springer. 

34 ISBN: 978-3-540-42863-3. https://doi.org/10.1007/3-540-45578-7_43 

35 https://www.researchgate.net/publication/220270875 

36""" 

37 

38from typing import Callable, Final, Iterable, TextIO, cast 

39 

40import numpy as np 

41from defusedxml import ElementTree # type: ignore 

42from moptipy.utils.logger import KeyValueLogSection 

43from moptipy.utils.nputils import DEFAULT_INT, int_range_to_dtype 

44from moptipy.utils.strings import sanitize_name 

45from pycommons.io.path import Path, file_path 

46from pycommons.types import check_int_range, check_to_int_range, type_error 

47 

48from moptipyapps.tsp.instance import Instance as TSPInstance 

49from moptipyapps.ttp.robinx import open_resource_stream 

50 

51 

52def _from_stream(stream: TextIO) -> "Instance": 

53 """ 

54 Read a TTP instance from an `robinxval.ugent.be`-formatted XML file. 

55 

56 This procedure ignores most of the data in the file and only focuses on 

57 the instance name, the team names, and the distance matrix as well as the 

58 constraints for home streak length, away streak length, and same-game 

59 separations. Everything else is ignored. 

60 

61 :param stream: the text stream 

62 :return: the instance 

63 """ 

64 used_names: set[str] = set() 

65 team_names: dict[int, str] = {} 

66 distances: dict[tuple[int, int], int] = {} 

67 name: str | None = None 

68 rounds: int | None = None 

69 home_streak_min: int | None = None 

70 home_streak_max: int | None = None 

71 away_streak_min: int | None = None 

72 away_streak_max: int | None = None 

73 separation_min: int | None = None 

74 separation_max: int | None = None 

75 

76 for _event, element in ElementTree.iterparse(stream, forbid_dtd=True, 

77 forbid_entities=True, 

78 forbid_external=True): 

79 if element.tag is None: 

80 continue 

81 tag: str = element.tag.strip().lower() 

82 if tag == "instancename": 

83 if name is None: 

84 name = sanitize_name(element.text).lower() 

85 if name in used_names: 

86 raise ValueError( 

87 f"name {element.text!r} invalid, as it " 

88 f"maps to {name!r}, which is already used.") 

89 used_names.add(name) 

90 else: 

91 raise ValueError(f"already got name={name!r}, but tag " 

92 f"'InstanceName' appears again?") 

93 elif tag == "distance": 

94 t1: int = check_to_int_range(element.attrib["team1"], 

95 "team1", 0, 1_000_000) 

96 t2: int = check_to_int_range(element.attrib["team2"], 

97 "team2", 0, 1_000_000) 

98 dst: int = check_to_int_range( 

99 element.attrib["dist"], "dist", 0, 1_000_000_000_000) 

100 if t1 == t2: 

101 if dst == 0: 

102 continue 

103 raise ValueError(f"distance for team1={t1}, team2={t2} is" 

104 f" {dst} but must be 0.") 

105 tpl: tuple[int, int] = (t1, t2) 

106 if tpl in distances: 

107 raise ValueError( 

108 f"got distance={dst} for {tpl!r}, but " 

109 f"{distances[tpl]} was already specified before") 

110 distances[tpl] = dst 

111 elif element.tag == "team": 

112 team: int = check_to_int_range(element.attrib["id"], "id", 

113 0, 1_000_000) 

114 nn: str = element.attrib["name"] 

115 tname: str = nn.strip() 

116 if tname in used_names: 

117 raise ValueError(f"name {nn!r} is invalid, as it maps to " 

118 f"{tname!r}, which is already used.") 

119 used_names.add(tname) 

120 team_names[team] = tname 

121 elif element.tag == "numberroundrobin": 

122 if rounds is not None: 

123 raise ValueError(f"rounds already set to {rounds}") 

124 rounds = check_to_int_range(element.text, "rounds", 1, 1000) 

125 elif tag == "ca3": 

126 if "mode1" not in element.attrib: 

127 continue 

128 if "mode2" not in element.attrib: 

129 continue 

130 if element.attrib["mode2"].lower() != "games": 

131 continue 

132 mode = element.attrib["mode1"].lower() 

133 mi = check_to_int_range( 

134 element.attrib["min"], "min", 0, 1_000_000) \ 

135 if "min" in element.attrib else None 

136 ma = check_to_int_range( 

137 element.attrib["max"], "max", 1, 1_000_000) \ 

138 if "max" in element.attrib else None 

139 if mode == "h": 

140 if mi is not None: 

141 if home_streak_min is not None: 

142 raise ValueError("minimum home streak already defined") 

143 home_streak_min = max(mi, 1) 

144 if ma is not None: 

145 if home_streak_max is not None: 

146 raise ValueError("maximum home streak already defined") 

147 home_streak_max = ma 

148 elif mode == "a": 

149 if mi is not None: 

150 if away_streak_min is not None: 

151 raise ValueError("minimum away streak already defined") 

152 away_streak_min = max(mi, 1) 

153 if ma is not None: 

154 if away_streak_max is not None: 

155 raise ValueError("maximum away streak already defined") 

156 away_streak_max = ma 

157 elif tag == "se1": 

158 mi = check_to_int_range( 

159 element.attrib["min"], "min", 0, 1_000_000) \ 

160 if "min" in element.attrib else None 

161 ma = check_to_int_range( 

162 element.attrib["max"], "max", 0, 1_000_000) \ 

163 if "max" in element.attrib else None 

164 if mi is not None: 

165 if separation_min is not None: 

166 raise ValueError("minimum separation already defined") 

167 separation_min = mi 

168 if ma is not None: 

169 if separation_max is not None: 

170 raise ValueError("maximum separation already defined") 

171 separation_max = ma 

172 

173 if name is None: 

174 raise ValueError("did not find instance name") 

175 n_teams: Final[int] = len(team_names) 

176 if n_teams <= 0: 

177 raise ValueError("did not find any team name") 

178 if len(used_names) != (n_teams + 1): 

179 raise ValueError(f"set of used names {used_names!r} has wrong " 

180 f"length, should be {n_teams + 1}.") 

181 dm: np.ndarray = np.zeros((n_teams, n_teams), DEFAULT_INT) 

182 for tup, dst in distances.items(): 

183 dm[tup[0], tup[1]] = dst 

184 

185 if rounds is None: 

186 rounds = 2 

187 ll: Final[int] = rounds * n_teams - 1 

188 if home_streak_min is None: 

189 home_streak_min = 1 

190 if home_streak_max is None: 

191 home_streak_max = min(max(home_streak_min, 3), ll) 

192 if away_streak_min is None: 

193 away_streak_min = 1 

194 if away_streak_max is None: 

195 away_streak_max = min(max(away_streak_min, 3), ll) 

196 if separation_min is None: 

197 separation_min = 1 

198 if separation_max is None: 

199 separation_max = min(max(separation_min, 1), ll) 

200 

201 return Instance(name, dm, [team_names[i] for i in range(n_teams)], 

202 rounds, home_streak_min, home_streak_max, away_streak_min, 

203 away_streak_max, separation_min, separation_max) 

204 

205 

206#: The instances made available within this package are taken from 

207#: <https://robinxval.ugent.be/RobinX/travelRepo.php>, where the following 

208#: descriptions are given: 

209#: - *Constant Distance (`con*`):* The constant distance instances are the 

210#: most simple instances in which the distance between the home venues of 

211#: any two teams is one. In this case, Urrutia and Ribeiro showed that 

212#: distance minimization is equivalent with break maximization. 

213#: - *Circular Distance (`circ*`):* Somewhat similar are the circular 

214#: distance instances in which the teams' venues are placed on a 

215#: circle. Any two consecutive teams are connected by an edge and the 

216#: distance between two teams is equal to the minimal number of edges that 

217#: must be traversed to get to the other team. Although traveling 

218#: salesperson problems with a circular distance matrix have a trivial 

219#: solution, it remains challenging to solve circular traveling tournament 

220#: instances. 

221#: - *Galaxy (`gal*`):* This artificial instance class consists of the Galaxy 

222#: instances that use a 3D-space that embeds the Earth and 39 other 

223#: exoplanets. 

224#: - *National league (`nl*`):* The `nl`-instances are based on air distance 

225#: between the city centers from teams in the National League of the Major 

226#: League Baseball. 

227#: - *National football league (`nfl*`):* The NFL-instances are based on air 

228#: distance between the city centers from teams in the National Football 

229#: League. 

230#: - *Super 14 (`sup*`):* The super 14 instances are based on air distance 

231#: between the city centers from teams in the Super 14 rugby cup. 

232#: - *Brazilian (`bra24`)):* The Brazilian instance is based on the air 

233#: distance between the home cities of 24 teams in the main division of the 

234#: 2003 edition of the Brazilian soccer championship. 

235#: - *Linear (`line*`):* In the linear instances, `n` teams are located on a 

236#: straight line with a distance of one unit separating each pair of 

237#: adjacent teams. 

238#: - *Increasing distance (`incr*`):* In the increasing distance instances, 

239#: `n` teams are located on a straight line with an increasing distance 

240#: separating each pair of adjacent teams such that the distance between 

241#: team `k` and `k+1` equals `k`. 

242_INSTANCES: Final[tuple[str, ...]] = ( 

243 "bra24", "circ4", "circ6", "circ8", "circ10", "circ12", "circ14", 

244 "circ16", "circ18", "circ20", "circ22", "circ24", "circ26", "circ28", 

245 "circ30", "circ32", "circ34", "circ36", "circ38", "circ40", "con4", 

246 "con6", "con8", "con10", "con12", "con14", "con16", "con18", "con20", 

247 "con22", "con24", "con26", "con28", "con30", "con32", "con34", "con36", 

248 "con38", "con40", "gal4", "gal6", "gal8", "gal10", "gal12", "gal14", 

249 "gal16", "gal18", "gal20", "gal22", "gal24", "gal26", "gal28", "gal30", 

250 "gal32", "gal34", "gal36", "gal38", "gal40", "incr4", "incr6", "incr8", 

251 "incr10", "incr12", "incr14", "incr16", "incr18", "incr20", "incr22", 

252 "incr24", "incr26", "incr28", "incr30", "incr32", "incr34", "incr36", 

253 "incr38", "incr40", "line4", "line6", "line8", "line10", "line12", 

254 "line14", "line16", "line18", "line20", "line22", "line24", "line26", 

255 "line28", "line30", "line32", "line34", "line36", "line38", "line40", 

256 "nfl16", "nfl18", "nfl20", "nfl22", "nfl24", "nfl26", "nfl28", "nfl30", 

257 "nfl32", "nl4", "nl6", "nl8", "nl10", "nl12", "nl14", "nl16", "sup4", 

258 "sup6", "sup8", "sup10", "sup12", "sup14") 

259 

260 

261#: The lower and upper bound for the *optimal* total tournament length, taken 

262#: from https://robinxval.ugent.be/RobinX/travelRepo.php on 2024-05-10. 

263_OPT_DISTANCE_BOUNDS: Final[dict[str, tuple[int, int]]] = { 

264 "bra24": (451406, 538866), "circ4": (20, 20), "circ6": (64, 64), 

265 "circ8": (132, 132), "circ10": (242, 242), "circ12": (388, 400), 

266 "circ14": (588, 616), "circ16": (846, 898), "circ18": (1188, 1268), 

267 "circ20": (1600, 1724), "circ22": (2068, 2366), "circ24": (2688, 3146), 

268 "circ26": (3380, 3992), "circ28": (4144, 4642), "circ30": (5100, 5842), 

269 "circ32": (6144, 7074), "circ34": (7276, 8042), "circ36": (8640, 9726), 

270 "circ38": (10108, 11424), "circ40": (11680, 12752), "con4": (17, 17), 

271 "con6": (43, 43), "con8": (80, 80), "con10": (124, 124), 

272 "con12": (181, 181), "con14": (252, 252), "con16": (327, 327), 

273 "con18": (414, 416), "con20": (520, 520), "con22": (626, 626), 

274 "con24": (744, 747), "con26": (884, 884), "con28": (1021, 1021), 

275 "con30": (1170, 1177), "con32": (1344, 1359), "con34": (1512, 1512), 

276 "con36": (1692, 1703), "con38": (1900, 1918), "con40": (2099, 2099), 

277 "gal4": (416, 416), "gal6": (1365, 1365), "gal8": (2373, 2373), 

278 "gal10": (4535, 4535), "gal12": (7034, 7135), "gal14": (10255, 10840), 

279 "gal16": (13619, 14583), "gal18": (19050, 20205), "gal20": (23738, 25401), 

280 "gal22": (31461, 33901), "gal24": (41287, 44260), "gal26": (53802, 58968), 

281 "gal28": (69992, 75276), "gal30": (88831, 95158), 

282 "gal32": (108374, 119665), "gal34": (133976, 143298), 

283 "gal36": (158549, 169387), "gal38": (189126, 204980), 

284 "gal40": (226820, 241908), "incr4": (48, 48), "incr6": (228, 228), 

285 "incr8": (624, 638), "incr10": (1440, 1612), "incr12": (2880, 3398), 

286 "incr14": (5180, 6488), "incr16": (8640, 10332), "incr18": (13548, 17278), 

287 "incr20": (20368, 25672), "incr22": (29484, 40944), 

288 "incr24": (41360, 56602), "incr26": (56500, 81866), 

289 "incr28": (75456, 106870), "incr30": (98820, 136810), 

290 "incr32": (127224, 177990), "incr34": (161348, 222082), 

291 "incr36": (201912, 278060), "incr38": (249686, 336008), 

292 "incr40": (305470, 406960), "line4": (24, 24), "line6": (76, 76), 

293 "line8": (156, 162), "line10": (288, 370), "line12": (480, 584), 

294 "line14": (740, 918), "line16": (1080, 1320), "line18": (1512, 1926), 

295 "line20": (2044, 2548), "line22": (2688, 3684), "line24": (3456, 4732), 

296 "line26": (4356, 6382), "line28": (5400, 7778), "line30": (6600, 9312), 

297 "line32": (7964, 11234), "line34": (9504, 13190), 

298 "line36": (11232, 15536), "line38": (13156, 17862), 

299 "line40": (15294, 20546), "nfl16": (223800, 231483), 

300 "nfl18": (272834, 282258), "nfl20": (316721, 332041), 

301 "nfl22": (378813, 400636), "nfl24": (431226, 463657), 

302 "nfl26": (495982, 536792), "nfl28": (560697, 598123), 

303 "nfl30": (688875, 739697), "nfl32": (836031, 914620), 

304 "nl4": (8276, 8276), "nl6": (23916, 23916), "nl8": (39721, 39721), 

305 "nl10": (59436, 59436), "nl12": (108629, 110729), 

306 "nl14": (183354, 188728), "nl16": (249477, 261687), 

307 "sup4": (63405, 63405), "sup6": (130365, 130365), 

308 "sup8": (182409, 182409), "sup10": (316329, 316329), 

309 "sup12": (453860, 458810), "sup14": (557354, 567891), 

310} 

311 

312 

313class Instance(TSPInstance): 

314 """An instance of Traveling Tournament Problem (TTP).""" 

315 

316 #: the names of the teams 

317 teams: tuple[str, ...] 

318 #: the number of rounds 

319 rounds: int 

320 #: the minimum number of games that can be played at home in a row 

321 home_streak_min: int 

322 #: the maximum number of games that can be played at home in a row 

323 home_streak_max: int 

324 #: the minimum number of games that can be played away in a row 

325 away_streak_min: int 

326 #: the maximum number of games that can be played away in a row 

327 away_streak_max: int 

328 #: the minimum number of games between a repetition of a game setup 

329 separation_min: int 

330 #: the maximum number of games between a repetition of a game setup 

331 separation_max: int 

332 #: the data type to be used for plans 

333 game_plan_dtype: np.dtype 

334 

335 def __new__(cls, name: str, matrix: np.ndarray, # noqa: PYI034 

336 teams: Iterable[str], rounds: int, home_streak_min: int, 

337 home_streak_max: int, away_streak_min: int, 

338 away_streak_max: int, separation_min: int, 

339 separation_max: int, 

340 tour_length_lower_bound: int = 0) -> "Instance": 

341 """ 

342 Create an instance of the Traveling Salesperson Problem. 

343 

344 :param cls: the class 

345 :param name: the name of the instance 

346 :param matrix: the matrix with the data (will be copied) 

347 :param teams: the iterable with the team names 

348 :param rounds: the number of rounds 

349 :param tour_length_lower_bound: the lower bound of the tour length 

350 :param home_streak_min: the minimum number of games that can be played 

351 at home in a row 

352 :param home_streak_max: the maximum number of games that can be played 

353 at home in a row 

354 :param away_streak_min: the minimum number of games that can be played 

355 away in a row 

356 :param away_streak_max: the maximum number of games that can be played 

357 away in a row 

358 :param separation_min: the minimum number of games between a repetition 

359 of a game setup 

360 :param separation_max: the maximum number of games between a repetition 

361 of a game setup 

362 """ 

363 names: Final[tuple[str, ...]] = tuple(map(str.strip, teams)) 

364 n: Final[int] = len(names) 

365 if (n % 2) != 0: 

366 raise ValueError(f"the number of teams must be even, but is {n}.") 

367 if n != len(set(names)): 

368 raise ValueError(f"some team name appears twice in {teams!r} " 

369 f"after fixing it to {names!r}.") 

370 for nn in names: 

371 for char in nn: 

372 if char.isspace(): 

373 raise ValueError( 

374 f"team name must not contain space, but {nn} does.") 

375 

376 obj: Final[Instance] = cast("Instance", super().__new__( 

377 cls, name, tour_length_lower_bound, matrix, rounds * n)) 

378 

379 if (obj.shape[0] != n) or (obj.shape[1] != n) or (obj.n_cities != n): 

380 raise ValueError(f"inconsistent n_teams={n}, n_cities=" 

381 f"{obj.n_cities} and shape={obj.shape}") 

382 #: the names of the teams that compete 

383 obj.teams = names 

384 #: the number of rounds 

385 obj.rounds = check_int_range(rounds, "rounds", 1, 100) 

386 ll: Final[int] = rounds * n - 1 # an upper bound for streaks 

387 #: the minimum number of games that can be played at home in a row 

388 obj.home_streak_min = check_int_range( 

389 home_streak_min, "home_streak_min", 1, ll) 

390 #: the maximum number of games that can be played at home in a row 

391 obj.home_streak_max = check_int_range( 

392 home_streak_max, "home_streak_max", home_streak_min, ll) 

393 #: the minimum number of games that can be played away in a row 

394 obj.away_streak_min = check_int_range( 

395 away_streak_min, "away_streak_min", 1, ll) 

396 #: the maximum number of games that can be played away in a row 

397 obj.away_streak_max = check_int_range( 

398 away_streak_max, "away_streak_max", away_streak_min, ll) 

399 #: the minimum number of games between a repetition of a game setup 

400 obj.separation_min = check_int_range( 

401 separation_min, "separation_min", 0, ll) 

402 #: the maximum number of games between a repetition of a game setup 

403 obj.separation_max = check_int_range( 

404 separation_max, "separation_max", separation_min, ll) 

405 #: the data type to be used for the game plans 

406 obj.game_plan_dtype = int_range_to_dtype(-n, n) 

407 return obj 

408 

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

410 """ 

411 Log the parameters of the instance to the given logger. 

412 

413 :param logger: the logger for the parameters 

414 

415 >>> from moptipy.utils.logger import InMemoryLogger 

416 >>> with InMemoryLogger() as l: 

417 ... with l.key_values("I") as kv: 

418 ... Instance.from_resource("gal4").log_parameters_to(kv) 

419 ... print(repr('@'.join(l.get_log()))) 

420 'BEGIN_I@name: gal4@class: moptipyapps.ttp.instance.Instance@\ 

421nCities: 4@tourLengthLowerBound: 67@tourLengthUpperBound: 160@symmetric: T@\ 

422dtype: h@rounds: 2@homeStreakMin: 1@homeStreakMax: 3@\ 

423awayStreakMin: 1@awayStreakMax: 3@separationMin: 1@separationMax: 6@\ 

424gamePlanDtype: b@END_I' 

425 """ 

426 super().log_parameters_to(logger) 

427 logger.key_value("rounds", self.rounds) 

428 logger.key_value("homeStreakMin", self.home_streak_min) 

429 logger.key_value("homeStreakMax", self.home_streak_max) 

430 logger.key_value("awayStreakMin", self.away_streak_min) 

431 logger.key_value("awayStreakMax", self.away_streak_max) 

432 logger.key_value("separationMin", self.separation_min) 

433 logger.key_value("separationMax", self.separation_max) 

434 logger.key_value("gamePlanDtype", self.game_plan_dtype.char) 

435 

436 def get_optimal_plan_length_bounds(self) -> tuple[int, int]: 

437 """ 

438 Get lower and upper bounds in which the *optimal* plan length resides. 

439 

440 These are the bounds for the optimal tour length of *feasible* 

441 solutions. If we know the feasible solution with the smallest possible 

442 tour length, then the :class:`~moptipyapps.ttp.game_plan` objective 

443 function would return a value within these limits for this solution. 

444 The limits for the RobinX instance have been taken from 

445 https://robinxval.ugent.be/RobinX/travelRepo.php on 2024-05-10. 

446 

447 :return: a tuple of the lower and upper limit for the optimal 

448 plan length 

449 """ 

450 if self.name in _OPT_DISTANCE_BOUNDS: 

451 return _OPT_DISTANCE_BOUNDS[self.name] 

452 # unknown instance, compute bounds including penalty 

453 n: Final[int] = self.n_cities 

454 rounds: Final[int] = self.rounds 

455 days: Final[int] = (n - 1) * rounds 

456 return 0, ((2 * int(self.max())) + 1) * n * days 

457 

458 @staticmethod 

459 def from_file(path: str, lower_bound_getter: Callable[[ # noqa: ARG004 

460 str], int] | None = None) -> "Instance": 

461 """ 

462 Read a TTP instance from a `robinX` formatted XML file. 

463 

464 :param path: the path to the file 

465 :param lower_bound_getter: ignored 

466 :return: the instance 

467 

468 >>> from os.path import dirname 

469 >>> inst = Instance.from_file(dirname(__file__) + "/robinx/con20.xml") 

470 >>> inst.name 

471 'con20' 

472 """ 

473 file: Final[Path] = file_path(path) 

474 with file.open_for_read() as stream: 

475 try: 

476 return _from_stream(cast("TextIO", stream)) 

477 except (TypeError, ValueError) as err: 

478 raise ValueError(f"error when parsing file {file!r}") from err 

479 

480 @staticmethod 

481 def list_resources(symmetric: bool = True, 

482 asymmetric: bool = True) -> tuple[str, ...]: # noqa 

483 """ 

484 Get a tuple of all the TTP instances available as resource. 

485 

486 All instances of the `robinX` set provided here are symmetric. 

487 

488 :param symmetric: include the instances with symmetric distance 

489 matrices 

490 :param asymmetric: include the asymmetric instances with asymmetric 

491 distance matrices 

492 :return: the tuple with the instance names 

493 

494 >>> len(Instance.list_resources()) 

495 118 

496 >>> len(Instance.list_resources(False, True)) 

497 0 

498 >>> len(Instance.list_resources(True, False)) 

499 118 

500 """ 

501 return _INSTANCES if symmetric else () 

502 

503 @staticmethod 

504 def from_resource(name: str) -> "Instance": 

505 """ 

506 Load a TTP instance from a resource. 

507 

508 :param name: the name string 

509 :return: the instance 

510 

511 >>> insta = Instance.from_resource("bra24") 

512 >>> insta.n_cities 

513 24 

514 >>> insta.name 

515 'bra24' 

516 >>> insta.teams[0] 

517 'Atl.Mineiro' 

518 >>> insta.teams[1] 

519 'Atl.Paranaense' 

520 >>> insta.rounds 

521 2 

522 >>> insta.home_streak_min 

523 1 

524 >>> insta.home_streak_max 

525 3 

526 >>> insta.away_streak_min 

527 1 

528 >>> insta.away_streak_max 

529 3 

530 >>> insta.separation_min 

531 1 

532 >>> insta.separation_max 

533 46 

534 """ 

535 if not isinstance(name, str): 

536 raise type_error(name, "name", str) 

537 container: Final = Instance.from_resource 

538 inst_attr: Final[str] = f"__inst_{name}" 

539 if hasattr(container, inst_attr): # instance loaded? 

540 return cast("Instance", getattr(container, inst_attr)) 

541 

542 with open_resource_stream(f"{name}.xml") as stream: 

543 inst: Final[Instance] = _from_stream(stream) 

544 

545 if inst.name != name: 

546 raise ValueError(f"got {inst.name!r} for instance {name!r}?") 

547 if inst.n_cities <= 1000: 

548 setattr(container, inst_attr, inst) 

549 return inst