Coverage for moptipyapps / binpacking2d / packing_result.py: 88%

272 statements  

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

1""" 

2An extended end result record to represent packings. 

3 

4This class extends the information provided by 

5:mod:`~moptipy.evaluation.end_results`. It allows us to compare the results 

6of experiments over different objective functions. It also represents the 

7bounds for the number of bins and for the objective functions. It also 

8includes the problem-specific information of two-dimensional bin packing 

9instances. 

10""" 

11import argparse 

12from dataclasses import dataclass 

13from math import isfinite 

14from typing import Any, Callable, Final, Generator, Iterable, Mapping, cast 

15 

16from moptipy.api.objective import Objective 

17from moptipy.evaluation.base import ( 

18 EvaluationDataElement, 

19) 

20from moptipy.evaluation.end_results import CsvReader as ErCsvReader 

21from moptipy.evaluation.end_results import CsvWriter as ErCsvWriter 

22from moptipy.evaluation.end_results import EndResult 

23from moptipy.evaluation.end_results import from_logs as er_from_logs 

24from moptipy.evaluation.log_parser import LogParser 

25from pycommons.ds.immutable_map import immutable_mapping 

26from pycommons.ds.sequences import reiterable 

27from pycommons.io.console import logger 

28from pycommons.io.csv import ( 

29 SCOPE_SEPARATOR, 

30 csv_column, 

31 csv_scope, 

32 csv_select_scope, 

33) 

34from pycommons.io.csv import CsvReader as CsvReaderBase 

35from pycommons.io.csv import CsvWriter as CsvWriterBase 

36from pycommons.io.path import Path, file_path, line_writer 

37from pycommons.strings.string_conv import ( 

38 num_to_str, 

39 str_to_num, 

40) 

41from pycommons.types import check_int_range, type_error 

42 

43from moptipyapps.binpacking2d.instance import ( 

44 Instance, 

45 _lower_bound_damv, 

46) 

47from moptipyapps.binpacking2d.objectives.bin_count import ( 

48 BIN_COUNT_NAME, 

49 BinCount, 

50) 

51from moptipyapps.binpacking2d.objectives.bin_count_and_empty import ( 

52 BinCountAndEmpty, 

53) 

54from moptipyapps.binpacking2d.objectives.bin_count_and_last_empty import ( 

55 BinCountAndLastEmpty, 

56) 

57from moptipyapps.binpacking2d.objectives.bin_count_and_last_skyline import ( 

58 BinCountAndLastSkyline, 

59) 

60from moptipyapps.binpacking2d.objectives.bin_count_and_last_small import ( 

61 BinCountAndLastSmall, 

62) 

63from moptipyapps.binpacking2d.objectives.bin_count_and_lowest_skyline import ( 

64 BinCountAndLowestSkyline, 

65) 

66from moptipyapps.binpacking2d.objectives.bin_count_and_small import ( 

67 BinCountAndSmall, 

68) 

69from moptipyapps.binpacking2d.packing import Packing 

70from moptipyapps.utils.shared import ( 

71 moptipyapps_argparser, 

72 motipyapps_footer_bottom_comments, 

73) 

74 

75#: the number of items 

76KEY_N_ITEMS: Final[str] = "nItems" 

77#: the number of different items 

78KEY_N_DIFFERENT_ITEMS: Final[str] = "nDifferentItems" 

79#: the bin width 

80KEY_BIN_WIDTH: Final[str] = "binWidth" 

81#: the bin height 

82KEY_BIN_HEIGHT: Final[str] = "binHeight" 

83 

84 

85#: the default objective functions 

86DEFAULT_OBJECTIVES: Final[tuple[Callable[[Instance], Objective], ...]] = ( 

87 BinCount, BinCountAndLastEmpty, BinCountAndLastSmall, 

88 BinCountAndLastSkyline, BinCountAndEmpty, BinCountAndSmall, 

89 BinCountAndLowestSkyline) 

90 

91 

92def __lb_geometric(inst: Instance) -> int: 

93 """ 

94 Compute the geometric lower bound. 

95 

96 :param inst: the instance 

97 :return: the lower bound 

98 """ 

99 area: Final[int] = sum(int(row[0]) * int(row[1]) * int(row[2]) 

100 for row in inst) 

101 bin_size: Final[int] = inst.bin_width * inst.bin_height 

102 res: int = area // bin_size 

103 return (res + 1) if ((res * bin_size) != area) else res 

104 

105 

106#: the lower bound of an objective 

107_OBJECTIVE_LOWER: Final[str] = "lowerBound" 

108#: the upper bound of an objective 

109_OBJECTIVE_UPPER: Final[str] = "upperBound" 

110#: the start string for bin bounds 

111LOWER_BOUNDS_BIN_COUNT: Final[str] = csv_scope("bins", _OBJECTIVE_LOWER) 

112#: the default bounds 

113_DEFAULT_BOUNDS: Final[Mapping[str, Callable[[Instance], int]]] = \ 

114 immutable_mapping({ 

115 LOWER_BOUNDS_BIN_COUNT: lambda i: i.lower_bound_bins, 

116 csv_scope(LOWER_BOUNDS_BIN_COUNT, "geometric"): __lb_geometric, 

117 csv_scope(LOWER_BOUNDS_BIN_COUNT, "damv"): lambda i: _lower_bound_damv( 

118 i.bin_width, i.bin_height, i), 

119 }) 

120 

121 

122@dataclass(frozen=True, init=False, order=False, eq=False) 

123class PackingResult(EvaluationDataElement): 

124 """ 

125 An end result record of one run of one packing algorithm on one problem. 

126 

127 This record provides the information of the outcome of one application of 

128 one algorithm to one problem instance in an immutable way. 

129 """ 

130 

131 #: the original end result record 

132 end_result: EndResult 

133 #: the number of items in the instance 

134 n_items: int 

135 #: the number of different items in the instance 

136 n_different_items: int 

137 #: the bin width 

138 bin_width: int 

139 #: the bin height 

140 bin_height: int 

141 #: the objective values evaluated after the optimization 

142 objectives: Mapping[str, int | float] 

143 #: the bounds for the objective values (append ".lowerBound" and 

144 #: ".upperBound" to all objective function names) 

145 objective_bounds: Mapping[str, int | float] 

146 #: the bounds for the minimum number of bins of the instance 

147 bin_bounds: Mapping[str, int] 

148 

149 def __init__(self, 

150 end_result: EndResult, 

151 n_items: int, 

152 n_different_items: int, 

153 bin_width: int, 

154 bin_height: int, 

155 objectives: Mapping[str, int | float], 

156 objective_bounds: Mapping[str, int | float], 

157 bin_bounds: Mapping[str, int]): 

158 """ 

159 Create a consistent instance of :class:`PackingResult`. 

160 

161 :param end_result: the end result 

162 :param n_items: the number of items 

163 :param n_different_items: the number of different items 

164 :param bin_width: the bin width 

165 :param bin_height: the bin height 

166 :param objectives: the objective values computed after the 

167 optimization 

168 :param bin_bounds: the different bounds for the number of bins 

169 :param objective_bounds: the bounds for the objective functions 

170 :raises TypeError: if any parameter has a wrong type 

171 :raises ValueError: if the parameter values are inconsistent 

172 """ 

173 super().__init__() 

174 if not isinstance(end_result, EndResult): 

175 raise type_error(end_result, "end_result", EndResult) 

176 if end_result.best_f != objectives[end_result.objective]: 

177 raise ValueError( 

178 f"end_result.best_f={end_result.best_f}, but objectives[" 

179 f"{end_result.objective!r}]=" 

180 f"{objectives[end_result.objective]}.") 

181 if not isinstance(objectives, Mapping): 

182 raise type_error(objectives, "objectives", Mapping) 

183 if not isinstance(objective_bounds, Mapping): 

184 raise type_error(objective_bounds, "objective_bounds", Mapping) 

185 if not isinstance(bin_bounds, Mapping): 

186 raise type_error(bin_bounds, "bin_bounds", Mapping) 

187 if len(objective_bounds) != (2 * len(objectives)): 

188 raise ValueError(f"it is required that there is a lower and an " 

189 f"upper bound for each of the {len(objectives)} " 

190 f"functions, but we got {len(objective_bounds)} " 

191 f"bounds, objectives={objectives}, " 

192 f"objective_bounds={objective_bounds}.") 

193 

194 for name, value in objectives.items(): 

195 if not isinstance(name, str): 

196 raise type_error( 

197 name, f"name of evaluation[{name!r}]={value!r}", str) 

198 if not isinstance(value, int | float): 

199 raise type_error( 

200 value, f"value of evaluation[{name!r}]={value!r}", 

201 (int, float)) 

202 if not isfinite(value): 

203 raise ValueError( 

204 f"non-finite value of evaluation[{name!r}]={value!r}") 

205 lll: str = csv_scope(name, _OBJECTIVE_LOWER) 

206 lower = objective_bounds[lll] 

207 if not isfinite(lower): 

208 raise ValueError(f"{lll}=={lower}.") 

209 uuu = csv_scope(name, _OBJECTIVE_UPPER) 

210 upper = objective_bounds[uuu] 

211 if not (lower <= value <= upper): 

212 raise ValueError( 

213 f"it is required that {lll}<=f<={uuu}, but got " 

214 f"{lower}, {value}, and {upper}.") 

215 

216 bins: Final[int | None] = cast("int", objectives[BIN_COUNT_NAME]) \ 

217 if BIN_COUNT_NAME in objectives else None 

218 for name, value in bin_bounds.items(): 

219 if not isinstance(name, str): 

220 raise type_error( 

221 name, f"name of bounds[{name!r}]={value!r}", str) 

222 check_int_range(value, f"bounds[{name!r}]", 1, 1_000_000_000) 

223 if (bins is not None) and (bins < value): 

224 raise ValueError( 

225 f"number of bins={bins} is inconsistent with " 

226 f"bound {name!r}={value}.") 

227 

228 object.__setattr__(self, "end_result", end_result) 

229 object.__setattr__(self, "objectives", immutable_mapping(objectives)) 

230 object.__setattr__(self, "objective_bounds", 

231 immutable_mapping(objective_bounds)) 

232 object.__setattr__(self, "bin_bounds", immutable_mapping(bin_bounds)) 

233 object.__setattr__(self, "n_different_items", check_int_range( 

234 n_different_items, "n_different_items", 1, 1_000_000_000_000)) 

235 object.__setattr__(self, "n_items", check_int_range( 

236 n_items, "n_items", n_different_items, 1_000_000_000_000)) 

237 object.__setattr__(self, "bin_width", check_int_range( 

238 bin_width, "bin_width", 1, 1_000_000_000_000)) 

239 object.__setattr__(self, "bin_height", check_int_range( 

240 bin_height, "bin_height", 1, 1_000_000_000_000)) 

241 

242 def _tuple(self) -> tuple[Any, ...]: 

243 """ 

244 Create a tuple with all the data of this data class for comparison. 

245 

246 :returns: a tuple with all the data of this class, where `None` values 

247 are masked out 

248 """ 

249 # noinspection PyProtectedMember 

250 return self.end_result._tuple() 

251 

252 

253def from_packing_and_end_result( # pylint: disable=W0102 

254 end_result: EndResult, packing: Packing, 

255 objectives: Iterable[Callable[[Instance], Objective]] = 

256 DEFAULT_OBJECTIVES, 

257 bin_bounds: Mapping[str, Callable[[Instance], int]] = 

258 _DEFAULT_BOUNDS, 

259 cache: Mapping[str, tuple[Mapping[str, int], tuple[ 

260 Objective, ...], Mapping[str, int | float]]] | None = 

261 None) -> PackingResult: 

262 """ 

263 Create a `PackingResult` from an `EndResult` and a `Packing`. 

264 

265 :param end_result: the end results record 

266 :param packing: the packing 

267 :param bin_bounds: the bounds computing functions 

268 :param objectives: the objective function factories 

269 :param cache: a cache that can store stuff if this function is to be 

270 called repeatedly 

271 :return: the packing result 

272 """ 

273 if not isinstance(end_result, EndResult): 

274 raise type_error(end_result, "end_result", EndResult) 

275 if not isinstance(packing, Packing): 

276 raise type_error(packing, "packing", Packing) 

277 if not isinstance(objectives, Iterable): 

278 raise type_error(objectives, "objectives", Iterable) 

279 if not isinstance(bin_bounds, Mapping): 

280 raise type_error(bin_bounds, "bin_bounds", Mapping) 

281 if (cache is not None) and (not isinstance(cache, dict)): 

282 raise type_error(cache, "cache", (None, dict)) 

283 

284 instance: Final[Instance] = packing.instance 

285 if instance.name != end_result.instance: 

286 raise ValueError( 

287 f"packing.instance.name={instance.name!r}, but " 

288 f"end_result.instance={end_result.instance!r}.") 

289 

290 row: tuple[Mapping[str, int], tuple[Objective, ...], 

291 Mapping[str, int | float]] | None = None \ 

292 if (cache is None) else cache.get(instance.name, None) 

293 if row is None: 

294 objfs = tuple(sorted((obj(instance) for obj in objectives), 

295 key=str)) 

296 obounds = {} 

297 for objf in objfs: 

298 obounds[csv_scope(str(objf), _OBJECTIVE_LOWER)] = \ 

299 objf.lower_bound() 

300 obounds[csv_scope(str(objf), _OBJECTIVE_UPPER)] = \ 

301 objf.upper_bound() 

302 row = ({key: bin_bounds[key](instance) 

303 for key in sorted(bin_bounds.keys())}, objfs, 

304 immutable_mapping(obounds)) 

305 if cache is not None: 

306 cache[instance.name] = row 

307 

308 objective_values: dict[str, int | float] = {} 

309 bin_count: int = -1 

310 bin_count_obj: str = "" 

311 for objf in row[1]: 

312 z: int | float = objf.evaluate(packing) 

313 objfn: str = str(objf) 

314 objective_values[objfn] = z 

315 if not isinstance(z, int): 

316 continue 

317 if not isinstance(objf, BinCount): 

318 continue 

319 bc: int = objf.to_bin_count(z) 

320 if bin_count == -1: 

321 bin_count = bc 

322 bin_count_obj = objfn 

323 elif bin_count != bc: 

324 raise ValueError( 

325 f"found bin count disagreement: {bin_count} of " 

326 f"{bin_count_obj!r} != {bc} of {objf!r}") 

327 

328 return PackingResult( 

329 end_result=end_result, 

330 n_items=instance.n_items, 

331 n_different_items=instance.n_different_items, 

332 bin_width=instance.bin_width, bin_height=instance.bin_height, 

333 objectives=objective_values, 

334 objective_bounds=row[2], 

335 bin_bounds=row[0]) 

336 

337 

338def from_single_log( # pylint: disable=W0102 

339 file: str, 

340 objectives: Iterable[Callable[[Instance], Objective]] = 

341 DEFAULT_OBJECTIVES, 

342 bin_bounds: Mapping[str, Callable[[Instance], int]] = 

343 _DEFAULT_BOUNDS, 

344 cache: Mapping[str, tuple[Mapping[str, int], tuple[ 

345 Objective, ...], Mapping[str, int | float]]] | None = 

346 None) -> PackingResult: 

347 """ 

348 Create a `PackingResult` from a file. 

349 

350 :param file: the file path 

351 :param objectives: the objective function factories 

352 :param bin_bounds: the bounds computing functions 

353 :param cache: a cache that can store stuff if this function is to be 

354 called repeatedly 

355 :return: the packing result 

356 """ 

357 the_file_path = file_path(file) 

358 end_result: Final[EndResult] = next(er_from_logs(the_file_path)) 

359 packing = Packing.from_log(the_file_path) 

360 if not isinstance(packing, Packing): 

361 raise type_error(packing, f"packing from {file!r}", Packing) 

362 return from_packing_and_end_result( 

363 end_result=end_result, packing=packing, 

364 objectives=objectives, bin_bounds=bin_bounds, cache=cache) 

365 

366 

367def from_logs( # pylint: disable=W0102 

368 directory: str, 

369 objectives: Iterable[Callable[[Instance], Objective]] = 

370 DEFAULT_OBJECTIVES, 

371 bin_bounds: Mapping[str, Callable[[Instance], int]] 

372 = _DEFAULT_BOUNDS) -> Generator[PackingResult, None, None]: 

373 """ 

374 Parse a directory recursively to get all packing results. 

375 

376 :param directory: the directory to parse 

377 :param objectives: the objective function factories 

378 :param bin_bounds: the bin bounds calculators 

379 """ 

380 return __LogParser(objectives, bin_bounds).parse(directory) 

381 

382 

383def to_csv(results: Iterable[PackingResult], file: str) -> Path: 

384 """ 

385 Write a sequence of packing results to a file in CSV format. 

386 

387 :param results: the end results 

388 :param file: the path 

389 :return: the path of the file that was written 

390 """ 

391 path: Final[Path] = Path(file) 

392 logger(f"Writing packing results to CSV file {path!r}.") 

393 path.ensure_parent_dir_exists() 

394 with path.open_for_write() as wt: 

395 consumer: Final[Callable[[str], None]] = line_writer(wt) 

396 for p in CsvWriter.write(sorted(results)): 

397 consumer(p) 

398 logger(f"Done writing packing results to CSV file {path!r}.") 

399 return path 

400 

401 

402def from_csv(file: str) -> Iterable[PackingResult]: 

403 """ 

404 Load the packing results from a CSV file. 

405 

406 :param file: the file to read from 

407 :returns: the iterable with the packing result 

408 """ 

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

410 logger(f"Now reading CSV file {path!r}.") 

411 with path.open_for_read() as rd: 

412 yield from CsvReader.read(rd) 

413 logger(f"Done reading CSV file {path!r}.") 

414 

415 

416class CsvWriter(CsvWriterBase[PackingResult]): 

417 """A class for CSV writing of :class:`PackingResult`.""" 

418 

419 def __init__(self, data: Iterable[PackingResult], 

420 scope: str | None = None) -> None: 

421 """ 

422 Initialize the csv writer. 

423 

424 :param data: the data to write 

425 :param scope: the prefix to be pre-pended to all columns 

426 """ 

427 data = reiterable(data) 

428 super().__init__(data, scope) 

429 #: the end result writer 

430 self.__er: Final[ErCsvWriter] = ErCsvWriter(( 

431 pr.end_result for pr in data), scope) 

432 

433 bin_bounds_set: Final[set[str]] = set() 

434 objectives_set: Final[set[str]] = set() 

435 for pr in data: 

436 bin_bounds_set.update(pr.bin_bounds.keys()) 

437 objectives_set.update(pr.objectives.keys()) 

438 #: the bin bounds 

439 self.__bin_bounds: Final[list[str] | None] = None \ 

440 if set.__len__(bin_bounds_set) <= 0 else sorted(bin_bounds_set) 

441 #: the objectives 

442 self.__objectives: Final[list[str] | None] = None \ 

443 if set.__len__(objectives_set) <= 0 else sorted(objectives_set) 

444 

445 def get_column_titles(self) -> Iterable[str]: 

446 """ 

447 Get the column titles. 

448 

449 :returns: the column titles 

450 """ 

451 p: Final[str] = self.scope 

452 yield from self.__er.get_column_titles() 

453 

454 yield csv_scope(p, KEY_BIN_HEIGHT) 

455 yield csv_scope(p, KEY_BIN_WIDTH) 

456 yield csv_scope(p, KEY_N_ITEMS) 

457 yield csv_scope(p, KEY_N_DIFFERENT_ITEMS) 

458 if self.__bin_bounds: 

459 for b in self.__bin_bounds: 

460 yield csv_scope(p, b) 

461 if self.__objectives: 

462 for o in self.__objectives: 

463 oo: str = csv_scope(p, o) 

464 yield csv_scope(oo, _OBJECTIVE_LOWER) 

465 yield oo 

466 yield csv_scope(oo, _OBJECTIVE_UPPER) 

467 

468 def get_row(self, data: PackingResult) -> Iterable[str]: 

469 """ 

470 Render a single packing result record to a CSV row. 

471 

472 :param data: the end result record 

473 :returns: the iterable with the row data 

474 """ 

475 yield from self.__er.get_row(data.end_result) 

476 yield repr(data.bin_height) 

477 yield repr(data.bin_width) 

478 yield repr(data.n_items) 

479 yield repr(data.n_different_items) 

480 if self.__bin_bounds: 

481 for bb in self.__bin_bounds: 

482 yield (repr(data.bin_bounds[bb]) 

483 if bb in data.bin_bounds else "") 

484 if self.__objectives: 

485 for ob in self.__objectives: 

486 ox = csv_scope(ob, _OBJECTIVE_LOWER) 

487 yield (num_to_str(data.objective_bounds[ox]) 

488 if ox in data.objective_bounds else "") 

489 yield (num_to_str(data.objectives[ob]) 

490 if ob in data.objectives else "") 

491 ox = csv_scope(ob, _OBJECTIVE_UPPER) 

492 yield (num_to_str(data.objective_bounds[ox]) 

493 if ox in data.objective_bounds else "") 

494 

495 def get_header_comments(self) -> Iterable[str]: 

496 """ 

497 Get any possible header comments. 

498 

499 :returns: the header comments 

500 """ 

501 return ("End Results of Bin Packing Experiments", 

502 "See the description at the bottom of the file.") 

503 

504 def get_footer_comments(self) -> Iterable[str]: 

505 """ 

506 Get any possible footer comments. 

507 

508 :return: the footer comments 

509 """ 

510 yield from self.__er.get_footer_comments() 

511 yield "" 

512 p: Final[str | None] = self.scope 

513 if self.__bin_bounds: 

514 for bb in self.__bin_bounds: 

515 yield (f"{csv_scope(p, bb)} is a lower bound " 

516 f"for the number of bins.") 

517 if self.__objectives: 

518 for obb in self.__objectives: 

519 ob: str = csv_scope(p, obb) 

520 ox: str = csv_scope(ob, _OBJECTIVE_LOWER) 

521 yield f"{ox}: a lower bound of the {ob} objective function." 

522 yield (f"{ob}: one of the possible objective functions for " 

523 "the two-dimensional bin packing problem.") 

524 ox = csv_scope(ob, _OBJECTIVE_UPPER) 

525 yield f"{ox}: an upper bound of the {ob} objective function." 

526 

527 def get_footer_bottom_comments(self) -> Iterable[str]: 

528 """Get the bottom footer comments.""" 

529 yield from motipyapps_footer_bottom_comments( 

530 self, "The packing data is assembled using module " 

531 "moptipyapps.binpacking2d.packing_statistics.") 

532 yield from ErCsvWriter.get_footer_bottom_comments(self.__er) 

533 

534 

535class CsvReader(CsvReaderBase): 

536 """A class for CSV reading of :class:`PackingResult` instances.""" 

537 

538 def __init__(self, columns: dict[str, int]) -> None: 

539 """ 

540 Create a CSV parser for :class:`EndResult`. 

541 

542 :param columns: the columns 

543 """ 

544 super().__init__(columns) 

545 #: the end result csv reader 

546 self.__er: Final[ErCsvReader] = ErCsvReader(columns) 

547 #: the index of the n-items column 

548 self.__idx_n_items: Final[int] = csv_column(columns, KEY_N_ITEMS) 

549 #: the index of the n different items column 

550 self.__idx_n_different: Final[int] = csv_column( 

551 columns, KEY_N_DIFFERENT_ITEMS) 

552 #: the index of the bin width column 

553 self.__idx_bin_width: Final[int] = csv_column(columns, KEY_BIN_WIDTH) 

554 #: the index of the bin height column 

555 self.__idx_bin_height: Final[int] = csv_column( 

556 columns, KEY_BIN_HEIGHT) 

557 #: the indices for the objective bounds 

558 self.__bin_bounds: Final[tuple[tuple[str, int], ...]] = \ 

559 csv_select_scope( 

560 lambda x: tuple(sorted(((k, v) for k, v in x.items()))), 

561 columns, LOWER_BOUNDS_BIN_COUNT) 

562 #: the objective bounds 

563 self.__objective_bounds: Final[tuple[tuple[str, int], ...]] = \ 

564 csv_select_scope( 

565 lambda x: tuple(sorted(((k, v) for k, v in x.items()))), 

566 columns, None, 

567 skip_orig_key=lambda s: not str.endswith( 

568 s, (_OBJECTIVE_LOWER, _OBJECTIVE_UPPER))) 

569 n_bounds: Final[int] = tuple.__len__(self.__objective_bounds) 

570 if n_bounds <= 0: 

571 raise ValueError("No objective function bounds found?") 

572 if (n_bounds & 1) != 0: 

573 raise ValueError(f"Number of bounds {n_bounds} should be even.") 

574 #: the parsers for the objective values 

575 self.__objectives: Final[tuple[tuple[str, int], ...]] = \ 

576 tuple((ss, csv_column(columns, ss)) 

577 for ss in sorted({s[0] for s in (str.split( 

578 kk[0], SCOPE_SEPARATOR) for kk in 

579 self.__objective_bounds) if (list.__len__(s) > 1) 

580 and (str.__len__(s[0]) > 0)})) 

581 n_objectives: Final[int] = tuple.__len__(self.__objectives) 

582 if n_objectives <= 0: 

583 raise ValueError("No objectives found?") 

584 if (2 * n_objectives) != n_bounds: 

585 raise ValueError( 

586 f"Number {n_objectives} of objectives " 

587 f"inconsistent with number {n_bounds} of bounds.") 

588 

589 def parse_row(self, data: list[str]) -> PackingResult: 

590 """ 

591 Parse a row of data. 

592 

593 :param data: the data row 

594 :return: the end result statistics 

595 """ 

596 return PackingResult( 

597 self.__er.parse_row(data), 

598 int(data[self.__idx_n_items]), 

599 int(data[self.__idx_n_different]), 

600 int(data[self.__idx_bin_width]), 

601 int(data[self.__idx_bin_height]), 

602 {n: str_to_num(data[i]) for n, i in self.__objectives 

603 if str.__len__(data[i]) > 0}, 

604 {n: str_to_num(data[i]) for n, i in self.__objective_bounds 

605 if str.__len__(data[i]) > 0}, 

606 {n: int(data[i]) for n, i in self.__bin_bounds 

607 if str.__len__(data[i]) > 0}) 

608 

609 

610class __LogParser(LogParser[PackingResult]): 

611 """The internal log parser class.""" 

612 

613 def __init__(self, objectives: Iterable[Callable[[Instance], Objective]], 

614 bin_bounds: Mapping[str, Callable[[Instance], int]]) -> None: 

615 """ 

616 Parse a directory recursively to get all packing results. 

617 

618 :param objectives: the objective function factories 

619 :param bin_bounds: the bin bounds calculators 

620 """ 

621 super().__init__() 

622 if not isinstance(objectives, Iterable): 

623 raise type_error(objectives, "objectives", Iterable) 

624 if not isinstance(bin_bounds, Mapping): 

625 raise type_error(bin_bounds, "bin_bounds", Mapping) 

626 #: the objectives holder 

627 self.__objectives: Final[ 

628 Iterable[Callable[[Instance], Objective]]] = objectives 

629 #: the bin bounds 

630 self.__bin_bounds: Final[ 

631 Mapping[str, Callable[[Instance], int]]] = bin_bounds 

632 #: the internal cache 

633 self.__cache: Final[Mapping[ 

634 str, tuple[Mapping[str, int], tuple[ 

635 Objective, ...], Mapping[str, int | float]]]] = {} 

636 

637 def _parse_file(self, file: Path) -> PackingResult: 

638 """ 

639 Parse a log file. 

640 

641 :param file: the file path 

642 :return: the parsed result 

643 """ 

644 return from_single_log(file, self.__objectives, self.__bin_bounds, 

645 self.__cache) 

646 

647 

648# Run log files to end results if executed as script 

649if __name__ == "__main__": 

650 parser: Final[argparse.ArgumentParser] = moptipyapps_argparser( 

651 __file__, 

652 "Convert log files for the bin packing experiment to a CSV file.", 

653 "Re-evaluate all results based on different objective functions.") 

654 parser.add_argument( 

655 "source", nargs="?", default="./results", 

656 help="the location of the experimental results, i.e., the root folder " 

657 "under which to search for log files", type=Path) 

658 parser.add_argument( 

659 "dest", help="the path to the end results CSV file to be created", 

660 type=Path, nargs="?", default="./evaluation/end_results.txt") 

661 args: Final[argparse.Namespace] = parser.parse_args() 

662 

663 to_csv(from_logs(args.source), args.dest)