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 @@
rpyc integration
4 changes: 4 additions & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,10 @@ viz = [
"ansys-tools-visualization-interface>=0.2.6",
"usd-core==24.8",
]
rpc = [
"rpyc==6.0.0",
"toolz==0.12.1",
]

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

"""RPC and Mechanical service implementation."""
from .client import Client
from .server import Server
from .service import MechanicalService
from .utils import get_remote_methods, remote_method
80 changes: 80 additions & 0 deletions src/ansys/mechanical/core/embedding/rpc/client.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
# 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.
"""Client for Mechanical services."""

import time

import rpyc


class Client:
"""Client for connecting to Mechanical services."""

def __init__(self, host: str, port: int, timeout: float = 60.0):
"""Initialize the client.

Parameters
----------
host : str, optional
IP address to connect to the server. The default is ``None``
in which case ``localhost`` is used.
port : int, optional
Port to connect to the Mecahnical server. The default is ``None``,
in which case ``10000`` is used.
timeout : float, optional
Maximum allowable time for connecting to the Mechanical server.
The default is ``60.0``.

"""
self.host = host
self.port = port
self.timeout = timeout
self.connection = None
self.root = None
self._connect()

def _connect(self):
self._wait_until_ready()
self.connection = rpyc.connect(self.host, self.port)
self.root = self.connection.root
print(f"Connected to {self.host}:{self.port}")

def _wait_until_ready(self):
t_max = time.time() + self.timeout
while time.time() < t_max:
try:
conn = rpyc.connect(self.host, self.port)
conn.ping() # Simple ping to check if the connection is healthy
conn.close()
print("Server is ready to connect")
break
except:
time.sleep(2)
else:
raise TimeoutError(
f"Server at {self.host}:{self.port} not ready within {self.timeout} seconds."
)

def close(self):
"""Close the connection."""
self.connection.close()
print(f"Connection to {self.host}:{self.port} closed")
100 changes: 100 additions & 0 deletions src/ansys/mechanical/core/embedding/rpc/server.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
# 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.
"""Remote Procedure Call (RPC) 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 rpc server."""

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
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")

if impl is None:
self._impl = None
else:
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/rpc/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)
Loading
Loading