solidipes-solid-mech-plugin 0.0.15__tar.gz → 0.0.16__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (53) hide show
  1. {solidipes_solid_mech_plugin-0.0.15 → solidipes_solid_mech_plugin-0.0.16}/PKG-INFO +6 -3
  2. {solidipes_solid_mech_plugin-0.0.15 → solidipes_solid_mech_plugin-0.0.16}/pyproject.toml +5 -2
  3. solidipes_solid_mech_plugin-0.0.16/solidipes_solid_mech_plugin/loaders/exodus.py +184 -0
  4. {solidipes_solid_mech_plugin-0.0.15 → solidipes_solid_mech_plugin-0.0.16}/solidipes_solid_mech_plugin/viewers/pyvista_plotter.py +3 -3
  5. {solidipes_solid_mech_plugin-0.0.15 → solidipes_solid_mech_plugin-0.0.16}/solidipes_solid_mech_plugin.egg-info/PKG-INFO +6 -3
  6. {solidipes_solid_mech_plugin-0.0.15 → solidipes_solid_mech_plugin-0.0.16}/solidipes_solid_mech_plugin.egg-info/SOURCES.txt +2 -0
  7. {solidipes_solid_mech_plugin-0.0.15 → solidipes_solid_mech_plugin-0.0.16}/solidipes_solid_mech_plugin.egg-info/requires.txt +5 -2
  8. {solidipes_solid_mech_plugin-0.0.15 → solidipes_solid_mech_plugin-0.0.16}/solidipes_solid_mech_plugin.egg-info/scm_file_list.json +2 -0
  9. solidipes_solid_mech_plugin-0.0.16/solidipes_solid_mech_plugin.egg-info/scm_version.json +8 -0
  10. solidipes_solid_mech_plugin-0.0.16/tests/test_exodus_mesh.py +88 -0
  11. {solidipes_solid_mech_plugin-0.0.15 → solidipes_solid_mech_plugin-0.0.16}/tests/test_streamlit_pyvista.py +109 -7
  12. solidipes_solid_mech_plugin-0.0.16/uv.lock +4464 -0
  13. solidipes_solid_mech_plugin-0.0.15/solidipes_solid_mech_plugin.egg-info/scm_version.json +0 -8
  14. solidipes_solid_mech_plugin-0.0.15/uv.lock +0 -3519
  15. {solidipes_solid_mech_plugin-0.0.15 → solidipes_solid_mech_plugin-0.0.16}/.flake8 +0 -0
  16. {solidipes_solid_mech_plugin-0.0.15 → solidipes_solid_mech_plugin-0.0.16}/.gitignore +0 -0
  17. {solidipes_solid_mech_plugin-0.0.15 → solidipes_solid_mech_plugin-0.0.16}/.gitlab-ci.yml +0 -0
  18. {solidipes_solid_mech_plugin-0.0.15 → solidipes_solid_mech_plugin-0.0.16}/.pre-commit-config.yaml +0 -0
  19. {solidipes_solid_mech_plugin-0.0.15 → solidipes_solid_mech_plugin-0.0.16}/CHANGELOG.md +0 -0
  20. {solidipes_solid_mech_plugin-0.0.15 → solidipes_solid_mech_plugin-0.0.16}/LICENSE +0 -0
  21. {solidipes_solid_mech_plugin-0.0.15 → solidipes_solid_mech_plugin-0.0.16}/README.md +0 -0
  22. {solidipes_solid_mech_plugin-0.0.15 → solidipes_solid_mech_plugin-0.0.16}/setup.cfg +0 -0
  23. {solidipes_solid_mech_plugin-0.0.15 → solidipes_solid_mech_plugin-0.0.16}/solidipes_solid_mech_plugin/__init__.py +0 -0
  24. {solidipes_solid_mech_plugin-0.0.15 → solidipes_solid_mech_plugin-0.0.16}/solidipes_solid_mech_plugin/config.py +0 -0
  25. {solidipes_solid_mech_plugin-0.0.15 → solidipes_solid_mech_plugin-0.0.16}/solidipes_solid_mech_plugin/loaders/__init__.py +0 -0
  26. {solidipes_solid_mech_plugin-0.0.15 → solidipes_solid_mech_plugin-0.0.16}/solidipes_solid_mech_plugin/loaders/abaqus.py +0 -0
  27. {solidipes_solid_mech_plugin-0.0.15 → solidipes_solid_mech_plugin-0.0.16}/solidipes_solid_mech_plugin/loaders/abaqus_python_code.py +0 -0
  28. {solidipes_solid_mech_plugin-0.0.15 → solidipes_solid_mech_plugin-0.0.16}/solidipes_solid_mech_plugin/loaders/geo_msh.py +0 -0
  29. {solidipes_solid_mech_plugin-0.0.15 → solidipes_solid_mech_plugin-0.0.16}/solidipes_solid_mech_plugin/loaders/geof_mesh.py +0 -0
  30. {solidipes_solid_mech_plugin-0.0.15 → solidipes_solid_mech_plugin-0.0.16}/solidipes_solid_mech_plugin/loaders/meshio.py +0 -0
  31. {solidipes_solid_mech_plugin-0.0.15 → solidipes_solid_mech_plugin-0.0.16}/solidipes_solid_mech_plugin/loaders/parse_inp.py +0 -0
  32. {solidipes_solid_mech_plugin-0.0.15 → solidipes_solid_mech_plugin-0.0.16}/solidipes_solid_mech_plugin/loaders/pyvista_mesh.py +0 -0
  33. {solidipes_solid_mech_plugin-0.0.15 → solidipes_solid_mech_plugin-0.0.16}/solidipes_solid_mech_plugin/loaders/xdmf.py +0 -0
  34. {solidipes_solid_mech_plugin-0.0.15 → solidipes_solid_mech_plugin-0.0.16}/solidipes_solid_mech_plugin/ontologies/solidipes_solid_mech.py +0 -0
  35. {solidipes_solid_mech_plugin-0.0.15 → solidipes_solid_mech_plugin-0.0.16}/solidipes_solid_mech_plugin/viewers/__init__.py +0 -0
  36. {solidipes_solid_mech_plugin-0.0.15 → solidipes_solid_mech_plugin-0.0.16}/solidipes_solid_mech_plugin/viewers/xdmf.py +0 -0
  37. {solidipes_solid_mech_plugin-0.0.15 → solidipes_solid_mech_plugin-0.0.16}/solidipes_solid_mech_plugin.egg-info/dependency_links.txt +0 -0
  38. {solidipes_solid_mech_plugin-0.0.15 → solidipes_solid_mech_plugin-0.0.16}/solidipes_solid_mech_plugin.egg-info/entry_points.txt +0 -0
  39. {solidipes_solid_mech_plugin-0.0.15 → solidipes_solid_mech_plugin-0.0.16}/solidipes_solid_mech_plugin.egg-info/top_level.txt +0 -0
  40. {solidipes_solid_mech_plugin-0.0.15 → solidipes_solid_mech_plugin-0.0.16}/tests/assets/generate_assets.py +0 -0
  41. {solidipes_solid_mech_plugin-0.0.15 → solidipes_solid_mech_plugin-0.0.16}/tests/assets/pyvista_mesh.vtu +0 -0
  42. {solidipes_solid_mech_plugin-0.0.15 → solidipes_solid_mech_plugin-0.0.16}/tests/assets/pyvista_mesh_no_fields.vtu +0 -0
  43. {solidipes_solid_mech_plugin-0.0.15 → solidipes_solid_mech_plugin-0.0.16}/tests/assets/pyvista_plot.png +0 -0
  44. {solidipes_solid_mech_plugin-0.0.15 → solidipes_solid_mech_plugin-0.0.16}/tests/assets/test_mesh_loaded.png +0 -0
  45. {solidipes_solid_mech_plugin-0.0.15 → solidipes_solid_mech_plugin-0.0.16}/tests/assets/xdmf_mesh.h5 +0 -0
  46. {solidipes_solid_mech_plugin-0.0.15 → solidipes_solid_mech_plugin-0.0.16}/tests/assets/xdmf_mesh.xdmf +0 -0
  47. {solidipes_solid_mech_plugin-0.0.15 → solidipes_solid_mech_plugin-0.0.16}/tests/conftest.py +0 -0
  48. {solidipes_solid_mech_plugin-0.0.15 → solidipes_solid_mech_plugin-0.0.16}/tests/helpers/utils.py +0 -0
  49. {solidipes_solid_mech_plugin-0.0.15 → solidipes_solid_mech_plugin-0.0.16}/tests/test_load_modules.py +0 -0
  50. {solidipes_solid_mech_plugin-0.0.15 → solidipes_solid_mech_plugin-0.0.16}/tests/test_ontologies.py +0 -0
  51. {solidipes_solid_mech_plugin-0.0.15 → solidipes_solid_mech_plugin-0.0.16}/tests/test_pyvista_mesh.py +0 -0
  52. {solidipes_solid_mech_plugin-0.0.15 → solidipes_solid_mech_plugin-0.0.16}/tests/test_pyvista_plotter.py +0 -0
  53. {solidipes_solid_mech_plugin-0.0.15 → solidipes_solid_mech_plugin-0.0.16}/tests/test_xdmf_mesh.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: solidipes-solid-mech-plugin
3
- Version: 0.0.15
3
+ Version: 0.0.16
4
4
  Summary: Plugin for Solidipes with solid mechanics components
5
5
  Author-email: Son Pham-Ba <son.phamba@epfl.ch>, Guillaume Anciaux <guillaume.anciaux@epfl.ch>
6
6
  License: GPL-3.0-or-later
@@ -12,9 +12,12 @@ Requires-Dist: ipython~=8.18.1
12
12
  Requires-Dist: mergedeep~=1.3.4
13
13
  Requires-Dist: meshio[all]~=5.3.5
14
14
  Requires-Dist: streamlit~=1.45.0
15
- Requires-Dist: trame~=3.5.2
16
- Requires-Dist: streamlit-pyvista==0.0.21
15
+ Requires-Dist: trame<4,>=3.13.2
16
+ Requires-Dist: trame-pyvista>=0.2.1
17
+ Requires-Dist: trame-vtk>=2.11.15
18
+ Requires-Dist: streamlit-pyvista~=0.0.24
17
19
  Requires-Dist: h5py~=3.13.0
20
+ Requires-Dist: netCDF4~=1.7.0
18
21
  Provides-Extra: dev
19
22
  Requires-Dist: pillow~=10.4.0; extra == "dev"
20
23
  Requires-Dist: pixelmatch~=0.3.0; extra == "dev"
@@ -16,9 +16,12 @@ dependencies = [
16
16
  "mergedeep~=1.3.4",
17
17
  "meshio[all]~=5.3.5",
18
18
  "streamlit~=1.45.0",
19
- "trame~=3.5.2",
20
- "streamlit-pyvista==0.0.21",
19
+ "trame>=3.13.2,<4",
20
+ "trame-pyvista>=0.2.1",
21
+ "trame-vtk>=2.11.15",
22
+ "streamlit-pyvista~=0.0.24",
21
23
  "h5py~=3.13.0",
24
+ "netCDF4~=1.7.0",
22
25
  ]
23
26
 
24
27
  [project.optional-dependencies]
@@ -0,0 +1,184 @@
1
+ import re
2
+
3
+ import numpy as np
4
+ from solidipes.loaders.file import File
5
+ from solidipes.loaders.sequence import Sequence
6
+ from solidipes_core_plugin.loaders.netcdf import NetCDF
7
+
8
+
9
+ def _decode_exodus_string(raw) -> str:
10
+ """Decode a character array row from an Exodus II netCDF file."""
11
+ if isinstance(raw, np.ndarray):
12
+ raw = raw.tobytes()
13
+
14
+ if isinstance(raw, (bytes, np.bytes_)):
15
+ return raw.decode("utf-8", errors="ignore").strip("\x00").strip()
16
+
17
+ return str(raw)
18
+
19
+
20
+ class Exodus(Sequence, NetCDF):
21
+ """Exodus II finite element database file, read with meshio and netCDF4.
22
+
23
+ Every Exodus file is treated as a sequence of time steps (a mesh-only
24
+ database counts as a single frame). Nodal and elemental result variables
25
+ are exposed as point and cell data for each available time step, and the
26
+ mesh can be viewed with the PyvistaPlotter.
27
+
28
+ The `netcdf` loadable and the netCDF metadata (dimensions, variables,
29
+ global attributes) are inherited from the NetCDF loader.
30
+ """
31
+
32
+ from ..viewers.pyvista_plotter import PyvistaPlotter
33
+
34
+ supported_mime_types = {"application/x-netcdf": ["e", "exo", "gen"]}
35
+ _compatible_viewers = [PyvistaPlotter]
36
+
37
+ @File.loadable
38
+ def meshio_mesh(self):
39
+ """Static mesh (connectivity and coordinates) read with meshio"""
40
+ import meshio
41
+
42
+ return meshio.read(self.file_info.path)
43
+
44
+ @File.rocrate_metadata
45
+ def title(self):
46
+ return self.netcdf.title
47
+
48
+ @File.rocrate_metadata
49
+ def n_time_steps(self):
50
+ if "time_step" not in self.netcdf.dimensions:
51
+ return 1
52
+
53
+ # A mesh-only database has an empty (or missing) time_step dimension:
54
+ # the mesh itself counts as a single frame
55
+ return max(1, len(self.netcdf.dimensions["time_step"]))
56
+
57
+ @File.rocrate_metadata
58
+ def n_nodes(self):
59
+ return len(self.netcdf.dimensions["num_nodes"])
60
+
61
+ @File.rocrate_metadata
62
+ def n_elements(self):
63
+ return len(self.netcdf.dimensions["num_elem"])
64
+
65
+ @File.rocrate_metadata
66
+ def nodal_variable_names(self):
67
+ if "name_nod_var" not in self.netcdf.variables:
68
+ return []
69
+
70
+ return [_decode_exodus_string(raw) for raw in self.netcdf.variables["name_nod_var"][:]]
71
+
72
+ @File.rocrate_metadata
73
+ def element_block_names(self):
74
+ if "eb_names" not in self.netcdf.variables:
75
+ return {}
76
+
77
+ return {i + 1: _decode_exodus_string(raw) for i, raw in enumerate(self.netcdf.variables["eb_names"][:])}
78
+
79
+ @File.rocrate_metadata
80
+ def elemental_variable_names(self):
81
+ """List of (element_block_number, variable_number, name) triples"""
82
+ names = []
83
+
84
+ for key, var in self.netcdf.variables.items():
85
+ if not key.startswith("name_elem_var"):
86
+ continue
87
+
88
+ block = int(key[len("name_elem_var") :]) if len(key) > len("name_elem_var") else 1
89
+
90
+ for i, raw in enumerate(var[:]):
91
+ name = _decode_exodus_string(raw)
92
+
93
+ if name:
94
+ names.append((block, i + 1, name))
95
+
96
+ return names
97
+
98
+ @property
99
+ def _element_count(self):
100
+ return self.n_time_steps
101
+
102
+ def select_frame(self, frame):
103
+ self.select_element(frame)
104
+
105
+ @property
106
+ def mesh(self):
107
+ """Pyvista mesh of the currently selected time step"""
108
+ return self._current_element
109
+
110
+ def _load_element(self, n):
111
+ """Load the mesh with nodal and elemental data at time step n"""
112
+ import pyvista as pv
113
+ from meshio import Mesh
114
+
115
+ meshio_mesh = self.meshio_mesh
116
+ point_data = self._point_data_at_step(n)
117
+ cell_data = self._cell_data_at_step(n)
118
+ mesh = Mesh(meshio_mesh.points, meshio_mesh.cells, point_data=point_data, cell_data=cell_data)
119
+ return pv.from_meshio(mesh)
120
+
121
+ def _point_data_at_step(self, n):
122
+ """Nodal variable values at time step n"""
123
+ nc = self.netcdf
124
+ names = self.nodal_variable_names
125
+ data = {}
126
+
127
+ if "vals_nod_var" in nc.variables:
128
+ # Standard layout: single variable with dimensions
129
+ # (time_step, num_nod_var, num_nodes)
130
+ all_data = np.asarray(nc.variables["vals_nod_var"][n])
131
+
132
+ for i, name in enumerate(names):
133
+ data[name] = all_data[i]
134
+
135
+ else:
136
+ # Alternative layout: one variable per nodal variable
137
+ for i, name in enumerate(names):
138
+ var_name = f"vals_nod_var{i + 1}"
139
+
140
+ if var_name in nc.variables:
141
+ data[name] = np.asarray(nc.variables[var_name][n])
142
+
143
+ return data
144
+
145
+ def _cell_data_at_step(self, n):
146
+ """Elemental variable values at time step n, per meshio cell block"""
147
+ nc = self.netcdf
148
+ names = self.elemental_variable_names
149
+
150
+ if len(names) == 0:
151
+ return {}
152
+
153
+ # Collect data per (variable index, element block)
154
+ raw: dict[int, dict[int, np.ndarray]] = {}
155
+
156
+ for key, var in nc.variables.items():
157
+ match = re.fullmatch(r"vals_elem_var(\d+)?(?:eb(\d+))?", key)
158
+
159
+ if match is None:
160
+ continue
161
+
162
+ var_index = 0 if match.group(1) is None else int(match.group(1)) - 1
163
+ block = 0 if match.group(2) is None else int(match.group(2)) - 1
164
+ raw.setdefault(var_index, {})[block] = np.asarray(var[n])
165
+
166
+ cell_sizes = [len(c.data) for c in self.meshio_mesh.cells]
167
+ offsets = np.cumsum([0, *cell_sizes])
168
+
169
+ cell_data = {}
170
+
171
+ for block, var_index, name in names:
172
+ if var_index - 1 not in raw:
173
+ continue
174
+
175
+ by_block = raw[var_index - 1]
176
+ concatenated = np.concatenate([by_block[b] for b in sorted(by_block)])
177
+
178
+ if len(concatenated) != offsets[-1]:
179
+ # Element blocks without data for this variable: skip
180
+ continue
181
+
182
+ cell_data[name] = [concatenated[start:stop] for start, stop in zip(offsets[:-1], offsets[1:])]
183
+
184
+ return cell_data
@@ -95,7 +95,7 @@ class PyvistaPlotter(Viewer):
95
95
  method
96
96
  """
97
97
  from streamlit_pyvista.mesh_viewer_component import MeshViewerComponent # noqa: E402
98
- from streamlit_pyvista.server_managers import ServerManagerProxified # noqa: E402
98
+ from streamlit_pyvista.server_managers import ServerManager # noqa: E402
99
99
  from streamlit_pyvista.trame_viewers import get_advanced_viewer_path # noqa: E402
100
100
 
101
101
  max_frames = 500
@@ -133,7 +133,7 @@ class PyvistaPlotter(Viewer):
133
133
  MeshViewerComponent(
134
134
  paths,
135
135
  trame_viewer_class=get_advanced_viewer_path(),
136
- server_manager_class=ServerManagerProxified,
136
+ server_manager_class=ServerManager,
137
137
  ).show()
138
138
  elif viewer_backends.current_backend == "python":
139
139
  self.shown = True
@@ -143,7 +143,7 @@ class PyvistaPlotter(Viewer):
143
143
  MeshViewerComponent(
144
144
  paths,
145
145
  trame_viewer_class=get_advanced_viewer_path(),
146
- server_manager_class=ServerManagerProxified,
146
+ server_manager_class=ServerManager,
147
147
  ).show()
148
148
 
149
149
  def save(self, path, **kwargs):
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: solidipes-solid-mech-plugin
3
- Version: 0.0.15
3
+ Version: 0.0.16
4
4
  Summary: Plugin for Solidipes with solid mechanics components
5
5
  Author-email: Son Pham-Ba <son.phamba@epfl.ch>, Guillaume Anciaux <guillaume.anciaux@epfl.ch>
6
6
  License: GPL-3.0-or-later
@@ -12,9 +12,12 @@ Requires-Dist: ipython~=8.18.1
12
12
  Requires-Dist: mergedeep~=1.3.4
13
13
  Requires-Dist: meshio[all]~=5.3.5
14
14
  Requires-Dist: streamlit~=1.45.0
15
- Requires-Dist: trame~=3.5.2
16
- Requires-Dist: streamlit-pyvista==0.0.21
15
+ Requires-Dist: trame<4,>=3.13.2
16
+ Requires-Dist: trame-pyvista>=0.2.1
17
+ Requires-Dist: trame-vtk>=2.11.15
18
+ Requires-Dist: streamlit-pyvista~=0.0.24
17
19
  Requires-Dist: h5py~=3.13.0
20
+ Requires-Dist: netCDF4~=1.7.0
18
21
  Provides-Extra: dev
19
22
  Requires-Dist: pillow~=10.4.0; extra == "dev"
20
23
  Requires-Dist: pixelmatch~=0.3.0; extra == "dev"
@@ -20,6 +20,7 @@ solidipes_solid_mech_plugin.egg-info/top_level.txt
20
20
  solidipes_solid_mech_plugin/loaders/__init__.py
21
21
  solidipes_solid_mech_plugin/loaders/abaqus.py
22
22
  solidipes_solid_mech_plugin/loaders/abaqus_python_code.py
23
+ solidipes_solid_mech_plugin/loaders/exodus.py
23
24
  solidipes_solid_mech_plugin/loaders/geo_msh.py
24
25
  solidipes_solid_mech_plugin/loaders/geof_mesh.py
25
26
  solidipes_solid_mech_plugin/loaders/meshio.py
@@ -31,6 +32,7 @@ solidipes_solid_mech_plugin/viewers/__init__.py
31
32
  solidipes_solid_mech_plugin/viewers/pyvista_plotter.py
32
33
  solidipes_solid_mech_plugin/viewers/xdmf.py
33
34
  tests/conftest.py
35
+ tests/test_exodus_mesh.py
34
36
  tests/test_load_modules.py
35
37
  tests/test_ontologies.py
36
38
  tests/test_pyvista_mesh.py
@@ -3,9 +3,12 @@ ipython~=8.18.1
3
3
  mergedeep~=1.3.4
4
4
  meshio[all]~=5.3.5
5
5
  streamlit~=1.45.0
6
- trame~=3.5.2
7
- streamlit-pyvista==0.0.21
6
+ trame<4,>=3.13.2
7
+ trame-pyvista>=0.2.1
8
+ trame-vtk>=2.11.15
9
+ streamlit-pyvista~=0.0.24
8
10
  h5py~=3.13.0
11
+ netCDF4~=1.7.0
9
12
 
10
13
  [dev]
11
14
  pillow~=10.4.0
@@ -13,6 +13,7 @@
13
13
  "solidipes_solid_mech_plugin/loaders/__init__.py",
14
14
  "solidipes_solid_mech_plugin/loaders/abaqus.py",
15
15
  "solidipes_solid_mech_plugin/loaders/abaqus_python_code.py",
16
+ "solidipes_solid_mech_plugin/loaders/exodus.py",
16
17
  "solidipes_solid_mech_plugin/loaders/geo_msh.py",
17
18
  "solidipes_solid_mech_plugin/loaders/geof_mesh.py",
18
19
  "solidipes_solid_mech_plugin/loaders/meshio.py",
@@ -32,6 +33,7 @@
32
33
  "tests/assets/xdmf_mesh.xdmf",
33
34
  "tests/conftest.py",
34
35
  "tests/helpers/utils.py",
36
+ "tests/test_exodus_mesh.py",
35
37
  "tests/test_load_modules.py",
36
38
  "tests/test_ontologies.py",
37
39
  "tests/test_pyvista_mesh.py",
@@ -0,0 +1,8 @@
1
+ {
2
+ "tag": "0.0.16",
3
+ "distance": 0,
4
+ "node": "gcfd62de0a703eb7a2bab88cd4f3655c523025f43",
5
+ "dirty": false,
6
+ "branch": "HEAD",
7
+ "node_date": "2026-10-21"
8
+ }
@@ -0,0 +1,88 @@
1
+ import os
2
+
3
+ import pytest
4
+ from solidipes.loaders import load_file
5
+
6
+ meshio = pytest.importorskip("meshio")
7
+ netCDF4 = pytest.importorskip("netCDF4")
8
+
9
+
10
+ @pytest.fixture
11
+ def exodus_file_path(tmp_path, study_dir):
12
+ """Create a small Exodus II file with a nodal variable and return its path."""
13
+ import numpy as np
14
+
15
+ points = [[0.0, 0.0, 0.0], [1.0, 0.0, 0.0], [1.0, 1.0, 0.0], [0.0, 1.0, 0.0]]
16
+ cells = [("quad", np.array([[0, 1, 2, 3]]))]
17
+ mesh = meshio.Mesh(points, cells, point_data={"v": np.array([0.1, 0.2, 0.3, 0.4])})
18
+
19
+ path = os.path.join(tmp_path, "test.e")
20
+ meshio.write(path, mesh, file_format="exodus")
21
+ return path
22
+
23
+
24
+ @pytest.fixture
25
+ def exodus_file(exodus_file_path):
26
+ file = load_file(exodus_file_path)
27
+ return file
28
+
29
+
30
+ def test_file_type(exodus_file):
31
+ """The Exodus loader is selected for .e files"""
32
+ from solidipes.loaders.file import File
33
+
34
+ assert isinstance(exodus_file, File)
35
+ assert type(exodus_file).__name__ == "Exodus"
36
+
37
+
38
+ def test_metadata(exodus_file):
39
+ assert exodus_file.n_nodes == 4
40
+ assert exodus_file.n_elements == 1
41
+ assert exodus_file.n_time_steps == 1
42
+ assert exodus_file.nodal_variable_names == ["v"]
43
+
44
+
45
+ def test_mesh(exodus_file):
46
+ import numpy as np
47
+ import pyvista as pv
48
+
49
+ mesh = exodus_file.mesh
50
+ assert isinstance(mesh, pv.UnstructuredGrid)
51
+ assert mesh.n_points == 4
52
+ assert mesh.n_cells == 1
53
+ assert "v" in mesh.array_names
54
+ assert np.allclose(mesh.point_data["v"], [0.1, 0.2, 0.3, 0.4])
55
+
56
+
57
+ def test_time_steps(exodus_file_path):
58
+ """Add a second time step and check that it can be selected"""
59
+ import numpy as np
60
+
61
+ with netCDF4.Dataset(exodus_file_path, "a") as nc:
62
+ var = nc.variables["vals_nod_var1"]
63
+ var[1] = np.array([0.5, 0.6, 0.7, 0.8])
64
+ nc.variables["time_whole"][1] = 1.0
65
+
66
+ exodus_file = load_file(exodus_file_path)
67
+ assert exodus_file.n_time_steps == 2
68
+
69
+ mesh_0 = exodus_file.mesh
70
+ assert np.allclose(mesh_0.point_data["v"], [0.1, 0.2, 0.3, 0.4])
71
+
72
+ exodus_file.select_frame(1)
73
+ mesh_1 = exodus_file.mesh
74
+ assert np.allclose(mesh_1.point_data["v"], [0.5, 0.6, 0.7, 0.8])
75
+
76
+
77
+ def test_validation(exodus_file):
78
+ assert exodus_file.is_valid
79
+
80
+
81
+ def test_inherited_netcdf(exodus_file):
82
+ """The netCDF loadable and metadata are inherited from the NetCDF loader"""
83
+ import netCDF4
84
+
85
+ assert isinstance(exodus_file.netcdf, netCDF4.Dataset)
86
+ assert exodus_file.dimensions["num_nodes"] == 4
87
+ assert exodus_file.global_attributes["title"].startswith("Created by meshio")
88
+ assert "coord" in exodus_file.variables
@@ -1,7 +1,9 @@
1
+ import glob
1
2
  import os
2
3
  import shutil
3
4
  import signal
4
5
  import subprocess
6
+ import tempfile
5
7
  import time
6
8
 
7
9
  import pytest
@@ -14,6 +16,59 @@ max_loading_checks = 20
14
16
  delay_page_load = 1
15
17
  test_download_dir = os.path.join(os.getcwd(), "_downloads")
16
18
 
19
+ # Command-line substrings identifying processes of the solidipes/streamlit-pyvista
20
+ # stack that must not survive between runs
21
+ leftover_process_patterns = [
22
+ "server_manager_proxified",
23
+ "server_manager.py",
24
+ "streamlit-pyvista-cache/viewer_",
25
+ "streamlit-pyvista-proxy",
26
+ "streamlit run",
27
+ "solidipes report web-report",
28
+ ]
29
+
30
+
31
+ def cleanup_leftover_processes():
32
+ """Kill leftover processes from previous runs.
33
+
34
+ The viewer component reuses any process already listening on its ports
35
+ (proxy: 5000, server manager: 9422), so a stale server left behind by a
36
+ crashed or timed-out earlier run breaks the current one (blank viewer,
37
+ websocket never connecting). Clean them up before starting.
38
+ """
39
+ victims = []
40
+ for proc_dir in glob.glob("/proc/[0-9]*"):
41
+ try:
42
+ with open(f"{proc_dir}/cmdline", "rb") as f:
43
+ cmdline = f.read().replace(b"\x00", b" ").decode(errors="ignore")
44
+ except OSError:
45
+ continue
46
+ pid = int(proc_dir.rsplit("/", 1)[-1])
47
+ if pid == os.getpid():
48
+ continue
49
+ if any(pattern in cmdline for pattern in leftover_process_patterns):
50
+ victims.append(pid)
51
+ for pid in victims:
52
+ try:
53
+ os.kill(pid, signal.SIGTERM)
54
+ except OSError:
55
+ pass
56
+ if not victims:
57
+ return
58
+ time.sleep(2)
59
+ for pid in victims:
60
+ try:
61
+ os.kill(pid, signal.SIGKILL)
62
+ except OSError:
63
+ pass
64
+ time.sleep(1)
65
+
66
+
67
+ @pytest.fixture()
68
+ def clean_environment():
69
+ cleanup_leftover_processes()
70
+ yield
71
+
17
72
 
18
73
  @pytest.fixture()
19
74
  def sb(request):
@@ -59,6 +114,12 @@ class WebReportLauncher:
59
114
  self.streamlit_subprocess = None
60
115
  self.out = None
61
116
  self.err = None
117
+ # Output goes to files, not pipes: every child (streamlit app, server
118
+ # manager, proxy, trame viewers) logs to the inherited descriptors, and
119
+ # an unread PIPE fills its 64KB buffer after a minute or two, blocking
120
+ # any process that logs and freezing the proxy mid-request
121
+ self.out_file = tempfile.TemporaryFile("w+")
122
+ self.err_file = tempfile.TemporaryFile("w+")
62
123
 
63
124
  def launch(self, dir_path):
64
125
  self.streamlit_subprocess = subprocess.Popen(
@@ -72,8 +133,8 @@ class WebReportLauncher:
72
133
  "--server.headless",
73
134
  "true",
74
135
  ],
75
- stdout=subprocess.PIPE,
76
- stderr=subprocess.PIPE,
136
+ stdout=self.out_file,
137
+ stderr=self.err_file,
77
138
  preexec_fn=os.setsid,
78
139
  )
79
140
  return self
@@ -83,10 +144,25 @@ class WebReportLauncher:
83
144
  return
84
145
  os.killpg(os.getpgid(self.streamlit_subprocess.pid), signal.SIGTERM)
85
146
  try:
86
- self.out, self.err = self.streamlit_subprocess.communicate(timeout=5)
147
+ self.streamlit_subprocess.wait(timeout=5)
87
148
  except subprocess.TimeoutExpired:
88
- return None, ""
149
+ # Force-kill the whole group so no orphan (server manager, proxy,
150
+ # trame viewers) survives this run and breaks the next one
151
+ os.killpg(os.getpgid(self.streamlit_subprocess.pid), signal.SIGKILL)
152
+ try:
153
+ self.streamlit_subprocess.wait(timeout=5)
154
+ except subprocess.TimeoutExpired:
155
+ pass
89
156
  self.streamlit_subprocess = None
157
+ self.out = self._read_file(self.out_file)
158
+ self.err = self._read_file(self.err_file)
159
+
160
+ @staticmethod
161
+ def _read_file(f):
162
+ f.seek(0)
163
+ content = f.read()
164
+ f.close()
165
+ return content
90
166
 
91
167
  def get_outputs(self):
92
168
  return self.out, self.err
@@ -138,7 +214,27 @@ def gracefully_stop_report(web_report):
138
214
  web_report.terminate()
139
215
  _, streamlit_errors = web_report.get_outputs()
140
216
  streamlit_errors = str(streamlit_errors).replace("\\n", "\n")
141
- error_found = "Traceback" in streamlit_errors or "already in use" in streamlit_errors
217
+ # Terminating a healthy run always leaves benign teardown tracebacks in the
218
+ # subprocess tree (multiprocessing managers exiting via sys.exit, the server
219
+ # manager catching SIGTERM); only other tracebacks are real faults
220
+ benign_markers = [
221
+ "sys.exit(0) or quit was called",
222
+ "Signals.SIGTERM: 15",
223
+ # streamlit CLI exits normally through click when terminated
224
+ "click.exceptions.Exit",
225
+ # streamlit-pyvista bug: e.stderr is None when the server manager is
226
+ # SIGTERM'd mid-teardown, causing a TypeError at re-check
227
+ "argument of type 'NoneType' is not iterable",
228
+ ]
229
+ # Scan traceback blocks individually: the string "Address already in use"
230
+ # also appears quoted inside a known-benign traceback's source line, so a
231
+ # naive whole-output search would false-positive
232
+ error_found = False
233
+ blocks = streamlit_errors.split("Traceback (most recent call last):")
234
+ for block in blocks[1:]:
235
+ if any(marker in block for marker in benign_markers):
236
+ continue
237
+ error_found = True
142
238
  if error_found:
143
239
  raise RuntimeError("Error encountered in Streamlit: \n{}".format(streamlit_errors))
144
240
 
@@ -150,7 +246,10 @@ def click_solidipes_mesh_button(sb):
150
246
 
151
247
 
152
248
  def click_wireframe_checkbox(sb):
153
- sb.click("label[for='wireframe_checkbox']")
249
+ # The trame viewer boots (VTK import + server start) after the iframe is
250
+ # rendered, which can take tens of seconds on slow machines: wait for the
251
+ # checkbox instead of relying on the 7s default timeout.
252
+ sb.click("label[for='wireframe_checkbox']", timeout=60)
154
253
  print("Clicked on the wireframe checkbox")
155
254
 
156
255
 
@@ -160,11 +259,14 @@ def download_mesh(sb):
160
259
  sb.wait(10)
161
260
 
162
261
 
163
- def test_streamlit_pyvista_in_web_report(sb, tmp_path, web_report):
262
+ def test_streamlit_pyvista_in_web_report(sb, tmp_path, web_report, clean_environment):
164
263
  dir_path = init_study_dir(tmp_path, git=True)
165
264
  os.makedirs(f"{dir_path}/data", exist_ok=True)
166
265
  shutil.copy2(utils.get_asset_path("pyvista_mesh.vtu"), f"{dir_path}/data/")
167
266
  start_web_report_without_fault(sb, dir_path, web_report, target="?page=display_page&file=./data/pyvista_mesh.vtu")
267
+ # The viewer iframe is rendered after the streamlit script run completes,
268
+ # which can be slow; wait for it instead of checking once.
269
+ sb.wait_for_element_present('iframe[data-testid="stIFrame"]', timeout=120)
168
270
  iframes = sb.find_elements('iframe[data-testid="stIFrame"]')
169
271
  assert len(iframes) > 0
170
272
  sb.switch_to_frame(iframes[0])