ansys-pyensight-core 0.11.0__py3-none-any.whl
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.
- ansys/pyensight/core/__init__.py +41 -0
- ansys/pyensight/core/common.py +341 -0
- ansys/pyensight/core/deep_pixel_view.html +98 -0
- ansys/pyensight/core/dockerlauncher.py +1124 -0
- ansys/pyensight/core/dvs.py +872 -0
- ansys/pyensight/core/enscontext.py +345 -0
- ansys/pyensight/core/enshell_grpc.py +641 -0
- ansys/pyensight/core/ensight_grpc.py +874 -0
- ansys/pyensight/core/ensobj.py +515 -0
- ansys/pyensight/core/launch_ensight.py +296 -0
- ansys/pyensight/core/launcher.py +388 -0
- ansys/pyensight/core/libuserd.py +2110 -0
- ansys/pyensight/core/listobj.py +280 -0
- ansys/pyensight/core/locallauncher.py +579 -0
- ansys/pyensight/core/py.typed +0 -0
- ansys/pyensight/core/renderable.py +880 -0
- ansys/pyensight/core/session.py +1923 -0
- ansys/pyensight/core/sgeo_poll.html +24 -0
- ansys/pyensight/core/utils/__init__.py +21 -0
- ansys/pyensight/core/utils/adr.py +111 -0
- ansys/pyensight/core/utils/dsg_server.py +1220 -0
- ansys/pyensight/core/utils/export.py +606 -0
- ansys/pyensight/core/utils/omniverse.py +769 -0
- ansys/pyensight/core/utils/omniverse_cli.py +614 -0
- ansys/pyensight/core/utils/omniverse_dsg_server.py +1196 -0
- ansys/pyensight/core/utils/omniverse_glb_server.py +848 -0
- ansys/pyensight/core/utils/parts.py +1221 -0
- ansys/pyensight/core/utils/query.py +487 -0
- ansys/pyensight/core/utils/readers.py +300 -0
- ansys/pyensight/core/utils/resources/Materials/000_sky.exr +0 -0
- ansys/pyensight/core/utils/support.py +128 -0
- ansys/pyensight/core/utils/variables.py +2019 -0
- ansys/pyensight/core/utils/views.py +674 -0
- ansys_pyensight_core-0.11.0.dist-info/METADATA +309 -0
- ansys_pyensight_core-0.11.0.dist-info/RECORD +37 -0
- ansys_pyensight_core-0.11.0.dist-info/WHEEL +4 -0
- ansys_pyensight_core-0.11.0.dist-info/licenses/LICENSE +21 -0
|
@@ -0,0 +1,2110 @@
|
|
|
1
|
+
# Copyright (C) 2022 - 2026 ANSYS, Inc. and/or its affiliates.
|
|
2
|
+
# SPDX-License-Identifier: MIT
|
|
3
|
+
#
|
|
4
|
+
#
|
|
5
|
+
# Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
# of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
# in the Software without restriction, including without limitation the rights
|
|
8
|
+
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
# copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
# furnished to do so, subject to the following conditions:
|
|
11
|
+
#
|
|
12
|
+
# The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
# copies or substantial portions of the Software.
|
|
14
|
+
#
|
|
15
|
+
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
# SOFTWARE.
|
|
22
|
+
|
|
23
|
+
"""
|
|
24
|
+
The ``libuserd`` module allows PyEnSight to directly access EnSight
|
|
25
|
+
user-defined readers (USERD). Any file format for which EnSight
|
|
26
|
+
uses a USERD interface can be read using this API
|
|
27
|
+
|
|
28
|
+
Examples
|
|
29
|
+
--------
|
|
30
|
+
|
|
31
|
+
>>> from ansys.pyensight.core import libuserd
|
|
32
|
+
>>> userd = libuserd.LibUserd()
|
|
33
|
+
>>> userd.initialize()
|
|
34
|
+
>>> print(userd.library_version())
|
|
35
|
+
>>> datafile = "/example/data/CFX/Axial_001.res"
|
|
36
|
+
>>> readers = userd.query_format(datafile)
|
|
37
|
+
>>> data = readers[0].read_dataset(datafile)
|
|
38
|
+
>>> print(data.parts())
|
|
39
|
+
>>> print(data.variables())
|
|
40
|
+
>>> userd.shutdown()
|
|
41
|
+
|
|
42
|
+
"""
|
|
43
|
+
import enum
|
|
44
|
+
import logging
|
|
45
|
+
import os
|
|
46
|
+
import platform
|
|
47
|
+
import shutil
|
|
48
|
+
import subprocess
|
|
49
|
+
import tempfile
|
|
50
|
+
import time
|
|
51
|
+
from typing import TYPE_CHECKING, Any, Dict, List, Optional, Tuple, Union
|
|
52
|
+
import uuid
|
|
53
|
+
import warnings
|
|
54
|
+
|
|
55
|
+
from ansys.api.pyensight.v0 import libuserd_pb2, libuserd_pb2_grpc
|
|
56
|
+
from ansys.pyensight.core.common import (
|
|
57
|
+
find_unused_ports,
|
|
58
|
+
get_file_service,
|
|
59
|
+
launch_enshell_interface,
|
|
60
|
+
populate_service_host_port,
|
|
61
|
+
pull_image,
|
|
62
|
+
)
|
|
63
|
+
import grpc
|
|
64
|
+
import numpy
|
|
65
|
+
import psutil
|
|
66
|
+
import requests
|
|
67
|
+
|
|
68
|
+
try:
|
|
69
|
+
import docker
|
|
70
|
+
except ModuleNotFoundError: # pragma: no cover
|
|
71
|
+
raise RuntimeError("The docker module must be installed for DockerLauncher")
|
|
72
|
+
except Exception: # pragma: no cover
|
|
73
|
+
raise RuntimeError("Cannot initialize Docker")
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
if TYPE_CHECKING:
|
|
77
|
+
from docker import DockerClient
|
|
78
|
+
from docker.models.containers import Container
|
|
79
|
+
from enshell_grpc import EnShellGRPC
|
|
80
|
+
|
|
81
|
+
# This code is currently in development/beta state
|
|
82
|
+
warnings.warn(
|
|
83
|
+
"The libuserd interface/API is still under active development and should be considered beta.",
|
|
84
|
+
stacklevel=2,
|
|
85
|
+
)
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
def _build_enum(name: str, pb_enum: Any, flag: bool = False) -> Union[enum.IntEnum, enum.IntFlag]:
|
|
89
|
+
values = {}
|
|
90
|
+
for v in pb_enum:
|
|
91
|
+
values[v[0]] = v[1]
|
|
92
|
+
if flag:
|
|
93
|
+
return enum.IntFlag(name, values)
|
|
94
|
+
return enum.IntEnum(name, values)
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
ErrorCodes = _build_enum("ErrorCodes", libuserd_pb2.ErrorCodes.items())
|
|
98
|
+
ElementType = _build_enum("ElementType", libuserd_pb2.ElementType.items())
|
|
99
|
+
VariableLocation = _build_enum("VariableLocation", libuserd_pb2.VariableLocation.items())
|
|
100
|
+
VariableType = _build_enum("VariableType", libuserd_pb2.VariableType.items())
|
|
101
|
+
PartHints = _build_enum("PartHints", libuserd_pb2.PartHints.items(), flag=True)
|
|
102
|
+
UpdateHints = _build_enum("UpdateHints", libuserd_pb2.UpdateHints.items(), flag=True)
|
|
103
|
+
RankValues = _build_enum("RankValues", libuserd_pb2.RankValues.items())
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
class LibUserdError(Exception):
|
|
107
|
+
"""
|
|
108
|
+
This class is an exception object raised from the libuserd
|
|
109
|
+
library itself (not the gRPC remote interface). The associated
|
|
110
|
+
numeric LibUserd.ErrorCode is available via the 'code' attribute.
|
|
111
|
+
|
|
112
|
+
Parameters
|
|
113
|
+
----------
|
|
114
|
+
msg : str
|
|
115
|
+
The message text to be included in the exception.
|
|
116
|
+
|
|
117
|
+
Attributes
|
|
118
|
+
----------
|
|
119
|
+
code : int
|
|
120
|
+
The LibUserd ErrorCodes enum value for this error.
|
|
121
|
+
"""
|
|
122
|
+
|
|
123
|
+
def __init__(self, msg) -> None:
|
|
124
|
+
super(LibUserdError, self).__init__(msg)
|
|
125
|
+
self._code = libuserd_pb2.ErrorCodes.UNKNOWN
|
|
126
|
+
if msg.startswith("LibUserd("):
|
|
127
|
+
try:
|
|
128
|
+
self._code = int(msg[len("LibUserd(") :].split(")")[0])
|
|
129
|
+
except Exception:
|
|
130
|
+
pass
|
|
131
|
+
|
|
132
|
+
@property
|
|
133
|
+
def code(self) -> int:
|
|
134
|
+
"""The numeric error code: LibUserd.ErrorCodes"""
|
|
135
|
+
return self._code
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
class Query(object):
|
|
139
|
+
"""
|
|
140
|
+
The class represents a reader "query" instance. It includes
|
|
141
|
+
the query name as well as the preferred titles. The ``data``
|
|
142
|
+
method may be used to access the X,Y plot values.
|
|
143
|
+
|
|
144
|
+
Parameters
|
|
145
|
+
----------
|
|
146
|
+
userd
|
|
147
|
+
The LibUserd instance this query is associated with.
|
|
148
|
+
pb
|
|
149
|
+
The protobuffer that represents this object.
|
|
150
|
+
|
|
151
|
+
Attributes
|
|
152
|
+
----------
|
|
153
|
+
id : int
|
|
154
|
+
The id of this query.
|
|
155
|
+
name : str
|
|
156
|
+
The name of this query.
|
|
157
|
+
x_title : str
|
|
158
|
+
String to use as the x-axis title.
|
|
159
|
+
y_title : str
|
|
160
|
+
String to use as the y-axis title.
|
|
161
|
+
metadata : Dict[str, str]
|
|
162
|
+
The metadata for this query.
|
|
163
|
+
"""
|
|
164
|
+
|
|
165
|
+
def __init__(self, userd: "LibUserd", pb: libuserd_pb2.QueryInfo) -> None:
|
|
166
|
+
self._userd = userd
|
|
167
|
+
self.id = pb.id
|
|
168
|
+
self.name = pb.name
|
|
169
|
+
self.x_title = pb.x_title
|
|
170
|
+
self.y_title = pb.y_title
|
|
171
|
+
self.metadata = {}
|
|
172
|
+
for key in pb.metadata.keys():
|
|
173
|
+
self.metadata[key] = pb.metadata[key]
|
|
174
|
+
|
|
175
|
+
def __str__(self) -> str:
|
|
176
|
+
return f"Query id: {self.id}, name: '{self.name}'"
|
|
177
|
+
|
|
178
|
+
def __repr__(self):
|
|
179
|
+
return f"<{self.__class__.__name__} object, id: {self.id}, name: '{self.name}'>"
|
|
180
|
+
|
|
181
|
+
def data(self) -> List["numpy.array"]:
|
|
182
|
+
"""
|
|
183
|
+
Get the X,Y values for this query.
|
|
184
|
+
|
|
185
|
+
Returns
|
|
186
|
+
-------
|
|
187
|
+
List[numpy.array]
|
|
188
|
+
A list of two numpy arrays [X, Y].
|
|
189
|
+
"""
|
|
190
|
+
self._userd.connect_check()
|
|
191
|
+
pb = libuserd_pb2.Query_dataRequest()
|
|
192
|
+
pb.query_id = self.id
|
|
193
|
+
try:
|
|
194
|
+
reply = self._userd.stub.Query_data(pb, metadata=self._userd.metadata())
|
|
195
|
+
except grpc.RpcError as e:
|
|
196
|
+
raise self._userd.libuserd_exception(e)
|
|
197
|
+
return [numpy.array(reply.x), numpy.array(reply.y)]
|
|
198
|
+
|
|
199
|
+
|
|
200
|
+
class Variable(object):
|
|
201
|
+
"""
|
|
202
|
+
The class represents a reader "variable" instance. It includes
|
|
203
|
+
information about the variable, including it type (vector, scalar, etc)
|
|
204
|
+
location (nodal, elemental, etc), name and units.
|
|
205
|
+
|
|
206
|
+
Parameters
|
|
207
|
+
----------
|
|
208
|
+
userd
|
|
209
|
+
The LibUserd instance this query is associated with.
|
|
210
|
+
pb
|
|
211
|
+
The protobuffer that represents this object.
|
|
212
|
+
|
|
213
|
+
Attributes
|
|
214
|
+
----------
|
|
215
|
+
id : int
|
|
216
|
+
The id of this variable.
|
|
217
|
+
name : str
|
|
218
|
+
The name of this variable.
|
|
219
|
+
unit_label : str
|
|
220
|
+
The unit label of this variable, "Pa" for example.
|
|
221
|
+
unit_dims : str
|
|
222
|
+
The dimensions of this variable, "L/S" for distance per second.
|
|
223
|
+
location : "VariableLocation"
|
|
224
|
+
The location of this variable.
|
|
225
|
+
type : "VariableType"
|
|
226
|
+
The type of this variable.
|
|
227
|
+
time_varying : bool
|
|
228
|
+
True if the variable is time-varying.
|
|
229
|
+
complex : bool
|
|
230
|
+
True if the variable is complex.
|
|
231
|
+
number_of_components : int
|
|
232
|
+
The number of components of this variable. A scalar is 1 and
|
|
233
|
+
a vector is 3.
|
|
234
|
+
metadata : Dict[str, str]
|
|
235
|
+
The metadata for this query.
|
|
236
|
+
"""
|
|
237
|
+
|
|
238
|
+
def __init__(self, userd: "LibUserd", pb: libuserd_pb2.VariableInfo) -> None:
|
|
239
|
+
self._userd = userd
|
|
240
|
+
self.id = pb.id
|
|
241
|
+
self.name = pb.name
|
|
242
|
+
self.unit_label = pb.unit_label
|
|
243
|
+
self.unit_dims = pb.unit_dims
|
|
244
|
+
self.location = VariableLocation(pb.location) # type: ignore
|
|
245
|
+
self.type = VariableType(pb.type) # type: ignore
|
|
246
|
+
self.time_varying = pb.time_varying
|
|
247
|
+
self.complex = pb.complex
|
|
248
|
+
self.interleave_flag = pb.interleave_flag
|
|
249
|
+
self.number_of_components = pb.number_of_components
|
|
250
|
+
self.metadata = {}
|
|
251
|
+
for key in pb.metadata.keys():
|
|
252
|
+
self.metadata[key] = pb.metadata[key]
|
|
253
|
+
|
|
254
|
+
def __str__(self) -> str:
|
|
255
|
+
return f"Variable id: {self.id}, name: '{self.name}', type: {self.type.name}, location: {self.location.name}"
|
|
256
|
+
|
|
257
|
+
def __repr__(self):
|
|
258
|
+
return f"<{self.__class__.__name__} object, id: {self.id}, name: '{self.name}'>"
|
|
259
|
+
|
|
260
|
+
|
|
261
|
+
class Part(object):
|
|
262
|
+
"""
|
|
263
|
+
This class represents the EnSight notion of a part. A part is a single mesh consisting
|
|
264
|
+
of a nodal array along with a collection of element specifications. Methods are provided
|
|
265
|
+
to access the nodes and connectivity as well as any variables that might be defined
|
|
266
|
+
on the nodes or elements of this mesh.
|
|
267
|
+
|
|
268
|
+
Parameters
|
|
269
|
+
----------
|
|
270
|
+
userd
|
|
271
|
+
The LibUserd instance this query is associated with.
|
|
272
|
+
pb
|
|
273
|
+
The protobuffer that represents this object.
|
|
274
|
+
|
|
275
|
+
Attributes
|
|
276
|
+
----------
|
|
277
|
+
id : int
|
|
278
|
+
The id of this part.
|
|
279
|
+
name : str
|
|
280
|
+
The name of this part.
|
|
281
|
+
reader_id : int
|
|
282
|
+
The id of the Reader this part is associated with.
|
|
283
|
+
hints : int
|
|
284
|
+
See: `PartHints`.
|
|
285
|
+
metadata : Dict[str, str]
|
|
286
|
+
The metadata for this query.
|
|
287
|
+
"""
|
|
288
|
+
|
|
289
|
+
def __init__(self, userd: "LibUserd", pb: libuserd_pb2.PartInfo):
|
|
290
|
+
self._userd = userd
|
|
291
|
+
self.index = pb.index
|
|
292
|
+
self.id = pb.id
|
|
293
|
+
self.name = pb.name
|
|
294
|
+
self.reader_id = pb.reader_id
|
|
295
|
+
self.hints = pb.hints
|
|
296
|
+
self.metadata = {}
|
|
297
|
+
for key in pb.metadata.keys():
|
|
298
|
+
self.metadata[key] = pb.metadata[key]
|
|
299
|
+
|
|
300
|
+
def __str__(self):
|
|
301
|
+
return f"Part id: {self.id}, name: '{self.name}'"
|
|
302
|
+
|
|
303
|
+
def __repr__(self):
|
|
304
|
+
return f"<{self.__class__.__name__} object, id: {self.id}, name: '{self.name}'>"
|
|
305
|
+
|
|
306
|
+
def nodes(self, rank: Optional[int] = None) -> "numpy.array":
|
|
307
|
+
"""
|
|
308
|
+
Return the vertex array for the part.
|
|
309
|
+
|
|
310
|
+
Parameters
|
|
311
|
+
----------
|
|
312
|
+
rank : int, optional
|
|
313
|
+
For a dataset using multiple ranks, the rank to return data from. The
|
|
314
|
+
default is RankValues.SINGLE_RANK.
|
|
315
|
+
|
|
316
|
+
Returns
|
|
317
|
+
-------
|
|
318
|
+
numpy.array
|
|
319
|
+
A numpy array of packed values: x,y,z,z,y,z, ...
|
|
320
|
+
|
|
321
|
+
Examples
|
|
322
|
+
--------
|
|
323
|
+
|
|
324
|
+
>>> part = reader.parts()[0]
|
|
325
|
+
>>> nodes = part.nodes()
|
|
326
|
+
>>> nodes.shape = (len(nodes)//3, 3)
|
|
327
|
+
>>> print(nodes)
|
|
328
|
+
|
|
329
|
+
"""
|
|
330
|
+
self._userd.connect_check()
|
|
331
|
+
rank = self._userd.rank_check(rank)
|
|
332
|
+
pb = libuserd_pb2.Part_nodesRequest()
|
|
333
|
+
pb.part_id = self.id
|
|
334
|
+
pb.rank = rank
|
|
335
|
+
try:
|
|
336
|
+
stream = self._userd.stub.Part_nodes(pb, metadata=self._userd.metadata())
|
|
337
|
+
except grpc.RpcError as e:
|
|
338
|
+
raise self._userd.libuserd_exception(e)
|
|
339
|
+
nodes = numpy.empty(0, dtype=numpy.float32)
|
|
340
|
+
for chunk in stream:
|
|
341
|
+
if len(nodes) < chunk.total_size:
|
|
342
|
+
nodes = numpy.empty(chunk.total_size, dtype=numpy.float32)
|
|
343
|
+
offset = chunk.offset
|
|
344
|
+
values = numpy.array(chunk.xyz)
|
|
345
|
+
nodes[offset : offset + len(values)] = values
|
|
346
|
+
return nodes
|
|
347
|
+
|
|
348
|
+
def num_elements(self, rank: Optional[int] = None) -> dict:
|
|
349
|
+
"""
|
|
350
|
+
Get the number of elements of a given type in the current part.
|
|
351
|
+
|
|
352
|
+
Parameters
|
|
353
|
+
----------
|
|
354
|
+
rank : int, optional
|
|
355
|
+
For a dataset using multiple ranks, the rank to return data from. The
|
|
356
|
+
default is RankValues.SINGLE_RANK.
|
|
357
|
+
|
|
358
|
+
Returns
|
|
359
|
+
-------
|
|
360
|
+
dict
|
|
361
|
+
A dictionary with keys being the element type and the values being the number of
|
|
362
|
+
such elements. Element types with zero elements are not included in the dictionary.
|
|
363
|
+
|
|
364
|
+
Examples
|
|
365
|
+
--------
|
|
366
|
+
|
|
367
|
+
>>> part = reader.parts()[0]
|
|
368
|
+
>>> elements = part.elements()
|
|
369
|
+
>>> for etype, count in elements.items():
|
|
370
|
+
... print(libuserd.ElementType(etype).name, count)
|
|
371
|
+
|
|
372
|
+
"""
|
|
373
|
+
self._userd.connect_check()
|
|
374
|
+
rank = self._userd.rank_check(rank)
|
|
375
|
+
pb = libuserd_pb2.Part_num_elementsRequest()
|
|
376
|
+
pb.part_id = self.id
|
|
377
|
+
pb.rank = rank
|
|
378
|
+
try:
|
|
379
|
+
reply = self._userd.stub.Part_num_elements(pb, metadata=self._userd.metadata())
|
|
380
|
+
except grpc.RpcError as e:
|
|
381
|
+
raise self._userd.libuserd_exception(e)
|
|
382
|
+
elements = {}
|
|
383
|
+
for key in reply.element_count.keys():
|
|
384
|
+
if reply.element_count[key] > 0:
|
|
385
|
+
elements[key] = reply.element_count[key]
|
|
386
|
+
return elements
|
|
387
|
+
|
|
388
|
+
def element_conn(self, elem_type: int, rank: Optional[int] = None) -> "numpy.array":
|
|
389
|
+
"""
|
|
390
|
+
For "zoo" element types, return the part element connectivity for the specified
|
|
391
|
+
element type.
|
|
392
|
+
|
|
393
|
+
Parameters
|
|
394
|
+
----------
|
|
395
|
+
elem_type : int
|
|
396
|
+
The element type. All but NFACED and NSIDED element types are allowed.
|
|
397
|
+
rank : int, optional
|
|
398
|
+
For a dataset using multiple ranks, the rank to return data from. The
|
|
399
|
+
default is RankValues.SINGLE_RANK.
|
|
400
|
+
|
|
401
|
+
Returns
|
|
402
|
+
-------
|
|
403
|
+
numpy.array
|
|
404
|
+
A numpy array of the node indices.
|
|
405
|
+
|
|
406
|
+
Examples
|
|
407
|
+
--------
|
|
408
|
+
|
|
409
|
+
>>> part = reader.parts()[0]
|
|
410
|
+
>>> conn = part.element_conn(libuserd.ElementType.HEX08)
|
|
411
|
+
>>> nodes_per_elem = libuserd_instance.nodes_per_element(libuserd.ElementType.HEX08)
|
|
412
|
+
>>> conn.shape = (len(conn)//nodes_per_elem, nodes_per_elem)
|
|
413
|
+
>>> for element in conn:
|
|
414
|
+
... print(element)
|
|
415
|
+
|
|
416
|
+
"""
|
|
417
|
+
if elem_type >= ElementType.NSIDED: # type: ignore
|
|
418
|
+
raise RuntimeError(f"Element type {elem_type} is not valid for this call")
|
|
419
|
+
self._userd.connect_check()
|
|
420
|
+
rank = self._userd.rank_check(rank)
|
|
421
|
+
pb = libuserd_pb2.Part_element_connRequest()
|
|
422
|
+
pb.part_id = self.id
|
|
423
|
+
pb.type = elem_type
|
|
424
|
+
pb.rank = rank
|
|
425
|
+
try:
|
|
426
|
+
stream = self._userd.stub.Part_element_conn(pb, metadata=self._userd.metadata())
|
|
427
|
+
conn = numpy.empty(0, dtype=numpy.uint32)
|
|
428
|
+
for chunk in stream:
|
|
429
|
+
if len(conn) < chunk.total_size:
|
|
430
|
+
conn = numpy.empty(chunk.total_size, dtype=numpy.uint32)
|
|
431
|
+
offset = chunk.offset
|
|
432
|
+
values = numpy.array(chunk.connectivity)
|
|
433
|
+
conn[offset : offset + len(values)] = values
|
|
434
|
+
except grpc.RpcError as e:
|
|
435
|
+
error = self._userd.libuserd_exception(e)
|
|
436
|
+
# if we get an "UNKNOWN" error, then return an empty array
|
|
437
|
+
if isinstance(error, LibUserdError):
|
|
438
|
+
if error.code == ErrorCodes.UNKNOWN: # type: ignore
|
|
439
|
+
return numpy.empty(0, dtype=numpy.uint32)
|
|
440
|
+
raise error
|
|
441
|
+
return conn
|
|
442
|
+
|
|
443
|
+
def element_conn_nsided(
|
|
444
|
+
self, elem_type: int, rank: Optional[int] = None
|
|
445
|
+
) -> List["numpy.array"]:
|
|
446
|
+
"""
|
|
447
|
+
For an N-Sided element type (regular or ghost), return the connectivity information
|
|
448
|
+
for the elements of that type in this part at this timestep.
|
|
449
|
+
|
|
450
|
+
Two arrays are returned in a list:
|
|
451
|
+
|
|
452
|
+
- num_nodes_per_element : one number per element that represent the number of nodes in that element
|
|
453
|
+
- nodes : the actual node indices
|
|
454
|
+
|
|
455
|
+
Arrays are packed sequentially. Walking the elements sequentially, if the number of
|
|
456
|
+
nodes for an element is 4, then there are 4 entries added to the nodes array
|
|
457
|
+
for that element.
|
|
458
|
+
|
|
459
|
+
Parameters
|
|
460
|
+
----------
|
|
461
|
+
elem_type: int
|
|
462
|
+
NSIDED or NSIDED_GHOST.
|
|
463
|
+
rank : int, optional
|
|
464
|
+
For a dataset using multiple ranks, the rank to return data from. The
|
|
465
|
+
default is RankValues.SINGLE_RANK.
|
|
466
|
+
|
|
467
|
+
Returns
|
|
468
|
+
-------
|
|
469
|
+
List[numpy.array]
|
|
470
|
+
Two numpy arrays: num_nodes_per_element, nodes
|
|
471
|
+
"""
|
|
472
|
+
self._userd.connect_check()
|
|
473
|
+
rank = self._userd.rank_check(rank)
|
|
474
|
+
pb = libuserd_pb2.Part_element_conn_nsidedRequest()
|
|
475
|
+
pb.part_id = self.id
|
|
476
|
+
pb.type = elem_type
|
|
477
|
+
pb.rank = rank
|
|
478
|
+
try:
|
|
479
|
+
stream = self._userd.stub.Part_element_conn_nsided(pb, metadata=self._userd.metadata())
|
|
480
|
+
nodes = numpy.empty(0, dtype=numpy.uint32)
|
|
481
|
+
indices = numpy.empty(0, dtype=numpy.uint32)
|
|
482
|
+
for chunk in stream:
|
|
483
|
+
if len(nodes) < chunk.nodes_total_size:
|
|
484
|
+
nodes = numpy.empty(chunk.nodes_total_size, dtype=numpy.uint32)
|
|
485
|
+
if len(indices) < chunk.indices_total_size:
|
|
486
|
+
indices = numpy.empty(chunk.indices_total_size, dtype=numpy.uint32)
|
|
487
|
+
if len(chunk.nodes_per_polygon):
|
|
488
|
+
offset = chunk.nodes_offset
|
|
489
|
+
values = numpy.array(chunk.nodes_per_polygon)
|
|
490
|
+
nodes[offset : offset + len(values)] = values
|
|
491
|
+
if len(chunk.node_indices):
|
|
492
|
+
offset = chunk.indices_offset
|
|
493
|
+
values = numpy.array(chunk.node_indices)
|
|
494
|
+
indices[offset : offset + len(values)] = values
|
|
495
|
+
except grpc.RpcError as e:
|
|
496
|
+
raise self._userd.libuserd_exception(e)
|
|
497
|
+
return [nodes, indices]
|
|
498
|
+
|
|
499
|
+
def element_conn_nfaced(
|
|
500
|
+
self, elem_type: int, rank: Optional[int] = None
|
|
501
|
+
) -> List["numpy.array"]:
|
|
502
|
+
"""
|
|
503
|
+
For an N-Faced element type (regular or ghost), return the connectivity information
|
|
504
|
+
for the elements of that type in this part at this timestep.
|
|
505
|
+
|
|
506
|
+
Three arrays are returned in a list:
|
|
507
|
+
|
|
508
|
+
- num_faces_per_element : one number per element that represent the number of faces in that element
|
|
509
|
+
- num_nodes_per_face : for each face, the number of nodes in the face.
|
|
510
|
+
- face_nodes : the actual node indices
|
|
511
|
+
|
|
512
|
+
All arrays are packed sequentially. Walking the elements sequentially, if the number of
|
|
513
|
+
faces for an element is 4, then there are 4 entries added to the num_nodes_per_face array
|
|
514
|
+
for that element. Likewise, the nodes for each face are appended in order to the
|
|
515
|
+
face_nodes array.
|
|
516
|
+
|
|
517
|
+
Parameters
|
|
518
|
+
----------
|
|
519
|
+
elem_type: int
|
|
520
|
+
NFACED or NFACED_GHOST.
|
|
521
|
+
rank : int, optional
|
|
522
|
+
For a dataset using multiple ranks, the rank to return data from. The
|
|
523
|
+
default is RankValues.SINGLE_RANK.
|
|
524
|
+
|
|
525
|
+
Returns
|
|
526
|
+
-------
|
|
527
|
+
List[numpy.array]
|
|
528
|
+
Three numpy arrays: num_faces_per_element, num_nodes_per_face, face_nodes
|
|
529
|
+
"""
|
|
530
|
+
self._userd.connect_check()
|
|
531
|
+
rank = self._userd.rank_check(rank)
|
|
532
|
+
pb = libuserd_pb2.Part_element_conn_nfacedRequest()
|
|
533
|
+
pb.part_id = self.id
|
|
534
|
+
pb.type = elem_type
|
|
535
|
+
pb.rank = rank
|
|
536
|
+
try:
|
|
537
|
+
stream = self._userd.stub.Part_element_conn_nfaced(pb, metadata=self._userd.metadata())
|
|
538
|
+
face = numpy.empty(0, dtype=numpy.uint32)
|
|
539
|
+
npf = numpy.empty(0, dtype=numpy.uint32)
|
|
540
|
+
nodes = numpy.empty(0, dtype=numpy.uint32)
|
|
541
|
+
for chunk in stream:
|
|
542
|
+
if len(face) < chunk.face_total_size:
|
|
543
|
+
face = numpy.empty(chunk.face_total_size, dtype=numpy.uint32)
|
|
544
|
+
if len(npf) < chunk.npf_total_size:
|
|
545
|
+
npf = numpy.empty(chunk.npf_total_size, dtype=numpy.uint32)
|
|
546
|
+
if len(nodes) < chunk.nodes_total_size:
|
|
547
|
+
nodes = numpy.empty(chunk.nodes_total_size, dtype=numpy.uint32)
|
|
548
|
+
if len(chunk.faces_per_element):
|
|
549
|
+
offset = chunk.face_offset
|
|
550
|
+
values = numpy.array(chunk.faces_per_element)
|
|
551
|
+
face[offset : offset + len(values)] = values
|
|
552
|
+
if len(chunk.nodes_per_face):
|
|
553
|
+
offset = chunk.npf_offset
|
|
554
|
+
values = numpy.array(chunk.nodes_per_face)
|
|
555
|
+
npf[offset : offset + len(values)] = values
|
|
556
|
+
if len(chunk.node_indices):
|
|
557
|
+
offset = chunk.nodes_offset
|
|
558
|
+
values = numpy.array(chunk.node_indices)
|
|
559
|
+
nodes[offset : offset + len(values)] = values
|
|
560
|
+
except grpc.RpcError as e:
|
|
561
|
+
raise self._userd.libuserd_exception(e)
|
|
562
|
+
return [face, npf, nodes]
|
|
563
|
+
|
|
564
|
+
def variable_values(
|
|
565
|
+
self,
|
|
566
|
+
variable: "Variable",
|
|
567
|
+
elem_type: int = 0,
|
|
568
|
+
imaginary: bool = False,
|
|
569
|
+
component: int = 0,
|
|
570
|
+
rank: Optional[int] = None,
|
|
571
|
+
) -> "numpy.array":
|
|
572
|
+
"""
|
|
573
|
+
Return a numpy array containing the value(s) of a variable. If the variable is a
|
|
574
|
+
part variable, a single float value is returned. If the variable is a nodal variable,
|
|
575
|
+
the resulting numpy array will have the same number of values as there are nodes.
|
|
576
|
+
If the variable is elemental, the `elem_type` selects the block of elements to return
|
|
577
|
+
the variable values for (`elem_type` is ignored for other variable types).
|
|
578
|
+
|
|
579
|
+
Parameters
|
|
580
|
+
----------
|
|
581
|
+
variable : Variable
|
|
582
|
+
The variable to return the values for.
|
|
583
|
+
elem_type : int
|
|
584
|
+
Used only if the variable location is elemental, this keyword selects the element
|
|
585
|
+
type to return the variable values for.
|
|
586
|
+
imaginary : bool
|
|
587
|
+
If the variable is of type complex, setting this to True will select the imaginary
|
|
588
|
+
portion of the data.
|
|
589
|
+
component : int
|
|
590
|
+
Select the channel for a multivalued variable type. For example, if the variable
|
|
591
|
+
is a vector, setting component to 1 will select the 'Y' component.
|
|
592
|
+
rank : int, optional
|
|
593
|
+
For a dataset using multiple ranks, the rank to return data from. The
|
|
594
|
+
default is RankValues.SINGLE_RANK.
|
|
595
|
+
|
|
596
|
+
Returns
|
|
597
|
+
-------
|
|
598
|
+
numpy.array
|
|
599
|
+
A numpy array or a single scalar float.
|
|
600
|
+
"""
|
|
601
|
+
self._userd.connect_check()
|
|
602
|
+
rank = self._userd.rank_check(rank)
|
|
603
|
+
pb = libuserd_pb2.Part_variable_valuesRequest()
|
|
604
|
+
pb.part_id = self.id
|
|
605
|
+
pb.var_id = variable.id
|
|
606
|
+
pb.type = elem_type
|
|
607
|
+
pb.component = component
|
|
608
|
+
pb.complex = imaginary
|
|
609
|
+
pb.rank = rank
|
|
610
|
+
try:
|
|
611
|
+
stream = self._userd.stub.Part_variable_values(pb, metadata=self._userd.metadata())
|
|
612
|
+
v = numpy.empty(0, dtype=numpy.float32)
|
|
613
|
+
for chunk in stream:
|
|
614
|
+
if len(v) < chunk.total_size:
|
|
615
|
+
v = numpy.empty(chunk.total_size, dtype=numpy.float32)
|
|
616
|
+
offset = chunk.offset
|
|
617
|
+
values = numpy.array(chunk.values)
|
|
618
|
+
v[offset : offset + len(values)] = values
|
|
619
|
+
except grpc.RpcError as e:
|
|
620
|
+
raise self._userd.libuserd_exception(e)
|
|
621
|
+
return v
|
|
622
|
+
|
|
623
|
+
def rigid_body_transform(self) -> dict:
|
|
624
|
+
"""
|
|
625
|
+
Return the rigid body transform for this part at the current timestep. The
|
|
626
|
+
returned dictionary includes the following fields:
|
|
627
|
+
|
|
628
|
+
- "translation" : Translation 3 floats x,y,z
|
|
629
|
+
- "euler_value" : Euler values 4 floats e0,e1,e2,e3
|
|
630
|
+
- "center_of_gravity" : Center of transform 3 floats x,y,z
|
|
631
|
+
- "rotation_order" : The order rotations are applied 1 float
|
|
632
|
+
- "rotation_angles" : The rotations in radians 3 floats rx,ry,rz
|
|
633
|
+
|
|
634
|
+
Returns
|
|
635
|
+
-------
|
|
636
|
+
dict
|
|
637
|
+
The transform dictionary.
|
|
638
|
+
"""
|
|
639
|
+
self._userd.connect_check()
|
|
640
|
+
pb = libuserd_pb2.Part_rigid_body_transformRequest()
|
|
641
|
+
pb.part_id = self.id
|
|
642
|
+
try:
|
|
643
|
+
reply = self._userd.stub.Part_rigid_body_transform(pb, metadata=self._userd.metadata())
|
|
644
|
+
except grpc.RpcError as e:
|
|
645
|
+
raise self._userd.libuserd_exception(e)
|
|
646
|
+
out = {
|
|
647
|
+
"translation": numpy.array(reply.transform.translation),
|
|
648
|
+
"euler_value": numpy.array(reply.transform.euler_value),
|
|
649
|
+
"center_of_gravity": numpy.array(reply.transform.center_of_gravity),
|
|
650
|
+
"rotation_order": reply.transform.rotation_order,
|
|
651
|
+
"rotation_angles": numpy.array(reply.transform.rotation_angles),
|
|
652
|
+
}
|
|
653
|
+
return out
|
|
654
|
+
|
|
655
|
+
|
|
656
|
+
class Reader(object):
|
|
657
|
+
"""
|
|
658
|
+
This class represents is an instance of a user-defined reader that is actively reading a
|
|
659
|
+
dataset.
|
|
660
|
+
|
|
661
|
+
Parameters
|
|
662
|
+
----------
|
|
663
|
+
userd
|
|
664
|
+
The LibUserd instance this query is associated with.
|
|
665
|
+
pb
|
|
666
|
+
The protobuffer that represents this object.
|
|
667
|
+
|
|
668
|
+
Attributes
|
|
669
|
+
----------
|
|
670
|
+
unit_system : str
|
|
671
|
+
The units system provided by the dataset.
|
|
672
|
+
metadata : Dict[str, str]
|
|
673
|
+
The metadata for this query.
|
|
674
|
+
reader_api_version : float
|
|
675
|
+
The reader USERD API version number
|
|
676
|
+
number_of_ranks : int
|
|
677
|
+
The number of ranks being used by the dataset reader
|
|
678
|
+
|
|
679
|
+
Notes
|
|
680
|
+
-----
|
|
681
|
+
There can only be one reader active in a single `LibUserd` instance.
|
|
682
|
+
|
|
683
|
+
"""
|
|
684
|
+
|
|
685
|
+
def __init__(self, userd: "LibUserd", pb: libuserd_pb2.Reader) -> None:
|
|
686
|
+
self._userd = userd
|
|
687
|
+
self.unit_system = pb.unit_system
|
|
688
|
+
self.reader_api_version = pb.reader_api_version
|
|
689
|
+
self.number_of_ranks = pb.number_of_ranks
|
|
690
|
+
self.metadata = {}
|
|
691
|
+
for key in pb.metadata.keys():
|
|
692
|
+
self.metadata[key] = pb.metadata[key]
|
|
693
|
+
self.raw_metadata = pb.raw_metadata
|
|
694
|
+
self._timesets: List["numpy.array"] = []
|
|
695
|
+
self._update_timesets()
|
|
696
|
+
|
|
697
|
+
def _update_timesets(self) -> None:
|
|
698
|
+
"""
|
|
699
|
+
To simplify the interface to time, the timesets are all queried and
|
|
700
|
+
cached. Additionally, a "common timeset" is generated that combines
|
|
701
|
+
all the timevalues from all timesets into a single timeset which is
|
|
702
|
+
saved as "timeset 0".
|
|
703
|
+
|
|
704
|
+
This method reads all the timesets and generates the common timeset.
|
|
705
|
+
"""
|
|
706
|
+
if len(self._timesets):
|
|
707
|
+
return
|
|
708
|
+
num_timesets = self.get_number_of_time_sets()
|
|
709
|
+
# The common timeset
|
|
710
|
+
common = set()
|
|
711
|
+
self._timesets = [numpy.array([], dtype="float32")]
|
|
712
|
+
# The other timesets
|
|
713
|
+
for ts in range(1, num_timesets + 1):
|
|
714
|
+
v = self.timevalues(timeset=ts)
|
|
715
|
+
# merge into the common timeset
|
|
716
|
+
common.update(v)
|
|
717
|
+
self._timesets.append(v)
|
|
718
|
+
self._timesets[0] = numpy.array(sorted(list(common)))
|
|
719
|
+
|
|
720
|
+
def _common_set_step(self, s: int) -> None:
|
|
721
|
+
"""
|
|
722
|
+
When the common timeset is used in a ``set_timestep()`` call, this
|
|
723
|
+
method selects the time value from the common timeset and then calls
|
|
724
|
+
``_common_set_time()`` to change the current simulation time.
|
|
725
|
+
|
|
726
|
+
Parameters
|
|
727
|
+
----------
|
|
728
|
+
s : int
|
|
729
|
+
The index (timestep) in the common timeset to change the current time to.
|
|
730
|
+
|
|
731
|
+
Raises
|
|
732
|
+
------
|
|
733
|
+
RuntimeError
|
|
734
|
+
If the timestep index is invalid.
|
|
735
|
+
|
|
736
|
+
"""
|
|
737
|
+
try:
|
|
738
|
+
v = self._timesets[0][s]
|
|
739
|
+
self._common_set_time(v)
|
|
740
|
+
except IndexError:
|
|
741
|
+
raise RuntimeError(f"Invalid step number {s}.") from None
|
|
742
|
+
|
|
743
|
+
def _common_set_time(self, t: float) -> None:
|
|
744
|
+
"""
|
|
745
|
+
Change the current time value to the passed time value. This method
|
|
746
|
+
walks all the timesets. It selects the largest time value in each timeset
|
|
747
|
+
that is less than or equal to the specified time value. It then sets
|
|
748
|
+
the time value for each timeset accordingly.
|
|
749
|
+
|
|
750
|
+
Parameters
|
|
751
|
+
----------
|
|
752
|
+
t : float
|
|
753
|
+
The time value (in the common timeset) to change the reader simulation time to.
|
|
754
|
+
|
|
755
|
+
"""
|
|
756
|
+
# given the time float from the common timeline,
|
|
757
|
+
# change the timestep in all the timesets to match.
|
|
758
|
+
for timeset in range(1, len(self._timesets)):
|
|
759
|
+
# check for perfect match first (avoids rounding)
|
|
760
|
+
where = numpy.where(self._timesets[timeset] == t)
|
|
761
|
+
if len(where[0]):
|
|
762
|
+
timestep = where[0][0]
|
|
763
|
+
else:
|
|
764
|
+
timestep = numpy.searchsorted(self._timesets[timeset], t)
|
|
765
|
+
timestep = min(timestep, len(self._timesets[timeset]) - 1)
|
|
766
|
+
self.set_timestep(timestep, timeset=timeset)
|
|
767
|
+
|
|
768
|
+
def parts(self) -> List[Part]:
|
|
769
|
+
"""
|
|
770
|
+
Get a list of the parts this reader can access.
|
|
771
|
+
|
|
772
|
+
Returns
|
|
773
|
+
-------
|
|
774
|
+
List[Part]
|
|
775
|
+
A list of Part objects.
|
|
776
|
+
"""
|
|
777
|
+
self._userd.connect_check()
|
|
778
|
+
pb = libuserd_pb2.Reader_partsRequest()
|
|
779
|
+
try:
|
|
780
|
+
parts = self._userd.stub.Reader_parts(pb, metadata=self._userd.metadata())
|
|
781
|
+
except grpc.RpcError as e:
|
|
782
|
+
raise self._userd.libuserd_exception(e)
|
|
783
|
+
out = []
|
|
784
|
+
for part in parts.part_list:
|
|
785
|
+
out.append(Part(self._userd, part))
|
|
786
|
+
return out
|
|
787
|
+
|
|
788
|
+
def variables(self) -> List[Variable]:
|
|
789
|
+
"""
|
|
790
|
+
Get a list of the variables this reader can access.
|
|
791
|
+
|
|
792
|
+
Returns
|
|
793
|
+
-------
|
|
794
|
+
List[Variable]
|
|
795
|
+
A list of Variable objects.
|
|
796
|
+
"""
|
|
797
|
+
self._userd.connect_check()
|
|
798
|
+
pb = libuserd_pb2.Reader_variablesRequest()
|
|
799
|
+
try:
|
|
800
|
+
variables = self._userd.stub.Reader_variables(pb, metadata=self._userd.metadata())
|
|
801
|
+
except grpc.RpcError as e:
|
|
802
|
+
raise self._userd.libuserd_exception(e)
|
|
803
|
+
out = []
|
|
804
|
+
for variable in variables.variable_list:
|
|
805
|
+
out.append(Variable(self._userd, variable))
|
|
806
|
+
return out
|
|
807
|
+
|
|
808
|
+
def queries(self) -> List[Query]:
|
|
809
|
+
"""
|
|
810
|
+
Get a list of the queries this reader can access.
|
|
811
|
+
|
|
812
|
+
Returns
|
|
813
|
+
-------
|
|
814
|
+
List[Query]
|
|
815
|
+
A list of Query objects.
|
|
816
|
+
"""
|
|
817
|
+
self._userd.connect_check()
|
|
818
|
+
pb = libuserd_pb2.Reader_queriesRequest()
|
|
819
|
+
try:
|
|
820
|
+
queries = self._userd.stub.Reader_queries(pb, metadata=self._userd.metadata())
|
|
821
|
+
except grpc.RpcError as e:
|
|
822
|
+
raise self._userd.libuserd_exception(e)
|
|
823
|
+
out = []
|
|
824
|
+
for query in queries.query_list:
|
|
825
|
+
out.append(Query(self._userd, query))
|
|
826
|
+
return out
|
|
827
|
+
|
|
828
|
+
def get_number_of_time_sets(self) -> int:
|
|
829
|
+
"""
|
|
830
|
+
Get the number of timesets in the dataset.
|
|
831
|
+
|
|
832
|
+
Returns
|
|
833
|
+
-------
|
|
834
|
+
int
|
|
835
|
+
The number of timesets.
|
|
836
|
+
"""
|
|
837
|
+
if len(self._timesets):
|
|
838
|
+
return len(self._timesets) - 1
|
|
839
|
+
self._userd.connect_check()
|
|
840
|
+
pb = libuserd_pb2.Reader_get_number_of_time_setsRequest()
|
|
841
|
+
try:
|
|
842
|
+
reply = self._userd.stub.Reader_get_number_of_time_sets(
|
|
843
|
+
pb, metadata=self._userd.metadata()
|
|
844
|
+
)
|
|
845
|
+
except grpc.RpcError as e:
|
|
846
|
+
raise self._userd.libuserd_exception(e)
|
|
847
|
+
return reply.number_of_timesets
|
|
848
|
+
|
|
849
|
+
def timevalues(self, timeset: int = 0) -> List[float]:
|
|
850
|
+
"""
|
|
851
|
+
Get a list of the time step values in this dataset for the specified timeset.
|
|
852
|
+
The default timeset is ``0`` which is a special, "common" timeset formed by
|
|
853
|
+
merging all the timesets in the data into a single timeset.
|
|
854
|
+
|
|
855
|
+
Parameters
|
|
856
|
+
----------
|
|
857
|
+
timeset : int, optional
|
|
858
|
+
The timestep to query (default is 0)
|
|
859
|
+
|
|
860
|
+
Returns
|
|
861
|
+
-------
|
|
862
|
+
numpy.array
|
|
863
|
+
The simulation time value floats.
|
|
864
|
+
"""
|
|
865
|
+
if timeset == 0:
|
|
866
|
+
try:
|
|
867
|
+
return self._timesets[timeset]
|
|
868
|
+
except IndexError:
|
|
869
|
+
return numpy.array([], dtype="float32")
|
|
870
|
+
self._userd.connect_check()
|
|
871
|
+
pb = libuserd_pb2.Reader_timevaluesRequest()
|
|
872
|
+
pb.timeset_number = timeset
|
|
873
|
+
try:
|
|
874
|
+
timevalues = self._userd.stub.Reader_timevalues(pb, metadata=self._userd.metadata())
|
|
875
|
+
except grpc.RpcError as e:
|
|
876
|
+
raise self._userd.libuserd_exception(e)
|
|
877
|
+
return numpy.array(timevalues.time_values)
|
|
878
|
+
|
|
879
|
+
def set_timevalue(self, timevalue: float, timeset: int = 0) -> None:
|
|
880
|
+
"""
|
|
881
|
+
Change the current time within the selected timeset to the specified value.
|
|
882
|
+
The default timeset selected is the merged "common" timeset ``0``. If the
|
|
883
|
+
"common" timeset is used, the appropriate time value will be set for
|
|
884
|
+
all timesets by this method.
|
|
885
|
+
|
|
886
|
+
Parameters
|
|
887
|
+
----------
|
|
888
|
+
timevalue : float
|
|
889
|
+
The time value to change the timestep closest to.
|
|
890
|
+
timeset : int, optional
|
|
891
|
+
The timeset to change (default is 0)
|
|
892
|
+
|
|
893
|
+
Examples
|
|
894
|
+
--------
|
|
895
|
+
>>> from ansys.pyensight.core import libuserd
|
|
896
|
+
>>> import numpy
|
|
897
|
+
>>> s = libuserd.LibUserd()
|
|
898
|
+
>>> s.initialize()
|
|
899
|
+
>>> opt = {'Long names': 0, 'Number of timesteps': 5, 'Number of scalars': 3,
|
|
900
|
+
... 'Number of spheres': 2, 'Number of cubes': 2}
|
|
901
|
+
>>> d = s.load_data("foo", file_format="Synthetic", reader_options=opt)
|
|
902
|
+
>>> parts = d.parts()
|
|
903
|
+
>>> for t in d.timevalues():
|
|
904
|
+
... d.set_timevalue(t)
|
|
905
|
+
... for p in parts:
|
|
906
|
+
... nodes = p.nodes()
|
|
907
|
+
... nodes.shape = (len(nodes)//3, 3)
|
|
908
|
+
... centroid = numpy.average(nodes, 0)
|
|
909
|
+
... print(f"Time: {t} Part: {p.name} Centroid: {centroid}")
|
|
910
|
+
>>> s.shutdown()
|
|
911
|
+
|
|
912
|
+
"""
|
|
913
|
+
if timeset == 0:
|
|
914
|
+
self._common_set_time(timevalue)
|
|
915
|
+
return
|
|
916
|
+
self._userd.connect_check()
|
|
917
|
+
pb = libuserd_pb2.Reader_set_timevalueRequest()
|
|
918
|
+
pb.timeset_number = timeset
|
|
919
|
+
pb.time_value = timevalue
|
|
920
|
+
try:
|
|
921
|
+
_ = self._userd.stub.Reader_set_timevalue(pb, metadata=self._userd.metadata())
|
|
922
|
+
except grpc.RpcError as e:
|
|
923
|
+
raise self._userd.libuserd_exception(e)
|
|
924
|
+
|
|
925
|
+
def set_timestep(self, timestep: int, timeset: int = 0) -> None:
|
|
926
|
+
"""
|
|
927
|
+
Change the current time to the specified timestep. This call is the same as:
|
|
928
|
+
``reader.set_timevalue(reader.timevalues()[timestep])``.
|
|
929
|
+
The default timeset selected is the merged "common" timeset ``0``. If the
|
|
930
|
+
"common" timeset is used, the appropriate time value will be set for
|
|
931
|
+
all timesets by this method.
|
|
932
|
+
|
|
933
|
+
Parameters
|
|
934
|
+
----------
|
|
935
|
+
timestep : int
|
|
936
|
+
The timestep to change to.
|
|
937
|
+
timeset : int, optional
|
|
938
|
+
The timeset to change (default is 0)
|
|
939
|
+
"""
|
|
940
|
+
if timeset == 0:
|
|
941
|
+
self._common_set_step(timestep)
|
|
942
|
+
return
|
|
943
|
+
self._userd.connect_check()
|
|
944
|
+
pb = libuserd_pb2.Reader_set_timestepRequest()
|
|
945
|
+
pb.timeset_number = timeset
|
|
946
|
+
pb.time_step = timestep
|
|
947
|
+
try:
|
|
948
|
+
_ = self._userd.stub.Reader_set_timestep(pb, metadata=self._userd.metadata())
|
|
949
|
+
except grpc.RpcError as e:
|
|
950
|
+
raise self._userd.libuserd_exception(e)
|
|
951
|
+
|
|
952
|
+
def is_geometry_changing(self) -> bool:
|
|
953
|
+
"""
|
|
954
|
+
Check to see if the geometry in this dataset is changing. over time
|
|
955
|
+
|
|
956
|
+
Returns
|
|
957
|
+
-------
|
|
958
|
+
bool
|
|
959
|
+
True if the geometry is changing, False otherwise.
|
|
960
|
+
"""
|
|
961
|
+
self._userd.connect_check()
|
|
962
|
+
pb = libuserd_pb2.Reader_is_geometry_changingRequest()
|
|
963
|
+
try:
|
|
964
|
+
reply = self._userd.stub.Reader_is_geometry_changing(
|
|
965
|
+
pb, metadata=self._userd.metadata()
|
|
966
|
+
)
|
|
967
|
+
except grpc.RpcError as e:
|
|
968
|
+
raise self._userd.libuserd_exception(e)
|
|
969
|
+
return reply.is_geometry_changing
|
|
970
|
+
|
|
971
|
+
def dynamic_update_check(self, changes_allowed: int) -> int:
|
|
972
|
+
"""
|
|
973
|
+
When this method is called, it allows the reader to change aspects of
|
|
974
|
+
dataset. If the reader changes the time steps, mesh data or the
|
|
975
|
+
dataset structure (variable, part or query lists) this function
|
|
976
|
+
will return the nature of the change. The returned bits will always be
|
|
977
|
+
a subset of the input allowed bits.
|
|
978
|
+
|
|
979
|
+
Parameters
|
|
980
|
+
----------
|
|
981
|
+
changes_allowed : int
|
|
982
|
+
Bitfield formed by oring UpdateHints values for the things that are allowed to change.
|
|
983
|
+
|
|
984
|
+
Returns
|
|
985
|
+
-------
|
|
986
|
+
int
|
|
987
|
+
A bitfield formed by oring UpdateHints values for the things that changed.
|
|
988
|
+
"""
|
|
989
|
+
self._userd.connect_check()
|
|
990
|
+
pb = libuserd_pb2.Reader_dynamic_update_checkRequest()
|
|
991
|
+
pb.changes_allowed = changes_allowed
|
|
992
|
+
try:
|
|
993
|
+
reply = self._userd.stub.Reader_dynamic_update_check(
|
|
994
|
+
pb, metadata=self._userd.metadata()
|
|
995
|
+
)
|
|
996
|
+
except grpc.RpcError as e:
|
|
997
|
+
raise self._userd.libuserd_exception(e)
|
|
998
|
+
return reply.changed
|
|
999
|
+
|
|
1000
|
+
def variable_value(self, variable: "Variable", rank: Optional[int] = None) -> float:
|
|
1001
|
+
"""
|
|
1002
|
+
For any "case" variable (e.g. time), the value of the variable.
|
|
1003
|
+
|
|
1004
|
+
Parameters
|
|
1005
|
+
----------
|
|
1006
|
+
variable
|
|
1007
|
+
The variable to query. Note, this variable location must be on a CASE.
|
|
1008
|
+
rank : int, optional
|
|
1009
|
+
For a dataset using multiple ranks, the rank to return data from. The
|
|
1010
|
+
default is RankValues.SINGLE_RANK.
|
|
1011
|
+
Returns
|
|
1012
|
+
-------
|
|
1013
|
+
float
|
|
1014
|
+
The value of the variable.
|
|
1015
|
+
"""
|
|
1016
|
+
self._userd.connect_check()
|
|
1017
|
+
rank = self._userd.rank_check(rank)
|
|
1018
|
+
pb = libuserd_pb2.Reader_variable_valueRequest()
|
|
1019
|
+
pb.variable_id = variable.id
|
|
1020
|
+
pb.rank = rank
|
|
1021
|
+
try:
|
|
1022
|
+
reply = self._userd.stub.Reader_variable_value(pb, metadata=self._userd.metadata())
|
|
1023
|
+
except grpc.RpcError as e:
|
|
1024
|
+
raise self._userd.libuserd_exception(e)
|
|
1025
|
+
return reply.value
|
|
1026
|
+
|
|
1027
|
+
|
|
1028
|
+
class ReaderInfo(object):
|
|
1029
|
+
"""
|
|
1030
|
+
This class represents an available reader, before it has been instantiated.
|
|
1031
|
+
The read_dataset() function actually tries to open a dataset and returns
|
|
1032
|
+
a `Reader` instance that is reading the data.
|
|
1033
|
+
|
|
1034
|
+
The class contains a list of options that can control/configure the reader.
|
|
1035
|
+
These include "boolean", "option" and "field" options. These include defaults
|
|
1036
|
+
supplied by the reader. To use these, change the value or value_index fields
|
|
1037
|
+
to the desired values before calling `read_dataset`.
|
|
1038
|
+
|
|
1039
|
+
Parameters
|
|
1040
|
+
----------
|
|
1041
|
+
userd
|
|
1042
|
+
The LibUserd instance this query is associated with.
|
|
1043
|
+
pb
|
|
1044
|
+
The protobuffer that represents this object.
|
|
1045
|
+
|
|
1046
|
+
Attributes
|
|
1047
|
+
----------
|
|
1048
|
+
id : int
|
|
1049
|
+
The reader id.
|
|
1050
|
+
name : str
|
|
1051
|
+
The reader name.
|
|
1052
|
+
description : str
|
|
1053
|
+
A brief description of the reader and in some cases its operation.
|
|
1054
|
+
file_label1 : str
|
|
1055
|
+
A string appropriate for a "file select" button for the primary filename.
|
|
1056
|
+
file_label2 : str
|
|
1057
|
+
A string appropriate for a "file select" button for the secondary filename.
|
|
1058
|
+
opt_booleans : List[dict]
|
|
1059
|
+
The boolean user options.
|
|
1060
|
+
opt_options : List[dict]
|
|
1061
|
+
The option user options suitable for display via an option menu.
|
|
1062
|
+
opt_fields : List[dict]
|
|
1063
|
+
The field user options suitable for display via a text field.
|
|
1064
|
+
"""
|
|
1065
|
+
|
|
1066
|
+
def __init__(self, userd: "LibUserd", pb: libuserd_pb2.ReaderInfo):
|
|
1067
|
+
self._userd = userd
|
|
1068
|
+
self.id = pb.id
|
|
1069
|
+
self.name = pb.name
|
|
1070
|
+
self.description = pb.description
|
|
1071
|
+
self.file_label1 = pb.file_label1
|
|
1072
|
+
self.file_label2 = pb.file_label2
|
|
1073
|
+
self.opt_booleans = []
|
|
1074
|
+
for b in pb.options.booleans:
|
|
1075
|
+
self.opt_booleans.append(dict(name=b.name, value=b.value, default=b.default_value))
|
|
1076
|
+
self.opt_options = []
|
|
1077
|
+
for o in pb.options.options:
|
|
1078
|
+
values = []
|
|
1079
|
+
for v in o.values:
|
|
1080
|
+
values.append(v)
|
|
1081
|
+
self.opt_options.append(
|
|
1082
|
+
dict(name=o.name, values=values, value=o.value_index, default=o.default_value_index)
|
|
1083
|
+
)
|
|
1084
|
+
self.opt_fields = []
|
|
1085
|
+
for f in pb.options.fields:
|
|
1086
|
+
self.opt_fields.append(dict(name=f.name, value=f.value, default=f.default_value))
|
|
1087
|
+
|
|
1088
|
+
def read_dataset(self, file1: str, file2: str = "") -> "Reader":
|
|
1089
|
+
"""
|
|
1090
|
+
Attempt to read some files on disk using this reader and the specified options.
|
|
1091
|
+
If successful, return an actual reader instance.
|
|
1092
|
+
|
|
1093
|
+
Parameters
|
|
1094
|
+
----------
|
|
1095
|
+
file1 : str
|
|
1096
|
+
The primary filename (e.g. "foo.cas")
|
|
1097
|
+
file2 : str
|
|
1098
|
+
An optional secondary filename (e.g. "foo.dat")
|
|
1099
|
+
|
|
1100
|
+
Returns
|
|
1101
|
+
-------
|
|
1102
|
+
Reader
|
|
1103
|
+
An instance of the `Reader` class.
|
|
1104
|
+
"""
|
|
1105
|
+
self._userd.connect_check()
|
|
1106
|
+
pb = libuserd_pb2.ReaderInfo_read_datasetRequest()
|
|
1107
|
+
pb.filename_1 = file1
|
|
1108
|
+
if file2:
|
|
1109
|
+
pb.filename_2 = file2
|
|
1110
|
+
pb.reader_id = self.id
|
|
1111
|
+
options = self._get_option_values()
|
|
1112
|
+
for b in options["booleans"]:
|
|
1113
|
+
pb.option_values_bools.append(b)
|
|
1114
|
+
for o in options["options"]:
|
|
1115
|
+
pb.option_values_options.append(o)
|
|
1116
|
+
for f in options["fields"]:
|
|
1117
|
+
pb.option_values_fields.append(f)
|
|
1118
|
+
try:
|
|
1119
|
+
reader = self._userd.stub.ReaderInfo_read_dataset(pb, metadata=self._userd.metadata())
|
|
1120
|
+
except grpc.RpcError as e:
|
|
1121
|
+
raise self._userd.libuserd_exception(e)
|
|
1122
|
+
return Reader(self._userd, reader.reader)
|
|
1123
|
+
|
|
1124
|
+
def _get_option_values(self) -> dict:
|
|
1125
|
+
"""Extract the current option values from the options dictionaries"""
|
|
1126
|
+
out = dict()
|
|
1127
|
+
booleans = []
|
|
1128
|
+
for b in self.opt_booleans:
|
|
1129
|
+
booleans.append(b["value"])
|
|
1130
|
+
out["booleans"] = booleans
|
|
1131
|
+
options = []
|
|
1132
|
+
for o in self.opt_options:
|
|
1133
|
+
options.append(o["value"])
|
|
1134
|
+
out["options"] = options
|
|
1135
|
+
fields = []
|
|
1136
|
+
for f in self.opt_fields:
|
|
1137
|
+
fields.append(f["value"])
|
|
1138
|
+
out["fields"] = fields
|
|
1139
|
+
return out
|
|
1140
|
+
|
|
1141
|
+
def __str__(self) -> str:
|
|
1142
|
+
return f"ReaderInfo id: {self.id}, name: {self.name}, description: {self.description}"
|
|
1143
|
+
|
|
1144
|
+
def __repr__(self):
|
|
1145
|
+
return f"<{self.__class__.__name__} object, id: {self.id}, name: '{self.name}'>"
|
|
1146
|
+
|
|
1147
|
+
|
|
1148
|
+
class LibUserd(object):
|
|
1149
|
+
"""
|
|
1150
|
+
LibUserd is the primary interface to the USERD library. All interaction starts at this object.
|
|
1151
|
+
|
|
1152
|
+
Parameters
|
|
1153
|
+
----------
|
|
1154
|
+
ansys_installation : str
|
|
1155
|
+
Optional location to search for an Ansys software installation.
|
|
1156
|
+
|
|
1157
|
+
Examples
|
|
1158
|
+
--------
|
|
1159
|
+
|
|
1160
|
+
>>> from ansys.pyensight.core import libuserd
|
|
1161
|
+
>>> l = libuserd.LibUserd()
|
|
1162
|
+
>>> l.initialize()
|
|
1163
|
+
>>> readers = l.query_format("D:\\data\\Axial_001.res")
|
|
1164
|
+
>>> data = readers[0].read_dataset("D:\\data\\Axial_001.res")
|
|
1165
|
+
>>> part = data.parts[0]
|
|
1166
|
+
>>> print(part, part.nodes())
|
|
1167
|
+
>>> l.shutdown()
|
|
1168
|
+
|
|
1169
|
+
"""
|
|
1170
|
+
|
|
1171
|
+
def __init__(
|
|
1172
|
+
self,
|
|
1173
|
+
ansys_installation: str = "",
|
|
1174
|
+
use_docker: bool = False,
|
|
1175
|
+
data_directory: Optional[str] = None,
|
|
1176
|
+
docker_image_name: Optional[str] = None,
|
|
1177
|
+
use_dev: bool = False,
|
|
1178
|
+
product_version: Optional[str] = None,
|
|
1179
|
+
channel: Optional[grpc.Channel] = None,
|
|
1180
|
+
pim_instance: Optional[Any] = None,
|
|
1181
|
+
timeout: float = 120.0,
|
|
1182
|
+
pull_image_if_not_available: bool = False,
|
|
1183
|
+
number_of_ranks: int = 1,
|
|
1184
|
+
):
|
|
1185
|
+
self._server_pathname: Optional[str] = None
|
|
1186
|
+
self._host = "127.0.0.1"
|
|
1187
|
+
self._security_token = str(uuid.uuid1())
|
|
1188
|
+
self._grpc_port = 0
|
|
1189
|
+
self._server_process: Optional[subprocess.Popen] = None
|
|
1190
|
+
self._channel: Optional[grpc.Channel] = None
|
|
1191
|
+
self._stub = None
|
|
1192
|
+
self._security_file: Optional[str] = None
|
|
1193
|
+
# Docker attributes
|
|
1194
|
+
self._pull_image = pull_image_if_not_available
|
|
1195
|
+
self._timeout = timeout
|
|
1196
|
+
self._product_version = product_version
|
|
1197
|
+
self._data_directory = data_directory
|
|
1198
|
+
self._image_name = "ghcr.io/ansys-internal/ensight"
|
|
1199
|
+
if use_dev:
|
|
1200
|
+
self._image_name = "ghcr.io/ansys-internal/ensight_dev"
|
|
1201
|
+
if docker_image_name:
|
|
1202
|
+
self._image_name = docker_image_name
|
|
1203
|
+
self._docker_client: Optional["DockerClient"] = None
|
|
1204
|
+
self._container: Optional["Container"] = None
|
|
1205
|
+
self._enshell: Optional["EnShellGRPC"] = None
|
|
1206
|
+
self._pim_instance = pim_instance
|
|
1207
|
+
self._enshell_grpc_channel: Optional[grpc.Channel] = channel
|
|
1208
|
+
self._pim_file_service: Optional[Any] = None
|
|
1209
|
+
self._service_host_port: Dict[str, Tuple[str, int]] = {}
|
|
1210
|
+
self._number_of_ranks = number_of_ranks
|
|
1211
|
+
local_launch = True
|
|
1212
|
+
if any([use_docker, use_dev, self._pim_instance]):
|
|
1213
|
+
local_launch = False
|
|
1214
|
+
self._launch_enshell()
|
|
1215
|
+
else:
|
|
1216
|
+
# find the pathname to the server
|
|
1217
|
+
self._server_pathname = self._find_ensight_server_name(
|
|
1218
|
+
ansys_installation=ansys_installation
|
|
1219
|
+
)
|
|
1220
|
+
if self._server_pathname is None:
|
|
1221
|
+
raise RuntimeError("Unable to detect an EnSight server installation.")
|
|
1222
|
+
# enums
|
|
1223
|
+
self._build_enums()
|
|
1224
|
+
if local_launch:
|
|
1225
|
+
self._local_launch()
|
|
1226
|
+
# Build the gRPC connection
|
|
1227
|
+
self._connect()
|
|
1228
|
+
|
|
1229
|
+
def _local_launch(self) -> None:
|
|
1230
|
+
"""Launch the gRPC server from a local installation."""
|
|
1231
|
+
# have the server save status so we can read it later
|
|
1232
|
+
with tempfile.TemporaryDirectory() as tmpdirname:
|
|
1233
|
+
self._security_file = os.path.join(tmpdirname, "security.grpc")
|
|
1234
|
+
|
|
1235
|
+
# Build the command line
|
|
1236
|
+
cmd = [str(self.server_pathname)]
|
|
1237
|
+
cmd.extend(["-grpc_server", str(self.grpc_port)])
|
|
1238
|
+
cmd.extend(["-security_file", self._security_file])
|
|
1239
|
+
env_vars = os.environ.copy()
|
|
1240
|
+
if self.security_token:
|
|
1241
|
+
env_vars["ENSIGHT_SECURITY_TOKEN"] = self.security_token
|
|
1242
|
+
env_vars["ENSIGHT_GRPC_SECURITY_FILE"] = self._security_file
|
|
1243
|
+
# start the server
|
|
1244
|
+
try:
|
|
1245
|
+
self._server_process = subprocess.Popen(
|
|
1246
|
+
cmd,
|
|
1247
|
+
close_fds=True,
|
|
1248
|
+
env=env_vars,
|
|
1249
|
+
stderr=subprocess.DEVNULL,
|
|
1250
|
+
stdout=subprocess.DEVNULL,
|
|
1251
|
+
)
|
|
1252
|
+
except Exception as error:
|
|
1253
|
+
raise error
|
|
1254
|
+
|
|
1255
|
+
start_time = time.time()
|
|
1256
|
+
while (self._grpc_port == 0) and (time.time() - start_time < 120.0):
|
|
1257
|
+
try:
|
|
1258
|
+
# Read the port and security token from the security file
|
|
1259
|
+
with open(self._security_file, "r") as f:
|
|
1260
|
+
for line in f:
|
|
1261
|
+
line = line.strip()
|
|
1262
|
+
if line.startswith("grpc_port:"):
|
|
1263
|
+
self._grpc_port = int(line[len("grpc_port:") :])
|
|
1264
|
+
elif line.startswith("grpc_password:"):
|
|
1265
|
+
self._security_token = line[len("grpc_password:") :]
|
|
1266
|
+
except (OSError, IOError):
|
|
1267
|
+
pass
|
|
1268
|
+
|
|
1269
|
+
# Unable to get the grpc port/password
|
|
1270
|
+
if self._grpc_port == 0:
|
|
1271
|
+
self.shutdown()
|
|
1272
|
+
raise RuntimeError(f"Unable to start the gRPC server ({str(self.server_pathname)})")
|
|
1273
|
+
|
|
1274
|
+
def _build_enums(self) -> None:
|
|
1275
|
+
# retained for backward compatibility
|
|
1276
|
+
self.ErrorCodes = ErrorCodes
|
|
1277
|
+
self.ElementType = ElementType
|
|
1278
|
+
self.VariableLocation = VariableLocation
|
|
1279
|
+
self.VariableType = VariableType
|
|
1280
|
+
self.PartHints = PartHints
|
|
1281
|
+
self.UpdateHints = UpdateHints
|
|
1282
|
+
self.RankValues = RankValues
|
|
1283
|
+
|
|
1284
|
+
def _pull_docker_image(self) -> None:
|
|
1285
|
+
"""Pull the docker image if not available"""
|
|
1286
|
+
pull_image(self._docker_client, self._image_name)
|
|
1287
|
+
|
|
1288
|
+
def _check_if_image_available(self) -> bool:
|
|
1289
|
+
"""Check if the input docker image is available."""
|
|
1290
|
+
if not self._docker_client:
|
|
1291
|
+
return False
|
|
1292
|
+
filtered_images = self._docker_client.images.list(filters={"reference": self._image_name})
|
|
1293
|
+
if len(filtered_images) > 0:
|
|
1294
|
+
return True
|
|
1295
|
+
return False
|
|
1296
|
+
|
|
1297
|
+
def _launch_enshell(self) -> None:
|
|
1298
|
+
"""Create an enshell entry point and use it to launch a Container."""
|
|
1299
|
+
if self._pim_instance:
|
|
1300
|
+
self._service_host_port = populate_service_host_port(self._pim_instance, {})
|
|
1301
|
+
self._pim_file_service = get_file_service(self._pim_instance)
|
|
1302
|
+
self._grpc_port = int(self._service_host_port["grpc_private"][1])
|
|
1303
|
+
self._host = self._service_host_port["grpc_private"][0]
|
|
1304
|
+
else:
|
|
1305
|
+
if not self._data_directory:
|
|
1306
|
+
self._data_directory = tempfile.mkdtemp(prefix="pyensight_")
|
|
1307
|
+
available = self._check_if_image_available()
|
|
1308
|
+
if not available and self._pull_image and not self._pim_instance:
|
|
1309
|
+
self._pull_docker_image()
|
|
1310
|
+
ports = find_unused_ports(2, avoid=[1999])
|
|
1311
|
+
self._service_host_port = {
|
|
1312
|
+
"grpc": ("127.0.0.1", ports[0]),
|
|
1313
|
+
"grpc_private": ("127.0.0.1", ports[1]),
|
|
1314
|
+
}
|
|
1315
|
+
self._grpc_port = ports[1]
|
|
1316
|
+
if not self._pim_instance:
|
|
1317
|
+
self._launch_container()
|
|
1318
|
+
self._enshell = launch_enshell_interface(
|
|
1319
|
+
self._enshell_grpc_channel, self._service_host_port["grpc"][1], self._timeout
|
|
1320
|
+
)
|
|
1321
|
+
self._cei_home = self._enshell.cei_home()
|
|
1322
|
+
self._ansys_version = self._enshell.ansys_version()
|
|
1323
|
+
# print("CEI_HOME=", self._cei_home)
|
|
1324
|
+
# print("Ansys Version=", self._ansys_version)
|
|
1325
|
+
grpc_port = self._service_host_port["grpc_private"][1]
|
|
1326
|
+
ensight_args = f"-grpc_server {grpc_port}"
|
|
1327
|
+
container_env_str = f"ENSIGHT_SECURITY_TOKEN={self._security_token}\n"
|
|
1328
|
+
ret = self._enshell.start_ensight_server(ensight_args, container_env_str)
|
|
1329
|
+
if ret[0] != 0: # pragma: no cover
|
|
1330
|
+
self._stop_container_and_enshell() # pragma: no cover
|
|
1331
|
+
raise RuntimeError(
|
|
1332
|
+
f"Error starting EnSight Server with args: {ensight_args}"
|
|
1333
|
+
) # pragma: no cover
|
|
1334
|
+
|
|
1335
|
+
def _launch_container(self) -> None:
|
|
1336
|
+
"""Launch a docker container for the input image."""
|
|
1337
|
+
self._docker_client = docker.from_env()
|
|
1338
|
+
grpc_port = self._service_host_port["grpc"][1]
|
|
1339
|
+
private_grpc_port = self._service_host_port["grpc_private"][1]
|
|
1340
|
+
ports_to_map = {
|
|
1341
|
+
str(self._service_host_port["grpc"][1]) + "/tcp": str(grpc_port),
|
|
1342
|
+
str(self._service_host_port["grpc_private"][1]) + "/tcp": str(private_grpc_port),
|
|
1343
|
+
}
|
|
1344
|
+
enshell_cmd = "-app -v 3 -grpc_server " + str(grpc_port)
|
|
1345
|
+
container_env = {
|
|
1346
|
+
"ENSIGHT_SECURITY_TOKEN": self.security_token,
|
|
1347
|
+
}
|
|
1348
|
+
data_volume = {self._data_directory: {"bind": "/data", "mode": "rw"}}
|
|
1349
|
+
|
|
1350
|
+
if not self._docker_client:
|
|
1351
|
+
raise RuntimeError("Could not startup docker.")
|
|
1352
|
+
self._container = self._docker_client.containers.run( # pragma: no cover
|
|
1353
|
+
self._image_name,
|
|
1354
|
+
command=enshell_cmd,
|
|
1355
|
+
volumes=data_volume,
|
|
1356
|
+
environment=container_env,
|
|
1357
|
+
ports=ports_to_map,
|
|
1358
|
+
tty=True,
|
|
1359
|
+
detach=True,
|
|
1360
|
+
auto_remove=True,
|
|
1361
|
+
remove=True,
|
|
1362
|
+
)
|
|
1363
|
+
|
|
1364
|
+
def _stop_container_and_enshell(self) -> None:
|
|
1365
|
+
"""Release any additional resources allocated during launching."""
|
|
1366
|
+
if self._enshell:
|
|
1367
|
+
if self._enshell.is_connected(): # pragma: no cover
|
|
1368
|
+
try:
|
|
1369
|
+
logging.debug("Stopping EnShell.\n")
|
|
1370
|
+
self._enshell.stop_server()
|
|
1371
|
+
except Exception: # pragma: no cover
|
|
1372
|
+
pass # pragma: no cover
|
|
1373
|
+
self._enshell = None
|
|
1374
|
+
if self._container:
|
|
1375
|
+
try:
|
|
1376
|
+
logging.debug("Stopping the Docker Container.\n")
|
|
1377
|
+
self._container.stop()
|
|
1378
|
+
except Exception:
|
|
1379
|
+
pass
|
|
1380
|
+
try:
|
|
1381
|
+
logging.debug("Removing the Docker Container.\n")
|
|
1382
|
+
self._container.remove()
|
|
1383
|
+
except Exception:
|
|
1384
|
+
pass
|
|
1385
|
+
self._container = None
|
|
1386
|
+
|
|
1387
|
+
if self._pim_instance is not None:
|
|
1388
|
+
logging.debug("Deleting the PIM instance.\n")
|
|
1389
|
+
self._pim_instance.delete()
|
|
1390
|
+
self._pim_instance = None
|
|
1391
|
+
|
|
1392
|
+
@property
|
|
1393
|
+
def stub(self):
|
|
1394
|
+
"""A libuserd_pb2_grpc.LibUSERDServiceStub instance bound to a gRPC connection channel"""
|
|
1395
|
+
return self._stub
|
|
1396
|
+
|
|
1397
|
+
@property
|
|
1398
|
+
def server_pathname(self) -> Optional[str]:
|
|
1399
|
+
"""The pathanme of the detected EnSight server executable used as the gRPC server"""
|
|
1400
|
+
return self._server_pathname
|
|
1401
|
+
|
|
1402
|
+
@property
|
|
1403
|
+
def security_token(self) -> str:
|
|
1404
|
+
"""The current gRPC security token"""
|
|
1405
|
+
return self._security_token
|
|
1406
|
+
|
|
1407
|
+
@property
|
|
1408
|
+
def grpc_port(self) -> int:
|
|
1409
|
+
"""The current gRPC port"""
|
|
1410
|
+
return self._grpc_port
|
|
1411
|
+
|
|
1412
|
+
def __del__(self) -> None:
|
|
1413
|
+
self.shutdown()
|
|
1414
|
+
|
|
1415
|
+
@staticmethod
|
|
1416
|
+
def _find_ensight_server_name(ansys_installation: str = "") -> Optional[str]:
|
|
1417
|
+
"""
|
|
1418
|
+
Parameters
|
|
1419
|
+
----------
|
|
1420
|
+
ansys_installation : str
|
|
1421
|
+
Path to the local Ansys installation, including the version
|
|
1422
|
+
directory. The default is ``None``, in which case common locations
|
|
1423
|
+
are scanned to detect the latest local Ansys installation. The
|
|
1424
|
+
``PYENSIGHT_ANSYS_INSTALLATION`` environmental variable is checked first.
|
|
1425
|
+
|
|
1426
|
+
Returns
|
|
1427
|
+
-------
|
|
1428
|
+
str
|
|
1429
|
+
The first valid ensight_server found or None
|
|
1430
|
+
|
|
1431
|
+
"""
|
|
1432
|
+
dirs_to_check = []
|
|
1433
|
+
if ansys_installation:
|
|
1434
|
+
dirs_to_check.append(ansys_installation)
|
|
1435
|
+
|
|
1436
|
+
if "PYENSIGHT_ANSYS_INSTALLATION" in os.environ:
|
|
1437
|
+
env_inst = os.environ["PYENSIGHT_ANSYS_INSTALLATION"]
|
|
1438
|
+
dirs_to_check.append(env_inst)
|
|
1439
|
+
# Note: PYENSIGHT_ANSYS_INSTALLATION is designed for devel builds
|
|
1440
|
+
# where there is no CEI directory, but for folks using it in other
|
|
1441
|
+
# ways, we'll add that one too, just in case.
|
|
1442
|
+
dirs_to_check.append(os.path.join(env_inst, "CEI"))
|
|
1443
|
+
|
|
1444
|
+
try:
|
|
1445
|
+
import enve
|
|
1446
|
+
|
|
1447
|
+
dirs_to_check.append(enve.home())
|
|
1448
|
+
except ModuleNotFoundError:
|
|
1449
|
+
pass
|
|
1450
|
+
|
|
1451
|
+
if "CEI_HOME" in os.environ:
|
|
1452
|
+
env_inst = os.environ["CEI_HOME"]
|
|
1453
|
+
dirs_to_check.append(env_inst)
|
|
1454
|
+
|
|
1455
|
+
# Look for most recent Ansys install
|
|
1456
|
+
awp_roots = []
|
|
1457
|
+
for env_name in dict(os.environ).keys():
|
|
1458
|
+
if env_name.startswith("AWP_ROOT"):
|
|
1459
|
+
try:
|
|
1460
|
+
version = int(env_name[len("AWP_ROOT") :])
|
|
1461
|
+
# this API is new in 2025 R1 distributions
|
|
1462
|
+
if version >= 251:
|
|
1463
|
+
awp_roots.append(env_name)
|
|
1464
|
+
except ValueError:
|
|
1465
|
+
pass
|
|
1466
|
+
awp_roots.sort(reverse=True)
|
|
1467
|
+
for env_name in awp_roots:
|
|
1468
|
+
dirs_to_check.append(os.path.join(os.environ[env_name], "CEI"))
|
|
1469
|
+
|
|
1470
|
+
# check all the collected locations in order
|
|
1471
|
+
app_name = "ensight_server"
|
|
1472
|
+
if platform.system() == "Windows":
|
|
1473
|
+
app_name += ".bat"
|
|
1474
|
+
for install_dir in dirs_to_check:
|
|
1475
|
+
launch_file = os.path.join(install_dir, "bin", app_name)
|
|
1476
|
+
if os.path.isfile(launch_file):
|
|
1477
|
+
return launch_file
|
|
1478
|
+
return None
|
|
1479
|
+
|
|
1480
|
+
def _is_connected(self) -> bool:
|
|
1481
|
+
"""Check to see if the gRPC connection is live
|
|
1482
|
+
|
|
1483
|
+
Returns
|
|
1484
|
+
-------
|
|
1485
|
+
bool
|
|
1486
|
+
True if the connection is active.
|
|
1487
|
+
"""
|
|
1488
|
+
return self._channel is not None
|
|
1489
|
+
|
|
1490
|
+
def _connect(self) -> None:
|
|
1491
|
+
"""Establish the gRPC connection to EnSight
|
|
1492
|
+
|
|
1493
|
+
Attempt to connect to an EnSight gRPC server using the host and port
|
|
1494
|
+
established by the constructor. Note on failure, this function just
|
|
1495
|
+
returns, but is_connected() will return False.
|
|
1496
|
+
|
|
1497
|
+
Parameters
|
|
1498
|
+
----------
|
|
1499
|
+
timeout: float
|
|
1500
|
+
how long to wait for the connection to timeout
|
|
1501
|
+
"""
|
|
1502
|
+
if self._is_connected():
|
|
1503
|
+
return
|
|
1504
|
+
# set up the channel
|
|
1505
|
+
|
|
1506
|
+
self._channel = grpc.insecure_channel(
|
|
1507
|
+
"{}:{}".format(self._host, self._grpc_port),
|
|
1508
|
+
options=[
|
|
1509
|
+
("grpc.max_receive_message_length", -1),
|
|
1510
|
+
("grpc.max_send_message_length", -1),
|
|
1511
|
+
("grpc.testing.fixed_reconnect_backoff_ms", 1100),
|
|
1512
|
+
],
|
|
1513
|
+
)
|
|
1514
|
+
try:
|
|
1515
|
+
grpc.channel_ready_future(self._channel).result(timeout=self._timeout)
|
|
1516
|
+
except grpc.FutureTimeoutError: # pragma: no cover
|
|
1517
|
+
self._channel = None # pragma: no cover
|
|
1518
|
+
return # pragma: no cover
|
|
1519
|
+
# hook up the stub interface
|
|
1520
|
+
self._stub = libuserd_pb2_grpc.LibUSERDServiceStub(self._channel)
|
|
1521
|
+
|
|
1522
|
+
def metadata(self) -> List[Tuple[bytes, Union[str, bytes]]]:
|
|
1523
|
+
"""Compute the gRPC stream metadata
|
|
1524
|
+
|
|
1525
|
+
Compute the list to be passed to the gRPC calls for things like security
|
|
1526
|
+
and the session name.
|
|
1527
|
+
|
|
1528
|
+
Returns
|
|
1529
|
+
-------
|
|
1530
|
+
List[Tuple[bytes, Union[str, bytes]]]
|
|
1531
|
+
A list object of the metadata elements needed in a gRPC call to
|
|
1532
|
+
satisfy the EnSight server gRPC requirements.
|
|
1533
|
+
"""
|
|
1534
|
+
ret: List[Tuple[bytes, Union[str, bytes]]] = list()
|
|
1535
|
+
s: Union[str, bytes]
|
|
1536
|
+
if self._security_token: # pragma: no cover
|
|
1537
|
+
s = self._security_token
|
|
1538
|
+
if type(s) == str: # pragma: no cover
|
|
1539
|
+
s = s.encode("utf-8")
|
|
1540
|
+
ret.append((b"shared_secret", s))
|
|
1541
|
+
return ret
|
|
1542
|
+
|
|
1543
|
+
def libuserd_exception(self, e: "grpc.RpcError") -> Exception:
|
|
1544
|
+
"""
|
|
1545
|
+
Given an exception raised as the result of a gRPC call, return either
|
|
1546
|
+
the input exception or a LibUserdError exception object to differentiate
|
|
1547
|
+
between gRPC issues and libuserd issues.
|
|
1548
|
+
|
|
1549
|
+
Parameters
|
|
1550
|
+
----------
|
|
1551
|
+
e
|
|
1552
|
+
The exception raised by a gRPC call.
|
|
1553
|
+
|
|
1554
|
+
Returns
|
|
1555
|
+
-------
|
|
1556
|
+
Exception
|
|
1557
|
+
Either the original exception or a LibUserdError exception instance, depending on
|
|
1558
|
+
the original exception message details.
|
|
1559
|
+
"""
|
|
1560
|
+
msg = e.details()
|
|
1561
|
+
if msg.startswith("LibUserd("):
|
|
1562
|
+
return LibUserdError(msg)
|
|
1563
|
+
return e
|
|
1564
|
+
|
|
1565
|
+
def _disconnect(self, no_error: bool = False) -> None:
|
|
1566
|
+
"""Close down the gRPC connection
|
|
1567
|
+
|
|
1568
|
+
Disconnect all connections to the gRPC server. Send the shutdown request gRPC command
|
|
1569
|
+
to the server first.
|
|
1570
|
+
|
|
1571
|
+
Parameters
|
|
1572
|
+
----------
|
|
1573
|
+
no_error
|
|
1574
|
+
If true, ignore errors resulting from the shutdown operation.
|
|
1575
|
+
"""
|
|
1576
|
+
if not self._is_connected(): # pragma: no cover
|
|
1577
|
+
return
|
|
1578
|
+
# Note: this is expected to return an error
|
|
1579
|
+
try:
|
|
1580
|
+
pb = libuserd_pb2.Libuserd_shutdownRequest()
|
|
1581
|
+
self._stub.Libuserd_shutdown(pb, metadata=self.metadata()) # type: ignore
|
|
1582
|
+
if self._channel:
|
|
1583
|
+
self._channel.close()
|
|
1584
|
+
except grpc.RpcError as e:
|
|
1585
|
+
if not no_error:
|
|
1586
|
+
raise self.libuserd_exception(e)
|
|
1587
|
+
finally:
|
|
1588
|
+
# clean up control objects
|
|
1589
|
+
self._stub = None
|
|
1590
|
+
self._channel = None
|
|
1591
|
+
|
|
1592
|
+
def connect_check(self) -> None:
|
|
1593
|
+
"""
|
|
1594
|
+
Verify that there is an active gRPC connection established. If not raise
|
|
1595
|
+
a RuntimeError
|
|
1596
|
+
|
|
1597
|
+
Raises
|
|
1598
|
+
------
|
|
1599
|
+
RuntimeError
|
|
1600
|
+
If there is no active connection.
|
|
1601
|
+
"""
|
|
1602
|
+
if not self._is_connected():
|
|
1603
|
+
raise RuntimeError("gRPC connection not established")
|
|
1604
|
+
|
|
1605
|
+
def rank_check(self, rank: Optional[int]) -> int:
|
|
1606
|
+
"""
|
|
1607
|
+
Validate the specified rank number. If the rank is None, return 0.
|
|
1608
|
+
|
|
1609
|
+
Parameters
|
|
1610
|
+
----------
|
|
1611
|
+
rank: Optional[int]
|
|
1612
|
+
The rank number to validate.
|
|
1613
|
+
|
|
1614
|
+
Returns
|
|
1615
|
+
-------
|
|
1616
|
+
int
|
|
1617
|
+
The validated rank number.
|
|
1618
|
+
"""
|
|
1619
|
+
if rank is None:
|
|
1620
|
+
return 0
|
|
1621
|
+
if int(rank) < 0 or int(rank) >= self._number_of_ranks:
|
|
1622
|
+
raise RuntimeError(
|
|
1623
|
+
f"Invalid rank number specified: {rank}. Valid range:[0, {self._number_of_ranks-1}]"
|
|
1624
|
+
)
|
|
1625
|
+
return int(rank)
|
|
1626
|
+
|
|
1627
|
+
"""
|
|
1628
|
+
gRPC method bindings
|
|
1629
|
+
"""
|
|
1630
|
+
|
|
1631
|
+
def shutdown(self) -> None:
|
|
1632
|
+
"""
|
|
1633
|
+
Close any active gRPC connection and shut down the EnSight server.
|
|
1634
|
+
The object is no longer usable.
|
|
1635
|
+
"""
|
|
1636
|
+
self._disconnect(no_error=True)
|
|
1637
|
+
# Just in case, we will try to kill the server directly as well
|
|
1638
|
+
if self._server_process:
|
|
1639
|
+
if psutil.pid_exists(self._server_process.pid):
|
|
1640
|
+
proc = psutil.Process(self._server_process.pid)
|
|
1641
|
+
for child in proc.children(recursive=True):
|
|
1642
|
+
if psutil.pid_exists(child.pid):
|
|
1643
|
+
# This can be a race condition, so it is ok if the child is dead already
|
|
1644
|
+
try:
|
|
1645
|
+
child.kill()
|
|
1646
|
+
except psutil.NoSuchProcess:
|
|
1647
|
+
pass
|
|
1648
|
+
# Same issue, this process might already be shutting down, so NoSuchProcess is ok.
|
|
1649
|
+
try:
|
|
1650
|
+
proc.kill()
|
|
1651
|
+
except psutil.NoSuchProcess:
|
|
1652
|
+
pass
|
|
1653
|
+
if self._container:
|
|
1654
|
+
self._stop_container_and_enshell()
|
|
1655
|
+
self._server_process = None
|
|
1656
|
+
|
|
1657
|
+
def ansys_release_string(self) -> str:
|
|
1658
|
+
"""
|
|
1659
|
+
Return the Ansys release for the library.
|
|
1660
|
+
|
|
1661
|
+
Returns
|
|
1662
|
+
-------
|
|
1663
|
+
str
|
|
1664
|
+
Return a string like "2025 R1"
|
|
1665
|
+
"""
|
|
1666
|
+
self.connect_check()
|
|
1667
|
+
pb = libuserd_pb2.Libuserd_ansys_release_stringRequest()
|
|
1668
|
+
try:
|
|
1669
|
+
ret = self.stub.Libuserd_ansys_release_string(pb, metadata=self.metadata())
|
|
1670
|
+
except grpc.RpcError as e:
|
|
1671
|
+
raise self.libuserd_exception(e)
|
|
1672
|
+
return ret.result
|
|
1673
|
+
|
|
1674
|
+
def ansys_release_number(self) -> int:
|
|
1675
|
+
"""
|
|
1676
|
+
Return the Ansys release number of the library.
|
|
1677
|
+
|
|
1678
|
+
Returns
|
|
1679
|
+
-------
|
|
1680
|
+
int
|
|
1681
|
+
A version number like 251 (for "2025 R1")
|
|
1682
|
+
"""
|
|
1683
|
+
self.connect_check()
|
|
1684
|
+
pb = libuserd_pb2.Libuserd_ansys_release_numberRequest()
|
|
1685
|
+
try:
|
|
1686
|
+
ret = self.stub.Libuserd_ansys_release_number(pb, metadata=self.metadata())
|
|
1687
|
+
except grpc.RpcError as e:
|
|
1688
|
+
raise self.libuserd_exception(e)
|
|
1689
|
+
return ret.result
|
|
1690
|
+
|
|
1691
|
+
def library_version(self) -> str:
|
|
1692
|
+
"""
|
|
1693
|
+
The library version number. This string is the version of the
|
|
1694
|
+
library interface itself. This is not the same as the version
|
|
1695
|
+
number of the Ansys release that corresponds to the library.
|
|
1696
|
+
|
|
1697
|
+
This number follows semantic versioning rules: "1.0.0" or
|
|
1698
|
+
"0.4.3-rc.1" would be examples of valid library_version() strings.
|
|
1699
|
+
|
|
1700
|
+
Returns
|
|
1701
|
+
-------
|
|
1702
|
+
str
|
|
1703
|
+
The library interface version number string.
|
|
1704
|
+
"""
|
|
1705
|
+
self.connect_check()
|
|
1706
|
+
pb = libuserd_pb2.Libuserd_library_versionRequest()
|
|
1707
|
+
try:
|
|
1708
|
+
ret = self.stub.Libuserd_library_version(pb, metadata=self.metadata())
|
|
1709
|
+
except grpc.RpcError as e:
|
|
1710
|
+
raise self.libuserd_exception(e)
|
|
1711
|
+
return ret.result
|
|
1712
|
+
|
|
1713
|
+
def nodes_per_element(self, element_type: int) -> int:
|
|
1714
|
+
"""
|
|
1715
|
+
For a given element type (e.g. HEX20), return the number of nodes used by the element.
|
|
1716
|
+
Note, this is not supported for NSIDED and NFACED element types.
|
|
1717
|
+
|
|
1718
|
+
Parameters
|
|
1719
|
+
----------
|
|
1720
|
+
element_type : int
|
|
1721
|
+
The element type: ElementType enum value
|
|
1722
|
+
|
|
1723
|
+
Returns
|
|
1724
|
+
-------
|
|
1725
|
+
int
|
|
1726
|
+
Number of nodes per element or 0 if elem_type is not a valid zoo element type.
|
|
1727
|
+
"""
|
|
1728
|
+
self.connect_check()
|
|
1729
|
+
pb = libuserd_pb2.Libuserd_nodes_per_elementRequest()
|
|
1730
|
+
pb.elemType = element_type
|
|
1731
|
+
try:
|
|
1732
|
+
ret = self.stub.Libuserd_nodes_per_element(pb, metadata=self.metadata())
|
|
1733
|
+
except grpc.RpcError as e:
|
|
1734
|
+
raise self.libuserd_exception(e)
|
|
1735
|
+
return ret.result
|
|
1736
|
+
|
|
1737
|
+
def element_is_ghost(self, element_type: int) -> bool:
|
|
1738
|
+
"""
|
|
1739
|
+
|
|
1740
|
+
For a given element type (e.g. HEX20), determine if the element type should be considered
|
|
1741
|
+
a "ghost" element.
|
|
1742
|
+
|
|
1743
|
+
Parameters
|
|
1744
|
+
----------
|
|
1745
|
+
element_type : int
|
|
1746
|
+
The element type: ElementType enum value
|
|
1747
|
+
|
|
1748
|
+
Returns
|
|
1749
|
+
-------
|
|
1750
|
+
bool
|
|
1751
|
+
True if the element is a ghost (or an invalid element type).
|
|
1752
|
+
"""
|
|
1753
|
+
self.connect_check()
|
|
1754
|
+
pb = libuserd_pb2.Libuserd_element_is_ghostRequest()
|
|
1755
|
+
pb.elemType = element_type
|
|
1756
|
+
try:
|
|
1757
|
+
ret = self.stub.Libuserd_element_is_ghost(pb, metadata=self.metadata())
|
|
1758
|
+
except grpc.RpcError as e:
|
|
1759
|
+
raise self.libuserd_exception(e)
|
|
1760
|
+
return ret.result
|
|
1761
|
+
|
|
1762
|
+
def element_is_zoo(self, element_type: int) -> bool:
|
|
1763
|
+
"""
|
|
1764
|
+
For a given element type (e.g. HEX20), determine if the element type is zoo or not
|
|
1765
|
+
|
|
1766
|
+
Parameters
|
|
1767
|
+
----------
|
|
1768
|
+
element_type : int
|
|
1769
|
+
The element type: ElementType enum value
|
|
1770
|
+
|
|
1771
|
+
Returns
|
|
1772
|
+
-------
|
|
1773
|
+
bool
|
|
1774
|
+
True if the element is a zoo element and false if it is NSIDED or NFACED.
|
|
1775
|
+
"""
|
|
1776
|
+
self.connect_check()
|
|
1777
|
+
pb = libuserd_pb2.Libuserd_element_is_zooRequest()
|
|
1778
|
+
pb.elemType = element_type
|
|
1779
|
+
try:
|
|
1780
|
+
ret = self.stub.Libuserd_element_is_zoo(pb, metadata=self.metadata())
|
|
1781
|
+
except grpc.RpcError as e:
|
|
1782
|
+
raise self.libuserd_exception(e)
|
|
1783
|
+
return ret.result
|
|
1784
|
+
|
|
1785
|
+
def element_is_nsided(self, element_type: int) -> bool:
|
|
1786
|
+
"""
|
|
1787
|
+
For a given element type, determine if the element type is n-sided or not
|
|
1788
|
+
|
|
1789
|
+
Parameters
|
|
1790
|
+
----------
|
|
1791
|
+
element_type : int
|
|
1792
|
+
The element type: ElementType enum value
|
|
1793
|
+
|
|
1794
|
+
Returns
|
|
1795
|
+
-------
|
|
1796
|
+
bool
|
|
1797
|
+
True if the element is a NSIDED or NSIDED_GHOST and False otherwise.
|
|
1798
|
+
"""
|
|
1799
|
+
self.connect_check()
|
|
1800
|
+
pb = libuserd_pb2.Libuserd_element_is_nsidedRequest()
|
|
1801
|
+
pb.elemType = element_type
|
|
1802
|
+
try:
|
|
1803
|
+
ret = self.stub.Libuserd_element_is_nsided(pb, metadata=self.metadata())
|
|
1804
|
+
except grpc.RpcError as e:
|
|
1805
|
+
raise self.libuserd_exception(e)
|
|
1806
|
+
return ret.result
|
|
1807
|
+
|
|
1808
|
+
def element_is_nfaced(self, element_type: int) -> bool:
|
|
1809
|
+
"""
|
|
1810
|
+
For a given element type, determine if the element type is n-faced or not
|
|
1811
|
+
|
|
1812
|
+
Parameters
|
|
1813
|
+
----------
|
|
1814
|
+
element_type : int
|
|
1815
|
+
The element type: ElementType enum value
|
|
1816
|
+
|
|
1817
|
+
Returns
|
|
1818
|
+
-------
|
|
1819
|
+
bool
|
|
1820
|
+
True if the element is a NFACED or NFACED_GHOST and False otherwise.
|
|
1821
|
+
"""
|
|
1822
|
+
self.connect_check()
|
|
1823
|
+
pb = libuserd_pb2.Libuserd_element_is_nfacedRequest()
|
|
1824
|
+
pb.elemType = element_type
|
|
1825
|
+
try:
|
|
1826
|
+
ret = self.stub.Libuserd_element_is_nfaced(pb, metadata=self.metadata())
|
|
1827
|
+
except grpc.RpcError as e:
|
|
1828
|
+
raise self.libuserd_exception(e)
|
|
1829
|
+
return ret.result
|
|
1830
|
+
|
|
1831
|
+
def number_of_simple_element_types(self) -> int:
|
|
1832
|
+
"""
|
|
1833
|
+
There is a consecutive range of element type enums that are supported by the
|
|
1834
|
+
Part.element_conn() method. This function returns the number of those elements
|
|
1835
|
+
and may be useful in common element type handling code.
|
|
1836
|
+
|
|
1837
|
+
Note: The value is effectively int(ElementType.NSIDED).
|
|
1838
|
+
|
|
1839
|
+
Returns
|
|
1840
|
+
-------
|
|
1841
|
+
int
|
|
1842
|
+
The number of zoo element types.
|
|
1843
|
+
"""
|
|
1844
|
+
self.connect_check()
|
|
1845
|
+
pb = libuserd_pb2.Libuserd_number_of_simple_element_typesRequest()
|
|
1846
|
+
try:
|
|
1847
|
+
ret = self.stub.Libuserd_number_of_simple_element_types(pb, metadata=self.metadata())
|
|
1848
|
+
except grpc.RpcError as e:
|
|
1849
|
+
raise self.libuserd_exception(e)
|
|
1850
|
+
return ret.result
|
|
1851
|
+
|
|
1852
|
+
def initialize(self, number_of_ranks: int = 0) -> None:
|
|
1853
|
+
"""
|
|
1854
|
+
This call initializes the libuserd system. It causes the library to scan for available
|
|
1855
|
+
readers and set up any required reduction engine bits. It can only be called once.
|
|
1856
|
+
|
|
1857
|
+
Parameters
|
|
1858
|
+
----------
|
|
1859
|
+
number_of_ranks : int, optional
|
|
1860
|
+
The degree of I/O parallelism to read data with. Zero is serial I/O. Note: this
|
|
1861
|
+
option is not currently implemented and 0 should be used.
|
|
1862
|
+
"""
|
|
1863
|
+
self.connect_check()
|
|
1864
|
+
pb = libuserd_pb2.Libuserd_initializeRequest()
|
|
1865
|
+
if number_of_ranks:
|
|
1866
|
+
self._number_of_ranks = number_of_ranks
|
|
1867
|
+
try:
|
|
1868
|
+
pb.parallel_node_count = number_of_ranks
|
|
1869
|
+
except Exception:
|
|
1870
|
+
# This exception is allowed to support older .proto versions
|
|
1871
|
+
pass
|
|
1872
|
+
try:
|
|
1873
|
+
_ = self.stub.Libuserd_initialize(pb, metadata=self.metadata())
|
|
1874
|
+
except grpc.RpcError as e:
|
|
1875
|
+
raise self.libuserd_exception(e)
|
|
1876
|
+
|
|
1877
|
+
def get_all_readers(self) -> List["ReaderInfo"]:
|
|
1878
|
+
"""
|
|
1879
|
+
Return a list of the readers that are available.
|
|
1880
|
+
|
|
1881
|
+
Returns
|
|
1882
|
+
-------
|
|
1883
|
+
List[ReaderInfo]
|
|
1884
|
+
List of all ReaderInfo objects.
|
|
1885
|
+
"""
|
|
1886
|
+
self.connect_check()
|
|
1887
|
+
pb = libuserd_pb2.Libuserd_get_all_readersRequest()
|
|
1888
|
+
try:
|
|
1889
|
+
readers = self.stub.Libuserd_get_all_readers(pb, metadata=self.metadata())
|
|
1890
|
+
except grpc.RpcError as e:
|
|
1891
|
+
raise self.libuserd_exception(e)
|
|
1892
|
+
out = []
|
|
1893
|
+
for reader in readers.reader_info:
|
|
1894
|
+
out.append(ReaderInfo(self, reader))
|
|
1895
|
+
return out
|
|
1896
|
+
|
|
1897
|
+
def query_format(self, name1: str, name2: str = "") -> List["ReaderInfo"]:
|
|
1898
|
+
"""
|
|
1899
|
+
For a given dataset (filename(s)), ask the readers if they should be able to read
|
|
1900
|
+
that data.
|
|
1901
|
+
|
|
1902
|
+
Parameters
|
|
1903
|
+
----------
|
|
1904
|
+
name1 : str
|
|
1905
|
+
Primary input filename
|
|
1906
|
+
|
|
1907
|
+
name2 : str
|
|
1908
|
+
Optional, secondary input filename
|
|
1909
|
+
|
|
1910
|
+
Returns
|
|
1911
|
+
-------
|
|
1912
|
+
List[ReaderInfo]
|
|
1913
|
+
List of ReaderInfo objects that might be able to read the dataset
|
|
1914
|
+
"""
|
|
1915
|
+
self.connect_check()
|
|
1916
|
+
pb = libuserd_pb2.Libuserd_query_formatRequest()
|
|
1917
|
+
pb.name1 = name1
|
|
1918
|
+
if name2:
|
|
1919
|
+
pb.name2 = name2
|
|
1920
|
+
try:
|
|
1921
|
+
readers = self.stub.Libuserd_query_format(pb, metadata=self.metadata())
|
|
1922
|
+
except grpc.RpcError as e:
|
|
1923
|
+
raise self.libuserd_exception(e)
|
|
1924
|
+
out = []
|
|
1925
|
+
for reader in readers.reader_info:
|
|
1926
|
+
out.append(ReaderInfo(self, reader))
|
|
1927
|
+
return out
|
|
1928
|
+
|
|
1929
|
+
def load_data(
|
|
1930
|
+
self,
|
|
1931
|
+
data_file: str,
|
|
1932
|
+
result_file: str = "",
|
|
1933
|
+
file_format: Optional[str] = None,
|
|
1934
|
+
reader_options: Dict[str, Any] = {},
|
|
1935
|
+
) -> "Reader":
|
|
1936
|
+
"""Use the reader to load a dataset and return an instance
|
|
1937
|
+
to the resulting ``Reader`` interface.
|
|
1938
|
+
|
|
1939
|
+
Parameters
|
|
1940
|
+
----------
|
|
1941
|
+
data_file : str
|
|
1942
|
+
Name of the data file to load.
|
|
1943
|
+
result_file : str, optional
|
|
1944
|
+
Name of the second data file for dual-file datasets.
|
|
1945
|
+
file_format : str, optional
|
|
1946
|
+
Name of the USERD reader to use. The default is ``None``,
|
|
1947
|
+
in which case libuserd selects a reader.
|
|
1948
|
+
reader_options : dict, optional
|
|
1949
|
+
Dictionary of reader-specific option-value pairs that can be used
|
|
1950
|
+
to customize the reader behavior. The default is ``None``.
|
|
1951
|
+
|
|
1952
|
+
Returns
|
|
1953
|
+
-------
|
|
1954
|
+
Reader
|
|
1955
|
+
Resulting Reader object instance.
|
|
1956
|
+
|
|
1957
|
+
Raises
|
|
1958
|
+
------
|
|
1959
|
+
RuntimeError
|
|
1960
|
+
If libused cannot guess the file format or an error occurs while the
|
|
1961
|
+
data is being read.
|
|
1962
|
+
|
|
1963
|
+
Examples
|
|
1964
|
+
--------
|
|
1965
|
+
|
|
1966
|
+
>>> from ansys.pyensight.core import libuserd
|
|
1967
|
+
>>> userd = libuserd.LibUserd()
|
|
1968
|
+
>>> userd.initialize()
|
|
1969
|
+
>>> opt = {'Long names': False, 'Number of timesteps': '10', 'Number of scalars': '3'}
|
|
1970
|
+
>>> data = userd.load_data("foo", file_format="Synthetic", reader_options=opt
|
|
1971
|
+
>>> print(data.parts())
|
|
1972
|
+
>>> print(data.variables())
|
|
1973
|
+
>>> userd.shutdown()
|
|
1974
|
+
|
|
1975
|
+
"""
|
|
1976
|
+
the_reader: Optional[ReaderInfo] = None
|
|
1977
|
+
if file_format:
|
|
1978
|
+
for reader in self.get_all_readers():
|
|
1979
|
+
if reader.name == file_format:
|
|
1980
|
+
the_reader = reader
|
|
1981
|
+
break
|
|
1982
|
+
if the_reader is None:
|
|
1983
|
+
raise RuntimeError(f"The reader '{file_format}' could not be found.")
|
|
1984
|
+
else:
|
|
1985
|
+
readers = self.query_format(data_file, name2=result_file)
|
|
1986
|
+
if len(readers):
|
|
1987
|
+
the_reader = readers[0]
|
|
1988
|
+
if the_reader is None:
|
|
1989
|
+
raise RuntimeError(f"Unable to find a reader for '{data_file}':'{result_file}'.")
|
|
1990
|
+
for key, value in reader_options.items():
|
|
1991
|
+
for b in the_reader.opt_booleans:
|
|
1992
|
+
if key == b["name"]:
|
|
1993
|
+
b["value"] = bool(value)
|
|
1994
|
+
for o in the_reader.opt_options:
|
|
1995
|
+
if key == o["name"]:
|
|
1996
|
+
o["value"] = int(value)
|
|
1997
|
+
for f in the_reader.opt_fields:
|
|
1998
|
+
if key == f["name"]:
|
|
1999
|
+
f["value"] = str(value)
|
|
2000
|
+
try:
|
|
2001
|
+
output = the_reader.read_dataset(data_file, result_file)
|
|
2002
|
+
except Exception:
|
|
2003
|
+
raise RuntimeError("Unable to open the specified dataset.") from None
|
|
2004
|
+
|
|
2005
|
+
return output
|
|
2006
|
+
|
|
2007
|
+
@staticmethod
|
|
2008
|
+
def _download_files(uri: str, pathname: str, folder: bool = False, override_root: bool = False):
|
|
2009
|
+
"""Download files from the input uri and save them on the input pathname.
|
|
2010
|
+
|
|
2011
|
+
Parameters:
|
|
2012
|
+
----------
|
|
2013
|
+
|
|
2014
|
+
uri : str
|
|
2015
|
+
The uri to get files from
|
|
2016
|
+
pathname : str
|
|
2017
|
+
The location were to save the files. It could be either a file or a folder.
|
|
2018
|
+
folder : bool
|
|
2019
|
+
True if the uri will server files from a directory. In this case,
|
|
2020
|
+
pathname will be used as the directory were to save the files.
|
|
2021
|
+
override_root: bool
|
|
2022
|
+
True if the root has been overridden. So don't consider the case
|
|
2023
|
+
of getting the download URL from the github API
|
|
2024
|
+
"""
|
|
2025
|
+
if not folder:
|
|
2026
|
+
if override_root:
|
|
2027
|
+
correct_url = uri
|
|
2028
|
+
else:
|
|
2029
|
+
correct_url = None
|
|
2030
|
+
with requests.get(uri) as r:
|
|
2031
|
+
data = r.json()
|
|
2032
|
+
correct_url = data["download_url"]
|
|
2033
|
+
if not correct_url:
|
|
2034
|
+
raise RuntimeError(f"Couldn't retrieve download URL from github uri {uri}")
|
|
2035
|
+
with requests.get(correct_url, stream=True) as r:
|
|
2036
|
+
with open(pathname, "wb") as f:
|
|
2037
|
+
shutil.copyfileobj(r.raw, f)
|
|
2038
|
+
else:
|
|
2039
|
+
with requests.get(uri) as r:
|
|
2040
|
+
data = r.json()
|
|
2041
|
+
os.makedirs(pathname, exist_ok=True)
|
|
2042
|
+
for item in data:
|
|
2043
|
+
if item["type"] == "file":
|
|
2044
|
+
file_url = item["download_url"]
|
|
2045
|
+
filename = os.path.join(pathname, item["name"])
|
|
2046
|
+
r = requests.get(file_url, stream=True)
|
|
2047
|
+
with open(filename, "wb") as f:
|
|
2048
|
+
f.write(r.content)
|
|
2049
|
+
|
|
2050
|
+
def file_service(self) -> Optional[Any]:
|
|
2051
|
+
"""Get the PIM file service object if available."""
|
|
2052
|
+
return self._pim_file_service
|
|
2053
|
+
|
|
2054
|
+
def download_pyansys_example(
|
|
2055
|
+
self,
|
|
2056
|
+
filename: str,
|
|
2057
|
+
directory: Optional[str] = None,
|
|
2058
|
+
root: Optional[str] = None,
|
|
2059
|
+
folder: bool = False,
|
|
2060
|
+
) -> str:
|
|
2061
|
+
"""Download an example dataset from the ansys/example-data repository.
|
|
2062
|
+
The dataset is downloaded local to the EnSight server location, so that it can
|
|
2063
|
+
be downloaded even if running from a container.
|
|
2064
|
+
|
|
2065
|
+
Parameters
|
|
2066
|
+
----------
|
|
2067
|
+
filename: str
|
|
2068
|
+
The filename to download
|
|
2069
|
+
directory: str
|
|
2070
|
+
The directory to download the filename from
|
|
2071
|
+
root: str
|
|
2072
|
+
If set, the download will happen from another location
|
|
2073
|
+
folder: bool
|
|
2074
|
+
If set to True, it marks the filename to be a directory rather
|
|
2075
|
+
than a single file
|
|
2076
|
+
|
|
2077
|
+
Returns
|
|
2078
|
+
-------
|
|
2079
|
+
pathname: str
|
|
2080
|
+
The download location, local to the EnSight server directory.
|
|
2081
|
+
If folder is set to True, the download location will be a folder containing
|
|
2082
|
+
all the items available in the repository location under that folder.
|
|
2083
|
+
|
|
2084
|
+
Examples
|
|
2085
|
+
--------
|
|
2086
|
+
>>> from ansys.pyensight.core import libuserd
|
|
2087
|
+
>>> l = libuserd.LibUserd()
|
|
2088
|
+
>>> cas_file = l.download_pyansys_example("mixing_elbow.cas.h5","pyfluent/mixing_elbow")
|
|
2089
|
+
>>> dat_file = l.download_pyansys_example("mixing_elbow.dat.h5","pyfluent/mixing_elbow")
|
|
2090
|
+
"""
|
|
2091
|
+
base_uri = "https://api.github.com/repos/ansys/example-data/contents"
|
|
2092
|
+
override_root = False
|
|
2093
|
+
if not folder:
|
|
2094
|
+
if root is not None:
|
|
2095
|
+
base_uri = root
|
|
2096
|
+
override_root = True
|
|
2097
|
+
uri = f"{base_uri}/{filename}"
|
|
2098
|
+
if directory:
|
|
2099
|
+
uri = f"{base_uri}/{directory}/{filename}"
|
|
2100
|
+
# Local installs and PIM instances
|
|
2101
|
+
download_path = f"{os.getcwd()}/{filename}"
|
|
2102
|
+
if self._container and self._data_directory:
|
|
2103
|
+
# Docker Image
|
|
2104
|
+
download_path = os.path.join(self._data_directory, filename)
|
|
2105
|
+
self._download_files(uri, download_path, folder=folder, override_root=override_root)
|
|
2106
|
+
pathname = download_path
|
|
2107
|
+
if self._container:
|
|
2108
|
+
# Convert local path to Docker mounted volume path
|
|
2109
|
+
pathname = f"/data/{filename}"
|
|
2110
|
+
return pathname
|