ansys-pyensight-core 0.8.11__py3-none-any.whl → 0.8.13__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.

Potentially problematic release.


This version of ansys-pyensight-core might be problematic. Click here for more details.

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