Skip to content

feat: implement ignore cache reset context #3778

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

Merged
merged 17 commits into from
Mar 4, 2025
Merged
Show file tree
Hide file tree
Changes from 13 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions doc/changelog.d/3778.miscellaneous.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
feat: implement ignore cache reset context
31 changes: 21 additions & 10 deletions src/ansys/mapdl/core/common_grpc.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,12 +21,14 @@
# SOFTWARE.

"""Common gRPC functions"""
from time import sleep
import time
from typing import Dict, Iterable, List, Literal, Optional, get_args

from ansys.api.mapdl.v0 import ansys_kernel_pb2 as anskernel
import grpc
import numpy as np

from ansys.mapdl.core import LOG
from ansys.mapdl.core.errors import MapdlConnectionError, MapdlRuntimeError

# chunk sizes for streaming and file streaming
Expand Down Expand Up @@ -188,19 +190,28 @@ def parse_chunks(
Deserialized numpy array.

"""
time_int = 0
timeout = 3 # seconds
time_step = 0.01
time_max = 3 # seconds
while not chunks.is_active():
time_int += 1
sleep(time_step)
if time_int > time_max / time_step:
raise MapdlConnectionError("The channel is not alive.")
time_max = timeout + time.time() # seconds

while not chunks.is_active() and time.time() < time_max:
time.sleep(time_step)

if not chunks.is_active() and chunks.code() != grpc.StatusCode.OK:
LOG.error("The channel might not alive.")

try:
chunk = chunks.next()
except:
return np.empty(0)

except StopIteration:
if chunks.code() == grpc.StatusCode.OK:
return np.empty(0)
else:
raise MapdlConnectionError(
"The chunk couldn't be parsed. The error information is:\n"
f"code: {chunks.code()}\n"
f"message: '{chunks.details()}'"
)

if not chunk.value_type and dtype is None:
raise ValueError("Must specify a data type for this record")
Expand Down
94 changes: 57 additions & 37 deletions src/ansys/mapdl/core/mesh_grpc.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@
from functools import wraps
import os
import re
import threading
import time
from typing import Dict
import weakref
Expand Down Expand Up @@ -76,7 +77,10 @@ def __init__(self, mapdl):
self.logger = mapdl._log
self._log = mapdl._log

self._freeze_model = False
self._ignore_cache_reset = False
self._thread_lock_nodes = threading.Lock()
self._thread_lock_elem = threading.Lock()
self._reset_cache()

def __repr__(self):
Expand Down Expand Up @@ -118,6 +122,7 @@ def _set_log_level(self, level):

def _reset_cache(self):
"""Reset entire mesh cache"""

if not self._ignore_cache_reset:
self.logger.debug("Resetting cache")

Expand Down Expand Up @@ -149,48 +154,52 @@ def _reset_cache(self):
self._tshape = None
self._tshape_key = None

@property
def ignore_cache_reset(self):
"""Context manager used to ignore mesh cache reset"""
return self._ignore_cache_reset_context(self)

def _update_cache(self):
"""Threaded local cache update.

Used when needing all the geometry entries from MAPDL.
"""
self.logger.debug("Updating cache")
# elements must have their underlying nodes selected to avoid
# VTK segfaul
with self._mapdl.save_selection:
self._mapdl.nsle("S", mute=True)

# not thread safe
self._update_cache_elem()

threads = [
self._update_cache_element_desc(),
self._update_cache_nnum(),
self._update_node_coord(),
]

for thread in threads:
thread.join()

# must occur after read
self._ignore_cache_reset = True

# somehow requesting path seems to help windows avoid an
# outright segfault prior to running CMSEL
if os.name == "nt":
_ = self._mapdl.path

# TODO: flaky
time.sleep(0.05)

self._ignore_cache_reset = False
if not self._ignore_cache_reset:
self.logger.debug("Updating cache")
# elements must have their underlying nodes selected to avoid
# VTK segfaul
with self._mapdl.save_selection:
self._mapdl.nsle("S", mute=True)

# not thread safe
self._update_cache_elem()

threads = [
self._update_cache_element_desc(),
self._update_cache_nnum(),
self._update_node_coord(),
]

for thread in threads:
thread.join()

# somehow requesting path seems to help windows avoid an
# outright segfault prior to running CMSEL
if os.name == "nt":
_ = self._mapdl.path

# TODO: flaky
time.sleep(0.05)
else:
self.logger.debug("Ignoring cache reset.")

@threaded
def _update_cache_nnum(self):
if self._cache_nnum is None:
self.logger.debug("Updating nodes cache")
nnum = self._mapdl.get_array("NODE", item1="NLIST")
self._cache_nnum = nnum.astype(np.int32)

if self._cache_nnum.size == 1:
if self._cache_nnum[0] == 0:
self._cache_nnum = np.empty(0, np.int32)
Expand Down Expand Up @@ -226,6 +235,7 @@ def _ekey(self):

@threaded
def _update_node_coord(self):
# with self._thread_lock_nodes:
if self._node_coord is None:
self._node_coord = self._load_nodes()

Expand Down Expand Up @@ -412,17 +422,14 @@ def nnum_all(self) -> np.ndarray:
>>> mapdl.mesh.nnum_all
array([ 1, 2, 3, ..., 19998, 19999, 20000])
"""
self._ignore_cache_reset = True
with self._mapdl.save_selection:
with self.ignore_cache_reset, self._mapdl.save_selection:
self._mapdl.nsel("all", mute=True)

nnum = self._mapdl.get_array("NODE", item1="NLIST")
nnum = nnum.astype(np.int32)
if nnum.size == 1 and nnum[0] == 0:
nnum = np.empty(0, np.int32)

self._ignore_cache_reset = False

return nnum

@property
Expand All @@ -434,16 +441,14 @@ def enum_all(self) -> np.ndarray:
>>> mapdl.mesh.enum_all
array([ 1, 2, 3, ..., 19998, 19999, 20000])
"""
self._ignore_cache_reset = True
with self._mapdl.save_selection:
with self.ignore_cache_reset, self._mapdl.save_selection:
self._mapdl.esel("all", mute=True)

enum = self._mapdl.get_array("ELEM", item1="ELIST")
enum = enum.astype(np.int32)
if enum.size == 1 and enum[0] == 0:
enum = np.empty(0, np.int32)

self._ignore_cache_reset = False
return enum

@property
Expand Down Expand Up @@ -859,3 +864,18 @@ def _parse_rlist(self) -> Dict[int, float]:
const_[set_][jlimit] = values_[i]

return const_

class _ignore_cache_reset_context:
"""Ignore cache reset context manager"""

def __init__(self, parent):
self._parent = weakref.ref(parent)
self._previous_state = None

def __enter__(self):
self._previous_state = self._parent()._ignore_cache_reset
self._parent()._ignore_cache_reset = True
self._parent()._log.debug("Ignore cache reset.")

def __exit__(self, *args):
self._parent()._ignore_cache_reset = self._previous_state
2 changes: 1 addition & 1 deletion tests/test_mapdl.py
Original file line number Diff line number Diff line change
Expand Up @@ -843,7 +843,7 @@ def test_partial_mesh_nnum(mapdl, make_block):

@requires("pyvista")
def test_partial_mesh_nnum2(mapdl, make_block):
mapdl.nsel("S", "NODE", vmin=1, vmax=10)
# mapdl.nsel("S", "NODE", vmin=1, vmax=10) #See #3782
mapdl.esel("S", "ELEM", vmin=10, vmax=20)
assert mapdl.mesh._grid.n_cells == 11

Expand Down
113 changes: 74 additions & 39 deletions tests/test_mesh_grpc.py
Original file line number Diff line number Diff line change
Expand Up @@ -82,46 +82,59 @@ def test_local(mapdl, cleared):
assert mapdl._local == mapdl.mesh.local


@pytest.mark.xfail(strict=False, reason="Flaky test. See #2435")
def test_empty_mesh(mapdl, cleared):
assert mapdl.mesh.n_node == 0
assert mapdl.mesh.n_elem == 0
assert mapdl.mesh.nnum_all.size == 0
assert mapdl.mesh.enum_all.size == 0
assert mapdl.mesh.nnum.size == 0
assert mapdl.mesh.enum.size == 0
assert mapdl.mesh.nodes.size == 0
# assert mapdl.mesh.node_angles.size == 0 Not implemented

# elem is a list
assert len(mapdl.mesh.elem) == 0

# Using size because it should be empty arrays
assert mapdl.mesh.ekey.size == 0
assert mapdl.mesh.et_id.size == 0
assert mapdl.mesh.tshape.size == 0
assert mapdl.mesh.material_type.size == 0
assert mapdl.mesh.etype.size == 0
assert mapdl.mesh.section.size == 0
assert mapdl.mesh.element_coord_system.size == 0
assert mapdl.mesh.elem_real_constant.size == 0
assert mapdl.mesh.ekey.size == 0

# should be empty dicts
assert not mapdl.mesh.key_option
assert not mapdl.mesh.tshape_key
assert not mapdl.mesh.element_components
assert not mapdl.mesh.node_components

# bools
assert not mapdl.mesh._has_elements
assert not mapdl.mesh._has_nodes

# Others
if has_dependency("pyvista"):
assert mapdl.mesh.grid is None
with pytest.raises(ValueError):
mapdl.mesh.save("file.vtk")
# Reset mesh grid
mapdl.mesh._grid_cache = None
assert mapdl.mesh.grid is not None

# To avoid further cache updates
with mapdl.mesh.ignore_cache_reset:

if has_dependency("pyvista"):
assert mapdl.mesh.grid.points.size == 0
assert mapdl.mesh.grid.cells.size == 0
assert mapdl.mesh.grid.n_points == 0
assert mapdl.mesh.grid.n_cells == 0

assert mapdl.mesh.n_node == 0
assert mapdl.mesh.n_elem == 0
assert mapdl.mesh.nnum_all.size == 0
assert mapdl.mesh.enum_all.size == 0
assert mapdl.mesh.nnum.size == 0
assert mapdl.mesh.enum.size == 0
assert mapdl.mesh.nodes.size == 0
# assert mapdl.mesh.node_angles.size == 0 Not implemented

# elem is a list
assert len(mapdl.mesh.elem) == 0

# Using size because it should be empty arrays
assert mapdl.mesh.ekey.size == 0
assert mapdl.mesh.et_id.size == 0
assert mapdl.mesh.tshape.size == 0
assert mapdl.mesh.material_type.size == 0
assert mapdl.mesh.etype.size == 0
assert mapdl.mesh.section.size == 0
assert mapdl.mesh.element_coord_system.size == 0
assert mapdl.mesh.elem_real_constant.size == 0
assert mapdl.mesh.ekey.size == 0

# should be empty dicts
assert not mapdl.mesh.key_option
assert not mapdl.mesh.tshape_key
assert not mapdl.mesh.element_components
assert not mapdl.mesh.node_components

# bools
assert not mapdl.mesh._has_elements
assert not mapdl.mesh._has_nodes

# Others
if has_dependency("pyvista"):
assert mapdl.mesh.grid.points.size == 0
assert mapdl.mesh.grid.cells.size == 0
assert mapdl.mesh.grid.n_points == 0
assert mapdl.mesh.grid.n_cells == 0


def test_element_node_components(mapdl, contact_geom_and_mesh):
Expand Down Expand Up @@ -357,3 +370,25 @@ def test_nsle(mapdl, two_dimensional_mesh):
]
)
assert all(selected_ids == expected_selected_ids)


@pytest.mark.parametrize("initial_state", [None, True, False])
def test_ignore_cache_reset_context(mapdl, cleared, initial_state):
mesh = mapdl.mesh
previous_state = mesh._ignore_cache_reset
mesh._ignore_cache_reset = initial_state

with mesh.ignore_cache_reset:
assert mesh._ignore_cache_reset is True

assert mesh._ignore_cache_reset == initial_state
mesh._ignore_cache_reset = previous_state


def test_ignore_cache_reset_context_logging(mapdl, cleared, caplog):
mesh = mapdl.mesh
with caplog.at_level("DEBUG"):
with mesh.ignore_cache_reset:
pass

assert "Ignore cache reset." in caplog.text
20 changes: 11 additions & 9 deletions tests/test_post.py
Original file line number Diff line number Diff line change
Expand Up @@ -214,17 +214,19 @@ def test_disp_plot(mapdl, resume, comp):

@staticmethod
@requires("ansys-tools-visualization_interface")
def test_disp_plot_subselection(mapdl, resume, verify_image_cache):
verify_image_cache.skip = True # skipping image verification
def test_disp_plot_subselection(mapdl, resume):
mapdl.esel("S", "ELEM", vmin=500, vmax=510, mute=True)

mapdl.nsel("S", "NODE", vmin=500, vmax=2000, mute=True)
mapdl.esel("S", "ELEM", vmin=500, vmax=2000, mute=True)
assert (
mapdl.post_processing.plot_nodal_displacement(
"X", smooth_shading=True, show_node_numbering=True
)
is None
pl = mapdl.post_processing.plot_nodal_displacement(
"X", smooth_shading=True, show_node_numbering=True, return_plotter=True
)
poly = pl.meshes[0]
elem_ids = np.unique(poly.cell_data["ansys_elem_num"])

assert mapdl.mesh.n_elem == len(elem_ids)
assert np.allclose(mapdl.mesh.enum, elem_ids)
assert pl.show() is None

mapdl.allsel()

@staticmethod
Expand Down
Loading