Coverage for moptipy / examples / jssp / demo_examples.py: 46%

193 statements  

« prev     ^ index     » next       coverage.py v7.12.0, created at 2025-11-24 08:49 +0000

1"""Some fixed demo codes for the JSSP examples.""" 

2import sys 

3import urllib.request as rq 

4from typing import Callable, Final, Iterable 

5 

6import numpy as np 

7from matplotlib.figure import Figure # type: ignore 

8from pycommons.io.console import logger 

9from pycommons.io.path import UTF8, Path, write_lines 

10from pycommons.types import type_error 

11 

12from moptipy.examples.jssp.experiment import INSTANCES 

13from moptipy.examples.jssp.gantt import Gantt 

14from moptipy.examples.jssp.gantt_space import GanttSpace 

15from moptipy.examples.jssp.instance import ( 

16 Instance, 

17 compute_makespan_lower_bound, 

18) 

19from moptipy.examples.jssp.ob_encoding import OperationBasedEncoding 

20from moptipy.examples.jssp.plot_gantt_chart import ( 

21 marker_lb, 

22 marker_makespan, 

23 plot_gantt_chart, 

24) 

25from moptipy.spaces.permutations import Permutations 

26from moptipy.utils.lang import Lang 

27from moptipy.utils.plot_utils import cm_to_inch, create_figure, save_figure 

28 

29 

30def demo_instance() -> Instance: 

31 """ 

32 Get the demo instance we use. 

33 

34 :return: the demo instance 

35 """ 

36 attr: Final[str] = "_res_" 

37 if not hasattr(demo_instance, attr): 

38 setattr(demo_instance, attr, Instance.from_resource("demo")) 

39 return getattr(demo_instance, attr) 

40 

41 

42def demo_search_space() -> Permutations: 

43 """ 

44 Obtain an instance of the demo search space. 

45 

46 :return: the demo search space 

47 """ 

48 attr: Final[str] = "_res_" 

49 if not hasattr(demo_search_space, attr): 

50 instance: Final[Instance] = demo_instance() 

51 setattr(demo_search_space, attr, 

52 Permutations.with_repetitions(instance.jobs, 

53 instance.machines)) 

54 return getattr(demo_search_space, attr) 

55 

56 

57def demo_point_in_search_space(optimum: bool = False) -> np.ndarray: 

58 """ 

59 Create a demo point in the search space. 

60 

61 :param optimum: should we return the optimal solution? 

62 :return: the point 

63 """ 

64 space: Final[Permutations] = demo_search_space() 

65 res = space.create() 

66 if optimum: 

67 np.copyto(res, [0, 1, 2, 3, 1, 0, 1, 2, 0, 1, 

68 3, 2, 2, 3, 1, 3, 0, 2, 3, 0]) 

69 else: 

70 np.copyto(res, [0, 2, 3, 2, 2, 3, 1, 1, 0, 3, 

71 1, 3, 2, 1, 3, 2, 0, 1, 0, 0]) 

72 space.validate(res) 

73 return res 

74 

75 

76def demo_solution_space() -> GanttSpace: 

77 """ 

78 Obtain an instance of the demo solution space. 

79 

80 :return: the demo solution space 

81 """ 

82 attr: Final[str] = "_res_" 

83 if not hasattr(demo_solution_space, attr): 

84 instance: Final[Instance] = demo_instance() 

85 setattr(demo_solution_space, attr, GanttSpace(instance)) 

86 return getattr(demo_solution_space, attr) 

87 

88 

89def demo_encoding() -> OperationBasedEncoding: 

90 """ 

91 Obtain an instance of the demo encoding. 

92 

93 :return: the demo encoding 

94 """ 

95 attr: Final[str] = "_res_" 

96 if not hasattr(demo_encoding, attr): 

97 instance: Final[Instance] = demo_instance() 

98 setattr(demo_encoding, attr, OperationBasedEncoding(instance)) 

99 return getattr(demo_encoding, attr) 

100 

101 

102def demo_solution(optimum: bool = False) -> Gantt: 

103 """ 

104 Create a demo solution. 

105 

106 :param optimum: should we return the optimal solution? 

107 :return: the demo solution 

108 """ 

109 space: Final[GanttSpace] = demo_solution_space() 

110 result: Final[Gantt] = space.create() 

111 demo_encoding().decode(demo_point_in_search_space(optimum=optimum), 

112 result) 

113 space.validate(result) 

114 return result 

115 

116 

117def __make_gantt_demo_name(optimum: bool, 

118 with_makespan: bool, 

119 with_lower_bound: bool) -> str: 

120 """ 

121 Construct the name for the demo gantt chart. 

122 

123 :param optimum: should we return the optimal solution? 

124 :param with_makespan: should the makespan be included? 

125 :param with_lower_bound: should the lower bound be included? 

126 :return: the file name 

127 """ 

128 prefix: str = "gantt_demo_opt_" if optimum else "gantt_demo_" 

129 if with_makespan: 

130 if with_lower_bound: 

131 return prefix + "with_makespan_and_lb" 

132 return prefix + "with_makespan" 

133 if with_lower_bound: 

134 return prefix + "with_lb" 

135 return prefix + "without_makespan" 

136 

137 

138def demo_gantt_chart(dirname: str, 

139 optimum: bool = False, 

140 with_makespan: bool = False, 

141 with_lower_bound: bool = False, 

142 width: float | int | None = cm_to_inch(10), 

143 height: float | int | None = None, 

144 filename: str | Callable = 

145 __make_gantt_demo_name) -> list[Path]: 

146 """ 

147 Plot the demo gantt chart. 

148 

149 :param dirname: the directory 

150 :param optimum: should we return the optimal solution? 

151 :param with_makespan: should the makespan be included? 

152 :param with_lower_bound: should the lower bound be included? 

153 :param width: the optional width 

154 :param height: the optional height 

155 :param filename: the file name 

156 """ 

157 the_dir: Final[Path] = Path(dirname) 

158 the_dir.ensure_dir_exists() 

159 

160 if callable(filename): 

161 filename = filename(optimum, 

162 with_makespan, 

163 with_lower_bound) 

164 if not isinstance(filename, str): 

165 raise type_error(filename, "filename", str) 

166 

167 result: Final[list[Path]] = [] 

168 markers: list[Callable] = [] 

169 

170 if with_makespan: 

171 def info(g: Gantt) -> str: 

172 return Lang.current().format_str("gantt_info", gantt=g) 

173 if not optimum: 

174 markers.append(marker_makespan) 

175 else: 

176 def info(g: Gantt) -> str: 

177 return Lang.current().format_str("gantt_info_no_ms", gantt=g) 

178 

179 if with_lower_bound: 

180 markers.append(marker_lb) 

181 

182 for lang in Lang.all_langs(): 

183 lang.set_current() 

184 figure: Figure = create_figure(width=width, height=height) 

185 

186 gantt = demo_solution(optimum=optimum) 

187 plot_gantt_chart(gantt=gantt, 

188 figure=figure, 

189 x_label_inside=False, 

190 y_label_inside=False, 

191 markers=markers, 

192 info=info) 

193 

194 result.extend(save_figure(fig=figure, 

195 dir_name=the_dir, 

196 file_name=lang.filename(filename), 

197 formats="svg")) 

198 return result 

199 

200 

201def makespan_lower_bound_table( 

202 dirname: str, 

203 filename: str = "makespan_lower_bound", 

204 instances: Iterable[str] = 

205 tuple(["demo"] + list(INSTANCES))) -> Path: # noqa 

206 """ 

207 Make a table with the makespan lower bounds. 

208 

209 Larger lower bounds are taken from the repository 

210 https://github.com/thomasWeise/jsspInstancesAndResults. 

211 

212 :param dirname: the directory where to store the generated file. 

213 :param filename: the filename 

214 :param instances: the instances 

215 :returns: the generated file 

216 """ 

217 insts = list(instances) 

218 insts.sort() 

219 i = insts.index("demo") 

220 if i >= 0: 

221 del insts[i] 

222 insts.insert(0, "demo") 

223 

224 # get the data with the lower bounds information 

225 url: Final[str] = "https://github.com/thomasWeise/jsspInstancesAndResults" 

226 logger(f"now loading data from {url!r}.") 

227 with rq.urlopen(url) as f: # nosec # noqa 

228 data: str = f.read().decode(UTF8).strip() 

229 if not data: 

230 raise ValueError(f"Could not load data form {url}.") 

231 logger(f"finished loading data from {url!r}.") 

232 start = data.find("<table>") 

233 if start <= 0: 

234 raise ValueError(f"Could not find <table> in {data}.") 

235 end = data.rfind("</dl>", start) 

236 if end <= start: 

237 raise ValueError(f"Could not find </dl> in {data}.") 

238 data = data[start:end].strip() 

239 

240 lang: Final[Lang] = Lang.current() 

241 

242 text: Final[list[str]] = [ 

243 (r"|name|$\jsspJobs$|$\jsspMachines$|$\lowerBound(\objf)$|" 

244 r"$\lowerBound(\objf)^{\star}$|source for&nbsp;" 

245 r"$\lowerBound(\objf)^{\star}$|"), 

246 "|:--|--:|--:|--:|--:|:--|", 

247 ] 

248 bsrc: Final[str] = "[@eq:jsspLowerBound]" 

249 

250 # compute the data 

251 for instn in insts: 

252 solved_to_optimality: bool = False 

253 inst = Instance.from_resource(instn) 

254 lb = compute_makespan_lower_bound(machines=inst.machines, 

255 jobs=inst.jobs, 

256 matrix=inst) 

257 src = bsrc 

258 if instn == "demo": 

259 lbx = lb 

260 solved_to_optimality = True 

261 else: 

262 start = data.find( 

263 f'<td align="right"><strong>{instn}</strong></td>') 

264 if start <= 0: 

265 start = data.find(f'<td align="right">{instn}</td>') 

266 if start <= 0: 

267 raise ValueError(f"Could not find instance {instn!r}.") 

268 prefix = "" 

269 suffix = "</td>" 

270 solved_to_optimality = True 

271 else: 

272 prefix = "<strong>" 

273 suffix = "</strong></td>" 

274 end = data.find("</tr>", start) 

275 if end <= start: 

276 raise ValueError( 

277 f"Could not find end of instance {instn!r}.") 

278 sel = data[start:end].strip() 

279 logger(f"located instance text {sel!r} for {instn!r}.") 

280 prefix = (f'<td align="right">{inst.jobs}</td>\n<td align="' 

281 f'right">{inst.machines}</td>\n<td align="right">' 

282 f"{prefix}") 

283 pi = sel.find(prefix) 

284 if pi <= 0: 

285 raise ValueError( 

286 f"Could not find prefix for instance {instn!r}.") 

287 sel = sel[(pi + len(prefix)):] 

288 si = sel.find(suffix) 

289 if si <= 0: 

290 raise ValueError( 

291 f"Could not find suffix for instance {instn!r}.") 

292 lbx = int(sel[0:si]) 

293 npt = '<td align="center"><a href="#' 

294 npi = sel.find(npt, si) 

295 if npi < si: 

296 raise ValueError( 

297 f"Could not find source start for instance {instn!r}.") 

298 sel = sel[npi + len(npt):] 

299 end = sel.find('"') 

300 if end <= 0: 

301 raise ValueError( 

302 f"Could not find source end for instance {instn!r}.") 

303 srcname = sel[:end].strip() 

304 logger(f"Source name is {srcname}.") 

305 

306 fsrc = data.find(f'<dt id="user-content-{srcname.lower()}">' 

307 f"{srcname}</dt><dd>") 

308 if fsrc <= 0: 

309 raise ValueError( 

310 f"Could not find source mark for instance {instn!r}.") 

311 fsrce = data.find("</dd>", fsrc) 

312 if fsrce <= fsrc: 

313 raise ValueError( 

314 f"Could not find end mark for instance {instn!r}.") 

315 sel = data[fsrc:fsrce].strip() 

316 nm = sel.rfind("</a>") 

317 if nm <= 0: 

318 raise ValueError( 

319 f"Could not find link end for instance {instn!r}" 

320 f" and source name {srcname}.") 

321 fm = sel.rfind(">", 0, nm) 

322 if fm >= nm: 

323 raise ValueError( 

324 f"Could not find label mark for instance {instn!r}" 

325 f" and source name {srcname}.") 

326 src = f"[@{sel[(fm + 1):nm].strip()}]" 

327 

328 if lbx <= lb: 

329 if lbx < lb: 

330 raise ValueError(f"Lbx {lbx} cannot be larger than lb {lb}.") 

331 if not solved_to_optimality: 

332 src = "[@eq:jsspLowerBound]" 

333 instname = f"`{instn}`" 

334 if not solved_to_optimality: 

335 instname = f"**{instname}**" 

336 

337 text.append( 

338 f"|{instname}|{inst.machines}|{inst.jobs}|{lb}|{lbx}|{src}|") 

339 

340 # write the result 

341 out_dir = Path(dirname) 

342 out_dir.ensure_dir_exists() 

343 out_file = out_dir.resolve_inside(lang.filename(filename) + ".md") 

344 with out_dir.open_for_write() as wd: 

345 write_lines(text, wd) 

346 out_file.enforce_file() 

347 logger(f"finished writing output results to file {out_file!r}.") 

348 return out_file 

349 

350 

351# are we being executed? 

352if __name__ == "__main__": 

353 dest_dir: Final[Path] = Path(sys.argv[1]) 

354 dest_dir.ensure_dir_exists() 

355 logger(f"We will print the JSSP examples into dir {dest_dir!r}.") 

356 makespan_lower_bound_table(dest_dir) 

357 demo_gantt_chart(dest_dir, True) 

358 demo_gantt_chart(dest_dir, False) 

359 logger(f"Finished printing the JSSP examples into dir {dest_dir!r}.")