Source code for toughio._run

import glob
import os
import pathlib
import platform
import secrets
import shutil
import signal
import subprocess
import tempfile

import psutil

_check_exec = True  # Bool to be monkeypatched in tests


[docs] def run( exec, input_filename, other_filenames=None, command=None, workers=None, docker=None, wsl=False, working_dir=None, use_temp=False, ignore_patterns=None, silent=False, petsc_args=None, docker_args=None, container_name=None, **kwargs, ): """ Run TOUGH executable. Parameters ---------- exec : str or pathlike Path to TOUGH executable. input_filename : str or pathlike TOUGH input file name. other_filenames : list, dict or None, optional, default None Other simulation files to copy to working directory (e.g., MESH, INCON, GENER) if not already present. If ``other_filenames`` is a dict, must be in the form ``{old: new}``, where ``old`` is the current name of the file to copy, and ``new`` is the name of the file copied. command : callable or None, optional, default None Command to execute TOUGH. Must be in the form ``f(exec, inp, [out])``, where ``exec`` is the path to TOUGH executable, ``inp`` is the input file name, and ``out`` is the output file name (optional). workers : int or None, optional, default None Number of MPI workers to invoke. docker : str, optional, default None Name of Docker image. wsl : bool, optional, default False Only for Windows. If `True`, run the final command as a Bash command. working_dir : str, pathlike or None, optional, default None Working directory. Input and output files will be generated in this directory. use_temp : bool, optional, default False If `True`, run simulation in a temporary directory, and copy simulation files to `working_dir` at the end of the simulation. This option may be required when running TOUGH through a Docker. ignore_patterns : list or None, optional, default None If provided, output files that match the glob-style patterns will be discarded. silent : bool, optional, default False If `True`, nothing will be printed to standard output. petsc_args : list or None, optional, default None List of arguments passed to PETSc solver (written to `.petscrc`). docker_args : list or None, optional, default None List of arguments passed to `docker run` command. container_name : str or None, optional, default None Name of Docker container. Other Parameters ---------------- block : str {'all', 'gener', 'mesh', 'incon'} or None, optional, default None Only if ``file_format = "tough"``. Blocks to be written: - 'all': write all blocks, - 'gener': only write block GENER, - 'mesh': only write blocks ELEME, COORD and CONNE, - 'incon': only write block INCON, - None: write all blocks except blocks defined in `ignore_blocks`. ignore_blocks : list of str or None, optional, default None Only if ``file_format = "tough"`` and `block` is None. Blocks to ignore. space_between_blocks : bool, optional, default False Only if ``file_format = "tough"``. Add an empty record between blocks. space_between_blocks : bool, optional, default True Only if ``file_format = "tough"``. Add a white space between floating point values. eos : str or None, optional, default None Only if ``file_format = "tough"``. Equation of State. If `eos` is defined in `parameters`, this option will be ignored. mopr_10 : int, optional, default 0 Only if ``file_format = "toughreact-solute"``. MOPR(10) value in file 'flow.inp'. mopr_11 : int, optional, default 0 Only if ``file_format = "toughreact-solute"``. MOPR(11) value in file 'flow.inp'. verbose : bool, optional, default True Only if ``file_format`` in {"toughreact-solute", "toughreact-chemical"}. If `True`, add comments to describe content of file. Returns ------- :class:`subprocess.CompletedProcess` Subprocess completion status. """ from . import write_input other_filenames = ( {k: k for k in other_filenames} if isinstance(other_filenames, (list, tuple)) else other_filenames if other_filenames else {} ) if command is None: command = lambda exec, inp, out: f"{exec} {inp} {out}" ignore_patterns = list(ignore_patterns) if ignore_patterns else [] ignore_patterns += [".OUTPUT*", "TABLE", "MESHA", "MESHB"] # Executable exec = str(exec) exec = f"{os.path.expanduser('~')}/{exec[2:]}" if exec.startswith("~/") else exec if _check_exec: if not docker: if shutil.which(exec) is None: raise RuntimeError(f"executable '{exec}' not found.") else: # Check if Docker is in the PATH if shutil.which("docker") is None: raise RuntimeError("Docker executable not found.") # Check if Docker daemon is running status = subprocess.run( ["docker", "version"], stdout=subprocess.PIPE, universal_newlines=True, ) if "Server" not in status.stdout: raise RuntimeError( "cannot connect to the Docker daemon. Is the Docker daemon running?" ) # Check if Docker image exists status = subprocess.run( ["docker", "images", "-q", docker], stdout=subprocess.PIPE, universal_newlines=True, ) if not status.stdout: raise RuntimeError(f"image '{docker}' not found.") # Check if the executable exists inside the image status = subprocess.run( ["docker", "run", "--rm", docker, "which", exec], stdout=subprocess.PIPE, universal_newlines=True, ) if not status.stdout: raise RuntimeError( f"executable '{exec}' not found in Docker image '{docker}'." ) # Working directory working_dir = os.getcwd() if working_dir is None else working_dir working_dir = pathlib.Path(working_dir) working_dir.mkdir(parents=True, exist_ok=True) # Simulation directory if use_temp: temp_dir = tempfile.mkdtemp() simulation_dir = pathlib.Path(temp_dir) with open(working_dir / "tempdir.txt", "w") as f: f.write(temp_dir) else: simulation_dir = working_dir # Check if input file is in simulation directory, otherwise copy if not isinstance(input_filename, dict): input_path = pathlib.Path(input_filename) input_filename = simulation_dir / input_path.name if input_path.parent.resolve() != simulation_dir.resolve(): shutil.copy(input_path, input_filename) else: write_input(simulation_dir / "INFILE", input_filename, **kwargs) input_filename = simulation_dir / "INFILE" # Copy other simulation files to working directory for k, v in other_filenames.items(): filename = pathlib.Path(k) new_filename = pathlib.Path(v) if ( filename.parent.resolve() != simulation_dir.resolve() or filename.name != new_filename.name ): shutil.copy(filename, simulation_dir / new_filename.name) # PETSc arguments petsc_args = petsc_args if petsc_args else [] if petsc_args: with open(simulation_dir / ".petscrc", "w") as f: for arg in petsc_args: if arg.startswith("-"): f.write(f"{arg} ") else: f.write(f"{arg}\n") # Output filename output_filename = f"{input_filename.stem}.out" # TOUGH command cmd = command(exec, str(input_filename.name), str(output_filename)) # Use MPI if workers is not None and workers > 1: cmd = f"mpiexec -n {workers} {cmd}" # Use Docker is_windows = platform.system().startswith("Win") if docker: container_name = ( container_name if container_name else f"toughio_{secrets.token_hex(4)}" ) if is_windows and os.getenv("ComSpec").endswith("cmd.exe"): cwd = '"%cd%"' else: cwd = "${PWD}" docker_args = docker_args if docker_args else [] docker_args += [ "--name", container_name, "--rm", # Sometime raises a duplicate mount point error, use old-school volume instead (but shell must be True in this case) # "--mount", # f"type=bind,source={simulation_dir},target=/shared", "--volume", f"{cwd}:/shared", "--workdir", "/shared", ] cmd = f"docker run {' '.join(str(arg) for arg in docker_args)} {docker} {cmd}" # Use WSL if wsl and is_windows: cmd = f"bash -c '{cmd}'" # Run simulation try: # See <https://www.koldfront.dk/making_subprocesspopen_in_python_3_play_nice_with_elaborate_output_1594> p = subprocess.Popen( cmd, shell=True, # shell must be True as the command may contain quotes cwd=str(simulation_dir), stderr=subprocess.STDOUT, stdout=subprocess.PIPE, universal_newlines=False, ) stdout = [] cr = False for line in open(os.dup(p.stdout.fileno()), newline=""): # Handle carriage return # newline in open is converting \r\n as \r moving \r at the end of the previous string # This is only an issue for Spyder line = f"\r{line}" if cr else line cr = line.endswith("\r") line = line[:-2] if cr else line if not silent: print(line, end="", flush=True) stdout.append(line) except (KeyboardInterrupt, Exception) as e: # Stop Docker container if docker: status = subprocess.run( ["docker", "stop", container_name], stdout=subprocess.PIPE, universal_newlines=True, ) # Handle children process termination # See <https://stackoverflow.com/a/25134985/9729313> try: proc = psutil.Process(p.pid) for child_process in proc.children(): child_process.send_signal(signal.SIGTERM) proc.send_signal(signal.SIGTERM) except psutil.NoSuchProcess: pass raise e p.wait() status = subprocess.CompletedProcess( args=p.args, returncode=p.returncode, stdout="".join(stdout), ) # Copy files from temporary directory and delete it if use_temp: shutil.copytree( simulation_dir, working_dir, ignore=shutil.ignore_patterns(*ignore_patterns), dirs_exist_ok=True, # Doesn't work with Python 3.7 ) shutil.rmtree(simulation_dir, ignore_errors=True) os.remove(working_dir / "tempdir.txt") # Clean up working directory patterns = [ pathlib.Path(filename) for pattern in ignore_patterns for filename in glob.glob(f"{str(simulation_dir)}/{pattern}") ] for pattern in patterns: os.remove(pattern) return status