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.
Files changed (37) hide show
  1. ansys/pyensight/core/__init__.py +41 -0
  2. ansys/pyensight/core/common.py +341 -0
  3. ansys/pyensight/core/deep_pixel_view.html +98 -0
  4. ansys/pyensight/core/dockerlauncher.py +1124 -0
  5. ansys/pyensight/core/dvs.py +872 -0
  6. ansys/pyensight/core/enscontext.py +345 -0
  7. ansys/pyensight/core/enshell_grpc.py +641 -0
  8. ansys/pyensight/core/ensight_grpc.py +874 -0
  9. ansys/pyensight/core/ensobj.py +515 -0
  10. ansys/pyensight/core/launch_ensight.py +296 -0
  11. ansys/pyensight/core/launcher.py +388 -0
  12. ansys/pyensight/core/libuserd.py +2110 -0
  13. ansys/pyensight/core/listobj.py +280 -0
  14. ansys/pyensight/core/locallauncher.py +579 -0
  15. ansys/pyensight/core/py.typed +0 -0
  16. ansys/pyensight/core/renderable.py +880 -0
  17. ansys/pyensight/core/session.py +1923 -0
  18. ansys/pyensight/core/sgeo_poll.html +24 -0
  19. ansys/pyensight/core/utils/__init__.py +21 -0
  20. ansys/pyensight/core/utils/adr.py +111 -0
  21. ansys/pyensight/core/utils/dsg_server.py +1220 -0
  22. ansys/pyensight/core/utils/export.py +606 -0
  23. ansys/pyensight/core/utils/omniverse.py +769 -0
  24. ansys/pyensight/core/utils/omniverse_cli.py +614 -0
  25. ansys/pyensight/core/utils/omniverse_dsg_server.py +1196 -0
  26. ansys/pyensight/core/utils/omniverse_glb_server.py +848 -0
  27. ansys/pyensight/core/utils/parts.py +1221 -0
  28. ansys/pyensight/core/utils/query.py +487 -0
  29. ansys/pyensight/core/utils/readers.py +300 -0
  30. ansys/pyensight/core/utils/resources/Materials/000_sky.exr +0 -0
  31. ansys/pyensight/core/utils/support.py +128 -0
  32. ansys/pyensight/core/utils/variables.py +2019 -0
  33. ansys/pyensight/core/utils/views.py +674 -0
  34. ansys_pyensight_core-0.11.0.dist-info/METADATA +309 -0
  35. ansys_pyensight_core-0.11.0.dist-info/RECORD +37 -0
  36. ansys_pyensight_core-0.11.0.dist-info/WHEEL +4 -0
  37. 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