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
« 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
15import psutil
17from ..logging import str_error, str_info, str_running, str_success, str_warn
19# pylint: disable = too-many-lines
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]
39def get_application_header():
40 """Prints the header of the command line tool"""
42 return """
44 ==== D I F F C R A S H ====
46 an open-lasso-python utility script
47 """
50def str2bool(value) -> bool:
51 """Converts some value from the cmd line to a boolean
53 Parameters
54 ----------
55 value: `str` or `bool`
57 Returns
58 -------
59 bool_value: `bool`
60 value as boolean
61 """
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.")
72def parse_diffcrash_args():
73 """Parse the arguments from the command line
75 Returns
76 -------
77 args : `argparse.Namespace`
78 parsed arguments
79 """
81 # print title
82 print(get_application_header())
84 parser = argparse.ArgumentParser(
85 description="Python utility script for Diffcrash written by OPEN-LASSO."
86 )
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 )
160 if len(sys.argv) < 2:
161 parser.print_help()
162 sys.exit(0)
164 return parser.parse_args(sys.argv[1:])
167def run_subprocess(args):
168 """Run a subprocess with the specified arguments
170 Parameters:
171 -----------
172 args : `list` of `str`
174 Returns
175 -------
176 rc : `int`
177 process return code
179 Notes
180 -----
181 Suppresses stderr.
182 """
183 return subprocess.Popen(args, stderr=subprocess.DEVNULL).wait()
186class DiffcrashRun:
187 """Class for handling the settings of a diffcrash run"""
189 # pylint: disable = too-many-instance-attributes
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
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 """
233 # settings
234 self._msg_option = "{:16s}: {}"
235 self._log_formatter = logging.Formatter("%(levelname)s:%(asctime)s %(message)s")
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")
244 # logger
245 self.logger = self._setup_logger()
247 # make some space in the log
248 self.logger.info(get_application_header())
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")
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 )
264 # project dir
265 self.project_dir = self._parse_project_dir(project_dir)
267 # crashcode
268 self.crash_code = self._parse_crash_code(crash_code)
270 # reference run
271 self.reference_run = self._parse_reference_run(reference_run)
273 # mapping
274 self.use_id_mapping = self._parse_use_id_mapping(use_id_mapping)
276 # exlude runs
277 self.exclude_runs = exclude_runs
279 # simulation runs
280 self.simulation_runs = self._parse_simulation_runs(
281 simulation_runs, self.reference_run, self.exclude_runs
282 )
284 # config file
285 self.config_file = self._parse_config_file(config_file)
287 # parameter file
288 self.parameter_file = self._parse_parameter_file(parameter_file)
290 # n processes
291 self.n_processes = self._parse_n_processes(n_processes)
293 def _setup_logger(self) -> logging.Logger:
295 # better safe than sorry
296 os.makedirs(self.logfile_dir, exist_ok=True)
298 # create console log channel
299 # streamHandler = logging.StreamHandler(sys.stdout)
300 # streamHandler.setLevel(logging.INFO)
301 # streamHandler.setFormatter(self._log_formatter)
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)
308 # create logger
309 logger = logging.getLogger("DiffcrashRun")
310 logger.setLevel(logging.INFO)
311 # logger.addHandler(streamHandler)
312 logger.addHandler(file_handler)
314 return logger
316 def _parse_diffcrash_home(self, diffcrash_home) -> str:
318 diffcrash_home_ok = len(diffcrash_home) != 0
320 msg = self._msg_option.format("diffcrash-home", diffcrash_home)
321 print(str_info(msg))
322 self.logger.info(msg)
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))
332 return diffcrash_home
334 def _parse_crash_code(self, crash_code) -> str:
336 # these guys are allowed
337 valid_crash_codes = ["dyna", "radioss", "pam"]
339 # do the thing
340 crash_code_ok = crash_code in valid_crash_codes
342 print(str_info(self._msg_option.format("crash-code", crash_code)))
343 self.logger.info(self._msg_option.format("crash-code", crash_code))
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))
353 return crash_code
355 def _parse_reference_run(self, reference_run) -> str:
357 reference_run_ok = os.path.isfile(reference_run)
359 msg = self._msg_option.format("reference-run", reference_run)
360 print(str_info(msg))
361 self.logger.info(msg)
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))
368 return reference_run
370 def _parse_use_id_mapping(self, use_id_mapping) -> bool:
372 msg = self._msg_option.format("use-id-mapping", use_id_mapping)
373 print(str_info(msg))
374 self.logger.info(msg)
376 return use_id_mapping
378 def _parse_project_dir(self, project_dir):
379 project_dir = os.path.abspath(project_dir)
381 msg = self._msg_option.format("project-dir", project_dir)
382 print(str_info(msg))
383 self.logger.info(msg)
385 return project_dir
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 ):
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)]
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)]
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)
412 # remove the reference run
413 if reference_run in simulation_runs:
414 simulation_runs.remove(reference_run)
416 # sort it because we can!
417 def atoi(text):
418 return int(text) if text.isdigit() else text
420 def natural_keys(text):
421 return [atoi(c) for c in re.split(r"(\d+)", text)]
423 simulation_runs = sorted(simulation_runs, key=natural_keys)
425 # check
426 simulation_runs_ok = len(simulation_runs) != 0
428 msg = self._msg_option.format("# simul.-files", len(simulation_runs))
429 print(str_info(msg))
430 self.logger.info(msg)
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)
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))
446 return simulation_runs
448 def _parse_config_file(self, config_file) -> Union[str, None]:
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}'"
455 # missing config file
456 else:
458 config_file = None
459 _msg_config_file = (
460 "Config file missing. "
461 "Consider specifying the path with the option '--config-file'."
462 )
464 msg = self._msg_option.format("config-file", config_file)
465 print(str_info(msg))
466 self.logger.info(msg)
468 if _msg_config_file:
469 print(str_warn(_msg_config_file))
470 self.logger.warning(_msg_config_file)
472 return config_file
474 def _parse_parameter_file(self, parameter_file) -> Union[None, str]:
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 )
488 msg = self._msg_option.format("parameter-file", parameter_file)
489 print(str_info(msg))
490 self.logger.info(msg)
492 if _msg_parameter_file:
493 print(str_warn(_msg_parameter_file))
494 self.logger.warning(_msg_parameter_file)
496 return parameter_file
498 def _parse_n_processes(self, n_processes) -> int:
500 print(str_info(self._msg_option.format("n-processes", n_processes)))
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))
507 return n_processes
509 def create_project_dirs(self):
510 """Creates all project relevant directores
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)
521 def run_setup(self, pool: futures.ThreadPoolExecutor):
522 """Run diffcrash setup
524 Parameters
525 ----------
526 pool : `concurrent.futures.ThreadPoolExecutor`
527 multiprocessing pool
528 """
530 # SETUP
531 msg = "Running Setup ... "
532 print(str_running(msg) + "\r", end="", flush="")
533 self.logger.info(msg)
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()
590 # submit task
591 return_code_future = pool.submit(run_subprocess, args)
592 return_code = return_code_future.result()
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)
600 err_msg = "Process somehow failed."
601 self.logger.error(err_msg)
602 raise RuntimeError(str_error(err_msg))
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)
611 # print failed logs
612 for msg in messages:
613 print(str_error(msg))
614 self.logger.error(msg)
616 err_msg = "Setup failed."
617 self.logger.error(err_msg)
618 raise RuntimeError(str_error(err_msg))
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)
625 def run_import(self, pool: futures.ThreadPoolExecutor):
626 """Run diffcrash import of runs
628 Parameters
629 ----------
630 pool : `concurrent.futures.ThreadPoolExecutor`
631 multiprocessing pool
632 """
634 # pylint: disable = too-many-locals, too-many-branches, too-many-statements
636 # list of arguments to run in the command line
637 import_arguments = []
639 # id 1 is the reference run
640 # id 2 and higher are the imported runs
641 counter_offset = 2
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)):
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 ]
683 # append args to list
684 import_arguments.append(args)
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]
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):
699 # check again
700 n_new_imports_finished = sum(
701 return_code_future.done() for return_code_future in return_code_futures
702 )
704 # print
705 percentage = n_new_imports_finished / len(return_code_futures) * 100
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)
715 n_imports_finished = n_new_imports_finished
717 # wait a little bit
718 time.sleep(0.25)
720 return_codes = [return_code_future.result() for return_code_future in return_code_futures]
722 # print failure
723 if any(return_code != 0 for return_code in return_codes):
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
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)
739 err_msg = f"Import of {n_failed_runs} runs failed."
740 self.logger.error(err_msg)
741 raise RuntimeError(str_error(err_msg))
743 # check log files
744 messages = self.check_if_logfiles_show_success("DFC_Import_*.log")
745 if messages:
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)
752 # print failed logs
753 for msg in messages:
754 self.logger.error(msg)
755 print(str_error(msg))
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))
763 # print success
764 print(str_success(f"Running Imports ... done in {time.time() - start_time:.2f}s "))
766 def run_math(self, pool: futures.ThreadPoolExecutor):
767 """Run diffcrash math
769 Parameters
770 ----------
771 pool : `concurrent.futures.ThreadPoolExecutor`
772 multiprocessing pool
773 """
775 msg = "Running Math ... \r"
776 print(str_running(msg), end="", flush=True)
777 self.logger.info(msg)
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()
786 # check return code
787 if return_code != 0:
789 msg = f"Running Math ... done in {time.time() - start_time:.2f}s "
790 print(str_error(msg))
791 self.logger.error(msg)
793 err_msg = f"Caught a nonzero return code '{return_code}'"
794 self.logger.error(err_msg)
795 raise RuntimeError(str_error(err_msg))
797 # check logs
798 messages = self.check_if_logfiles_show_success("DFC_MATH*.log")
799 if messages:
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)
806 # print failed logs
807 for msg in messages:
808 print(str_error(msg))
809 self.logger.error(msg)
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))
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)
823 def run_export(self, pool: futures.ThreadPoolExecutor):
824 """Run diffcrash export
826 Parameters
827 ----------
828 pool : `concurrent.futures.ThreadPoolExecutor`
829 multiprocessing pool
830 """
832 msg = "Running Export ... "
833 print(str_running(msg) + "\r", end="", flush=True)
834 self.logger.info(msg)
836 if self.config_file is None:
837 export_item_list = []
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]))
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]))
849 else:
850 export_item_list = self.read_config_file(self.config_file)
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)
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 ]
872 return_codes = [result_future.result() for result_future in return_code_futures]
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)
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))
889 msg = "At least one export process failed."
890 self.logger.error(msg)
891 raise RuntimeError(str_error(msg))
893 # check logs
894 messages = self.check_if_logfiles_show_success("DFC_Export_*")
895 if messages:
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)
902 # print logs
903 for msg in messages:
904 print(str_error(msg))
905 self.logger.error(msg)
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))
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)
919 def run_matrix(self, pool: futures.ThreadPoolExecutor):
920 """Run diffcrash matrix
922 Parameters
923 ----------
924 pool : `concurrent.futures.ThreadPoolExecutor`
925 multiprocessing pool
926 """
928 msg = "Running Matrix ... "
929 print(str_running(msg) + "\r", end="", flush=True)
930 self.logger.info(msg)
932 start_time = time.time()
934 # create the input file for the process
935 matrix_inputfile = self._create_matrix_input_file(self.project_dir)
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 )
947 # please hold the line ...
948 return_code = return_code_future.result()
950 # check return code
951 if return_code != 0:
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)
958 msg = "The DFC_Matrix process failed somehow."
959 self.logger.error(msg)
960 raise RuntimeError(str_error(msg))
962 # check log file
963 messages = self.check_if_logfiles_show_success("DFC_Matrix_*")
964 if messages:
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)
971 # print why
972 for msg in messages:
973 print(str_error(msg))
974 self.logger.error(msg)
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))
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)
985 def run_eigen(self, pool: futures.ThreadPoolExecutor):
986 """Run diffcrash eigen
988 Parameters
989 ----------
990 pool : `concurrent.futures.ThreadPoolExecutor`
991 multiprocessing pool
992 """
994 msg = "Running Eigen ... "
995 print(str_running(msg) + "\r", end="", flush=True)
996 self.logger.info(msg)
998 # create input file for process
999 eigen_inputfile = self._create_eigen_input_file(self.project_dir)
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 )
1012 # please hold the line ...
1013 return_code = return_code_future.result()
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)
1021 msg = "The process failed somehow."
1022 self.logger.error(msg)
1023 raise RuntimeError(str_error(msg))
1025 # check log file
1026 messages = self.check_if_logfiles_show_success("DFC_Matrix_*")
1027 if messages:
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)
1034 # print why
1035 for msg in messages:
1036 print(str_error(msg))
1037 self.logger.error(msg)
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))
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)
1048 def run_merge(self, pool: futures.ThreadPoolExecutor):
1049 """Run diffcrash merge
1051 Parameters
1052 ----------
1053 pool : `concurrent.futures.ThreadPoolExecutor`
1054 multiprocessing pool
1055 """
1057 msg = "Running Merge ... "
1058 print(str_running(msg) + "\r", end="", flush=True)
1059 self.logger.info(msg)
1061 # create ionput file for merge
1062 merge_inputfile = self._create_merge_input_file(self.project_dir)
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)
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()
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)
1087 msg = "The process failed somehow."
1088 self.logger.error(msg)
1089 raise RuntimeError(str_error(msg))
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)
1098 for msg in messages:
1099 print(str_error(msg))
1100 self.logger.info(msg)
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))
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)
1111 def is_logfile_successful(self, logfile: str) -> bool:
1112 """Checks if a logfile indicates a success
1114 Parameters
1115 ----------
1116 logfile : `str`
1117 filepath to the logfile
1119 Returns
1120 -------
1121 success : `bool`
1122 """
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
1130 def _create_merge_input_file(self, directory: str) -> str:
1131 """Create an input file for the merge executable
1133 Notes
1134 -----
1135 From the official diffcrash docs.
1136 """
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 )
1157 return filepath
1159 def _create_eigen_input_file(self, directory: str) -> str:
1160 """Create an input file for the eigen executable
1162 Notes
1163 -----
1164 From the official diffcrash docs.
1165 """
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")
1180 return filepath
1182 def _create_matrix_input_file(self, directory: str) -> str:
1183 """Create an input file for the matrix executable
1185 Notes
1186 -----
1187 From the official diffcrash docs.
1188 """
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")
1198 return filepath
1200 def clear_project_dir(self):
1201 """Clears the entire project dir"""
1203 # disable logging
1204 for handler in self.logger.handlers:
1205 handler.close()
1206 self.logger.removeHandler(handler)
1208 # delete folder
1209 if os.path.exists(self.project_dir):
1210 shutil.rmtree(self.project_dir)
1212 # reinit logger
1213 self.logger = self._setup_logger()
1215 def read_config_file(self, config_file: str) -> List[str]:
1216 """Read a diffcrash config file
1218 Parameters
1219 ----------
1220 config_file : `str`
1221 path to the config file
1223 Notes
1224 -----
1225 From the official diffcrash docs ... seriously.
1226 """
1228 # Just to make it clear, this is not code from OPEN-LASSO
1229 # ...
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
1237 with open(config_file, "r", encoding="utf-8") as conf:
1238 conf_lines = conf.readlines()
1239 line = 0
1241 for i in range(0, len(conf_lines)):
1242 if conf_lines[i].find("FUNCTION") >= 0:
1243 line = i + 1
1244 break
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]
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)
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
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("_")
1306 # hotfix
1307 # sometimes the engine writes 'Geometry' instead of 'geometry'
1308 postval = postval.lower()
1310 items[k] = items[k].strip()
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]
1330 if conf_lines[line].find("FUNCTION") >= 0:
1331 break
1332 else:
1333 export_item_list = ["NODE_geometry_pdmx", "NODE_geometry_pdij"]
1335 return export_item_list
1337 def check_if_logfiles_show_success(self, pattern: str) -> List[str]:
1338 """Check if a logfiles with given pattern show success
1340 Parameters
1341 ----------
1342 pattern : `str`
1343 file pattern used to search for logfiles
1345 Returns
1346 -------
1347 messages : `list`
1348 list with messages of failed log checks
1349 """
1351 _msg_logfile_nok = str_error("Logfile '{0}' reports no success.")
1352 messages = []
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))
1359 return messages