"""Module for launching the pydyna solver service locally."""
import os
import socket
import subprocess
import sys
from time import sleep
from zipfile import ZipFile
from ansys.dyna.core.solver.dynalogging import LOG
try:
import ansys.platform.instancemanagement as pypim
_HAS_PIM = True
except ModuleNotFoundError: # pragma: no cover
_HAS_PIM = False
from ansys.tools.path import get_available_ansys_installations, get_latest_ansys_installation
from ansys.dyna.core.solver import DynaSolver
[docs]
LOCALHOST = "127.0.0.1"
[docs]
DYNA_DEFAULT_PORT = 5000
[docs]
SERVER_SOLVER_VERSION = "v0.4.13"
[docs]
MAX_MESSAGE_LENGTH = 8 * 1024**2
[docs]
def check_ports(port_range, ip="localhost"):
"""Check the state of ports in a port range.
Parameters
----------
port_range :
ip : str, optional
IP address. The default is ``"localhost"``, in which case
``"127.0.0.1"``is used.
"""
ports = {}
for port in port_range:
ports[port] = port_in_use(port, ip)
return ports
[docs]
def port_in_use(port, host=LOCALHOST):
"""
Determine if a port is in use at a given host.
Parameters
----------
port : int
Port.
host :
Host. The default is ``LOCALHOST``, in which case ``"127.0.0.1"``
is used.
Returns
-------
``True`` when a port is in use at the given host, ``False`` otherwise.
Notes
-----
The port must "bind" the address. Just checking if a socket can be created
is insufficient because it is possible to run into permission
errors like this one:
"An attempt was made to access a socket in a way forbidden by its
access permissions."
"""
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as sock:
try:
sock.bind((host, port))
return False
except:
return True
def _check_minimal_versions(latest_installed_version: int) -> None:
"""Check client is compatible with Ansys Products.
Check that at least V241 is installed.
"""
if abs(latest_installed_version) < 241:
msg = (
"PyAnsys Geometry is compatible with Ansys Products from version 24.1.0. "
+ "Please install Ansys products 24.1.0 or later."
)
raise SystemError(msg)
def _check_version_is_available(version: int, installations: dict[int, str]) -> None:
"""Check that the requested version for launcher is installed."""
if version not in installations:
msg = (
f"The requested Ansys product's version {version} is not available, "
+ "please specify a different version."
)
raise SystemError(msg)
[docs]
def launch_grpc(
port=DYNA_DEFAULT_PORT, ip=LOCALHOST, server_path="", product_version=None
) -> tuple: # pragma: no cover
"""
Launch the solver service locally in gRPC mode.
Parameters
----------
port : int, optional
Port to launch the solver service on. The default is ``DYNAPRE_DEFAULT_PORT``.
The final port is the first port available after (or including) this
port.
ip : str, optional
IP address. The default is ``LOCALHOST``, in which case ``"127.0.0.1"``
is used.
server_path : string, optional
Path to the solver service. The default is ``None``.
Returns
-------
int
Port number that the gRPC instance started on.
"""
# start server locally
if (ip.lower() == "localhost" or ip == LOCALHOST) and not DynaSolver.grpc_local_server_on():
# LOG.debug("Starting solver server")
if len(server_path) == 0:
# server_path = os.getenv("ANSYS_PYDYNA_SOLVER_SERVER_PATH")
# if server_path is None:
url = "https://github.com/ansys/pydyna/releases/download/v0.4.12/ansys-pydyna-solver-server.zip"
directory = DynaSolver.get_appdata_path()
filename = directory + os.sep + "ansys-pydyna-solver-server.zip"
server_package = directory + os.sep + "ansys-pydyna-solver-server"
extractpath = directory
if not os.path.exists(server_package):
DynaSolver.downloadfile(url, filename)
with ZipFile(filename, "r") as zipf:
zipf.extractall(extractpath)
else:
with ZipFile(filename, "r") as zipf:
zipinfo = zipf.getinfo("ansys-pydyna-solver-server/")
version = str(zipinfo.comment, encoding="utf-8")
if version != SERVER_SOLVER_VERSION:
DynaSolver.downloadfile(url, filename)
with ZipFile(filename, "r") as zipf:
zipf.extractall(extractpath)
server_path = server_package
# os.environ["ANSYS_PYDYNA_SOLVER_SERVER_PATH"] = server_path
# Check Ansys version
installations = get_available_ansys_installations()
if product_version is not None:
try:
_check_version_is_available(product_version, installations)
except SystemError as serr:
# The user requested a version as a Student version...
# Let's negate it and try again... if this works, we override the
# product_version variable.
try:
_check_version_is_available(-product_version, installations)
except SystemError:
# The student version is not installed either... raise the original error.
raise serr
product_version = -product_version
else:
product_version = get_latest_ansys_installation()[0]
# Verify that the minimum version is installed.
_check_minimal_versions(product_version)
if os.path.isdir(server_path):
# threadserver = ServerThread(1, port=port, ip=ip, server_path=server_path)
# threadserver.run()
# threadserver.setDaemon(True)
# threadserver.start()
process = subprocess.Popen(f"{sys.executable} server.py {product_version}", cwd=server_path, shell=True)
# if platform.system() == "Windows":
# process = subprocess.Popen("python server.py", cwd=server_path, shell=True)
# else:
# process = subprocess.Popen("python3 server.py", cwd=server_path, shell=True)
waittime = 0
while not DynaSolver.grpc_local_server_on():
sleep(5)
waittime += 5
if waittime > 60:
print("Failed to start pydyna solver server locally")
break
else:
print("Failed to start pydyna solver server locally,Invalid server path!")
LOG.debug("Starting 'launch_grpc'.")
command = "python server.py"
LOG.debug(f"Starting the solver service with command: {command}")
# env_vars = update_env_vars(add_env_vars, replace_env_vars)
LOG.info(f"Running in {ip}:{port} the following command: '{command}'")
LOG.debug("the solver service starting in background.")
# if platform.system() == "Windows":
# process = subprocess.Popen("python server.py", cwd=server_path, shell=True)
# else:
# process = subprocess.Popen("python3 server.py", cwd=server_path, shell=True)
# process.wait()
# return port
[docs]
def launch_remote_dyna(
version=None,
cleanup_on_exit=True,
) -> DynaSolver:
"""Start DYNA 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 DYNA 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 DYNA when python exits or the dyna Python instance is
garbage collected.
If unspecified, it will be cleaned up.
Returns
-------
ansys.dyna.core.solver.DynaSolver
An instance of DynaSolver.
"""
if not _HAS_PIM: # pragma: no cover
raise ModuleNotFoundError("The package 'ansys-platform-instancemanagement' is required to use this function.")
pim = pypim.connect()
instance = pim.create_instance(product_name="dynasolver", product_version=version)
instance.wait_for_ready()
channel = instance.build_grpc_channel(
options=[
("grpc.max_receive_message_length", MAX_MESSAGE_LENGTH),
]
)
client = DynaSolver(channel=channel)
client.pim_client = pim
client.remote_instance = instance
return client
[docs]
def launch_dyna(
product_version: int = None,
port=None,
ip=None,
) -> DynaSolver:
"""Start DYNA locally.
Parameters
----------
product_version: int, optional
The product version to be started. Goes from v20.1 to
the latest. Default is ``None``.
If a specific product version is requested but not installed locally,
a SystemError will be raised.
**Ansys products versions and their corresponding int values:**
* ``241`` : Ansys 24R1
* ``242`` : Ansys 24R2
port : int
Port to launch DYNA gRPC on. Final port will be the first
port available after (or including) this port. Defaults to
5000.
ip : bool, optional
You can provide a hostname as an alternative to an IP address.
Defaults to ``'127.0.0.1'``.
Examples
--------
Launch DYNA using the best protocol.
>>> from ansys.dyna.core.solver import launch_dyna
>>> solution = launch_dyna()
Connect to an existing instance of DYNA at IP 192.168.1.30 and
port 5000.
>>> solution = launch_dyna(ip='192.168.1.30',port=5000)
"""
# check_valid_ip(ip) # double check
# check_valid_port(port)
# Start DYNA with PyPIM if the environment is configured for it
# and the user did not pass a directive on how to launch it.
if _HAS_PIM and pypim.is_configured():
LOG.info("Starting DYNA remotely. The startup configuration will be ignored.")
return launch_remote_dyna()
launch_grpc(port=port, ip=ip, product_version=product_version)
dyna = DynaSolver(
hostname=ip,
port=port,
)
return dyna
# class ServerThread(threading.Thread):
[docs]
class ServerThread:
"""Provides server thread properties.
Parameters
----------
threadID :
port :
ip :
server_path :
"""
def __init__(self, threadID, port, ip, server_path):
# threading.Thread.__init__(self)
[docs]
self.threadID = threadID
[docs]
self.server_path = server_path
[docs]
def run(self):
self.process = launch_grpc(ip=self.ip, port=self.port, server_path=self.server_path)
[docs]
def termination(self):
self.process.termination()