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

FEAT: rpyc integration #859

Draft
wants to merge 12 commits into
base: main
Choose a base branch
from
1 change: 1 addition & 0 deletions doc/changelog.d/859.added.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
FEAT: rpyc integration
4 changes: 4 additions & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,10 @@ viz = [
"ansys-tools-visualization-interface>=0.2.6",
"usd-core==24.8",
]
rpyc = [
"rpyc==6.0.0",
"toolz==0.12.1",
]

[project.scripts]
ansys-mechanical = "ansys.mechanical.core.run:cli"
Expand Down
26 changes: 26 additions & 0 deletions src/ansys/mechanical/core/embedding/rpyc/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
# Copyright (C) 2022 - 2024 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.

"""Rpyc and Mechanical service implementation."""
from .server import Server
from .service import MechanicalService
from .utils import get_remote_methods, remote_method
96 changes: 96 additions & 0 deletions src/ansys/mechanical/core/embedding/rpyc/server.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
# Copyright (C) 2022 - 2024 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.
"""Rpyc server."""

import threading
import time
import typing

from rpyc.utils.server import ThreadedServer

import ansys.mechanical.core as mech
import ansys.mechanical.core.embedding.utils as utils

from .service import MechanicalService


class Server:
"""Start rpyc server."""
dipinknair marked this conversation as resolved.
Show resolved Hide resolved

def __init__(
self,
service: typing.Type[MechanicalService],
port: int = 18861,
version: int = None,
methods: typing.List[typing.Callable] = [],
impl=None,
):
"""Initialize the server."""
self._exited = False
self._app: mech.App = None
Copy link
Collaborator

Choose a reason for hiding this comment

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

use the "BackgroundApp" class to wrap exited, thread, and app. It'll simplify this class

self._poster = None
self._port = port
self._service = service
self._methods = methods
init_thread = threading.Thread(target=self._start_app, args=(version,))
print("initializing mechanical")
init_thread.start()

while self._poster is None:
time.sleep(0.01)
continue
print("done initializing mechanical")

self._impl = impl(self._app)
my_service = self._service(self._app, self._poster, self._methods, self._impl)
self._server = ThreadedServer(my_service, port=self._port)

def start(self) -> None:
"""Start server on specified port."""
print(
f"starting mechanical application in server."
f"Listening on port {self._port}\n{self._app}"
)
self._server.start()
"""try:
try:
conn.serve_all()
except KeyboardInterrupt:
print("User interrupt!")
finally:
conn.close()"""
self._exited = True

def _start_app(self, version: int) -> None:
print("starting app")
self._app = mech.App(version=version)
print("started app")
self._poster = self._app.poster
while True:
if self._exited:
break
try:
utils.sleep(40)
except Exception as e:
print(str(e))
pass
print("out of loop!")
123 changes: 123 additions & 0 deletions src/ansys/mechanical/core/embedding/rpyc/service.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
# Copyright (C) 2022 - 2024 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.
"""Mechanical service."""

import rpyc
import toolz

from .utils import get_remote_methods


class MechanicalService(rpyc.Service):
"""Starts Mechanical app services."""

def __init__(self, app, poster, functions=[], impl=None):
"""Initialize the service."""
super().__init__()
self._app = app
self._poster = poster
self._install_functions(functions)
self._install_class(impl)

def _install_functions(self, methods):
"""Install the given list of methods."""
[self._install_function(method) for method in methods]

def _install_class(self, impl):
"""Install methods from the given implemented class."""
if impl is None:
return
for methodname, method in get_remote_methods(impl):
print(f"installing {methodname} of {impl}")
self._install_method(method)

def on_connect(self, conn):
"""Handle client connection."""
print("Client connected")
print(self._app)

def on_disconnect(self, conn):
"""Handle client disconnection."""
print("Client disconnected")

def _curry_method(self, method, realmethodname):
"""Curry the given method."""

def posted(*args):
def curried():
original_method = getattr(method._owner, realmethodname)
result = original_method(*args)
return result

return self._poster.post(curried)

return posted

def _curry_function(self, methodname):
"""Curry the given function."""
wrapped = getattr(self, methodname)
curried_method = toolz.curry(wrapped)

def posted(*args):
def curried():
return curried_method(self._app, *args)

return self._poster.post(curried)

return posted

def _install_method(self, method):
"""Install methods of impl with inner and exposed pairs."""
exposed_name = f"exposed_{method.__name__}"
inner_name = f"inner_{method.__name__}"

def inner_method(*args):
"""Convert to inner method."""
result = method(*args)
return result

def exposed_method(*args):
"""Convert to exposed method."""
f = self._curry_method(method, method.__name__)
result = f(*args)
return result

setattr(self, inner_name, inner_method)
setattr(self, exposed_name, exposed_method)

def _install_function(self, function):
"""Install a functions with inner and exposed pairs."""
print(f"Installing {function}")
exposed_name = f"exposed_{function.__name__}"
inner_name = f"inner_{function.__name__}"

def inner_method(app, *args):
"""Convert to inner method."""
return function(app, *args)

def exposed_method(*args):
"""Convert to exposed method."""
f = self._curry_function(inner_name)
return f(*args)

setattr(self, inner_name, inner_method)
setattr(self, exposed_name, exposed_method)
81 changes: 81 additions & 0 deletions src/ansys/mechanical/core/embedding/rpyc/utils.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
# Copyright (C) 2022 - 2024 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.
"""Utilities necessary for remote calls."""
import typing


class remote_method:
"""Decorator for passing remote methods.

Parameters
----------
func : Callable
The function to be decorated as a remote method.
"""

def __init__(self, func):
"""Initialize with the given function."""
self._func = func

def __call__(self, *args, **kwargs):
"""Call the stored function with provided arguments."""
return self._func(*args, **kwargs)

def __call_method__(self, instance, *args, **kwargs):
"""Call the stored function with the instance and provided arguments."""
return self._func(instance, *args, **kwargs)

def __get__(self, obj, objtype):
"""Return a partially applied method."""
from functools import partial

func = partial(self.__call_method__, obj)
func._is_remote = True
func.__name__ = self._func.__name__
func._owner = obj
return func


def get_remote_methods(obj) -> typing.Generator[typing.Tuple[str, typing.Callable], None, None]:
"""Yield names and methods of an object's remote methods.

A remote method is identified by the presence of an attribute `_is_remote` set to `True`.

Parameters
----------
obj: Any
The object to inspect for remote methods.

Yields
------
Generator[Tuple[str, Callable], None, None]
A tuple containing the method name and the method itself
for each remote method found in the object.
"""
for methodname in dir(obj):
if methodname.startswith("__"):
continue
method = getattr(obj, methodname)
if not callable(method):
continue
if hasattr(method, "_is_remote") and method._is_remote is True:
yield methodname, method
Loading