ansys / pymapdl

Pythonic interface to MAPDL
https://mapdl.docs.pyansys.com
MIT License
424 stars 120 forks source link

Very variable time for launch_mapdl() #2564

Open pmaroneh opened 9 months ago

pmaroneh commented 9 months ago

🤓 Before submitting the issue

🔍 Description of the bug

I've noticed that time to execute mapdl.launch_mapdl() can vary from a lot when executing sucessive tests. To reproduce this I've adapted [this example](https://mapdl.docs.pyansys.com/version/stable/examples/gallery_examples/00-mapdl-examples/2d_plate_with_a_hole.html#sphx-glr-examples-gallery-examples-00-mapdl-examples-2d-plate-with-a-hole-py). Here is the bar graph obtained when evaluating elapsed times: image The difference in total elapsed time is entirely explained from the difference in launch time for MAPDL. It is not normal that this launch time can vary that much. Any ideas?

🕵️ Steps To Reproduce

"""
Testing PyMAPDL launch time with model
MAPDL 2D Plane Stress Concentration Analysis
"""

import matplotlib.pyplot as plt
import numpy as np
import time
from ansys.mapdl.core import launch_mapdl
import pandas as pd

length = 0.4
width = 0.1
ratio = 0.3  # diameter/width
diameter = width * ratio
radius = diameter * 0.5

###############################################################################
# Batch Analysis
# ~~~~~~~~~~~~~~
# Function to relaunch PyMAPDL, compute the
# stress concentration for a variety of hole diameters.  

def compute_stress_con(ratio):
    """Compute the stress concentration for plate with a hole loaded
    with a uniaxial force.
    """
    start = time.time()
    mapdl = launch_mapdl()
    mapdl_launch_time = time.time() - start
    begin_time = time.time()
    mapdl.clear("NOSTART")
    mapdl.prep7()
    mapdl.units("SI")  # SI - International system (m, kg, s, K).

    # define a PLANE183 element type with thickness
    mapdl.et(1, "PLANE183", kop3=3)
    mapdl.r(1, 0.001)  # thickness of 0.001 meters)

    # Define a material (nominal steel in SI)
    mapdl.mp("EX", 1, 210e9)  # Elastic moduli in Pa (kg/(m*s**2))
    mapdl.mp("DENS", 1, 7800)  # Density in kg/m3
    mapdl.mp("NUXY", 1, 0.3)  # Poisson's Ratio
    mapdl.emodif("ALL", "MAT", 1)

    # Geometry
    # ~~~~~~~~
    # Create a rectangular area with the hole in the middle
    diameter = width * ratio
    radius = diameter * 0.5

    # create the rectangle
    rect_anum = mapdl.blc4(width=length, height=width)

    # create a circle in the middle of the rectangle
    circ_anum = mapdl.cyl4(length / 2, width / 2, radius)

    # Note how pyansys parses the output and returns the area numbers
    # created by each command.  This can be used to execute a boolean
    # operation on these areas to cut the circle out of the rectangle.
    plate_with_hole_anum = mapdl.asba(rect_anum, circ_anum)

    # Meshing
    # ~~~~~~~
    # Mesh the plate using a higher density near the hole and a lower
    # density for the remainder of the plate

    mapdl.aclear("all")

    # ensure there are at least 100 elements around the hole
    hole_esize = np.pi * diameter / 100  # 0.0002
    plate_esize = 0.01

    # increased the density of the mesh at the center
    mapdl.lsel("S", "LINE", vmin=5, vmax=8)
    mapdl.lesize("ALL", hole_esize, kforc=1)
    mapdl.lsel("ALL")

    # Decrease the area mesh expansion.  This ensures that the mesh
    # remains fine nearby the hole
    mapdl.mopt("EXPND", 0.7)  # default 1

    mapdl.esize(plate_esize)
    mapdl.amesh(plate_with_hole_anum)

    # Boundary Conditions
    # ~~~~~~~~~~~~~~~~~~~
    # Fix the left-hand side of the plate in the X direction
    mapdl.nsel("S", "LOC", "X", 0)
    mapdl.d("ALL", "UX")

    # Fix a single node on the left-hand side of the plate in the Y direction
    mapdl.nsel("R", "LOC", "Y", width / 2)
    assert mapdl.mesh.n_node == 1
    mapdl.d("ALL", "UY")

    # Apply a force on the right-hand side of the plate.  For this
    # example, we select the right-hand side of the plate.
    mapdl.nsel("S", "LOC", "X", length)

    # Next, couple the DOF for these nodes
    mapdl.cp(5, "UX", "ALL")

    # Again, select a single node in this set and apply a force to it
    mapdl.nsel("r", "loc", "y", width / 2)
    mapdl.f("ALL", "FX", 1000)

    # finally, be sure to select all nodes again to solve the entire solution
    mapdl.allsel()

    # Solve the Static Problem
    # ~~~~~~~~~~~~~~~~~~~~~~~~
    mapdl.run("/SOLU")
    mapdl.antype("STATIC")
    mapdl.solve()

    # Post-Processing
    # ~~~~~~~~~~~~~~~
    # grab the stress from the result
    result = mapdl.result
    nnum, stress = result.principal_nodal_stress(0)
    von_mises = stress[:, -1]
    max_stress = np.nanmax(von_mises)

    # compare to the "far field" stress by getting the mean value of the
    # stress at the wall
    mask = result.mesh.nodes[:, 0] == length
    far_field_stress = np.nanmean(von_mises[mask])

    # adjust by the cross sectional area at the hole
    adj = width / (width - diameter)
    stress_adj = far_field_stress * adj

    model_time = time.time() - begin_time
    mapdl.exit()
    total_elapsed = time.time() - start

    # finally, compute the stress concentration
    return max_stress / stress_adj, mapdl_launch_time, model_time, total_elapsed

###############################################################################
# Run the batch and record the stress concentration
k_t_exp = []
times = {k:[] for k in ["MAPDL_Launch_Time", "Model_Time", "Total_Elapsed"]}
ratios = np.linspace(0.01, 0.5, 10)
print("    Ratio  : Stress Concentration (K_t) : MAPDL Launch Time : Model Time : Total Elapsed")
for i,ratio in enumerate(ratios):
    stress_con, mapdl_launch_time, model_time, total_elapsed = compute_stress_con(ratio)
    print("%10.4f : %10.4f : %10.4f : %10.4f : %10.4f" % (ratio, stress_con, mapdl_launch_time, model_time, total_elapsed))
    times["MAPDL_Launch_Time"].append(mapdl_launch_time)
    times["Model_Time"].append(model_time)
    times["Total_Elapsed"].append(total_elapsed)
    k_t_exp.append(stress_con)
time_df = pd.DataFrame(times)

fig, ((ax0, ax1, ax2)) = plt.subplots(nrows=1, ncols=3)
len_data = len(time_df["MAPDL_Launch_Time"])
run_nb = [i for i in range(len_data)]
ax0.bar(run_nb,time_df["MAPDL_Launch_Time"])
ax0.set_title('MAPDL launch time')
ax1.bar(run_nb,time_df["Model_Time"])
ax1.set_title('Time for model run')
ax2.bar(run_nb,time_df["Total_Elapsed"])
ax2.set_title('Total elapsed time')
plt.show()

💻 Which Operating System are you using?

Windows

🐍 Which Python version are you using?

3.10

📝 PyMAPDL Report

PyMAPDL Software and Environment Report

Packages Requirements


Core packages

ansys.mapdl.core : 0.67.0 numpy : 1.26.2 platformdirs : 4.1.0 scipy : 1.11.4 grpc : Package not found ansys.api.mapdl.v0 : Package not found ansys.mapdl.reader : 0.53.0 google.protobuf : Package not found

Optional packages

matplotlib : 3.8.2 pyvista : 0.43.1 pyiges : 0.3.1 tqdm : 4.66.1

Ansys Installation


Version Location

192 C:\Program Files\ANSYS Inc\v192 195 C:\Program Files\ANSYS Inc\v195 202 C:\Program Files\ANSYS Inc\v202 212 C:\Program Files\ANSYS Inc\v212 221 C:\Program Files\ANSYS Inc\v221 222 C:\Program Files\ANSYS Inc\v222 231 C:\Program Files\ANSYS Inc\v231 232 C:\Program Files\ANSYS Inc\v232 241 C:\Program Files\ANSYS Inc\v241

Ansys Environment Variables


ANSYS192_DIR C:\Program Files\ANSYS Inc\v192\ANSYS ANSYS195_DIR C:\Program Files\ANSYS Inc\v195\ANSYS ANSYS202_DIR C:\Program Files\ANSYS Inc\v202\ANSYS ANSYS212_DIR C:\Program Files\ANSYS Inc\v212\ANSYS ANSYS221_DIR C:\Program Files\ANSYS Inc\v221\ANSYS ANSYS222_DIR C:\Program Files\ANSYS Inc\v222\ANSYS ANSYS231_DIR C:\Program Files\ANSYS Inc\v231\ANSYS ANSYS232_DIR C:\Program Files\ANSYS Inc\v232\ANSYS ANSYS241_DIR C:\Program Files\ANSYS Inc\v241\ANSYS ANSYSLIC_DIR C:\Program Files\ANSYS Inc\Shared Files\Licensing ANSYS_DPF_ACCEPT_LA Y ANSYS_SYSDIR winx64 ANSYS_SYSDIR32 win32 AWP_LOCALE192 en-us AWP_LOCALE195 en-us AWP_LOCALE202 en-us AWP_LOCALE212 en-us AWP_LOCALE221 en-us AWP_LOCALE222 en-us AWP_LOCALE231 en-us AWP_LOCALE232 en-us AWP_LOCALE241 en-us AWP_ROOT192 C:\Program Files\ANSYS Inc\v192 AWP_ROOT195 C:\Program Files\ANSYS Inc\v195 AWP_ROOT202 C:\Program Files\ANSYS Inc\v202 AWP_ROOT212 C:\Program Files\ANSYS Inc\v212 AWP_ROOT221 C:\Program Files\ANSYS Inc\v221 AWP_ROOT222 C:\Program Files\ANSYS Inc\v222 AWP_ROOT231 C:\Program Files\ANSYS Inc\v231 AWP_ROOT232 C:\Program Files\ANSYS Inc\v232 AWP_ROOT241 C:\Program Files\ANSYS Inc\v241 CADOE_LIBDIR192 C:\Program Files\ANSYS Inc\v192\CommonFiles\Language\en-us CADOE_LIBDIR195 C:\Program Files\ANSYS Inc\v195\CommonFiles\Language\en-us CADOE_LIBDIR202 C:\Program Files\ANSYS Inc\v202\CommonFiles\Language\en-us CADOE_LIBDIR212 C:\Program Files\ANSYS Inc\v212\CommonFiles\Language\en-us CADOE_LIBDIR221 C:\Program Files\ANSYS Inc\v221\CommonFiles\Language\en-us CADOE_LIBDIR222 C:\Program Files\ANSYS Inc\v222\CommonFiles\Language\en-us CADOE_LIBDIR231 C:\Program Files\ANSYS Inc\v231\CommonFiles\Language\en-us CADOE_LIBDIR232 C:\Program Files\ANSYS Inc\v232\CommonFiles\Language\en-us CADOE_LIBDIR241 C:\Program Files\ANSYS Inc\v241\CommonFiles\Language\en-us PYANSYS_PRIVATE_PYPI_PAT 65w37vhx6b2r7rweix54hxblqhrkm5u6aleiz76tyua53hqpafvq

📝 Installed packages

ansys-api-mapdl==0.5.1 ansys-api-platform-instancemanagement==1.0.0 ansys-mapdl-core==0.67.0 ansys-mapdl-reader==0.53.0 ansys-math-core==0.1.3 ansys-platform-instancemanagement==1.1.2 ansys-tools-path==0.4.0 appdirs==1.4.4 asttokens==2.4.1 certifi==2023.11.17 charset-normalizer==3.3.2 click==8.1.7 colorama==0.4.6 comm==0.2.0 contourpy==1.2.0 cycler==0.12.1 debugpy==1.8.0 decorator==5.1.1 exceptiongroup==1.2.0 executing==2.0.1 fonttools==4.46.0 geomdl==5.3.1 grpcio==1.60.0 idna==3.6 importlib-metadata==7.0.0 ipykernel==6.27.1 ipython==8.18.1 jedi==0.19.1 jupyter_client==8.6.0 jupyter_core==5.5.0 kiwisolver==1.4.5 matplotlib==3.8.2 matplotlib-inline==0.1.6 nest-asyncio==1.5.8 numpy==1.26.2 packaging==23.2 pandas==2.1.4 parso==0.8.3 Pillow==10.1.0 platformdirs==4.1.0 pooch==1.8.0 prompt-toolkit==3.0.43 protobuf==3.20.3 psutil==5.9.6 pure-eval==0.2.2 pyansys-tools-versioning==0.5.0 Pygments==2.17.2 pyiges==0.3.1 pyparsing==3.1.1 python-dateutil==2.8.2 pytz==2023.3.post1 pyvista==0.43.1 pywin32==306 pyzmq==25.1.2 requests==2.31.0 scipy==1.11.4 scooby==0.9.2 six==1.16.0 stack-data==0.6.3 tornado==6.4 tqdm==4.66.1 traitlets==5.14.0 tzdata==2023.3 urllib3==2.1.0 vtk==9.3.0 wcwidth==0.2.12 zipp==3.17.0

📝 Logger output file

No response

pmaroneh commented 9 months ago

Another run, on same model, leads to different results: image

germa89 commented 9 months ago

This is very interesting. I would expect very similar times.

I would test adding some waiting time after the mapdl.exit (of course do not count it towards the graph).

I'm assuming that the difference is because of the time to launch_mapdl trying to find an available port, which might be still being used by the previous MAPDL instance. But I'm not 100% that is the reason.

pmaroneh commented 9 months ago

Thanks for the suggestion @germa89 . I added time.sleep as follows: image There is now less difference for launch_mapdl, but all the values are now big :thinking: image

germa89 commented 9 months ago

You mean for launching MAPDL? ... I mean, launching could be reduced if you allocate less memory at the beginning:

mapdl = launch_mapdl(nproc=2, ram=100)

But I guess you will be transferring the time to the solving step. If the model needs more memory, it is going to increase its allocation (which takes time) or go slower during solving.

pmaroneh commented 9 months ago

Yes, launch_mapdl() execution time seems to be:

germa89 commented 9 months ago

Thanks for the suggestion @germa89 . I added time.sleep as follows: image There is now less difference for launch_mapdl, but all the values are now big 🤔 image

I think this comment kind of justify what I thought. MAPDL takes quite a few seconds to start and be ready to connect to it.

I would say this is more an expected behaviour, and we cannot really fix it in PyMAPDL.

I'm closing the issue. Thank you for the feedback @pmaroneh !!

pmaroneh commented 9 months ago

Hi @germa89 , I don't think this issue can be closed, as it is not solved nor the behavior explained. launch-mapdl() execution time:

pthieffry commented 9 months ago

@germa89 @pmaroneh I agree with Pernelle on not closing. The issue is not so much how much time a given session takes to start than the repeatability, all settings being the same. And changing memory allocation or number of cores is not the solution.

germa89 commented 9 months ago

Hi @pmaroneh @pthieffry

I think I might be missing something from your comments. Probably this a #MondayIssue.

Regarding this graph:

(Assuming that the Y-axis is seconds)

There is now less difference for launch_mapdl, but all the values are now big 🤔 image

Regarding this:

Hi @germa89 , I don't think this issue can be closed, as it is not solved nor the behavior explained. launch-mapdl() execution time:

  • varies from a factor 1 to 4 for similar configuration and run

  • is very dependent on the rest of the code - and can take (too) many seconds (note - the time spent in time.sleep was not counted in the above test, so if freeing the port had any positive impact it should have decreased the time to launch, not make it increase). Reopening again. @pthieffry FYI

  • varies from a factor 1 to 4 for similar configuration and run

  • I do not see the 1-4 factor difference. Where? (I understand factor as 1-4x times, I dont see a 4 times variation). I guess you mean seconds? But then, where?

  • is very dependent on the rest of the code - and can take (too) many seconds (note - the time spent in time.sleep was not counted in the above test, so if freeing the port had any positive impact it should have decreased the time to launch, not make it increase).

  • What do you think it is taking many seconds? I mean, starting MAPDL is slow, definitely. But I don't think we can really trim that much. And "dependent on the rest of the code"??

By the way, I should mention that exiting MAPDL is not clear. It relies on "killing" the MAPDL process which is probably not very good way to proceed. That has been fixed in v241 though.

pmaroneh commented 9 months ago

Hi @germa89 ! Let's only focus on time to execute launch_mapdl() command. Total run time of the model is not important.

germa89 commented 9 months ago

I will try to run it locally today

js4561207 commented 8 months ago

Same issue, and sometimes it even leads to timeout error. I tested launching mapdl with LOG.setLevel("DEBUG") model, and it shows it takes more time for `launch_mapdl to find an available port when launching pymapdl the second time

debuglog.txt

js4561207 commented 8 months ago

An available workaround Releasing port 50052,50053,50054 and its process before launching pymapdl is useful.

# code for releaseing port(chatgpt)
import psutil
def find_and_terminate_process_by_port(port):
    for process in psutil.process_iter(['pid', 'name', 'connections']):
        try:
            connections = process.info['connections']
            for conn in connections:
                if conn.laddr.port == port:
                    print(f"Found process {process.info['pid']} using port {port}. Terminating...")
                    process.terminate()
        except (psutil.NoSuchProcess, psutil.AccessDenied, psutil.ZombieProcess):
            pass
#The port you want to release
port_to_release = 50052
find_and_terminate_process_by_port(port_to_release)
germa89 commented 8 months ago

Same issue, and sometimes it even leads to timeout error. I tested launching mapdl with LOG.setLevel("DEBUG") model, and it shows it takes more time for `launch_mapdl to find an available port when launching pymapdl the second time

debuglog.txt

This is very useful @js4561207. I was under the impression that it is the port which is not properly released after the process has died. There might be a way to "release" the port internally in MAPDL before exiting.

An available workaround Releasing port 50052,50053,50054 and its process before launching pymapdl is useful.

# code for releaseing port(chatgpt)
import psutil
def find_and_terminate_process_by_port(port):
    for process in psutil.process_iter(['pid', 'name', 'connections']):
        try:
            connections = process.info['connections']
            for conn in connections:
                if conn.laddr.port == port:
                    print(f"Found process {process.info['pid']} using port {port}. Terminating...")
                    process.terminate()
        except (psutil.NoSuchProcess, psutil.AccessDenied, psutil.ZombieProcess):
            pass
#The port you want to release
port_to_release = 50052
find_and_terminate_process_by_port(port_to_release)

Thank you for this. We do something very similar with the launch_mapdl stop CLI: https://mapdl.docs.pyansys.com/version/dev/user_guide/cli.html#launch-mapdl-instances

It is only available at the moment if you are using PyMAPDL installed from github:

pip install git+https://github.com/ansys/pymapdl.git@main
PyMAPDL command line interface — PyMAPDL