ansys-pyensight-core 0.8.12__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,1953 +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
- 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
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