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

202 statements  

« prev     ^ index     » next       coverage.py v7.13.0, created at 2025-12-11 04:40 +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, teams: Iterable[str], 

336 rounds: int, home_streak_min: int, home_streak_max: int, 

337 away_streak_min: int, away_streak_max: int, 

338 separation_min: int, separation_max: int, 

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

340 """ 

341 Create an instance of the Traveling Salesperson Problem. 

342 

343 :param cls: the class 

344 :param name: the name of the instance 

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

346 :param teams: the iterable with the team names 

347 :param rounds: the number of rounds 

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

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

350 at home in a row 

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

352 at home in a row 

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

354 away in a row 

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

356 away in a row 

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

358 of a game setup 

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

360 of a game setup 

361 """ 

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

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

364 if (n % 2) != 0: 

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

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

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

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

369 for nn in names: 

370 for char in nn: 

371 if char.isspace(): 

372 raise ValueError( 

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

374 

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

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

377 

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

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

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

381 #: the names of the teams that compete 

382 obj.teams = names 

383 #: the number of rounds 

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

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

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

387 obj.home_streak_min = check_int_range( 

388 home_streak_min, "home_streak_min", 1, ll) 

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

390 obj.home_streak_max = check_int_range( 

391 home_streak_max, "home_streak_max", home_streak_min, ll) 

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

393 obj.away_streak_min = check_int_range( 

394 away_streak_min, "away_streak_min", 1, ll) 

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

396 obj.away_streak_max = check_int_range( 

397 away_streak_max, "away_streak_max", away_streak_min, ll) 

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

399 obj.separation_min = check_int_range( 

400 separation_min, "separation_min", 0, ll) 

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

402 obj.separation_max = check_int_range( 

403 separation_max, "separation_max", separation_min, ll) 

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

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

406 return obj 

407 

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

409 """ 

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

411 

412 :param logger: the logger for the parameters 

413 

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

415 >>> with InMemoryLogger() as l: 

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

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

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

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

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

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

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

423gamePlanDtype: b@END_I' 

424 """ 

425 super().log_parameters_to(logger) 

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

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

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

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

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

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

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

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

434 

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

436 """ 

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

438 

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

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

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

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

443 The limits for the RobinX instance have been taken from 

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

445 

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

447 plan length 

448 """ 

449 if self.name in _OPT_DISTANCE_BOUNDS: 

450 return _OPT_DISTANCE_BOUNDS[self.name] 

451 # unknown instance, compute bounds including penalty 

452 n: Final[int] = self.n_cities 

453 rounds: Final[int] = self.rounds 

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

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

456 

457 @staticmethod 

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

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

460 """ 

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

462 

463 :param path: the path to the file 

464 :param lower_bound_getter: ignored 

465 :return: the instance 

466 

467 >>> from os.path import dirname 

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

469 >>> inst.name 

470 'con20' 

471 """ 

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

473 with file.open_for_read() as stream: 

474 try: 

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

476 except (TypeError, ValueError) as err: 

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

478 

479 @staticmethod 

480 def list_resources(symmetric: bool = True, 

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

482 """ 

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

484 

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

486 

487 :param symmetric: include the instances with symmetric distance 

488 matrices 

489 :param asymmetric: include the asymmetric instances with asymmetric 

490 distance matrices 

491 :return: the tuple with the instance names 

492 

493 >>> len(Instance.list_resources()) 

494 118 

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

496 0 

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

498 118 

499 """ 

500 return _INSTANCES if symmetric else () 

501 

502 @staticmethod 

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

504 """ 

505 Load a TTP instance from a resource. 

506 

507 :param name: the name string 

508 :return: the instance 

509 

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

511 >>> insta.n_cities 

512 24 

513 >>> insta.name 

514 'bra24' 

515 >>> insta.teams[0] 

516 'Atl.Mineiro' 

517 >>> insta.teams[1] 

518 'Atl.Paranaense' 

519 >>> insta.rounds 

520 2 

521 >>> insta.home_streak_min 

522 1 

523 >>> insta.home_streak_max 

524 3 

525 >>> insta.away_streak_min 

526 1 

527 >>> insta.away_streak_max 

528 3 

529 >>> insta.separation_min 

530 1 

531 >>> insta.separation_max 

532 46 

533 """ 

534 if not isinstance(name, str): 

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

536 container: Final = Instance.from_resource 

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

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

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

540 

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

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

543 

544 if inst.name != name: 

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

546 if inst.n_cities <= 1000: 

547 setattr(container, inst_attr, inst) 

548 return inst