Coverage for moptipyapps / binpacking2d / packing.py: 85%

74 statements  

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

1"""A two-dimensional packing.""" 

2 

3from typing import Final 

4 

5import numpy as np 

6from moptipy.api.component import Component 

7from moptipy.api.logging import SECTION_RESULT_Y, SECTION_SETUP 

8from moptipy.evaluation.log_parser import LogParser 

9from pycommons.io.path import Path 

10from pycommons.types import type_error 

11 

12from moptipyapps.binpacking2d.instance import Instance 

13 

14#: the index of the ID in a :class:`Packing` row 

15IDX_ID: Final[int] = 0 

16#: the index of the bin in a :class:`Packing` row 

17IDX_BIN: Final[int] = 1 

18#: the index of the left x coordinate in a :class:`Packing` row 

19IDX_LEFT_X: Final[int] = 2 

20#: the index of the bottom y coordinate in a :class:`Packing` row 

21IDX_BOTTOM_Y: Final[int] = 3 

22#: the index of the right x coordinate in a :class:`Packing` row 

23IDX_RIGHT_X: Final[int] = 4 

24#: the index of the top y coordinate in a :class:`Packing` row 

25IDX_TOP_Y: Final[int] = 5 

26 

27 

28class Packing(Component, np.ndarray): 

29 """ 

30 A packing, i.e., a solution to an 2D bin packing instance. 

31 

32 A packing is a two-dimensional numpy array. In each row, the position of 

33 one item is stored: 1. the item ID (starts at 1), 2. the bin into which 

34 the item is packaged (starts at 1), 3. the left x coordinate, 4. the 

35 bottom y coordinate, 5. the right x coordinate, 6. the top y coordinate. 

36 """ 

37 

38 #: the 2d bin packing instance 

39 instance: Instance 

40 #: the number of bins 

41 n_bins: int 

42 

43 def __new__(cls, instance: Instance) -> "Packing": 

44 """ 

45 Create a solution record for the 2D bin packing problem. 

46 

47 :param cls: the class 

48 :param instance: the solution record 

49 """ 

50 if not isinstance(instance, Instance): 

51 raise type_error(instance, "instance", Instance) 

52 obj: Final[Packing] = super().__new__( 

53 cls, (instance.n_items, 6), instance.dtype) 

54 #: the 2d bin packing instance 

55 obj.instance = instance 

56 #: the number of bins 

57 obj.n_bins = -1 

58 return obj 

59 

60 def __str__(self): 

61 """ 

62 Convert the packing to a compact string. 

63 

64 :return: the compact string 

65 """ 

66 return "\n".join(";".join(str(self[i, j]) for j in range(6)) 

67 for i in range(self.shape[0])) 

68 

69 @staticmethod 

70 def from_log(file: str, instance: Instance | None = None) -> "Packing": 

71 """ 

72 Load a packing from a log file. 

73 

74 :param file: the log file path 

75 :param instance: the optional Packing instance: if `None` is provided, 

76 we try to load it from the resources 

77 :returns: the Packing 

78 """ 

79 return _PackingParser(instance).parse_file(file) 

80 

81 

82class _PackingParser(LogParser[Packing]): 

83 """The log parser for loading packings.""" 

84 

85 def __init__(self, instance: Instance | None = None): 

86 """ 

87 Create the packing parser. 

88 

89 :param instance: the optional packing instance: if `None` is provided, 

90 we try to load it from the resources 

91 """ 

92 super().__init__() 

93 if (instance is not None) and (not isinstance(instance, Instance)): 

94 raise type_error(instance, "instance", Instance) 

95 #: the internal instance 

96 self.__instance: Instance | None = instance 

97 #: the internal section mode: 0=none, 1=setup, 2=y 

98 self.__sec_mode: int = 0 

99 #: the packing string 

100 self.__packing_str: str | None = None 

101 

102 def _start_section(self, title: str) -> bool: 

103 """Start a section.""" 

104 super()._start_section(title) 

105 self.__sec_mode = 0 

106 if title == SECTION_SETUP: 

107 if self.__instance is None: 

108 self.__sec_mode = 1 

109 return True 

110 return False 

111 if title == SECTION_RESULT_Y: 

112 self.__sec_mode = 2 

113 return True 

114 return False 

115 

116 def _lines(self, lines: list[str]) -> bool: 

117 """Parse the lines.""" 

118 if self.__sec_mode == 1: 

119 if self.__instance is not None: 

120 raise ValueError( 

121 f"instance is already set to {self.__instance}.") 

122 key_1: Final[str] = "y.inst.name: " 

123 for line in lines: 

124 if line.startswith(key_1): 

125 self.__instance = Instance.from_resource( 

126 line[len(key_1):].strip()) 

127 if self.__instance is None: 

128 raise ValueError(f"Did not find instance key {key_1!r} " 

129 f"in section {SECTION_SETUP}!") 

130 elif self.__sec_mode == 2: 

131 self.__packing_str = " ".join(lines).strip() 

132 else: 

133 raise ValueError("Should not be in section?") 

134 return (self.__instance is None) or (self.__packing_str is None) 

135 

136 def _parse_file(self, file: Path) -> Packing: 

137 """End the file.""" 

138 super()._parse_file(file) 

139 if self.__packing_str is None: 

140 raise ValueError(f"Section {SECTION_RESULT_Y} missing!") 

141 if self.__instance is None: 

142 raise ValueError(f"Section {SECTION_SETUP} missing or empty!") 

143 # pylint: disable=C0415,R0401 

144 from moptipyapps.binpacking2d.packing_space import ( # noqa: PLC0415 

145 PackingSpace, # pylint: disable=C0415,R0401 # noqa: PLC0415 

146 ) 

147 return PackingSpace(self.__instance).from_str(self.__packing_str) 

148 

149 def _end_parse_file(self, file: Path) -> None: 

150 """Cleanup.""" 

151 self.__packing_str = None 

152 super()._end_parse_file(file)