Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

PyPIM integration #1091

Merged
merged 5 commits into from May 4, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
2 changes: 2 additions & 0 deletions doc/source/conf.py
Expand Up @@ -72,6 +72,8 @@
"matplotlib": ("https://matplotlib.org/stable", None),
"pandas": ("https://pandas.pydata.org/pandas-docs/stable", None),
"pyvista": ("https://docs.pyvista.org/", None),
"grpc": ("https://grpc.github.io/grpc/python/", None),
"pypim": ("https://pypim.docs.pyansys.com/", None),
}

suppress_warnings = ["label.*"]
Expand Down
1 change: 1 addition & 0 deletions pyproject.toml
Expand Up @@ -20,6 +20,7 @@ dependencies = [
"ansys-api-mapdl==0.5.1", # supports at least 2020R2 - 2022R1
"ansys-corba; python_version < '3.9'",
"ansys-mapdl-reader>=0.51.7",
"ansys-platform-instancemanagement~=0.2.0",
"appdirs>=1.4.0",
"grpcio>=1.30.0", # tested up to grpcio==1.35
"importlib-metadata >=4.0",
Expand Down
54 changes: 53 additions & 1 deletion src/ansys/mapdl/core/launcher.py
Expand Up @@ -11,14 +11,15 @@
import time
import warnings

import ansys.platform.instancemanagement as pypim
import appdirs

from ansys.mapdl import core as pymapdl
from ansys.mapdl.core import LOG
from ansys.mapdl.core.errors import LockFileException, VersionError
from ansys.mapdl.core.licensing import ALLOWABLE_LICENSES, LicenseChecker
from ansys.mapdl.core.mapdl import _MapdlCore
from ansys.mapdl.core.mapdl_grpc import MapdlGrpc
from ansys.mapdl.core.mapdl_grpc import MAX_MESSAGE_LENGTH, MapdlGrpc
from ansys.mapdl.core.misc import (
check_valid_ip,
check_valid_port,
Expand Down Expand Up @@ -519,6 +520,46 @@ def launch_grpc(
return port, run_location


def launch_remote_mapdl(
version=None,
cleanup_on_exit=True,
) -> _MapdlCore:
"""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 <ansys.platform.instancemanagement.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._MapdlCore
An instance of 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
)


def get_start_instance(start_instance_default=True):
"""Check if the environment variable ``PYMAPDL_START_INSTANCE`` exists and is valid.

Expand Down Expand Up @@ -1156,6 +1197,11 @@ def launch_mapdl(
Enables shared-memory parallelism.
See the Parallel Processing Guide for more information.

If the environment is configured to use `PyPIM <https://pypim.docs.pyansys.com>`_
and ``start_instance`` is ``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.
Expand Down Expand Up @@ -1201,6 +1247,12 @@ def launch_mapdl(
port = int(os.environ.get("PYMAPDL_PORT", MAPDL_DEFAULT_PORT))
check_valid_port(port)

# Start MAPDL with PyPIM if the environment is configured for it
# and the user did not pass a directive on how to launch it.
if pypim.is_configured() and exec_file is None:
LOG.info("Starting MAPDL remotely. The startup configuration will be ignored.")
return launch_remote_mapdl(cleanup_on_exit=cleanup_on_exit)

# connect to an existing instance if enabled
if start_instance is None:
start_instance = check_valid_start_instance(
Expand Down
15 changes: 15 additions & 0 deletions src/ansys/mapdl/core/mapdl_grpc.py
Expand Up @@ -221,6 +221,15 @@ class MapdlGrpc(_MapdlCore):
Print the command ``/COM`` arguments to the standard output.
Default ``False``.

channel : grpc.Channel, optional
gRPC channel to use for the connection. Can be used as an
alternative to the ``ip`` and ``port`` parameters.

remote_instance : ansys.platform.instancemanagement.Instance
The corresponding remote instance when MAPDL is launched through
PyPIM. This instance will be deleted when calling
:func:`Mapdl.exit <ansys.mapdl.core.Mapdl.exit>`.

Examples
--------
Connect to an instance of MAPDL already running on locally on the
Expand Down Expand Up @@ -269,10 +278,12 @@ def __init__(
remove_temp_files=False,
print_com=False,
channel=None,
remote_instance=None,
**kwargs,
):
"""Initialize connection to the mapdl server"""
self.__distributed = None
self._remote_instance = remote_instance

if channel is not None:
if ip is not None or port is not None:
Expand Down Expand Up @@ -815,6 +826,10 @@ def exit(self, save=False, force=False): # pragma: no cover
self._close_process()
self._remove_lock_file()

if self._remote_instance:
# No cover: The CI is working with a single MAPDL instance
self._remote_instance.delete() # pragma: no cover

if self._remove_tmp and self._local:
self._log.debug("Removing local temporary files")
shutil.rmtree(self.directory, ignore_errors=True)
Expand Down
64 changes: 64 additions & 0 deletions tests/test_launcher_remote.py
@@ -0,0 +1,64 @@
"""Test the PyPIM integration."""
from unittest.mock import create_autospec
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why unittest? I cannot even see when this is installed in pymapdl

Copy link
Collaborator

@akaszynski akaszynski May 3, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

unittest is a standard Python library: https://docs.python.org/3/library/unittest.html

That being said, we should be using pytest instead.

This appears to be compatible with pytest, so there's no issue here.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, fortunately pytest is very unopinionated on how the assertion are done, only on how tests are declared. Here I created the mocks using the built-in unittest (pytest does not have any specific tooling for that), but injected them with the monkeypatch fixture from pytest.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There is pytest-mock but yeah... another package.


import ansys.platform.instancemanagement as pypim
import grpc

from ansys.mapdl.core.launcher import launch_mapdl
from ansys.mapdl.core.mapdl_grpc import MAX_MESSAGE_LENGTH


def test_launch_remote_instance(monkeypatch, mapdl):
# Create a mock pypim pretenting it is configured and returning a channel to an already running mapdl
mock_instance = pypim.Instance(
definition_name="definitions/fake-mapdl",
name="instances/fake-mapdl",
ready=True,
status_message=None,
services={"grpc": pypim.Service(uri=mapdl._channel_str, headers={})},
)
pim_channel = grpc.insecure_channel(
mapdl._channel_str,
options=[
("grpc.max_receive_message_length", MAX_MESSAGE_LENGTH),
],
)
mock_instance.wait_for_ready = create_autospec(mock_instance.wait_for_ready)
mock_instance.build_grpc_channel = create_autospec(
mock_instance.build_grpc_channel, return_value=pim_channel
)
mock_instance.delete = create_autospec(mock_instance.delete)

mock_client = pypim.Client(channel=grpc.insecure_channel("localhost:12345"))
mock_client.create_instance = create_autospec(
mock_client.create_instance, return_value=mock_instance
)

mock_connect = create_autospec(pypim.connect, return_value=mock_client)
mock_is_configured = create_autospec(pypim.is_configured, return_value=True)
monkeypatch.setattr(pypim, "connect", mock_connect)
monkeypatch.setattr(pypim, "is_configured", mock_is_configured)
germa89 marked this conversation as resolved.
Show resolved Hide resolved

# Start MAPDL with launch_mapdl
# Note: This is mocking to start MAPDL, but actually reusing the common one
# Thus cleanup_on_exit is set to false
mapdl = launch_mapdl(cleanup_on_exit=False)
germa89 marked this conversation as resolved.
Show resolved Hide resolved

# Assert: pymapdl went through the pypim workflow
assert mock_is_configured.called
assert mock_connect.called
mock_client.create_instance.assert_called_with(
product_name="mapdl", product_version=None
)
assert mock_instance.wait_for_ready.called
mock_instance.build_grpc_channel.assert_called_with(
options=[
("grpc.max_receive_message_length", MAX_MESSAGE_LENGTH),
]
)

# And it connected using the channel created by PyPIM
assert mapdl._channel == pim_channel

# and it kept track of the instance to be able to delete it
assert mapdl._remote_instance == mock_instance