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.
- {solidipes_solid_mech_plugin-0.0.15 → solidipes_solid_mech_plugin-0.0.16}/PKG-INFO +6 -3
- {solidipes_solid_mech_plugin-0.0.15 → solidipes_solid_mech_plugin-0.0.16}/pyproject.toml +5 -2
- solidipes_solid_mech_plugin-0.0.16/solidipes_solid_mech_plugin/loaders/exodus.py +184 -0
- {solidipes_solid_mech_plugin-0.0.15 → solidipes_solid_mech_plugin-0.0.16}/solidipes_solid_mech_plugin/viewers/pyvista_plotter.py +3 -3
- {solidipes_solid_mech_plugin-0.0.15 → solidipes_solid_mech_plugin-0.0.16}/solidipes_solid_mech_plugin.egg-info/PKG-INFO +6 -3
- {solidipes_solid_mech_plugin-0.0.15 → solidipes_solid_mech_plugin-0.0.16}/solidipes_solid_mech_plugin.egg-info/SOURCES.txt +2 -0
- {solidipes_solid_mech_plugin-0.0.15 → solidipes_solid_mech_plugin-0.0.16}/solidipes_solid_mech_plugin.egg-info/requires.txt +5 -2
- {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
- solidipes_solid_mech_plugin-0.0.16/solidipes_solid_mech_plugin.egg-info/scm_version.json +8 -0
- solidipes_solid_mech_plugin-0.0.16/tests/test_exodus_mesh.py +88 -0
- {solidipes_solid_mech_plugin-0.0.15 → solidipes_solid_mech_plugin-0.0.16}/tests/test_streamlit_pyvista.py +109 -7
- solidipes_solid_mech_plugin-0.0.16/uv.lock +4464 -0
- solidipes_solid_mech_plugin-0.0.15/solidipes_solid_mech_plugin.egg-info/scm_version.json +0 -8
- solidipes_solid_mech_plugin-0.0.15/uv.lock +0 -3519
- {solidipes_solid_mech_plugin-0.0.15 → solidipes_solid_mech_plugin-0.0.16}/.flake8 +0 -0
- {solidipes_solid_mech_plugin-0.0.15 → solidipes_solid_mech_plugin-0.0.16}/.gitignore +0 -0
- {solidipes_solid_mech_plugin-0.0.15 → solidipes_solid_mech_plugin-0.0.16}/.gitlab-ci.yml +0 -0
- {solidipes_solid_mech_plugin-0.0.15 → solidipes_solid_mech_plugin-0.0.16}/.pre-commit-config.yaml +0 -0
- {solidipes_solid_mech_plugin-0.0.15 → solidipes_solid_mech_plugin-0.0.16}/CHANGELOG.md +0 -0
- {solidipes_solid_mech_plugin-0.0.15 → solidipes_solid_mech_plugin-0.0.16}/LICENSE +0 -0
- {solidipes_solid_mech_plugin-0.0.15 → solidipes_solid_mech_plugin-0.0.16}/README.md +0 -0
- {solidipes_solid_mech_plugin-0.0.15 → solidipes_solid_mech_plugin-0.0.16}/setup.cfg +0 -0
- {solidipes_solid_mech_plugin-0.0.15 → solidipes_solid_mech_plugin-0.0.16}/solidipes_solid_mech_plugin/__init__.py +0 -0
- {solidipes_solid_mech_plugin-0.0.15 → solidipes_solid_mech_plugin-0.0.16}/solidipes_solid_mech_plugin/config.py +0 -0
- {solidipes_solid_mech_plugin-0.0.15 → solidipes_solid_mech_plugin-0.0.16}/solidipes_solid_mech_plugin/loaders/__init__.py +0 -0
- {solidipes_solid_mech_plugin-0.0.15 → solidipes_solid_mech_plugin-0.0.16}/solidipes_solid_mech_plugin/loaders/abaqus.py +0 -0
- {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
- {solidipes_solid_mech_plugin-0.0.15 → solidipes_solid_mech_plugin-0.0.16}/solidipes_solid_mech_plugin/loaders/geo_msh.py +0 -0
- {solidipes_solid_mech_plugin-0.0.15 → solidipes_solid_mech_plugin-0.0.16}/solidipes_solid_mech_plugin/loaders/geof_mesh.py +0 -0
- {solidipes_solid_mech_plugin-0.0.15 → solidipes_solid_mech_plugin-0.0.16}/solidipes_solid_mech_plugin/loaders/meshio.py +0 -0
- {solidipes_solid_mech_plugin-0.0.15 → solidipes_solid_mech_plugin-0.0.16}/solidipes_solid_mech_plugin/loaders/parse_inp.py +0 -0
- {solidipes_solid_mech_plugin-0.0.15 → solidipes_solid_mech_plugin-0.0.16}/solidipes_solid_mech_plugin/loaders/pyvista_mesh.py +0 -0
- {solidipes_solid_mech_plugin-0.0.15 → solidipes_solid_mech_plugin-0.0.16}/solidipes_solid_mech_plugin/loaders/xdmf.py +0 -0
- {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
- {solidipes_solid_mech_plugin-0.0.15 → solidipes_solid_mech_plugin-0.0.16}/solidipes_solid_mech_plugin/viewers/__init__.py +0 -0
- {solidipes_solid_mech_plugin-0.0.15 → solidipes_solid_mech_plugin-0.0.16}/solidipes_solid_mech_plugin/viewers/xdmf.py +0 -0
- {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
- {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
- {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
- {solidipes_solid_mech_plugin-0.0.15 → solidipes_solid_mech_plugin-0.0.16}/tests/assets/generate_assets.py +0 -0
- {solidipes_solid_mech_plugin-0.0.15 → solidipes_solid_mech_plugin-0.0.16}/tests/assets/pyvista_mesh.vtu +0 -0
- {solidipes_solid_mech_plugin-0.0.15 → solidipes_solid_mech_plugin-0.0.16}/tests/assets/pyvista_mesh_no_fields.vtu +0 -0
- {solidipes_solid_mech_plugin-0.0.15 → solidipes_solid_mech_plugin-0.0.16}/tests/assets/pyvista_plot.png +0 -0
- {solidipes_solid_mech_plugin-0.0.15 → solidipes_solid_mech_plugin-0.0.16}/tests/assets/test_mesh_loaded.png +0 -0
- {solidipes_solid_mech_plugin-0.0.15 → solidipes_solid_mech_plugin-0.0.16}/tests/assets/xdmf_mesh.h5 +0 -0
- {solidipes_solid_mech_plugin-0.0.15 → solidipes_solid_mech_plugin-0.0.16}/tests/assets/xdmf_mesh.xdmf +0 -0
- {solidipes_solid_mech_plugin-0.0.15 → solidipes_solid_mech_plugin-0.0.16}/tests/conftest.py +0 -0
- {solidipes_solid_mech_plugin-0.0.15 → solidipes_solid_mech_plugin-0.0.16}/tests/helpers/utils.py +0 -0
- {solidipes_solid_mech_plugin-0.0.15 → solidipes_solid_mech_plugin-0.0.16}/tests/test_load_modules.py +0 -0
- {solidipes_solid_mech_plugin-0.0.15 → solidipes_solid_mech_plugin-0.0.16}/tests/test_ontologies.py +0 -0
- {solidipes_solid_mech_plugin-0.0.15 → solidipes_solid_mech_plugin-0.0.16}/tests/test_pyvista_mesh.py +0 -0
- {solidipes_solid_mech_plugin-0.0.15 → solidipes_solid_mech_plugin-0.0.16}/tests/test_pyvista_plotter.py +0 -0
- {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.
|
|
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
|
|
16
|
-
Requires-Dist:
|
|
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
|
|
20
|
-
"
|
|
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
|
|
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=
|
|
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=
|
|
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.
|
|
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
|
|
16
|
-
Requires-Dist:
|
|
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
|
|
7
|
-
|
|
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,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=
|
|
76
|
-
stderr=
|
|
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.
|
|
147
|
+
self.streamlit_subprocess.wait(timeout=5)
|
|
87
148
|
except subprocess.TimeoutExpired:
|
|
88
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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])
|