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
« 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.
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
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
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)
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"
85#: the default objective functions
86DEFAULT_OBJECTIVES: Final[tuple[Callable[[Instance], Objective], ...]] = (
87 BinCount, BinCountAndLastEmpty, BinCountAndLastSmall,
88 BinCountAndLastSkyline, BinCountAndEmpty, BinCountAndSmall,
89 BinCountAndLowestSkyline)
92def __lb_geometric(inst: Instance) -> int:
93 """
94 Compute the geometric lower bound.
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
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 })
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.
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 """
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]
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`.
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}.")
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}.")
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}.")
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))
242 def _tuple(self) -> tuple[Any, ...]:
243 """
244 Create a tuple with all the data of this data class for comparison.
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()
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`.
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))
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}.")
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
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}")
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])
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.
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)
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.
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)
383def to_csv(results: Iterable[PackingResult], file: str) -> Path:
384 """
385 Write a sequence of packing results to a file in CSV format.
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
402def from_csv(file: str) -> Iterable[PackingResult]:
403 """
404 Load the packing results from a CSV file.
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}.")
416class CsvWriter(CsvWriterBase[PackingResult]):
417 """A class for CSV writing of :class:`PackingResult`."""
419 def __init__(self, data: Iterable[PackingResult],
420 scope: str | None = None) -> None:
421 """
422 Initialize the csv writer.
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)
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)
445 def get_column_titles(self) -> Iterable[str]:
446 """
447 Get the column titles.
449 :returns: the column titles
450 """
451 p: Final[str] = self.scope
452 yield from self.__er.get_column_titles()
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)
468 def get_row(self, data: PackingResult) -> Iterable[str]:
469 """
470 Render a single packing result record to a CSV row.
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 "")
495 def get_header_comments(self) -> Iterable[str]:
496 """
497 Get any possible header comments.
499 :returns: the header comments
500 """
501 return ("End Results of Bin Packing Experiments",
502 "See the description at the bottom of the file.")
504 def get_footer_comments(self) -> Iterable[str]:
505 """
506 Get any possible footer comments.
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."
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)
535class CsvReader(CsvReaderBase):
536 """A class for CSV reading of :class:`PackingResult` instances."""
538 def __init__(self, columns: dict[str, int]) -> None:
539 """
540 Create a CSV parser for :class:`EndResult`.
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.")
589 def parse_row(self, data: list[str]) -> PackingResult:
590 """
591 Parse a row of data.
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})
610class __LogParser(LogParser[PackingResult]):
611 """The internal log parser class."""
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.
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]]]] = {}
637 def _parse_file(self, file: Path) -> PackingResult:
638 """
639 Parse a log file.
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)
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()
663 to_csv(from_logs(args.source), args.dest)