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

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