diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 8a90f39a1d..98d2e826b1 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)}} diff --git a/doc/changelog.d/3649.maintenance.md b/doc/changelog.d/3649.maintenance.md new file mode 100644 index 0000000000..611ab91fb8 --- /dev/null +++ b/doc/changelog.d/3649.maintenance.md @@ -0,0 +1 @@ +feat: refactoring launcher module \ No newline at end of file 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/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/launcher/__init__.py b/src/ansys/mapdl/core/launcher/__init__.py new file mode 100644 index 0000000000..e9df630d5f --- /dev/null +++ b/src/ansys/mapdl/core/launcher/__init__.py @@ -0,0 +1,68 @@ +# 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.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 ( + 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..e4a1d5bdd4 --- /dev/null +++ b/src/ansys/mapdl/core/launcher/console.py @@ -0,0 +1,107 @@ +# 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 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 + + +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..336a674815 --- /dev/null +++ b/src/ansys/mapdl/core/launcher/grpc.py @@ -0,0 +1,429 @@ +# 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 Any, Dict, 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 ( + check_mapdl_launch, + generate_mapdl_launch_command, + 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( + 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()) + + args["port"] = get_port(args["port"], args["start_instance"]) + + start_parm = generate_start_parameters(args) + + # 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: + # 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"), + ) + + # 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( + 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, + ) # nosec B604 diff --git a/src/ansys/mapdl/core/launcher/hpc.py b/src/ansys/mapdl/core/launcher/hpc.py new file mode 100644 index 0000000000..322a2922fb --- /dev/null +++ b/src/ansys/mapdl/core/launcher/hpc.py @@ -0,0 +1,790 @@ +# 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 # 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 ( + generate_start_parameters, + get_port, + 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_locally( + 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"``. + + launch_mapdl_args : Dict[str, Any], optional + Any keyword argument from the :func:`ansys.mapdl.core.launcher.grpc.launch_mapdl_grpc` function. + + 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 + 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( + "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." + ) + + 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_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) + + ######################################## + # 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 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) + + 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: + 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 + + +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..6209a79b2e --- /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/v251/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 key 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 nodes + # 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..850a5aa3ae --- /dev/null +++ b/src/ansys/mapdl/core/launcher/local.py @@ -0,0 +1,118 @@ +# 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 typing import Any, Dict + +from ansys.mapdl.core import LOG +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_: Dict[str, Any]): + # packing arguments + args = pack_arguments(args_) # packs args and kwargs + + check_kwargs(args) # check if passing wrong arguments + + 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) + + ######################################## + # 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"] + ) + + args["env_vars"] = 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. + args["env_vars"] = configure_ubuntu(args["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"]: + 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/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..2042303b2e --- /dev/null +++ b/src/ansys/mapdl/core/launcher/remote.py @@ -0,0 +1,123 @@ +# 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 import LOG +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 + +_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, + cleanup_on_exit: bool = True, + 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 + + check_remote_args(args) + + pre_check_args(args) + + get_ip(args) + + args["port"] = get_port(args["port"], args["start_instance"]) + + start_parm = generate_start_parameters(args) + + ######################################## + # 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=args["cleanup_on_exit"], + 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..e699868362 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 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, + ) # nosec B604 + + 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,94 @@ 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: + from ansys.mapdl.core.launcher import launch_mapdl + + 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 +607,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 +664,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 +803,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, _ = _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): @@ -1820,205 +1114,39 @@ def set_license_switch(license_type, additional_switches): "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] - + warnings.warn(warn_text, UserWarning) -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) - ] + additional_switches += " -p " + license_type + LOG.debug( + f"Using specified license name '{license_type}' in the 'license_type' keyword argument." ) - 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." - ) + 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_): @@ -2048,27 +1176,10 @@ 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 -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 @@ -2465,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) @@ -2475,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 " @@ -2488,10 +1553,12 @@ 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 - if args["start_timeout"] is None: + if args.get("start_timeout", None) is None: if args["launch_on_hpc"]: args["start_timeout"] = 90 else: @@ -2505,6 +1572,20 @@ 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("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]): """Get number of CPUs @@ -2523,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 @@ -2561,335 +1642,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_core.py b/src/ansys/mapdl/core/mapdl_core.py index aa71a1d014..bba4d31b0f 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/src/ansys/mapdl/core/mapdl_grpc.py b/src/ansys/mapdl/core/mapdl_grpc.py index 8f44fe4a66..81bdf7f08d 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) @@ -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) @@ -523,7 +522,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 ) @@ -651,7 +650,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 ) @@ -890,7 +889,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.grpc import launch_grpc + from ansys.mapdl.core.launcher.tools import generate_mapdl_launch_command self._exited = False # reset exit state @@ -3804,8 +3804,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""" @@ -3825,6 +3824,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 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 a262eb5e62..b3dde2ba0d 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 @@ -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 3d3e7ef47f..7513be3ca5 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() @@ -223,7 +243,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"): @@ -680,13 +701,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_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 c1de62b542..af32d89c7e 100644 --- a/tests/test_launcher.py +++ b/tests/test_launcher.py @@ -23,58 +23,34 @@ """Test the mapdl launcher""" import os -import subprocess +import subprocess # nosec B404 import tempfile from time import sleep 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.errors import ( - MapdlDidNotStart, - NotEnoughResources, - PortAlreadyInUseByAnMAPDLInstance, - VersionError, -) -from ansys.mapdl.core.launcher import ( - _HAS_ATP, - LOCALHOST, - _is_ubuntu, - _parse_ip_route, +from ansys.mapdl.core import _HAS_ATP +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 ( 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, - 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, @@ -92,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 @@ -101,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), @@ -151,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): @@ -216,8 +167,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 +187,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 +214,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) @@ -338,69 +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() - - 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) - - -@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) + with ( + patch.object(mapdl, "finish_job_on_exit", False), + patch.object(mapdl, "_local", True), + patch.object(mapdl, "remove_temp_dir_on_exit", True), + ): -def test_env_injection(): - no_inject = update_env_vars(None, None) - assert no_inject == os.environ.copy() # return os.environ + # Testing reaching the method + with patch.object(mapdl, "_remove_temp_dir_on_exit") as mock_rm: + mock_rm.side_effect = None - assert "myenvvar" in update_env_vars({"myenvvar": "True"}, None) + mapdl.exit(force=True) - _env_vars = update_env_vars(None, {"myenvvar": "True"}) - assert len(_env_vars) == 1 - assert "myenvvar" in _env_vars + mock_rm.assert_called() + assert mapdl.directory == mock_rm.call_args.args[0] - with pytest.raises(ValueError): - update_env_vars({"myenvvar": "True"}, {"myenvvar": "True"}) + # Testing the method + # Directory to be deleted + ans_temp_dir = os.path.join(tempfile.gettempdir(), "ansys_") - with pytest.raises(TypeError): - update_env_vars("asdf", None) + 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) - with pytest.raises(TypeError): - update_env_vars(None, "asdf") + # 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) @pytest.mark.requires_gui @@ -427,92 +354,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): @@ -545,24 +386,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( @@ -579,19 +402,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 @@ -840,48 +650,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]) @@ -993,251 +761,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._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]) @@ -1270,38 +793,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}], @@ -1370,7 +861,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 +914,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 +951,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 +1002,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 +1043,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 @@ -1627,65 +1118,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.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: @@ -1729,7 +1161,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): if raises: context = pytest.raises(raises) @@ -1759,123 +1191,10 @@ 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): - 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 +1203,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 +1297,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", [ @@ -2000,25 +1324,11 @@ 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.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 +1339,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): mapdl_output = os.path.join(tmpdir, "apdl.txt") args = launch_mapdl(just_launch=True, mapdl_output=mapdl_output) @@ -2056,18 +1366,44 @@ 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) + + +@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_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_launcher_console.py b/tests/test_launcher/test_launcher_console.py new file mode 100644 index 0000000000..0af1786498 --- /dev/null +++ b/tests/test_launcher/test_launcher_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_launcher_grpc.py b/tests/test_launcher/test_launcher_grpc.py new file mode 100644 index 0000000000..0dc6276ada --- /dev/null +++ b/tests/test_launcher/test_launcher_grpc.py @@ -0,0 +1,34 @@ +# 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.grpc import launch_mapdl_grpc +from conftest import requires + + +@requires("local") +@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..a2a87bf05e --- /dev/null +++ b/tests/test_launcher/test_local.py @@ -0,0 +1,48 @@ +# 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 + + +@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_remote.py b/tests/test_launcher/test_pim.py similarity index 100% rename from tests/test_launcher_remote.py rename to tests/test_launcher/test_pim.py diff --git a/tests/test_launcher/test_remote.py b/tests/test_launcher/test_remote.py new file mode 100644 index 0000000000..94259c51db --- /dev/null +++ b/tests/test_launcher/test_remote.py @@ -0,0 +1,78 @@ +# 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.remote import _NON_VALID_ARGS, connect_to_mapdl + + +def test_connect_to_mapdl(mapdl): + mapdl_2 = connect_to_mapdl(port=mapdl.port) + + assert "PREP7" in mapdl_2.prep7() + + assert not mapdl_2._start_instance + assert not mapdl_2._launched + + +@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 diff --git a/tests/test_launcher/test_tools.py b/tests/test_launcher/test_tools.py new file mode 100644 index 0000000000..9fbe951010 --- /dev/null +++ b/tests/test_launcher/test_tools.py @@ -0,0 +1,739 @@ +# 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 +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 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) +_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( + "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", + [ + [ + {"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) + + +@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() diff --git a/tests/test_mapdl.py b/tests/test_mapdl.py index 89b7cc1e95..d8f633c7a7 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 @@ -2287,43 +2286,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'") diff --git a/tests/test_pool.py b/tests/test_pool.py index 5e73940a30..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: @@ -576,7 +577,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 +589,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 +601,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(