Coverage for lasso/diffcrash/diffcrash_run.py: 0%

587 statements  

« prev     ^ index     » next       coverage.py v7.2.4, created at 2023-04-28 18:42 +0100

1import argparse 

2import glob 

3import logging 

4import os 

5import platform 

6import re 

7import shutil 

8import subprocess 

9import sys 

10import time 

11import typing 

12from concurrent import futures 

13from typing import List, Union 

14 

15import psutil 

16 

17from ..logging import str_error, str_info, str_running, str_success, str_warn 

18 

19# pylint: disable = too-many-lines 

20 

21DC_STAGE_SETUP = "SETUP" 

22DC_STAGE_IMPORT = "IMPORT" 

23DC_STAGE_MATH = "MATH" 

24DC_STAGE_EXPORT = "EXPORT" 

25DC_STAGE_MATRIX = "MATRIX" 

26DC_STAGE_EIGEN = "EIGEN" 

27DC_STAGE_MERGE = "MERGE" 

28DC_STAGES = [ 

29 DC_STAGE_SETUP, 

30 DC_STAGE_IMPORT, 

31 DC_STAGE_MATH, 

32 DC_STAGE_EXPORT, 

33 DC_STAGE_MATRIX, 

34 DC_STAGE_EIGEN, 

35 DC_STAGE_MERGE, 

36] 

37 

38 

39def get_application_header(): 

40 """Prints the header of the command line tool""" 

41 

42 return """ 

43 

44 ==== D I F F C R A S H ==== 

45 

46 an open-lasso-python utility script 

47 """ 

48 

49 

50def str2bool(value) -> bool: 

51 """Converts some value from the cmd line to a boolean 

52 

53 Parameters 

54 ---------- 

55 value: `str` or `bool` 

56 

57 Returns 

58 ------- 

59 bool_value: `bool` 

60 value as boolean 

61 """ 

62 

63 if isinstance(value, bool): 

64 return value 

65 if value.lower() in ("yes", "true", "t", "y", "1"): 

66 return True 

67 if value.lower() in ("no", "false", "f", "n", "0"): 

68 return False 

69 raise argparse.ArgumentTypeError("Boolean value expected.") 

70 

71 

72def parse_diffcrash_args(): 

73 """Parse the arguments from the command line 

74 

75 Returns 

76 ------- 

77 args : `argparse.Namespace` 

78 parsed arguments 

79 """ 

80 

81 # print title 

82 print(get_application_header()) 

83 

84 parser = argparse.ArgumentParser( 

85 description="Python utility script for Diffcrash written by OPEN-LASSO." 

86 ) 

87 

88 parser.add_argument( 

89 "--reference-run", type=str, required=True, help="filepath of the reference run." 

90 ) 

91 parser.add_argument( 

92 "--exclude-runs", type=str, nargs="*", default=[], help="Runs to exclude from the analysis." 

93 ) 

94 parser.add_argument( 

95 "--crash-code", 

96 type=str, 

97 required=True, 

98 help="Which crash code is used ('dyna', 'pam' or 'radioss').", 

99 ) 

100 parser.add_argument( 

101 "--start-stage", 

102 type=str, 

103 nargs="?", 

104 default=DC_STAGES[0], 

105 help=f"At which specific stage to start the analysis ({', '.join(DC_STAGES)}).", 

106 ) 

107 parser.add_argument( 

108 "--end-stage", 

109 type=str, 

110 nargs="?", 

111 default=DC_STAGES[-1], 

112 help=f"At which specific stage to stop the analysis ({', '.join(DC_STAGES)}).", 

113 ) 

114 parser.add_argument( 

115 "--diffcrash-home", 

116 type=str, 

117 default=os.environ["DIFFCRASHHOME"] if "DIFFCRASHHOME" in os.environ else "", 

118 nargs="?", 

119 required=False, 

120 help=( 

121 "Home directory where Diffcrash is installed." 

122 " Uses environment variable 'DIFFCRASHHOME' if unspecified." 

123 ), 

124 ) 

125 parser.add_argument( 

126 "--use-id-mapping", 

127 type=str2bool, 

128 nargs="?", 

129 const=True, 

130 default=False, 

131 help="Whether to use id-based mapping (default is nearest neighbour).", 

132 ) 

133 parser.add_argument( 

134 "--project-dir", 

135 type=str, 

136 nargs="?", 

137 default="project", 

138 help="Project dir to use for femzip.", 

139 ) 

140 parser.add_argument( 

141 "--config-file", type=str, nargs="?", default="", help="Path to the config file." 

142 ) 

143 parser.add_argument( 

144 "--parameter-file", type=str, nargs="?", default="", help="Path to the parameter file." 

145 ) 

146 parser.add_argument( 

147 "--n-processes", 

148 type=int, 

149 nargs="?", 

150 default=max(1, psutil.cpu_count() - 1), 

151 help="Number of processes to use (default: max-1).", 

152 ) 

153 parser.add_argument( 

154 "simulation_runs", 

155 type=str, 

156 nargs="*", 

157 help="Simulation runs or patterns used to search for simulation runs.", 

158 ) 

159 

160 if len(sys.argv) < 2: 

161 parser.print_help() 

162 sys.exit(0) 

163 

164 return parser.parse_args(sys.argv[1:]) 

165 

166 

167def run_subprocess(args): 

168 """Run a subprocess with the specified arguments 

169 

170 Parameters: 

171 ----------- 

172 args : `list` of `str` 

173 

174 Returns 

175 ------- 

176 rc : `int` 

177 process return code 

178 

179 Notes 

180 ----- 

181 Suppresses stderr. 

182 """ 

183 return subprocess.Popen(args, stderr=subprocess.DEVNULL).wait() 

184 

185 

186class DiffcrashRun: 

187 """Class for handling the settings of a diffcrash run""" 

188 

189 # pylint: disable = too-many-instance-attributes 

190 

191 # pylint: disable = too-many-arguments 

192 def __init__( 

193 self, 

194 project_dir: str, 

195 crash_code: str, 

196 reference_run: str, 

197 simulation_runs: typing.Sequence[str], 

198 exclude_runs: typing.Sequence[str], 

199 diffcrash_home: str = "", 

200 use_id_mapping: bool = False, 

201 config_file: str = None, 

202 parameter_file: str = None, 

203 n_processes: int = 1, 

204 logfile_dir: str = None, 

205 ): 

206 """Object handling a diffcrash run 

207 

208 Parameters 

209 ---------- 

210 project_dir : `str` 

211 directory to put all buffer files etc., in 

212 crash_code : `str` 

213 crash code to use. 

214 reference_run : `str` 

215 filepath to the reference run 

216 simulation_runs: `list` of `str` 

217 patterns used to search for simulation runs 

218 diffcrash_home : `str` 

219 home directory of diffcrash installation. Uses environment 

220 variable DIFFCRASHHOME if not set. 

221 use_id_mapping : `bool` 

222 whether to use id mapping instead of nearest neighbor mapping 

223 config_file : `str` 

224 filepath to a config file 

225 parameter_file : `str` 

226 filepath to the parameter file 

227 n_processes : `int` 

228 number of processes to spawn for worker pool 

229 logfile_dir : `str` 

230 directory to put logfiles in 

231 """ 

232 

233 # settings 

234 self._msg_option = "{:16s}: {}" 

235 self._log_formatter = logging.Formatter("%(levelname)s:%(asctime)s %(message)s") 

236 

237 # logdir 

238 if logfile_dir is not None: 

239 self.logfile_dir = logfile_dir 

240 else: 

241 self.logfile_dir = os.path.join(project_dir, "Log") 

242 self.logfile_filepath = os.path.join(self.logfile_dir, "DiffcrashRun.log") 

243 

244 # logger 

245 self.logger = self._setup_logger() 

246 

247 # make some space in the log 

248 self.logger.info(get_application_header()) 

249 

250 # diffcrash home 

251 self.diffcrash_home = self._parse_diffcrash_home(diffcrash_home) 

252 self.diffcrash_home = os.path.join(self.diffcrash_home, "bin") 

253 self.diffcrash_lib = os.path.join(os.path.dirname(self.diffcrash_home), "lib") 

254 

255 if platform.system() == "Linux": 

256 os.environ["PATH"] = ( 

257 os.environ["PATH"] + ":" + self.diffcrash_home + ":" + self.diffcrash_lib 

258 ) 

259 if platform.system() == "Windows": 

260 os.environ["PATH"] = ( 

261 os.environ["PATH"] + ";" + self.diffcrash_home + ";" + self.diffcrash_lib 

262 ) 

263 

264 # project dir 

265 self.project_dir = self._parse_project_dir(project_dir) 

266 

267 # crashcode 

268 self.crash_code = self._parse_crash_code(crash_code) 

269 

270 # reference run 

271 self.reference_run = self._parse_reference_run(reference_run) 

272 

273 # mapping 

274 self.use_id_mapping = self._parse_use_id_mapping(use_id_mapping) 

275 

276 # exlude runs 

277 self.exclude_runs = exclude_runs 

278 

279 # simulation runs 

280 self.simulation_runs = self._parse_simulation_runs( 

281 simulation_runs, self.reference_run, self.exclude_runs 

282 ) 

283 

284 # config file 

285 self.config_file = self._parse_config_file(config_file) 

286 

287 # parameter file 

288 self.parameter_file = self._parse_parameter_file(parameter_file) 

289 

290 # n processes 

291 self.n_processes = self._parse_n_processes(n_processes) 

292 

293 def _setup_logger(self) -> logging.Logger: 

294 

295 # better safe than sorry 

296 os.makedirs(self.logfile_dir, exist_ok=True) 

297 

298 # create console log channel 

299 # streamHandler = logging.StreamHandler(sys.stdout) 

300 # streamHandler.setLevel(logging.INFO) 

301 # streamHandler.setFormatter(self._log_formatter) 

302 

303 # create file log channel 

304 file_handler = logging.FileHandler(self.logfile_filepath) 

305 file_handler.setLevel(logging.INFO) 

306 file_handler.setFormatter(self._log_formatter) 

307 

308 # create logger 

309 logger = logging.getLogger("DiffcrashRun") 

310 logger.setLevel(logging.INFO) 

311 # logger.addHandler(streamHandler) 

312 logger.addHandler(file_handler) 

313 

314 return logger 

315 

316 def _parse_diffcrash_home(self, diffcrash_home) -> str: 

317 

318 diffcrash_home_ok = len(diffcrash_home) != 0 

319 

320 msg = self._msg_option.format("diffcrash-home", diffcrash_home) 

321 print(str_info(msg)) 

322 self.logger.info(msg) 

323 

324 if not diffcrash_home_ok: 

325 err_msg = ( 

326 "Specify the path to the Diffcrash installation either " 

327 + "with the environment variable 'DIFFCRASHHOME' or the option --diffcrash-home." 

328 ) 

329 self.logger.error(err_msg) 

330 raise RuntimeError(str_error(err_msg)) 

331 

332 return diffcrash_home 

333 

334 def _parse_crash_code(self, crash_code) -> str: 

335 

336 # these guys are allowed 

337 valid_crash_codes = ["dyna", "radioss", "pam"] 

338 

339 # do the thing 

340 crash_code_ok = crash_code in valid_crash_codes 

341 

342 print(str_info(self._msg_option.format("crash-code", crash_code))) 

343 self.logger.info(self._msg_option.format("crash-code", crash_code)) 

344 

345 if not crash_code_ok: 

346 err_msg = ( 

347 f"Invalid crash code '{crash_code}'. " 

348 f"Please use one of: {str(valid_crash_codes)}" 

349 ) 

350 self.logger.error(err_msg) 

351 raise RuntimeError(str_error(err_msg)) 

352 

353 return crash_code 

354 

355 def _parse_reference_run(self, reference_run) -> str: 

356 

357 reference_run_ok = os.path.isfile(reference_run) 

358 

359 msg = self._msg_option.format("reference-run", reference_run) 

360 print(str_info(msg)) 

361 self.logger.info(msg) 

362 

363 if not reference_run_ok: 

364 err_msg = f"Filepath '{reference_run}' is not a file." 

365 self.logger.error(err_msg) 

366 raise RuntimeError(str_error(err_msg)) 

367 

368 return reference_run 

369 

370 def _parse_use_id_mapping(self, use_id_mapping) -> bool: 

371 

372 msg = self._msg_option.format("use-id-mapping", use_id_mapping) 

373 print(str_info(msg)) 

374 self.logger.info(msg) 

375 

376 return use_id_mapping 

377 

378 def _parse_project_dir(self, project_dir): 

379 project_dir = os.path.abspath(project_dir) 

380 

381 msg = self._msg_option.format("project-dir", project_dir) 

382 print(str_info(msg)) 

383 self.logger.info(msg) 

384 

385 return project_dir 

386 

387 def _parse_simulation_runs( 

388 self, 

389 simulation_run_patterns: typing.Sequence[str], 

390 reference_run: str, 

391 exclude_runs: typing.Sequence[str], 

392 ): 

393 

394 # search all denoted runs 

395 simulation_runs = [] 

396 for pattern in simulation_run_patterns: 

397 simulation_runs += glob.glob(pattern) 

398 simulation_runs = [filepath for filepath in simulation_runs if os.path.isfile(filepath)] 

399 

400 # search all excluded runs 

401 runs_to_exclude = [] 

402 for pattern in exclude_runs: 

403 runs_to_exclude += glob.glob(pattern) 

404 runs_to_exclude = [filepath for filepath in runs_to_exclude if os.path.isfile(filepath)] 

405 

406 n_runs_before_filtering = len(simulation_runs) 

407 simulation_runs = [ 

408 filepath for filepath in simulation_runs if filepath not in runs_to_exclude 

409 ] 

410 n_runs_after_filtering = len(simulation_runs) 

411 

412 # remove the reference run 

413 if reference_run in simulation_runs: 

414 simulation_runs.remove(reference_run) 

415 

416 # sort it because we can! 

417 def atoi(text): 

418 return int(text) if text.isdigit() else text 

419 

420 def natural_keys(text): 

421 return [atoi(c) for c in re.split(r"(\d+)", text)] 

422 

423 simulation_runs = sorted(simulation_runs, key=natural_keys) 

424 

425 # check 

426 simulation_runs_ok = len(simulation_runs) != 0 

427 

428 msg = self._msg_option.format("# simul.-files", len(simulation_runs)) 

429 print(str_info(msg)) 

430 self.logger.info(msg) 

431 

432 msg = self._msg_option.format( 

433 "# excluded files", (n_runs_before_filtering - n_runs_after_filtering) 

434 ) 

435 print(str_info(msg)) 

436 self.logger.info(msg) 

437 

438 if not simulation_runs_ok: 

439 err_msg = ( 

440 "No simulation files could be found with the specified patterns. " 

441 "Check the argument 'simulation_runs'." 

442 ) 

443 self.logger.error(err_msg) 

444 raise RuntimeError(str_error(err_msg)) 

445 

446 return simulation_runs 

447 

448 def _parse_config_file(self, config_file) -> Union[str, None]: 

449 

450 _msg_config_file = "" 

451 if len(config_file) > 0 and not os.path.isfile(config_file): 

452 config_file = None 

453 _msg_config_file = f"Can not find config file '{config_file}'" 

454 

455 # missing config file 

456 else: 

457 

458 config_file = None 

459 _msg_config_file = ( 

460 "Config file missing. " 

461 "Consider specifying the path with the option '--config-file'." 

462 ) 

463 

464 msg = self._msg_option.format("config-file", config_file) 

465 print(str_info(msg)) 

466 self.logger.info(msg) 

467 

468 if _msg_config_file: 

469 print(str_warn(_msg_config_file)) 

470 self.logger.warning(_msg_config_file) 

471 

472 return config_file 

473 

474 def _parse_parameter_file(self, parameter_file) -> Union[None, str]: 

475 

476 _msg_parameter_file = "" 

477 if len(parameter_file) > 0 and not os.path.isfile(parameter_file): 

478 parameter_file = None 

479 _msg_parameter_file = f"Can not find parameter file '{parameter_file}'" 

480 # missing parameter file 

481 else: 

482 parameter_file = None 

483 _msg_parameter_file = ( 

484 "Parameter file missing. Consider specifying the " 

485 "path with the option '--parameter-file'." 

486 ) 

487 

488 msg = self._msg_option.format("parameter-file", parameter_file) 

489 print(str_info(msg)) 

490 self.logger.info(msg) 

491 

492 if _msg_parameter_file: 

493 print(str_warn(_msg_parameter_file)) 

494 self.logger.warning(_msg_parameter_file) 

495 

496 return parameter_file 

497 

498 def _parse_n_processes(self, n_processes) -> int: 

499 

500 print(str_info(self._msg_option.format("n-processes", n_processes))) 

501 

502 if n_processes <= 0: 

503 err_msg = f"n-processes is '{n_processes}' but must be at least 1." 

504 self.logger.error(err_msg) 

505 raise ValueError(str_error(err_msg)) 

506 

507 return n_processes 

508 

509 def create_project_dirs(self): 

510 """Creates all project relevant directores 

511 

512 Notes 

513 ----- 

514 Created dirs: 

515 - logfile_dir 

516 - project_dir 

517 """ 

518 os.makedirs(self.project_dir, exist_ok=True) 

519 os.makedirs(self.logfile_dir, exist_ok=True) 

520 

521 def run_setup(self, pool: futures.ThreadPoolExecutor): 

522 """Run diffcrash setup 

523 

524 Parameters 

525 ---------- 

526 pool : `concurrent.futures.ThreadPoolExecutor` 

527 multiprocessing pool 

528 """ 

529 

530 # SETUP 

531 msg = "Running Setup ... " 

532 print(str_running(msg) + "\r", end="", flush="") 

533 self.logger.info(msg) 

534 

535 args = [] 

536 if self.config_file is None and self.parameter_file is None: 

537 args = [ 

538 os.path.join(self.diffcrash_home, "DFC_Setup_" + self.crash_code + "_fem"), 

539 self.reference_run, 

540 self.project_dir, 

541 ] 

542 elif self.config_file is not None and self.parameter_file is None: 

543 args = [ 

544 os.path.join(self.diffcrash_home, "DFC_Setup_" + self.crash_code + "_fem"), 

545 self.reference_run, 

546 self.project_dir, 

547 "-C", 

548 self.config_file, 

549 ] 

550 elif self.config_file is None and self.parameter_file is not None: 

551 if ".fz" in self.reference_run: 

552 args = [ 

553 os.path.join(self.diffcrash_home, "DFC_Setup_" + self.crash_code + "_fem"), 

554 self.reference_run, 

555 self.project_dir, 

556 "-P", 

557 self.parameter_file, 

558 ] 

559 else: 

560 args = [ 

561 os.path.join(self.diffcrash_home, "DFC_Setup_" + self.crash_code), 

562 self.reference_run, 

563 self.project_dir, 

564 "-P", 

565 self.parameter_file, 

566 ] 

567 elif self.config_file is not None and self.parameter_file is not None: 

568 if ".fz" in self.reference_run: 

569 args = [ 

570 os.path.join(self.diffcrash_home, "DFC_Setup_" + self.crash_code + "_fem"), 

571 self.reference_run, 

572 self.project_dir, 

573 "-C", 

574 self.config_file, 

575 "-P", 

576 self.parameter_file, 

577 ] 

578 else: 

579 args = [ 

580 os.path.join(self.diffcrash_home, "DFC_Setup_" + self.crash_code), 

581 self.reference_run, 

582 self.project_dir, 

583 "-C", 

584 self.config_file, 

585 "-P", 

586 self.parameter_file, 

587 ] 

588 start_time = time.time() 

589 

590 # submit task 

591 return_code_future = pool.submit(run_subprocess, args) 

592 return_code = return_code_future.result() 

593 

594 # check return code 

595 if return_code != 0: 

596 err_msg = f"Running Setup ... done in {time.time() - start_time:.2f}s" 

597 print(str_error(err_msg)) 

598 self.logger.error(err_msg) 

599 

600 err_msg = "Process somehow failed." 

601 self.logger.error(err_msg) 

602 raise RuntimeError(str_error(err_msg)) 

603 

604 # check log 

605 messages = self.check_if_logfiles_show_success("DFC_Setup.log") 

606 if messages: 

607 err_msg = f"Running Setup ... done in {time.time() - start_time:.2f}s" 

608 print(str_error(err_msg)) 

609 self.logger.error(err_msg) 

610 

611 # print failed logs 

612 for msg in messages: 

613 print(str_error(msg)) 

614 self.logger.error(msg) 

615 

616 err_msg = "Setup failed." 

617 self.logger.error(err_msg) 

618 raise RuntimeError(str_error(err_msg)) 

619 

620 # print success 

621 err_msg = f"Running Setup ... done in {time.time() - start_time:.2f}s" 

622 print(str_success(msg)) 

623 self.logger.info(msg) 

624 

625 def run_import(self, pool: futures.ThreadPoolExecutor): 

626 """Run diffcrash import of runs 

627 

628 Parameters 

629 ---------- 

630 pool : `concurrent.futures.ThreadPoolExecutor` 

631 multiprocessing pool 

632 """ 

633 

634 # pylint: disable = too-many-locals, too-many-branches, too-many-statements 

635 

636 # list of arguments to run in the command line 

637 import_arguments = [] 

638 

639 # id 1 is the reference run 

640 # id 2 and higher are the imported runs 

641 counter_offset = 2 

642 

643 # assemble arguments for running the import 

644 # entry 0 is the reference run, thus we start at 1 

645 # pylint: disable = consider-using-enumerate 

646 for i_filepath in range(len(self.simulation_runs)): 

647 

648 # parameter file missing 

649 if self.parameter_file is None: 

650 if self.use_id_mapping: 

651 args = [ 

652 os.path.join(self.diffcrash_home, "DFC_Import_" + self.crash_code + "_fem"), 

653 "-id", 

654 self.simulation_runs[i_filepath], 

655 self.project_dir, 

656 str(i_filepath + counter_offset), 

657 ] 

658 else: 

659 args = [ 

660 os.path.join(self.diffcrash_home, "DFC_Import_" + self.crash_code + "_fem"), 

661 self.simulation_runs[i_filepath], 

662 self.project_dir, 

663 str(i_filepath + counter_offset), 

664 ] 

665 # indeed there is a parameter file 

666 else: 

667 if self.use_id_mapping: 

668 args = [ 

669 os.path.join(self.diffcrash_home, "DFC_Import_" + self.crash_code), 

670 "-ID", 

671 self.simulation_runs[i_filepath], 

672 self.project_dir, 

673 str(i_filepath + counter_offset), 

674 ] 

675 else: 

676 args = [ 

677 os.path.join(self.diffcrash_home, "DFC_Import_" + self.crash_code), 

678 self.simulation_runs[i_filepath], 

679 self.project_dir, 

680 str(i_filepath + counter_offset), 

681 ] 

682 

683 # append args to list 

684 import_arguments.append(args) 

685 

686 # do the thing 

687 msg = "Running Imports ...\r" 

688 print(str_running(msg), end="", flush=True) 

689 self.logger.info(msg) 

690 start_time = time.time() 

691 return_code_futures = [pool.submit(run_subprocess, args) for args in import_arguments] 

692 

693 # wait for imports to finish (with a progressbar) 

694 n_imports_finished = sum( 

695 return_code_future.done() for return_code_future in return_code_futures 

696 ) 

697 while n_imports_finished != len(return_code_futures): 

698 

699 # check again 

700 n_new_imports_finished = sum( 

701 return_code_future.done() for return_code_future in return_code_futures 

702 ) 

703 

704 # print 

705 percentage = n_new_imports_finished / len(return_code_futures) * 100 

706 

707 if n_imports_finished != n_new_imports_finished: 

708 # pylint: disable = consider-using-f-string 

709 msg = "Running Imports ... [{0}/{1}] - {2:3.2f}%\r".format( 

710 n_new_imports_finished, len(return_code_futures), percentage 

711 ) 

712 print(str_running(msg), end="", flush=True) 

713 self.logger.info(msg) 

714 

715 n_imports_finished = n_new_imports_finished 

716 

717 # wait a little bit 

718 time.sleep(0.25) 

719 

720 return_codes = [return_code_future.result() for return_code_future in return_code_futures] 

721 

722 # print failure 

723 if any(return_code != 0 for return_code in return_codes): 

724 

725 n_failed_runs = 0 

726 for i_run, return_code in enumerate(return_codes): 

727 if return_code != 0: 

728 _err_msg = str_error( 

729 f"Run {i_run} failed to import with error code '{return_code}'." 

730 ) 

731 print(str_error(_err_msg)) 

732 self.logger.error(_err_msg) 

733 n_failed_runs += 1 

734 

735 err_msg = f"Running Imports ... done in {time.time() - start_time:.2f}s " 

736 print(str_error(err_msg)) 

737 self.logger.error(err_msg) 

738 

739 err_msg = f"Import of {n_failed_runs} runs failed." 

740 self.logger.error(err_msg) 

741 raise RuntimeError(str_error(err_msg)) 

742 

743 # check log files 

744 messages = self.check_if_logfiles_show_success("DFC_Import_*.log") 

745 if messages: 

746 

747 # print failure 

748 msg = f"Running Imports ... done in {time.time() - start_time:.2f}s " 

749 print(str_error(msg)) 

750 self.logger.info(msg) 

751 

752 # print failed logs 

753 for msg in messages: 

754 self.logger.error(msg) 

755 print(str_error(msg)) 

756 

757 err_msg = ( 

758 f"At least one import failed. Please check the log files in '{self.logfile_dir}'." 

759 ) 

760 self.logger.error(err_msg) 

761 raise RuntimeError(str_error(err_msg)) 

762 

763 # print success 

764 print(str_success(f"Running Imports ... done in {time.time() - start_time:.2f}s ")) 

765 

766 def run_math(self, pool: futures.ThreadPoolExecutor): 

767 """Run diffcrash math 

768 

769 Parameters 

770 ---------- 

771 pool : `concurrent.futures.ThreadPoolExecutor` 

772 multiprocessing pool 

773 """ 

774 

775 msg = "Running Math ... \r" 

776 print(str_running(msg), end="", flush=True) 

777 self.logger.info(msg) 

778 

779 start_time = time.time() 

780 return_code_future = pool.submit( 

781 run_subprocess, 

782 [os.path.join(self.diffcrash_home, "DFC_Math_" + self.crash_code), self.project_dir], 

783 ) 

784 return_code = return_code_future.result() 

785 

786 # check return code 

787 if return_code != 0: 

788 

789 msg = f"Running Math ... done in {time.time() - start_time:.2f}s " 

790 print(str_error(msg)) 

791 self.logger.error(msg) 

792 

793 err_msg = f"Caught a nonzero return code '{return_code}'" 

794 self.logger.error(err_msg) 

795 raise RuntimeError(str_error(err_msg)) 

796 

797 # check logs 

798 messages = self.check_if_logfiles_show_success("DFC_MATH*.log") 

799 if messages: 

800 

801 # print failure 

802 msg = f"Running Math ... done in {time.time() - start_time:.2f}s " 

803 print(str_error(msg)) 

804 self.logger.error(msg) 

805 

806 # print failed logs 

807 for msg in messages: 

808 print(str_error(msg)) 

809 self.logger.error(msg) 

810 

811 err_msg = ( 

812 "Logfile does indicate a failure. " 

813 f"Please check the log files in '{self.logfile_dir}'." 

814 ) 

815 self.logger.error(err_msg) 

816 raise RuntimeError(str_error(err_msg)) 

817 

818 # print success 

819 msg = f"Running Math ... done in {time.time() - start_time:.2f}s " 

820 print(str_success(msg)) 

821 self.logger.info(msg) 

822 

823 def run_export(self, pool: futures.ThreadPoolExecutor): 

824 """Run diffcrash export 

825 

826 Parameters 

827 ---------- 

828 pool : `concurrent.futures.ThreadPoolExecutor` 

829 multiprocessing pool 

830 """ 

831 

832 msg = "Running Export ... " 

833 print(str_running(msg) + "\r", end="", flush=True) 

834 self.logger.info(msg) 

835 

836 if self.config_file is None: 

837 export_item_list = [] 

838 

839 # check for pdmx 

840 pdmx_filepath_list = glob.glob(os.path.join(self.project_dir, "*_pdmx")) 

841 if pdmx_filepath_list: 

842 export_item_list.append(os.path.basename(pdmx_filepath_list[0])) 

843 

844 # check for pdij 

845 pdij_filepath_list = glob.glob(os.path.join(self.project_dir, "*_pdij")) 

846 if pdij_filepath_list: 

847 export_item_list.append(os.path.basename(pdij_filepath_list[0])) 

848 

849 else: 

850 export_item_list = self.read_config_file(self.config_file) 

851 

852 # remove previous existing exports 

853 for export_item in export_item_list: 

854 export_item_filepath = os.path.join(self.project_dir, export_item + ".d3plot.fz") 

855 if os.path.isfile(export_item_filepath): 

856 os.remove(export_item_filepath) 

857 

858 # do the thing 

859 start_time = time.time() 

860 return_code_futures = [ 

861 pool.submit( 

862 run_subprocess, 

863 [ 

864 os.path.join(self.diffcrash_home, "DFC_Export_" + self.crash_code), 

865 self.project_dir, 

866 export_item, 

867 ], 

868 ) 

869 for export_item in export_item_list 

870 ] 

871 

872 return_codes = [result_future.result() for result_future in return_code_futures] 

873 

874 # check return code 

875 if any(rc != 0 for rc in return_codes): 

876 msg = f"Running Export ... done in {time.time() - start_time:.2f}s " 

877 print(str_error(msg)) 

878 self.logger.error(msg) 

879 

880 for i_export, export_return_code in enumerate(return_codes): 

881 if export_return_code != 0: 

882 msg = ( 

883 f"Return code of export '{export_item_list[i_export]}' " 

884 f"was nonzero: '{export_return_code}'" 

885 ) 

886 self.logger.error(msg) 

887 print(str_error(msg)) 

888 

889 msg = "At least one export process failed." 

890 self.logger.error(msg) 

891 raise RuntimeError(str_error(msg)) 

892 

893 # check logs 

894 messages = self.check_if_logfiles_show_success("DFC_Export_*") 

895 if messages: 

896 

897 # print failure 

898 msg = f"Running Export ... done in {time.time() - start_time:.2f}s " 

899 print(str_error(msg)) 

900 self.logger.error(msg) 

901 

902 # print logs 

903 for msg in messages: 

904 print(str_error(msg)) 

905 self.logger.error(msg) 

906 

907 msg = ( 

908 "At least one export failed. " 

909 f"Please check the log files in '{self.logfile_dir}'." 

910 ) 

911 self.logger.error(msg) 

912 raise RuntimeError(str_error(msg)) 

913 

914 # print success 

915 msg = f"Running Export ... done in {time.time() - start_time:.2f}s " 

916 print(str_success(msg)) 

917 self.logger.info(msg) 

918 

919 def run_matrix(self, pool: futures.ThreadPoolExecutor): 

920 """Run diffcrash matrix 

921 

922 Parameters 

923 ---------- 

924 pool : `concurrent.futures.ThreadPoolExecutor` 

925 multiprocessing pool 

926 """ 

927 

928 msg = "Running Matrix ... " 

929 print(str_running(msg) + "\r", end="", flush=True) 

930 self.logger.info(msg) 

931 

932 start_time = time.time() 

933 

934 # create the input file for the process 

935 matrix_inputfile = self._create_matrix_input_file(self.project_dir) 

936 

937 # run the thing 

938 return_code_future = pool.submit( 

939 run_subprocess, 

940 [ 

941 os.path.join(self.diffcrash_home, "DFC_Matrix_" + self.crash_code), 

942 self.project_dir, 

943 matrix_inputfile, 

944 ], 

945 ) 

946 

947 # please hold the line ... 

948 return_code = return_code_future.result() 

949 

950 # check return code 

951 if return_code != 0: 

952 

953 # print failure 

954 msg = f"Running Matrix ... done in {time.time() - start_time:.2f}s " 

955 print(str_error(msg)) 

956 self.logger.error(msg) 

957 

958 msg = "The DFC_Matrix process failed somehow." 

959 self.logger.error(msg) 

960 raise RuntimeError(str_error(msg)) 

961 

962 # check log file 

963 messages = self.check_if_logfiles_show_success("DFC_Matrix_*") 

964 if messages: 

965 

966 # print failure 

967 msg = f"Running Matrix ... done in {time.time() - start_time:.2f}s " 

968 print(str_error(msg)) 

969 self.logger.info(msg) 

970 

971 # print why 

972 for msg in messages: 

973 print(str_error(msg)) 

974 self.logger.error(msg) 

975 

976 msg = f"DFC_Matrix failed. Please check the log files in '{self.logfile_dir}'." 

977 self.logger.error(msg) 

978 raise RuntimeError(str_error(msg)) 

979 

980 # print success 

981 msg = f"Running Matrix ... done in {time.time() - start_time:.2f}s " 

982 print(str_success(msg)) 

983 self.logger.info(msg) 

984 

985 def run_eigen(self, pool: futures.ThreadPoolExecutor): 

986 """Run diffcrash eigen 

987 

988 Parameters 

989 ---------- 

990 pool : `concurrent.futures.ThreadPoolExecutor` 

991 multiprocessing pool 

992 """ 

993 

994 msg = "Running Eigen ... " 

995 print(str_running(msg) + "\r", end="", flush=True) 

996 self.logger.info(msg) 

997 

998 # create input file for process 

999 eigen_inputfile = self._create_eigen_input_file(self.project_dir) 

1000 

1001 # run the thing 

1002 start_time = time.time() 

1003 return_code_future = pool.submit( 

1004 run_subprocess, 

1005 [ 

1006 os.path.join(self.diffcrash_home, "DFC_Eigen_" + self.crash_code), 

1007 self.project_dir, 

1008 eigen_inputfile, 

1009 ], 

1010 ) 

1011 

1012 # please hold the line ... 

1013 return_code = return_code_future.result() 

1014 

1015 # check return code 

1016 if return_code != 0: 

1017 msg = f"Running Eigen ... done in {time.time() - start_time:.2f}s " 

1018 print(str_error(msg)) 

1019 self.logger.error(msg) 

1020 

1021 msg = "The process failed somehow." 

1022 self.logger.error(msg) 

1023 raise RuntimeError(str_error(msg)) 

1024 

1025 # check log file 

1026 messages = self.check_if_logfiles_show_success("DFC_Matrix_*") 

1027 if messages: 

1028 

1029 # print failure 

1030 msg = f"Running Eigen ... done in {time.time() - start_time:.2f}s " 

1031 print(str_error(msg)) 

1032 self.logger.error(msg) 

1033 

1034 # print why 

1035 for msg in messages: 

1036 print(str_error(msg)) 

1037 self.logger.error(msg) 

1038 

1039 msg = f"DFC_Eigen failed. Please check the log files in '{self.logfile_dir}'." 

1040 self.logger.error(msg) 

1041 raise RuntimeError(str_error(msg)) 

1042 

1043 # print success 

1044 msg = f"Running Eigen ... done in {time.time() - start_time:.2f}s " 

1045 print(str_success(msg)) 

1046 self.logger.info(msg) 

1047 

1048 def run_merge(self, pool: futures.ThreadPoolExecutor): 

1049 """Run diffcrash merge 

1050 

1051 Parameters 

1052 ---------- 

1053 pool : `concurrent.futures.ThreadPoolExecutor` 

1054 multiprocessing pool 

1055 """ 

1056 

1057 msg = "Running Merge ... " 

1058 print(str_running(msg) + "\r", end="", flush=True) 

1059 self.logger.info(msg) 

1060 

1061 # create ionput file for merge 

1062 merge_inputfile = self._create_merge_input_file(self.project_dir) 

1063 

1064 # clear previous merges 

1065 for filepath in glob.glob(os.path.join(self.project_dir, "mode_*")): 

1066 if os.path.isfile(filepath): 

1067 os.remove(filepath) 

1068 

1069 # run the thing 

1070 start_time = time.time() 

1071 return_code_future = pool.submit( 

1072 run_subprocess, 

1073 [ 

1074 os.path.join(self.diffcrash_home, "DFC_Merge_All_" + self.crash_code), 

1075 self.project_dir, 

1076 merge_inputfile, 

1077 ], 

1078 ) 

1079 return_code = return_code_future.result() 

1080 

1081 # check return code 

1082 if return_code != 0: 

1083 msg = f"Running Merge ... done in {time.time() - start_time:.2f}s " 

1084 print(str_error(msg)) 

1085 self.logger.info(msg) 

1086 

1087 msg = "The process failed somehow." 

1088 self.logger.error(msg) 

1089 raise RuntimeError(str_error(msg)) 

1090 

1091 # check logfiles 

1092 messages = self.check_if_logfiles_show_success("DFC_Merge_All.log") 

1093 if messages: 

1094 msg = f"Running Merge ... done in {time.time() - start_time:.2f}s " 

1095 print(str_error(msg)) 

1096 self.logger.error(msg) 

1097 

1098 for msg in messages: 

1099 print(str_error(msg)) 

1100 self.logger.info(msg) 

1101 

1102 msg = "DFC_Merge_All failed. Please check the log files in '{self.logfile_dir}'." 

1103 self.logger.error(msg) 

1104 raise RuntimeError(str_error(msg)) 

1105 

1106 # print success 

1107 msg = f"Running Merge ... done in {time.time() - start_time:.2f}s " 

1108 print(str_success(msg)) 

1109 self.logger.info(msg) 

1110 

1111 def is_logfile_successful(self, logfile: str) -> bool: 

1112 """Checks if a logfile indicates a success 

1113 

1114 Parameters 

1115 ---------- 

1116 logfile : `str` 

1117 filepath to the logfile 

1118 

1119 Returns 

1120 ------- 

1121 success : `bool` 

1122 """ 

1123 

1124 with open(logfile, "r", encoding="utf-8") as fp: 

1125 for line in fp: 

1126 if "successfully" in line: 

1127 return True 

1128 return False 

1129 

1130 def _create_merge_input_file(self, directory: str) -> str: 

1131 """Create an input file for the merge executable 

1132 

1133 Notes 

1134 ----- 

1135 From the official diffcrash docs. 

1136 """ 

1137 

1138 # creates default inputfile for DFC_Merge 

1139 filepath = os.path.join(directory, "merge_all.txt") 

1140 with open(filepath, "w", encoding="utf-8") as merge_input_file: 

1141 merge_input_file.write("eigen_all ! Name of eigen input file\n") 

1142 merge_input_file.write( 

1143 "mode_ ! Name of Output file " 

1144 + "(string will be apended with mode information)\n" 

1145 ) 

1146 merge_input_file.write("1 1 ! Mode number to be generated\n") 

1147 merge_input_file.write("'d+ d-' ! Mode type to be generated\n") 

1148 # TIMESTEPSFILE optional 

1149 merge_input_file.write( 

1150 " ! Optional: Timestepfile (specify timesteps used for merge)\n" 

1151 ) 

1152 # PARTSFILE optional 

1153 merge_input_file.write( 

1154 " ! Optional: Partlistfile (specify parts used for merge)\n" 

1155 ) 

1156 

1157 return filepath 

1158 

1159 def _create_eigen_input_file(self, directory: str) -> str: 

1160 """Create an input file for the eigen executable 

1161 

1162 Notes 

1163 ----- 

1164 From the official diffcrash docs. 

1165 """ 

1166 

1167 # creates default inputfile for DFC_Eigen 

1168 filepath = os.path.join(directory, "eigen_all.txt") 

1169 with open(filepath, "w", encoding="utf-8") as eigen_input_file: 

1170 eigen_input_file.write("matrix_all\n") 

1171 eigen_input_file.write('""\n') 

1172 eigen_input_file.write("1 1000\n") 

1173 eigen_input_file.write('""\n') 

1174 eigen_input_file.write("0 0\n") 

1175 eigen_input_file.write('""\n') 

1176 eigen_input_file.write("eigen_all\n") 

1177 eigen_input_file.write('""\n') 

1178 eigen_input_file.write("0 0\n") 

1179 

1180 return filepath 

1181 

1182 def _create_matrix_input_file(self, directory: str) -> str: 

1183 """Create an input file for the matrix executable 

1184 

1185 Notes 

1186 ----- 

1187 From the official diffcrash docs. 

1188 """ 

1189 

1190 # creates default inputfile for DFC_Matrix 

1191 filepath = os.path.join(directory, "matrix.txt") 

1192 with open(filepath, "w", encoding="utf-8") as matrix_input_file: 

1193 matrix_input_file.write("0 1000 ! Initial and final time stept to consider\n") 

1194 matrix_input_file.write('"" ! not used\n') 

1195 matrix_input_file.write('"" ! not used\n') 

1196 matrix_input_file.write("matrix_all ! Name of matrix file set (Output)\n") 

1197 

1198 return filepath 

1199 

1200 def clear_project_dir(self): 

1201 """Clears the entire project dir""" 

1202 

1203 # disable logging 

1204 for handler in self.logger.handlers: 

1205 handler.close() 

1206 self.logger.removeHandler(handler) 

1207 

1208 # delete folder 

1209 if os.path.exists(self.project_dir): 

1210 shutil.rmtree(self.project_dir) 

1211 

1212 # reinit logger 

1213 self.logger = self._setup_logger() 

1214 

1215 def read_config_file(self, config_file: str) -> List[str]: 

1216 """Read a diffcrash config file 

1217 

1218 Parameters 

1219 ---------- 

1220 config_file : `str` 

1221 path to the config file 

1222 

1223 Notes 

1224 ----- 

1225 From the official diffcrash docs ... seriously. 

1226 """ 

1227 

1228 # Just to make it clear, this is not code from OPEN-LASSO 

1229 # ... 

1230 

1231 # pylint: disable = too-many-locals 

1232 # pylint: disable = consider-using-enumerate 

1233 # pylint: disable = too-many-nested-blocks 

1234 # pylint: disable = too-many-branches 

1235 # pylint: disable = too-many-statements 

1236 

1237 with open(config_file, "r", encoding="utf-8") as conf: 

1238 conf_lines = conf.readlines() 

1239 line = 0 

1240 

1241 for i in range(0, len(conf_lines)): 

1242 if conf_lines[i].find("FUNCTION") >= 0: 

1243 line = i + 1 

1244 break 

1245 

1246 export_item_list = [] 

1247 j = 1 

1248 if line != 0: 

1249 while 1: 

1250 while 1: 

1251 for i in range(0, len(conf_lines[line])): 

1252 if conf_lines[line][i] == "<": 

1253 element_start = i + 1 

1254 if conf_lines[line][i] == ">": 

1255 element_end = i 

1256 elem = conf_lines[line][element_start:element_end] 

1257 check = conf_lines[line + j][:-1] 

1258 

1259 if check.find(elem) >= 0: 

1260 line = line + j + 1 

1261 j = 1 

1262 break 

1263 j += 1 

1264 items = check.split(" ") 

1265 pos = -1 

1266 for n in range(0, len(items)): 

1267 if items[n].startswith("!"): 

1268 msg = f"FOUND at {n}" 

1269 print(msg) 

1270 self.logger.info(msg) 

1271 pos = n 

1272 break 

1273 pos = len(items) 

1274 

1275 for n in range(0, pos): 

1276 if items[n] == "PDMX" or items[n] == "pdmx": 

1277 break 

1278 if items[n] == "PDXMX" or items[n] == "pdxmx": 

1279 break 

1280 if items[n] == "PDYMX" or items[n] == "pdymx": 

1281 break 

1282 if items[n] == "PDZMX" or items[n] == "pdzmx": 

1283 break 

1284 if items[n] == "PDIJ" or items[n] == "pdij": 

1285 break 

1286 if items[n] == "STDDEV" or items[n] == "stddev": 

1287 break 

1288 if items[n] == "NCOUNT" or items[n] == "ncount": 

1289 break 

1290 if items[n] == "MISES_MX" or items[n] == "mises_mx": 

1291 break 

1292 if items[n] == "MISES_IJ" or items[n] == "mises_ij": 

1293 break 

1294 

1295 for k in range(n, pos): 

1296 postval = None 

1297 for m in range(0, n): 

1298 if items[m] == "coordinates": 

1299 items[m] = "geometry" 

1300 if postval is None: 

1301 postval = items[m] 

1302 else: 

1303 postval = postval + "_" + items[m] 

1304 postval = postval.strip("_") 

1305 

1306 # hotfix 

1307 # sometimes the engine writes 'Geometry' instead of 'geometry' 

1308 postval = postval.lower() 

1309 

1310 items[k] = items[k].strip() 

1311 

1312 if items[k] != "" and items[k] != "\r": 

1313 if postval.lower() == "sigma": 

1314 export_item_list.append( 

1315 elem + "_" + postval + "_" + "001_" + items[k].lower() 

1316 ) 

1317 export_item_list.append( 

1318 elem + "_" + postval + "_" + "002_" + items[k].lower() 

1319 ) 

1320 export_item_list.append( 

1321 elem + "_" + postval + "_" + "003_" + items[k].lower() 

1322 ) 

1323 else: 

1324 export_item_list.append( 

1325 elem + "_" + postval + "_" + items[k].lower() 

1326 ) 

1327 if export_item_list[-1].endswith("\r"): 

1328 export_item_list[-1] = export_item_list[-1][:-1] 

1329 

1330 if conf_lines[line].find("FUNCTION") >= 0: 

1331 break 

1332 else: 

1333 export_item_list = ["NODE_geometry_pdmx", "NODE_geometry_pdij"] 

1334 

1335 return export_item_list 

1336 

1337 def check_if_logfiles_show_success(self, pattern: str) -> List[str]: 

1338 """Check if a logfiles with given pattern show success 

1339 

1340 Parameters 

1341 ---------- 

1342 pattern : `str` 

1343 file pattern used to search for logfiles 

1344 

1345 Returns 

1346 ------- 

1347 messages : `list` 

1348 list with messages of failed log checks 

1349 """ 

1350 

1351 _msg_logfile_nok = str_error("Logfile '{0}' reports no success.") 

1352 messages = [] 

1353 

1354 logfiles = glob.glob(os.path.join(self.logfile_dir, pattern)) 

1355 for filepath in logfiles: 

1356 if not self.is_logfile_successful(filepath): 

1357 messages.append(_msg_logfile_nok.format(filepath)) 

1358 

1359 return messages