From 62cf01e97874729c6fa66db2dfaa6248920d7d96 Mon Sep 17 00:00:00 2001 From: German <28149841+germa89@users.noreply.github.com> Date: Fri, 10 Jan 2025 12:42:04 +0100 Subject: [PATCH 01/42] feat: initial refactor. Still missing unit testing on new functions. --- src/ansys/mapdl/core/__init__.py | 4 +- src/ansys/mapdl/core/launcher/__init__.py | 66 + src/ansys/mapdl/core/launcher/console.py | 105 + src/ansys/mapdl/core/launcher/grpc.py | 215 ++ src/ansys/mapdl/core/launcher/hpc.py | 561 +++++ .../mapdl/core/{ => launcher}/jupyter.py | 0 src/ansys/mapdl/core/launcher/launcher.py | 726 +++++++ src/ansys/mapdl/core/launcher/local.py | 114 + src/ansys/mapdl/core/launcher/pim.py | 83 + src/ansys/mapdl/core/launcher/remote.py | 101 + .../core/{launcher.py => launcher/tools.py} | 1868 +++-------------- src/ansys/mapdl/core/licensing.py | 1 - src/ansys/mapdl/core/mapdl_grpc.py | 7 +- src/ansys/mapdl/core/misc.py | 2 +- src/ansys/mapdl/core/pool.py | 5 +- tests/common.py | 4 +- tests/conftest.py | 10 +- tests/test_launcher.py | 108 +- tests/test_launcher/test_console.py | 50 + tests/test_launcher/test_grpc.py | 21 + tests/test_launcher/test_hpc.py | 21 + tests/test_launcher/test_jupyter.py | 21 + tests/test_launcher/test_pim.py | 21 + tests/test_launcher/test_remote.py | 21 + 24 files changed, 2499 insertions(+), 1636 deletions(-) create mode 100644 src/ansys/mapdl/core/launcher/__init__.py create mode 100644 src/ansys/mapdl/core/launcher/console.py create mode 100644 src/ansys/mapdl/core/launcher/grpc.py create mode 100644 src/ansys/mapdl/core/launcher/hpc.py rename src/ansys/mapdl/core/{ => launcher}/jupyter.py (100%) create mode 100644 src/ansys/mapdl/core/launcher/launcher.py create mode 100644 src/ansys/mapdl/core/launcher/local.py create mode 100644 src/ansys/mapdl/core/launcher/pim.py create mode 100644 src/ansys/mapdl/core/launcher/remote.py rename src/ansys/mapdl/core/{launcher.py => launcher/tools.py} (52%) create mode 100644 tests/test_launcher/test_console.py create mode 100644 tests/test_launcher/test_grpc.py create mode 100644 tests/test_launcher/test_hpc.py create mode 100644 tests/test_launcher/test_jupyter.py create mode 100644 tests/test_launcher/test_pim.py create mode 100644 tests/test_launcher/test_remote.py diff --git a/src/ansys/mapdl/core/__init__.py b/src/ansys/mapdl/core/__init__.py index 9d56358ad0..b6d47537ee 100644 --- a/src/ansys/mapdl/core/__init__.py +++ b/src/ansys/mapdl/core/__init__.py @@ -110,7 +110,9 @@ # override default launcher when on pyansys.com if "ANSJUPHUB_VER" in os.environ: # pragma: no cover - from ansys.mapdl.core.jupyter import launch_mapdl_on_cluster as launch_mapdl + from ansys.mapdl.core.launcher.jupyter import ( + launch_mapdl_on_cluster as launch_mapdl, + ) else: from ansys.mapdl.core.launcher import launch_mapdl diff --git a/src/ansys/mapdl/core/launcher/__init__.py b/src/ansys/mapdl/core/launcher/__init__.py new file mode 100644 index 0000000000..8ef00b7f19 --- /dev/null +++ b/src/ansys/mapdl/core/launcher/__init__.py @@ -0,0 +1,66 @@ +# Copyright (C) 2016 - 2025 ANSYS, Inc. and/or its affiliates. +# SPDX-License-Identifier: MIT +# +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in all +# copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +# SOFTWARE. + +import os +import warnings + +from ansys.mapdl.core import _HAS_ATP, LOG + +LOCALHOST = "127.0.0.1" +MAPDL_DEFAULT_PORT = 50052 + +ON_WSL = os.name == "posix" and ( + os.environ.get("WSL_DISTRO_NAME") or os.environ.get("WSL_INTEROP") +) + +if ON_WSL: + LOG.info("On WSL: Running on WSL detected.") + LOG.debug("On WSL: Allowing 'start_instance' and 'ip' arguments together.") + + +from ansys.mapdl.core.launcher.console import launch_mapdl_console +from ansys.mapdl.core.launcher.launcher import launch_mapdl +from ansys.mapdl.core.launcher.remote import connect_to_mapdl +from ansys.mapdl.core.launcher.tools import ( + close_all_local_instances, + get_default_ansys, + get_default_ansys_path, + get_default_ansys_version, +) + +if _HAS_ATP: + from functools import wraps + + from ansys.tools.path import find_mapdl, get_mapdl_path + from ansys.tools.path import version_from_path as _version_from_path + + @wraps(_version_from_path) + def version_from_path(*args, **kwargs): + """Wrap ansys.tool.path.version_from_path to raise a warning if the + executable couldn't be found""" + if kwargs.pop("launch_on_hpc", False): + try: + return _version_from_path(*args, **kwargs) + except RuntimeError: + warnings.warn("PyMAPDL could not find the ANSYS executable. ") + else: + return _version_from_path(*args, **kwargs) diff --git a/src/ansys/mapdl/core/launcher/console.py b/src/ansys/mapdl/core/launcher/console.py new file mode 100644 index 0000000000..1f9456dd96 --- /dev/null +++ b/src/ansys/mapdl/core/launcher/console.py @@ -0,0 +1,105 @@ +# Copyright (C) 2016 - 2025 ANSYS, Inc. and/or its affiliates. +# SPDX-License-Identifier: MIT +# +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in all +# copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +# SOFTWARE. + +from typing import Optional, Union + +from ansys.mapdl.core.launcher.tools import generate_start_parameters +from ansys.mapdl.core.licensing import LicenseChecker +from ansys.mapdl.core.mapdl_console import MapdlConsole + + +def check_console_start_parameters(start_parm): + valid_args = [ + "exec_file", + "run_location", + "jobname", + "nproc", + "additional_switches", + "start_timeout", + ] + for each in list(start_parm.keys()): + if each not in valid_args: + start_parm.pop(each) + + return start_parm + + +def launch_mapdl_console( + exec_file: Optional[str] = None, + run_location: Optional[str] = None, + jobname: str = "file", + *, + nproc: Optional[int] = None, + ram: Optional[Union[int, str]] = None, + override: bool = False, + loglevel: str = "ERROR", + additional_switches: str = "", + start_timeout: Optional[int] = None, + log_apdl: Optional[Union[bool, str]] = None, +): + ######################################## + # Processing arguments + # -------------------- + # + # processing arguments + args = processing_local_arguments(locals()) + + # Check for a valid connection mode + if args.get("mode", "console") != "console": + raise ValueError("Invalid 'mode'.") + + start_parm = generate_start_parameters(args) + + # Early exit for debugging. + if args["_debug_no_launch"]: + # Early exit, just for testing + return args # type: ignore + + ######################################## + # Local launching + # --------------- + # + # Check the license server + if args["license_server_check"]: + LOG.debug("Checking license server.") + lic_check = LicenseChecker(timeout=args["start_timeout"]) + lic_check.start() + + LOG.debug("Starting MAPDL") + ######################################## + # Launch MAPDL on console mode + # ---------------------------- + # + start_parm = check_console_start_parameters(start_parm) + mapdl = MapdlConsole( + loglevel=args["loglevel"], + log_apdl=args["log_apdl"], + use_vtk=args["use_vtk"], + **start_parm, + ) + + # Stop license checker + if args["license_server_check"]: + LOG.debug("Stopping check on license server.") + lic_check.stop() + + return mapdl diff --git a/src/ansys/mapdl/core/launcher/grpc.py b/src/ansys/mapdl/core/launcher/grpc.py new file mode 100644 index 0000000000..e3c77d8b69 --- /dev/null +++ b/src/ansys/mapdl/core/launcher/grpc.py @@ -0,0 +1,215 @@ +# Copyright (C) 2016 - 2025 ANSYS, Inc. and/or its affiliates. +# SPDX-License-Identifier: MIT +# +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in all +# copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +# SOFTWARE. + +import os + +# Subprocess is needed to start the backend. But +# the input is controlled by the library. Excluding bandit check. +import subprocess # nosec B404 +from typing import Dict, Optional + +from ansys.mapdl.core import LOG +from ansys.mapdl.core.launcher.local import processing_local_arguments +from ansys.mapdl.core.launcher.tools import ( + generate_start_parameters, + get_port, + submitter, +) +from ansys.mapdl.core.licensing import LicenseChecker +from ansys.mapdl.core.mapdl_grpc import MapdlGrpc + + +def launch_mapdl_grpc(): + args = processing_local_arguments(locals()) + if args.get("mode", "grpc") != "grpc": + raise ValueError("Invalid 'mode'.") + args["port"] = get_port(args["port"], args["start_instance"]) + + start_parm = generate_start_parameters(args) + + # Early exit for debugging. + if args["_debug_no_launch"]: + # Early exit, just for testing + return args # type: ignore + + # Check the license server + if args["license_server_check"]: + LOG.debug("Checking license server.") + lic_check = LicenseChecker(timeout=args["start_timeout"]) + lic_check.start() + + ######################################## + # Launch MAPDL with gRPC + # ---------------------- + # + cmd = generate_mapdl_launch_command( + exec_file=args["exec_file"], + jobname=args["jobname"], + nproc=args["nproc"], + ram=args["ram"], + port=args["port"], + additional_switches=args["additional_switches"], + ) + + try: + # + process = launch_grpc( + cmd=cmd, + run_location=args["run_location"], + env_vars=env_vars, + launch_on_hpc=args.get("launch_on_hpc"), + mapdl_output=args.get("mapdl_output"), + ) + + # Local mapdl launch check + check_mapdl_launch( + process, args["run_location"], args["start_timeout"], cmd + ) + + except Exception as exception: + LOG.error("An error occurred when launching MAPDL.") + + jobid: int = start_parm.get("jobid", "Not found") + + if args["license_server_check"]: + LOG.debug("Checking license server.") + lic_check.check() + + raise exception + + if args["just_launch"]: + out = [args["ip"], args["port"]] + if hasattr(process, "pid"): + out += [process.pid] + return out + + ######################################## + # Connect to MAPDL using gRPC + # --------------------------- + # + try: + mapdl = MapdlGrpc( + cleanup_on_exit=args["cleanup_on_exit"], + loglevel=args["loglevel"], + set_no_abort=args["set_no_abort"], + remove_temp_dir_on_exit=args["remove_temp_dir_on_exit"], + log_apdl=args["log_apdl"], + process=process, + use_vtk=args["use_vtk"], + **start_parm, + ) + + except Exception as exception: + LOG.error("An error occurred when connecting to MAPDL.") + raise exception + + return mapdl + + +def launch_grpc( + cmd: list[str], + run_location: str = None, + env_vars: Optional[Dict[str, str]] = None, + launch_on_hpc: bool = False, + mapdl_output: Optional[str] = None, +) -> subprocess.Popen: + """Start MAPDL locally in gRPC mode. + + Parameters + ---------- + cmd : str + Command to use to launch the MAPDL instance. + + run_location : str, optional + MAPDL working directory. The default is the temporary working + directory. + + env_vars : dict, optional + Dictionary with the environment variables to inject in the process. + + launch_on_hpc : bool, optional + If running on an HPC, this needs to be :class:`True` to avoid the + temporary file creation on Windows. + + mapdl_output : str, optional + Whether redirect MAPDL console output (stdout and stderr) to a file. + + Returns + ------- + subprocess.Popen + Process object + """ + if env_vars is None: + env_vars = {} + + # disable all MAPDL pop-up errors: + env_vars.setdefault("ANS_CMD_NODIAG", "TRUE") + + cmd_string = " ".join(cmd) + if "sbatch" in cmd: + header = "Running an MAPDL instance on the Cluster:" + shell = os.name != "nt" + cmd_ = cmd_string + else: + header = "Running an MAPDL instance" + shell = False # To prevent shell injection + cmd_ = cmd + + LOG.info( + "\n============" + "\n============\n" + f"{header}\nLocation:\n{run_location}\n" + f"Command:\n{cmd_string}\n" + f"Env vars:\n{env_vars}" + "\n============" + "\n============" + ) + + if mapdl_output: + stdout = open(str(mapdl_output), "wb", 0) + stderr = subprocess.STDOUT + else: + stdout = subprocess.PIPE + stderr = subprocess.PIPE + + if os.name == "nt": + # getting tmp file name + if not launch_on_hpc: + # if we are running on an HPC cluster (case not considered), we will + # have to upload/create this file because it is needed for starting. + tmp_inp = cmd[cmd.index("-i") + 1] + with open(os.path.join(run_location, tmp_inp), "w") as f: + f.write("FINISH\r\n") + LOG.debug( + f"Writing temporary input file: {tmp_inp} with 'FINISH' command." + ) + + LOG.debug("MAPDL starting in background.") + return submitter( + cmd_, + shell=shell, # sbatch does not work without shell. + cwd=run_location, + stdin=subprocess.DEVNULL, + stdout=stdout, + stderr=stderr, + env_vars=env_vars, + ) diff --git a/src/ansys/mapdl/core/launcher/hpc.py b/src/ansys/mapdl/core/launcher/hpc.py new file mode 100644 index 0000000000..c192f868f9 --- /dev/null +++ b/src/ansys/mapdl/core/launcher/hpc.py @@ -0,0 +1,561 @@ +# Copyright (C) 2016 - 2025 ANSYS, Inc. and/or its affiliates. +# SPDX-License-Identifier: MIT +# +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in all +# copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +# SOFTWARE. + +import os +import socket +import subprocess +import time +from typing import Any, Callable, Dict, List, Optional, Union + +from ansys.mapdl.core import LOG +from ansys.mapdl.core.errors import MapdlDidNotStart +from ansys.mapdl.core.launcher.grpc import launch_grpc +from ansys.mapdl.core.launcher.tools import submitter +from ansys.mapdl.core.mapdl_grpc import MapdlGrpc + +LAUNCH_ON_HCP_ERROR_MESSAGE_IP = ( + "PyMAPDL cannot ensure a specific IP will be used when launching " + "MAPDL on a cluster. Hence the 'ip' argument is not compatible. " + "If you want to connect to an already started MAPDL instance, " + "just connect normally as you would with a remote instance. " + "For example:\n\n" + ">>> mapdl = launch_mapdl(start_instance=False, ip='123.45.67.89')\n\n" + "where '123.45.67.89' is the IP of the machine where MAPDL is running." +) + + +def is_running_on_slurm(args: Dict[str, Any]) -> bool: + running_on_hpc_env_var = os.environ.get("PYMAPDL_RUNNING_ON_HPC", "True") + + is_flag_false = running_on_hpc_env_var.lower() == "false" + + # Let's require the following env vars to exist to go into slurm mode. + args["running_on_hpc"] = bool( + args["running_on_hpc"] + and not is_flag_false # default is true + and os.environ.get("SLURM_JOB_NAME") + and os.environ.get("SLURM_JOB_ID") + ) + return args["running_on_hpc"] + + +def kill_job(jobid: int) -> subprocess.Popen: + """Kill SLURM job""" + submitter(["scancel", str(jobid)]) + + +def send_scontrol(args: str) -> subprocess.Popen: + cmd = f"scontrol {args}".split(" ") + return submitter(cmd) + + +def check_mapdl_launch_on_hpc( + process: subprocess.Popen, start_parm: Dict[str, str] +) -> int: + """Check if the job is ready on the HPC + + Check if the job has been successfully submitted, and additionally, it does + retrieve the BathcHost hostname which is the IP to connect to using the gRPC + interface. + + Parameters + ---------- + process : subprocess.Popen + Process used to submit the job. The stdout is read from there. + start_parm : Dict[str, str] + To store the job ID, the BatchHost hostname and IP into. + + Returns + ------- + int : + The jobID + + Raises + ------ + MapdlDidNotStart + The job submission failed. + """ + stdout = process.stdout.read().decode() + if "Submitted batch job" not in stdout: + stderr = process.stderr.read().decode() + raise MapdlDidNotStart( + "PyMAPDL failed to submit the sbatch job:\n" + f"stdout:\n{stdout}\nstderr:\n{stderr}" + ) + + jobid = get_jobid(stdout) + LOG.info(f"HPC job successfully submitted. JobID: {jobid}") + return jobid + + +def get_job_info( + start_parm: Dict[str, str], jobid: Optional[int] = None, timeout: int = 30 +) -> None: + """Get job info like BatchHost IP and hostname + + Get BatchHost hostname and ip and stores them in the start_parm argument + + Parameters + ---------- + start_parm : Dict[str, str] + Starting parameters for MAPDL. + jobid : int + Job ID + timeout : int + Timeout for checking if the job is ready. Default checks for + 'start_instance' key in the 'start_parm' argument, if none + is found, it passes :class:`None` to + :func:`ansys.mapdl.core.launcher.hpc.get_hostname_host_cluster`. + """ + timeout = timeout or start_parm.get("start_instance") + + jobid = jobid or start_parm["jobid"] + + batch_host, batch_ip = get_hostname_host_cluster(jobid, timeout=timeout) + + start_parm["ip"] = batch_ip + start_parm["hostname"] = batch_host + start_parm["jobid"] = jobid + + +def get_hostname_host_cluster(job_id: int, timeout: int = 30) -> str: + options = f"show jobid -dd {job_id}" + LOG.debug(f"Executing the command 'scontrol {options}'") + + ready = False + time_start = time.time() + counter = 0 + while not ready: + proc = send_scontrol(options) + + stdout = proc.stdout.read().decode() + if "JobState=RUNNING" not in stdout: + counter += 1 + time.sleep(1) + if (counter % 3 + 1) == 0: # print every 3 seconds. Skipping the first. + LOG.debug("The job is not ready yet. Waiting...") + print("The job is not ready yet. Waiting...") + else: + ready = True + break + + # Exit by raising exception + if time.time() > time_start + timeout: + state = get_state_from_scontrol(stdout) + + # Trying to get the hostname from the last valid message + try: + host = get_hostname_from_scontrol(stdout) + if not host: + # If string is empty, go to the exception clause. + raise IndexError() + + hostname_msg = f"The BatchHost for this job is '{host}'" + except (IndexError, AttributeError): + hostname_msg = "PyMAPDL couldn't get the BatchHost hostname" + + # Raising exception + raise MapdlDidNotStart( + f"The HPC job (id: {job_id}) didn't start on time (timeout={timeout}). " + f"The job state is '{state}'. " + f"{hostname_msg}. " + "You can check more information by issuing in your console:\n" + f" scontrol show jobid -dd {job_id}" + ) + + LOG.debug(f"The 'scontrol' command returned:\n{stdout}") + batchhost = get_hostname_from_scontrol(stdout) + LOG.debug(f"Batchhost: {batchhost}") + + # we should validate + batchhost_ip = socket.gethostbyname(batchhost) + LOG.debug(f"Batchhost IP: {batchhost_ip}") + + LOG.info( + f"Job {job_id} successfully allocated and running in '{batchhost}'({batchhost_ip})" + ) + return batchhost, batchhost_ip + + +def get_jobid(stdout: str) -> int: + """Extract the jobid from a command output""" + job_id = stdout.strip().split(" ")[-1] + + try: + job_id = int(job_id) + except ValueError: + LOG.error(f"The console output does not seems to have a valid jobid:\n{stdout}") + raise ValueError("PyMAPDL could not retrieve the job id.") + + LOG.debug(f"The job id is: {job_id}") + return job_id + + +def generate_sbatch_command( + cmd: Union[str, List[str]], scheduler_options: Optional[Union[str, Dict[str, str]]] +) -> List[str]: + """Generate sbatch command for a given MAPDL launch command.""" + + def add_minus(arg: str): + if not arg: + return "" + + arg = str(arg) + + if not arg.startswith("-"): + if len(arg) == 1: + arg = f"-{arg}" + else: + arg = f"--{arg}" + elif not arg.startswith("--") and len(arg) > 2: + # missing one "-" for a long argument + arg = f"-{arg}" + + return arg + + if scheduler_options: + if isinstance(scheduler_options, dict): + scheduler_options = " ".join( + [ + f"{add_minus(key)}='{value}'" + for key, value in scheduler_options.items() + ] + ) + else: + scheduler_options = "" + + if "wrap" in scheduler_options: + raise ValueError( + "The sbatch argument 'wrap' is used by PyMAPDL to submit the job." + "Hence you cannot use it as sbatch argument." + ) + LOG.debug(f"The additional sbatch arguments are: {scheduler_options}") + + if isinstance(cmd, list): + cmd = " ".join(cmd) + + cmd = ["sbatch", scheduler_options, "--wrap", f"'{cmd}'"] + cmd = [each for each in cmd if bool(each)] + return cmd + + +def get_hostname_from_scontrol(stdout: str) -> str: + return stdout.split("BatchHost=")[1].splitlines()[0].strip() + + +def get_state_from_scontrol(stdout: str) -> str: + return stdout.split("JobState=")[1].splitlines()[0].strip() + + +def launch_mapdl_on_cluster( + nproc: int, + *, + scheduler_options: Union[str, Dict[str, str]] = None, + **launch_mapdl_args: Dict[str, Any], +) -> MapdlGrpc: + """Launch MAPDL on a HPC cluster + + Launches an interactive MAPDL instance on an HPC cluster. + + Parameters + ---------- + nproc : int + Number of CPUs to be used in the simulation. + + scheduler_options : Dict[str, str], optional + A string or dictionary specifying the job configuration for the + scheduler. For example ``scheduler_options = "-N 10"``. + + Returns + ------- + MapdlGrpc + Mapdl instance running on the HPC cluster. + + Examples + -------- + Run a job with 10 nodes and 2 tasks per node: + + >>> from ansys.mapdl.core import launch_mapdl + >>> scheduler_options = {"nodes": 10, "ntasks-per-node": 2} + >>> mapdl = launch_mapdl( + launch_on_hpc=True, + nproc=20, + scheduler_options=scheduler_options + ) + + """ + from ansys.mapdl.core.launcher import launch_mapdl + + # Processing the arguments + launch_mapdl_args["launch_on_hpc"] = True + + if launch_mapdl_args.get("mode", "grpc") != "grpc": + raise ValueError( + "The only mode allowed for launch MAPDL on an HPC cluster is gRPC." + ) + + if launch_mapdl_args.get("ip"): + raise ValueError(LAUNCH_ON_HCP_ERROR_MESSAGE_IP) + + if not launch_mapdl_args.get("start_instance", True): + raise ValueError( + "The 'start_instance' argument must be 'True' when launching on HPC." + ) + + return launch_mapdl( + nproc=nproc, + scheduler_options=scheduler_options, + **launch_mapdl_args, + ) + + +def launch_mapdl_grpc(): + args = processing_local_arguments(locals()) + if args.get("mode", "grpc") != "grpc": + raise ValueError("Invalid 'mode'.") + args["port"] = get_port(args["port"], args["start_instance"]) + + start_parm = generate_start_parameters(args) + + # Early exit for debugging. + if args["_debug_no_launch"]: + # Early exit, just for testing + return args # type: ignore + + # Check the license server + if args["license_server_check"]: + LOG.debug("Checking license server.") + lic_check = LicenseChecker(timeout=args["start_timeout"]) + lic_check.start() + + ######################################## + # Launch MAPDL with gRPC + # ---------------------- + # + cmd = generate_mapdl_launch_command( + exec_file=args["exec_file"], + jobname=args["jobname"], + nproc=args["nproc"], + ram=args["ram"], + port=args["port"], + additional_switches=args["additional_switches"], + ) + + # wrapping command if on HPC + cmd = generate_sbatch_command(cmd, scheduler_options=args.get("scheduler_options")) + + try: + # + process = launch_grpc( + cmd=cmd, + run_location=args["run_location"], + env_vars=env_vars, + launch_on_hpc=args.get("launch_on_hpc"), + mapdl_output=args.get("mapdl_output"), + ) + + start_parm["jobid"] = check_mapdl_launch_on_hpc(process, start_parm) + get_job_info(start_parm=start_parm, timeout=args["start_timeout"]) + + except Exception as exception: + LOG.error("An error occurred when launching MAPDL.") + + jobid: int = start_parm.get("jobid", "Not found") + + if ( + args["launch_on_hpc"] + and start_parm.get("finish_job_on_exit", True) + and jobid not in ["Not found", None] + ): + + LOG.debug(f"Killing HPC job with id: {jobid}") + kill_job(jobid) + + if args["license_server_check"]: + LOG.debug("Checking license server.") + lic_check.check() + + raise exception + + if args["just_launch"]: + out = [args["ip"], args["port"]] + if hasattr(process, "pid"): + out += [process.pid] + return out + + ######################################## + # Connect to MAPDL using gRPC + # --------------------------- + # + try: + mapdl = MapdlGrpc( + cleanup_on_exit=args["cleanup_on_exit"], + loglevel=args["loglevel"], + set_no_abort=args["set_no_abort"], + remove_temp_dir_on_exit=args["remove_temp_dir_on_exit"], + log_apdl=args["log_apdl"], + process=process, + use_vtk=args["use_vtk"], + **start_parm, + ) + + except Exception as exception: + LOG.error("An error occurred when connecting to MAPDL.") + raise exception + + return mapdl + + +def get_slurm_options( + args: Dict[str, Any], + kwargs: Dict[str, Any], +) -> Dict[str, Any]: + def get_value( + variable: str, + kwargs: Dict[str, Any], + default: Optional[Union[str, int, float]] = 1, + astype: Optional[Callable[[Any], Any]] = int, + ): + value_from_env_vars = os.environ.get(variable) + value_from_kwargs = kwargs.pop(variable, None) + value = value_from_kwargs or value_from_env_vars or default + if astype and value: + return astype(value) + else: + return value + + ## Getting env vars + SLURM_NNODES = get_value("SLURM_NNODES", kwargs) + LOG.info(f"SLURM_NNODES: {SLURM_NNODES}") + # ntasks is for mpi + SLURM_NTASKS = get_value("SLURM_NTASKS", kwargs) + LOG.info(f"SLURM_NTASKS: {SLURM_NTASKS}") + # Sharing tasks across multiple nodes (DMP) + # the format of this envvar is a bit tricky. Avoiding it for the moment. + # SLURM_TASKS_PER_NODE = int( + # kwargs.pop( + # "SLURM_TASKS_PER_NODE", os.environ.get("SLURM_TASKS_PER_NODE", 1) + # ) + # ) + + # cpus-per-task is for multithreading, + # sharing tasks across multiple CPUs in same node (SMP) + SLURM_CPUS_PER_TASK = get_value("SLURM_CPUS_PER_TASK", kwargs) + LOG.info(f"SLURM_CPUS_PER_TASK: {SLURM_CPUS_PER_TASK}") + + # Set to value of the --ntasks option, if specified. See SLURM_NTASKS. + # Included for backwards compatibility. + SLURM_NPROCS = get_value("SLURM_NPROCS", kwargs) + LOG.info(f"SLURM_NPROCS: {SLURM_NPROCS}") + + # Number of CPUs allocated to the batch step. + SLURM_CPUS_ON_NODE = get_value("SLURM_CPUS_ON_NODE", kwargs) + LOG.info(f"SLURM_CPUS_ON_NODE: {SLURM_CPUS_ON_NODE}") + + SLURM_MEM_PER_NODE = get_value( + "SLURM_MEM_PER_NODE", kwargs, default="", astype=str + ).upper() + LOG.info(f"SLURM_MEM_PER_NODE: {SLURM_MEM_PER_NODE}") + + SLURM_NODELIST = get_value( + "SLURM_NODELIST", kwargs, default="", astype=None + ).lower() + LOG.info(f"SLURM_NODELIST: {SLURM_NODELIST}") + + if not args["exec_file"]: + args["exec_file"] = os.environ.get("PYMAPDL_MAPDL_EXEC") + + if not args["exec_file"]: + # We should probably make a way to find it. + # We will use the module thing + pass + LOG.info(f"Using MAPDL executable in: {args['exec_file']}") + + if not args["jobname"]: + args["jobname"] = os.environ.get("SLURM_JOB_NAME", "file") + LOG.info(f"Using jobname: {args['jobname']}") + + # Checking specific env var + if not args["nproc"]: + ## Attempt to calculate the appropriate number of cores: + # Reference: https://stackoverflow.com/a/51141287/6650211 + # I'm assuming the env var makes sense. + # + # - SLURM_CPUS_ON_NODE is a property of the cluster, not of the job. + # + options = max( + [ + # 4, # Fall back option + SLURM_CPUS_PER_TASK * SLURM_NTASKS, # (CPUs) + SLURM_NPROCS, # (CPUs) + # SLURM_NTASKS, # (tasks) Not necessary the number of CPUs, + # SLURM_NNODES * SLURM_TASKS_PER_NODE * SLURM_CPUS_PER_TASK, # (CPUs) + SLURM_CPUS_ON_NODE * SLURM_NNODES, # (cpus) + ] + ) + LOG.info(f"On SLURM number of processors options {options}") + + args["nproc"] = int(os.environ.get("PYMAPDL_NPROC", options)) + + LOG.info(f"Setting number of CPUs to: {args['nproc']}") + + if not args["ram"]: + if SLURM_MEM_PER_NODE: + # RAM argument is in MB, so we need to convert + units = None + if SLURM_MEM_PER_NODE[-1].isalpha(): + units = SLURM_MEM_PER_NODE[-1] + ram = SLURM_MEM_PER_NODE[:-1] + else: + units = None + ram = SLURM_MEM_PER_NODE + + if not units: + args["ram"] = int(ram) + elif units == "T": # tera + args["ram"] = int(ram) * (2**10) ** 2 + elif units == "G": # giga + args["ram"] = int(ram) * (2**10) ** 1 + elif units == "M": # mega + args["ram"] = int(ram) + elif units == "K": # kilo + args["ram"] = int(ram) * (2**10) ** (-1) + else: # Mega + raise ValueError( + "The memory defined in 'SLURM_MEM_PER_NODE' env var(" + f"'{SLURM_MEM_PER_NODE}') is not valid." + ) + + LOG.info(f"Setting RAM to: {args['ram']}") + + # We use "-dis " (with space) to avoid collision with user variables such + # as `-distro` or so + if "-dis " not in args["additional_switches"] and not args[ + "additional_switches" + ].endswith("-dis"): + args["additional_switches"] += " -dis" + + # Finally set to avoid timeouts + args["license_server_check"] = False + args["start_timeout"] = 2 * args["start_timeout"] + + return args diff --git a/src/ansys/mapdl/core/jupyter.py b/src/ansys/mapdl/core/launcher/jupyter.py similarity index 100% rename from src/ansys/mapdl/core/jupyter.py rename to src/ansys/mapdl/core/launcher/jupyter.py diff --git a/src/ansys/mapdl/core/launcher/launcher.py b/src/ansys/mapdl/core/launcher/launcher.py new file mode 100644 index 0000000000..43cdf1b7f1 --- /dev/null +++ b/src/ansys/mapdl/core/launcher/launcher.py @@ -0,0 +1,726 @@ +# Copyright (C) 2016 - 2025 ANSYS, Inc. and/or its affiliates. +# SPDX-License-Identifier: MIT +# +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in all +# copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +# SOFTWARE. + +"""Module for launching MAPDL locally or connecting to a remote instance with gRPC.""" + +import atexit +from typing import TYPE_CHECKING, Any, Dict, Optional, Union + +from ansys.mapdl import core as pymapdl +from ansys.mapdl.core import LOG +from ansys.mapdl.core.launcher.pim import is_ready_for_pypim, launch_remote_mapdl +from ansys.mapdl.core.licensing import LicenseChecker +from ansys.mapdl.core.mapdl_grpc import MapdlGrpc + +if TYPE_CHECKING: # pragma: no cover + from ansys.mapdl.core.mapdl_console import MapdlConsole + +from ansys.mapdl.core.launcher.grpc import launch_grpc +from ansys.mapdl.core.launcher.hpc import ( + check_mapdl_launch_on_hpc, + generate_sbatch_command, + get_job_info, + get_slurm_options, + is_running_on_slurm, + kill_job, +) +from ansys.mapdl.core.launcher.tools import ( + _cleanup_gallery_instance, + check_kwargs, + check_lock_file, + check_mapdl_launch, + check_mode, + configure_ubuntu, + create_gallery_instances, + force_smp_in_student, + generate_mapdl_launch_command, + generate_start_parameters, + get_cpus, + get_exec_file, + get_ip, + get_port, + get_run_location, + get_start_instance_arg, + get_version, + pack_arguments, + pre_check_args, + remove_err_files, + set_license_switch, + set_MPI_additional_switches, + update_env_vars, +) + +atexit.register(_cleanup_gallery_instance) + + +def launch_mapdl( + exec_file: Optional[str] = None, + run_location: Optional[str] = None, + jobname: str = "file", + *, + nproc: Optional[int] = None, + ram: Optional[Union[int, str]] = None, + mode: Optional[str] = None, + override: bool = False, + loglevel: str = "ERROR", + additional_switches: str = "", + start_timeout: Optional[int] = None, + port: Optional[int] = None, + cleanup_on_exit: bool = True, + start_instance: Optional[bool] = None, + ip: Optional[str] = None, + clear_on_connect: bool = True, + log_apdl: Optional[Union[bool, str]] = None, + remove_temp_dir_on_exit: bool = False, + license_server_check: bool = False, + license_type: Optional[bool] = None, + print_com: bool = False, + add_env_vars: Optional[Dict[str, str]] = None, + replace_env_vars: Optional[Dict[str, str]] = None, + version: Optional[Union[int, str]] = None, + running_on_hpc: bool = True, + launch_on_hpc: bool = False, + mapdl_output: Optional[str] = None, + **kwargs: Dict[str, Any], +) -> Union[MapdlGrpc, "MapdlConsole"]: + """Start MAPDL locally. + + Parameters + ---------- + exec_file : str, optional + The location of the MAPDL executable. Will use the cached + location when left at the default :class:`None` and no environment + variable is set. + + The executable path can be also set through the environment variable + :envvar:`PYMAPDL_MAPDL_EXEC`. For example: + + .. code:: console + + export PYMAPDL_MAPDL_EXEC=/ansys_inc/v211/ansys/bin/mapdl + + run_location : str, optional + MAPDL working directory. Defaults to a temporary working + directory. If directory doesn't exist, one is created. + + jobname : str, optional + MAPDL jobname. Defaults to ``'file'``. + + nproc : int, optional + Number of processors. Defaults to ``2``. If running on an HPC cluster, + this value is adjusted to the number of CPUs allocated to the job, + unless the argument ``running_on_hpc`` is set to ``"false"``. + + ram : float, optional + Total size in megabytes of the workspace (memory) used for the initial + allocation. The default is :class:`None`, in which case 2 GB (2048 MB) is + used. To force a fixed size throughout the run, specify a negative + number. + + mode : str, optional + Mode to launch MAPDL. Must be one of the following: + + - ``'grpc'`` + - ``'console'`` + + The ``'grpc'`` mode is available on ANSYS 2021R1 or newer and + provides the best performance and stability. + The ``'console'`` mode is for legacy use only Linux only prior to 2020R2. + This console mode is pending depreciation. + Visit :ref:`versions_and_interfaces` for more information. + + override : bool, optional + Attempts to delete the lock file at the ``run_location``. + Useful when a prior MAPDL session has exited prematurely and + the lock file has not been deleted. + + loglevel : str, optional + Sets which messages are printed to the console. ``'INFO'`` + prints out all ANSYS messages, ``'WARNING'`` prints only + messages containing ANSYS warnings, and ``'ERROR'`` logs only + error messages. + + additional_switches : str, optional + Additional switches for MAPDL, for example ``'aa_r'``, the + academic research license, would be added with: + + - ``additional_switches="-aa_r"`` + + Avoid adding switches like ``-i``, ``-o`` or ``-b`` as these are already + included to start up the MAPDL server. See the notes + section for additional details. + + start_timeout : float, optional + Maximum allowable time to connect to the MAPDL server. By default it is + 45 seconds, however, it is increased to 90 seconds if running on HPC. + + port : int + Port to launch MAPDL gRPC on. Final port will be the first + port available after (or including) this port. Defaults to + ``50052``. You can also provide this value through the environment variable + :envvar:`PYMAPDL_PORT`. For instance ``PYMAPDL_PORT=50053``. + However the argument (if specified) has precedence over the environment + variable. If this environment variable is empty, it is as it is not set. + + cleanup_on_exit : bool, optional + Exit MAPDL when python exits or the mapdl Python instance is + garbage collected. + + start_instance : bool, optional + When :class:`False`, connect to an existing MAPDL instance at ``ip`` + and ``port``, which default to ip ``'127.0.0.1'`` at port ``50052``. + Otherwise, launch a local instance of MAPDL. You can also + provide this value through the environment variable + :envvar:`PYMAPDL_START_INSTANCE`. + However the argument (if specified) has precedence over the environment + variable. If this environment variable is empty, it is as it is not set. + + ip : str, optional + Specify the IP address of the MAPDL instance to connect to. + You can also provide a hostname as an alternative to an IP address. + Defaults to ``'127.0.0.1'``. + Used only when ``start_instance`` is :class:`False`. If this argument + is provided, and ``start_instance`` (or its correspondent environment + variable :envvar:`PYMAPDL_START_INSTANCE`) is :class:`True` then, an + exception is raised. + You can also provide this value through the environment variable + :envvar:`PYMAPDL_IP`. For instance ``PYMAPDL_IP=123.45.67.89``. + However the argument (if specified) has precedence over the environment + variable. If this environment variable is empty, it is as it is not set. + + clear_on_connect : bool, optional + Defaults to :class:`True`, giving you a fresh environment when + connecting to MAPDL. When if ``start_instance`` is specified + it defaults to :class:`False`. + + log_apdl : str, optional + Enables logging every APDL command to the local disk. This + can be used to "record" all the commands that are sent to + MAPDL via PyMAPDL so a script can be run within MAPDL without + PyMAPDL. This argument is the path of the output file (e.g. + ``log_apdl='pymapdl_log.txt'``). By default this is disabled. + + remove_temp_dir_on_exit : bool, optional + When ``run_location`` is :class:`None`, this launcher creates a new MAPDL + working directory within the user temporary directory, obtainable with + ``tempfile.gettempdir()``. When this parameter is + :class:`True`, this directory will be deleted when MAPDL is exited. + Default to :class:`False`. + If you change the working directory, PyMAPDL does not delete the original + working directory nor the new one. + + license_server_check : bool, optional + Check if the license server is available if MAPDL fails to + start. Only available on ``mode='grpc'``. Defaults :class:`False`. + + license_type : str, optional + Enable license type selection. You can input a string for its + license name (for example ``'meba'`` or ``'ansys'``) or its description + ("enterprise solver" or "enterprise" respectively). + You can also use legacy licenses (for example ``'aa_t_a'``) but it will + also raise a warning. If it is not used (:class:`None`), no specific + license will be requested, being up to the license server to provide a + specific license type. Default is :class:`None`. + + print_com : bool, optional + Print the command ``/COM`` arguments to the standard output. + Default :class:`False`. + + add_env_vars : dict, optional + The provided dictionary will be used to extend the MAPDL process + environment variables. If you want to control all of the environment + variables, use the argument ``replace_env_vars``. + Defaults to :class:`None`. + + replace_env_vars : dict, optional + The provided dictionary will be used to replace all the MAPDL process + environment variables. It replace the system environment variables + which otherwise would be used in the process. + To just add some environment variables to the MAPDL + process, use ``add_env_vars``. Defaults to :class:`None`. + + version : float, optional + Version of MAPDL to launch. If :class:`None`, the latest version is used. + Versions can be provided as integers (i.e. ``version=222``) or + floats (i.e. ``version=22.2``). + To retrieve the available installed versions, use the function + :meth:`ansys.tools.path.path.get_available_ansys_installations`. + You can also provide this value through the environment variable + :envvar:`PYMAPDL_MAPDL_VERSION`. + For instance ``PYMAPDL_MAPDL_VERSION=22.2``. + However the argument (if specified) has precedence over the environment + variable. If this environment variable is empty, it is as it is not set. + + running_on_hpc: bool, optional + Whether detect if PyMAPDL is running on an HPC cluster. Currently + only SLURM clusters are supported. By default, it is set to true. + This option can be bypassed if the :envvar:`PYMAPDL_RUNNING_ON_HPC` + environment variable is set to :class:`True`. + For more information, see :ref:`ref_hpc_slurm`. + + launch_on_hpc : bool, Optional + If :class:`True`, it uses the implemented scheduler (SLURM only) to launch + an MAPDL instance on the HPC. In this case you can pass the + '`scheduler_options`' argument to + :func:`launch_mapdl() ` + to specify the scheduler arguments as a string or as a dictionary. + For more information, see :ref:`ref_hpc_slurm`. + + mapdl_output : str, optional + Redirect the MAPDL console output to a given file. + + kwargs : dict, Optional + These keyword arguments are interface-specific or for + development purposes. For more information, see Notes. + + scheduler_options : :class:`str`, :class:`dict` + Use it to specify options to the scheduler run command. It can be a + string or a dictionary with arguments and its values (both as strings). + For more information visit :ref:`ref_hpc_slurm`. + + set_no_abort : :class:`bool` + *(Development use only)* + Sets MAPDL to not abort at the first error within /BATCH mode. + Defaults to :class:`True`. + + force_intel : :class:`bool` + *(Development use only)* + Forces the use of Intel message pass interface (MPI) in versions between + Ansys 2021R0 and 2022R2, where because of VPNs issues this MPI is + deactivated by default. + See :ref:`vpn_issues_troubleshooting` for more information. + Defaults to :class:`False`. + + Returns + ------- + Union[MapdlGrpc, MapdlConsole] + An instance of Mapdl. Type depends on the selected ``mode``. + + Notes + ----- + + **Ansys Student Version** + + If an Ansys Student version is detected, PyMAPDL will launch MAPDL in + shared-memory parallelism (SMP) mode unless another option is specified. + + **Additional switches** + + These are the MAPDL switch options as of 2020R2 applicable for + running MAPDL as a service via gRPC. Excluded switches not applicable or + are set via keyword arguments such as ``"-j"`` . + + \\-acc + Enables the use of GPU hardware. See GPU + Accelerator Capability in the Parallel Processing Guide for more + information. + + \\-amfg + Enables the additive manufacturing capability. Requires + an additive manufacturing license. For general information about + this feature, see AM Process Simulation in ANSYS Workbench. + + \\-ansexe + Activates a custom mechanical APDL executable. + In the ANSYS Workbench environment, activates a custom + Mechanical APDL executable. + + \\-custom + Calls a custom Mechanical APDL executable + See Running Your Custom Executable in the Programmer's Reference + for more information. + + \\-db value + Initial memory allocation + Defines the portion of workspace (memory) to be used as the + initial allocation for the database. The default is 1024 + MB. Specify a negative number to force a fixed size throughout + the run; useful on small memory systems. + + \\-dis + Enables Distributed ANSYS + See the Parallel Processing Guide for more information. + + \\-dvt + Enables ANSYS DesignXplorer advanced task (add-on). + Requires DesignXplorer. + + \\-l + Specifies a language file to use other than English + This option is valid only if you have a translated message file + in an appropriately named subdirectory in + ``/ansys_inc/v201/ansys/docu`` or + ``Program Files\\ANSYS\\Inc\\V201\\ANSYS\\docu`` + + \\-m + Specifies the total size of the workspace + Workspace (memory) in megabytes used for the initial + allocation. If you omit the ``-m`` option, the default is 2 GB + (2048 MB). Specify a negative number to force a fixed size + throughout the run. + + \\-machines + Specifies the distributed machines + Machines on which to run a Distributed ANSYS analysis. See + Starting Distributed ANSYS in the Parallel Processing Guide for + more information. + + \\-mpi + Specifies the type of MPI to use. + See the Parallel Processing Guide for more information. + + \\-mpifile + Specifies an existing MPI file + Specifies an existing MPI file (appfile) to be used in a + Distributed ANSYS run. See Using MPI Files in the Parallel + Processing Guide for more information. + + \\-na + Specifies the number of GPU accelerator devices + Number of GPU devices per machine or compute node when running + with the GPU accelerator feature. See GPU Accelerator Capability + in the Parallel Processing Guide for more information. + + \\-name + Defines Mechanical APDL parameters + Set mechanical APDL parameters at program start-up. The parameter + name must be at least two characters long. For details about + parameters, see the ANSYS Parametric Design Language Guide. + + \\-p + ANSYS session product + Defines the ANSYS session product that will run during the + session. For more detailed information about the ``-p`` option, + see Selecting an ANSYS Product via the Command Line. + + \\-ppf + HPC license + Specifies which HPC license to use during a parallel processing + run. See HPC Licensing in the Parallel Processing Guide for more + information. + + \\-smp + Enables shared-memory parallelism. + See the Parallel Processing Guide for more information. + + **PyPIM** + + If the environment is configured to use `PyPIM `_ + and ``start_instance`` is :class:`True`, then starting the instance will be delegated to PyPIM. + In this event, most of the options will be ignored and the server side configuration will + be used. + + Examples + -------- + Launch MAPDL using the best protocol. + + >>> from ansys.mapdl.core import launch_mapdl + >>> mapdl = launch_mapdl() + + Run MAPDL with shared memory parallel and specify the location of + the Ansys binary. + + >>> exec_file = 'C:/Program Files/ANSYS Inc/v231/ansys/bin/winx64/ANSYS231.exe' + >>> mapdl = launch_mapdl(exec_file, additional_switches='-smp') + + Connect to an existing instance of MAPDL at IP 192.168.1.30 and + port 50001. This is only available using the latest ``'grpc'`` + mode. + + >>> mapdl = launch_mapdl(start_instance=False, ip='192.168.1.30', + ... port=50001) + + Run MAPDL using the console mode (not recommended, and available only on Linux). + + >>> mapdl = launch_mapdl('/ansys_inc/v194/ansys/bin/ansys194', + ... mode='console') + + Run MAPDL with additional environment variables. + + >>> my_env_vars = {"my_var":"true", "ANSYS_LOCK":"FALSE"} + >>> mapdl = launch_mapdl(add_env_vars=my_env_vars) + + Run MAPDL with our own set of environment variables. It replace the system + environment variables which otherwise would be used in the process. + + >>> my_env_vars = {"my_var":"true", + "ANSYS_LOCK":"FALSE", + "ANSYSLMD_LICENSE_FILE":"1055@MYSERVER"} + >>> mapdl = launch_mapdl(replace_env_vars=my_env_vars) + """ + ######################################## + # Processing arguments + # -------------------- + # + # packing arguments + + args = pack_arguments(locals()) # packs args and kwargs + + check_kwargs(args) # check if passing wrong arguments + + pre_check_args(args) + + ######################################## + # PyPIM connection + # ---------------- + # Delegating to PyPIM if applicable + # + if is_ready_for_pypim(exec_file): + # Start MAPDL with PyPIM if the environment is configured for it + # and the user did not pass a directive on how to launch it. + LOG.info("Starting MAPDL remotely. The startup configuration will be ignored.") + + return launch_remote_mapdl( + cleanup_on_exit=args["cleanup_on_exit"], version=args["version"] + ) + + ######################################## + # SLURM settings + # -------------- + # Checking if running on SLURM HPC + # + if is_running_on_slurm(args): + LOG.info("On Slurm mode.") + + # extracting parameters + get_slurm_options(args, kwargs) + + get_start_instance_arg(args) + + get_cpus(args) + + get_ip(args) + + args["port"] = get_port(args["port"], args["start_instance"]) + + if args["start_instance"]: + ######################################## + # Local adjustments + # ----------------- + # + # Only when starting MAPDL (aka Local) + + get_exec_file(args) + + args["version"] = get_version( + args["version"], args.get("exec_file"), launch_on_hpc=args["launch_on_hpc"] + ) + + args["additional_switches"] = set_license_switch( + args["license_type"], args["additional_switches"] + ) + + env_vars: Dict[str, str] = update_env_vars( + args["add_env_vars"], args["replace_env_vars"] + ) + + get_run_location(args) + + # verify lock file does not exist + check_lock_file(args["run_location"], args["jobname"], args["override"]) + + # remove err file so we can track its creation + # (as way to check if MAPDL started or not) + remove_err_files(args["run_location"], args["jobname"]) + + # Check for a valid connection mode + args["mode"] = check_mode(args["mode"], args["version"]) + + ######################################## + # Context specific launching adjustments + # -------------------------------------- + # + if args["start_instance"]: + # ON HPC: + # Assuming that if login node is ubuntu, the computation ones + # are also ubuntu. + env_vars = configure_ubuntu(env_vars) + + # Set SMP by default if student version is used. + args["additional_switches"] = force_smp_in_student( + args["additional_switches"], args["exec_file"] + ) + + # Set compatible MPI + args["additional_switches"] = set_MPI_additional_switches( + args["additional_switches"], + force_intel=args["force_intel"], + version=args["version"], + ) + + LOG.debug(f"Using additional switches {args['additional_switches']}.") + + if args["running_on_hpc"] or args["launch_on_hpc"]: + env_vars.setdefault("ANS_MULTIPLE_NODES", "1") + env_vars.setdefault("HYDRA_BOOTSTRAP", "slurm") + + start_parm = generate_start_parameters(args) + + # Early exit for debugging. + if args["_debug_no_launch"]: + # Early exit, just for testing + return args # type: ignore + + if not args["start_instance"]: + ######################################## + # Connecting to a remote instance + # ------------------------------- + # + LOG.debug( + f"Connecting to an existing instance of MAPDL at {args['ip']}:{args['port']}" + ) + start_parm["launched"] = False + + mapdl = MapdlGrpc( + cleanup_on_exit=False, + loglevel=args["loglevel"], + set_no_abort=args["set_no_abort"], + use_vtk=args["use_vtk"], + log_apdl=args["log_apdl"], + **start_parm, + ) + if args["clear_on_connect"]: + mapdl.clear() + return mapdl + + ######################################## + # Sphinx docs adjustments + # ----------------------- + # + # special handling when building the gallery outside of CI. This + # creates an instance of mapdl the first time. + if pymapdl.BUILDING_GALLERY: # pragma: no cover + return create_gallery_instances(args, start_parm) + + ######################################## + # Local launching + # --------------- + # + # Check the license server + if args["license_server_check"]: + LOG.debug("Checking license server.") + lic_check = LicenseChecker(timeout=args["start_timeout"]) + lic_check.start() + + LOG.debug("Starting MAPDL") + if args["mode"] == "console": # pragma: no cover + ######################################## + # Launch MAPDL on console mode + # ---------------------------- + # + from ansys.mapdl.core.launcher.console import check_console_start_parameters + from ansys.mapdl.core.mapdl_console import MapdlConsole + + start_parm = check_console_start_parameters(start_parm) + mapdl = MapdlConsole( + loglevel=args["loglevel"], + log_apdl=args["log_apdl"], + use_vtk=args["use_vtk"], + **start_parm, + ) + + elif args["mode"] == "grpc": + ######################################## + # Launch MAPDL with gRPC + # ---------------------- + # + cmd = generate_mapdl_launch_command( + exec_file=args["exec_file"], + jobname=args["jobname"], + nproc=args["nproc"], + ram=args["ram"], + port=args["port"], + additional_switches=args["additional_switches"], + ) + + if args["launch_on_hpc"]: + # wrapping command if on HPC + cmd = generate_sbatch_command( + cmd, scheduler_options=args.get("scheduler_options") + ) + + try: + # + process = launch_grpc( + cmd=cmd, + run_location=args["run_location"], + env_vars=env_vars, + launch_on_hpc=args.get("launch_on_hpc"), + mapdl_output=args.get("mapdl_output"), + ) + + if args["launch_on_hpc"]: + start_parm["jobid"] = check_mapdl_launch_on_hpc(process, start_parm) + get_job_info(start_parm=start_parm, timeout=args["start_timeout"]) + else: + # Local mapdl launch check + check_mapdl_launch( + process, args["run_location"], args["start_timeout"], cmd + ) + + except Exception as exception: + LOG.error("An error occurred when launching MAPDL.") + + jobid: int = start_parm.get("jobid", "Not found") + + if ( + args["launch_on_hpc"] + and start_parm.get("finish_job_on_exit", True) + and jobid not in ["Not found", None] + ): + + LOG.debug(f"Killing HPC job with id: {jobid}") + kill_job(jobid) + + if args["license_server_check"]: + LOG.debug("Checking license server.") + lic_check.check() + + raise exception + + if args["just_launch"]: + out = [args["ip"], args["port"]] + if hasattr(process, "pid"): + out += [process.pid] + return out + + ######################################## + # Connect to MAPDL using gRPC + # --------------------------- + # + try: + mapdl = MapdlGrpc( + cleanup_on_exit=args["cleanup_on_exit"], + loglevel=args["loglevel"], + set_no_abort=args["set_no_abort"], + remove_temp_dir_on_exit=args["remove_temp_dir_on_exit"], + log_apdl=args["log_apdl"], + process=process, + use_vtk=args["use_vtk"], + **start_parm, + ) + + except Exception as exception: + LOG.error("An error occurred when connecting to MAPDL.") + raise exception + + return mapdl diff --git a/src/ansys/mapdl/core/launcher/local.py b/src/ansys/mapdl/core/launcher/local.py new file mode 100644 index 0000000000..8494ca664a --- /dev/null +++ b/src/ansys/mapdl/core/launcher/local.py @@ -0,0 +1,114 @@ +# Copyright (C) 2016 - 2025 ANSYS, Inc. and/or its affiliates. +# SPDX-License-Identifier: MIT +# +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in all +# copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +# SOFTWARE. + +"""Launch MAPDL locally""" +from ansys.mapdl.core.launcher.tools import ( + check_kwargs, + check_lock_file, + check_mode, + configure_ubuntu, + force_smp_in_student, + get_cpus, + get_exec_file, + get_run_location, + get_version, + pack_arguments, + pre_check_args, + remove_err_files, + set_license_switch, + set_MPI_additional_switches, + update_env_vars, +) + + +def processing_local_arguments(args): + # packing arguments + args = pack_arguments(locals()) # packs args and kwargs + + check_kwargs(args) # check if passing wrong arguments + + if args.get("start_instance") and args["start_instance"] != False: + raise ValueError( + "'start_instance' argument is not valid." + "If you intend to connect to an already started instance use either " + "'connect_to_mapdl' or the infamous 'launch_mapdl(start_instance=False)'." + ) + + pre_check_args(args) + + get_cpus(args) + + ######################################## + # Local adjustments + # ----------------- + # + # Only when starting MAPDL (aka Local) + get_exec_file(args) + + args["version"] = get_version( + args["version"], args.get("exec_file"), launch_on_hpc=args["launch_on_hpc"] + ) + + args["additional_switches"] = set_license_switch( + args["license_type"], args["additional_switches"] + ) + + env_vars: Dict[str, str] = update_env_vars( + args["add_env_vars"], args["replace_env_vars"] + ) + + get_run_location(args) + + # verify lock file does not exist + check_lock_file(args["run_location"], args["jobname"], args["override"]) + + # remove err file so we can track its creation + # (as way to check if MAPDL started or not) + remove_err_files(args["run_location"], args["jobname"]) + + # Check for a valid connection mode + args["mode"] = check_mode(args["mode"], args["version"]) + + # ON HPC: + # Assuming that if login node is ubuntu, the computation ones + # are also ubuntu. + env_vars = configure_ubuntu(env_vars) + + # Set SMP by default if student version is used. + args["additional_switches"] = force_smp_in_student( + args["additional_switches"], args["exec_file"] + ) + + # Set compatible MPI + args["additional_switches"] = set_MPI_additional_switches( + args["additional_switches"], + force_intel=args["force_intel"], + version=args["version"], + ) + + LOG.debug(f"Using additional switches {args['additional_switches']}.") + + if args["running_on_hpc"] or args["launch_on_hpc"]: + env_vars.setdefault("ANS_MULTIPLE_NODES", "1") + env_vars.setdefault("HYDRA_BOOTSTRAP", "slurm") + + return args diff --git a/src/ansys/mapdl/core/launcher/pim.py b/src/ansys/mapdl/core/launcher/pim.py new file mode 100644 index 0000000000..05386aa19a --- /dev/null +++ b/src/ansys/mapdl/core/launcher/pim.py @@ -0,0 +1,83 @@ +# Copyright (C) 2016 - 2025 ANSYS, Inc. and/or its affiliates. +# SPDX-License-Identifier: MIT +# +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in all +# copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +# SOFTWARE. + +from typing import Optional + +from ansys.mapdl.core import _HAS_PIM, LOG +from ansys.mapdl.core.mapdl_grpc import MAX_MESSAGE_LENGTH, MapdlGrpc + +if _HAS_PIM: + import ansys.platform.instancemanagement as pypim + + +def is_ready_for_pypim(exec_file): + return _HAS_PIM and exec_file is None and pypim.is_configured() + + +def launch_remote_mapdl( + version: Optional[str] = None, + cleanup_on_exit: bool = True, +) -> MapdlGrpc: + """Start MAPDL remotely using the product instance management API. + + When calling this method, you need to ensure that you are in an environment where PyPIM is configured. + This can be verified with :func:`pypim.is_configured `. + + Parameters + ---------- + version : str, optional + The MAPDL version to run, in the 3 digits format, such as "212". + + If unspecified, the version will be chosen by the server. + + cleanup_on_exit : bool, optional + Exit MAPDL when python exits or the mapdl Python instance is + garbage collected. + + If unspecified, it will be cleaned up. + + Returns + ------- + ansys.mapdl.core.mapdl.MapdlBase + An instance of Mapdl. + """ + if not _HAS_PIM: # pragma: no cover + raise ModuleNotFoundError( + "The package 'ansys-platform-instancemanagement' is required to use this function." + ) + + LOG.debug("Connecting using PyPIM.") + pim = pypim.connect() + instance = pim.create_instance(product_name="mapdl", product_version=version) + instance.wait_for_ready() + channel = instance.build_grpc_channel( + options=[ + ("grpc.max_receive_message_length", MAX_MESSAGE_LENGTH), + ] + ) + + LOG.debug("Channel created and passing it to the Mapdl object") + return MapdlGrpc( + channel=channel, + cleanup_on_exit=cleanup_on_exit, + remote_instance=instance, + ) diff --git a/src/ansys/mapdl/core/launcher/remote.py b/src/ansys/mapdl/core/launcher/remote.py new file mode 100644 index 0000000000..27cec710cb --- /dev/null +++ b/src/ansys/mapdl/core/launcher/remote.py @@ -0,0 +1,101 @@ +# Copyright (C) 2016 - 2025 ANSYS, Inc. and/or its affiliates. +# SPDX-License-Identifier: MIT +# +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in all +# copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +# SOFTWARE. + +from typing import Any, Dict, Optional, Union + +from ansys.mapdl.core.launcher.tools import ( + check_kwargs, + generate_start_parameters, + get_ip, + get_port, + pack_arguments, + pre_check_args, +) +from ansys.mapdl.core.mapdl_grpc import MapdlGrpc + + +def connect_to_mapdl( + *, + loglevel: str = "ERROR", + start_timeout: Optional[int] = None, + port: Optional[int] = None, + cleanup_on_exit: bool = True, + ip: Optional[str] = None, + clear_on_connect: bool = True, + log_apdl: Optional[Union[bool, str]] = None, + print_com: bool = False, + **kwargs: Dict[str, Any], +): + ######################################## + # Processing arguments + # -------------------- + # + # packing arguments + args = pack_arguments(locals()) # packs args and kwargs + + check_kwargs(args) # check if passing wrong arguments + + if args.get("start_instance"): + raise ValueError( + "'connect_to_mapdl' only accept 'start_instance' equals 'False'. " + "If you intend to launch locally an instance use either " + "'launch_mapdl_grpc' or the infamous 'launch_mapdl(start_instance=True)'." + ) + + pre_check_args(args) + + get_ip(args) + + args["port"] = get_port(args["port"], args["start_instance"]) + + # Check for a valid connection mode + # args["mode"] = check_mode(args["mode"], args["version"]) + if args.get("mode", "grpc") != "grpc": + raise ValueError("Only a 'grpc' instance can be connected to remotely.") + + start_parm = generate_start_parameters(args) + + # Early exit for debugging. + if args["_debug_no_launch"]: + # Early exit, just for testing + return args # type: ignore + + ######################################## + # Connecting to a remote instance + # ------------------------------- + # + LOG.debug( + f"Connecting to an existing instance of MAPDL at {args['ip']}:{args['port']}" + ) + start_parm["launched"] = False + + mapdl = MapdlGrpc( + cleanup_on_exit=False, + loglevel=args["loglevel"], + set_no_abort=args["set_no_abort"], + use_vtk=args["use_vtk"], + log_apdl=args["log_apdl"], + **start_parm, + ) + if args["clear_on_connect"]: + mapdl.clear() + return mapdl diff --git a/src/ansys/mapdl/core/launcher.py b/src/ansys/mapdl/core/launcher/tools.py similarity index 52% rename from src/ansys/mapdl/core/launcher.py rename to src/ansys/mapdl/core/launcher/tools.py index 23dc24633c..d7d41077fb 100644 --- a/src/ansys/mapdl/core/launcher.py +++ b/src/ansys/mapdl/core/launcher/tools.py @@ -20,28 +20,21 @@ # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # SOFTWARE. -"""Module for launching MAPDL locally or connecting to a remote instance with gRPC.""" - -import atexit -from functools import wraps import os import platform from queue import Empty, Queue import re import socket - -# Subprocess is needed to start the backend. But -# the input is controlled by the library. Excluding bandit check. -import subprocess # nosec B404 +import subprocess import threading import time -from typing import TYPE_CHECKING, Any, Callable, Dict, List, Optional, Tuple, Union +from typing import Any, Dict, List, Optional, Tuple, Union import warnings import psutil from ansys.mapdl import core as pymapdl -from ansys.mapdl.core import _HAS_ATP, _HAS_PIM, LOG +from ansys.mapdl.core import _HAS_ATP, LOG from ansys.mapdl.core._version import SUPPORTED_ANSYS_VERSIONS from ansys.mapdl.core.errors import ( LockFileException, @@ -51,9 +44,10 @@ PortAlreadyInUseByAnMAPDLInstance, VersionError, ) -from ansys.mapdl.core.licensing import ALLOWABLE_LICENSES, LicenseChecker +from ansys.mapdl.core.launcher import LOCALHOST, MAPDL_DEFAULT_PORT, ON_WSL +from ansys.mapdl.core.licensing import ALLOWABLE_LICENSES from ansys.mapdl.core.mapdl_core import _ALLOWED_START_PARM -from ansys.mapdl.core.mapdl_grpc import MAX_MESSAGE_LENGTH, MapdlGrpc +from ansys.mapdl.core.mapdl_grpc import MapdlGrpc from ansys.mapdl.core.misc import ( check_valid_ip, check_valid_port, @@ -61,28 +55,8 @@ threaded, ) -if _HAS_PIM: - import ansys.platform.instancemanagement as pypim - -if _HAS_ATP: - from ansys.tools.path import find_mapdl, get_mapdl_path - from ansys.tools.path import version_from_path as _version_from_path - - @wraps(_version_from_path) - def version_from_path(*args, **kwargs): - """Wrap ansys.tool.path.version_from_path to raise a warning if the - executable couldn't be found""" - if kwargs.pop("launch_on_hpc", False): - try: - return _version_from_path(*args, **kwargs) - except RuntimeError: - warnings.warn("PyMAPDL could not find the ANSYS executable. ") - else: - return _version_from_path(*args, **kwargs) - +GALLERY_INSTANCE = [None] -if TYPE_CHECKING: # pragma: no cover - from ansys.mapdl.core.mapdl_console import MapdlConsole # settings directory SETTINGS_DIR = pymapdl.USER_DATA_PATH @@ -96,6 +70,7 @@ def version_from_path(*args, **kwargs): "Will be unable to cache MAPDL executable location" ) + CONFIG_FILE = os.path.join(SETTINGS_DIR, "config.txt") ALLOWABLE_MODES = ["console", "grpc"] ALLOWABLE_VERSION_INT = tuple(SUPPORTED_ANSYS_VERSIONS.keys()) @@ -137,16 +112,6 @@ def version_from_path(*args, **kwargs): "use_vtk", ] -ON_WSL = os.name == "posix" and ( - os.environ.get("WSL_DISTRO_NAME") or os.environ.get("WSL_INTEROP") -) - -if ON_WSL: - LOG.info("On WSL: Running on WSL detected.") - LOG.debug("On WSL: Allowing 'start_instance' and 'ip' arguments together.") - -LOCALHOST = "127.0.0.1" -MAPDL_DEFAULT_PORT = 50052 INTEL_MSG = """Due to incompatibilities between this MAPDL version, Windows, and VPN connections, the flat '-mpi INTELMPI' is overwritten by '-mpi msmpi'. @@ -158,17 +123,6 @@ def version_from_path(*args, **kwargs): Be aware of possible errors or unexpected behavior with this configuration. """ -LAUNCH_ON_HCP_ERROR_MESSAGE_IP = ( - "PyMAPDL cannot ensure a specific IP will be used when launching " - "MAPDL on a cluster. Hence the 'ip' argument is not compatible. " - "If you want to connect to an already started MAPDL instance, " - "just connect normally as you would with a remote instance. " - "For example:\n\n" - ">>> mapdl = launch_mapdl(start_instance=False, ip='123.45.67.89')\n\n" - "where '123.45.67.89' is the IP of the machine where MAPDL is running." -) -GALLERY_INSTANCE = [None] - def _cleanup_gallery_instance() -> None: # pragma: no cover """This cleans up any left over instances of MAPDL from building the gallery.""" @@ -180,9 +134,6 @@ def _cleanup_gallery_instance() -> None: # pragma: no cover mapdl.exit(force=True) -atexit.register(_cleanup_gallery_instance) - - def _is_ubuntu() -> bool: """Determine if running as Ubuntu. @@ -215,6 +166,44 @@ def _is_ubuntu() -> bool: return "ubuntu" in platform.platform().lower() +def submitter( + cmd: Union[str, List[str]], + *, + executable: str = None, + shell: bool = False, + cwd: str = None, + stdin: subprocess.PIPE = None, + stdout: subprocess.PIPE = None, + stderr: subprocess.PIPE = None, + env_vars: dict[str, str] = None, +) -> subprocess.Popen: + + if executable: + if isinstance(cmd, list): + cmd = [executable] + cmd + else: + cmd = [executable, cmd] + + if not stdin: + stdin = subprocess.DEVNULL + if not stdout: + stdout = subprocess.PIPE + if not stderr: + stderr = subprocess.PIPE + + # cmd is controlled by the library with generate_mapdl_launch_command. + # Excluding bandit check. + return subprocess.Popen( + args=cmd, + shell=shell, # sbatch does not work without shell. + cwd=cwd, + stdin=stdin, + stdout=stdout, + stderr=stderr, + env=env_vars, + ) + + def close_all_local_instances(port_range: range = None) -> None: """Close all MAPDL instances within a port_range. @@ -332,256 +321,6 @@ def port_in_use_using_psutil(port: Union[int, str]) -> bool: return False -def generate_mapdl_launch_command( - exec_file: str = "", - jobname: str = "file", - nproc: int = 2, - ram: Optional[int] = None, - port: int = MAPDL_DEFAULT_PORT, - additional_switches: str = "", -) -> list[str]: - """Generate the command line to start MAPDL in gRPC mode. - - Parameters - ---------- - exec_file : str, optional - The location of the MAPDL executable. Will use the cached - location when left at the default :class:`None`. - - jobname : str, optional - MAPDL jobname. Defaults to ``'file'``. - - nproc : int, optional - Number of processors. Defaults to 2. - - ram : float, optional - Total size in megabytes of the workspace (memory) used for the initial allocation. - The default is :class:`None`, in which case 2 GB (2048 MB) is used. To force a fixed size - throughout the run, specify a negative number. - - port : int - Port to launch MAPDL gRPC on. Final port will be the first - port available after (or including) this port. - - additional_switches : str, optional - Additional switches for MAPDL, for example ``"-p aa_r"``, the - academic research license, would be added with: - - - ``additional_switches="-p aa_r"`` - - Avoid adding switches like ``"-i"`` ``"-o"`` or ``"-b"`` as - these are already included to start up the MAPDL server. See - the notes section for additional details. - - - Returns - ------- - list[str] - Command - - """ - cpu_sw = "-np %d" % nproc - - if ram: - ram_sw = "-m %d" % int(1024 * ram) - LOG.debug(f"Setting RAM: {ram_sw}") - else: - ram_sw = "" - - job_sw = "-j %s" % jobname - port_sw = "-port %d" % port - grpc_sw = "-grpc" - - # Windows will spawn a new window, special treatment - if os.name == "nt": - exec_file = f"{exec_file}" - # must start in batch mode on windows to hide APDL window - tmp_inp = ".__tmp__.inp" - command_parm = [ - job_sw, - cpu_sw, - ram_sw, - "-b", - "-i", - tmp_inp, - "-o", - ".__tmp__.out", - additional_switches, - port_sw, - grpc_sw, - ] - - else: # linux - command_parm = [ - job_sw, - cpu_sw, - ram_sw, - additional_switches, - port_sw, - grpc_sw, - ] - - command_parm = [ - each for each in command_parm if each.strip() - ] # cleaning empty args. - - # removing spaces in cells - command_parm = " ".join(command_parm).split(" ") - command_parm.insert(0, f"{exec_file}") - - LOG.debug(f"Generated command: {' '.join(command_parm)}") - return command_parm - - -def launch_grpc( - cmd: list[str], - run_location: str = None, - env_vars: Optional[Dict[str, str]] = None, - launch_on_hpc: bool = False, - mapdl_output: Optional[str] = None, -) -> subprocess.Popen: - """Start MAPDL locally in gRPC mode. - - Parameters - ---------- - cmd : str - Command to use to launch the MAPDL instance. - - run_location : str, optional - MAPDL working directory. The default is the temporary working - directory. - - env_vars : dict, optional - Dictionary with the environment variables to inject in the process. - - launch_on_hpc : bool, optional - If running on an HPC, this needs to be :class:`True` to avoid the - temporary file creation on Windows. - - mapdl_output : str, optional - Whether redirect MAPDL console output (stdout and stderr) to a file. - - Returns - ------- - subprocess.Popen - Process object - """ - if env_vars is None: - env_vars = {} - - # disable all MAPDL pop-up errors: - env_vars.setdefault("ANS_CMD_NODIAG", "TRUE") - - cmd_string = " ".join(cmd) - if "sbatch" in cmd: - header = "Running an MAPDL instance on the Cluster:" - shell = os.name != "nt" - cmd_ = cmd_string - else: - header = "Running an MAPDL instance" - shell = False # To prevent shell injection - cmd_ = cmd - - LOG.info( - "\n============" - "\n============\n" - f"{header}\nLocation:\n{run_location}\n" - f"Command:\n{cmd_string}\n" - f"Env vars:\n{env_vars}" - "\n============" - "\n============" - ) - - if mapdl_output: - stdout = open(str(mapdl_output), "wb", 0) - stderr = subprocess.STDOUT - else: - stdout = subprocess.PIPE - stderr = subprocess.PIPE - - if os.name == "nt": - # getting tmp file name - if not launch_on_hpc: - # if we are running on an HPC cluster (case not considered), we will - # have to upload/create this file because it is needed for starting. - tmp_inp = cmd[cmd.index("-i") + 1] - with open(os.path.join(run_location, tmp_inp), "w") as f: - f.write("FINISH\r\n") - LOG.debug( - f"Writing temporary input file: {tmp_inp} with 'FINISH' command." - ) - - LOG.debug("MAPDL starting in background.") - return submitter( - cmd_, - shell=shell, # sbatch does not work without shell. - cwd=run_location, - stdin=subprocess.DEVNULL, - stdout=stdout, - stderr=stderr, - env_vars=env_vars, - ) - - -def check_mapdl_launch( - process: subprocess.Popen, run_location: str, timeout: int, cmd: str -) -> None: - """Check MAPDL launching process. - - Check several things to confirm MAPDL has been launched: - - * MAPDL process: - Check process is alive still. - * File error: - Check if error file has been created. - * [On linux, but not WSL] Check if server is alive. - Read stdout looking for 'Server listening on' string. - - Parameters - ---------- - process : subprocess.Popen - MAPDL process object coming from 'launch_grpc' - run_location : str - MAPDL path. - timeout : int - Timeout - cmd : str - Command line used to launch MAPDL. Just for error printing. - - Raises - ------ - MapdlDidNotStart - MAPDL did not start. - """ - LOG.debug("Generating queue object for stdout") - stdout_queue, thread = _create_queue_for_std(process.stdout) - - # Checking connection - try: - LOG.debug("Checking process is alive") - _check_process_is_alive(process, run_location) - - LOG.debug("Checking file error is created") - _check_file_error_created(run_location, timeout) - - if os.name == "posix" and not ON_WSL: - LOG.debug("Checking if gRPC server is alive.") - _check_server_is_alive(stdout_queue, timeout) - - except MapdlDidNotStart as e: # pragma: no cover - msg = ( - str(e) - + f"\nRun location: {run_location}" - + f"\nCommand line used: {' '.join(cmd)}\n\n" - ) - - terminal_output = "\n".join(_get_std_output(std_queue=stdout_queue)).strip() - if terminal_output.strip(): - msg = msg + "The full terminal output is:\n\n" + terminal_output - - raise MapdlDidNotStart(msg) from e - - def _check_process_is_alive(process, run_location): if process.poll() is not None: # pragma: no cover msg = f"MAPDL process died." @@ -687,51 +426,92 @@ def enqueue_output(out: subprocess.PIPE, queue: Queue[str]) -> None: return q, t -def launch_remote_mapdl( - version: Optional[str] = None, - cleanup_on_exit: bool = True, -) -> MapdlGrpc: - """Start MAPDL remotely using the product instance management API. +def create_gallery_instances( + args: Dict[str, Any], start_parm: Dict[str, Any] +) -> MapdlGrpc: # pragma: no cover + """Create MAPDL instances for the documentation gallery built. - When calling this method, you need to ensure that you are in an environment where PyPIM is configured. - This can be verified with :func:`pypim.is_configured `. + This function is not tested with Pytest, but it is used during CICD docs + building. Parameters ---------- - version : str, optional - The MAPDL version to run, in the 3 digits format, such as "212". - - If unspecified, the version will be chosen by the server. - - cleanup_on_exit : bool, optional - Exit MAPDL when python exits or the mapdl Python instance is - garbage collected. - - If unspecified, it will be cleaned up. + args : Dict[str, Any] + Arguments dict + start_parm : Dict[str, Any] + MAPDL start parameters Returns ------- - ansys.mapdl.core.mapdl.MapdlBase - An instance of Mapdl. + MapdlGrpc + MAPDL instance """ - if not _HAS_PIM: # pragma: no cover - raise ModuleNotFoundError( - "The package 'ansys-platform-instancemanagement' is required to use this function." + LOG.debug("Building gallery.") + # launch an instance of pymapdl if it does not already exist and + # we're allowed to start instances + if GALLERY_INSTANCE[0] is None: + LOG.debug("Loading first MAPDL instance for gallery building.") + GALLERY_INSTANCE[0] = "Loading..." + mapdl = launch_mapdl( + start_instance=True, + cleanup_on_exit=False, + loglevel=args["loglevel"], + set_no_abort=args["set_no_abort"], + **start_parm, ) + GALLERY_INSTANCE[0] = {"ip": mapdl._ip, "port": mapdl._port} + return mapdl - pim = pypim.connect() - instance = pim.create_instance(product_name="mapdl", product_version=version) - instance.wait_for_ready() - channel = instance.build_grpc_channel( - options=[ - ("grpc.max_receive_message_length", MAX_MESSAGE_LENGTH), - ] - ) - return MapdlGrpc( - channel=channel, - cleanup_on_exit=cleanup_on_exit, - remote_instance=instance, - ) + # otherwise, connect to the existing gallery instance if available, but it needs to be fully loaded. + else: + while not isinstance(GALLERY_INSTANCE[0], dict): + # Waiting for MAPDL instance to be ready + time.sleep(0.1) + + LOG.debug("Connecting to an existing MAPDL instance for gallery building.") + start_parm.pop("ip", None) + start_parm.pop("port", None) + mapdl = MapdlGrpc( + ip=GALLERY_INSTANCE[0]["ip"], + port=GALLERY_INSTANCE[0]["port"], + cleanup_on_exit=False, + loglevel=args["loglevel"], + set_no_abort=args["set_no_abort"], + use_vtk=args["use_vtk"], + **start_parm, + ) + if args["clear_on_connect"]: + mapdl.clear() + return mapdl + + +def _get_windows_host_ip(): + output = _run_ip_route() + if output: + return _parse_ip_route(output) + + +def _run_ip_route(): + try: + # args value is controlled by the library. + # ip is not a partial path - Bandit false positive + # Excluding bandit check. + p = subprocess.run(["ip", "route"], capture_output=True) # nosec B603 B607 + except Exception: + LOG.debug( + "Detecting the IP address of the host Windows machine requires being able to execute the command 'ip route'." + ) + return None + + if p and p.stdout and isinstance(p.stdout, bytes): + return p.stdout.decode() + + +def _parse_ip_route(output): + match = re.findall(r"(\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}).*", output) + + if match: + return match[0] def get_start_instance(start_instance: Optional[Union[bool, str]] = None) -> bool: @@ -825,6 +605,8 @@ def get_default_ansys(): >>> get_default_ansys() (/usr/ansys_inc/v211/ansys/bin/ansys211, 21.1) """ + from ansys.tools.path import find_mapdl + return find_mapdl(supported_versions=SUPPORTED_ANSYS_VERSIONS) @@ -880,6 +662,8 @@ def get_default_ansys_version(): def check_valid_ansys(): """Checks if a valid version of ANSYS is installed and preconfigured""" + from ansys.mapdl.core.launcher import get_mapdl_path, version_from_path + ansys_bin = get_mapdl_path(allow_input=False) if ansys_bin is not None: version = version_from_path("mapdl", ansys_bin) @@ -1017,656 +801,164 @@ def force_smp_in_student(add_sw, exec_path): return add_sw -def launch_mapdl( - exec_file: Optional[str] = None, - run_location: Optional[str] = None, - jobname: str = "file", - *, - nproc: Optional[int] = None, - ram: Optional[Union[int, str]] = None, - mode: Optional[str] = None, - override: bool = False, - loglevel: str = "ERROR", - additional_switches: str = "", - start_timeout: Optional[int] = None, - port: Optional[int] = None, - cleanup_on_exit: bool = True, - start_instance: Optional[bool] = None, - ip: Optional[str] = None, - clear_on_connect: bool = True, - log_apdl: Optional[Union[bool, str]] = None, - remove_temp_dir_on_exit: bool = False, - license_server_check: bool = False, - license_type: Optional[bool] = None, - print_com: bool = False, - add_env_vars: Optional[Dict[str, str]] = None, - replace_env_vars: Optional[Dict[str, str]] = None, - version: Optional[Union[int, str]] = None, - running_on_hpc: bool = True, - launch_on_hpc: bool = False, - mapdl_output: Optional[str] = None, - **kwargs: Dict[str, Any], -) -> Union[MapdlGrpc, "MapdlConsole"]: - """Start MAPDL locally. - - Parameters - ---------- - exec_file : str, optional - The location of the MAPDL executable. Will use the cached - location when left at the default :class:`None` and no environment - variable is set. - - The executable path can be also set through the environment variable - :envvar:`PYMAPDL_MAPDL_EXEC`. For example: - - .. code:: console - - export PYMAPDL_MAPDL_EXEC=/ansys_inc/v211/ansys/bin/mapdl - - run_location : str, optional - MAPDL working directory. Defaults to a temporary working - directory. If directory doesn't exist, one is created. - - jobname : str, optional - MAPDL jobname. Defaults to ``'file'``. - - nproc : int, optional - Number of processors. Defaults to ``2``. If running on an HPC cluster, - this value is adjusted to the number of CPUs allocated to the job, - unless the argument ``running_on_hpc`` is set to ``"false"``. - - ram : float, optional - Total size in megabytes of the workspace (memory) used for the initial - allocation. The default is :class:`None`, in which case 2 GB (2048 MB) is - used. To force a fixed size throughout the run, specify a negative - number. - - mode : str, optional - Mode to launch MAPDL. Must be one of the following: - - - ``'grpc'`` - - ``'console'`` - - The ``'grpc'`` mode is available on ANSYS 2021R1 or newer and - provides the best performance and stability. - The ``'console'`` mode is for legacy use only Linux only prior to 2020R2. - This console mode is pending depreciation. - Visit :ref:`versions_and_interfaces` for more information. - - override : bool, optional - Attempts to delete the lock file at the ``run_location``. - Useful when a prior MAPDL session has exited prematurely and - the lock file has not been deleted. - - loglevel : str, optional - Sets which messages are printed to the console. ``'INFO'`` - prints out all ANSYS messages, ``'WARNING'`` prints only - messages containing ANSYS warnings, and ``'ERROR'`` logs only - error messages. - - additional_switches : str, optional - Additional switches for MAPDL, for example ``'aa_r'``, the - academic research license, would be added with: - - - ``additional_switches="-aa_r"`` - - Avoid adding switches like ``-i``, ``-o`` or ``-b`` as these are already - included to start up the MAPDL server. See the notes - section for additional details. - - start_timeout : float, optional - Maximum allowable time to connect to the MAPDL server. By default it is - 45 seconds, however, it is increased to 90 seconds if running on HPC. - - port : int - Port to launch MAPDL gRPC on. Final port will be the first - port available after (or including) this port. Defaults to - ``50052``. You can also provide this value through the environment variable - :envvar:`PYMAPDL_PORT`. For instance ``PYMAPDL_PORT=50053``. - However the argument (if specified) has precedence over the environment - variable. If this environment variable is empty, it is as it is not set. - - cleanup_on_exit : bool, optional - Exit MAPDL when python exits or the mapdl Python instance is - garbage collected. - - start_instance : bool, optional - When :class:`False`, connect to an existing MAPDL instance at ``ip`` - and ``port``, which default to ip ``'127.0.0.1'`` at port ``50052``. - Otherwise, launch a local instance of MAPDL. You can also - provide this value through the environment variable - :envvar:`PYMAPDL_START_INSTANCE`. - However the argument (if specified) has precedence over the environment - variable. If this environment variable is empty, it is as it is not set. - - ip : str, optional - Specify the IP address of the MAPDL instance to connect to. - You can also provide a hostname as an alternative to an IP address. - Defaults to ``'127.0.0.1'``. - Used only when ``start_instance`` is :class:`False`. If this argument - is provided, and ``start_instance`` (or its correspondent environment - variable :envvar:`PYMAPDL_START_INSTANCE`) is :class:`True` then, an - exception is raised. - You can also provide this value through the environment variable - :envvar:`PYMAPDL_IP`. For instance ``PYMAPDL_IP=123.45.67.89``. - However the argument (if specified) has precedence over the environment - variable. If this environment variable is empty, it is as it is not set. - - clear_on_connect : bool, optional - Defaults to :class:`True`, giving you a fresh environment when - connecting to MAPDL. When if ``start_instance`` is specified - it defaults to :class:`False`. - - log_apdl : str, optional - Enables logging every APDL command to the local disk. This - can be used to "record" all the commands that are sent to - MAPDL via PyMAPDL so a script can be run within MAPDL without - PyMAPDL. This argument is the path of the output file (e.g. - ``log_apdl='pymapdl_log.txt'``). By default this is disabled. - - remove_temp_dir_on_exit : bool, optional - When ``run_location`` is :class:`None`, this launcher creates a new MAPDL - working directory within the user temporary directory, obtainable with - ``tempfile.gettempdir()``. When this parameter is - :class:`True`, this directory will be deleted when MAPDL is exited. - Default to :class:`False`. - If you change the working directory, PyMAPDL does not delete the original - working directory nor the new one. - - license_server_check : bool, optional - Check if the license server is available if MAPDL fails to - start. Only available on ``mode='grpc'``. Defaults :class:`False`. - - license_type : str, optional - Enable license type selection. You can input a string for its - license name (for example ``'meba'`` or ``'ansys'``) or its description - ("enterprise solver" or "enterprise" respectively). - You can also use legacy licenses (for example ``'aa_t_a'``) but it will - also raise a warning. If it is not used (:class:`None`), no specific - license will be requested, being up to the license server to provide a - specific license type. Default is :class:`None`. - - print_com : bool, optional - Print the command ``/COM`` arguments to the standard output. - Default :class:`False`. - - add_env_vars : dict, optional - The provided dictionary will be used to extend the MAPDL process - environment variables. If you want to control all of the environment - variables, use the argument ``replace_env_vars``. - Defaults to :class:`None`. - - replace_env_vars : dict, optional - The provided dictionary will be used to replace all the MAPDL process - environment variables. It replace the system environment variables - which otherwise would be used in the process. - To just add some environment variables to the MAPDL - process, use ``add_env_vars``. Defaults to :class:`None`. - - version : float, optional - Version of MAPDL to launch. If :class:`None`, the latest version is used. - Versions can be provided as integers (i.e. ``version=222``) or - floats (i.e. ``version=22.2``). - To retrieve the available installed versions, use the function - :meth:`ansys.tools.path.path.get_available_ansys_installations`. - You can also provide this value through the environment variable - :envvar:`PYMAPDL_MAPDL_VERSION`. - For instance ``PYMAPDL_MAPDL_VERSION=22.2``. - However the argument (if specified) has precedence over the environment - variable. If this environment variable is empty, it is as it is not set. - - running_on_hpc: bool, optional - Whether detect if PyMAPDL is running on an HPC cluster. Currently - only SLURM clusters are supported. By default, it is set to true. - This option can be bypassed if the :envvar:`PYMAPDL_RUNNING_ON_HPC` - environment variable is set to :class:`True`. - For more information, see :ref:`ref_hpc_slurm`. - - launch_on_hpc : bool, Optional - If :class:`True`, it uses the implemented scheduler (SLURM only) to launch - an MAPDL instance on the HPC. In this case you can pass the - '`scheduler_options`' argument to - :func:`launch_mapdl() ` - to specify the scheduler arguments as a string or as a dictionary. - For more information, see :ref:`ref_hpc_slurm`. - - mapdl_output : str, optional - Redirect the MAPDL console output to a given file. - - kwargs : dict, Optional - These keyword arguments are interface-specific or for - development purposes. For more information, see Notes. - - scheduler_options : :class:`str`, :class:`dict` - Use it to specify options to the scheduler run command. It can be a - string or a dictionary with arguments and its values (both as strings). - For more information visit :ref:`ref_hpc_slurm`. - - set_no_abort : :class:`bool` - *(Development use only)* - Sets MAPDL to not abort at the first error within /BATCH mode. - Defaults to :class:`True`. - - force_intel : :class:`bool` - *(Development use only)* - Forces the use of Intel message pass interface (MPI) in versions between - Ansys 2021R0 and 2022R2, where because of VPNs issues this MPI is - deactivated by default. - See :ref:`vpn_issues_troubleshooting` for more information. - Defaults to :class:`False`. - - Returns - ------- - Union[MapdlGrpc, MapdlConsole] - An instance of Mapdl. Type depends on the selected ``mode``. - - Notes - ----- - - **Ansys Student Version** - - If an Ansys Student version is detected, PyMAPDL will launch MAPDL in - shared-memory parallelism (SMP) mode unless another option is specified. - - **Additional switches** - - These are the MAPDL switch options as of 2020R2 applicable for - running MAPDL as a service via gRPC. Excluded switches not applicable or - are set via keyword arguments such as ``"-j"`` . - - \\-acc - Enables the use of GPU hardware. See GPU - Accelerator Capability in the Parallel Processing Guide for more - information. - - \\-amfg - Enables the additive manufacturing capability. Requires - an additive manufacturing license. For general information about - this feature, see AM Process Simulation in ANSYS Workbench. - - \\-ansexe - Activates a custom mechanical APDL executable. - In the ANSYS Workbench environment, activates a custom - Mechanical APDL executable. - - \\-custom - Calls a custom Mechanical APDL executable - See Running Your Custom Executable in the Programmer's Reference - for more information. - - \\-db value - Initial memory allocation - Defines the portion of workspace (memory) to be used as the - initial allocation for the database. The default is 1024 - MB. Specify a negative number to force a fixed size throughout - the run; useful on small memory systems. - - \\-dis - Enables Distributed ANSYS - See the Parallel Processing Guide for more information. - - \\-dvt - Enables ANSYS DesignXplorer advanced task (add-on). - Requires DesignXplorer. - - \\-l - Specifies a language file to use other than English - This option is valid only if you have a translated message file - in an appropriately named subdirectory in - ``/ansys_inc/v201/ansys/docu`` or - ``Program Files\\ANSYS\\Inc\\V201\\ANSYS\\docu`` - - \\-m - Specifies the total size of the workspace - Workspace (memory) in megabytes used for the initial - allocation. If you omit the ``-m`` option, the default is 2 GB - (2048 MB). Specify a negative number to force a fixed size - throughout the run. - - \\-machines - Specifies the distributed machines - Machines on which to run a Distributed ANSYS analysis. See - Starting Distributed ANSYS in the Parallel Processing Guide for - more information. - - \\-mpi - Specifies the type of MPI to use. - See the Parallel Processing Guide for more information. - - \\-mpifile - Specifies an existing MPI file - Specifies an existing MPI file (appfile) to be used in a - Distributed ANSYS run. See Using MPI Files in the Parallel - Processing Guide for more information. - - \\-na - Specifies the number of GPU accelerator devices - Number of GPU devices per machine or compute node when running - with the GPU accelerator feature. See GPU Accelerator Capability - in the Parallel Processing Guide for more information. - - \\-name - Defines Mechanical APDL parameters - Set mechanical APDL parameters at program start-up. The parameter - name must be at least two characters long. For details about - parameters, see the ANSYS Parametric Design Language Guide. - - \\-p - ANSYS session product - Defines the ANSYS session product that will run during the - session. For more detailed information about the ``-p`` option, - see Selecting an ANSYS Product via the Command Line. - - \\-ppf - HPC license - Specifies which HPC license to use during a parallel processing - run. See HPC Licensing in the Parallel Processing Guide for more - information. - - \\-smp - Enables shared-memory parallelism. - See the Parallel Processing Guide for more information. - - **PyPIM** - - If the environment is configured to use `PyPIM `_ - and ``start_instance`` is :class:`True`, then starting the instance will be delegated to PyPIM. - In this event, most of the options will be ignored and the server side configuration will - be used. - - Examples - -------- - Launch MAPDL using the best protocol. - - >>> from ansys.mapdl.core import launch_mapdl - >>> mapdl = launch_mapdl() - - Run MAPDL with shared memory parallel and specify the location of - the Ansys binary. - - >>> exec_file = 'C:/Program Files/ANSYS Inc/v231/ansys/bin/winx64/ANSYS231.exe' - >>> mapdl = launch_mapdl(exec_file, additional_switches='-smp') - - Connect to an existing instance of MAPDL at IP 192.168.1.30 and - port 50001. This is only available using the latest ``'grpc'`` - mode. - - >>> mapdl = launch_mapdl(start_instance=False, ip='192.168.1.30', - ... port=50001) - - Run MAPDL using the console mode (not recommended, and available only on Linux). - - >>> mapdl = launch_mapdl('/ansys_inc/v194/ansys/bin/ansys194', - ... mode='console') +def check_mapdl_launch( + process: subprocess.Popen, run_location: str, timeout: int, cmd: str +) -> None: + """Check MAPDL launching process. - Run MAPDL with additional environment variables. + Check several things to confirm MAPDL has been launched: - >>> my_env_vars = {"my_var":"true", "ANSYS_LOCK":"FALSE"} - >>> mapdl = launch_mapdl(add_env_vars=my_env_vars) + * MAPDL process: + Check process is alive still. + * File error: + Check if error file has been created. + * [On linux, but not WSL] Check if server is alive. + Read stdout looking for 'Server listening on' string. - Run MAPDL with our own set of environment variables. It replace the system - environment variables which otherwise would be used in the process. + Parameters + ---------- + process : subprocess.Popen + MAPDL process object coming from 'launch_grpc' + run_location : str + MAPDL path. + timeout : int + Timeout + cmd : str + Command line used to launch MAPDL. Just for error printing. - >>> my_env_vars = {"my_var":"true", - "ANSYS_LOCK":"FALSE", - "ANSYSLMD_LICENSE_FILE":"1055@MYSERVER"} - >>> mapdl = launch_mapdl(replace_env_vars=my_env_vars) + Raises + ------ + MapdlDidNotStart + MAPDL did not start. """ - ######################################## - # Processing arguments - # -------------------- - # - # packing arguments - args = pack_arguments(locals()) # packs args and kwargs - - check_kwargs(args) # check if passing wrong arguments - - pre_check_args(args) - - ######################################## - # PyPIM connection - # ---------------- - # Delegating to PyPIM if applicable - # - if _HAS_PIM and exec_file is None and pypim.is_configured(): - # Start MAPDL with PyPIM if the environment is configured for it - # and the user did not pass a directive on how to launch it. - LOG.info("Starting MAPDL remotely. The startup configuration will be ignored.") - - return launch_remote_mapdl( - cleanup_on_exit=args["cleanup_on_exit"], version=args["version"] - ) - - ######################################## - # SLURM settings - # -------------- - # Checking if running on SLURM HPC - # - if is_running_on_slurm(args): - LOG.info("On Slurm mode.") - - # extracting parameters - get_slurm_options(args, kwargs) - - get_start_instance_arg(args) - - get_cpus(args) - - get_ip(args) - - args["port"] = get_port(args["port"], args["start_instance"]) - - if args["start_instance"]: - ######################################## - # Local adjustments - # ----------------- - # - # Only when starting MAPDL (aka Local) - - get_exec_file(args) - - args["version"] = get_version( - args["version"], args.get("exec_file"), launch_on_hpc=args["launch_on_hpc"] - ) - - args["additional_switches"] = set_license_switch( - args["license_type"], args["additional_switches"] - ) - - env_vars: Dict[str, str] = update_env_vars( - args["add_env_vars"], args["replace_env_vars"] - ) - - get_run_location(args) - - # verify lock file does not exist - check_lock_file(args["run_location"], args["jobname"], args["override"]) - - # remove err file so we can track its creation - # (as way to check if MAPDL started or not) - remove_err_files(args["run_location"], args["jobname"]) + LOG.debug("Generating queue object for stdout") + stdout_queue, thread = _create_queue_for_std(process.stdout) - # Check for a valid connection mode - args["mode"] = check_mode(args["mode"], args["version"]) + # Checking connection + try: + LOG.debug("Checking process is alive") + _check_process_is_alive(process, run_location) - ######################################## - # Context specific launching adjustments - # -------------------------------------- - # - if args["start_instance"]: - # ON HPC: - # Assuming that if login node is ubuntu, the computation ones - # are also ubuntu. - env_vars = configure_ubuntu(env_vars) + LOG.debug("Checking file error is created") + _check_file_error_created(run_location, timeout) - # Set SMP by default if student version is used. - args["additional_switches"] = force_smp_in_student( - args["additional_switches"], args["exec_file"] - ) + if os.name == "posix" and not ON_WSL: + LOG.debug("Checking if gRPC server is alive.") + _check_server_is_alive(stdout_queue, timeout) - # Set compatible MPI - args["additional_switches"] = set_MPI_additional_switches( - args["additional_switches"], - force_intel=args["force_intel"], - version=args["version"], + except MapdlDidNotStart as e: # pragma: no cover + msg = ( + str(e) + + f"\nRun location: {run_location}" + + f"\nCommand line used: {' '.join(cmd)}\n\n" ) - LOG.debug(f"Using additional switches {args['additional_switches']}.") + terminal_output = "\n".join(_get_std_output(std_queue=stdout_queue)).strip() + if terminal_output.strip(): + msg = msg + "The full terminal output is:\n\n" + terminal_output - if args["running_on_hpc"] or args["launch_on_hpc"]: - env_vars.setdefault("ANS_MULTIPLE_NODES", "1") - env_vars.setdefault("HYDRA_BOOTSTRAP", "slurm") + raise MapdlDidNotStart(msg) from e - start_parm = generate_start_parameters(args) - # Early exit for debugging. - if args["_debug_no_launch"]: - # Early exit, just for testing - return args # type: ignore +def generate_mapdl_launch_command( + exec_file: str = "", + jobname: str = "file", + nproc: int = 2, + ram: Optional[int] = None, + port: int = MAPDL_DEFAULT_PORT, + additional_switches: str = "", +) -> list[str]: + """Generate the command line to start MAPDL in gRPC mode. - if not args["start_instance"]: - ######################################## - # Remote launching - # ---------------- - # - LOG.debug( - f"Connecting to an existing instance of MAPDL at {args['ip']}:{args['port']}" - ) - start_parm["launched"] = False + Parameters + ---------- + exec_file : str, optional + The location of the MAPDL executable. Will use the cached + location when left at the default :class:`None`. - mapdl = MapdlGrpc( - cleanup_on_exit=False, - loglevel=args["loglevel"], - set_no_abort=args["set_no_abort"], - use_vtk=args["use_vtk"], - log_apdl=args["log_apdl"], - **start_parm, - ) - if args["clear_on_connect"]: - mapdl.clear() - return mapdl + jobname : str, optional + MAPDL jobname. Defaults to ``'file'``. - ######################################## - # Sphinx docs adjustments - # ----------------------- - # - # special handling when building the gallery outside of CI. This - # creates an instance of mapdl the first time. - if pymapdl.BUILDING_GALLERY: # pragma: no cover - return create_gallery_instances(args, start_parm) - - ######################################## - # Local launching - # --------------- - # - # Check the license server - if args["license_server_check"]: - LOG.debug("Checking license server.") - lic_check = LicenseChecker(timeout=args["start_timeout"]) - lic_check.start() - - LOG.debug("Starting MAPDL") - if args["mode"] == "console": # pragma: no cover - ######################################## - # Launch MAPDL on console mode - # ---------------------------- - # - from ansys.mapdl.core.mapdl_console import MapdlConsole + nproc : int, optional + Number of processors. Defaults to 2. - start_parm = check_console_start_parameters(start_parm) - mapdl = MapdlConsole( - loglevel=args["loglevel"], - log_apdl=args["log_apdl"], - use_vtk=args["use_vtk"], - **start_parm, - ) + ram : float, optional + Total size in megabytes of the workspace (memory) used for the initial allocation. + The default is :class:`None`, in which case 2 GB (2048 MB) is used. To force a fixed size + throughout the run, specify a negative number. - elif args["mode"] == "grpc": - ######################################## - # Launch MAPDL with gRPC - # ---------------------- - # - cmd = generate_mapdl_launch_command( - exec_file=args["exec_file"], - jobname=args["jobname"], - nproc=args["nproc"], - ram=args["ram"], - port=args["port"], - additional_switches=args["additional_switches"], - ) + port : int + Port to launch MAPDL gRPC on. Final port will be the first + port available after (or including) this port. - if args["launch_on_hpc"]: - # wrapping command if on HPC - cmd = generate_sbatch_command( - cmd, scheduler_options=args.get("scheduler_options") - ) + additional_switches : str, optional + Additional switches for MAPDL, for example ``"-p aa_r"``, the + academic research license, would be added with: - try: - # - process = launch_grpc( - cmd=cmd, - run_location=args["run_location"], - env_vars=env_vars, - launch_on_hpc=args.get("launch_on_hpc"), - mapdl_output=args.get("mapdl_output"), - ) + - ``additional_switches="-p aa_r"`` - if args["launch_on_hpc"]: - start_parm["jobid"] = check_mapdl_launch_on_hpc(process, start_parm) - get_job_info(start_parm=start_parm, timeout=args["start_timeout"]) - else: - # Local mapdl launch check - check_mapdl_launch( - process, args["run_location"], args["start_timeout"], cmd - ) + Avoid adding switches like ``"-i"`` ``"-o"`` or ``"-b"`` as + these are already included to start up the MAPDL server. See + the notes section for additional details. - except Exception as exception: - LOG.error("An error occurred when launching MAPDL.") - jobid: int = start_parm.get("jobid", "Not found") + Returns + ------- + list[str] + Command - if ( - args["launch_on_hpc"] - and start_parm.get("finish_job_on_exit", True) - and jobid not in ["Not found", None] - ): + """ + cpu_sw = "-np %d" % nproc - LOG.debug(f"Killing HPC job with id: {jobid}") - kill_job(jobid) + if ram: + ram_sw = "-m %d" % int(1024 * ram) + LOG.debug(f"Setting RAM: {ram_sw}") + else: + ram_sw = "" - if args["license_server_check"]: - LOG.debug("Checking license server.") - lic_check.check() + job_sw = "-j %s" % jobname + port_sw = "-port %d" % port + grpc_sw = "-grpc" - raise exception + # Windows will spawn a new window, special treatment + if os.name == "nt": + exec_file = f"{exec_file}" + # must start in batch mode on windows to hide APDL window + tmp_inp = ".__tmp__.inp" + command_parm = [ + job_sw, + cpu_sw, + ram_sw, + "-b", + "-i", + tmp_inp, + "-o", + ".__tmp__.out", + additional_switches, + port_sw, + grpc_sw, + ] - if args["just_launch"]: - out = [args["ip"], args["port"]] - if hasattr(process, "pid"): - out += [process.pid] - return out + else: # linux + command_parm = [ + job_sw, + cpu_sw, + ram_sw, + additional_switches, + port_sw, + grpc_sw, + ] - ######################################## - # Connect to MAPDL using gRPC - # --------------------------- - # - try: - mapdl = MapdlGrpc( - cleanup_on_exit=args["cleanup_on_exit"], - loglevel=args["loglevel"], - set_no_abort=args["set_no_abort"], - remove_temp_dir_on_exit=args["remove_temp_dir_on_exit"], - log_apdl=args["log_apdl"], - process=process, - use_vtk=args["use_vtk"], - **start_parm, - ) + command_parm = [ + each for each in command_parm if each.strip() + ] # cleaning empty args. - except Exception as exception: - LOG.error("An error occurred when connecting to MAPDL.") - raise exception + # removing spaces in cells + command_parm = " ".join(command_parm).split(" ") + command_parm.insert(0, f"{exec_file}") - return mapdl + LOG.debug(f"Generated command: {' '.join(command_parm)}") + return command_parm def check_mode(mode: ALLOWABLE_MODES, version: Optional[int] = None): @@ -1808,217 +1100,51 @@ def set_license_switch(license_type, additional_switches): elif "premium" in license_type: license_type = "mech_2" - elif "pro" in license_type: - license_type = "mech_1" - - elif license_type not in ALLOWABLE_LICENSES: - allow_lics = [f"'{each}'" for each in ALLOWABLE_LICENSES] - warn_text = ( - f"The keyword argument 'license_type' value ('{license_type}') is not a recognized\n" - "license name or has been deprecated.\n" - "Still PyMAPDL will try to use it but in older MAPDL versions you might experience\n" - "problems connecting to the server.\n" - f"Recognized license names: {' '.join(allow_lics)}" - ) - warnings.warn(warn_text, UserWarning) - - additional_switches += " -p " + license_type - LOG.debug( - f"Using specified license name '{license_type}' in the 'license_type' keyword argument." - ) - - elif "-p " in additional_switches: - # There is already a license request in additional switches. - license_type = re.findall(r"-p\s+\b(\w*)", additional_switches)[ - 0 - ] # getting only the first product license. - - if license_type not in ALLOWABLE_LICENSES: - allow_lics = [f"'{each}'" for each in ALLOWABLE_LICENSES] - warn_text = ( - f"The additional switch product value ('-p {license_type}') is not a recognized\n" - "license name or has been deprecated.\n" - "Still PyMAPDL will try to use it but in older MAPDL versions you might experience\n" - "problems connecting to the server.\n" - f"Recognized license names: {' '.join(allow_lics)}" - ) - warnings.warn(warn_text, UserWarning) - LOG.warning(warn_text) - - LOG.debug( - f"Using specified license name '{license_type}' in the additional switches parameter." - ) - - elif license_type is not None: - raise TypeError("The argument 'license_type' does only accept str or None.") - - return additional_switches - - -def _get_windows_host_ip(): - output = _run_ip_route() - if output: - return _parse_ip_route(output) - - -def _run_ip_route(): - - try: - # args value is controlled by the library. - # ip is not a partial path - Bandit false positive - # Excluding bandit check. - p = subprocess.run(["ip", "route"], capture_output=True) # nosec B603 B607 - except Exception: - LOG.debug( - "Detecting the IP address of the host Windows machine requires being able to execute the command 'ip route'." - ) - return None - - if p and p.stdout and isinstance(p.stdout, bytes): - return p.stdout.decode() - - -def _parse_ip_route(output): - match = re.findall(r"(\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}).*", output) - - if match: - return match[0] - - -def get_slurm_options( - args: Dict[str, Any], - kwargs: Dict[str, Any], -) -> Dict[str, Any]: - def get_value( - variable: str, - kwargs: Dict[str, Any], - default: Optional[Union[str, int, float]] = 1, - astype: Optional[Callable[[Any], Any]] = int, - ): - value_from_env_vars = os.environ.get(variable) - value_from_kwargs = kwargs.pop(variable, None) - value = value_from_kwargs or value_from_env_vars or default - if astype and value: - return astype(value) - else: - return value - - ## Getting env vars - SLURM_NNODES = get_value("SLURM_NNODES", kwargs) - LOG.info(f"SLURM_NNODES: {SLURM_NNODES}") - # ntasks is for mpi - SLURM_NTASKS = get_value("SLURM_NTASKS", kwargs) - LOG.info(f"SLURM_NTASKS: {SLURM_NTASKS}") - # Sharing tasks across multiple nodes (DMP) - # the format of this envvar is a bit tricky. Avoiding it for the moment. - # SLURM_TASKS_PER_NODE = int( - # kwargs.pop( - # "SLURM_TASKS_PER_NODE", os.environ.get("SLURM_TASKS_PER_NODE", 1) - # ) - # ) - - # cpus-per-task is for multithreading, - # sharing tasks across multiple CPUs in same node (SMP) - SLURM_CPUS_PER_TASK = get_value("SLURM_CPUS_PER_TASK", kwargs) - LOG.info(f"SLURM_CPUS_PER_TASK: {SLURM_CPUS_PER_TASK}") - - # Set to value of the --ntasks option, if specified. See SLURM_NTASKS. - # Included for backwards compatibility. - SLURM_NPROCS = get_value("SLURM_NPROCS", kwargs) - LOG.info(f"SLURM_NPROCS: {SLURM_NPROCS}") - - # Number of CPUs allocated to the batch step. - SLURM_CPUS_ON_NODE = get_value("SLURM_CPUS_ON_NODE", kwargs) - LOG.info(f"SLURM_CPUS_ON_NODE: {SLURM_CPUS_ON_NODE}") - - SLURM_MEM_PER_NODE = get_value( - "SLURM_MEM_PER_NODE", kwargs, default="", astype=str - ).upper() - LOG.info(f"SLURM_MEM_PER_NODE: {SLURM_MEM_PER_NODE}") - - SLURM_NODELIST = get_value( - "SLURM_NODELIST", kwargs, default="", astype=None - ).lower() - LOG.info(f"SLURM_NODELIST: {SLURM_NODELIST}") - - if not args["exec_file"]: - args["exec_file"] = os.environ.get("PYMAPDL_MAPDL_EXEC") - - if not args["exec_file"]: - # We should probably make a way to find it. - # We will use the module thing - pass - LOG.info(f"Using MAPDL executable in: {args['exec_file']}") - - if not args["jobname"]: - args["jobname"] = os.environ.get("SLURM_JOB_NAME", "file") - LOG.info(f"Using jobname: {args['jobname']}") - - # Checking specific env var - if not args["nproc"]: - ## Attempt to calculate the appropriate number of cores: - # Reference: https://stackoverflow.com/a/51141287/6650211 - # I'm assuming the env var makes sense. - # - # - SLURM_CPUS_ON_NODE is a property of the cluster, not of the job. - # - options = max( - [ - # 4, # Fall back option - SLURM_CPUS_PER_TASK * SLURM_NTASKS, # (CPUs) - SLURM_NPROCS, # (CPUs) - # SLURM_NTASKS, # (tasks) Not necessary the number of CPUs, - # SLURM_NNODES * SLURM_TASKS_PER_NODE * SLURM_CPUS_PER_TASK, # (CPUs) - SLURM_CPUS_ON_NODE * SLURM_NNODES, # (cpus) - ] - ) - LOG.info(f"On SLURM number of processors options {options}") - - args["nproc"] = int(os.environ.get("PYMAPDL_NPROC", options)) + elif "pro" in license_type: + license_type = "mech_1" - LOG.info(f"Setting number of CPUs to: {args['nproc']}") + elif license_type not in ALLOWABLE_LICENSES: + allow_lics = [f"'{each}'" for each in ALLOWABLE_LICENSES] + warn_text = ( + f"The keyword argument 'license_type' value ('{license_type}') is not a recognized\n" + "license name or has been deprecated.\n" + "Still PyMAPDL will try to use it but in older MAPDL versions you might experience\n" + "problems connecting to the server.\n" + f"Recognized license names: {' '.join(allow_lics)}" + ) + warnings.warn(warn_text, UserWarning) - if not args["ram"]: - if SLURM_MEM_PER_NODE: - # RAM argument is in MB, so we need to convert - units = None - if SLURM_MEM_PER_NODE[-1].isalpha(): - units = SLURM_MEM_PER_NODE[-1] - ram = SLURM_MEM_PER_NODE[:-1] - else: - units = None - ram = SLURM_MEM_PER_NODE - - if not units: - args["ram"] = int(ram) - elif units == "T": # tera - args["ram"] = int(ram) * (2**10) ** 2 - elif units == "G": # giga - args["ram"] = int(ram) * (2**10) ** 1 - elif units == "M": # mega - args["ram"] = int(ram) - elif units == "K": # kilo - args["ram"] = int(ram) * (2**10) ** (-1) - else: # Mega - raise ValueError( - "The memory defined in 'SLURM_MEM_PER_NODE' env var(" - f"'{SLURM_MEM_PER_NODE}') is not valid." - ) + additional_switches += " -p " + license_type + LOG.debug( + f"Using specified license name '{license_type}' in the 'license_type' keyword argument." + ) + + elif "-p " in additional_switches: + # There is already a license request in additional switches. + license_type = re.findall(r"-p\s+\b(\w*)", additional_switches)[ + 0 + ] # getting only the first product license. - LOG.info(f"Setting RAM to: {args['ram']}") + if license_type not in ALLOWABLE_LICENSES: + allow_lics = [f"'{each}'" for each in ALLOWABLE_LICENSES] + warn_text = ( + f"The additional switch product value ('-p {license_type}') is not a recognized\n" + "license name or has been deprecated.\n" + "Still PyMAPDL will try to use it but in older MAPDL versions you might experience\n" + "problems connecting to the server.\n" + f"Recognized license names: {' '.join(allow_lics)}" + ) + warnings.warn(warn_text, UserWarning) + LOG.warning(warn_text) - # We use "-dis " (with space) to avoid collision with user variables such - # as `-distro` or so - if "-dis " not in args["additional_switches"] and not args[ - "additional_switches" - ].endswith("-dis"): - args["additional_switches"] += " -dis" + LOG.debug( + f"Using specified license name '{license_type}' in the additional switches parameter." + ) - # Finally set to avoid timeouts - args["license_server_check"] = False - args["start_timeout"] = 2 * args["start_timeout"] + elif license_type is not None: + raise TypeError("The argument 'license_type' does only accept str or None.") - return args + return additional_switches def pack_arguments(locals_): @@ -2054,21 +1180,6 @@ def pack_arguments(locals_): return args -def is_running_on_slurm(args: Dict[str, Any]) -> bool: - running_on_hpc_env_var = os.environ.get("PYMAPDL_RUNNING_ON_HPC", "True") - - is_flag_false = running_on_hpc_env_var.lower() == "false" - - # Let's require the following env vars to exist to go into slurm mode. - args["running_on_hpc"] = bool( - args["running_on_hpc"] - and not is_flag_false # default is true - and os.environ.get("SLURM_JOB_NAME") - and os.environ.get("SLURM_JOB_ID") - ) - return args["running_on_hpc"] - - def generate_start_parameters(args: Dict[str, Any]) -> Dict[str, Any]: """Generate start parameters @@ -2260,6 +1371,8 @@ def get_version( if not version: # verify version if exec_file and _HAS_ATP: + from ansys.mapdl.core.launcher import version_from_path + version = version_from_path("mapdl", exec_file, launch_on_hpc=launch_on_hpc) if version and version < 202: raise VersionError( @@ -2297,65 +1410,6 @@ def get_version( return version # return a int version or none -def create_gallery_instances( - args: Dict[str, Any], start_parm: Dict[str, Any] -) -> MapdlGrpc: # pragma: no cover - """Create MAPDL instances for the documentation gallery built. - - This function is not tested with Pytest, but it is used during CICD docs - building. - - Parameters - ---------- - args : Dict[str, Any] - Arguments dict - start_parm : Dict[str, Any] - MAPDL start parameters - - Returns - ------- - MapdlGrpc - MAPDL instance - """ - LOG.debug("Building gallery.") - # launch an instance of pymapdl if it does not already exist and - # we're allowed to start instances - if GALLERY_INSTANCE[0] is None: - LOG.debug("Loading first MAPDL instance for gallery building.") - GALLERY_INSTANCE[0] = "Loading..." - mapdl = launch_mapdl( - start_instance=True, - cleanup_on_exit=False, - loglevel=args["loglevel"], - set_no_abort=args["set_no_abort"], - **start_parm, - ) - GALLERY_INSTANCE[0] = {"ip": mapdl._ip, "port": mapdl._port} - return mapdl - - # otherwise, connect to the existing gallery instance if available, but it needs to be fully loaded. - else: - while not isinstance(GALLERY_INSTANCE[0], dict): - # Waiting for MAPDL instance to be ready - time.sleep(0.1) - - LOG.debug("Connecting to an existing MAPDL instance for gallery building.") - start_parm.pop("ip", None) - start_parm.pop("port", None) - mapdl = MapdlGrpc( - ip=GALLERY_INSTANCE[0]["ip"], - port=GALLERY_INSTANCE[0]["port"], - cleanup_on_exit=False, - loglevel=args["loglevel"], - set_no_abort=args["set_no_abort"], - use_vtk=args["use_vtk"], - **start_parm, - ) - if args["clear_on_connect"]: - mapdl.clear() - return mapdl - - def get_exec_file(args: Dict[str, Any]) -> None: """Get exec file argument @@ -2388,6 +1442,8 @@ def get_exec_file(args: Dict[str, Any]) -> None: "'PYMAPDL_MAPDL_EXEC' environment variable." ) + from ansys.mapdl.core.launcher import get_mapdl_path + if args.get("_debug_no_launch", False): args["exec_file"] = "" return @@ -2488,6 +1544,8 @@ def pre_check_args(args: dict[str, Any]): raise ValueError("Cannot specify both ``exec_file`` and ``version``.") if args["launch_on_hpc"] and args["ip"]: + from ansys.mapdl.core.launcher.hpc import LAUNCH_ON_HCP_ERROR_MESSAGE_IP + raise ValueError(LAUNCH_ON_HCP_ERROR_MESSAGE_IP) # Setting timeout @@ -2561,335 +1619,3 @@ def remove_err_files(run_location, jobname): f'"{run_location}"' ) raise error - - -def launch_mapdl_on_cluster( - nproc: int, - *, - scheduler_options: Union[str, Dict[str, str]] = None, - **launch_mapdl_args: Dict[str, Any], -) -> MapdlGrpc: - """Launch MAPDL on a HPC cluster - - Launches an interactive MAPDL instance on an HPC cluster. - - Parameters - ---------- - nproc : int - Number of CPUs to be used in the simulation. - - scheduler_options : Dict[str, str], optional - A string or dictionary specifying the job configuration for the - scheduler. For example ``scheduler_options = "-N 10"``. - - Returns - ------- - MapdlGrpc - Mapdl instance running on the HPC cluster. - - Examples - -------- - Run a job with 10 nodes and 2 tasks per node: - - >>> from ansys.mapdl.core import launch_mapdl - >>> scheduler_options = {"nodes": 10, "ntasks-per-node": 2} - >>> mapdl = launch_mapdl( - launch_on_hpc=True, - nproc=20, - scheduler_options=scheduler_options - ) - - Raises - ------ - ValueError - _description_ - ValueError - _description_ - ValueError - _description_ - """ - - # Processing the arguments - launch_mapdl_args["launch_on_hpc"] = True - - if launch_mapdl_args.get("mode", "grpc") != "grpc": - raise ValueError( - "The only mode allowed for launch MAPDL on an HPC cluster is gRPC." - ) - - if launch_mapdl_args.get("ip"): - raise ValueError(LAUNCH_ON_HCP_ERROR_MESSAGE_IP) - - if not launch_mapdl_args.get("start_instance", True): - raise ValueError( - "The 'start_instance' argument must be 'True' when launching on HPC." - ) - - return launch_mapdl( - nproc=nproc, - scheduler_options=scheduler_options, - **launch_mapdl_args, - ) - - -def get_hostname_host_cluster(job_id: int, timeout: int = 30) -> str: - options = f"show jobid -dd {job_id}" - LOG.debug(f"Executing the command 'scontrol {options}'") - - ready = False - time_start = time.time() - counter = 0 - while not ready: - proc = send_scontrol(options) - - stdout = proc.stdout.read().decode() - - if "JobState=RUNNING" not in stdout: - counter += 1 - time.sleep(1) - if (counter % 3 + 1) == 0: # print every 3 seconds. Skipping the first. - LOG.debug("The job is not ready yet. Waiting...") - print("The job is not ready yet. Waiting...") - else: - ready = True - break - - # Exit by raising exception - if time.time() > time_start + timeout: - state = get_state_from_scontrol(stdout) - - # Trying to get the hostname from the last valid message - try: - host = get_hostname_from_scontrol(stdout) - if not host: - # If string is empty, go to the exception clause. - raise IndexError() - - hostname_msg = f"The BatchHost for this job is '{host}'" - except (IndexError, AttributeError): - hostname_msg = "PyMAPDL couldn't get the BatchHost hostname" - - # Raising exception - raise MapdlDidNotStart( - f"The HPC job (id: {job_id}) didn't start on time (timeout={timeout}). " - f"The job state is '{state}'. " - f"{hostname_msg}. " - "You can check more information by issuing in your console:\n" - f" scontrol show jobid -dd {job_id}" - ) - - LOG.debug(f"The 'scontrol' command returned:\n{stdout}") - batchhost = get_hostname_from_scontrol(stdout) - LOG.debug(f"Batchhost: {batchhost}") - - # we should validate - batchhost_ip = socket.gethostbyname(batchhost) - LOG.debug(f"Batchhost IP: {batchhost_ip}") - - LOG.info( - f"Job {job_id} successfully allocated and running in '{batchhost}'({batchhost_ip})" - ) - return batchhost, batchhost_ip - - -def get_jobid(stdout: str) -> int: - """Extract the jobid from a command output""" - job_id = stdout.strip().split(" ")[-1] - - try: - job_id = int(job_id) - except ValueError: - LOG.error(f"The console output does not seems to have a valid jobid:\n{stdout}") - raise ValueError("PyMAPDL could not retrieve the job id.") - - LOG.debug(f"The job id is: {job_id}") - return job_id - - -def generate_sbatch_command( - cmd: Union[str, List[str]], scheduler_options: Optional[Union[str, Dict[str, str]]] -) -> List[str]: - """Generate sbatch command for a given MAPDL launch command.""" - - def add_minus(arg: str): - if not arg: - return "" - - arg = str(arg) - - if not arg.startswith("-"): - if len(arg) == 1: - arg = f"-{arg}" - else: - arg = f"--{arg}" - elif not arg.startswith("--") and len(arg) > 2: - # missing one "-" for a long argument - arg = f"-{arg}" - - return arg - - if scheduler_options: - if isinstance(scheduler_options, dict): - scheduler_options = " ".join( - [ - f"{add_minus(key)}='{value}'" - for key, value in scheduler_options.items() - ] - ) - else: - scheduler_options = "" - - if "wrap" in scheduler_options: - raise ValueError( - "The sbatch argument 'wrap' is used by PyMAPDL to submit the job." - "Hence you cannot use it as sbatch argument." - ) - LOG.debug(f"The additional sbatch arguments are: {scheduler_options}") - - if isinstance(cmd, list): - cmd = " ".join(cmd) - - cmd = ["sbatch", scheduler_options, "--wrap", f"'{cmd}'"] - cmd = [each for each in cmd if bool(each)] - return cmd - - -def get_hostname_from_scontrol(stdout: str) -> str: - return stdout.split("BatchHost=")[1].splitlines()[0].strip() - - -def get_state_from_scontrol(stdout: str) -> str: - return stdout.split("JobState=")[1].splitlines()[0].strip() - - -def check_mapdl_launch_on_hpc( - process: subprocess.Popen, start_parm: Dict[str, str] -) -> int: - """Check if the job is ready on the HPC - - Check if the job has been successfully submitted, and additionally, it does - retrieve the BathcHost hostname which is the IP to connect to using the gRPC - interface. - - Parameters - ---------- - process : subprocess.Popen - Process used to submit the job. The stdout is read from there. - start_parm : Dict[str, str] - To store the job ID, the BatchHost hostname and IP into. - - Returns - ------- - int : - The jobID - - Raises - ------ - MapdlDidNotStart - The job submission failed. - """ - stdout = process.stdout.read().decode() - if "Submitted batch job" not in stdout: - stderr = process.stderr.read().decode() - raise MapdlDidNotStart( - "PyMAPDL failed to submit the sbatch job:\n" - f"stdout:\n{stdout}\nstderr:\n{stderr}" - ) - - jobid = get_jobid(stdout) - LOG.info(f"HPC job successfully submitted. JobID: {jobid}") - return jobid - - -def get_job_info( - start_parm: Dict[str, str], jobid: Optional[int] = None, timeout: int = 30 -): - """Get job info like BatchHost IP and hostname - - Get BatchHost hostname and ip and stores them in the start_parm argument - - Parameters - ---------- - start_parm : Dict[str, str] - Starting parameters for MAPDL. - jobid : int - Job ID - timeout : int - Timeout for checking if the job is ready. Default checks for - 'start_instance' key in the 'start_parm' argument, if none - is found, it passes :class:`None` to - :func:`ansys.mapdl.core.launcher.get_hostname_host_cluster`. - """ - timeout = timeout or start_parm.get("start_instance") - - jobid = jobid or start_parm["jobid"] - - batch_host, batch_ip = get_hostname_host_cluster(jobid, timeout=timeout) - - start_parm["ip"] = batch_ip - start_parm["hostname"] = batch_host - start_parm["jobid"] = jobid - - -def kill_job(jobid: int): - """Kill SLURM job""" - submitter(["scancel", str(jobid)]) - - -def send_scontrol(args: str): - cmd = f"scontrol {args}".split(" ") - return submitter(cmd) - - -def submitter( - cmd: Union[str, List[str]], - *, - executable: str = None, - shell: bool = False, - cwd: str = None, - stdin: subprocess.PIPE = None, - stdout: subprocess.PIPE = None, - stderr: subprocess.PIPE = None, - env_vars: dict[str, str] = None, -): - - if executable: - if isinstance(cmd, list): - cmd = [executable] + cmd - else: - cmd = [executable, cmd] - - if not stdin: - stdin = subprocess.DEVNULL - if not stdout: - stdout = subprocess.PIPE - if not stderr: - stderr = subprocess.PIPE - - # cmd is controlled by the library with generate_mapdl_launch_command. - # Excluding bandit check. - return subprocess.Popen( - args=cmd, - shell=shell, # sbatch does not work without shell. - cwd=cwd, - stdin=stdin, - stdout=stdout, - stderr=stderr, - env=env_vars, - ) - - -def check_console_start_parameters(start_parm): - valid_args = [ - "exec_file", - "run_location", - "jobname", - "nproc", - "additional_switches", - "start_timeout", - ] - for each in list(start_parm.keys()): - if each not in valid_args: - start_parm.pop(each) - - return start_parm diff --git a/src/ansys/mapdl/core/licensing.py b/src/ansys/mapdl/core/licensing.py index a459096cd4..b68a297c1e 100644 --- a/src/ansys/mapdl/core/licensing.py +++ b/src/ansys/mapdl/core/licensing.py @@ -37,7 +37,6 @@ if _HAS_ATP: from ansys.tools.path import get_mapdl_path, version_from_path -LOCALHOST = "127.0.0.1" LIC_PATH_ENVAR = "ANSYSLIC_DIR" LIC_FILE_ENVAR = "ANSYSLMD_LICENSE_FILE" APP_NAME = "FEAT_ANSYS" # TODO: We need to make sure this is the type of feature we need to checkout. diff --git a/src/ansys/mapdl/core/mapdl_grpc.py b/src/ansys/mapdl/core/mapdl_grpc.py index 8f44fe4a66..30ebe31789 100644 --- a/src/ansys/mapdl/core/mapdl_grpc.py +++ b/src/ansys/mapdl/core/mapdl_grpc.py @@ -145,7 +145,7 @@ def get_start_instance(*args, **kwargs) -> bool: """Wraps get_start_instance to avoid circular imports""" - from ansys.mapdl.core.launcher import get_start_instance + from ansys.mapdl.core.launcher.tools import get_start_instance return get_start_instance(*args, **kwargs) @@ -523,7 +523,7 @@ def _before_run(self, _command: str) -> None: self._create_session() def _create_process_stds_queue(self, process=None): - from ansys.mapdl.core.launcher import ( + from ansys.mapdl.core.launcher.tools import ( _create_queue_for_std, # Avoid circular import error ) @@ -890,7 +890,8 @@ def _launch(self, start_parm, timeout=10): raise MapdlRuntimeError( "Can only launch the GUI with a local instance of MAPDL" ) - from ansys.mapdl.core.launcher import generate_mapdl_launch_command, launch_grpc + from ansys.mapdl.core.launcher import launch_grpc + from ansys.mapdl.core.launcher.tools import generate_mapdl_launch_command self._exited = False # reset exit state diff --git a/src/ansys/mapdl/core/misc.py b/src/ansys/mapdl/core/misc.py index 63b2e35b61..aa5e0f6a02 100644 --- a/src/ansys/mapdl/core/misc.py +++ b/src/ansys/mapdl/core/misc.py @@ -121,7 +121,7 @@ def check_has_mapdl() -> bool: True when this local installation has ANSYS installed in a standard location. """ - from ansys.mapdl.core.launcher import check_valid_ansys + from ansys.mapdl.core.launcher.tools import check_valid_ansys try: return check_valid_ansys() diff --git a/src/ansys/mapdl/core/pool.py b/src/ansys/mapdl/core/pool.py index 963e5bcf06..4be725d891 100755 --- a/src/ansys/mapdl/core/pool.py +++ b/src/ansys/mapdl/core/pool.py @@ -31,9 +31,8 @@ from ansys.mapdl.core import _HAS_ATP, _HAS_TQDM, LOG, launch_mapdl from ansys.mapdl.core.errors import MapdlDidNotStart, MapdlRuntimeError, VersionError -from ansys.mapdl.core.launcher import ( - LOCALHOST, - MAPDL_DEFAULT_PORT, +from ansys.mapdl.core.launcher import LOCALHOST, MAPDL_DEFAULT_PORT +from ansys.mapdl.core.launcher.tools import ( check_valid_ip, get_start_instance, port_in_use, diff --git a/tests/common.py b/tests/common.py index 51ed2e2185..817a7c62c7 100644 --- a/tests/common.py +++ b/tests/common.py @@ -31,11 +31,11 @@ from ansys.mapdl.core import LOG, Mapdl from ansys.mapdl.core.errors import MapdlConnectionError, MapdlExitedError -from ansys.mapdl.core.launcher import ( +from ansys.mapdl.core.launcher import launch_mapdl +from ansys.mapdl.core.launcher.tools import ( _is_ubuntu, get_start_instance, is_ansys_process, - launch_mapdl, ) PROCESS_OK_STATUS = [ diff --git a/tests/conftest.py b/tests/conftest.py index 3f87e5fedd..b703f06470 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -224,7 +224,8 @@ def requires_dependency(dependency: str): from ansys.mapdl.core import Mapdl from ansys.mapdl.core.errors import MapdlExitedError, MapdlRuntimeError from ansys.mapdl.core.examples import vmfiles -from ansys.mapdl.core.launcher import get_start_instance, launch_mapdl +from ansys.mapdl.core.launcher import launch_mapdl +from ansys.mapdl.core.launcher.tools import get_start_instance from ansys.mapdl.core.mapdl_core import VALID_DEVICES if has_dependency("ansys-tools-visualization_interface"): @@ -679,13 +680,6 @@ def _patch_method(method): ] _meth_patch_MAPDL = _meth_patch_MAPDL_launch.copy() -_meth_patch_MAPDL.extend( - [ - # launcher methods - ("ansys.mapdl.core.launcher.launch_grpc", _returns(None)), - ("ansys.mapdl.core.launcher.check_mapdl_launch", _returns(None)), - ] -) # For testing # Patch some of the starting procedures diff --git a/tests/test_launcher.py b/tests/test_launcher.py index c1de62b542..9558e52909 100644 --- a/tests/test_launcher.py +++ b/tests/test_launcher.py @@ -34,40 +34,41 @@ import pytest from ansys.mapdl import core as pymapdl +from ansys.mapdl.core import _HAS_ATP from ansys.mapdl.core.errors import ( MapdlDidNotStart, NotEnoughResources, PortAlreadyInUseByAnMAPDLInstance, VersionError, ) -from ansys.mapdl.core.launcher import ( - _HAS_ATP, - LOCALHOST, +from ansys.mapdl.core.launcher import LOCALHOST, launch_mapdl +from ansys.mapdl.core.launcher.grpc import launch_grpc +from ansys.mapdl.core.launcher.hpc import ( + check_mapdl_launch_on_hpc, + generate_sbatch_command, + get_hostname_host_cluster, + get_jobid, + get_slurm_options, + is_running_on_slurm, + kill_job, + launch_mapdl_on_cluster, + send_scontrol, +) +from ansys.mapdl.core.launcher.tools import ( _is_ubuntu, _parse_ip_route, - check_mapdl_launch_on_hpc, check_mode, force_smp_in_student, generate_mapdl_launch_command, - generate_sbatch_command, generate_start_parameters, get_cpus, get_exec_file, - get_hostname_host_cluster, get_ip, - get_jobid, get_port, get_run_location, - get_slurm_options, get_start_instance, get_version, - is_running_on_slurm, - kill_job, - launch_grpc, - launch_mapdl, - launch_mapdl_on_cluster, remove_err_files, - send_scontrol, set_license_switch, set_MPI_additional_switches, submitter, @@ -216,8 +217,10 @@ def test_find_mapdl_linux(my_fs, path, version, raises): @requires("ansys-tools-path") @patch("psutil.cpu_count", lambda *args, **kwargs: 2) -@patch("ansys.mapdl.core.launcher._is_ubuntu", lambda *args, **kwargs: True) -@patch("ansys.mapdl.core.launcher.get_process_at_port", lambda *args, **kwargs: None) +@patch("ansys.mapdl.core.launcher.tools._is_ubuntu", lambda *args, **kwargs: True) +@patch( + "ansys.mapdl.core.launcher.tools.get_process_at_port", lambda *args, **kwargs: None +) def test_invalid_mode(mapdl, my_fs, cleared, monkeypatch): monkeypatch.delenv("PYMAPDL_START_INSTANCE", False) monkeypatch.delenv("PYMAPDL_IP", False) @@ -234,8 +237,10 @@ def test_invalid_mode(mapdl, my_fs, cleared, monkeypatch): @requires("ansys-tools-path") @pytest.mark.parametrize("version", [120, 170, 190]) @patch("psutil.cpu_count", lambda *args, **kwargs: 2) -@patch("ansys.mapdl.core.launcher._is_ubuntu", lambda *args, **kwargs: True) -@patch("ansys.mapdl.core.launcher.get_process_at_port", lambda *args, **kwargs: None) +@patch("ansys.mapdl.core.launcher.tools._is_ubuntu", lambda *args, **kwargs: True) +@patch( + "ansys.mapdl.core.launcher.tools.get_process_at_port", lambda *args, **kwargs: None +) def test_old_version_not_version(mapdl, my_fs, cleared, monkeypatch, version): monkeypatch.delenv("PYMAPDL_START_INSTANCE", False) monkeypatch.delenv("PYMAPDL_IP", False) @@ -259,8 +264,10 @@ def test_old_version_not_version(mapdl, my_fs, cleared, monkeypatch, version): @requires("ansys-tools-path") @pytest.mark.parametrize("version", [203, 213, 351]) @patch("psutil.cpu_count", lambda *args, **kwargs: 2) -@patch("ansys.mapdl.core.launcher._is_ubuntu", lambda *args, **kwargs: True) -@patch("ansys.mapdl.core.launcher.get_process_at_port", lambda *args, **kwargs: None) +@patch("ansys.mapdl.core.launcher.tools._is_ubuntu", lambda *args, **kwargs: True) +@patch( + "ansys.mapdl.core.launcher.tools.get_process_at_port", lambda *args, **kwargs: None +) def test_not_valid_versions(mapdl, my_fs, cleared, monkeypatch, version): monkeypatch.delenv("PYMAPDL_START_INSTANCE", False) monkeypatch.delenv("PYMAPDL_IP", False) @@ -1110,7 +1117,7 @@ def test_generate_start_parameters_console(): assert "timeout" not in new_args -@patch("ansys.mapdl.core.launcher._HAS_ATP", False) +@patch("ansys.mapdl.core.launcher.tools._HAS_ATP", False) def test_get_exec_file(monkeypatch): monkeypatch.delenv("PYMAPDL_MAPDL_EXEC", False) @@ -1370,7 +1377,7 @@ def myfakegethostbynameIP(*args, **kwargs): ], ) @patch("socket.gethostbyname", myfakegethostbynameIP) -@patch("ansys.mapdl.core.launcher.get_hostname_host_cluster", myfakegethostbyname) +@patch("ansys.mapdl.core.launcher.hpc.get_hostname_host_cluster", myfakegethostbyname) def test_check_mapdl_launch_on_hpc(message_stdout, message_stderr): process = get_fake_process(message_stdout, message_stderr) @@ -1423,9 +1430,9 @@ def test_exit_job(mock_popen, mapdl, cleared): ) @patch("ansys.tools.path.path._mapdl_version_from_path", lambda *args, **kwargs: 242) @stack(*PATCH_MAPDL_START) -@patch("ansys.mapdl.core.launcher.launch_grpc") +@patch("ansys.mapdl.core.launcher.launcher.launch_grpc") @patch("ansys.mapdl.core.mapdl_grpc.MapdlGrpc.kill_job") -@patch("ansys.mapdl.core.launcher.send_scontrol") +@patch("ansys.mapdl.core.launcher.hpc.send_scontrol") def test_launch_on_hpc_found_ansys(mck_ssctrl, mck_del, mck_launch_grpc, monkeypatch): monkeypatch.delenv("PYMAPDL_START_INSTANCE", False) @@ -1460,8 +1467,8 @@ def test_launch_on_hpc_found_ansys(mck_ssctrl, mck_del, mck_launch_grpc, monkeyp @stack(*PATCH_MAPDL_START) @patch("ansys.mapdl.core.mapdl_grpc.MapdlGrpc.kill_job") -@patch("ansys.mapdl.core.launcher.launch_grpc") -@patch("ansys.mapdl.core.launcher.send_scontrol") +@patch("ansys.mapdl.core.launcher.launcher.launch_grpc") +@patch("ansys.mapdl.core.launcher.hpc.send_scontrol") def test_launch_on_hpc_not_found_ansys(mck_sc, mck_lgrpc, mck_kj, monkeypatch): monkeypatch.delenv("PYMAPDL_START_INSTANCE", False) exec_file = "path/to/mapdl/v242/executable/ansys242" @@ -1511,8 +1518,8 @@ def test_launch_on_hpc_exception_launch_mapdl(monkeypatch): process = get_fake_process("ERROR") - with patch("ansys.mapdl.core.launcher.launch_grpc") as mock_launch_grpc: - with patch("ansys.mapdl.core.launcher.kill_job") as mock_popen: + with patch("ansys.mapdl.core.launcher.launcher.launch_grpc") as mock_launch_grpc: + with patch("ansys.mapdl.core.launcher.hpc.kill_job") as mock_popen: mock_launch_grpc.return_value = process @@ -1552,9 +1559,9 @@ def raise_exception(*args, **kwargs): process_scontrol = get_fake_process("Submitted batch job 1001") process_scontrol.stdout.read = raise_exception - with patch("ansys.mapdl.core.launcher.launch_grpc") as mock_launch_grpc: - with patch("ansys.mapdl.core.launcher.send_scontrol") as mock_scontrol: - with patch("ansys.mapdl.core.launcher.kill_job") as mock_kill_job: + with patch("ansys.mapdl.core.launcher.launcher.launch_grpc") as mock_launch_grpc: + with patch("ansys.mapdl.core.launcher.hpc.send_scontrol") as mock_scontrol: + with patch("ansys.mapdl.core.launcher.launcher.kill_job") as mock_kill_job: mock_launch_grpc.return_value = process_launch_grpc mock_scontrol.return_value = process_scontrol @@ -1680,7 +1687,9 @@ def test_get_port(monkeypatch, port, port_envvar, start_instance, port_busy, res else: side_effect = [False] - context = patch("ansys.mapdl.core.launcher.port_in_use", side_effect=side_effect) + context = patch( + "ansys.mapdl.core.launcher.tools.port_in_use", side_effect=side_effect + ) with context: assert get_port(port, start_instance) == result @@ -1729,7 +1738,7 @@ def fake_proc(*args, **kwargs): time_to_stop, ) - with patch("ansys.mapdl.core.launcher.send_scontrol", fake_proc) as mck_sc: + with patch("ansys.mapdl.core.launcher.hpc.send_scontrol", fake_proc) as mck_sc: if raises: context = pytest.raises(raises) @@ -1875,7 +1884,7 @@ def test_check_mode(mode, version, osname, context, res): @pytest.mark.parametrize("jobid", [1001, 2002]) @patch("subprocess.Popen", lambda *args, **kwargs: None) def test_kill_job(jobid): - with patch("ansys.mapdl.core.launcher.submitter") as mck_sub: + with patch("ansys.mapdl.core.launcher.hpc.submitter") as mck_sub: assert kill_job(jobid) is None mck_sub.assert_called_once() arg = mck_sub.call_args_list[0][0][0] @@ -1884,13 +1893,13 @@ def test_kill_job(jobid): @pytest.mark.parametrize("jobid", [1001, 2002]) -@patch( - "ansys.mapdl.core.launcher.submitter", lambda *args, **kwargs: kwargs -) # return command def test_send_scontrol(jobid): - with patch("ansys.mapdl.core.launcher.submitter") as mck_sub: + with patch("ansys.mapdl.core.launcher.hpc.submitter") as mck_sub: + mck_sub.side_effect = lambda *args, **kwargs: (args, kwargs) + args = f"my args {jobid}" - assert send_scontrol(args) + ret_args = send_scontrol(args) + assert ret_args mck_sub.assert_called_once() arg = mck_sub.call_args_list[0][0][0] @@ -1978,6 +1987,11 @@ def return_everything(*arg, **kwags): ) @patch("ansys.tools.path.path._mapdl_version_from_path", lambda *args, **kwargs: 242) @stack(*PATCH_MAPDL) +@patch("ansys.mapdl.core.launcher.launcher.launch_grpc", lambda *args, **kwargs: None) +@patch( + "ansys.mapdl.core.launcher.launcher.check_mapdl_launch", + lambda *args, **kwargs: None, +) @pytest.mark.parametrize( "arg,value,method", [ @@ -2011,14 +2025,16 @@ def raising(): raise Exception("An error") -@patch("ansys.mapdl.core.launcher.check_valid_ansys", raising) +@patch("ansys.mapdl.core.launcher.tools.check_valid_ansys", raising) def test_check_has_mapdl_failed(): assert check_has_mapdl() is False @requires("local") -@patch("ansys.mapdl.core.launcher._is_ubuntu", lambda *args, **kwargs: True) -@patch("ansys.mapdl.core.launcher.check_mapdl_launch", lambda *args, **kwargs: None) +@patch("ansys.mapdl.core.launcher.tools._is_ubuntu", lambda *args, **kwargs: True) +@patch( + "ansys.mapdl.core.launcher.tools.check_mapdl_launch", lambda *args, **kwargs: None +) def test_mapdl_output_pass_arg(tmpdir): def submitter(*args, **kwargs): from _io import FileIO @@ -2029,7 +2045,7 @@ def submitter(*args, **kwargs): return - with patch("ansys.mapdl.core.launcher.submitter", submitter) as mck_sub: + with patch("ansys.mapdl.core.launcher.tools.submitter", submitter) as mck_sub: mapdl_output = os.path.join(tmpdir, "apdl.txt") args = launch_mapdl(just_launch=True, mapdl_output=mapdl_output) @@ -2056,18 +2072,18 @@ def test_mapdl_output(tmpdir): def test_check_server_is_alive_no_queue(): - from ansys.mapdl.core.launcher import _check_server_is_alive + from ansys.mapdl.core.launcher.tools import _check_server_is_alive assert _check_server_is_alive(None, 30) is None def test_get_std_output_no_queue(): - from ansys.mapdl.core.launcher import _get_std_output + from ansys.mapdl.core.launcher.tools import _get_std_output assert _get_std_output(None, 30) == [None] def test_create_queue_for_std_no_queue(): - from ansys.mapdl.core.launcher import _create_queue_for_std + from ansys.mapdl.core.launcher.tools import _create_queue_for_std assert _create_queue_for_std(None) == (None, None) diff --git a/tests/test_launcher/test_console.py b/tests/test_launcher/test_console.py new file mode 100644 index 0000000000..0af1786498 --- /dev/null +++ b/tests/test_launcher/test_console.py @@ -0,0 +1,50 @@ +# Copyright (C) 2016 - 2025 ANSYS, Inc. and/or its affiliates. +# SPDX-License-Identifier: MIT +# +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in all +# copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +# SOFTWARE. + +from ansys.mapdl.core.launcher.console import launch_mapdl_console +from conftest import requires + + +@requires("console") +def test_launch_mapdl_console(tmpdir): + filename = str(tmpdir.mkdir("tmpdir").join("tmp.inp")) + + mapdl = launch_mapdl_console(log_apdl=filename, mode="console") + + mapdl.prep7() + mapdl.run("!comment test") + mapdl.k(1, 0, 0, 0) + mapdl.k(2, 1, 0, 0) + mapdl.k(3, 1, 1, 0) + mapdl.k(4, 0, 1, 0) + + mapdl.exit() + + with open(filename, "r") as fid: + text = "".join(fid.readlines()) + + assert "PREP7" in text + assert "!comment test" in text + assert "K,1,0,0,0" in text + assert "K,2,1,0,0" in text + assert "K,3,1,1,0" in text + assert "K,4,0,1,0" in text diff --git a/tests/test_launcher/test_grpc.py b/tests/test_launcher/test_grpc.py new file mode 100644 index 0000000000..b55dfdc012 --- /dev/null +++ b/tests/test_launcher/test_grpc.py @@ -0,0 +1,21 @@ +# Copyright (C) 2016 - 2025 ANSYS, Inc. and/or its affiliates. +# SPDX-License-Identifier: MIT +# +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in all +# copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +# SOFTWARE. diff --git a/tests/test_launcher/test_hpc.py b/tests/test_launcher/test_hpc.py new file mode 100644 index 0000000000..b55dfdc012 --- /dev/null +++ b/tests/test_launcher/test_hpc.py @@ -0,0 +1,21 @@ +# Copyright (C) 2016 - 2025 ANSYS, Inc. and/or its affiliates. +# SPDX-License-Identifier: MIT +# +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in all +# copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +# SOFTWARE. diff --git a/tests/test_launcher/test_jupyter.py b/tests/test_launcher/test_jupyter.py new file mode 100644 index 0000000000..b55dfdc012 --- /dev/null +++ b/tests/test_launcher/test_jupyter.py @@ -0,0 +1,21 @@ +# Copyright (C) 2016 - 2025 ANSYS, Inc. and/or its affiliates. +# SPDX-License-Identifier: MIT +# +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in all +# copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +# SOFTWARE. diff --git a/tests/test_launcher/test_pim.py b/tests/test_launcher/test_pim.py new file mode 100644 index 0000000000..b55dfdc012 --- /dev/null +++ b/tests/test_launcher/test_pim.py @@ -0,0 +1,21 @@ +# Copyright (C) 2016 - 2025 ANSYS, Inc. and/or its affiliates. +# SPDX-License-Identifier: MIT +# +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in all +# copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +# SOFTWARE. diff --git a/tests/test_launcher/test_remote.py b/tests/test_launcher/test_remote.py new file mode 100644 index 0000000000..b55dfdc012 --- /dev/null +++ b/tests/test_launcher/test_remote.py @@ -0,0 +1,21 @@ +# Copyright (C) 2016 - 2025 ANSYS, Inc. and/or its affiliates. +# SPDX-License-Identifier: MIT +# +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in all +# copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +# SOFTWARE. From d4ce97e61838c664c66df9c0bef9411e91abb181 Mon Sep 17 00:00:00 2001 From: pyansys-ci-bot <92810346+pyansys-ci-bot@users.noreply.github.com> Date: Fri, 10 Jan 2025 11:44:32 +0000 Subject: [PATCH 02/42] chore: adding changelog file 3649.miscellaneous.md [dependabot-skip] --- doc/changelog.d/3649.miscellaneous.md | 1 + 1 file changed, 1 insertion(+) create mode 100644 doc/changelog.d/3649.miscellaneous.md diff --git a/doc/changelog.d/3649.miscellaneous.md b/doc/changelog.d/3649.miscellaneous.md new file mode 100644 index 0000000000..611ab91fb8 --- /dev/null +++ b/doc/changelog.d/3649.miscellaneous.md @@ -0,0 +1 @@ +feat: refactoring launcher module \ No newline at end of file From f57b73b2ace7bdc5bf1ff8f6ff31d70fb98a3c9c Mon Sep 17 00:00:00 2001 From: German <28149841+germa89@users.noreply.github.com> Date: Fri, 10 Jan 2025 13:14:38 +0100 Subject: [PATCH 03/42] refactor: rename files --- tests/test_launcher/{test_console.py => test_launcher_console.py} | 0 tests/test_launcher/{test_grpc.py => test_launcher_grpc.py} | 0 2 files changed, 0 insertions(+), 0 deletions(-) rename tests/test_launcher/{test_console.py => test_launcher_console.py} (100%) rename tests/test_launcher/{test_grpc.py => test_launcher_grpc.py} (100%) diff --git a/tests/test_launcher/test_console.py b/tests/test_launcher/test_launcher_console.py similarity index 100% rename from tests/test_launcher/test_console.py rename to tests/test_launcher/test_launcher_console.py diff --git a/tests/test_launcher/test_grpc.py b/tests/test_launcher/test_launcher_grpc.py similarity index 100% rename from tests/test_launcher/test_grpc.py rename to tests/test_launcher/test_launcher_grpc.py From cb2b78ef4ca034aba89670a577ab5787f02516a1 Mon Sep 17 00:00:00 2001 From: German <28149841+germa89@users.noreply.github.com> Date: Fri, 10 Jan 2025 15:33:40 +0100 Subject: [PATCH 04/42] fix: codacity suggestions --- src/ansys/mapdl/core/launcher/console.py | 2 ++ src/ansys/mapdl/core/launcher/grpc.py | 4 ++-- src/ansys/mapdl/core/launcher/hpc.py | 12 +++++++++--- src/ansys/mapdl/core/launcher/local.py | 12 ++++++------ src/ansys/mapdl/core/launcher/remote.py | 1 + src/ansys/mapdl/core/launcher/tools.py | 6 ++++-- tests/common.py | 2 +- tests/test_cli.py | 2 +- tests/test_launcher.py | 6 +++--- 9 files changed, 29 insertions(+), 18 deletions(-) diff --git a/src/ansys/mapdl/core/launcher/console.py b/src/ansys/mapdl/core/launcher/console.py index 1f9456dd96..e4a1d5bdd4 100644 --- a/src/ansys/mapdl/core/launcher/console.py +++ b/src/ansys/mapdl/core/launcher/console.py @@ -22,6 +22,8 @@ from typing import Optional, Union +from ansys.mapdl.core import LOG +from ansys.mapdl.core.launcher.local import processing_local_arguments from ansys.mapdl.core.launcher.tools import generate_start_parameters from ansys.mapdl.core.licensing import LicenseChecker from ansys.mapdl.core.mapdl_console import MapdlConsole diff --git a/src/ansys/mapdl/core/launcher/grpc.py b/src/ansys/mapdl/core/launcher/grpc.py index e3c77d8b69..65e490a98d 100644 --- a/src/ansys/mapdl/core/launcher/grpc.py +++ b/src/ansys/mapdl/core/launcher/grpc.py @@ -30,6 +30,8 @@ from ansys.mapdl.core import LOG from ansys.mapdl.core.launcher.local import processing_local_arguments from ansys.mapdl.core.launcher.tools import ( + check_mapdl_launch, + generate_mapdl_launch_command, generate_start_parameters, get_port, submitter, @@ -88,8 +90,6 @@ def launch_mapdl_grpc(): except Exception as exception: LOG.error("An error occurred when launching MAPDL.") - jobid: int = start_parm.get("jobid", "Not found") - if args["license_server_check"]: LOG.debug("Checking license server.") lic_check.check() diff --git a/src/ansys/mapdl/core/launcher/hpc.py b/src/ansys/mapdl/core/launcher/hpc.py index c192f868f9..a02a50eb68 100644 --- a/src/ansys/mapdl/core/launcher/hpc.py +++ b/src/ansys/mapdl/core/launcher/hpc.py @@ -22,14 +22,20 @@ import os import socket -import subprocess +import subprocess # nosec B404 import time from typing import Any, Callable, Dict, List, Optional, Union from ansys.mapdl.core import LOG from ansys.mapdl.core.errors import MapdlDidNotStart from ansys.mapdl.core.launcher.grpc import launch_grpc -from ansys.mapdl.core.launcher.tools import submitter +from ansys.mapdl.core.launcher.local import processing_local_arguments +from ansys.mapdl.core.launcher.tools import ( + generate_start_parameters, + get_port, + submitter, +) +from ansys.mapdl.core.licensing import LicenseChecker from ansys.mapdl.core.mapdl_grpc import MapdlGrpc LAUNCH_ON_HCP_ERROR_MESSAGE_IP = ( @@ -328,7 +334,7 @@ def launch_mapdl_on_cluster( ) -def launch_mapdl_grpc(): +def launch_mapdl_grpc(): # to be fixed args = processing_local_arguments(locals()) if args.get("mode", "grpc") != "grpc": raise ValueError("Invalid 'mode'.") diff --git a/src/ansys/mapdl/core/launcher/local.py b/src/ansys/mapdl/core/launcher/local.py index 8494ca664a..25936ae784 100644 --- a/src/ansys/mapdl/core/launcher/local.py +++ b/src/ansys/mapdl/core/launcher/local.py @@ -21,6 +21,8 @@ # SOFTWARE. """Launch MAPDL locally""" + +from ansys.mapdl.core import LOG from ansys.mapdl.core.launcher.tools import ( check_kwargs, check_lock_file, @@ -72,9 +74,7 @@ def processing_local_arguments(args): args["license_type"], args["additional_switches"] ) - env_vars: Dict[str, str] = update_env_vars( - args["add_env_vars"], args["replace_env_vars"] - ) + args["env_vars"] = update_env_vars(args["add_env_vars"], args["replace_env_vars"]) get_run_location(args) @@ -91,7 +91,7 @@ def processing_local_arguments(args): # ON HPC: # Assuming that if login node is ubuntu, the computation ones # are also ubuntu. - env_vars = configure_ubuntu(env_vars) + args["env_vars"] = configure_ubuntu(args["env_vars"]) # Set SMP by default if student version is used. args["additional_switches"] = force_smp_in_student( @@ -108,7 +108,7 @@ def processing_local_arguments(args): LOG.debug(f"Using additional switches {args['additional_switches']}.") if args["running_on_hpc"] or args["launch_on_hpc"]: - env_vars.setdefault("ANS_MULTIPLE_NODES", "1") - env_vars.setdefault("HYDRA_BOOTSTRAP", "slurm") + args["env_vars"].setdefault("ANS_MULTIPLE_NODES", "1") + args["env_vars"].setdefault("HYDRA_BOOTSTRAP", "slurm") return args diff --git a/src/ansys/mapdl/core/launcher/remote.py b/src/ansys/mapdl/core/launcher/remote.py index 27cec710cb..184a1ec2e6 100644 --- a/src/ansys/mapdl/core/launcher/remote.py +++ b/src/ansys/mapdl/core/launcher/remote.py @@ -22,6 +22,7 @@ from typing import Any, Dict, Optional, Union +from ansys.mapdl.core import LOG from ansys.mapdl.core.launcher.tools import ( check_kwargs, generate_start_parameters, diff --git a/src/ansys/mapdl/core/launcher/tools.py b/src/ansys/mapdl/core/launcher/tools.py index d7d41077fb..9a8a7504cf 100644 --- a/src/ansys/mapdl/core/launcher/tools.py +++ b/src/ansys/mapdl/core/launcher/tools.py @@ -25,7 +25,7 @@ from queue import Empty, Queue import re import socket -import subprocess +import subprocess # nosec B404 import threading import time from typing import Any, Dict, List, Optional, Tuple, Union @@ -450,6 +450,8 @@ def create_gallery_instances( # launch an instance of pymapdl if it does not already exist and # we're allowed to start instances if GALLERY_INSTANCE[0] is None: + from ansys.mapdl.core.launcher import launch_mapdl + LOG.debug("Loading first MAPDL instance for gallery building.") GALLERY_INSTANCE[0] = "Loading..." mapdl = launch_mapdl( @@ -832,7 +834,7 @@ def check_mapdl_launch( MAPDL did not start. """ LOG.debug("Generating queue object for stdout") - stdout_queue, thread = _create_queue_for_std(process.stdout) + stdout_queue, _ = _create_queue_for_std(process.stdout) # Checking connection try: diff --git a/tests/common.py b/tests/common.py index 817a7c62c7..4808f9a5d1 100644 --- a/tests/common.py +++ b/tests/common.py @@ -23,7 +23,7 @@ """Shared testing module""" from collections import namedtuple import os -import subprocess +import subprocess # nosec B404 import time from typing import Dict, List diff --git a/tests/test_cli.py b/tests/test_cli.py index a59b957038..eba3675327 100644 --- a/tests/test_cli.py +++ b/tests/test_cli.py @@ -21,7 +21,7 @@ # SOFTWARE. import re -import subprocess +import subprocess # nosec B404 import psutil import pytest diff --git a/tests/test_launcher.py b/tests/test_launcher.py index 9558e52909..4c85496591 100644 --- a/tests/test_launcher.py +++ b/tests/test_launcher.py @@ -23,7 +23,7 @@ """Test the mapdl launcher""" import os -import subprocess +import subprocess # nosec B404 import tempfile from time import sleep from unittest.mock import patch @@ -1738,7 +1738,7 @@ def fake_proc(*args, **kwargs): time_to_stop, ) - with patch("ansys.mapdl.core.launcher.hpc.send_scontrol", fake_proc) as mck_sc: + with patch("ansys.mapdl.core.launcher.hpc.send_scontrol", fake_proc): if raises: context = pytest.raises(raises) @@ -2045,7 +2045,7 @@ def submitter(*args, **kwargs): return - with patch("ansys.mapdl.core.launcher.tools.submitter", submitter) as mck_sub: + with patch("ansys.mapdl.core.launcher.tools.submitter", submitter): mapdl_output = os.path.join(tmpdir, "apdl.txt") args = launch_mapdl(just_launch=True, mapdl_output=mapdl_output) From 1feea58a49bda20e1f2aeb98b6d14328943c6787 Mon Sep 17 00:00:00 2001 From: German <28149841+germa89@users.noreply.github.com> Date: Fri, 10 Jan 2025 15:41:16 +0100 Subject: [PATCH 05/42] fix: vulnerabilities --- src/ansys/mapdl/core/launcher/grpc.py | 2 +- src/ansys/mapdl/core/launcher/tools.py | 2 +- src/ansys/mapdl/core/mapdl_grpc.py | 7 +++---- 3 files changed, 5 insertions(+), 6 deletions(-) diff --git a/src/ansys/mapdl/core/launcher/grpc.py b/src/ansys/mapdl/core/launcher/grpc.py index 65e490a98d..e36fe8de85 100644 --- a/src/ansys/mapdl/core/launcher/grpc.py +++ b/src/ansys/mapdl/core/launcher/grpc.py @@ -212,4 +212,4 @@ def launch_grpc( stdout=stdout, stderr=stderr, env_vars=env_vars, - ) + ) # nosec B604 diff --git a/src/ansys/mapdl/core/launcher/tools.py b/src/ansys/mapdl/core/launcher/tools.py index 9a8a7504cf..93214e2bf4 100644 --- a/src/ansys/mapdl/core/launcher/tools.py +++ b/src/ansys/mapdl/core/launcher/tools.py @@ -201,7 +201,7 @@ def submitter( stdout=stdout, stderr=stderr, env=env_vars, - ) + ) # nosec B604 def close_all_local_instances(port_range: range = None) -> None: diff --git a/src/ansys/mapdl/core/mapdl_grpc.py b/src/ansys/mapdl/core/mapdl_grpc.py index 30ebe31789..f9d0c8de5e 100644 --- a/src/ansys/mapdl/core/mapdl_grpc.py +++ b/src/ansys/mapdl/core/mapdl_grpc.py @@ -3805,8 +3805,7 @@ def kill_job(self, jobid: int) -> None: Job ID. """ cmd = ["scancel", f"{jobid}"] - # to ensure the job is stopped properly, let's issue the scancel twice. - subprocess.Popen(cmd) + subprocess.Popen(cmd) # nosec B603 def __del__(self): """In case the object is deleted""" @@ -3826,6 +3825,6 @@ def __del__(self): if not self._start_instance: return - except Exception as e: + except Exception as e: # nosec B110 # This is on clean up. - pass + pass # nosec B110 From d7239a35eb951ad609428fc02a9f667afdc37a27 Mon Sep 17 00:00:00 2001 From: German <28149841+germa89@users.noreply.github.com> Date: Fri, 10 Jan 2025 16:09:53 +0100 Subject: [PATCH 06/42] fix: missing import --- src/ansys/mapdl/core/mapdl_grpc.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/ansys/mapdl/core/mapdl_grpc.py b/src/ansys/mapdl/core/mapdl_grpc.py index f9d0c8de5e..683d1deca3 100644 --- a/src/ansys/mapdl/core/mapdl_grpc.py +++ b/src/ansys/mapdl/core/mapdl_grpc.py @@ -651,7 +651,7 @@ def _post_mortem_checks(self): def _read_stds(self): """Read the stdout and stderr from the subprocess.""" - from ansys.mapdl.core.launcher import ( + from ansys.mapdl.core.launcher.tools import ( _get_std_output, # Avoid circular import error ) From 02d2d61d2ab10d7c6eb10dad5cbe03591a89367c Mon Sep 17 00:00:00 2001 From: German <28149841+germa89@users.noreply.github.com> Date: Wed, 15 Jan 2025 12:08:47 +0100 Subject: [PATCH 07/42] fix: test message --- tests/test_pool.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/tests/test_pool.py b/tests/test_pool.py index 5e73940a30..2a0262b3b2 100644 --- a/tests/test_pool.py +++ b/tests/test_pool.py @@ -576,7 +576,7 @@ def test_multiple_ips(self, monkeypatch): [MAPDL_DEFAULT_PORT, MAPDL_DEFAULT_PORT + 1], NullContext(), marks=pytest.mark.xfail( - reason="Available ports cannot does not start in `MAPDL_DEFAULT_PORT`. Probably because there are other instances running already." + reason="Cannot start in `MAPDL_DEFAULT_PORT`. Probably because there are other instances running already." ), ), pytest.param( @@ -588,7 +588,7 @@ def test_multiple_ips(self, monkeypatch): [MAPDL_DEFAULT_PORT, MAPDL_DEFAULT_PORT + 1, MAPDL_DEFAULT_PORT + 2], NullContext(), marks=pytest.mark.xfail( - reason="Available ports cannot does not start in `MAPDL_DEFAULT_PORT`. Probably because there are other instances running already." + reason="Cannot start in `MAPDL_DEFAULT_PORT`. Probably because there are other instances running already." ), ), pytest.param( @@ -600,7 +600,7 @@ def test_multiple_ips(self, monkeypatch): [50053, 50053 + 1, 50053 + 2], NullContext(), marks=pytest.mark.xfail( - reason="Available ports cannot does not start in `MAPDL_DEFAULT_PORT`. Probably because there are other instances running already." + reason="Cannot start in `MAPDL_DEFAULT_PORT`. Probably because there are other instances running already." ), ), pytest.param( From df93cb3d0b6784fd0ca287ef33a06117af499a69 Mon Sep 17 00:00:00 2001 From: German <28149841+germa89@users.noreply.github.com> Date: Wed, 15 Jan 2025 11:31:13 +0000 Subject: [PATCH 08/42] ci: increase timeout for local --- .github/workflows/ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index de7f80d324..2addda71af 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -633,7 +633,7 @@ jobs: runs-on: ubuntu-22.04 if: github.ref != 'refs/heads/main' || github.event_name == 'schedule' || github.event_name == 'workflow_dispatch' needs: [smoke-tests, build-test-local-minimal-matrix] - timeout-minutes: 75 + timeout-minutes: 120 strategy: fail-fast: false matrix: ${{fromJson(needs.build-test-local-minimal-matrix.outputs.matrix)}} From 36ab642b639f8254f6c5fed9054ae1e5658c2d9a Mon Sep 17 00:00:00 2001 From: pyansys-ci-bot <92810346+pyansys-ci-bot@users.noreply.github.com> Date: Wed, 15 Jan 2025 11:33:26 +0000 Subject: [PATCH 09/42] chore: adding changelog file 3649.maintenance.md [dependabot-skip] --- doc/changelog.d/{3649.miscellaneous.md => 3649.maintenance.md} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename doc/changelog.d/{3649.miscellaneous.md => 3649.maintenance.md} (100%) diff --git a/doc/changelog.d/3649.miscellaneous.md b/doc/changelog.d/3649.maintenance.md similarity index 100% rename from doc/changelog.d/3649.miscellaneous.md rename to doc/changelog.d/3649.maintenance.md From 9bd866c37d07a2b8001934f6a270623046acfc20 Mon Sep 17 00:00:00 2001 From: German <28149841+germa89@users.noreply.github.com> Date: Thu, 16 Jan 2025 11:43:12 +0100 Subject: [PATCH 10/42] ci: adding profiling to pytest --- .ci/collect_mapdl_logs_locals.sh | 4 ++++ .ci/collect_mapdl_logs_remote.sh | 3 +++ .github/workflows/ci.yml | 2 +- pyproject.toml | 1 + tests/conftest.py | 1 + 5 files changed, 10 insertions(+), 1 deletion(-) diff --git a/.ci/collect_mapdl_logs_locals.sh b/.ci/collect_mapdl_logs_locals.sh index 5b4eff1c4a..bc97f05555 100755 --- a/.ci/collect_mapdl_logs_locals.sh +++ b/.ci/collect_mapdl_logs_locals.sh @@ -1,10 +1,14 @@ mkdir "$LOG_NAMES" && echo "Successfully generated directory $LOG_NAMES" +echo "Copying the log files..." cp *.log ./"$LOG_NAMES"/ || echo "No log files could be found" cp *apdl.out ./"$LOG_NAMES"/ || echo "No APDL log files could be found" cp *pymapdl.apdl ./"$LOG_NAMES"/ || echo "No PYMAPDL APDL log files could be found" +echo "Copying the profiling files..." +cp prof ./"$LOG_NAMES"/prof || echo "No profile files could be found" + ls -la ./"$LOG_NAMES" diff --git a/.ci/collect_mapdl_logs_remote.sh b/.ci/collect_mapdl_logs_remote.sh index 57ce2bedaf..3647de6309 100755 --- a/.ci/collect_mapdl_logs_remote.sh +++ b/.ci/collect_mapdl_logs_remote.sh @@ -35,6 +35,9 @@ echo "Copying docker launch log..." cp mapdl_launch_0.log ./"$LOG_NAMES"/mapdl_launch_0.log || echo "MAPDL launch docker log not found." cp mapdl_launch_1.log ./"$LOG_NAMES"/mapdl_launch_1.log || echo "MAPDL launch docker log not found." +echo "Copying the profiling files..." +cp prof ./"$LOG_NAMES"/prof || echo "No profile files could be found" + echo "Collecting file structure..." ls -R > ./"$LOG_NAMES"/files_structure.txt || echo "Failed to copy file structure to a file" diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 2addda71af..1648fc1ca8 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -33,7 +33,7 @@ env: DPF_PORT: 21004 MAPDL_PACKAGE: ghcr.io/ansys/mapdl ON_CI: True - PYTEST_ARGUMENTS: '-vvv -rxXsa --color=yes --durations=10 --random-order --random-order-bucket=class --maxfail=10 --reruns 3 --reruns-delay 4 --cov=ansys.mapdl.core --cov-report=html --timeout=180' + PYTEST_ARGUMENTS: '-vvv -rxXsa --color=yes --durations=30 --random-order --random-order-bucket=class --maxfail=10 --reruns 3 --reruns-delay 4 --cov=ansys.mapdl.core --cov-report=html --timeout=180 --profile-svg --profile' BUILD_CHEATSHEET: True diff --git a/pyproject.toml b/pyproject.toml index d486c5bac2..6d652c496b 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -63,6 +63,7 @@ tests = [ "pyfakefs==5.7.3", "pyiges[full]==0.3.1", "pytest-cov==6.0.0", + "pytest-profiling==1.8.1", "pytest-pyvista==0.1.9", "pytest-random-order==1.1.1", "pytest-rerunfailures==15.0", diff --git a/tests/conftest.py b/tests/conftest.py index b703f06470..48502c4eeb 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -57,6 +57,7 @@ # Setting testing environment # --------------------------- # +pytest_plugins = ["pytest_profiling"] DEBUG_TESTING = debug_testing() TESTING_MINIMAL = testing_minimal() From b2a7b0e46f5ad39fd672fb50100124a6b509b3c4 Mon Sep 17 00:00:00 2001 From: pyansys-ci-bot <92810346+pyansys-ci-bot@users.noreply.github.com> Date: Thu, 16 Jan 2025 10:44:33 +0000 Subject: [PATCH 11/42] chore: adding changelog file 3649.dependencies.md [dependabot-skip] --- doc/changelog.d/{3649.maintenance.md => 3649.dependencies.md} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename doc/changelog.d/{3649.maintenance.md => 3649.dependencies.md} (100%) diff --git a/doc/changelog.d/3649.maintenance.md b/doc/changelog.d/3649.dependencies.md similarity index 100% rename from doc/changelog.d/3649.maintenance.md rename to doc/changelog.d/3649.dependencies.md From bebd49ffeb50f014f6f82062e7eaeac9d5338a71 Mon Sep 17 00:00:00 2001 From: German <28149841+germa89@users.noreply.github.com> Date: Thu, 16 Jan 2025 12:29:20 +0100 Subject: [PATCH 12/42] fix: plugin registering --- tests/conftest.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/tests/conftest.py b/tests/conftest.py index 48502c4eeb..6cf5ba3d4c 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -57,8 +57,6 @@ # Setting testing environment # --------------------------- # -pytest_plugins = ["pytest_profiling"] - DEBUG_TESTING = debug_testing() TESTING_MINIMAL = testing_minimal() From b5106e13b2e683b179155a8f304fda89c6da40c8 Mon Sep 17 00:00:00 2001 From: German <28149841+germa89@users.noreply.github.com> Date: Thu, 16 Jan 2025 14:02:11 +0100 Subject: [PATCH 13/42] tests: added unit tests for connect_to_mapdl --- src/ansys/mapdl/core/launcher/remote.py | 59 +++++++++++++++++-------- src/ansys/mapdl/core/launcher/tools.py | 5 ++- src/ansys/mapdl/core/mapdl_core.py | 3 +- tests/test_launcher/test_remote.py | 59 +++++++++++++++++++++++++ 4 files changed, 104 insertions(+), 22 deletions(-) diff --git a/src/ansys/mapdl/core/launcher/remote.py b/src/ansys/mapdl/core/launcher/remote.py index 184a1ec2e6..2042303b2e 100644 --- a/src/ansys/mapdl/core/launcher/remote.py +++ b/src/ansys/mapdl/core/launcher/remote.py @@ -33,14 +33,50 @@ ) from ansys.mapdl.core.mapdl_grpc import MapdlGrpc +_NON_VALID_ARGS = ( + "add_env_vars", + "additional_switches", + "exec_file", + "jobname", + "launch_on_hpc", + "license_server_check", + "license_type", + "mapdl_output", + "mode", + "nproc", + "override", + "ram", + "remove_temp_dir_on_exit", + "replace_env_vars", + "run_location", + "running_on_hpc", + "start_instance", + "version", +) + + +def check_remote_args(args): + for each_arg in _NON_VALID_ARGS: + if each_arg in args: + raise ValueError( + f"'connect_to_mapdl' does not accept '{each_arg}' argument." + ) + else: + if each_arg == "mode": + args[each_arg] = "grpc" + elif each_arg == "start_instance": + args[each_arg] = False + else: + args[each_arg] = None # setting everything as None. + def connect_to_mapdl( + port: Optional[int] = None, + ip: Optional[str] = None, *, loglevel: str = "ERROR", start_timeout: Optional[int] = None, - port: Optional[int] = None, cleanup_on_exit: bool = True, - ip: Optional[str] = None, clear_on_connect: bool = True, log_apdl: Optional[Union[bool, str]] = None, print_com: bool = False, @@ -55,12 +91,7 @@ def connect_to_mapdl( check_kwargs(args) # check if passing wrong arguments - if args.get("start_instance"): - raise ValueError( - "'connect_to_mapdl' only accept 'start_instance' equals 'False'. " - "If you intend to launch locally an instance use either " - "'launch_mapdl_grpc' or the infamous 'launch_mapdl(start_instance=True)'." - ) + check_remote_args(args) pre_check_args(args) @@ -68,18 +99,8 @@ def connect_to_mapdl( args["port"] = get_port(args["port"], args["start_instance"]) - # Check for a valid connection mode - # args["mode"] = check_mode(args["mode"], args["version"]) - if args.get("mode", "grpc") != "grpc": - raise ValueError("Only a 'grpc' instance can be connected to remotely.") - start_parm = generate_start_parameters(args) - # Early exit for debugging. - if args["_debug_no_launch"]: - # Early exit, just for testing - return args # type: ignore - ######################################## # Connecting to a remote instance # ------------------------------- @@ -90,7 +111,7 @@ def connect_to_mapdl( start_parm["launched"] = False mapdl = MapdlGrpc( - cleanup_on_exit=False, + cleanup_on_exit=args["cleanup_on_exit"], loglevel=args["loglevel"], set_no_abort=args["set_no_abort"], use_vtk=args["use_vtk"], diff --git a/src/ansys/mapdl/core/launcher/tools.py b/src/ansys/mapdl/core/launcher/tools.py index 93214e2bf4..91632770f5 100644 --- a/src/ansys/mapdl/core/launcher/tools.py +++ b/src/ansys/mapdl/core/launcher/tools.py @@ -1176,8 +1176,6 @@ def pack_arguments(locals_): args["_debug_no_launch"] = locals_.get( "_debug_no_launch", locals_["kwargs"].get("_debug_no_launch", None) ) - args.setdefault("launch_on_hpc", False) - args.setdefault("ip", None) return args @@ -1565,6 +1563,9 @@ def pre_check_args(args: dict[str, Any]): "the argument 'nproc' in 'launch_mapdl'." ) + args.setdefault("launch_on_hpc", False) + args.setdefault("ip", None) + def get_cpus(args: Dict[str, Any]): """Get number of CPUs diff --git a/src/ansys/mapdl/core/mapdl_core.py b/src/ansys/mapdl/core/mapdl_core.py index 94626cf9c3..f8c97a9d44 100644 --- a/src/ansys/mapdl/core/mapdl_core.py +++ b/src/ansys/mapdl/core/mapdl_core.py @@ -169,8 +169,8 @@ _ALLOWED_START_PARM = [ "additional_switches", "check_parameter_names", + "clear_on_connect", "env_vars", - "launched", "exec_file", "finish_job_on_exit", "hostname", @@ -178,6 +178,7 @@ "jobid", "jobname", "launch_on_hpc", + "launched", "mode", "nproc", "override", diff --git a/tests/test_launcher/test_remote.py b/tests/test_launcher/test_remote.py index b55dfdc012..53dfac8d75 100644 --- a/tests/test_launcher/test_remote.py +++ b/tests/test_launcher/test_remote.py @@ -19,3 +19,62 @@ # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # SOFTWARE. +from unittest.mock import patch + +import pytest + +from ansys.mapdl.core.launcher.remote import _NON_VALID_ARGS, connect_to_mapdl + + +def test_connect_to_mapdl(): + mapdl = connect_to_mapdl() + + assert "PREP7" in mapdl.prep7() + + assert not mapdl._start_instance + assert not mapdl._launched + + mapdl.exit(force=False) + + +@pytest.mark.parametrize("arg", _NON_VALID_ARGS) +def test_connect_to_mapdl_exceptions(arg): + with pytest.raises( + ValueError, match=f"'connect_to_mapdl' does not accept '{arg}' argument." + ): + connect_to_mapdl(**{arg: True}) + + +_IP_TEST = "my_ip" + + +@pytest.mark.parametrize( + "arg,value", + ( + ("ip", _IP_TEST), + ("port", 50053), + ("loglevel", "DEBUG"), + ("loglevel", "ERROR"), + ("start_timeout", 12), + ("start_timeout", 15), + ("cleanup_on_exit", True), + ("cleanup_on_exit", False), + ("clear_on_connect", True), + ("clear_on_connect", False), + ("log_apdl", True), + ("log_apdl", False), + ("log_apdl", "log.out"), + ("print_com", False), + ), +) +@patch("socket.gethostbyname", lambda *args, **kwargs: _IP_TEST) +@patch("socket.inet_aton", lambda *args, **kwargs: _IP_TEST) +def test_connect_to_mapdl_kwargs(arg, value): + with patch("ansys.mapdl.core.launcher.remote.MapdlGrpc") as mock_mg: + args = {arg: value} + mapdl = connect_to_mapdl(**args) + + mock_mg.assert_called_once() + kw = mock_mg.call_args_list[0].kwargs + assert "ip" in kw and kw["ip"] == _IP_TEST + assert arg in kw and kw[arg] == value From ca87253cc25fe322a7564ef911ad87d7626ea920 Mon Sep 17 00:00:00 2001 From: German <28149841+germa89@users.noreply.github.com> Date: Thu, 16 Jan 2025 20:15:43 +0100 Subject: [PATCH 14/42] test: testing launch local grpc --- src/ansys/mapdl/core/launcher/grpc.py | 356 +++++++++++++++++----- src/ansys/mapdl/core/launcher/launcher.py | 2 +- src/ansys/mapdl/core/launcher/local.py | 10 +- src/ansys/mapdl/core/launcher/tools.py | 28 +- tests/test_launcher/test_launcher_grpc.py | 12 + tests/test_launcher/test_local.py | 52 ++++ tests/test_launcher/test_tools.py | 73 +++++ 7 files changed, 454 insertions(+), 79 deletions(-) create mode 100644 tests/test_launcher/test_local.py create mode 100644 tests/test_launcher/test_tools.py diff --git a/src/ansys/mapdl/core/launcher/grpc.py b/src/ansys/mapdl/core/launcher/grpc.py index e36fe8de85..336a674815 100644 --- a/src/ansys/mapdl/core/launcher/grpc.py +++ b/src/ansys/mapdl/core/launcher/grpc.py @@ -25,7 +25,7 @@ # Subprocess is needed to start the backend. But # the input is controlled by the library. Excluding bandit check. import subprocess # nosec B404 -from typing import Dict, Optional +from typing import Any, Dict, Optional, Union from ansys.mapdl.core import LOG from ansys.mapdl.core.launcher.local import processing_local_arguments @@ -40,89 +40,303 @@ from ansys.mapdl.core.mapdl_grpc import MapdlGrpc -def launch_mapdl_grpc(): +def launch_mapdl_grpc( + exec_file: Optional[str] = None, + run_location: Optional[str] = None, + jobname: str = "file", + *, + nproc: Optional[int] = None, + ram: Optional[Union[int, str]] = None, + mode: Optional[str] = None, + override: bool = False, + loglevel: str = "ERROR", + additional_switches: str = "", + start_timeout: Optional[int] = None, + port: Optional[int] = None, + cleanup_on_exit: bool = True, + start_instance: Optional[bool] = None, + clear_on_connect: bool = True, + log_apdl: Optional[Union[bool, str]] = None, + remove_temp_dir_on_exit: bool = False, + license_server_check: bool = False, + license_type: Optional[bool] = None, + print_com: bool = False, + add_env_vars: Optional[Dict[str, str]] = None, + replace_env_vars: Optional[Dict[str, str]] = None, + version: Optional[Union[int, str]] = None, + running_on_hpc: bool = True, + launch_on_hpc: bool = False, + mapdl_output: Optional[str] = None, + **kwargs: Dict[str, Any], +) -> MapdlGrpc: + """Start MAPDL locally with gRPC interface. + + Parameters + ---------- + exec_file : str, optional + The location of the MAPDL executable. Will use the cached + location when left at the default :class:`None` and no environment + variable is set. + + The executable path can be also set through the environment variable + :envvar:`PYMAPDL_MAPDL_EXEC`. For example: + + .. code:: console + + export PYMAPDL_MAPDL_EXEC=/ansys_inc/v211/ansys/bin/mapdl + + run_location : str, optional + MAPDL working directory. Defaults to a temporary working + directory. If directory doesn't exist, one is created. + + jobname : str, optional + MAPDL jobname. Defaults to ``'file'``. + + nproc : int, optional + Number of processors. Defaults to ``2``. If running on an HPC cluster, + this value is adjusted to the number of CPUs allocated to the job, + unless the argument ``running_on_hpc`` is set to ``"false"``. + + ram : float, optional + Total size in megabytes of the workspace (memory) used for the initial + allocation. The default is :class:`None`, in which case 2 GB (2048 MB) is + used. To force a fixed size throughout the run, specify a negative + number. + + mode : str, optional + Mode to launch MAPDL. Must be one of the following: + + - ``'grpc'`` + - ``'console'`` + + The ``'grpc'`` mode is available on ANSYS 2021R1 or newer and + provides the best performance and stability. + The ``'console'`` mode is for legacy use only Linux only prior to 2020R2. + This console mode is pending depreciation. + Visit :ref:`versions_and_interfaces` for more information. + + override : bool, optional + Attempts to delete the lock file at the ``run_location``. + Useful when a prior MAPDL session has exited prematurely and + the lock file has not been deleted. + + loglevel : str, optional + Sets which messages are printed to the console. ``'INFO'`` + prints out all ANSYS messages, ``'WARNING'`` prints only + messages containing ANSYS warnings, and ``'ERROR'`` logs only + error messages. + + additional_switches : str, optional + Additional switches for MAPDL, for example ``'aa_r'``, the + academic research license, would be added with: + + - ``additional_switches="-aa_r"`` + + Avoid adding switches like ``-i``, ``-o`` or ``-b`` as these are already + included to start up the MAPDL server. See the notes + section for additional details. + + start_timeout : float, optional + Maximum allowable time to connect to the MAPDL server. By default it is + 45 seconds, however, it is increased to 90 seconds if running on HPC. + + port : int + Port to launch MAPDL gRPC on. Final port will be the first + port available after (or including) this port. Defaults to + ``50052``. You can also provide this value through the environment variable + :envvar:`PYMAPDL_PORT`. For instance ``PYMAPDL_PORT=50053``. + However the argument (if specified) has precedence over the environment + variable. If this environment variable is empty, it is as it is not set. + + cleanup_on_exit : bool, optional + Exit MAPDL when python exits or the mapdl Python instance is + garbage collected. + + start_instance : bool, optional + When :class:`False`, connect to an existing MAPDL instance at ``ip`` + and ``port``, which default to ip ``'127.0.0.1'`` at port ``50052``. + Otherwise, launch a local instance of MAPDL. You can also + provide this value through the environment variable + :envvar:`PYMAPDL_START_INSTANCE`. + However the argument (if specified) has precedence over the environment + variable. If this environment variable is empty, it is as it is not set. + + clear_on_connect : bool, optional + Defaults to :class:`True`, giving you a fresh environment when + connecting to MAPDL. When if ``start_instance`` is specified + it defaults to :class:`False`. + + log_apdl : str, optional + Enables logging every APDL command to the local disk. This + can be used to "record" all the commands that are sent to + MAPDL via PyMAPDL so a script can be run within MAPDL without + PyMAPDL. This argument is the path of the output file (e.g. + ``log_apdl='pymapdl_log.txt'``). By default this is disabled. + + remove_temp_dir_on_exit : bool, optional + When ``run_location`` is :class:`None`, this launcher creates a new MAPDL + working directory within the user temporary directory, obtainable with + ``tempfile.gettempdir()``. When this parameter is + :class:`True`, this directory will be deleted when MAPDL is exited. + Default to :class:`False`. + If you change the working directory, PyMAPDL does not delete the original + working directory nor the new one. + + license_server_check : bool, optional + Check if the license server is available if MAPDL fails to + start. Only available on ``mode='grpc'``. Defaults :class:`False`. + + license_type : str, optional + Enable license type selection. You can input a string for its + license name (for example ``'meba'`` or ``'ansys'``) or its description + ("enterprise solver" or "enterprise" respectively). + You can also use legacy licenses (for example ``'aa_t_a'``) but it will + also raise a warning. If it is not used (:class:`None`), no specific + license will be requested, being up to the license server to provide a + specific license type. Default is :class:`None`. + + print_com : bool, optional + Print the command ``/COM`` arguments to the standard output. + Default :class:`False`. + + add_env_vars : dict, optional + The provided dictionary will be used to extend the MAPDL process + environment variables. If you want to control all of the environment + variables, use the argument ``replace_env_vars``. + Defaults to :class:`None`. + + replace_env_vars : dict, optional + The provided dictionary will be used to replace all the MAPDL process + environment variables. It replace the system environment variables + which otherwise would be used in the process. + To just add some environment variables to the MAPDL + process, use ``add_env_vars``. Defaults to :class:`None`. + + version : float, optional + Version of MAPDL to launch. If :class:`None`, the latest version is used. + Versions can be provided as integers (i.e. ``version=222``) or + floats (i.e. ``version=22.2``). + To retrieve the available installed versions, use the function + :meth:`ansys.tools.path.path.get_available_ansys_installations`. + You can also provide this value through the environment variable + :envvar:`PYMAPDL_MAPDL_VERSION`. + For instance ``PYMAPDL_MAPDL_VERSION=22.2``. + However the argument (if specified) has precedence over the environment + variable. If this environment variable is empty, it is as it is not set. + + running_on_hpc: bool, optional + Whether detect if PyMAPDL is running on an HPC cluster. Currently + only SLURM clusters are supported. By default, it is set to true. + This option can be bypassed if the :envvar:`PYMAPDL_RUNNING_ON_HPC` + environment variable is set to :class:`True`. + For more information, see :ref:`ref_hpc_slurm`. + + launch_on_hpc : bool, Optional + If :class:`True`, it uses the implemented scheduler (SLURM only) to launch + an MAPDL instance on the HPC. In this case you can pass the + '`scheduler_options`' argument to + :func:`launch_mapdl() ` + to specify the scheduler arguments as a string or as a dictionary. + For more information, see :ref:`ref_hpc_slurm`. + + mapdl_output : str, optional + Redirect the MAPDL console output to a given file. + + kwargs : dict, Optional + These keyword arguments are interface-specific or for + development purposes. For more information, see Notes. + + scheduler_options : :class:`str`, :class:`dict` + Use it to specify options to the scheduler run command. It can be a + string or a dictionary with arguments and its values (both as strings). + For more information visit :ref:`ref_hpc_slurm`. + + set_no_abort : :class:`bool` + *(Development use only)* + Sets MAPDL to not abort at the first error within /BATCH mode. + Defaults to :class:`True`. + + force_intel : :class:`bool` + *(Development use only)* + Forces the use of Intel message pass interface (MPI) in versions between + Ansys 2021R0 and 2022R2, where because of VPNs issues this MPI is + deactivated by default. + See :ref:`vpn_issues_troubleshooting` for more information. + Defaults to :class:`False`. + + Returns + ------- + MapdlGrpc + An instance of Mapdl. + """ args = processing_local_arguments(locals()) - if args.get("mode", "grpc") != "grpc": - raise ValueError("Invalid 'mode'.") + args["port"] = get_port(args["port"], args["start_instance"]) start_parm = generate_start_parameters(args) - # Early exit for debugging. - if args["_debug_no_launch"]: - # Early exit, just for testing - return args # type: ignore - # Check the license server if args["license_server_check"]: LOG.debug("Checking license server.") lic_check = LicenseChecker(timeout=args["start_timeout"]) lic_check.start() - ######################################## - # Launch MAPDL with gRPC - # ---------------------- - # - cmd = generate_mapdl_launch_command( - exec_file=args["exec_file"], - jobname=args["jobname"], - nproc=args["nproc"], - ram=args["ram"], - port=args["port"], - additional_switches=args["additional_switches"], + ######################################## + # Launch MAPDL with gRPC + # ---------------------- + # + cmd = generate_mapdl_launch_command( + exec_file=args["exec_file"], + jobname=args["jobname"], + nproc=args["nproc"], + ram=args["ram"], + port=args["port"], + additional_switches=args["additional_switches"], + ) + + try: + # Launching MAPDL + process = launch_grpc( + cmd=cmd, + run_location=args["run_location"], + env_vars=args["env_vars"], + launch_on_hpc=args.get("launch_on_hpc"), + mapdl_output=args.get("mapdl_output"), ) - try: - # - process = launch_grpc( - cmd=cmd, - run_location=args["run_location"], - env_vars=env_vars, - launch_on_hpc=args.get("launch_on_hpc"), - mapdl_output=args.get("mapdl_output"), - ) - - # Local mapdl launch check - check_mapdl_launch( - process, args["run_location"], args["start_timeout"], cmd - ) - - except Exception as exception: - LOG.error("An error occurred when launching MAPDL.") - - if args["license_server_check"]: - LOG.debug("Checking license server.") - lic_check.check() - - raise exception - - if args["just_launch"]: - out = [args["ip"], args["port"]] - if hasattr(process, "pid"): - out += [process.pid] - return out - - ######################################## - # Connect to MAPDL using gRPC - # --------------------------- - # - try: - mapdl = MapdlGrpc( - cleanup_on_exit=args["cleanup_on_exit"], - loglevel=args["loglevel"], - set_no_abort=args["set_no_abort"], - remove_temp_dir_on_exit=args["remove_temp_dir_on_exit"], - log_apdl=args["log_apdl"], - process=process, - use_vtk=args["use_vtk"], - **start_parm, - ) - - except Exception as exception: - LOG.error("An error occurred when connecting to MAPDL.") - raise exception - - return mapdl + # Local mapdl launch check + check_mapdl_launch(process, args["run_location"], args["start_timeout"], cmd) + + except Exception as exception: + LOG.error("An error occurred when launching MAPDL.") + + if args["license_server_check"]: + LOG.debug("Checking license server.") + lic_check.check() + + raise exception + + ######################################## + # Connect to MAPDL using gRPC + # --------------------------- + # + try: + mapdl = MapdlGrpc( + cleanup_on_exit=args["cleanup_on_exit"], + loglevel=args["loglevel"], + set_no_abort=args["set_no_abort"], + remove_temp_dir_on_exit=args["remove_temp_dir_on_exit"], + log_apdl=args["log_apdl"], + process=process, + use_vtk=args["use_vtk"], + **start_parm, + ) + + except Exception as exception: + LOG.error("An error occurred when connecting to MAPDL.") + raise exception + + return mapdl def launch_grpc( diff --git a/src/ansys/mapdl/core/launcher/launcher.py b/src/ansys/mapdl/core/launcher/launcher.py index 43cdf1b7f1..daa0d807a1 100644 --- a/src/ansys/mapdl/core/launcher/launcher.py +++ b/src/ansys/mapdl/core/launcher/launcher.py @@ -475,7 +475,7 @@ def launch_mapdl( args = pack_arguments(locals()) # packs args and kwargs - check_kwargs(args) # check if passing wrong arguments + check_kwargs(args) # check if passing wrong key arguments pre_check_args(args) diff --git a/src/ansys/mapdl/core/launcher/local.py b/src/ansys/mapdl/core/launcher/local.py index 25936ae784..850a5aa3ae 100644 --- a/src/ansys/mapdl/core/launcher/local.py +++ b/src/ansys/mapdl/core/launcher/local.py @@ -22,6 +22,8 @@ """Launch MAPDL locally""" +from typing import Any, Dict + from ansys.mapdl.core import LOG from ansys.mapdl.core.launcher.tools import ( check_kwargs, @@ -42,20 +44,22 @@ ) -def processing_local_arguments(args): +def processing_local_arguments(args_: Dict[str, Any]): # packing arguments - args = pack_arguments(locals()) # packs args and kwargs + args = pack_arguments(args_) # packs args and kwargs check_kwargs(args) # check if passing wrong arguments - if args.get("start_instance") and args["start_instance"] != False: + if "start_instance" in args and args["start_instance"] is False: raise ValueError( "'start_instance' argument is not valid." "If you intend to connect to an already started instance use either " "'connect_to_mapdl' or the infamous 'launch_mapdl(start_instance=False)'." ) + args["start_instance"] = True pre_check_args(args) + args["running_on_hpc"] = False get_cpus(args) diff --git a/src/ansys/mapdl/core/launcher/tools.py b/src/ansys/mapdl/core/launcher/tools.py index 91632770f5..e699868362 100644 --- a/src/ansys/mapdl/core/launcher/tools.py +++ b/src/ansys/mapdl/core/launcher/tools.py @@ -1521,7 +1521,7 @@ def check_kwargs(args: Dict[str, Any]): kwargs = list(args["kwargs"].keys()) # Raising error if using non-allowed arguments - for each in kwargs.copy(): + for each in args["kwargs"]: if each in _ALLOWED_START_PARM or each in ALLOWABLE_LAUNCH_MAPDL_ARGS: kwargs.remove(each) @@ -1531,6 +1531,15 @@ def check_kwargs(args: Dict[str, Any]): def pre_check_args(args: dict[str, Any]): + """Set defaults arguments if missing and check the arguments are consistent""" + + args.setdefault("start_instance", None) + args.setdefault("ip", None) + args.setdefault("on_pool", None) + args.setdefault("version", None) + args.setdefault("launch_on_hpc", None) + args.setdefault("exec_file", None) + if args["start_instance"] and args["ip"] and not args["on_pool"]: raise ValueError( "When providing a value for the argument 'ip', the argument " @@ -1549,7 +1558,7 @@ def pre_check_args(args: dict[str, Any]): raise ValueError(LAUNCH_ON_HCP_ERROR_MESSAGE_IP) # Setting timeout - if args["start_timeout"] is None: + if args.get("start_timeout", None) is None: if args["launch_on_hpc"]: args["start_timeout"] = 90 else: @@ -1563,8 +1572,19 @@ def pre_check_args(args: dict[str, Any]): "the argument 'nproc' in 'launch_mapdl'." ) + # Setting defaults + args.setdefault("mapdl_output", None) args.setdefault("launch_on_hpc", False) - args.setdefault("ip", None) + args.setdefault("running_on_hpc", None) + args.setdefault("add_env_vars", None) + args.setdefault("replace_env_vars", None) + args.setdefault("license_type", None) + args.setdefault("additional_switches", "") + args.setdefault("run_location", None) + args.setdefault("jobname", "file") + args.setdefault("override", False) + args.setdefault("mode", None) + args.setdefault("nproc", None) def get_cpus(args: Dict[str, Any]): @@ -1584,7 +1604,7 @@ def get_cpus(args: Dict[str, Any]): # Bypassing number of processors checks because VDI/VNC might have # different number of processors than the cluster compute nodes. # Also the CPUs are set in `get_slurm_options` - if args["running_on_hpc"]: + if "running_on_hpc" in args and args["running_on_hpc"]: return # Setting number of processors diff --git a/tests/test_launcher/test_launcher_grpc.py b/tests/test_launcher/test_launcher_grpc.py index b55dfdc012..5427619ed6 100644 --- a/tests/test_launcher/test_launcher_grpc.py +++ b/tests/test_launcher/test_launcher_grpc.py @@ -19,3 +19,15 @@ # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # SOFTWARE. + +from ansys.mapdl.core.launcher.grpc import launch_mapdl_grpc +from conftest import requires + + +@requires("nostudent") +def test_launch_mapdl_grpc(): + + mapdl = launch_mapdl_grpc() + + assert "PREP7" in mapdl.prep7() + mapdl.exit() diff --git a/tests/test_launcher/test_local.py b/tests/test_launcher/test_local.py new file mode 100644 index 0000000000..96a8c7f67c --- /dev/null +++ b/tests/test_launcher/test_local.py @@ -0,0 +1,52 @@ +# Copyright (C) 2016 - 2025 ANSYS, Inc. and/or its affiliates. +# SPDX-License-Identifier: MIT +# +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in all +# copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +# SOFTWARE. +from unittest.mock import patch + +import pytest + +from ansys.mapdl.core.launcher.local import processing_local_arguments + + +def test_processing_local_arguments(): + pass + + +@pytest.mark.parametrize("start_instance", [True, False, None, ""]) +@patch("ansys.mapdl.core.launcher.local.get_cpus", lambda *args, **kwargs: None) +@patch("psutil.cpu_count", lambda *args, **kwargs: 4) +def test_processing_local_arguments_start_instance(start_instance): + args = { + "exec_file": "my_path/v242/ansys/bin/ansys242", # To skip checks + "launch_on_hpc": True, # To skip checks + "kwargs": {}, + } + + if start_instance == "": + processing_local_arguments(args) + else: + args["start_instance"] = start_instance + + if start_instance is False: + with pytest.raises(ValueError): + processing_local_arguments(args) + else: + processing_local_arguments(args) diff --git a/tests/test_launcher/test_tools.py b/tests/test_launcher/test_tools.py new file mode 100644 index 0000000000..90fb1d3a09 --- /dev/null +++ b/tests/test_launcher/test_tools.py @@ -0,0 +1,73 @@ +# Copyright (C) 2016 - 2025 ANSYS, Inc. and/or its affiliates. +# SPDX-License-Identifier: MIT +# +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in all +# copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +# SOFTWARE. + +import pytest + +from ansys.mapdl.core.launcher.tools import ( + ALLOWABLE_LAUNCH_MAPDL_ARGS, + check_kwargs, + pre_check_args, +) +from ansys.mapdl.core.mapdl_core import _ALLOWED_START_PARM +from conftest import NullContext + +_ARGS_VALIDS = ALLOWABLE_LAUNCH_MAPDL_ARGS.copy() +_ARGS_VALIDS.extend(_ALLOWED_START_PARM) +_ARGS = _ARGS_VALIDS.copy() +_ARGS.extend(["asdf", "non_valid_argument"]) + + +@pytest.mark.parametrize("arg", _ARGS) +def test_check_kwargs(arg): + if arg in _ARGS_VALIDS: + context = NullContext() + else: + context = pytest.raises(ValueError) + + with context: + check_kwargs({"kwargs": {arg: None}}) + + +@pytest.mark.parametrize( + "args,match", + [ + [ + {"start_instance": True, "ip": True, "on_pool": False}, + "When providing a value for the argument 'ip', the argument", + ], + [ + {"exec_file": True, "version": True}, + "Cannot specify both ``exec_file`` and ``version``.", + ], + [ + {"scheduler_options": True}, + "PyMAPDL does not read the number of cores from the 'scheduler_options'.", + ], + [ + {"launch_on_hpc": True, "ip": "111.22.33.44"}, + "PyMAPDL cannot ensure a specific IP will be used when launching", + ], + ], +) +def test_pre_check_args(args, match): + with pytest.raises(ValueError, match=match): + pre_check_args(args) From 3a53c83174cab979d613a0cbb6aa8eae5bb92fa0 Mon Sep 17 00:00:00 2001 From: German <28149841+germa89@users.noreply.github.com> Date: Thu, 16 Jan 2025 20:16:37 +0100 Subject: [PATCH 15/42] feat: typing --- tests/conftest.py | 22 +++++++++++++++++++++- 1 file changed, 21 insertions(+), 1 deletion(-) diff --git a/tests/conftest.py b/tests/conftest.py index 6cf5ba3d4c..4d3ab96cf7 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -26,6 +26,7 @@ from pathlib import Path from shutil import get_terminal_size from sys import platform +from typing import Literal, Union from unittest.mock import patch from _pytest.terminal import TerminalReporter # for terminal customization @@ -124,8 +125,27 @@ reason="This tests does not work on student version.", ) +_REQUIRES_ARG = Union[ + Literal[ + "GRPC", + "DPF", + "LOCAL", + "REMOTE", + "CICD", + "NOCICD", + "XSERVER", + "LINUX", + "NOLINUX", + "WINDOWS", + "NOWINDOWS", + "NOSTUDENT", + "CONSOLE", + ], + str, +] + -def requires(requirement: str): +def requires(requirement: _REQUIRES_ARG): """Check requirements""" requirement = requirement.lower() From 67173317fb245a29fa4414cf690642cbb38409a4 Mon Sep 17 00:00:00 2001 From: German <28149841+germa89@users.noreply.github.com> Date: Thu, 16 Jan 2025 20:20:59 +0100 Subject: [PATCH 16/42] refactor: move pim tests to its place --- tests/test_launcher/test_pim.py | 75 ++++++++++++++++++++++++++ tests/test_launcher_remote.py | 96 --------------------------------- 2 files changed, 75 insertions(+), 96 deletions(-) delete mode 100644 tests/test_launcher_remote.py diff --git a/tests/test_launcher/test_pim.py b/tests/test_launcher/test_pim.py index b55dfdc012..fa5e937085 100644 --- a/tests/test_launcher/test_pim.py +++ b/tests/test_launcher/test_pim.py @@ -19,3 +19,78 @@ # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # SOFTWARE. + +"""Test the PyPIM integration.""" +import pytest + +from conftest import has_dependency + +if not has_dependency("ansys-platform-instancemanagement"): + pytest.skip(allow_module_level=True) + +from unittest.mock import create_autospec + +import ansys.platform.instancemanagement as pypim +import grpc + +from ansys.mapdl.core.launcher import launch_mapdl +from ansys.mapdl.core.mapdl_grpc import MAX_MESSAGE_LENGTH +from conftest import QUICK_LAUNCH_SWITCHES + + +def test_launch_remote_instance(mapdl, cleared, monkeypatch): + # Create a mock pypim pretenting it is configured and returning a channel to an already running mapdl + mock_instance = pypim.Instance( + definition_name="definitions/fake-mapdl", + name="instances/fake-mapdl", + ready=True, + status_message=None, + services={"grpc": pypim.Service(uri=mapdl._channel_str, headers={})}, + ) + pim_channel = grpc.insecure_channel( + mapdl._channel_str, + options=[ + ("grpc.max_receive_message_length", MAX_MESSAGE_LENGTH), + ], + ) + mock_instance.wait_for_ready = create_autospec(mock_instance.wait_for_ready) + mock_instance.build_grpc_channel = create_autospec( + mock_instance.build_grpc_channel, return_value=pim_channel + ) + mock_instance.delete = create_autospec(mock_instance.delete) + + mock_client = pypim.Client(channel=grpc.insecure_channel("localhost:12345")) + mock_client.create_instance = create_autospec( + mock_client.create_instance, return_value=mock_instance + ) + + mock_connect = create_autospec(pypim.connect, return_value=mock_client) + mock_is_configured = create_autospec(pypim.is_configured, return_value=True) + monkeypatch.setattr(pypim, "connect", mock_connect) + monkeypatch.setattr(pypim, "is_configured", mock_is_configured) + + # Start MAPDL with launch_mapdl + # Note: This is mocking to start MAPDL, but actually reusing the common one + # Thus cleanup_on_exit is set to false + mapdl = launch_mapdl( + cleanup_on_exit=False, additional_switches=QUICK_LAUNCH_SWITCHES + ) + + # Assert: pymapdl went through the pypim workflow + assert mock_is_configured.called + assert mock_connect.called + mock_client.create_instance.assert_called_with( + product_name="mapdl", product_version=None + ) + assert mock_instance.wait_for_ready.called + mock_instance.build_grpc_channel.assert_called_with( + options=[ + ("grpc.max_receive_message_length", MAX_MESSAGE_LENGTH), + ] + ) + + # And it connected using the channel created by PyPIM + assert mapdl._channel == pim_channel + + # and it kept track of the instance to be able to delete it + assert mapdl._remote_instance == mock_instance diff --git a/tests/test_launcher_remote.py b/tests/test_launcher_remote.py deleted file mode 100644 index fa5e937085..0000000000 --- a/tests/test_launcher_remote.py +++ /dev/null @@ -1,96 +0,0 @@ -# Copyright (C) 2016 - 2025 ANSYS, Inc. and/or its affiliates. -# SPDX-License-Identifier: MIT -# -# -# Permission is hereby granted, free of charge, to any person obtaining a copy -# of this software and associated documentation files (the "Software"), to deal -# in the Software without restriction, including without limitation the rights -# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -# copies of the Software, and to permit persons to whom the Software is -# furnished to do so, subject to the following conditions: -# -# The above copyright notice and this permission notice shall be included in all -# copies or substantial portions of the Software. -# -# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -# SOFTWARE. - -"""Test the PyPIM integration.""" -import pytest - -from conftest import has_dependency - -if not has_dependency("ansys-platform-instancemanagement"): - pytest.skip(allow_module_level=True) - -from unittest.mock import create_autospec - -import ansys.platform.instancemanagement as pypim -import grpc - -from ansys.mapdl.core.launcher import launch_mapdl -from ansys.mapdl.core.mapdl_grpc import MAX_MESSAGE_LENGTH -from conftest import QUICK_LAUNCH_SWITCHES - - -def test_launch_remote_instance(mapdl, cleared, monkeypatch): - # Create a mock pypim pretenting it is configured and returning a channel to an already running mapdl - mock_instance = pypim.Instance( - definition_name="definitions/fake-mapdl", - name="instances/fake-mapdl", - ready=True, - status_message=None, - services={"grpc": pypim.Service(uri=mapdl._channel_str, headers={})}, - ) - pim_channel = grpc.insecure_channel( - mapdl._channel_str, - options=[ - ("grpc.max_receive_message_length", MAX_MESSAGE_LENGTH), - ], - ) - mock_instance.wait_for_ready = create_autospec(mock_instance.wait_for_ready) - mock_instance.build_grpc_channel = create_autospec( - mock_instance.build_grpc_channel, return_value=pim_channel - ) - mock_instance.delete = create_autospec(mock_instance.delete) - - mock_client = pypim.Client(channel=grpc.insecure_channel("localhost:12345")) - mock_client.create_instance = create_autospec( - mock_client.create_instance, return_value=mock_instance - ) - - mock_connect = create_autospec(pypim.connect, return_value=mock_client) - mock_is_configured = create_autospec(pypim.is_configured, return_value=True) - monkeypatch.setattr(pypim, "connect", mock_connect) - monkeypatch.setattr(pypim, "is_configured", mock_is_configured) - - # Start MAPDL with launch_mapdl - # Note: This is mocking to start MAPDL, but actually reusing the common one - # Thus cleanup_on_exit is set to false - mapdl = launch_mapdl( - cleanup_on_exit=False, additional_switches=QUICK_LAUNCH_SWITCHES - ) - - # Assert: pymapdl went through the pypim workflow - assert mock_is_configured.called - assert mock_connect.called - mock_client.create_instance.assert_called_with( - product_name="mapdl", product_version=None - ) - assert mock_instance.wait_for_ready.called - mock_instance.build_grpc_channel.assert_called_with( - options=[ - ("grpc.max_receive_message_length", MAX_MESSAGE_LENGTH), - ] - ) - - # And it connected using the channel created by PyPIM - assert mapdl._channel == pim_channel - - # and it kept track of the instance to be able to delete it - assert mapdl._remote_instance == mock_instance From 205f80e5e6c024b013f99b227f68c0e8e9d34eb3 Mon Sep 17 00:00:00 2001 From: German <28149841+germa89@users.noreply.github.com> Date: Thu, 16 Jan 2025 20:22:46 +0100 Subject: [PATCH 17/42] fix: small fixes --- src/ansys/mapdl/core/launcher/launcher.py | 2 +- tests/test_launcher/test_local.py | 4 ---- 2 files changed, 1 insertion(+), 5 deletions(-) diff --git a/src/ansys/mapdl/core/launcher/launcher.py b/src/ansys/mapdl/core/launcher/launcher.py index daa0d807a1..41f2f5a792 100644 --- a/src/ansys/mapdl/core/launcher/launcher.py +++ b/src/ansys/mapdl/core/launcher/launcher.py @@ -116,7 +116,7 @@ def launch_mapdl( .. code:: console - export PYMAPDL_MAPDL_EXEC=/ansys_inc/v211/ansys/bin/mapdl + export PYMAPDL_MAPDL_EXEC=/ansys_inc/v251/ansys/bin/mapdl run_location : str, optional MAPDL working directory. Defaults to a temporary working diff --git a/tests/test_launcher/test_local.py b/tests/test_launcher/test_local.py index 96a8c7f67c..a2a87bf05e 100644 --- a/tests/test_launcher/test_local.py +++ b/tests/test_launcher/test_local.py @@ -26,10 +26,6 @@ from ansys.mapdl.core.launcher.local import processing_local_arguments -def test_processing_local_arguments(): - pass - - @pytest.mark.parametrize("start_instance", [True, False, None, ""]) @patch("ansys.mapdl.core.launcher.local.get_cpus", lambda *args, **kwargs: None) @patch("psutil.cpu_count", lambda *args, **kwargs: 4) From 2b5c8c934e4ac461112dfd83d7a7075a2b650154 Mon Sep 17 00:00:00 2001 From: German <28149841+germa89@users.noreply.github.com> Date: Thu, 16 Jan 2025 21:14:47 +0100 Subject: [PATCH 18/42] refactor: moving tools tests to test_tools --- src/ansys/mapdl/core/mapdl_grpc.py | 5 +- tests/test_launcher.py | 728 +--------------------- tests/test_launcher/test_launcher_grpc.py | 1 + tests/test_launcher/test_remote.py | 12 +- tests/test_launcher/test_tools.py | 668 +++++++++++++++++++- 5 files changed, 704 insertions(+), 710 deletions(-) diff --git a/src/ansys/mapdl/core/mapdl_grpc.py b/src/ansys/mapdl/core/mapdl_grpc.py index 683d1deca3..3c603430c8 100644 --- a/src/ansys/mapdl/core/mapdl_grpc.py +++ b/src/ansys/mapdl/core/mapdl_grpc.py @@ -420,9 +420,8 @@ def __init__( self.remove_temp_dir_on_exit: bool = remove_temp_dir_on_exit self._jobname: str = start_parm.get("jobname", "file") self._path: Optional[str] = start_parm.get("run_location", None) - self._start_instance: Optional[str] = ( - start_parm.get("start_instance") or get_start_instance() - ) + self._start_instance: Optional[str] = start_parm.get("start_instance") + self._busy: bool = False # used to check if running a command on the server self._local: bool = start_parm.get("local", True) self._launched: bool = start_parm.get("launched", True) diff --git a/tests/test_launcher.py b/tests/test_launcher.py index 4c85496591..340fb25dc1 100644 --- a/tests/test_launcher.py +++ b/tests/test_launcher.py @@ -29,18 +29,12 @@ from unittest.mock import patch import warnings -import psutil from pyfakefs.fake_filesystem import OSType import pytest from ansys.mapdl import core as pymapdl from ansys.mapdl.core import _HAS_ATP -from ansys.mapdl.core.errors import ( - MapdlDidNotStart, - NotEnoughResources, - PortAlreadyInUseByAnMAPDLInstance, - VersionError, -) +from ansys.mapdl.core.errors import MapdlDidNotStart, PortAlreadyInUseByAnMAPDLInstance from ansys.mapdl.core.launcher import LOCALHOST, launch_mapdl from ansys.mapdl.core.launcher.grpc import launch_grpc from ansys.mapdl.core.launcher.hpc import ( @@ -54,28 +48,9 @@ launch_mapdl_on_cluster, send_scontrol, ) -from ansys.mapdl.core.launcher.tools import ( - _is_ubuntu, - _parse_ip_route, - check_mode, - force_smp_in_student, - generate_mapdl_launch_command, - generate_start_parameters, - get_cpus, - get_exec_file, - get_ip, - get_port, - get_run_location, - get_start_instance, - get_version, - remove_err_files, - set_license_switch, - set_MPI_additional_switches, - submitter, - update_env_vars, -) +from ansys.mapdl.core.launcher.tools import submitter from ansys.mapdl.core.licensing import LICENSES -from ansys.mapdl.core.misc import check_has_mapdl, stack +from ansys.mapdl.core.misc import stack from conftest import ( ON_LOCAL, PATCH_MAPDL, @@ -93,8 +68,6 @@ version_from_path, ) - from ansys.mapdl.core.launcher import get_default_ansys - installed_mapdl_versions = list(get_available_ansys_installations().keys()) except: from conftest import MAPDL_VERSION @@ -102,8 +75,6 @@ installed_mapdl_versions = [MAPDL_VERSION] -from ansys.mapdl.core._version import SUPPORTED_ANSYS_VERSIONS as versions - paths = [ ("/usr/dir_v2019.1/slv/ansys_inc/v211/ansys/bin/ansys211", 211), ("C:/Program Files/ANSYS Inc/v202/ansys/bin/win64/ANSYS202.exe", 202), @@ -152,27 +123,6 @@ def fake_local_mapdl(mapdl): mapdl._local = False -@patch("os.name", "nt") -def test_validate_sw(): - # ensure that windows adds msmpi - # fake windows path - version = 211 - add_sw = set_MPI_additional_switches("", version=version) - assert "msmpi" in add_sw - - with pytest.warns( - UserWarning, match="Due to incompatibilities between this MAPDL version" - ): - add_sw = set_MPI_additional_switches("-mpi intelmpi", version=version) - assert "msmpi" in add_sw and "intelmpi" not in add_sw - - with pytest.warns( - UserWarning, match="Due to incompatibilities between this MAPDL version" - ): - add_sw = set_MPI_additional_switches("-mpi INTELMPI", version=version) - assert "msmpi" in add_sw and "INTELMPI" not in add_sw - - @requires("ansys-tools-path") @pytest.mark.parametrize("path_data", paths) def test_version_from_path(path_data): @@ -390,26 +340,6 @@ def test_remove_temp_dir_on_exit_fail(mapdl, cleared, tmpdir): assert os.listdir(old_path) -def test_env_injection(): - no_inject = update_env_vars(None, None) - assert no_inject == os.environ.copy() # return os.environ - - assert "myenvvar" in update_env_vars({"myenvvar": "True"}, None) - - _env_vars = update_env_vars(None, {"myenvvar": "True"}) - assert len(_env_vars) == 1 - assert "myenvvar" in _env_vars - - with pytest.raises(ValueError): - update_env_vars({"myenvvar": "True"}, {"myenvvar": "True"}) - - with pytest.raises(TypeError): - update_env_vars("asdf", None) - - with pytest.raises(TypeError): - update_env_vars(None, "asdf") - - @pytest.mark.requires_gui @pytest.mark.parametrize( "include_result,inplace,to_check", @@ -434,92 +364,6 @@ def test_open_gui( mapdl.open_gui(inplace=inplace, include_result=include_result) -def test_force_smp_in_student(): - add_sw = "" - exec_path = ( - r"C:\Program Files\ANSYS Inc\ANSYS Student\v222\ansys\bin\winx64\ANSYS222.exe" - ) - assert "-smp" in force_smp_in_student(add_sw, exec_path) - - add_sw = "-mpi" - exec_path = ( - r"C:\Program Files\ANSYS Inc\ANSYS Student\v222\ansys\bin\winx64\ANSYS222.exe" - ) - assert "-smp" not in force_smp_in_student(add_sw, exec_path) - - add_sw = "-dmp" - exec_path = ( - r"C:\Program Files\ANSYS Inc\ANSYS Student\v222\ansys\bin\winx64\ANSYS222.exe" - ) - assert "-smp" not in force_smp_in_student(add_sw, exec_path) - - add_sw = "" - exec_path = r"C:\Program Files\ANSYS Inc\v222\ansys\bin\winx64\ANSYS222.exe" - assert "-smp" not in force_smp_in_student(add_sw, exec_path) - - add_sw = "-SMP" - exec_path = r"C:\Program Files\ANSYS Inc\v222\ansys\bin\winx64\ANSYS222.exe" - assert "-SMP" in force_smp_in_student(add_sw, exec_path) - - -@pytest.mark.parametrize( - "license_short,license_name", - [[each_key, each_value] for each_key, each_value in LICENSES.items()], -) -def test_license_product_argument(license_short, license_name): - additional_switches = set_license_switch(license_name, "qwer") - assert f"qwer -p {license_short}" in additional_switches - - -@pytest.mark.parametrize("unvalid_type", [1, {}, ()]) -def test_license_product_argument_type_error(unvalid_type): - with pytest.raises(TypeError): - set_license_switch(unvalid_type, "") - - -def test_license_product_argument_warning(): - with pytest.warns(UserWarning): - assert "-p asdf" in set_license_switch("asdf", "qwer") - - -@pytest.mark.parametrize( - "license_short,license_name", - [[each_key, each_value] for each_key, each_value in LICENSES.items()], -) -def test_license_product_argument_p_arg(license_short, license_name): - assert f"qw1234 -p {license_short}" == set_license_switch( - None, f"qw1234 -p {license_short}" - ) - - -def test_license_product_argument_p_arg_warning(): - with pytest.warns(UserWarning): - assert "qwer -p asdf" in set_license_switch(None, "qwer -p asdf") - - -installed_mapdl_versions = [] -installed_mapdl_versions.extend([int(each) for each in list(versions.keys())]) -installed_mapdl_versions.extend([float(each / 10) for each in versions.keys()]) -installed_mapdl_versions.extend([str(each) for each in list(versions.keys())]) -installed_mapdl_versions.extend([str(each / 10) for each in versions.keys()]) -installed_mapdl_versions.extend(list(versions.values())) -installed_mapdl_versions.extend([None]) - - -@pytest.mark.parametrize("version", installed_mapdl_versions) -def test__verify_version_pass(version): - ver = get_version(version) - if version: - assert isinstance(ver, int) - assert min(versions.keys()) <= ver <= max(versions.keys()) - else: - assert ver is None - - -def test__verify_version_latest(): - assert get_version("latest") is None - - @requires("ansys-tools-path") @requires("local") def test_find_ansys(mapdl, cleared): @@ -552,24 +396,6 @@ def test_version(mapdl, cleared): assert str(version) in str(launching_arg["version"]) -@requires("local") -def test_raise_exec_path_and_version_launcher(mapdl, cleared): - with pytest.raises(ValueError): - get_version("asdf", "asdf") - - -@requires("linux") -@requires("local") -def test_is_ubuntu(): - assert _is_ubuntu() - - -@requires("ansys-tools-path") -@requires("local") -def test_get_default_ansys(): - assert get_default_ansys() is not None - - def test_launch_mapdl_non_recognaised_arguments(mapdl, cleared): with pytest.raises(ValueError, match="my_fake_argument"): launch_mapdl( @@ -586,19 +412,6 @@ def test_mapdl_non_recognaised_arguments(): ) -def test__parse_ip_route(): - output = """default via 172.25.192.1 dev eth0 proto kernel <<<=== this -172.25.192.0/20 dev eth0 proto kernel scope link src 172.25.195.101 <<<=== not this""" - - assert "172.25.192.1" == _parse_ip_route(output) - - output = """ -default via 172.23.112.1 dev eth0 proto kernel -172.23.112.0/20 dev eth0 proto kernel scope link src 172.23.121.145""" - - assert "172.23.112.1" == _parse_ip_route(output) - - def test_launched(mapdl, cleared): if ON_LOCAL: assert mapdl.launched @@ -847,48 +660,6 @@ def test_is_running_on_slurm( ) -@pytest.mark.parametrize( - "start_instance,context", - [ - pytest.param(True, NullContext(), id="Boolean true"), - pytest.param(False, NullContext(), id="Boolean false"), - pytest.param("true", NullContext(), id="String true"), - pytest.param("TRue", NullContext(), id="String true weird capitalization"), - pytest.param("2", pytest.raises(ValueError), id="String number"), - pytest.param(2, pytest.raises(ValueError), id="Int"), - ], -) -def test_get_start_instance_argument(monkeypatch, start_instance, context): - if "PYMAPDL_START_INSTANCE" in os.environ: - monkeypatch.delenv("PYMAPDL_START_INSTANCE") - with context: - if "true" in str(start_instance).lower(): - assert get_start_instance(start_instance) - else: - assert not get_start_instance(start_instance) - - -@pytest.mark.parametrize( - "start_instance, context", - [ - pytest.param("true", NullContext()), - pytest.param("TRue", NullContext()), - pytest.param("False", NullContext()), - pytest.param("FaLSE", NullContext()), - pytest.param("asdf", pytest.raises(OSError)), - pytest.param("1", pytest.raises(OSError)), - pytest.param("", NullContext()), - ], -) -def test_get_start_instance_envvar(monkeypatch, start_instance, context): - monkeypatch.setenv("PYMAPDL_START_INSTANCE", start_instance) - with context: - if "true" in start_instance.lower() or start_instance == "": - assert get_start_instance(start_instance=None) - else: - assert not get_start_instance(start_instance=None) - - @requires("local") @requires("ansys-tools-path") @pytest.mark.parametrize("start_instance", [True, False]) @@ -1000,251 +771,6 @@ def test_ip_and_start_instance( assert options["ip"] in (LOCALHOST, "0.0.0.0", "127.0.0.1") -@patch("os.name", "nt") -@patch("psutil.cpu_count", lambda *args, **kwargs: 10) -def test_generate_mapdl_launch_command_windows(): - assert os.name == "nt" # Checking mocking is properly done - - exec_file = "C:/Program Files/ANSYS Inc/v242/ansys/bin/winx64/ANSYS242.exe" - jobname = "myjob" - nproc = 10 - port = 1000 - ram = 2 - additional_switches = "-my_add=switch" - - cmd = generate_mapdl_launch_command( - exec_file=exec_file, - jobname=jobname, - nproc=nproc, - port=port, - ram=ram, - additional_switches=additional_switches, - ) - - assert isinstance(cmd, list) - - assert f"{exec_file}" in cmd - assert "-j" in cmd - assert f"{jobname}" in cmd - assert "-port" in cmd - assert f"{port}" in cmd - assert "-m" in cmd - assert f"{ram*1024}" in cmd - assert "-np" in cmd - assert f"{nproc}" in cmd - assert "-grpc" in cmd - assert f"{additional_switches}" in cmd - assert "-b" in cmd - assert "-i" in cmd - assert ".__tmp__.inp" in cmd - assert "-o" in cmd - assert ".__tmp__.out" in cmd - - cmd = " ".join(cmd) - assert f"{exec_file}" in cmd - assert f" -j {jobname} " in cmd - assert f" -port {port} " in cmd - assert f" -m {ram*1024} " in cmd - assert f" -np {nproc} " in cmd - assert " -grpc" in cmd - assert f" {additional_switches} " in cmd - assert f" -b -i .__tmp__.inp " in cmd - assert f" -o .__tmp__.out " in cmd - - -@patch("os.name", "posix") -def test_generate_mapdl_launch_command_linux(): - assert os.name != "nt" # Checking mocking is properly done - - exec_file = "/ansys_inc/v242/ansys/bin/ansys242" - jobname = "myjob" - nproc = 10 - port = 1000 - ram = 2 - additional_switches = "-my_add=switch" - - cmd = generate_mapdl_launch_command( - exec_file=exec_file, - jobname=jobname, - nproc=nproc, - port=port, - ram=ram, - additional_switches=additional_switches, - ) - assert isinstance(cmd, list) - assert all([isinstance(each, str) for each in cmd]) - - assert isinstance(cmd, list) - - assert f"{exec_file}" in cmd - assert "-j" in cmd - assert f"{jobname}" in cmd - assert "-port" in cmd - assert f"{port}" in cmd - assert "-m" in cmd - assert f"{ram*1024}" in cmd - assert "-np" in cmd - assert f"{nproc}" in cmd - assert "-grpc" in cmd - assert f"{additional_switches}" in cmd - - assert "-b" not in cmd - assert "-i" not in cmd - assert ".__tmp__.inp" not in cmd - assert "-o" not in cmd - assert ".__tmp__.out" not in cmd - - cmd = " ".join(cmd) - assert f"{exec_file} " in cmd - assert f" -j {jobname} " in cmd - assert f" -port {port} " in cmd - assert f" -m {ram*1024} " in cmd - assert f" -np {nproc} " in cmd - assert " -grpc" in cmd - assert f" {additional_switches} " in cmd - - assert f" -i .__tmp__.inp " not in cmd - assert f" -o .__tmp__.out " not in cmd - - -def test_generate_start_parameters_console(): - args = {"mode": "console", "start_timeout": 90} - - new_args = generate_start_parameters(args) - assert "start_timeout" in new_args - assert "ram" not in new_args - assert "override" not in new_args - assert "timeout" not in new_args - - -@patch("ansys.mapdl.core.launcher.tools._HAS_ATP", False) -def test_get_exec_file(monkeypatch): - monkeypatch.delenv("PYMAPDL_MAPDL_EXEC", False) - - args = {"exec_file": None, "start_instance": True} - - with pytest.raises(ModuleNotFoundError): - get_exec_file(args) - - -def test_get_exec_file_not_found(monkeypatch): - monkeypatch.delenv("PYMAPDL_MAPDL_EXEC", False) - - args = {"exec_file": "my/fake/path", "start_instance": True} - - with pytest.raises(FileNotFoundError): - get_exec_file(args) - - -def _get_application_path(*args, **kwargs): - return None - - -@requires("ansys-tools-path") -@patch("ansys.tools.path.path._get_application_path", _get_application_path) -def test_get_exec_file_not_found_two(monkeypatch): - monkeypatch.delenv("PYMAPDL_MAPDL_EXEC", False) - args = {"exec_file": None, "start_instance": True} - with pytest.raises( - FileNotFoundError, match="Invalid exec_file path or cannot load cached " - ): - get_exec_file(args) - - -@pytest.mark.parametrize("run_location", [None, True]) -@pytest.mark.parametrize("remove_temp_dir_on_exit", [None, False, True]) -def test_get_run_location(tmpdir, remove_temp_dir_on_exit, run_location): - if run_location: - new_path = os.path.join(str(tmpdir), "my_new_path") - assert not os.path.exists(new_path) - else: - new_path = None - - args = { - "run_location": new_path, - "remove_temp_dir_on_exit": remove_temp_dir_on_exit, - } - - get_run_location(args) - - assert os.path.exists(args["run_location"]) - - assert "remove_temp_dir_on_exit" in args - - if run_location: - assert not args["remove_temp_dir_on_exit"] - elif remove_temp_dir_on_exit: - assert args["remove_temp_dir_on_exit"] - else: - assert not args["remove_temp_dir_on_exit"] - - -def fake_os_access(*args, **kwargs): - return False - - -@patch("os.access", lambda *args, **kwargs: False) -def test_get_run_location_no_access(tmpdir): - with pytest.raises(IOError, match="Unable to write to ``run_location``:"): - get_run_location({"run_location": str(tmpdir)}) - - -@pytest.mark.parametrize( - "args,match", - [ - [ - {"start_instance": True, "ip": True, "on_pool": False}, - "When providing a value for the argument 'ip', the argument", - ], - [ - {"exec_file": True, "version": True}, - "Cannot specify both ``exec_file`` and ``version``.", - ], - [ - {"scheduler_options": True}, - "PyMAPDL does not read the number of cores from the 'scheduler_options'.", - ], - [ - {"launch_on_hpc": True, "ip": "111.22.33.44"}, - "PyMAPDL cannot ensure a specific IP will be used when launching", - ], - ], -) -def test_pre_check_args(args, match): - with pytest.raises(ValueError, match=match): - launch_mapdl(**args) - - -def test_remove_err_files(tmpdir): - run_location = str(tmpdir) - jobname = "jobname" - err_file = os.path.join(run_location, f"{jobname}.err") - with open(err_file, "w") as fid: - fid.write("Dummy") - - assert os.path.isfile(err_file) - remove_err_files(run_location, jobname) - assert not os.path.isfile(err_file) - - -def myosremove(*args, **kwargs): - raise IOError("Generic error") - - -@patch("os.remove", myosremove) -def test_remove_err_files_fail(tmpdir): - run_location = str(tmpdir) - jobname = "jobname" - err_file = os.path.join(run_location, f"{jobname}.err") - with open(err_file, "w") as fid: - fid.write("Dummy") - - assert os.path.isfile(err_file) - with pytest.raises(IOError): - remove_err_files(run_location, jobname) - assert os.path.isfile(err_file) - - # testing on windows to account for temp file @patch("os.name", "nt") @pytest.mark.parametrize("launch_on_hpc", [None, False, True]) @@ -1277,38 +803,6 @@ def test_launch_grpc(tmpdir, launch_on_hpc): assert isinstance(kwargs["stderr"], type(subprocess.PIPE)) -@patch("psutil.cpu_count", lambda *args, **kwags: 5) -@pytest.mark.parametrize("arg", [None, 3, 10]) -@pytest.mark.parametrize("env", [None, 3, 10]) -def test_get_cpus(monkeypatch, arg, env): - if env: - monkeypatch.setenv("PYMAPDL_NPROC", str(env)) - - context = NullContext() - cores_machine = psutil.cpu_count(logical=False) # it is patched - - if (arg and arg > cores_machine) or (arg is None and env and env > cores_machine): - context = pytest.raises(NotEnoughResources) - - args = {"nproc": arg, "running_on_hpc": False} - with context: - get_cpus(args) - - if arg: - assert args["nproc"] == arg - elif env: - assert args["nproc"] == env - else: - assert args["nproc"] == 2 - - -@patch("psutil.cpu_count", lambda *args, **kwags: 1) -def test_get_cpus_min(): - args = {"nproc": None, "running_on_hpc": False} - get_cpus(args) - assert args["nproc"] == 1 - - @pytest.mark.parametrize( "scheduler_options", [None, "-N 10", {"N": 10, "nodes": 10, "-tasks": 3, "--ntask-per-node": 2}], @@ -1634,67 +1128,6 @@ def test_launch_mapdl_on_cluster_exceptions(args, context): assert ret["nproc"] == 10 -@patch( - "socket.gethostbyname", - lambda *args, **kwargs: "123.45.67.89" if args[0] != LOCALHOST else LOCALHOST, -) -@pytest.mark.parametrize( - "ip,ip_env", - [[None, None], [None, "123.45.67.89"], ["123.45.67.89", "111.22.33.44"]], -) -def test_get_ip(monkeypatch, ip, ip_env): - monkeypatch.delenv("PYMAPDL_IP", False) - if ip_env: - monkeypatch.setenv("PYMAPDL_IP", ip_env) - args = {"ip": ip} - - get_ip(args) - - if ip: - assert args["ip"] == ip - else: - if ip_env: - assert args["ip"] == ip_env - else: - assert args["ip"] == LOCALHOST - - -@pytest.mark.parametrize( - "port,port_envvar,start_instance,port_busy,result", - ( - [None, None, True, False, 50052], # Standard case - [None, None, True, True, 50054], - [None, 50053, True, True, 50053], - [None, 50053, False, False, 50053], - [50054, 50053, True, False, 50054], - [50054, 50053, True, False, 50054], - [50054, None, False, False, 50054], - ), -) -def test_get_port(monkeypatch, port, port_envvar, start_instance, port_busy, result): - # Settings - pymapdl._LOCAL_PORTS = [] # Resetting - - monkeypatch.delenv("PYMAPDL_PORT", False) - if port_envvar: - monkeypatch.setenv("PYMAPDL_PORT", str(port_envvar)) - - # Testing - if port_busy: - # Success after the second retry, it should go up to 2. - # But for some reason, it goes up 3. - side_effect = [True, True, False] - else: - side_effect = [False] - - context = patch( - "ansys.mapdl.core.launcher.tools.port_in_use", side_effect=side_effect - ) - - with context: - assert get_port(port, start_instance) == result - - @pytest.mark.parametrize("stdout", ["Submitted batch job 1001", "Something bad"]) def test_get_jobid(stdout): if "1001" in stdout: @@ -1768,119 +1201,6 @@ def fake_proc(*args, **kwargs): assert batchhost_ip == "111.22.33.44" -@requires("ansys-tools-path") -@patch("ansys.tools.path.path._mapdl_version_from_path", lambda *args, **kwargs: 201) -@patch("ansys.mapdl.core._HAS_ATP", True) -def test_get_version_version_error(monkeypatch): - monkeypatch.delenv("PYMAPDL_MAPDL_VERSION", False) - - with pytest.raises( - VersionError, match="The MAPDL gRPC interface requires MAPDL 20.2 or later" - ): - get_version(None, "/path/to/executable") - - -@pytest.mark.parametrize("version", [211, 221, 232]) -def test_get_version_env_var(monkeypatch, version): - monkeypatch.setenv("PYMAPDL_MAPDL_VERSION", str(version)) - - assert version == get_version(None) - assert version != get_version(241) - - -@pytest.mark.parametrize( - "mode, version, osname, context, res", - [ - [None, None, None, NullContext(), "grpc"], # default - [ - "grpc", - 201, - "nt", - pytest.raises( - VersionError, match="gRPC mode requires MAPDL 2020R2 or newer on Window" - ), - None, - ], - [ - "grpc", - 202, - "posix", - pytest.raises( - VersionError, match="gRPC mode requires MAPDL 2021R1 or newer on Linux." - ), - None, - ], - ["grpc", 212, "nt", NullContext(), "grpc"], - ["grpc", 221, "posix", NullContext(), "grpc"], - ["grpc", 221, "nt", NullContext(), "grpc"], - [ - "console", - 221, - "nt", - pytest.raises(ValueError, match="Console mode requires Linux."), - None, - ], - [ - "console", - 221, - "posix", - pytest.warns( - UserWarning, - match="Console mode not recommended in MAPDL 2021R1 or newer.", - ), - "console", - ], - [ - "nomode", - 221, - "posix", - pytest.raises(ValueError, match=f'Invalid MAPDL server mode "nomode"'), - None, - ], - [None, 211, "posix", NullContext(), "grpc"], - [None, 211, "nt", NullContext(), "grpc"], - [None, 202, "nt", NullContext(), "grpc"], - [ - None, - 201, - "nt", - pytest.raises(VersionError, match="Running MAPDL as a service requires"), - None, - ], - [None, 202, "posix", NullContext(), "console"], - [None, 201, "posix", NullContext(), "console"], - [ - None, - 110, - "posix", - pytest.warns( - UserWarning, - match="MAPDL as a service has not been tested on MAPDL < v13", - ), - "console", - ], - [ - None, - 110, - "nt", - pytest.raises(VersionError, match="Running MAPDL as a service requires"), - None, - ], - [ - "anymode", - None, - "posix", - pytest.warns(UserWarning, match="PyMAPDL couldn't detect MAPDL version"), - "anymode", - ], - ], -) -def test_check_mode(mode, version, osname, context, res): - with patch("os.name", osname): - with context as cnt: - assert res == check_mode(mode, version) - - @pytest.mark.parametrize("jobid", [1001, 2002]) @patch("subprocess.Popen", lambda *args, **kwargs: None) def test_kill_job(jobid): @@ -2014,22 +1334,6 @@ def test_args_pass(monkeypatch, arg, value, method): assert meth == value -def test_check_has_mapdl(): - if TESTING_MINIMAL: - assert check_has_mapdl() is False - else: - assert check_has_mapdl() == ON_LOCAL - - -def raising(): - raise Exception("An error") - - -@patch("ansys.mapdl.core.launcher.tools.check_valid_ansys", raising) -def test_check_has_mapdl_failed(): - assert check_has_mapdl() is False - - @requires("local") @patch("ansys.mapdl.core.launcher.tools._is_ubuntu", lambda *args, **kwargs: True) @patch( @@ -2087,3 +1391,29 @@ def test_create_queue_for_std_no_queue(): from ansys.mapdl.core.launcher.tools import _create_queue_for_std assert _create_queue_for_std(None) == (None, None) + + +@pytest.mark.parametrize( + "args,match", + [ + [ + {"start_instance": True, "ip": True, "on_pool": False}, + "When providing a value for the argument 'ip', the argument", + ], + [ + {"exec_file": True, "version": True}, + "Cannot specify both ``exec_file`` and ``version``.", + ], + [ + {"scheduler_options": True}, + "PyMAPDL does not read the number of cores from the 'scheduler_options'.", + ], + [ + {"launch_on_hpc": True, "ip": "111.22.33.44"}, + "PyMAPDL cannot ensure a specific IP will be used when launching", + ], + ], +) +def test_launch_mapdl_pre_check_args(args, match): + with pytest.raises(ValueError, match=match): + launch_mapdl(**args) diff --git a/tests/test_launcher/test_launcher_grpc.py b/tests/test_launcher/test_launcher_grpc.py index 5427619ed6..0dc6276ada 100644 --- a/tests/test_launcher/test_launcher_grpc.py +++ b/tests/test_launcher/test_launcher_grpc.py @@ -24,6 +24,7 @@ from conftest import requires +@requires("local") @requires("nostudent") def test_launch_mapdl_grpc(): diff --git a/tests/test_launcher/test_remote.py b/tests/test_launcher/test_remote.py index 53dfac8d75..94259c51db 100644 --- a/tests/test_launcher/test_remote.py +++ b/tests/test_launcher/test_remote.py @@ -26,15 +26,13 @@ from ansys.mapdl.core.launcher.remote import _NON_VALID_ARGS, connect_to_mapdl -def test_connect_to_mapdl(): - mapdl = connect_to_mapdl() +def test_connect_to_mapdl(mapdl): + mapdl_2 = connect_to_mapdl(port=mapdl.port) - assert "PREP7" in mapdl.prep7() + assert "PREP7" in mapdl_2.prep7() - assert not mapdl._start_instance - assert not mapdl._launched - - mapdl.exit(force=False) + assert not mapdl_2._start_instance + assert not mapdl_2._launched @pytest.mark.parametrize("arg", _NON_VALID_ARGS) diff --git a/tests/test_launcher/test_tools.py b/tests/test_launcher/test_tools.py index 90fb1d3a09..9fbe951010 100644 --- a/tests/test_launcher/test_tools.py +++ b/tests/test_launcher/test_tools.py @@ -20,15 +20,42 @@ # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # SOFTWARE. +import os +from unittest.mock import patch + +import psutil import pytest +from ansys.mapdl import core as pymapdl +from ansys.mapdl.core._version import SUPPORTED_ANSYS_VERSIONS as versions +from ansys.mapdl.core.errors import NotEnoughResources, VersionError +from ansys.mapdl.core.launcher import LOCALHOST from ansys.mapdl.core.launcher.tools import ( ALLOWABLE_LAUNCH_MAPDL_ARGS, + _is_ubuntu, + _parse_ip_route, check_kwargs, + check_mode, + force_smp_in_student, + generate_mapdl_launch_command, + generate_start_parameters, + get_cpus, + get_exec_file, + get_ip, + get_port, + get_run_location, + get_start_instance, + get_version, pre_check_args, + remove_err_files, + set_license_switch, + set_MPI_additional_switches, + update_env_vars, ) +from ansys.mapdl.core.licensing import LICENSES from ansys.mapdl.core.mapdl_core import _ALLOWED_START_PARM -from conftest import NullContext +from ansys.mapdl.core.misc import check_has_mapdl +from conftest import ON_LOCAL, TESTING_MINIMAL, NullContext, requires _ARGS_VALIDS = ALLOWABLE_LAUNCH_MAPDL_ARGS.copy() _ARGS_VALIDS.extend(_ALLOWED_START_PARM) @@ -47,6 +74,564 @@ def test_check_kwargs(arg): check_kwargs({"kwargs": {arg: None}}) +@pytest.mark.parametrize( + "start_instance,context", + [ + pytest.param(True, NullContext(), id="Boolean true"), + pytest.param(False, NullContext(), id="Boolean false"), + pytest.param("true", NullContext(), id="String true"), + pytest.param("TRue", NullContext(), id="String true weird capitalization"), + pytest.param("2", pytest.raises(ValueError), id="String number"), + pytest.param(2, pytest.raises(ValueError), id="Int"), + ], +) +def test_get_start_instance_argument(monkeypatch, start_instance, context): + if "PYMAPDL_START_INSTANCE" in os.environ: + monkeypatch.delenv("PYMAPDL_START_INSTANCE") + with context: + if "true" in str(start_instance).lower(): + assert get_start_instance(start_instance) + else: + assert not get_start_instance(start_instance) + + +@pytest.mark.parametrize( + "start_instance, context", + [ + pytest.param("true", NullContext()), + pytest.param("TRue", NullContext()), + pytest.param("False", NullContext()), + pytest.param("FaLSE", NullContext()), + pytest.param("asdf", pytest.raises(OSError)), + pytest.param("1", pytest.raises(OSError)), + pytest.param("", NullContext()), + ], +) +def test_get_start_instance_envvar(monkeypatch, start_instance, context): + monkeypatch.setenv("PYMAPDL_START_INSTANCE", start_instance) + with context: + if "true" in start_instance.lower() or start_instance == "": + assert get_start_instance(start_instance=None) + else: + assert not get_start_instance(start_instance=None) + + +@requires("ansys-tools-path") +@requires("local") +def test_get_default_ansys(): + from ansys.mapdl.core.launcher import get_default_ansys + + assert get_default_ansys() is not None + + +def raising(): + raise Exception("An error") + + +@patch("ansys.mapdl.core.launcher.tools.check_valid_ansys", raising) +def test_check_has_mapdl_failed(): + assert check_has_mapdl() is False + + +def test_check_has_mapdl(): + if TESTING_MINIMAL: + assert check_has_mapdl() is False + else: + assert check_has_mapdl() == ON_LOCAL + + +@patch("os.name", "nt") +def test_validate_sw(): + # ensure that windows adds msmpi + # fake windows path + version = 211 + add_sw = set_MPI_additional_switches("", version=version) + assert "msmpi" in add_sw + + with pytest.warns( + UserWarning, match="Due to incompatibilities between this MAPDL version" + ): + add_sw = set_MPI_additional_switches("-mpi intelmpi", version=version) + assert "msmpi" in add_sw and "intelmpi" not in add_sw + + with pytest.warns( + UserWarning, match="Due to incompatibilities between this MAPDL version" + ): + add_sw = set_MPI_additional_switches("-mpi INTELMPI", version=version) + assert "msmpi" in add_sw and "INTELMPI" not in add_sw + + +def test_force_smp_in_student(): + add_sw = "" + exec_path = ( + r"C:\Program Files\ANSYS Inc\ANSYS Student\v222\ansys\bin\winx64\ANSYS222.exe" + ) + assert "-smp" in force_smp_in_student(add_sw, exec_path) + + add_sw = "-mpi" + exec_path = ( + r"C:\Program Files\ANSYS Inc\ANSYS Student\v222\ansys\bin\winx64\ANSYS222.exe" + ) + assert "-smp" not in force_smp_in_student(add_sw, exec_path) + + add_sw = "-dmp" + exec_path = ( + r"C:\Program Files\ANSYS Inc\ANSYS Student\v222\ansys\bin\winx64\ANSYS222.exe" + ) + assert "-smp" not in force_smp_in_student(add_sw, exec_path) + + add_sw = "" + exec_path = r"C:\Program Files\ANSYS Inc\v222\ansys\bin\winx64\ANSYS222.exe" + assert "-smp" not in force_smp_in_student(add_sw, exec_path) + + add_sw = "-SMP" + exec_path = r"C:\Program Files\ANSYS Inc\v222\ansys\bin\winx64\ANSYS222.exe" + assert "-SMP" in force_smp_in_student(add_sw, exec_path) + + +@patch("os.name", "nt") +@patch("psutil.cpu_count", lambda *args, **kwargs: 10) +def test_generate_mapdl_launch_command_windows(): + assert os.name == "nt" # Checking mocking is properly done + + exec_file = "C:/Program Files/ANSYS Inc/v242/ansys/bin/winx64/ANSYS242.exe" + jobname = "myjob" + nproc = 10 + port = 1000 + ram = 2 + additional_switches = "-my_add=switch" + + cmd = generate_mapdl_launch_command( + exec_file=exec_file, + jobname=jobname, + nproc=nproc, + port=port, + ram=ram, + additional_switches=additional_switches, + ) + + assert isinstance(cmd, list) + + assert f"{exec_file}" in cmd + assert "-j" in cmd + assert f"{jobname}" in cmd + assert "-port" in cmd + assert f"{port}" in cmd + assert "-m" in cmd + assert f"{ram*1024}" in cmd + assert "-np" in cmd + assert f"{nproc}" in cmd + assert "-grpc" in cmd + assert f"{additional_switches}" in cmd + assert "-b" in cmd + assert "-i" in cmd + assert ".__tmp__.inp" in cmd + assert "-o" in cmd + assert ".__tmp__.out" in cmd + + cmd = " ".join(cmd) + assert f"{exec_file}" in cmd + assert f" -j {jobname} " in cmd + assert f" -port {port} " in cmd + assert f" -m {ram*1024} " in cmd + assert f" -np {nproc} " in cmd + assert " -grpc" in cmd + assert f" {additional_switches} " in cmd + assert f" -b -i .__tmp__.inp " in cmd + assert f" -o .__tmp__.out " in cmd + + +@patch("os.name", "posix") +def test_generate_mapdl_launch_command_linux(): + assert os.name != "nt" # Checking mocking is properly done + + exec_file = "/ansys_inc/v242/ansys/bin/ansys242" + jobname = "myjob" + nproc = 10 + port = 1000 + ram = 2 + additional_switches = "-my_add=switch" + + cmd = generate_mapdl_launch_command( + exec_file=exec_file, + jobname=jobname, + nproc=nproc, + port=port, + ram=ram, + additional_switches=additional_switches, + ) + assert isinstance(cmd, list) + assert all([isinstance(each, str) for each in cmd]) + + assert isinstance(cmd, list) + + assert f"{exec_file}" in cmd + assert "-j" in cmd + assert f"{jobname}" in cmd + assert "-port" in cmd + assert f"{port}" in cmd + assert "-m" in cmd + assert f"{ram*1024}" in cmd + assert "-np" in cmd + assert f"{nproc}" in cmd + assert "-grpc" in cmd + assert f"{additional_switches}" in cmd + + assert "-b" not in cmd + assert "-i" not in cmd + assert ".__tmp__.inp" not in cmd + assert "-o" not in cmd + assert ".__tmp__.out" not in cmd + + cmd = " ".join(cmd) + assert f"{exec_file} " in cmd + assert f" -j {jobname} " in cmd + assert f" -port {port} " in cmd + assert f" -m {ram*1024} " in cmd + assert f" -np {nproc} " in cmd + assert " -grpc" in cmd + assert f" {additional_switches} " in cmd + + assert f" -i .__tmp__.inp " not in cmd + assert f" -o .__tmp__.out " not in cmd + + +@pytest.mark.parametrize( + "mode, version, osname, context, res", + [ + [None, None, None, NullContext(), "grpc"], # default + [ + "grpc", + 201, + "nt", + pytest.raises( + VersionError, match="gRPC mode requires MAPDL 2020R2 or newer on Window" + ), + None, + ], + [ + "grpc", + 202, + "posix", + pytest.raises( + VersionError, match="gRPC mode requires MAPDL 2021R1 or newer on Linux." + ), + None, + ], + ["grpc", 212, "nt", NullContext(), "grpc"], + ["grpc", 221, "posix", NullContext(), "grpc"], + ["grpc", 221, "nt", NullContext(), "grpc"], + [ + "console", + 221, + "nt", + pytest.raises(ValueError, match="Console mode requires Linux."), + None, + ], + [ + "console", + 221, + "posix", + pytest.warns( + UserWarning, + match="Console mode not recommended in MAPDL 2021R1 or newer.", + ), + "console", + ], + [ + "nomode", + 221, + "posix", + pytest.raises(ValueError, match=f'Invalid MAPDL server mode "nomode"'), + None, + ], + [None, 211, "posix", NullContext(), "grpc"], + [None, 211, "nt", NullContext(), "grpc"], + [None, 202, "nt", NullContext(), "grpc"], + [ + None, + 201, + "nt", + pytest.raises(VersionError, match="Running MAPDL as a service requires"), + None, + ], + [None, 202, "posix", NullContext(), "console"], + [None, 201, "posix", NullContext(), "console"], + [ + None, + 110, + "posix", + pytest.warns( + UserWarning, + match="MAPDL as a service has not been tested on MAPDL < v13", + ), + "console", + ], + [ + None, + 110, + "nt", + pytest.raises(VersionError, match="Running MAPDL as a service requires"), + None, + ], + [ + "anymode", + None, + "posix", + pytest.warns(UserWarning, match="PyMAPDL couldn't detect MAPDL version"), + "anymode", + ], + ], +) +def test_check_mode(mode, version, osname, context, res): + with patch("os.name", osname): + with context as cnt: + assert res == check_mode(mode, version) + + +def test_env_injection(): + no_inject = update_env_vars(None, None) + assert no_inject == os.environ.copy() # return os.environ + + assert "myenvvar" in update_env_vars({"myenvvar": "True"}, None) + + _env_vars = update_env_vars(None, {"myenvvar": "True"}) + assert len(_env_vars) == 1 + assert "myenvvar" in _env_vars + + with pytest.raises(ValueError): + update_env_vars({"myenvvar": "True"}, {"myenvvar": "True"}) + + with pytest.raises(TypeError): + update_env_vars("asdf", None) + + with pytest.raises(TypeError): + update_env_vars(None, "asdf") + + +@pytest.mark.parametrize( + "license_short,license_name", + [[each_key, each_value] for each_key, each_value in LICENSES.items()], +) +def test_license_product_argument(license_short, license_name): + additional_switches = set_license_switch(license_name, "qwer") + assert f"qwer -p {license_short}" in additional_switches + + +@pytest.mark.parametrize("unvalid_type", [1, {}, ()]) +def test_license_product_argument_type_error(unvalid_type): + with pytest.raises(TypeError): + set_license_switch(unvalid_type, "") + + +def test_license_product_argument_warning(): + with pytest.warns(UserWarning): + assert "-p asdf" in set_license_switch("asdf", "qwer") + + +@pytest.mark.parametrize( + "license_short,license_name", + [[each_key, each_value] for each_key, each_value in LICENSES.items()], +) +def test_license_product_argument_p_arg(license_short, license_name): + assert f"qw1234 -p {license_short}" == set_license_switch( + None, f"qw1234 -p {license_short}" + ) + + +def test_license_product_argument_p_arg_warning(): + with pytest.warns(UserWarning): + assert "qwer -p asdf" in set_license_switch(None, "qwer -p asdf") + + +def test_generate_start_parameters_console(): + args = {"mode": "console", "start_timeout": 90} + + new_args = generate_start_parameters(args) + assert "start_timeout" in new_args + assert "ram" not in new_args + assert "override" not in new_args + assert "timeout" not in new_args + + +@patch( + "socket.gethostbyname", + lambda *args, **kwargs: "123.45.67.89" if args[0] != LOCALHOST else LOCALHOST, +) +@pytest.mark.parametrize( + "ip,ip_env", + [[None, None], [None, "123.45.67.89"], ["123.45.67.89", "111.22.33.44"]], +) +def test_get_ip(monkeypatch, ip, ip_env): + monkeypatch.delenv("PYMAPDL_IP", False) + if ip_env: + monkeypatch.setenv("PYMAPDL_IP", ip_env) + args = {"ip": ip} + + get_ip(args) + + if ip: + assert args["ip"] == ip + else: + if ip_env: + assert args["ip"] == ip_env + else: + assert args["ip"] == LOCALHOST + + +@pytest.mark.parametrize( + "port,port_envvar,start_instance,port_busy,result", + ( + [None, None, True, False, 50052], # Standard case + [None, None, True, True, 50054], + [None, 50053, True, True, 50053], + [None, 50053, False, False, 50053], + [50054, 50053, True, False, 50054], + [50054, 50053, True, False, 50054], + [50054, None, False, False, 50054], + ), +) +def test_get_port(monkeypatch, port, port_envvar, start_instance, port_busy, result): + # Settings + pymapdl._LOCAL_PORTS = [] # Resetting + + monkeypatch.delenv("PYMAPDL_PORT", False) + if port_envvar: + monkeypatch.setenv("PYMAPDL_PORT", str(port_envvar)) + + # Testing + if port_busy: + # Success after the second retry, it should go up to 2. + # But for some reason, it goes up 3. + side_effect = [True, True, False] + else: + side_effect = [False] + + context = patch( + "ansys.mapdl.core.launcher.tools.port_in_use", side_effect=side_effect + ) + + with context: + assert get_port(port, start_instance) == result + + +@requires("ansys-tools-path") +@patch("ansys.tools.path.path._mapdl_version_from_path", lambda *args, **kwargs: 201) +@patch("ansys.mapdl.core._HAS_ATP", True) +def test_get_version_version_error(monkeypatch): + monkeypatch.delenv("PYMAPDL_MAPDL_VERSION", False) + + with pytest.raises( + VersionError, match="The MAPDL gRPC interface requires MAPDL 20.2 or later" + ): + get_version(None, "/path/to/executable") + + +@pytest.mark.parametrize("version", [211, 221, 232]) +def test_get_version_env_var(monkeypatch, version): + monkeypatch.setenv("PYMAPDL_MAPDL_VERSION", str(version)) + + assert version == get_version(None) + assert version != get_version(241) + + +@requires("local") +def test_raise_exec_path_and_version_launcher(mapdl, cleared): + with pytest.raises(ValueError): + get_version("asdf", "asdf") + + +installed_mapdl_versions = [] +installed_mapdl_versions.extend([int(each) for each in list(versions.keys())]) +installed_mapdl_versions.extend([float(each / 10) for each in versions.keys()]) +installed_mapdl_versions.extend([str(each) for each in list(versions.keys())]) +installed_mapdl_versions.extend([str(each / 10) for each in versions.keys()]) +installed_mapdl_versions.extend(list(versions.values())) +installed_mapdl_versions.extend([None]) + + +@pytest.mark.parametrize("version", installed_mapdl_versions) +def test__verify_version_pass(version): + ver = get_version(version) + if version: + assert isinstance(ver, int) + assert min(versions.keys()) <= ver <= max(versions.keys()) + else: + assert ver is None + + +def test__verify_version_latest(): + assert get_version("latest") is None + + +@patch("ansys.mapdl.core.launcher.tools._HAS_ATP", False) +def test_get_exec_file(monkeypatch): + monkeypatch.delenv("PYMAPDL_MAPDL_EXEC", False) + + args = {"exec_file": None, "start_instance": True} + + with pytest.raises(ModuleNotFoundError): + get_exec_file(args) + + +def test_get_exec_file_not_found(monkeypatch): + monkeypatch.delenv("PYMAPDL_MAPDL_EXEC", False) + + args = {"exec_file": "my/fake/path", "start_instance": True} + + with pytest.raises(FileNotFoundError): + get_exec_file(args) + + +def _get_application_path(*args, **kwargs): + return None + + +@requires("ansys-tools-path") +@patch("ansys.tools.path.path._get_application_path", _get_application_path) +def test_get_exec_file_not_found_two(monkeypatch): + monkeypatch.delenv("PYMAPDL_MAPDL_EXEC", False) + args = {"exec_file": None, "start_instance": True} + with pytest.raises( + FileNotFoundError, match="Invalid exec_file path or cannot load cached " + ): + get_exec_file(args) + + +@pytest.mark.parametrize("run_location", [None, True]) +@pytest.mark.parametrize("remove_temp_dir_on_exit", [None, False, True]) +def test_get_run_location(tmpdir, remove_temp_dir_on_exit, run_location): + if run_location: + new_path = os.path.join(str(tmpdir), "my_new_path") + assert not os.path.exists(new_path) + else: + new_path = None + + args = { + "run_location": new_path, + "remove_temp_dir_on_exit": remove_temp_dir_on_exit, + } + + get_run_location(args) + + assert os.path.exists(args["run_location"]) + + assert "remove_temp_dir_on_exit" in args + + if run_location: + assert not args["remove_temp_dir_on_exit"] + elif remove_temp_dir_on_exit: + assert args["remove_temp_dir_on_exit"] + else: + assert not args["remove_temp_dir_on_exit"] + + +@patch("os.access", lambda *args, **kwargs: False) +def test_get_run_location_no_access(tmpdir): + with pytest.raises(IOError, match="Unable to write to ``run_location``:"): + get_run_location({"run_location": str(tmpdir)}) + + @pytest.mark.parametrize( "args,match", [ @@ -71,3 +656,84 @@ def test_check_kwargs(arg): def test_pre_check_args(args, match): with pytest.raises(ValueError, match=match): pre_check_args(args) + + +@patch("psutil.cpu_count", lambda *args, **kwags: 5) +@pytest.mark.parametrize("arg", [None, 3, 10]) +@pytest.mark.parametrize("env", [None, 3, 10]) +def test_get_cpus(monkeypatch, arg, env): + if env: + monkeypatch.setenv("PYMAPDL_NPROC", str(env)) + + context = NullContext() + cores_machine = psutil.cpu_count(logical=False) # it is patched + + if (arg and arg > cores_machine) or (arg is None and env and env > cores_machine): + context = pytest.raises(NotEnoughResources) + + args = {"nproc": arg, "running_on_hpc": False} + with context: + get_cpus(args) + + if arg: + assert args["nproc"] == arg + elif env: + assert args["nproc"] == env + else: + assert args["nproc"] == 2 + + +@patch("psutil.cpu_count", lambda *args, **kwags: 1) +def test_get_cpus_min(): + args = {"nproc": None, "running_on_hpc": False} + get_cpus(args) + assert args["nproc"] == 1 + + +def test_remove_err_files(tmpdir): + run_location = str(tmpdir) + jobname = "jobname" + err_file = os.path.join(run_location, f"{jobname}.err") + with open(err_file, "w") as fid: + fid.write("Dummy") + + assert os.path.isfile(err_file) + remove_err_files(run_location, jobname) + assert not os.path.isfile(err_file) + + +def myosremove(*args, **kwargs): + raise IOError("Generic error") + + +@patch("os.remove", myosremove) +def test_remove_err_files_fail(tmpdir): + run_location = str(tmpdir) + jobname = "jobname" + err_file = os.path.join(run_location, f"{jobname}.err") + with open(err_file, "w") as fid: + fid.write("Dummy") + + assert os.path.isfile(err_file) + with pytest.raises(IOError): + remove_err_files(run_location, jobname) + assert os.path.isfile(err_file) + + +def test__parse_ip_route(): + output = """default via 172.25.192.1 dev eth0 proto kernel <<<=== this +172.25.192.0/20 dev eth0 proto kernel scope link src 172.25.195.101 <<<=== not this""" + + assert "172.25.192.1" == _parse_ip_route(output) + + output = """ +default via 172.23.112.1 dev eth0 proto kernel +172.23.112.0/20 dev eth0 proto kernel scope link src 172.23.121.145""" + + assert "172.23.112.1" == _parse_ip_route(output) + + +@requires("linux") +@requires("local") +def test_is_ubuntu(): + assert _is_ubuntu() From ad4fe52ac66bc5382ce1bb856d3de6651a1aaced Mon Sep 17 00:00:00 2001 From: German <28149841+germa89@users.noreply.github.com> Date: Fri, 17 Jan 2025 13:08:09 +0100 Subject: [PATCH 19/42] ci: adding graphviz to dependencies for profiling plotting --- .github/workflows/ci.yml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 6dddaf54db..98d2e826b1 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -513,7 +513,7 @@ jobs: - name: "Install os packages" run: | sudo apt update - sudo apt install libgl1-mesa-glx xvfb + sudo apt install libgl1-mesa-glx xvfb graphviz - name: "Test virtual framebuffer" run: | @@ -685,7 +685,7 @@ jobs: - name: "Install OS packages" run: | apt update - apt install -y libgl1-mesa-glx xvfb libgomp1 + apt install -y libgl1-mesa-glx xvfb libgomp1 graphviz - name: "Test virtual framebuffer" run: | @@ -827,7 +827,7 @@ jobs: - name: "Installing missing package" run: | sudo apt-get update - sudo apt-get install -y libgomp1 + sudo apt-get install -y libgomp1 graphviz - name: "Setup Python" uses: actions/setup-python@v5 @@ -957,7 +957,7 @@ jobs: - name: "Installing missing package" run: | sudo apt-get update - sudo apt-get install -y libgomp1 + sudo apt-get install -y libgomp1 graphviz - name: "Setup Python" uses: actions/setup-python@v5 From 789cfe5ac56cbc586733098a85dd1af79d06f7f5 Mon Sep 17 00:00:00 2001 From: German <28149841+germa89@users.noreply.github.com> Date: Fri, 17 Jan 2025 12:10:16 +0000 Subject: [PATCH 20/42] ci: adding prof file to gitignore --- .gitignore | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/.gitignore b/.gitignore index ee27c203f2..d36f8c19b8 100755 --- a/.gitignore +++ b/.gitignore @@ -79,7 +79,6 @@ docker/mapdl/v212 docker/mapdl/_old docker/mapdl/*.sh - # temp testing tmp.out @@ -101,3 +100,6 @@ doc/source/sg_execution_times.rst *prin-stresses.html doc/webserver.log doc/webserver.pid + +# Profiling +prof \ No newline at end of file From aa2ee1190ac721fa3322bd4d00a0739458fb8924 Mon Sep 17 00:00:00 2001 From: German <28149841+germa89@users.noreply.github.com> Date: Fri, 17 Jan 2025 13:14:45 +0100 Subject: [PATCH 21/42] fix: wrong callers --- src/ansys/mapdl/core/cli/stop.py | 2 +- src/ansys/mapdl/core/mapdl_grpc.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/ansys/mapdl/core/cli/stop.py b/src/ansys/mapdl/core/cli/stop.py index e49ebbff0c..57d78f3985 100644 --- a/src/ansys/mapdl/core/cli/stop.py +++ b/src/ansys/mapdl/core/cli/stop.py @@ -68,7 +68,7 @@ def stop(port: int, pid: Optional[int], all: bool) -> None: """ import psutil - from ansys.mapdl.core.launcher import is_ansys_process + from ansys.mapdl.core.launcher.tools import is_ansys_process PROCESS_OK_STATUS = [ # List of all process status, comment out the ones that means that diff --git a/src/ansys/mapdl/core/mapdl_grpc.py b/src/ansys/mapdl/core/mapdl_grpc.py index 3c603430c8..81bdf7f08d 100644 --- a/src/ansys/mapdl/core/mapdl_grpc.py +++ b/src/ansys/mapdl/core/mapdl_grpc.py @@ -889,7 +889,7 @@ def _launch(self, start_parm, timeout=10): raise MapdlRuntimeError( "Can only launch the GUI with a local instance of MAPDL" ) - from ansys.mapdl.core.launcher import launch_grpc + from ansys.mapdl.core.launcher.grpc import launch_grpc from ansys.mapdl.core.launcher.tools import generate_mapdl_launch_command self._exited = False # reset exit state From fb2ccfbc606f5167fa340a835e6aa4dd4eec87ac Mon Sep 17 00:00:00 2001 From: German <28149841+germa89@users.noreply.github.com> Date: Tue, 21 Jan 2025 12:06:45 +0100 Subject: [PATCH 22/42] refactor: test to avoid launch_mapdl --- tests/test_launcher.py | 48 ++++++++++++++++++++++++++---------------- 1 file changed, 30 insertions(+), 18 deletions(-) diff --git a/tests/test_launcher.py b/tests/test_launcher.py index 340fb25dc1..67da33fed6 100644 --- a/tests/test_launcher.py +++ b/tests/test_launcher.py @@ -295,27 +295,39 @@ def test_license_type_dummy(mapdl, cleared): ) -@requires("local") -@requires("nostudent") -def test_remove_temp_dir_on_exit(mapdl, cleared): +@patch("ansys.mapdl.core.Mapdl._exit_mapdl", lambda *args, **kwargs: None) +def test_remove_temp_dir_on_exit(mapdl, cleared, tmpdir): """Ensure the working directory is removed when run_location is not set.""" - mapdl_ = launch_mapdl( - port=mapdl.port + 1, - remove_temp_dir_on_exit=True, - start_timeout=start_timeout, - additional_switches=QUICK_LAUNCH_SWITCHES, - ) - # possible MAPDL is installed but running in "remote" mode - path = mapdl_.directory - mapdl_.exit() + with ( + patch.object(mapdl, "finish_job_on_exit", False), + patch.object(mapdl, "_local", True), + patch.object(mapdl, "remove_temp_dir_on_exit", True), + ): - tmp_dir = tempfile.gettempdir() - ans_temp_dir = os.path.join(tmp_dir, "ansys_") - if path.startswith(ans_temp_dir): - assert not os.path.isdir(path) - else: - assert os.path.isdir(path) + # Testing reaching the method + with patch.object(mapdl, "_remove_temp_dir_on_exit") as mock_rm: + mock_rm.side_effect = None + + mapdl.exit(force=True) + + mock_rm.assert_called() + assert mapdl.directory == mock_rm.call_args.args[0] + + # Testing the method + # Directory to be deleted + ans_temp_dir = os.path.join(tempfile.gettempdir(), "ansys_") + + os.makedirs(ans_temp_dir, exist_ok=True) + assert os.path.isdir(ans_temp_dir) + mapdl._remove_temp_dir_on_exit(ans_temp_dir) + assert not os.path.isdir(ans_temp_dir) + + # Directory to NOT be deleted + tmp_dir = str(tmpdir) + assert os.path.isdir(tmp_dir) + mapdl._remove_temp_dir_on_exit(tmp_dir) + assert os.path.isdir(tmp_dir) @requires("local") From e54988719826889c5aa44a6ff16ce7528b560ad2 Mon Sep 17 00:00:00 2001 From: German <28149841+germa89@users.noreply.github.com> Date: Tue, 21 Jan 2025 12:08:38 +0100 Subject: [PATCH 23/42] test: remove unnecessary test --- tests/test_launcher.py | 22 ---------------------- 1 file changed, 22 deletions(-) diff --git a/tests/test_launcher.py b/tests/test_launcher.py index 67da33fed6..af32d89c7e 100644 --- a/tests/test_launcher.py +++ b/tests/test_launcher.py @@ -330,28 +330,6 @@ def test_remove_temp_dir_on_exit(mapdl, cleared, tmpdir): assert os.path.isdir(tmp_dir) -@requires("local") -@requires("nostudent") -def test_remove_temp_dir_on_exit_fail(mapdl, cleared, tmpdir): - """Ensure the working directory is not removed when the cwd is changed.""" - mapdl_ = launch_mapdl( - port=mapdl.port + 1, - remove_temp_dir_on_exit=True, - start_timeout=start_timeout, - additional_switches=QUICK_LAUNCH_SWITCHES, - ) - old_path = mapdl_.directory - assert os.path.isdir(str(tmpdir)) - mapdl_.cwd(str(tmpdir)) - path = mapdl_.directory - mapdl_.exit() - assert os.path.isdir(path) - - # Checking no changes in the old path - assert os.path.isdir(old_path) - assert os.listdir(old_path) - - @pytest.mark.requires_gui @pytest.mark.parametrize( "include_result,inplace,to_check", From c9b82dbe6287bcf72fb0c82355d66f168c466ec4 Mon Sep 17 00:00:00 2001 From: pyansys-ci-bot <92810346+pyansys-ci-bot@users.noreply.github.com> Date: Tue, 21 Jan 2025 15:07:53 +0000 Subject: [PATCH 24/42] chore: adding changelog file 3649.maintenance.md [dependabot-skip] --- doc/changelog.d/{3649.dependencies.md => 3649.maintenance.md} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename doc/changelog.d/{3649.dependencies.md => 3649.maintenance.md} (100%) diff --git a/doc/changelog.d/3649.dependencies.md b/doc/changelog.d/3649.maintenance.md similarity index 100% rename from doc/changelog.d/3649.dependencies.md rename to doc/changelog.d/3649.maintenance.md From 282812c5e0e7d3f70945eaa01b3b9e6089608b69 Mon Sep 17 00:00:00 2001 From: German <28149841+germa89@users.noreply.github.com> Date: Wed, 22 Jan 2025 11:36:36 +0100 Subject: [PATCH 25/42] test: remove redundant tests --- tests/test_mapdl.py | 38 -------------------------------------- 1 file changed, 38 deletions(-) diff --git a/tests/test_mapdl.py b/tests/test_mapdl.py index afad366a70..57b828ece7 100644 --- a/tests/test_mapdl.py +++ b/tests/test_mapdl.py @@ -28,7 +28,6 @@ from pathlib import Path import re import shutil -import tempfile import time from unittest.mock import patch from warnings import catch_warnings @@ -2276,43 +2275,6 @@ def test_use_vtk(mapdl, cleared): mapdl.use_vtk = prev -@requires("local") -@pytest.mark.xfail(reason="Flaky test. See #2435") -def test_remove_temp_dir_on_exit(mapdl, cleared, tmpdir): - path = os.path.join(tempfile.gettempdir(), "ansys_" + random_string()) - os.makedirs(path) - filename = os.path.join(path, "file.txt") - with open(filename, "w") as f: - f.write("Hello World") - assert os.path.exists(filename) - - prev = mapdl.remove_temp_dir_on_exit - mapdl.remove_temp_dir_on_exit = True - mapdl._local = True # Sanity check - mapdl._remove_temp_dir_on_exit(path) - mapdl.remove_temp_dir_on_exit = prev - - assert os.path.exists(filename) is False - assert os.path.exists(path) is False - - -@requires("local") -@requires("nostudent") -@pytest.mark.xfail(reason="Flaky test. See #2435") -def test_remove_temp_dir_on_exit_with_launch_mapdl(mapdl, cleared): - - mapdl_2 = launch_mapdl(remove_temp_dir_on_exit=True, port=PORT1) - path_ = mapdl_2.directory - assert os.path.exists(path_) - - pids = mapdl_2._pids - assert all([psutil.pid_exists(pid) for pid in pids]) # checking pids too - - mapdl_2.exit() - assert not os.path.exists(path_) - assert not all([psutil.pid_exists(pid) for pid in pids]) - - def test_sys(mapdl, cleared): assert "hi" in mapdl.sys("echo 'hi'") From 3c0359b5ebc4dd3f9712a50e1ee2afc1c7a7bb39 Mon Sep 17 00:00:00 2001 From: German <28149841+germa89@users.noreply.github.com> Date: Wed, 22 Jan 2025 12:33:15 +0100 Subject: [PATCH 26/42] ci: marking test as flacky. Ref https://github.com/ansys/pymapdl/issues/2435 #2435 --- tests/test_pool.py | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/test_pool.py b/tests/test_pool.py index 2a0262b3b2..2139faf197 100644 --- a/tests/test_pool.py +++ b/tests/test_pool.py @@ -292,6 +292,7 @@ def test_directory_names_custom_string(self, tmpdir): assert all(["my_instance" in each for each in dirs_path_pool]) @skip_if_ignore_pool + @pytest.mark.xfail(reason="Flaky test. See #2435") def test_directory_names_function(self, tmpdir): def myfun(i): if i == 0: From dc28563d311ab9ec12fe2e34b5608c492c4c69c7 Mon Sep 17 00:00:00 2001 From: German <28149841+germa89@users.noreply.github.com> Date: Mon, 27 Jan 2025 15:54:01 +0100 Subject: [PATCH 27/42] feat: starting to write launch_mapdl_on_cluster_locally --- src/ansys/mapdl/core/launcher/__init__.py | 2 + src/ansys/mapdl/core/launcher/hpc.py | 291 +++++++++++++++++++--- src/ansys/mapdl/core/launcher/launcher.py | 2 +- 3 files changed, 260 insertions(+), 35 deletions(-) diff --git a/src/ansys/mapdl/core/launcher/__init__.py b/src/ansys/mapdl/core/launcher/__init__.py index 8ef00b7f19..e9df630d5f 100644 --- a/src/ansys/mapdl/core/launcher/__init__.py +++ b/src/ansys/mapdl/core/launcher/__init__.py @@ -38,6 +38,8 @@ from ansys.mapdl.core.launcher.console import launch_mapdl_console +from ansys.mapdl.core.launcher.grpc import launch_mapdl_grpc +from ansys.mapdl.core.launcher.hpc import launch_mapdl_on_cluster_locally from ansys.mapdl.core.launcher.launcher import launch_mapdl from ansys.mapdl.core.launcher.remote import connect_to_mapdl from ansys.mapdl.core.launcher.tools import ( diff --git a/src/ansys/mapdl/core/launcher/hpc.py b/src/ansys/mapdl/core/launcher/hpc.py index a02a50eb68..322a2922fb 100644 --- a/src/ansys/mapdl/core/launcher/hpc.py +++ b/src/ansys/mapdl/core/launcher/hpc.py @@ -29,13 +29,11 @@ from ansys.mapdl.core import LOG from ansys.mapdl.core.errors import MapdlDidNotStart from ansys.mapdl.core.launcher.grpc import launch_grpc -from ansys.mapdl.core.launcher.local import processing_local_arguments from ansys.mapdl.core.launcher.tools import ( generate_start_parameters, get_port, submitter, ) -from ansys.mapdl.core.licensing import LicenseChecker from ansys.mapdl.core.mapdl_grpc import MapdlGrpc LAUNCH_ON_HCP_ERROR_MESSAGE_IP = ( @@ -272,7 +270,7 @@ def get_state_from_scontrol(stdout: str) -> str: return stdout.split("JobState=")[1].splitlines()[0].strip() -def launch_mapdl_on_cluster( +def launch_mapdl_on_cluster_locally( nproc: int, *, scheduler_options: Union[str, Dict[str, str]] = None, @@ -291,6 +289,9 @@ def launch_mapdl_on_cluster( A string or dictionary specifying the job configuration for the scheduler. For example ``scheduler_options = "-N 10"``. + launch_mapdl_args : Dict[str, Any], optional + Any keyword argument from the :func:`ansys.mapdl.core.launcher.grpc.launch_mapdl_grpc` function. + Returns ------- MapdlGrpc @@ -309,10 +310,18 @@ def launch_mapdl_on_cluster( ) """ - from ansys.mapdl.core.launcher import launch_mapdl + # from ansys.mapdl.core.launcher import launch_mapdl # Processing the arguments launch_mapdl_args["launch_on_hpc"] = True + launch_mapdl_args["running_on_hpc"] = True + + if launch_mapdl_args.get("license_server_check", False): + raise ValueError( + "The argument 'license_server_check' is not allowed when launching on an HPC platform." + ) + + launch_mapdl_args["license_server_check"] = False if launch_mapdl_args.get("mode", "grpc") != "grpc": raise ValueError( @@ -327,32 +336,254 @@ def launch_mapdl_on_cluster( "The 'start_instance' argument must be 'True' when launching on HPC." ) - return launch_mapdl( + if launch_mapdl_args.get("mapdl_output", False): + raise ValueError( + "The 'mapdl_output' argument is not allowed when launching on an HPC platform." + ) + + return launch_mapdl_grpc_on_hpc( nproc=nproc, scheduler_options=scheduler_options, **launch_mapdl_args, ) -def launch_mapdl_grpc(): # to be fixed - args = processing_local_arguments(locals()) +def launch_mapdl_grpc_on_hpc( + *, + exec_file: Optional[str] = None, + run_location: Optional[str] = None, + jobname: str = "file", + nproc: Optional[int] = None, + ram: Optional[Union[int, str]] = None, + mode: Optional[str] = None, + override: bool = False, + loglevel: str = "ERROR", + additional_switches: str = "", + start_timeout: Optional[int] = None, + port: Optional[int] = None, + cleanup_on_exit: bool = True, + start_instance: Optional[bool] = None, + clear_on_connect: bool = True, + log_apdl: Optional[Union[bool, str]] = None, + remove_temp_dir_on_exit: bool = False, + license_server_check: bool = False, + license_type: Optional[bool] = None, + print_com: bool = False, + add_env_vars: Optional[Dict[str, str]] = None, + replace_env_vars: Optional[Dict[str, str]] = None, + version: Optional[Union[int, str]] = None, + running_on_hpc: bool = True, + launch_on_hpc: bool = False, + **kwargs: Dict[str, Any], +) -> MapdlGrpc: + """Start MAPDL locally with gRPC interface. + + Parameters + ---------- + exec_file : str, optional + The location of the MAPDL executable. Will use the cached + location when left at the default :class:`None` and no environment + variable is set. + + The executable path can be also set through the environment variable + :envvar:`PYMAPDL_MAPDL_EXEC`. For example: + + .. code:: console + + export PYMAPDL_MAPDL_EXEC=/ansys_inc/v211/ansys/bin/mapdl + + run_location : str, optional + MAPDL working directory. Defaults to a temporary working + directory. If directory doesn't exist, one is created. + + jobname : str, optional + MAPDL jobname. Defaults to ``'file'``. + + nproc : int, optional + Number of processors. Defaults to ``2``. If running on an HPC cluster, + this value is adjusted to the number of CPUs allocated to the job, + unless the argument ``running_on_hpc`` is set to ``"false"``. + + ram : float, optional + Total size in megabytes of the workspace (memory) used for the initial + allocation. The default is :class:`None`, in which case 2 GB (2048 MB) is + used. To force a fixed size throughout the run, specify a negative + number. + + mode : str, optional + Mode to launch MAPDL. Must be one of the following: + + - ``'grpc'`` + - ``'console'`` + + The ``'grpc'`` mode is available on ANSYS 2021R1 or newer and + provides the best performance and stability. + The ``'console'`` mode is for legacy use only Linux only prior to 2020R2. + This console mode is pending depreciation. + Visit :ref:`versions_and_interfaces` for more information. + + override : bool, optional + Attempts to delete the lock file at the ``run_location``. + Useful when a prior MAPDL session has exited prematurely and + the lock file has not been deleted. + + loglevel : str, optional + Sets which messages are printed to the console. ``'INFO'`` + prints out all ANSYS messages, ``'WARNING'`` prints only + messages containing ANSYS warnings, and ``'ERROR'`` logs only + error messages. + + additional_switches : str, optional + Additional switches for MAPDL, for example ``'aa_r'``, the + academic research license, would be added with: + + - ``additional_switches="-aa_r"`` + + Avoid adding switches like ``-i``, ``-o`` or ``-b`` as these are already + included to start up the MAPDL server. See the notes + section for additional details. + + start_timeout : float, optional + Maximum allowable time to connect to the MAPDL server. By default it is + 45 seconds, however, it is increased to 90 seconds if running on HPC. + + port : int + Port to launch MAPDL gRPC on. Final port will be the first + port available after (or including) this port. Defaults to + ``50052``. You can also provide this value through the environment variable + :envvar:`PYMAPDL_PORT`. For instance ``PYMAPDL_PORT=50053``. + However the argument (if specified) has precedence over the environment + variable. If this environment variable is empty, it is as it is not set. + + cleanup_on_exit : bool, optional + Exit MAPDL when python exits or the mapdl Python instance is + garbage collected. + + start_instance : bool, optional + When :class:`False`, connect to an existing MAPDL instance at ``ip`` + and ``port``, which default to ip ``'127.0.0.1'`` at port ``50052``. + Otherwise, launch a local instance of MAPDL. You can also + provide this value through the environment variable + :envvar:`PYMAPDL_START_INSTANCE`. + However the argument (if specified) has precedence over the environment + variable. If this environment variable is empty, it is as it is not set. + + clear_on_connect : bool, optional + Defaults to :class:`True`, giving you a fresh environment when + connecting to MAPDL. When if ``start_instance`` is specified + it defaults to :class:`False`. + + log_apdl : str, optional + Enables logging every APDL command to the local disk. This + can be used to "record" all the commands that are sent to + MAPDL via PyMAPDL so a script can be run within MAPDL without + PyMAPDL. This argument is the path of the output file (e.g. + ``log_apdl='pymapdl_log.txt'``). By default this is disabled. + + remove_temp_dir_on_exit : bool, optional + When ``run_location`` is :class:`None`, this launcher creates a new MAPDL + working directory within the user temporary directory, obtainable with + ``tempfile.gettempdir()``. When this parameter is + :class:`True`, this directory will be deleted when MAPDL is exited. + Default to :class:`False`. + If you change the working directory, PyMAPDL does not delete the original + working directory nor the new one. + + license_server_check : bool, optional + Check if the license server is available if MAPDL fails to + start. Only available on ``mode='grpc'``. Defaults :class:`False`. + + license_type : str, optional + Enable license type selection. You can input a string for its + license name (for example ``'meba'`` or ``'ansys'``) or its description + ("enterprise solver" or "enterprise" respectively). + You can also use legacy licenses (for example ``'aa_t_a'``) but it will + also raise a warning. If it is not used (:class:`None`), no specific + license will be requested, being up to the license server to provide a + specific license type. Default is :class:`None`. + + print_com : bool, optional + Print the command ``/COM`` arguments to the standard output. + Default :class:`False`. + + add_env_vars : dict, optional + The provided dictionary will be used to extend the MAPDL process + environment variables. If you want to control all of the environment + variables, use the argument ``replace_env_vars``. + Defaults to :class:`None`. + + replace_env_vars : dict, optional + The provided dictionary will be used to replace all the MAPDL process + environment variables. It replace the system environment variables + which otherwise would be used in the process. + To just add some environment variables to the MAPDL + process, use ``add_env_vars``. Defaults to :class:`None`. + + version : float, optional + Version of MAPDL to launch. If :class:`None`, the latest version is used. + Versions can be provided as integers (i.e. ``version=222``) or + floats (i.e. ``version=22.2``). + To retrieve the available installed versions, use the function + :meth:`ansys.tools.path.path.get_available_ansys_installations`. + You can also provide this value through the environment variable + :envvar:`PYMAPDL_MAPDL_VERSION`. + For instance ``PYMAPDL_MAPDL_VERSION=22.2``. + However the argument (if specified) has precedence over the environment + variable. If this environment variable is empty, it is as it is not set. + + kwargs : dict, Optional + These keyword arguments are interface-specific or for + development purposes. For more information, see Notes. + + scheduler_options : :class:`str`, :class:`dict` + Use it to specify options to the scheduler run command. It can be a + string or a dictionary with arguments and its values (both as strings). + For more information visit :ref:`ref_hpc_slurm`. + + set_no_abort : :class:`bool` + *(Development use only)* + Sets MAPDL to not abort at the first error within /BATCH mode. + Defaults to :class:`True`. + + force_intel : :class:`bool` + *(Development use only)* + Forces the use of Intel message pass interface (MPI) in versions between + Ansys 2021R0 and 2022R2, where because of VPNs issues this MPI is + deactivated by default. + See :ref:`vpn_issues_troubleshooting` for more information. + Defaults to :class:`False`. + + Returns + ------- + MapdlGrpc + An instance of Mapdl. + """ + args = pack_arguments(locals()) # packs args and kwargs + + check_kwargs(args) # check if passing wrong key arguments + + pre_check_args(args) + + if is_running_on_slurm(args): + LOG.info("On Slurm mode.") + + # extracting parameters + get_slurm_options(args, kwargs) + + get_start_instance_arg(args) + + get_cpus(args) + + get_ip(args) + + args["port"] = get_port(args["port"], args["start_instance"]) + if args.get("mode", "grpc") != "grpc": raise ValueError("Invalid 'mode'.") args["port"] = get_port(args["port"], args["start_instance"]) start_parm = generate_start_parameters(args) - # Early exit for debugging. - if args["_debug_no_launch"]: - # Early exit, just for testing - return args # type: ignore - - # Check the license server - if args["license_server_check"]: - LOG.debug("Checking license server.") - lic_check = LicenseChecker(timeout=args["start_timeout"]) - lic_check.start() - ######################################## # Launch MAPDL with gRPC # ---------------------- @@ -387,27 +618,16 @@ def launch_mapdl_grpc(): # to be fixed jobid: int = start_parm.get("jobid", "Not found") - if ( - args["launch_on_hpc"] - and start_parm.get("finish_job_on_exit", True) - and jobid not in ["Not found", None] - ): + if start_parm.get("finish_job_on_exit", True) and jobid not in [ + "Not found", + None, + ]: LOG.debug(f"Killing HPC job with id: {jobid}") kill_job(jobid) - if args["license_server_check"]: - LOG.debug("Checking license server.") - lic_check.check() - raise exception - if args["just_launch"]: - out = [args["ip"], args["port"]] - if hasattr(process, "pid"): - out += [process.pid] - return out - ######################################## # Connect to MAPDL using gRPC # --------------------------- @@ -425,7 +645,10 @@ def launch_mapdl_grpc(): # to be fixed ) except Exception as exception: - LOG.error("An error occurred when connecting to MAPDL.") + jobid = start_parm.get("jobid", "'Not found'") + LOG.error( + f"An error occurred when connecting to the MAPDL instance running on job {jobid}." + ) raise exception return mapdl diff --git a/src/ansys/mapdl/core/launcher/launcher.py b/src/ansys/mapdl/core/launcher/launcher.py index 41f2f5a792..6209a79b2e 100644 --- a/src/ansys/mapdl/core/launcher/launcher.py +++ b/src/ansys/mapdl/core/launcher/launcher.py @@ -551,7 +551,7 @@ def launch_mapdl( # if args["start_instance"]: # ON HPC: - # Assuming that if login node is ubuntu, the computation ones + # Assuming that if login node is ubuntu, the computation nodes # are also ubuntu. env_vars = configure_ubuntu(env_vars) From fc547561b9c0325f595f8b7c191567d3b8524719 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 14 Feb 2025 10:40:28 +0100 Subject: [PATCH 28/42] build: bump the minimal group with 2 updates (#3742) * build: bump the minimal group with 2 updates Bumps the minimal group with 2 updates: [numpy](https://github.com/numpy/numpy) and [psutil](https://github.com/giampaolo/psutil). Updates from 2.2.2 to 2.2.3 - [Release notes](https://github.com/numpy/numpy/releases) - [Changelog](https://github.com/numpy/numpy/blob/main/doc/RELEASE_WALKTHROUGH.rst) - [Commits](https://github.com/numpy/numpy/compare/v2.2.2...v2.2.3) Updates from 6.1.1 to 7.0.0 - [Changelog](https://github.com/giampaolo/psutil/blob/master/HISTORY.rst) - [Commits](https://github.com/giampaolo/psutil/compare/release-6.1.1...release-7.0.0) --- updated-dependencies: - dependency-name: numpy dependency-type: direct:production update-type: version-update:semver-patch dependency-group: minimal - dependency-name: psutil dependency-type: direct:production update-type: version-update:semver-major dependency-group: minimal ... Signed-off-by: dependabot[bot] * chore: adding changelog file 3742.maintenance.md [dependabot-skip] --------- Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: pyansys-ci-bot <92810346+pyansys-ci-bot@users.noreply.github.com> --- doc/changelog.d/3742.maintenance.md | 1 + minimum_requirements.txt | 4 ++-- 2 files changed, 3 insertions(+), 2 deletions(-) create mode 100644 doc/changelog.d/3742.maintenance.md diff --git a/doc/changelog.d/3742.maintenance.md b/doc/changelog.d/3742.maintenance.md new file mode 100644 index 0000000000..f6c510962f --- /dev/null +++ b/doc/changelog.d/3742.maintenance.md @@ -0,0 +1 @@ +build: bump the minimal group with 2 updates \ No newline at end of file diff --git a/minimum_requirements.txt b/minimum_requirements.txt index fda055b768..252037cbe4 100644 --- a/minimum_requirements.txt +++ b/minimum_requirements.txt @@ -1,5 +1,5 @@ ansys-api-mapdl==0.5.2 -numpy==2.2.1 +numpy==2.2.3 platformdirs==4.3.6 -psutil==6.1.1 +psutil==7.0.0 pyansys-tools-versioning==0.6.0 \ No newline at end of file From ff906af2a39312592b7a7a18b205d205e5e8e908 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 14 Feb 2025 10:38:19 +0100 Subject: [PATCH 29/42] build: bump pyansys-tools-report from 0.8.1 to 0.8.2 in the testing group (#3744) * build: bump pyansys-tools-report in the testing group Bumps the testing group with 1 update: [pyansys-tools-report](https://github.com/ansys/pyansys-tools-report). Updates `pyansys-tools-report` from 0.8.1 to 0.8.2 - [Release notes](https://github.com/ansys/pyansys-tools-report/releases) - [Commits](https://github.com/ansys/pyansys-tools-report/compare/v0.8.1...v0.8.2) --- updated-dependencies: - dependency-name: pyansys-tools-report dependency-type: direct:production update-type: version-update:semver-patch dependency-group: testing ... Signed-off-by: dependabot[bot] * chore: adding changelog file 3744.dependencies.md [dependabot-skip] --------- Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: pyansys-ci-bot <92810346+pyansys-ci-bot@users.noreply.github.com> --- doc/changelog.d/3744.dependencies.md | 1 + pyproject.toml | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) create mode 100644 doc/changelog.d/3744.dependencies.md diff --git a/doc/changelog.d/3744.dependencies.md b/doc/changelog.d/3744.dependencies.md new file mode 100644 index 0000000000..fd67384d6c --- /dev/null +++ b/doc/changelog.d/3744.dependencies.md @@ -0,0 +1 @@ +build: bump pyansys-tools-report from 0.8.1 to 0.8.2 in the testing group \ No newline at end of file diff --git a/pyproject.toml b/pyproject.toml index 5db768103d..23a1b45712 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -59,7 +59,7 @@ tests = [ "autopep8==2.3.2", "matplotlib==3.10.0", "pandas==2.2.3", - "pyansys-tools-report==0.8.1", + "pyansys-tools-report==0.8.2", "pyfakefs==5.7.4", "pyiges[full]==0.3.1", "pytest-cov==6.0.0", From 7e3e2fe35725305840aeed919d9ce7f56f0b20a9 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 14 Feb 2025 10:10:39 +0100 Subject: [PATCH 30/42] build: bump sphinx-gallery from 0.18.0 to 0.19.0 in the documentation group (#3743) * build: bump sphinx-gallery in the documentation group Bumps the documentation group with 1 update: [sphinx-gallery](https://github.com/sphinx-gallery/sphinx-gallery). Updates `sphinx-gallery` from 0.18.0 to 0.19.0 - [Release notes](https://github.com/sphinx-gallery/sphinx-gallery/releases) - [Changelog](https://github.com/sphinx-gallery/sphinx-gallery/blob/master/.github_changelog_generator) - [Commits](https://github.com/sphinx-gallery/sphinx-gallery/compare/v0.18.0...v0.19.0) --- updated-dependencies: - dependency-name: sphinx-gallery dependency-type: direct:production update-type: version-update:semver-minor dependency-group: documentation ... Signed-off-by: dependabot[bot] * chore: adding changelog file 3743.dependencies.md [dependabot-skip] --------- Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: pyansys-ci-bot <92810346+pyansys-ci-bot@users.noreply.github.com> --- doc/changelog.d/3743.dependencies.md | 1 + pyproject.toml | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) create mode 100644 doc/changelog.d/3743.dependencies.md diff --git a/doc/changelog.d/3743.dependencies.md b/doc/changelog.d/3743.dependencies.md new file mode 100644 index 0000000000..61312683d7 --- /dev/null +++ b/doc/changelog.d/3743.dependencies.md @@ -0,0 +1 @@ +build: bump sphinx-gallery from 0.18.0 to 0.19.0 in the documentation group \ No newline at end of file diff --git a/pyproject.toml b/pyproject.toml index 23a1b45712..bc4d2a86ff 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -96,7 +96,7 @@ doc = [ "sphinx-autodoc-typehints==1.25.2", "sphinx-copybutton==0.5.2", "sphinx-design==0.6.1", - "sphinx-gallery==0.18.0", + "sphinx-gallery==0.19.0", "sphinx-jinja==2.0.2", "sphinx-notfound-page==1.0.4", "sphinx==8.1.3", From 0e0ca5b288307c1bd6b9a3455a8c2e4c0a3b4ca1 Mon Sep 17 00:00:00 2001 From: German <28149841+germa89@users.noreply.github.com> Date: Wed, 12 Feb 2025 20:36:33 +0100 Subject: [PATCH 31/42] docs: homgenizing commit/branches/pull request prefix (#3737) * docs: homogenizing prefixes * docs: improving table header * chore: adding changelog file 3737.documentation.md [dependabot-skip] --------- Co-authored-by: pyansys-ci-bot <92810346+pyansys-ci-bot@users.noreply.github.com> --- doc/changelog.d/3737.documentation.md | 1 + .../getting_started/develop_pymapdl.rst | 109 ++++++++++++++---- .../config/vocabularies/ANSYS/accept.txt | 3 + 3 files changed, 88 insertions(+), 25 deletions(-) create mode 100644 doc/changelog.d/3737.documentation.md diff --git a/doc/changelog.d/3737.documentation.md b/doc/changelog.d/3737.documentation.md new file mode 100644 index 0000000000..d4a6f9c19c --- /dev/null +++ b/doc/changelog.d/3737.documentation.md @@ -0,0 +1 @@ +docs: homogenizing commit/branches/pull request prefix \ No newline at end of file diff --git a/doc/source/getting_started/develop_pymapdl.rst b/doc/source/getting_started/develop_pymapdl.rst index 242a7ef2f6..d0786ee22b 100644 --- a/doc/source/getting_started/develop_pymapdl.rst +++ b/doc/source/getting_started/develop_pymapdl.rst @@ -63,35 +63,46 @@ guidelines for developing code in a repository: #. **Use branches**: Create branches for different features, bug fixes, or experiments. This keeps changes isolated and facilitates parallel development. The CI/CD checks that the branch name is compliant. For example, - the branch name must start with a prefix and a backslash. + the branch name must start with a lower case prefix and a backslash. The allowed prefixes are: - - `fix/` - Bug fixes. - - `feat/` - Changes that introduce a new feature or significant addition. - - `maint/` - General maintenance of the repository. For instance, improving the CI/CD workflows. + - `build/` - Changes that affect the build system or external dependencies (such as to ``pip`` or ``make``). + - `ci/` - Changes to the CI/CD configuration files and scripts. + - `dependabot/` - Created by Dependabot. - `docs/` - Improves documentation and examples. + - `feat/` - Changes that introduce a new feature or significant addition. + - `fix/` - Bug fixes. + - `junk/` - Other purposes. It should not be used for branches that are going to be merged to ``main``. + - `maint/` - General maintenance of the repository. - `no-ci/` - (Not applicable to PyMAPDL) In some repositories, branches with this prefix do not trigger CI/CD. - - `test/` - Improvements or changes to testing. - - `testing/` - For testing and debugging. It should not be used for branches that are going to be merged to ``main``. + - `perf/` - A code change that improves performance. + - `refactor/` - A code change that neither fixes a bug nor adds a feature. - `release/` - Contains the released versions changes. - - `dependabot/` - Created by Dependabot. - - `junk/` - Other purposes. It should not be used for branches that are going to be merged to ``main``. + - `revert/` - Reverts a previous commit. + - `testing/` - For testing and debugging. It can be used to add new tests. + + **Note**: For more information, see `Table of allowed prefix `_. #. **Write descriptive commit messages**: Provide clear and concise commit messages that explain the purpose and context of the changes. Follow a consistent style. - - `fix:` - Bug fixes. - - `feat:` - Changes that introduce a new feature or significant addition. - - `docs:` - Changes pertaining only to documentation. - - `style:` - Changes that do not affect the meaning of the code (such as white space, formatting, and missing semicolons). - - `refactor:` - A code change that neither fixes a bug nor adds a feature. - - `perf:` - A code change that improves performance. - - `test:` - Improvements or changes to testing. - `build:` - Changes that affect the build system or external dependencies (such as to ``pip`` or ``make``). + - `chore:` - Other changes that don't modify the code. It can be used as a fall back general branch name. - `ci:` - Changes to the CI/CD configuration files and scripts. - - `chore:` - Other changes that don't modify the code (such as releasing and versioning). + - `docs:` - Improves documentation and examples. + - `feat:` - Changes that introduce a new feature or significant addition. + - `fix:` - Bug fixes. + - `maint:` - General maintenance of the repository. + - `no-ci:` - (Not applicable to PyMAPDL) In some repositories, branches with this prefix do not trigger CI/CD. + - `perf:` - A code change that improves performance. + - `refactor:` - A code change that neither fixes a bug nor adds a feature. + - `release:` - Contains the released versions changes. - `revert:` - Reverts a previous commit. + - `style:` - Changes that do not affect the meaning of the code (white-space, formatting, missing semi-colons, etc). + - `testing:` - For testing and debugging. It can be used to add new tests. + + **Note**: For more information, see `Table of allowed prefix `_. #. **Commit frequently**: Make small, meaningful commits frequently. Avoid making a large number of unrelated changes in a single commit. @@ -105,17 +116,19 @@ guidelines for developing code in a repository: Pull requests must follow the same convention as the commit messages. The following prefixes are allowed in the pull request names: - - `fix:` - Bug fixes. - - `feat:` - Changes that introduce a new feature or significant addition. - - `docs:` - Changes pertaining only to documentation. - - `style:` - Changes that do not affect the meaning of the code (such as white space, formatting, and missing semicolons). - - `refactor:` - A code change that neither fixes a bug nor adds a feature. - - `perf:` - A code change that improves performance. - - `test:` - Improvements or changes to testing. - `build:` - Changes that affect the build system or external dependencies (such as to ``pip`` or ``make``). - `ci:` - Changes to the CI/CD configuration files and scripts. - - `chore:` - Other changes that don't modify the code (such as releasing and versioning). - - `revert:` - Reverts a previous pull request. + - `docs:` - Improves documentation and examples. + - `feat:` - Changes that introduce a new feature or significant addition. + - `fix:` - Bug fixes. + - `maint:` - General maintenance of the repository. + - `no-ci:` - (Not applicable to PyMAPDL) In some repositories, branches with this prefix do not trigger CI/CD. + - `perf:` - A code change that improves performance. + - `refactor:` - A code change that neither fixes a bug nor adds a feature. + - `revert:` - Reverts a previous commit. + - `testing:` - For testing and debugging. It can be used to add new tests. + + **Note**: For more information, see `Table of allowed prefix `_. The pull requests can also be labeled for easier repository maintenance. The CI/CD automatically labels each pull request based on the pull requests prefix and @@ -149,6 +162,52 @@ guidelines for developing code in a repository: By following these guidelines, you can ensure smooth and organized code development within a repository, fostering collaboration, code quality, and feature enhancement. +**Table of allowed prefix** + +.. _table_prefix: + ++-------------+-----------------------------+------------------------------+----------------------------------+ +| Prefix | Commit (``prefix:``) | Branch (``prefix/``) | Pull-request (``prefix:``) | ++=============+=============================+==============================+==================================+ +| `build` | |:white_check_mark:| | |:white_check_mark:| | |:white_check_mark:| | ++-------------+-----------------------------+------------------------------+----------------------------------+ +| `dependabot`| |:x:| | |:white_check_mark:| | |:x:| | ++-------------+-----------------------------+------------------------------+----------------------------------+ +| `chore` | |:white_check_mark:| | |:x:| | |:x:| | ++-------------+-----------------------------+------------------------------+----------------------------------+ +| `ci` | |:white_check_mark:| | |:white_check_mark:| | |:white_check_mark:| | ++-------------+-----------------------------+------------------------------+----------------------------------+ +| `docs` | |:white_check_mark:| | |:white_check_mark:| | |:white_check_mark:| | ++-------------+-----------------------------+------------------------------+----------------------------------+ +| `feat` | |:white_check_mark:| | |:white_check_mark:| | |:white_check_mark:| | ++-------------+-----------------------------+------------------------------+----------------------------------+ +| `fix` | |:white_check_mark:| | |:white_check_mark:| | |:white_check_mark:| | ++-------------+-----------------------------+------------------------------+----------------------------------+ +| `junk` | |:x:| | |:white_check_mark:| | |:x:| | ++-------------+-----------------------------+------------------------------+----------------------------------+ +| `maint` | |:white_check_mark:| | |:white_check_mark:| | |:white_check_mark:| | ++-------------+-----------------------------+------------------------------+----------------------------------+ +| `no-ci` | |:white_check_mark:| | |:white_check_mark:| | |:white_check_mark:| | ++-------------+-----------------------------+------------------------------+----------------------------------+ +| `perf` | |:white_check_mark:| | |:white_check_mark:| | |:white_check_mark:| | ++-------------+-----------------------------+------------------------------+----------------------------------+ +| `refactor` | |:white_check_mark:| | |:white_check_mark:| | |:white_check_mark:| | ++-------------+-----------------------------+------------------------------+----------------------------------+ +| `release` | |:white_check_mark:| | |:white_check_mark:| | |:white_check_mark:| | ++-------------+-----------------------------+------------------------------+----------------------------------+ +| `revert` | |:white_check_mark:| | |:white_check_mark:| | |:x:| | ++-------------+-----------------------------+------------------------------+----------------------------------+ +| `style` | |:white_check_mark:| | |:x:| | |:x:| | ++-------------+-----------------------------+------------------------------+----------------------------------+ +| `testing` | |:white_check_mark:| | |:white_check_mark:| | |:x:| | ++-------------+-----------------------------+------------------------------+----------------------------------+ + + +Where: + +* |:white_check_mark:| means that the prefix is allowed. +* |:x:| means that the prefix is not allowed. + .. _ref_unit_testing_contributing: diff --git a/doc/styles/config/vocabularies/ANSYS/accept.txt b/doc/styles/config/vocabularies/ANSYS/accept.txt index 583fb27fac..4702e704cb 100644 --- a/doc/styles/config/vocabularies/ANSYS/accept.txt +++ b/doc/styles/config/vocabularies/ANSYS/accept.txt @@ -60,6 +60,7 @@ Dependabot Dev devcontainer DevContainer +Dimitris dof ect eigenfrequency @@ -88,6 +89,7 @@ HTML Image[Mm]agick imagin importlib +Ioannis ist Julia Krylov @@ -99,6 +101,7 @@ levl Linder Linux MacOS +maint mapdl MAPDL mater From c21864f3f47a83aa078f4e964100c81b11e74a74 Mon Sep 17 00:00:00 2001 From: German <28149841+germa89@users.noreply.github.com> Date: Wed, 12 Feb 2025 11:35:10 +0100 Subject: [PATCH 32/42] fix: harfrq command (#3729) * fix: harfrq command * chore: adding changelog file 3729.fixed.md [dependabot-skip] --------- Co-authored-by: pyansys-ci-bot <92810346+pyansys-ci-bot@users.noreply.github.com> --- doc/changelog.d/3729.fixed.md | 1 + src/ansys/mapdl/core/_commands/solution/dynamic_options.py | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) create mode 100644 doc/changelog.d/3729.fixed.md diff --git a/doc/changelog.d/3729.fixed.md b/doc/changelog.d/3729.fixed.md new file mode 100644 index 0000000000..97f1a2155a --- /dev/null +++ b/doc/changelog.d/3729.fixed.md @@ -0,0 +1 @@ +fix: harfrq command \ No newline at end of file diff --git a/src/ansys/mapdl/core/_commands/solution/dynamic_options.py b/src/ansys/mapdl/core/_commands/solution/dynamic_options.py index c44501d652..9e12aa400a 100644 --- a/src/ansys/mapdl/core/_commands/solution/dynamic_options.py +++ b/src/ansys/mapdl/core/_commands/solution/dynamic_options.py @@ -261,7 +261,7 @@ def harfrq(self, freqb="", freqe="", logopt="", freqarr="", toler="", **kwargs): This command is also valid in PREP7. """ - command = f"HARFRQ,{freqb},{freqe},{logopt},{freqarr},{toler}" + command = f"HARFRQ,{freqb},{freqe},,{logopt},{freqarr},{toler}" return self.run(command, **kwargs) def hrexp(self, angle="", **kwargs): From 1ad3c7d708eeb976429766fd2b60be5425d4f2ba Mon Sep 17 00:00:00 2001 From: German <28149841+germa89@users.noreply.github.com> Date: Tue, 11 Feb 2025 13:54:56 +0100 Subject: [PATCH 33/42] feat: adding opened attribute (#3731) * feat: adding opened attribute * feat: adding opened attribute * chore: adding changelog file 3731.miscellaneous.md [dependabot-skip] * docs: adding docstring * fix: tests * fix: test --------- Co-authored-by: pyansys-ci-bot <92810346+pyansys-ci-bot@users.noreply.github.com> --- doc/changelog.d/3731.miscellaneous.md | 1 + src/ansys/mapdl/core/xpl.py | 19 ++++++++++++++++++- tests/test_xpl.py | 25 ++++++++++++++++++++----- 3 files changed, 39 insertions(+), 6 deletions(-) create mode 100644 doc/changelog.d/3731.miscellaneous.md diff --git a/doc/changelog.d/3731.miscellaneous.md b/doc/changelog.d/3731.miscellaneous.md new file mode 100644 index 0000000000..e05ee10b4e --- /dev/null +++ b/doc/changelog.d/3731.miscellaneous.md @@ -0,0 +1 @@ +feat: adding opened attribute \ No newline at end of file diff --git a/src/ansys/mapdl/core/xpl.py b/src/ansys/mapdl/core/xpl.py index a7e0542bf2..539fa9d804 100644 --- a/src/ansys/mapdl/core/xpl.py +++ b/src/ansys/mapdl/core/xpl.py @@ -124,6 +124,7 @@ def close(self): """ response = self._mapdl.run("*XPL,CLOSE") self._check_ignored(response) + self._filename = None self._open = False return response @@ -373,6 +374,8 @@ def save(self): """Save the current file, ignoring the marked records.""" response = self._mapdl.run("*XPL,SAVE").strip() self._check_ignored(response) + self._open = False + self._filename = None return response def extract(self, recordname, sets="ALL", asarray=False): @@ -550,8 +553,22 @@ def write(self, recordname, vecname): def __repr__(self): txt = "MAPDL File Explorer\n" if self._open: - txt += "\tOpen file:%s" % self._filename + txt += f"\tOpen file : {self._filename}" txt += "\n".join(self.where().splitlines()[1:]) else: txt += "\tNo open file" return txt + + @property + def opened(self): + """ + Check if a file is currently open. + + Returns: + str or None: The filename if a file is open, otherwise None. + """ + + if self._open: + return self._filename + else: + return None diff --git a/tests/test_xpl.py b/tests/test_xpl.py index 9bae8c9da7..7bb66bf52b 100644 --- a/tests/test_xpl.py +++ b/tests/test_xpl.py @@ -46,6 +46,8 @@ def create_cube(self, mapdl): from conftest import clear clear(mapdl) + mapdl.clear() + mapdl.prep7() # set up the full file mapdl.block(0, 1, 0, 1, 0, 1) @@ -70,9 +72,12 @@ def create_cube(self, mapdl): if mapdl.result_file in mapdl.list_files(): mapdl.slashdelete(mapdl.result_file) + if "cube_solve_xpl" in mapdl.list_files(): + mapdl.slashdelete("cube_solve_xpl.db") + # solve first 10 non-trivial modes mapdl.modal_analysis(nmode=10, freqb=1) - mapdl.save("cube_solve_xpl") + mapdl.save("cube_solve_xpl", "db") @pytest.fixture(scope="class") def cube_solve(self, mapdl): @@ -81,17 +86,20 @@ def cube_solve(self, mapdl): @pytest.fixture(scope="function") def xpl(self, mapdl, cube_solve): mapdl.prep7() - mapdl.resume("cube_solve_xpl") + mapdl.resume("cube_solve_xpl", "db") xpl = mapdl.xpl if not self.full_file and not self.full_file in mapdl.list_files(): self.create_cube(mapdl) xpl.open(self.full_file) - return xpl - @staticmethod - def test_close(xpl): + yield xpl + + if xpl.opened: + xpl.close() + + def test_close(self, xpl): xpl.close() with pytest.raises(MapdlCommandIgnoredError): xpl.list() @@ -198,3 +206,10 @@ def test_extract(self, xpl): mat = xpl.extract("NSL") assert mat.shape == (243, 10) + + def test_opened(self, xpl): + assert xpl.opened + xpl.close() + assert not xpl.opened + xpl.open(self.full_file) + assert xpl.opened From 4d364ddbd37d756f59849e69c8f1d6b46541899c Mon Sep 17 00:00:00 2001 From: German <28149841+germa89@users.noreply.github.com> Date: Tue, 11 Feb 2025 11:44:52 +0100 Subject: [PATCH 34/42] build: update sphinx-autodoc-typehints to 3.0.1 (#3733) * build: update sphinx-autodoc-typehints to 3.0.1 * chore: adding changelog file 3733.dependencies.md [dependabot-skip] --------- Co-authored-by: pyansys-ci-bot <92810346+pyansys-ci-bot@users.noreply.github.com> --- .devcontainer/codespaces-docs/requirements.txt | 2 +- .devcontainer/devcontainer-local/requirements.txt | 2 +- doc/changelog.d/3733.dependencies.md | 1 + pyproject.toml | 2 +- 4 files changed, 4 insertions(+), 3 deletions(-) create mode 100644 doc/changelog.d/3733.dependencies.md diff --git a/.devcontainer/codespaces-docs/requirements.txt b/.devcontainer/codespaces-docs/requirements.txt index a6253ea2f8..3f78d22d3a 100644 --- a/.devcontainer/codespaces-docs/requirements.txt +++ b/.devcontainer/codespaces-docs/requirements.txt @@ -16,7 +16,7 @@ pypandoc==1.14 pytest-sphinx==0.6.3 pythreejs==2.4.2 sphinx-autobuild==2024.10.3 -sphinx-autodoc-typehints==1.25.2 +sphinx-autodoc-typehints==3.0.1 sphinx-copybutton==0.5.2 sphinx-design==0.6.1 sphinx-gallery==0.18.0 diff --git a/.devcontainer/devcontainer-local/requirements.txt b/.devcontainer/devcontainer-local/requirements.txt index ab6cb9c35f..1028f6e99c 100644 --- a/.devcontainer/devcontainer-local/requirements.txt +++ b/.devcontainer/devcontainer-local/requirements.txt @@ -26,7 +26,7 @@ pytest==8.3.3 pythreejs==2.4.2 scipy==1.14.1 sphinx-autobuild==2024.10.3 -sphinx-autodoc-typehints==1.25.2 +sphinx-autodoc-typehints==3.0.1 sphinx-copybutton==0.5.2 sphinx-design==0.6.1 sphinx-gallery==0.18.0 diff --git a/doc/changelog.d/3733.dependencies.md b/doc/changelog.d/3733.dependencies.md new file mode 100644 index 0000000000..b121a85f31 --- /dev/null +++ b/doc/changelog.d/3733.dependencies.md @@ -0,0 +1 @@ +build: update sphinx-autodoc-typehints to 3.0.1 \ No newline at end of file diff --git a/pyproject.toml b/pyproject.toml index bc4d2a86ff..edc1a92b9b 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -93,7 +93,7 @@ doc = [ "pytest-sphinx==0.6.3", "pythreejs==2.4.2", "sphinx-autobuild==2024.10.3", - "sphinx-autodoc-typehints==1.25.2", + "sphinx-autodoc-typehints==3.0.1", "sphinx-copybutton==0.5.2", "sphinx-design==0.6.1", "sphinx-gallery==0.19.0", From 062ac32a1804c0a37416f2416b5e5b471ead11ff Mon Sep 17 00:00:00 2001 From: German <28149841+germa89@users.noreply.github.com> Date: Mon, 10 Feb 2025 19:32:22 +0300 Subject: [PATCH 35/42] fix: ram units (#3730) * fix: ram units * chore: adding changelog file 3730.fixed.md [dependabot-skip] --------- Co-authored-by: pyansys-ci-bot <92810346+pyansys-ci-bot@users.noreply.github.com> --- doc/changelog.d/3730.fixed.md | 1 + tests/test_launcher/test_tools.py | 12 ++++++------ 2 files changed, 7 insertions(+), 6 deletions(-) create mode 100644 doc/changelog.d/3730.fixed.md diff --git a/doc/changelog.d/3730.fixed.md b/doc/changelog.d/3730.fixed.md new file mode 100644 index 0000000000..c73767bc51 --- /dev/null +++ b/doc/changelog.d/3730.fixed.md @@ -0,0 +1 @@ +fix: ram units \ No newline at end of file diff --git a/tests/test_launcher/test_tools.py b/tests/test_launcher/test_tools.py index 9fbe951010..8cf4debe2a 100644 --- a/tests/test_launcher/test_tools.py +++ b/tests/test_launcher/test_tools.py @@ -198,7 +198,7 @@ def test_generate_mapdl_launch_command_windows(): jobname = "myjob" nproc = 10 port = 1000 - ram = 2 + ram = 2024 additional_switches = "-my_add=switch" cmd = generate_mapdl_launch_command( @@ -218,7 +218,7 @@ def test_generate_mapdl_launch_command_windows(): assert "-port" in cmd assert f"{port}" in cmd assert "-m" in cmd - assert f"{ram*1024}" in cmd + assert f"{ram}" in cmd assert "-np" in cmd assert f"{nproc}" in cmd assert "-grpc" in cmd @@ -233,7 +233,7 @@ def test_generate_mapdl_launch_command_windows(): assert f"{exec_file}" in cmd assert f" -j {jobname} " in cmd assert f" -port {port} " in cmd - assert f" -m {ram*1024} " in cmd + assert f" -m {ram} " in cmd assert f" -np {nproc} " in cmd assert " -grpc" in cmd assert f" {additional_switches} " in cmd @@ -249,7 +249,7 @@ def test_generate_mapdl_launch_command_linux(): jobname = "myjob" nproc = 10 port = 1000 - ram = 2 + ram = 2024 additional_switches = "-my_add=switch" cmd = generate_mapdl_launch_command( @@ -271,7 +271,7 @@ def test_generate_mapdl_launch_command_linux(): assert "-port" in cmd assert f"{port}" in cmd assert "-m" in cmd - assert f"{ram*1024}" in cmd + assert f"{ram}" in cmd assert "-np" in cmd assert f"{nproc}" in cmd assert "-grpc" in cmd @@ -287,7 +287,7 @@ def test_generate_mapdl_launch_command_linux(): assert f"{exec_file} " in cmd assert f" -j {jobname} " in cmd assert f" -port {port} " in cmd - assert f" -m {ram*1024} " in cmd + assert f" -m {ram} " in cmd assert f" -np {nproc} " in cmd assert " -grpc" in cmd assert f" {additional_switches} " in cmd From 12a64c204685410f36fbe7030bcc6fd6dea011ba Mon Sep 17 00:00:00 2001 From: German <28149841+germa89@users.noreply.github.com> Date: Mon, 10 Feb 2025 12:52:30 +0300 Subject: [PATCH 36/42] fix: allow numpy types for parameters (#3720) * feat: allowing numpy types for setting parameters * docs: adding comment * chore: adding changelog file 3720.fixed.md [dependabot-skip] * fix: parameters length to 32+ * feat: update to 32 chars variable * fix: test --------- Co-authored-by: pyansys-ci-bot <92810346+pyansys-ci-bot@users.noreply.github.com> --- doc/changelog.d/3720.fixed.md | 1 + src/ansys/mapdl/core/mapdl_core.py | 12 ++++++--- src/ansys/mapdl/core/parameters.py | 26 ++++++++++++-------- tests/test_mapdl.py | 13 +++++++--- tests/test_parameters.py | 39 ++++++++++++++++++++++++++++++ 5 files changed, 73 insertions(+), 18 deletions(-) create mode 100644 doc/changelog.d/3720.fixed.md diff --git a/doc/changelog.d/3720.fixed.md b/doc/changelog.d/3720.fixed.md new file mode 100644 index 0000000000..6973b30c2f --- /dev/null +++ b/doc/changelog.d/3720.fixed.md @@ -0,0 +1 @@ +fix: allow numpy types for parameters \ No newline at end of file diff --git a/src/ansys/mapdl/core/mapdl_core.py b/src/ansys/mapdl/core/mapdl_core.py index bba4d31b0f..d0cc0b4edb 100644 --- a/src/ansys/mapdl/core/mapdl_core.py +++ b/src/ansys/mapdl/core/mapdl_core.py @@ -88,6 +88,8 @@ from ansys.mapdl.core.post import PostProcessing +MAX_PARAM_CHARS = 32 + DEBUG_LEVELS = Literal["DEBUG", "INFO", "WARNING", "ERROR"] VALID_DEVICES = ["PNG", "TIFF", "VRML", "TERM", "CLOSE"] @@ -2491,12 +2493,14 @@ def _check_parameter_name(self, param_name): param_name = param_name.strip() - match_valid_parameter_name = r"^[a-zA-Z_][a-zA-Z\d_\(\),\s\%]{0,31}$" + match_valid_parameter_name = ( + r"^[a-zA-Z_][a-zA-Z\d_\(\),\s\%]{0," + f"{MAX_PARAM_CHARS-1}" + r"}$" + ) # Using % is allowed, because of substitution, but it is very likely MAPDL will complain. if not re.search(match_valid_parameter_name, param_name): raise ValueError( - f"The parameter name `{param_name}` is an invalid parameter name." - "Only letters, numbers and `_` are permitted, up to 32 characters long." + f"The parameter name `{param_name}` is an invalid parameter name. " + f"Only letters, numbers and `_` are permitted, up to {MAX_PARAM_CHARS} characters long. " "It cannot start with a number either." ) @@ -2520,7 +2524,7 @@ def _check_parameter_name(self, param_name): # Using leading underscored parameters match_reserved_leading_underscored_parameter_name = ( - r"^_[a-zA-Z\d_\(\),\s_]{1,31}[a-zA-Z\d\(\),\s]$" + r"^_[a-zA-Z\d_\(\),\s_]{1," + f"{MAX_PARAM_CHARS}" + r"}[a-zA-Z\d\(\),\s]$" ) # If it also ends in underscore, this won't be triggered. if re.search(match_reserved_leading_underscored_parameter_name, param_name): diff --git a/src/ansys/mapdl/core/parameters.py b/src/ansys/mapdl/core/parameters.py index 594b40188e..aed30d671a 100644 --- a/src/ansys/mapdl/core/parameters.py +++ b/src/ansys/mapdl/core/parameters.py @@ -38,6 +38,7 @@ from ansys.mapdl.core.errors import MapdlRuntimeError from ansys.mapdl.core.mapdl import MapdlBase +from ansys.mapdl.core.mapdl_core import MAX_PARAM_CHARS from ansys.mapdl.core.misc import supress_logging ROUTINE_MAP = { @@ -354,7 +355,7 @@ def __repr__(self): value_str = str(info["value"]) else: continue - lines.append("%-32s : %s" % (key, value_str)) + lines.append(f"%-{MAX_PARAM_CHARS}s : %s" % (key, value_str)) return "\n".join(lines) def __getitem__(self, key): @@ -493,22 +494,27 @@ def _set_parameter(self, name, value): ---------- name : str An alphanumeric name used to identify this parameter. Name - may be up to 32 characters, beginning with a letter and - containing only letters, numbers, and underscores. + may be up to 32 character or the value given in + :attr:`ansys.mapdl.core.mapdl_core.MAX_PARAM_CHARS`, beginning with + a letter and containing only letters, numbers, and underscores. Examples: ``"ABC" "A3X" "TOP_END"``. """ - if not isinstance(value, (str, int, float)): + if not isinstance(value, (str, int, float, np.integer, np.floating)): raise TypeError("``Parameter`` must be either a float, int, or string") - if isinstance(value, str) and len(value) >= 32: - raise ValueError("Length of ``value`` must be 32 characters or less") + if isinstance(value, str) and len(value) > MAX_PARAM_CHARS: + raise ValueError( + f"Length of ``value`` must be {MAX_PARAM_CHARS} characters or less" + ) if not isinstance(name, str): raise TypeError("``name`` must be a string") - if len(name) >= 32: - raise ValueError("Length of ``name`` must be 32 characters or less") + if len(name) > MAX_PARAM_CHARS: + raise ValueError( + f"Length of ``name`` must be {MAX_PARAM_CHARS} characters or less" + ) # delete the parameter if it exists as an array parm = self._parm @@ -830,8 +836,8 @@ def interp_star_status(status): # line will contain either a character, scalar, or array name = items[0] if len(items) == 2 or "CHARACTER" in items[-1].upper(): - name = line[:32].strip() - value = line.replace(items[-1], "")[33:].strip() + name = line[:MAX_PARAM_CHARS].strip() + value = line.replace(items[-1], "")[(MAX_PARAM_CHARS + 1) :].strip() parameters[name] = {"type": "CHARACTER", "value": value} elif len(items) == 3: diff --git a/tests/test_mapdl.py b/tests/test_mapdl.py index d8f633c7a7..76b786f2e1 100644 --- a/tests/test_mapdl.py +++ b/tests/test_mapdl.py @@ -779,15 +779,20 @@ def test_set_parameters_string_spaces(mapdl, cleared): def test_set_parameters_too_long(mapdl, cleared): + from ansys.mapdl.core.mapdl_core import MAX_PARAM_CHARS + + parm_name = "a" * (MAX_PARAM_CHARS + 1) with pytest.raises( - ValueError, match="Length of ``name`` must be 32 characters or less" + ValueError, + match=f"The parameter name `{parm_name}` is an invalid parameter name.* {MAX_PARAM_CHARS} characters long", ): - mapdl.parameters["a" * 32] = 2 + mapdl.parameters[parm_name] = 2 with pytest.raises( - ValueError, match="Length of ``value`` must be 32 characters or less" + ValueError, + match=f"Length of ``value`` must be {MAX_PARAM_CHARS} characters or less", ): - mapdl.parameters["asdf"] = "a" * 32 + mapdl.parameters["asdf"] = "a" * (MAX_PARAM_CHARS + 1) def test_builtin_parameters(mapdl, cleared): diff --git a/tests/test_parameters.py b/tests/test_parameters.py index 3f6e8c2c9e..61bf263715 100644 --- a/tests/test_parameters.py +++ b/tests/test_parameters.py @@ -493,3 +493,42 @@ def test_failing_get_routine(mapdl, caplog, value): assert routine == ROUTINE_MAP[0] mapdl.logger.setLevel(prev_level) + + +@pytest.mark.parametrize( + "parameter", + [ + "asdf", + "32_chars_length", + 1, + 1.0, + np.array([1, 2, 3]), + np.array([1, 3])[0], + np.array([1.0, 2.2, 3.5]), + np.array([1.03, 3.9])[0], + np.array([1.4, 2.3], dtype=np.int32), + np.array([1.4, 2.3], dtype=np.int32)[0], + np.array([1.4, 2.3], dtype=np.int64), + np.array([1.4, 2.3], dtype=np.int64)[0], + ], +) +def test_parameter_types(mapdl, cleared, parameter): + mapdl.parameters["temp_arr"] = parameter + + if isinstance(parameter, np.ndarray): + # Reshaping arrays until #3717 is closed + assert np.allclose( + parameter.reshape((-1, 1)), mapdl.parameters["temp_arr"].reshape((-1, 1)) + ) + else: + assert parameter == mapdl.parameters["temp_arr"] + + if isinstance(parameter, (int, np.integer)): + # All numbers in MAPDL are stored as float. + assert isinstance(mapdl.parameters["temp_arr"], float) + + elif isinstance(parameter, (float, np.floating)): + assert isinstance(mapdl.parameters["temp_arr"], float) + + else: + assert isinstance(mapdl.parameters["temp_arr"], type(parameter)) From de0d586b8e79b4f83edaa69f722c41d53096673c Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 10 Feb 2025 08:54:52 +0000 Subject: [PATCH 37/42] ci: pre-commit autoupdate (#3723) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * [pre-commit.ci] pre-commit autoupdate updates: - [github.com/codespell-project/codespell: v2.2.2 → v2.2.4](https://github.com/codespell-project/codespell/compare/v2.2.2...v2.2.4) - [github.com/python-jsonschema/check-jsonschema: 0.21.0 → 0.22.0](https://github.com/python-jsonschema/check-jsonschema/compare/0.21.0...0.22.0) * Fixing codespell * Fixing pre-commit * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci * simplifying accepted words list * [pre-commit.ci] pre-commit autoupdate updates: - [github.com/psf/black: 23.1.0 → 23.3.0](https://github.com/psf/black/compare/23.1.0...23.3.0) * Updating also blackend * [pre-commit.ci] pre-commit autoupdate updates: - [github.com/adamchainz/blacken-docs: 1.13.0 → 1.14.0](https://github.com/adamchainz/blacken-docs/compare/1.13.0...1.14.0) - [github.com/codespell-project/codespell: v2.2.4 → v2.2.5](https://github.com/codespell-project/codespell/compare/v2.2.4...v2.2.5) - [github.com/python-jsonschema/check-jsonschema: 0.23.1 → 0.23.2](https://github.com/python-jsonschema/check-jsonschema/compare/0.23.1...0.23.2) * removing vale warnings * Adding words to ignore * [pre-commit.ci] pre-commit autoupdate updates: - [github.com/psf/black: 23.10.0 → 23.10.1](https://github.com/psf/black/compare/23.10.0...23.10.1) * [pre-commit.ci] pre-commit autoupdate updates: - [github.com/python-jsonschema/check-jsonschema: 0.27.0 → 0.27.1](https://github.com/python-jsonschema/check-jsonschema/compare/0.27.0...0.27.1) * [pre-commit.ci] pre-commit autoupdate updates: - [github.com/psf/black: 23.10.1 → 23.11.0](https://github.com/psf/black/compare/23.10.1...23.11.0) * Updating blacken-docs * [pre-commit.ci] pre-commit autoupdate updates: - [github.com/pycqa/isort: 5.13.0 → 5.13.2](https://github.com/pycqa/isort/compare/5.13.0...5.13.2) - [github.com/psf/black: 23.11.0 → 23.12.0](https://github.com/psf/black/compare/23.11.0...23.12.0) * [pre-commit.ci] pre-commit autoupdate updates: - [github.com/psf/black: 23.12.0 → 23.12.1](https://github.com/psf/black/compare/23.12.0...23.12.1) * lower * [pre-commit.ci] pre-commit autoupdate updates: - [github.com/psf/black: 24.1.1 → 24.2.0](https://github.com/psf/black/compare/24.1.1...24.2.0) - [github.com/python-jsonschema/check-jsonschema: 0.27.4 → 0.28.0](https://github.com/python-jsonschema/check-jsonschema/compare/0.27.4...0.28.0) * [pre-commit.ci] pre-commit autoupdate updates: - [github.com/ansys/pre-commit-hooks: v0.2.8 → v0.2.9](https://github.com/ansys/pre-commit-hooks/compare/v0.2.8...v0.2.9) * installing xindy * [pre-commit.ci] pre-commit autoupdate updates: - [github.com/psf/black: 24.3.0 → 24.4.0](https://github.com/psf/black/compare/24.3.0...24.4.0) - [github.com/python-jsonschema/check-jsonschema: 0.28.1 → 0.28.2](https://github.com/python-jsonschema/check-jsonschema/compare/0.28.1...0.28.2) * Updating black in blacken * [pre-commit.ci] pre-commit autoupdate updates: - [github.com/ansys/pre-commit-hooks: v0.2.9 → v0.3.1](https://github.com/ansys/pre-commit-hooks/compare/v0.2.9...v0.3.1) - [github.com/psf/black: 24.4.0 → 24.4.2](https://github.com/psf/black/compare/24.4.0...24.4.2) * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci * [pre-commit.ci] pre-commit autoupdate updates: - [github.com/ansys/pre-commit-hooks: v0.3.1 → v0.4.2](https://github.com/ansys/pre-commit-hooks/compare/v0.3.1...v0.4.2) - [github.com/psf/black: 24.4.2 → 24.8.0](https://github.com/psf/black/compare/24.4.2...24.8.0) - [github.com/PyCQA/flake8: 7.1.0 → 7.1.1](https://github.com/PyCQA/flake8/compare/7.1.0...7.1.1) * chore: adding changelog file 3330.miscellaneous.md * Update .pre-commit-config.yaml * ci: pre-commit autoupdate updates: - [github.com/codespell-project/codespell: v2.3.0 → v2.4.0](https://github.com/codespell-project/codespell/compare/v2.3.0...v2.4.0) * chore: adding changelog file 3710.maintenance.md [dependabot-skip] * fix: pre-commit warnings * chore: adding changelog file 3710.documentation.md [dependabot-skip] * maint: update codespell * ci: pre-commit autoupdate updates: - [github.com/pycqa/isort: 5.13.2 → 6.0.0](https://github.com/pycqa/isort/compare/5.13.2...6.0.0) - [github.com/psf/black: 24.10.0 → 25.1.0](https://github.com/psf/black/compare/24.10.0...25.1.0) - [github.com/python-jsonschema/check-jsonschema: 0.31.0 → 0.31.1](https://github.com/python-jsonschema/check-jsonschema/compare/0.31.0...0.31.1) * chore: adding changelog file 3723.maintenance.md [dependabot-skip] * ci: auto fixes from pre-commit.com hooks. for more information, see https://pre-commit.ci * build: update black * ci: auto fixes from pre-commit.com hooks. for more information, see https://pre-commit.ci --------- Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> Co-authored-by: German Co-authored-by: pyansys-ci-bot <92810346+pyansys-ci-bot@users.noreply.github.com> Co-authored-by: German <28149841+germa89@users.noreply.github.com> --- .pre-commit-config.yaml | 18 +++++++++--------- doc/changelog.d/3723.documentation.md | 1 + doc/source/user_guide/convert.rst | 2 +- src/ansys/mapdl/core/component.py | 2 +- src/ansys/mapdl/core/examples/downloads.py | 3 +-- src/ansys/mapdl/core/examples/verif_files.py | 3 +-- src/ansys/mapdl/core/mapdl_grpc.py | 2 +- tests/test_logging.py | 2 +- tests/test_mesh_grpc.py | 2 +- 9 files changed, 17 insertions(+), 18 deletions(-) create mode 100644 doc/changelog.d/3723.documentation.md diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 57284cc2dc..2a14f22549 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -21,7 +21,7 @@ repos: - --start_year=2016 - repo: https://github.com/pycqa/isort - rev: 5.13.2 + rev: 6.0.0 hooks: - id: isort @@ -38,19 +38,19 @@ repos: src/ansys/mapdl/core/commands ) +- repo: https://github.com/adamchainz/blacken-docs + rev: 1.19.1 + hooks: + - id: blacken-docs + additional_dependencies: [black==25.1.0] + - repo: https://github.com/psf/black - rev: 24.10.0 # If version changes --> modify "blacken-docs" manually as well. + rev: 25.1.0 # If version changes --> modify "blacken-docs" manually as well. hooks: - id: black args: - --line-length=88 -- repo: https://github.com/adamchainz/blacken-docs - rev: 1.19.1 - hooks: - - id: blacken-docs - additional_dependencies: [black==24.10.0] - - repo: https://github.com/PyCQA/flake8 rev: 7.1.1 hooks: @@ -78,6 +78,6 @@ repos: # this validates our github workflow files - repo: https://github.com/python-jsonschema/check-jsonschema - rev: 0.31.0 + rev: 0.31.1 hooks: - id: check-github-workflows diff --git a/doc/changelog.d/3723.documentation.md b/doc/changelog.d/3723.documentation.md new file mode 100644 index 0000000000..23264207b9 --- /dev/null +++ b/doc/changelog.d/3723.documentation.md @@ -0,0 +1 @@ +ci: pre-commit autoupdate \ No newline at end of file diff --git a/doc/source/user_guide/convert.rst b/doc/source/user_guide/convert.rst index 51090e155c..2ab7b34532 100644 --- a/doc/source/user_guide/convert.rst +++ b/doc/source/user_guide/convert.rst @@ -227,7 +227,7 @@ which can be done with: .. code:: python - """ Script generated by ansys-mapdl-core version 0.57.0""" + """Script generated by ansys-mapdl-core version 0.57.0""" from ansys.mapdl.core import launch_mapdl diff --git a/src/ansys/mapdl/core/component.py b/src/ansys/mapdl/core/component.py index 4093a9a56e..8cf17774ce 100644 --- a/src/ansys/mapdl/core/component.py +++ b/src/ansys/mapdl/core/component.py @@ -83,7 +83,7 @@ def _check_valid_pyobj_to_entities( - items: Union[Tuple[int, ...], List[int], NDArray[Any]] + items: Union[Tuple[int, ...], List[int], NDArray[Any]], ) -> None: """Check whether the python objects can be converted to entities. At the moment, only list and numpy arrays of ints are allowed. diff --git a/src/ansys/mapdl/core/examples/downloads.py b/src/ansys/mapdl/core/examples/downloads.py index dd4c8a02d8..f2f2f82eb3 100644 --- a/src/ansys/mapdl/core/examples/downloads.py +++ b/src/ansys/mapdl/core/examples/downloads.py @@ -20,8 +20,7 @@ # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # SOFTWARE. -"""Functions to download sample datasets from the pyansys data repository. -""" +"""Functions to download sample datasets from the pyansys data repository.""" from functools import wraps import os import shutil diff --git a/src/ansys/mapdl/core/examples/verif_files.py b/src/ansys/mapdl/core/examples/verif_files.py index a34ca23d74..39dfda35ca 100755 --- a/src/ansys/mapdl/core/examples/verif_files.py +++ b/src/ansys/mapdl/core/examples/verif_files.py @@ -20,8 +20,7 @@ # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # SOFTWARE. -"""loads a list of verification files -""" +"""loads a list of verification files""" import glob import inspect import os diff --git a/src/ansys/mapdl/core/mapdl_grpc.py b/src/ansys/mapdl/core/mapdl_grpc.py index 81bdf7f08d..c2c53b38c4 100644 --- a/src/ansys/mapdl/core/mapdl_grpc.py +++ b/src/ansys/mapdl/core/mapdl_grpc.py @@ -20,7 +20,7 @@ # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # SOFTWARE. -"""A gRPC specific class and methods for the MAPDL gRPC client """ +"""A gRPC specific class and methods for the MAPDL gRPC client""" import fnmatch from functools import wraps diff --git a/tests/test_logging.py b/tests/test_logging.py index 35069035d4..c00866c30f 100644 --- a/tests/test_logging.py +++ b/tests/test_logging.py @@ -20,7 +20,7 @@ # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # SOFTWARE. -""""Testing of log module""" +""" "Testing of log module""" import logging as deflogging # Default logging import os import re diff --git a/tests/test_mesh_grpc.py b/tests/test_mesh_grpc.py index e914974b22..91af3673ef 100644 --- a/tests/test_mesh_grpc.py +++ b/tests/test_mesh_grpc.py @@ -20,7 +20,7 @@ # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # SOFTWARE. -"""Test mesh """ +"""Test mesh""" import os import numpy as np From 260ba80123ff1e226c9eee70ecc2f157f8a09717 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 10 Feb 2025 09:29:24 +0100 Subject: [PATCH 38/42] build: bump the documentation group across 1 directory with 3 updates (#3727) * build: bump the documentation group across 1 directory with 3 updates Bumps the documentation group with 3 updates in the / directory: [ansys-sphinx-theme](https://github.com/ansys/ansys-sphinx-theme), [plotly](https://github.com/plotly/plotly.py) and [sphinx-notfound-page](https://github.com/readthedocs/sphinx-notfound-page). Updates `ansys-sphinx-theme` from 1.2.6 to 1.3.1 - [Release notes](https://github.com/ansys/ansys-sphinx-theme/releases) - [Commits](https://github.com/ansys/ansys-sphinx-theme/compare/v1.2.6...v1.3.1) Updates `plotly` from 5.24.1 to 6.0.0 - [Release notes](https://github.com/plotly/plotly.py/releases) - [Changelog](https://github.com/plotly/plotly.py/blob/master/CHANGELOG.md) - [Commits](https://github.com/plotly/plotly.py/compare/v5.24.1...v6.0.0) Updates `sphinx-notfound-page` from 1.0.4 to 1.1.0 - [Changelog](https://github.com/readthedocs/sphinx-notfound-page/blob/main/CHANGELOG.rst) - [Commits](https://github.com/readthedocs/sphinx-notfound-page/compare/1.0.4...1.1.0) --- updated-dependencies: - dependency-name: ansys-sphinx-theme dependency-type: direct:production update-type: version-update:semver-minor dependency-group: documentation - dependency-name: plotly dependency-type: direct:production update-type: version-update:semver-major dependency-group: documentation - dependency-name: sphinx-notfound-page dependency-type: direct:production update-type: version-update:semver-minor dependency-group: documentation ... Signed-off-by: dependabot[bot] * chore: adding changelog file 3727.dependencies.md [dependabot-skip] --------- Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: pyansys-ci-bot <92810346+pyansys-ci-bot@users.noreply.github.com> --- doc/changelog.d/3727.dependencies.md | 1 + pyproject.toml | 6 +++--- 2 files changed, 4 insertions(+), 3 deletions(-) create mode 100644 doc/changelog.d/3727.dependencies.md diff --git a/doc/changelog.d/3727.dependencies.md b/doc/changelog.d/3727.dependencies.md new file mode 100644 index 0000000000..36ec12fcae --- /dev/null +++ b/doc/changelog.d/3727.dependencies.md @@ -0,0 +1 @@ +build: bump the documentation group across 1 directory with 3 updates \ No newline at end of file diff --git a/pyproject.toml b/pyproject.toml index edc1a92b9b..8b7468f4bd 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -75,7 +75,7 @@ tests = [ doc = [ "ansys-dpf-core==0.10.1", "ansys-mapdl-reader==0.54.2", - "ansys-sphinx-theme==1.2.6", + "ansys-sphinx-theme==1.3.1", "ansys-tools-visualization-interface==0.8.1", "grpcio==1.69.0", "imageio-ffmpeg==0.6.0", @@ -87,7 +87,7 @@ doc = [ "nbformat==5.10.4", "numpydoc==1.8.0", "pandas==2.2.3", - "plotly==5.24.1", + "plotly==6.0.0", "pyiges[full]==0.3.1", "pypandoc==1.15", "pytest-sphinx==0.6.3", @@ -98,7 +98,7 @@ doc = [ "sphinx-design==0.6.1", "sphinx-gallery==0.19.0", "sphinx-jinja==2.0.2", - "sphinx-notfound-page==1.0.4", + "sphinx-notfound-page==1.1.0", "sphinx==8.1.3", "sphinxcontrib-websupport==2.0.0", "sphinxemoji==0.3.1", From f8ef9d600fbbf0e25c67b50726a44388dacd1dcf Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Wed, 29 Jan 2025 13:36:31 +0000 Subject: [PATCH 39/42] ci: pre-commit autoupdate (#3710) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * [pre-commit.ci] pre-commit autoupdate updates: - [github.com/codespell-project/codespell: v2.2.2 → v2.2.4](https://github.com/codespell-project/codespell/compare/v2.2.2...v2.2.4) - [github.com/python-jsonschema/check-jsonschema: 0.21.0 → 0.22.0](https://github.com/python-jsonschema/check-jsonschema/compare/0.21.0...0.22.0) * Fixing codespell * Fixing pre-commit * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci * simplifying accepted words list * [pre-commit.ci] pre-commit autoupdate updates: - [github.com/psf/black: 23.1.0 → 23.3.0](https://github.com/psf/black/compare/23.1.0...23.3.0) * Updating also blackend * [pre-commit.ci] pre-commit autoupdate updates: - [github.com/adamchainz/blacken-docs: 1.13.0 → 1.14.0](https://github.com/adamchainz/blacken-docs/compare/1.13.0...1.14.0) - [github.com/codespell-project/codespell: v2.2.4 → v2.2.5](https://github.com/codespell-project/codespell/compare/v2.2.4...v2.2.5) - [github.com/python-jsonschema/check-jsonschema: 0.23.1 → 0.23.2](https://github.com/python-jsonschema/check-jsonschema/compare/0.23.1...0.23.2) * removing vale warnings * Adding words to ignore * [pre-commit.ci] pre-commit autoupdate updates: - [github.com/psf/black: 23.10.0 → 23.10.1](https://github.com/psf/black/compare/23.10.0...23.10.1) * [pre-commit.ci] pre-commit autoupdate updates: - [github.com/python-jsonschema/check-jsonschema: 0.27.0 → 0.27.1](https://github.com/python-jsonschema/check-jsonschema/compare/0.27.0...0.27.1) * [pre-commit.ci] pre-commit autoupdate updates: - [github.com/psf/black: 23.10.1 → 23.11.0](https://github.com/psf/black/compare/23.10.1...23.11.0) * Updating blacken-docs * [pre-commit.ci] pre-commit autoupdate updates: - [github.com/pycqa/isort: 5.13.0 → 5.13.2](https://github.com/pycqa/isort/compare/5.13.0...5.13.2) - [github.com/psf/black: 23.11.0 → 23.12.0](https://github.com/psf/black/compare/23.11.0...23.12.0) * [pre-commit.ci] pre-commit autoupdate updates: - [github.com/psf/black: 23.12.0 → 23.12.1](https://github.com/psf/black/compare/23.12.0...23.12.1) * lower * [pre-commit.ci] pre-commit autoupdate updates: - [github.com/psf/black: 24.1.1 → 24.2.0](https://github.com/psf/black/compare/24.1.1...24.2.0) - [github.com/python-jsonschema/check-jsonschema: 0.27.4 → 0.28.0](https://github.com/python-jsonschema/check-jsonschema/compare/0.27.4...0.28.0) * [pre-commit.ci] pre-commit autoupdate updates: - [github.com/ansys/pre-commit-hooks: v0.2.8 → v0.2.9](https://github.com/ansys/pre-commit-hooks/compare/v0.2.8...v0.2.9) * installing xindy * [pre-commit.ci] pre-commit autoupdate updates: - [github.com/psf/black: 24.3.0 → 24.4.0](https://github.com/psf/black/compare/24.3.0...24.4.0) - [github.com/python-jsonschema/check-jsonschema: 0.28.1 → 0.28.2](https://github.com/python-jsonschema/check-jsonschema/compare/0.28.1...0.28.2) * Updating black in blacken * [pre-commit.ci] pre-commit autoupdate updates: - [github.com/ansys/pre-commit-hooks: v0.2.9 → v0.3.1](https://github.com/ansys/pre-commit-hooks/compare/v0.2.9...v0.3.1) - [github.com/psf/black: 24.4.0 → 24.4.2](https://github.com/psf/black/compare/24.4.0...24.4.2) * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci * [pre-commit.ci] pre-commit autoupdate updates: - [github.com/ansys/pre-commit-hooks: v0.3.1 → v0.4.2](https://github.com/ansys/pre-commit-hooks/compare/v0.3.1...v0.4.2) - [github.com/psf/black: 24.4.2 → 24.8.0](https://github.com/psf/black/compare/24.4.2...24.8.0) - [github.com/PyCQA/flake8: 7.1.0 → 7.1.1](https://github.com/PyCQA/flake8/compare/7.1.0...7.1.1) * chore: adding changelog file 3330.miscellaneous.md * Update .pre-commit-config.yaml * ci: pre-commit autoupdate updates: - [github.com/codespell-project/codespell: v2.3.0 → v2.4.0](https://github.com/codespell-project/codespell/compare/v2.3.0...v2.4.0) * chore: adding changelog file 3710.maintenance.md [dependabot-skip] * fix: pre-commit warnings * chore: adding changelog file 3710.documentation.md [dependabot-skip] * maint: update codespell --------- Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> Co-authored-by: German Co-authored-by: pyansys-ci-bot <92810346+pyansys-ci-bot@users.noreply.github.com> Co-authored-by: German <28149841+germa89@users.noreply.github.com> --- .pre-commit-config.yaml | 2 +- doc/changelog.d/3330.miscellaneous.md | 1 + doc/changelog.d/3710.documentation.md | 1 + .../ex_01-gmsh_example/modal_analysis.py | 2 +- examples/00-mapdl-examples/pressure_vessel.py | 2 +- examples/00-mapdl-examples/pyvista_mesh.py | 2 +- src/ansys/mapdl/core/post.py | 10 +++++----- tests/test_dpf.py | 2 +- 8 files changed, 12 insertions(+), 10 deletions(-) create mode 100644 doc/changelog.d/3330.miscellaneous.md create mode 100644 doc/changelog.d/3710.documentation.md diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 2a14f22549..e17fc8c0f0 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -57,7 +57,7 @@ repos: - id: flake8 - repo: https://github.com/codespell-project/codespell - rev: v2.3.0 + rev: v2.4.1 hooks: - id: codespell args: ["--toml", "pyproject.toml"] diff --git a/doc/changelog.d/3330.miscellaneous.md b/doc/changelog.d/3330.miscellaneous.md new file mode 100644 index 0000000000..b42b241032 --- /dev/null +++ b/doc/changelog.d/3330.miscellaneous.md @@ -0,0 +1 @@ +[pre-commit.ci] pre-commit autoupdate \ No newline at end of file diff --git a/doc/changelog.d/3710.documentation.md b/doc/changelog.d/3710.documentation.md new file mode 100644 index 0000000000..23264207b9 --- /dev/null +++ b/doc/changelog.d/3710.documentation.md @@ -0,0 +1 @@ +ci: pre-commit autoupdate \ No newline at end of file diff --git a/doc/source/examples/extended_examples/ex_01-gmsh_example/modal_analysis.py b/doc/source/examples/extended_examples/ex_01-gmsh_example/modal_analysis.py index f5f3df7173..485402e754 100644 --- a/doc/source/examples/extended_examples/ex_01-gmsh_example/modal_analysis.py +++ b/doc/source/examples/extended_examples/ex_01-gmsh_example/modal_analysis.py @@ -35,7 +35,7 @@ mapdl.shpp("SUMM") # specify material properties -# using aprox values for AISI 5000 Series Steel +# using approximated values for AISI 5000 Series Steel mapdl.units("SI") mapdl.mp("EX", 1, 200e9) # Elastic moduli in Pa (kg/(m*s**2)) mapdl.mp("DENS", 1, 7700) # Density in kg/m3 diff --git a/examples/00-mapdl-examples/pressure_vessel.py b/examples/00-mapdl-examples/pressure_vessel.py index 36c2fab461..10db4c3cc9 100644 --- a/examples/00-mapdl-examples/pressure_vessel.py +++ b/examples/00-mapdl-examples/pressure_vessel.py @@ -156,7 +156,7 @@ # access the result result = mapdl.result -# Get the von Mises stess and show that this is equivalent to the +# Get the von Mises stress and show that this is equivalent to the # stress obtained from MAPDL. nnum, stress = result.principal_nodal_stress(0) von_mises = stress[:, -1] # von-Mises stress is the right most column diff --git a/examples/00-mapdl-examples/pyvista_mesh.py b/examples/00-mapdl-examples/pyvista_mesh.py index c1f9487ee8..c0d726d203 100644 --- a/examples/00-mapdl-examples/pyvista_mesh.py +++ b/examples/00-mapdl-examples/pyvista_mesh.py @@ -62,7 +62,7 @@ mapdl.emodif("ALL", "SECNUM", 1) # specify material properties -# using aprox values for AISI 5000 Series Steel +# using approximated values for AISI 5000 Series Steel # http://www.matweb.com/search/datasheet.aspx?matguid=89d4b891eece40fbbe6b71f028b64e9e mapdl.units("SI") # not necessary, but helpful for book keeping mapdl.mp("EX", 1, 200e9) # Elastic moduli in Pa (kg/(m*s**2)) diff --git a/src/ansys/mapdl/core/post.py b/src/ansys/mapdl/core/post.py index 57cbc59555..3ac2e471aa 100644 --- a/src/ansys/mapdl/core/post.py +++ b/src/ansys/mapdl/core/post.py @@ -2383,7 +2383,7 @@ def nodal_total_eqv_strain(self) -> np.ndarray: Examples -------- - Total quivalent strain for the current result. + Total equivalent strain for the current result. >>> mapdl.post_processing.nodal_total_eqv_strain() array([15488.84357602, 16434.95432337, 15683.2334295 , ..., @@ -2757,7 +2757,7 @@ def nodal_elastic_eqv_strain(self) -> np.ndarray: Examples -------- - Elastic quivalent strain for the current result. + Elastic equivalent strain for the current result. >>> mapdl.post_processing.nodal_elastic_eqv_strain() array([15488.84357602, 16434.95432337, 15683.2334295 , ..., @@ -3138,7 +3138,7 @@ def nodal_plastic_eqv_strain(self) -> np.ndarray: Examples -------- - Plastic quivalent strain for the current result + Plastic equivalent strain for the current result >>> mapdl.post_processing.nodal_plastic_eqv_strain() array([15488.84357602, 16434.95432337, 15683.2334295 , ..., @@ -3526,7 +3526,7 @@ def nodal_thermal_eqv_strain(self) -> np.ndarray: Examples -------- - Thermal quivalent strain for the current result. + Thermal equivalent strain for the current result. >>> mapdl.post_processing.nodal_thermal_eqv_strain() array([15488.84357602, 16434.95432337, 15683.2334295 , ..., @@ -3618,7 +3618,7 @@ def nodal_contact_friction_stress(self) -> np.ndarray: Examples -------- - Thermal quivalent strain for the current result. + Thermal equivalent strain for the current result. >>> mapdl.post_processing.nodal_contact_friction_stress() array([15488.84357602, 16434.95432337, 15683.2334295 , ..., diff --git a/tests/test_dpf.py b/tests/test_dpf.py index 882e4e1f7e..4d83004ad2 100644 --- a/tests/test_dpf.py +++ b/tests/test_dpf.py @@ -65,7 +65,7 @@ def test_upload(skip_dpf, mapdl, solved_box, tmpdir): # Download RST file rst_path = mapdl.download_result(str(tmpdir.mkdir("tmpdir"))) - # Stabilishing connection + # Establishing connection grpc_con = dpf.connect_to_server(port=DPF_PORT) assert grpc_con.live From b833c3c5fae8e1408c79f6feff19c61f5195186d Mon Sep 17 00:00:00 2001 From: German <28149841+germa89@users.noreply.github.com> Date: Mon, 27 Jan 2025 18:28:09 +0100 Subject: [PATCH 40/42] fix: using cached version for remove lock on exit (#3709) * feat: using cached for removing lock file when exiting * test: adding tests * chore: adding changelog file 3709.fixed.md [dependabot-skip] * fix: vulnerabilities warning * fix: warnings import * chore: revert "fix: warnings import" This reverts commit 5593e6b8abb38785ad94f4b652cea2c15975d6ba. * fix: warnings import --------- Co-authored-by: pyansys-ci-bot <92810346+pyansys-ci-bot@users.noreply.github.com> --- doc/changelog.d/3709.fixed.md | 1 + src/ansys/mapdl/core/mapdl_grpc.py | 14 +++++++++++--- tests/test_mapdl.py | 16 ++++++++++++++-- 3 files changed, 26 insertions(+), 5 deletions(-) create mode 100644 doc/changelog.d/3709.fixed.md diff --git a/doc/changelog.d/3709.fixed.md b/doc/changelog.d/3709.fixed.md new file mode 100644 index 0000000000..0afc2fc81f --- /dev/null +++ b/doc/changelog.d/3709.fixed.md @@ -0,0 +1 @@ +fix: using cached version for remove lock on exit \ No newline at end of file diff --git a/src/ansys/mapdl/core/mapdl_grpc.py b/src/ansys/mapdl/core/mapdl_grpc.py index c2c53b38c4..98cb72ec40 100644 --- a/src/ansys/mapdl/core/mapdl_grpc.py +++ b/src/ansys/mapdl/core/mapdl_grpc.py @@ -1197,7 +1197,7 @@ def _exit_mapdl(self, path: str = None) -> None: self._close_process() - self._remove_lock_file(path) + self._remove_lock_file(path, use_cached=True) else: self._exit_mapdl_server() @@ -1360,17 +1360,24 @@ def _cache_pids(self): self._log.debug(f"Recaching PIDs: {self._pids}") - def _remove_lock_file(self, mapdl_path=None): + def _remove_lock_file( + self, mapdl_path: str = None, jobname: str = None, use_cached: bool = False + ): """Removes the lock file. Necessary to call this as a segfault of MAPDL or exit(0) will not remove the lock file. """ + if jobname is None and use_cached: + jobname = self._jobname + elif jobname is None: + jobname = self.jobname + self._log.debug("Removing lock file after exit.") if mapdl_path is None: # pragma: no cover mapdl_path = self.directory if mapdl_path: - for lockname in [self.jobname + ".lock", "file.lock"]: + for lockname in [jobname + ".lock", "file.lock"]: lock_file = os.path.join(mapdl_path, lockname) if os.path.isfile(lock_file): try: @@ -3804,6 +3811,7 @@ def kill_job(self, jobid: int) -> None: Job ID. """ cmd = ["scancel", f"{jobid}"] + # to ensure the job is stopped properly, let's issue the scancel twice. subprocess.Popen(cmd) # nosec B603 def __del__(self): diff --git a/tests/test_mapdl.py b/tests/test_mapdl.py index 76b786f2e1..b000fe5eae 100644 --- a/tests/test_mapdl.py +++ b/tests/test_mapdl.py @@ -1727,13 +1727,25 @@ def test_mode(mapdl, cleared): mapdl._mode = "grpc" # Going back to default -def test_remove_lock_file(mapdl, cleared, tmpdir): +@pytest.mark.parametrize("use_cached", (True, False)) +def test_remove_lock_file(mapdl, cleared, tmpdir, use_cached): tmpdir_ = tmpdir.mkdir("ansys") lock_file = tmpdir_.join("file.lock") with open(lock_file, "w") as fid: fid.write("test") - mapdl._remove_lock_file(tmpdir_) + with patch( + "ansys.mapdl.core.mapdl_grpc.MapdlGrpc.jobname", new_callable=PropertyMock + ) as mock_jb: + mock_jb.return_value = mapdl._jobname + + mapdl._remove_lock_file(tmpdir_, use_cached=use_cached) + + if use_cached: + mock_jb.assert_not_called() + else: + mock_jb.assert_called() + assert not os.path.exists(lock_file) From 78960853c5f23c01957e8922bdda52b302c12bb8 Mon Sep 17 00:00:00 2001 From: German <28149841+germa89@users.noreply.github.com> Date: Mon, 27 Jan 2025 18:27:01 +0100 Subject: [PATCH 41/42] feat: avoiding reconnecting if MAPDL exited already (#3708) * test: refactor check_stds and post_mortem_checks * test: backing off algorithm * chore: adding changelog file 3703.added.md [dependabot-skip] * fix: codacity warnings * feat: using get_value to obtain the n elements * revert: revert "feat: using get_value to obtain the n elements" Performance is not as go This reverts commit 877f8030961e188b082e7c87cbaa2b0e8e2d2477. * feat: using get_value to obtain the n elements * revert: revert "feat: using get_value to obtain the n elements" Performance is not as go This reverts commit 877f8030961e188b082e7c87cbaa2b0e8e2d2477. * feat: using mapdl.exit when raising final error. * test: fix test by avoiding killing mapdl. * fix: test * fix: test * feat: adding warnings when restarting MAPDL during testing * fix: test * feat: caching requires_package * test: adding more tests * chore: adding changelog file 3705.added.md [dependabot-skip] * chore: adding changelog file 3705.added.md [dependabot-skip] * fix: warnings import * feat: not reconnecting if MAPDL already exited * test: adding tests * chore: adding changelog file 3708.miscellaneous.md [dependabot-skip] * fix: tests --------- Co-authored-by: pyansys-ci-bot <92810346+pyansys-ci-bot@users.noreply.github.com> --- doc/changelog.d/3708.miscellaneous.md | 1 + src/ansys/mapdl/core/errors.py | 30 ++++--- tests/test_cli.py | 75 ++++++++++++++++- tests/test_mapdl.py | 116 ++++++++++++++++++++++++++ 4 files changed, 207 insertions(+), 15 deletions(-) create mode 100644 doc/changelog.d/3708.miscellaneous.md diff --git a/doc/changelog.d/3708.miscellaneous.md b/doc/changelog.d/3708.miscellaneous.md new file mode 100644 index 0000000000..8f7fec73b7 --- /dev/null +++ b/doc/changelog.d/3708.miscellaneous.md @@ -0,0 +1 @@ +feat: avoiding reconnecting if MAPDL exited already \ No newline at end of file diff --git a/src/ansys/mapdl/core/errors.py b/src/ansys/mapdl/core/errors.py index 503074f62f..5a5c0bd909 100644 --- a/src/ansys/mapdl/core/errors.py +++ b/src/ansys/mapdl/core/errors.py @@ -323,25 +323,29 @@ def wrapper(*args, **kwargs): except grpc.RpcError as error: mapdl = retrieve_mapdl_from_args(args) + mapdl._log.debug("A gRPC error has been detected.") - i_attemps += 1 - if i_attemps <= n_attempts: + if not mapdl.exited: + i_attemps += 1 + if i_attemps <= n_attempts: - wait = ( - initial_backoff * multiplier_backoff**i_attemps - ) # Exponential backoff - sleep(wait) + wait = ( + initial_backoff * multiplier_backoff**i_attemps + ) # Exponential backoff - # reconnect - mapdl._log.debug( - f"Re-connection attempt {i_attemps} after waiting {wait:0.3f} seconds" - ) + # reconnect + mapdl._log.debug( + f"Re-connection attempt {i_attemps} after waiting {wait:0.3f} seconds" + ) - connected = mapdl._connect(timeout=wait) + if not mapdl.is_alive: + connected = mapdl._connect(timeout=wait) + else: + sleep(wait) - # Retry again - continue + # Retry again + continue # Custom errors reason = "" diff --git a/tests/test_cli.py b/tests/test_cli.py index eba3675327..8e95fc9c45 100644 --- a/tests/test_cli.py +++ b/tests/test_cli.py @@ -76,8 +76,79 @@ def test_launch_mapdl_cli(monkeypatch, run_cli, start_instance): # grab ips and port pid = int(re.search(r"\(PID=(\d+)\)", output).groups()[0]) - output = run_cli(f"stop --port {PORT1}") - assert "success" in output.lower() + +@requires("click") +@pytest.mark.parametrize( + "mapping", + ((False, True), (False, True, False), (False, False, False), (True, True, False)), +) +def test_pymapdl_stop_instances(run_cli, mapping): + + fake_process_ = [ + { + "pid": np.random.randint(10000, 100000), + "name": f"ansys251_{ind}" if each else "process", + "port": str(50052 + ind), + "ansys_process": each, + } + for ind, each in enumerate(mapping) + ] + + fake_processes = [make_fake_process(**each) for each in fake_process_] + + with ( + patch("ansys.mapdl.core.cli.stop._kill_process") as mock_kill, + patch("psutil.pid_exists") as mock_pid, + patch("psutil.process_iter", return_value=iter(fake_processes)), + ): + + mock_pid.return_value = lambda *args, **kwargs: True # All process exists + mock_kill.side_effect = lambda *args, **kwargs: None # avoid kill nothing + + if sum(mapping) == 0: + output = run_cli(f"stop --port {PORT1}") + assert ( + f"error: no ansys instances running on port {PORT1}" in output.lower() + ) + mock_kill.assert_not_called() + + output = run_cli(f"stop --all") + assert f"error: no ansys instances have been found." in output.lower() + mock_kill.assert_not_called() + + elif sum(mapping) == 1: + # Port + process, process_mock = [ + (each, each_mock) + for each, each_mock in zip(fake_process_, fake_processes) + if "ansys251" in each["name"] + ][0] + port = process["port"] + + output = run_cli(f"stop --port {port}") + assert ( + f"success: ansys instances running on port {port} have been stopped" + in output.lower() + ) + + # PID + pid = process["pid"] + with patch("psutil.Process") as mock_process: + mock_process.return_value = process_mock + + output = run_cli(f"stop --pid {pid}") + assert ( + f"the process with pid {pid} and its children have been stopped." + in output.lower() + ) + + mock_kill.assert_called() + assert mock_kill.call_count == 2 + + else: + output = run_cli(f"stop --all") + assert "success: ansys instances have been stopped." in output.lower() + assert mock_kill.call_count == sum(mapping) @requires("click") diff --git a/tests/test_mapdl.py b/tests/test_mapdl.py index b000fe5eae..6a2731bfc1 100644 --- a/tests/test_mapdl.py +++ b/tests/test_mapdl.py @@ -2599,3 +2599,119 @@ def test_comment_on_debug_mode(mapdl, cleared): mockcom.assert_called_once_with("Entering in non_interactive mode") mapdl.logger.logger.level = loglevel + + +@patch("ansys.mapdl.core.errors.N_ATTEMPTS", 2) +@patch("ansys.mapdl.core.errors.MULTIPLIER_BACKOFF", 1) +@pytest.mark.parametrize("is_exited", [True, False]) +def test_timeout_when_exiting(mapdl, is_exited): + from ansys.mapdl.core import errors + + def raise_exception(*args, **kwargs): + from grpc import RpcError + + e = RpcError("My patched error") + e.code = lambda: grpc.StatusCode.ABORTED + e.details = lambda: "My gRPC error details" + + # Simulating MAPDL exiting by force + mapdl._exited = is_exited + + raise e + + handle_generic_grpc_error = errors.handle_generic_grpc_error + + with ( + patch("ansys.mapdl.core.mapdl_grpc.pb_types.CmdRequest") as mock_cmdrequest, + patch( + "ansys.mapdl.core.mapdl_grpc.MapdlGrpc.is_alive", new_callable=PropertyMock + ) as mock_is_alive, + patch.object(mapdl, "_connect") as mock_connect, + patch( + "ansys.mapdl.core.errors.handle_generic_grpc_error", autospec=True + ) as mock_handle, + patch.object(mapdl, "_exit_mapdl") as mock_exit_mapdl, + ): + + mock_exit_mapdl.return_value = None # Avoid exiting + mock_is_alive.return_value = False + mock_connect.return_value = None # patched to avoid timeout + mock_cmdrequest.side_effect = raise_exception + mock_handle.side_effect = handle_generic_grpc_error + + with pytest.raises(MapdlExitedError): + mapdl.prep7() + + # After + assert mapdl._exited + + assert mock_handle.call_count == 1 + + if is_exited: + # Checking no trying to reconnect + assert mock_connect.call_count == 0 + assert mock_cmdrequest.call_count == 1 + assert mock_is_alive.call_count == 1 + + else: + assert mock_connect.call_count == errors.N_ATTEMPTS + assert mock_cmdrequest.call_count == errors.N_ATTEMPTS + 1 + assert mock_is_alive.call_count == errors.N_ATTEMPTS + 1 + + mapdl._exited = False + + +@pytest.mark.parametrize( + "cmd,arg", + ( + ("block", None), + ("nsel", None), + ("esel", None), + ("ksel", None), + ("modopt", None), + ), +) +def test_none_as_argument(mapdl, make_block, cmd, arg): + if "sel" in cmd: + kwargs = {"wraps": mapdl._run} + else: + kwargs = {} + + with patch.object(mapdl, "_run", **kwargs) as mock_run: + + mock_run.assert_not_called() + + func = getattr(mapdl, cmd) + out = func(arg) + + mock_run.assert_called() + + if "sel" in cmd: + assert isinstance(out, np.ndarray) + assert len(out) == 0 + + cmd = mock_run.call_args_list[0].args[0] + assert isinstance(cmd, str) + assert "NONE" in cmd.upper() + + +@pytest.mark.parametrize("func", ["ksel", "lsel", "asel", "vsel"]) +def test_none_on_selecting(mapdl, cleared, func): + mapdl.block(0, 1, 0, 1, 0, 1) + + selfunc = getattr(mapdl, func) + + assert len(selfunc("all")) > 0 + assert len(selfunc(None)) == 0 + + +@requires("pyvista") +def test_requires_package_speed(): + from ansys.mapdl.core.misc import requires_package + + @requires_package("pyvista") + def my_func(i): + return i + 1 + + for i in range(1_000_000): + my_func(i) From ec8659421ac8e0ad4d4ab6d8560b437b7b97f5f7 Mon Sep 17 00:00:00 2001 From: German <28149841+germa89@users.noreply.github.com> Date: Thu, 13 Feb 2025 13:40:08 +0100 Subject: [PATCH 42/42] fix: exiting on class deletion (#3738) * test: __del__method. * refactor: simplifying exit * chore: adding changelog file 3738.fixed.md [dependabot-skip] * refactor: simplyfing for other interfaces. * feat: exiting mapdl * feat: adding __del__ to mapdl_console * fix: improve job ID handling and remove atexit registration in MAPDL core --------- Co-authored-by: pyansys-ci-bot <92810346+pyansys-ci-bot@users.noreply.github.com> --- doc/changelog.d/3738.fixed.md | 1 + src/ansys/mapdl/core/mapdl_console.py | 31 ++++++++---- src/ansys/mapdl/core/mapdl_core.py | 20 ++------ src/ansys/mapdl/core/mapdl_grpc.py | 73 +++++++++++++-------------- tests/conftest.py | 5 +- tests/test_console.py | 65 +++++++++++++++++++++++- tests/test_mapdl.py | 50 ++++++++++++++++++ 7 files changed, 174 insertions(+), 71 deletions(-) create mode 100644 doc/changelog.d/3738.fixed.md diff --git a/doc/changelog.d/3738.fixed.md b/doc/changelog.d/3738.fixed.md new file mode 100644 index 0000000000..23c78ec00b --- /dev/null +++ b/doc/changelog.d/3738.fixed.md @@ -0,0 +1 @@ +fix: exiting on class deletion \ No newline at end of file diff --git a/src/ansys/mapdl/core/mapdl_console.py b/src/ansys/mapdl/core/mapdl_console.py index 6931d8cfca..d48c360975 100644 --- a/src/ansys/mapdl/core/mapdl_console.py +++ b/src/ansys/mapdl/core/mapdl_console.py @@ -27,6 +27,7 @@ import os import re import time +from warnings import warn from ansys.mapdl.core import LOG from ansys.mapdl.core.errors import MapdlExitedError, MapdlRuntimeError @@ -274,6 +275,20 @@ def mesh(self): """ return self._mesh + def __del__(self): + """Garbage cleaning the class""" + self._exit() + + def _exit(self): + """Minimal exit command. No logging or cleanup so it does not raise + exceptions""" + if self._process is not None: + try: + self._process.sendline("FINISH") + self._process.sendline("EXIT") + except Exception as e: + LOG.warning(f"Unable to exit ANSYS MAPDL: {e}") + def exit(self, close_log=True, timeout=3): """Exit MAPDL process. @@ -284,12 +299,7 @@ def exit(self, close_log=True, timeout=3): ``None`` to not wait until MAPDL stops. """ self._log.debug("Exiting ANSYS") - if self._process is not None: - try: - self._process.sendline("FINISH") - self._process.sendline("EXIT") - except Exception as e: - LOG.warning(f"Unable to exit ANSYS MAPDL: {e}") + self._exit() if close_log: self._close_apdl_log() @@ -302,11 +312,10 @@ def exit(self, close_log=True, timeout=3): tstart = time.time() while self._process.isalive(): time.sleep(0.05) - telap = tstart - time.time() - if telap > timeout: - return 1 - - return 0 + if (time.time() - tstart) > timeout: + if self._process.isalive(): + warn("MAPDL couldn't be exited on time.") + return def kill(self): """Forces ANSYS process to end and removes lock file""" diff --git a/src/ansys/mapdl/core/mapdl_core.py b/src/ansys/mapdl/core/mapdl_core.py index d0cc0b4edb..70e433b956 100644 --- a/src/ansys/mapdl/core/mapdl_core.py +++ b/src/ansys/mapdl/core/mapdl_core.py @@ -22,7 +22,7 @@ """Module to control interaction with MAPDL through Python""" -import atexit +# import atexit from functools import wraps import glob import logging @@ -247,7 +247,6 @@ def __init__( **start_parm, ): """Initialize connection with MAPDL.""" - atexit.register(self.__del__) # registering to exit properly self._show_matplotlib_figures = True # for testing self._query = None self._exited: bool = False @@ -2344,21 +2343,8 @@ def exit(self): # pragma: no cover raise NotImplementedError("Implemented by child class") def __del__(self): - """Clean up when complete""" - if self._cleanup: - # removing logging handlers if they are closed to avoid I/O errors - # when exiting after the logger file has been closed. - # self._cleanup_loggers() - logging.disable(logging.CRITICAL) - - try: - self.exit() - except Exception as e: - try: # logger might be closed - if hasattr(self, "_log") and self._log is not None: - self._log.error("exit: %s", str(e)) - except ValueError: - pass + """Kill MAPDL when garbage cleaning""" + self.exit() def _cleanup_loggers(self): """Clean up all the loggers""" diff --git a/src/ansys/mapdl/core/mapdl_grpc.py b/src/ansys/mapdl/core/mapdl_grpc.py index 98cb72ec40..ac3812955f 100644 --- a/src/ansys/mapdl/core/mapdl_grpc.py +++ b/src/ansys/mapdl/core/mapdl_grpc.py @@ -424,7 +424,7 @@ def __init__( self._busy: bool = False # used to check if running a command on the server self._local: bool = start_parm.get("local", True) - self._launched: bool = start_parm.get("launched", True) + self._launched: bool = start_parm.get("launched", False) self._health_response_queue: Optional["Queue"] = None self._exiting: bool = False self._exited: Optional[bool] = None @@ -1117,11 +1117,17 @@ def exit(self, save=False, force=False, **kwargs): Notes ----- + If Mapdl didn't start the instance, then this will be ignored unless + ``force=True``. + If ``PYMAPDL_START_INSTANCE`` is set to ``False`` (generally set in remote testing or documentation build), then this will be ignored. Override this behavior with ``force=True`` to always force exiting MAPDL regardless of your local environment. + If ``Mapdl.finish_job_on_exit`` is set to ``True`` and there is a valid + JobID in ``Mapdl.jobid``, then the SLURM job will be canceled. + Examples -------- >>> mapdl.exit() @@ -1129,20 +1135,13 @@ def exit(self, save=False, force=False, **kwargs): # check if permitted to start (and hence exit) instances from ansys.mapdl import core as pymapdl - if hasattr(self, "_log"): - self._log.debug( - f"Exiting MAPLD gRPC instance {self.ip}:{self.port} on '{self._path}'." - ) + self._log.debug( + f"Exiting MAPLD gRPC instance {self.ip}:{self.port} on '{self._path}'." + ) mapdl_path = self._path # using cached version - if self._exited is None: - self._log.debug("'self._exited' is none.") - return # Some edge cases the class object is not completely - # initialized but the __del__ method - # is called when exiting python. So, early exit here instead an - # error in the following self.directory command. - # See issue #1796 - elif self._exited: + + if self._exited: # Already exited. self._log.debug("Already exited") return @@ -1153,8 +1152,10 @@ def exit(self, save=False, force=False, **kwargs): if not force: # ignore this method if PYMAPDL_START_INSTANCE=False - if not self._start_instance: - self._log.info("Ignoring exit due to PYMAPDL_START_INSTANCE=False") + if not self._start_instance or not self._launched: + self._log.info( + "Ignoring exit due to PYMAPDL_START_INSTANCE=False or because PyMAPDL didn't launch the instance." + ) return # or building the gallery @@ -1162,17 +1163,14 @@ def exit(self, save=False, force=False, **kwargs): self._log.info("Ignoring exit due as BUILDING_GALLERY=True") return - # Actually exiting MAPDL instance - if self.finish_job_on_exit: - self._exiting = True - self._exit_mapdl(path=mapdl_path) - self._exited = True + # Exiting MAPDL instance if we launched. + self._exiting = True + self._exit_mapdl(path=mapdl_path) + self._exited = True - # Exiting HPC job - if self._mapdl_on_hpc: - self.kill_job(self.jobid) - if hasattr(self, "_log"): - self._log.debug(f"Job (id: {self.jobid}) has been cancel.") + if self.finish_job_on_exit and self._mapdl_on_hpc: + self.kill_job(self.jobid) + self._log.debug(f"Job (id: {self.jobid}) has been cancel.") # Exiting remote instances if self._remote_instance: # pragma: no cover @@ -3818,20 +3816,17 @@ def __del__(self): """In case the object is deleted""" # We are just going to escape early if needed, and kill the HPC job. # The garbage collector remove attributes before we can evaluate this. - try: - # Exiting HPC job - if ( - hasattr(self, "_mapdl_on_hpc") - and self._mapdl_on_hpc - and hasattr(self, "finish_job_on_exit") - and self.finish_job_on_exit - ): + if self._exited: + return - self.kill_job(self.jobid) + if not self._start_instance: + # Early skip if start_instance is False + return - if not self._start_instance: - return + # Killing the instance if we launched it. + if self._launched: + self._exit_mapdl(self._path) - except Exception as e: # nosec B110 - # This is on clean up. - pass # nosec B110 + # Exiting HPC job + if self._mapdl_on_hpc and self.finish_job_on_exit: + self.kill_job(self.jobid) diff --git a/tests/conftest.py b/tests/conftest.py index 7513be3ca5..d49c79aed5 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -658,8 +658,9 @@ def mapdl(request, tmpdir_factory): with pytest.raises(MapdlExitedError): mapdl._send_command_stream("/PREP7") - # Delete Mapdl object - del mapdl + # Delete Mapdl object + mapdl.exit() + del mapdl ################################################################ diff --git a/tests/test_console.py b/tests/test_console.py index 02aae3c419..91588aab3b 100644 --- a/tests/test_console.py +++ b/tests/test_console.py @@ -27,6 +27,8 @@ """ import os import time +from unittest.mock import patch +from warnings import catch_warnings import pytest @@ -111,6 +113,8 @@ def test_basic_command(cleared, mapdl_console): def test_allow_ignore(mapdl_console, cleared): mapdl_console.allow_ignore = False assert mapdl_console.allow_ignore is False + + mapdl_console.finish() with pytest.raises(pymapdl.errors.MapdlInvalidRoutineError): mapdl_console.k() @@ -132,7 +136,7 @@ def test_chaining(mapdl_console, cleared): def test_e(mapdl_console, cleared): - mapdl.prep7() + mapdl_console.prep7() mapdl_console.et("", 183) n0 = mapdl_console.n("", 0, 0, 0) n1 = mapdl_console.n("", 1, 0, 0) @@ -613,7 +617,10 @@ def test_load_table(mapdl_console, cleared): ] ) mapdl_console.load_table("my_conv", my_conv, "TIME") - assert np.allclose(mapdl_console.parameters["my_conv"], my_conv[:, -1]) + assert np.allclose( + mapdl_console.parameters["my_conv"].reshape(-1, 1), + my_conv[:, -1].reshape(-1, 1), + ) def test_mode_console(mapdl_console, cleared): @@ -647,3 +654,57 @@ def test_console_apdl_logging_start(tmpdir): assert "K,2,1,0,0" in text assert "K,3,1,1,0" in text assert "K,4,0,1,0" in text + + +def test__del__console(): + from ansys.mapdl.core.mapdl_console import MapdlConsole + + class FakeProcess: + def sendline(self, command): + pass + + class DummyMapdl(MapdlConsole): + @property + def _process(self): + return _proc + + def __init__(self): + self._proc = FakeProcess() + + with ( + patch.object(DummyMapdl, "_process", autospec=True) as mock_process, + patch.object(DummyMapdl, "_close_apdl_log") as mock_close_log, + ): + + mock_close_log.return_value = None + + # Setup + mapdl = DummyMapdl() + + del mapdl + + mock_close_log.assert_not_called() + assert [each.args[0] for each in mock_process.sendline.call_args_list] == [ + "FINISH", + "EXIT", + ] + + +@pytest.mark.parametrize("close_log", [True, False]) +def test_exit_console(mapdl_console, close_log): + with ( + patch.object(mapdl_console, "_close_apdl_log") as mock_close_log, + patch.object(mapdl_console, "_exit") as mock_exit, + ): + mock_exit.return_value = None + mock_close_log.return_value = None + + with catch_warnings(record=True): + mapdl_console.exit(close_log=close_log, timeout=1) + + if close_log: + mock_close_log.assert_called_once() + else: + mock_close_log.assert_not_called() + + mock_exit.assert_called_once() diff --git a/tests/test_mapdl.py b/tests/test_mapdl.py index 6a2731bfc1..8f5b48c57d 100644 --- a/tests/test_mapdl.py +++ b/tests/test_mapdl.py @@ -2715,3 +2715,53 @@ def my_func(i): for i in range(1_000_000): my_func(i) + + +@pytest.mark.parametrize("start_instance", [True, False]) +@pytest.mark.parametrize("exited", [True, False]) +@pytest.mark.parametrize("launched", [True, False]) +@pytest.mark.parametrize("on_hpc", [True, False]) +@pytest.mark.parametrize("finish_job_on_exit", [True, False]) +def test_garbage_clean_del( + start_instance, exited, launched, on_hpc, finish_job_on_exit +): + from ansys.mapdl.core import Mapdl + + class DummyMapdl(Mapdl): + def __init__(self): + pass + + with ( + patch.object(DummyMapdl, "_exit_mapdl") as mock_exit, + patch.object(DummyMapdl, "kill_job") as mock_kill, + ): + + mock_exit.return_value = None + mock_kill.return_value = None + + # Setup + mapdl = DummyMapdl() + mapdl._path = "" + mapdl._jobid = 1001 + + # Config + mapdl._start_instance = start_instance + mapdl._exited = exited + mapdl._launched = launched + mapdl._mapdl_on_hpc = on_hpc + mapdl.finish_job_on_exit = finish_job_on_exit + + del mapdl + + if exited or not start_instance or not launched: + mock_exit.assert_not_called() + else: + mock_exit.assert_called_once() + + if exited or not start_instance: + mock_kill.assert_not_called() + else: + if on_hpc and finish_job_on_exit: + mock_kill.assert_called_once() + else: + mock_kill.assert_not_called()