ansys-pyensight-core 0.8.12__py3-none-any.whl → 0.9.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


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

@@ -1,584 +1,584 @@
1
- import glob
2
- import os
3
- import tempfile
4
- from types import ModuleType
5
- from typing import Any, List, Optional, Union
6
- import uuid
7
-
8
- from PIL import Image
9
- import numpy
10
-
11
- try:
12
- import ensight
13
- import enve
14
- except ImportError:
15
- from ansys.api.pyensight import ensight_api
16
-
17
-
18
- class Export:
19
- """Provides the ``ensight.utils.export`` interface.
20
-
21
- The methods in this class implement simplified interfaces to common
22
- image and animation export operations.
23
-
24
- This class is instantiated as ``ensight.utils.export`` in EnSight Python
25
- and as ``Session.ensight.utils.export`` in PyEnSight. The constructor is
26
- passed the interface, which serves as the ``ensight`` module for either
27
- case. As a result, the methods can be accessed as ``ensight.utils.export.image()``
28
- in EnSight Python or ``session.ensight.utils.export.animation()`` in PyEnSight.
29
-
30
- Parameters
31
- ----------
32
- interface :
33
- Entity that provides the ``ensight`` namespace. In the case of
34
- EnSight Python, the ``ensight`` module is passed. In the case
35
- of PyEnSight, ``Session.ensight`` is passed.
36
- """
37
-
38
- def __init__(self, interface: Union["ensight_api.ensight", "ensight"]):
39
- self._ensight = interface
40
-
41
- def _remote_support_check(self):
42
- """Determine if ``ensight.utils.export`` exists on the remote system.
43
-
44
- Before trying to use this module, use this method to determine if this
45
- module is available in the EnSight instance.
46
-
47
- Raises
48
- ------
49
- RuntimeError if the module is not present.
50
- """
51
- # if a module, then we are inside EnSight
52
- if isinstance(self._ensight, ModuleType): # pragma: no cover
53
- return # pragma: no cover
54
- try:
55
- _ = self._ensight._session.cmd("dir(ensight.utils.export)")
56
- except RuntimeError: # pragma: no cover
57
- import ansys.pyensight.core # pragma: no cover
58
-
59
- raise RuntimeError( # pragma: no cover
60
- f"Remote EnSight session must have PyEnsight version \
61
- {ansys.pyensight.core.DEFAULT_ANSYS_VERSION} or higher installed to use this API."
62
- )
63
-
64
- TIFFTAG_IMAGEDESCRIPTION: int = 0x010E
65
-
66
- def image(
67
- self,
68
- filename: str,
69
- width: Optional[int] = None,
70
- height: Optional[int] = None,
71
- passes: int = 4,
72
- enhanced: bool = False,
73
- raytrace: bool = False,
74
- ) -> None:
75
- """Render an image of the current EnSight scene.
76
-
77
- Parameters
78
- ----------
79
- filename : str
80
- Name of the local file to save the image to.
81
- width : int, optional
82
- Width of the image in pixels. The default is ``None``, in which case
83
- ```ensight.objs.core.WINDOWSIZE[0]`` is used.
84
- height : int, optional
85
- Height of the image in pixels. The default is ``None``, in which case
86
- ``ensight.objs.core.WINDOWSIZE[1]`` is used.
87
- passes : int, optional
88
- Number of antialiasing passes. The default is ``4``.
89
- enhanced : bool, optional
90
- Whether to save the image to the filename specified in the TIFF format.
91
- The default is ``False``. The TIFF format includes additional channels
92
- for the per-pixel object and variable information.
93
- raytrace : bool, optional
94
- Whether to render the image with the raytracing engine. The default is ``False``.
95
-
96
- Examples
97
- --------
98
- >>> s = LocalLauncher().start()
99
- >>> s.load_data(f"{s.cei_home}/ensight{s.cei_suffix}/data/cube/cube.case")
100
- >>> s.ensight.utils.export.image("example.png")
101
-
102
- """
103
- self._remote_support_check()
104
-
105
- win_size = self._ensight.objs.core.WINDOWSIZE
106
- if width is None:
107
- width = win_size[0]
108
- if height is None:
109
- height = win_size[1]
110
-
111
- if isinstance(self._ensight, ModuleType): # pragma: no cover
112
- raw_image = self._image_remote(
113
- width, height, passes, enhanced, raytrace
114
- ) # pragma: no cover
115
- else:
116
- cmd = f"ensight.utils.export._image_remote({width}, {height}, {passes}, "
117
- cmd += f"{enhanced}, {raytrace})"
118
- raw_image = self._ensight._session.cmd(cmd)
119
-
120
- pil_image = self._dict_to_pil(raw_image)
121
- if enhanced:
122
- tiffinfo_dir = {self.TIFFTAG_IMAGEDESCRIPTION: raw_image["metadata"]}
123
- pil_image[0].save(
124
- filename,
125
- save_all=True,
126
- append_images=[pil_image[1], pil_image[2]],
127
- tiffinfo=tiffinfo_dir,
128
- )
129
- else:
130
- pil_image[0].save(filename)
131
-
132
- def _dict_to_pil(self, data: dict) -> list:
133
- """Convert the contents of the dictionary into a PIL image.
134
-
135
- Parameters
136
- ----------
137
- data : dict
138
- Dictionary representation of the contents of the ``enve`` object.
139
-
140
- Returns
141
- -------
142
- list
143
- List of one or three image objects, [RGB {, pick, variable}].
144
- """
145
- images = [
146
- Image.fromarray(self._numpy_from_dict(data["pixeldata"])).transpose(
147
- Image.FLIP_TOP_BOTTOM
148
- )
149
- ]
150
- if data.get("variabledata", None) and data.get("pickdata", None):
151
- images.append(
152
- Image.fromarray(self._numpy_from_dict(data["pickdata"])).transpose(
153
- Image.FLIP_TOP_BOTTOM
154
- )
155
- )
156
- images.append(
157
- Image.fromarray(self._numpy_from_dict(data["variabledata"])).transpose(
158
- Image.FLIP_TOP_BOTTOM
159
- )
160
- )
161
- return images
162
-
163
- @staticmethod
164
- def _numpy_to_dict(array: Any) -> Optional[dict]:
165
- """Convert a numpy array into a dictionary.
166
-
167
- Parameters
168
- ----------
169
- array:
170
- Numpy array or None.
171
-
172
- Returns
173
- -------
174
- ``None`` or a dictionary that can be serialized.
175
- """
176
- if array is None:
177
- return None
178
- return dict(shape=array.shape, dtype=array.dtype.str, data=array.tostring())
179
-
180
- @staticmethod
181
- def _numpy_from_dict(obj: Optional[dict]) -> Any:
182
- """Convert a dictionary into a numpy array.
183
-
184
- Parameters
185
- ----------
186
- obj:
187
- Dictionary generated by ``_numpy_to_dict`` or ``None``.
188
-
189
- Returns
190
- -------
191
- ``None`` or a numpy array.
192
- """
193
- if obj is None:
194
- return None
195
- return numpy.frombuffer(obj["data"], dtype=obj["dtype"]).reshape(obj["shape"])
196
-
197
- def _image_remote(
198
- self, width: int, height: int, passes: int, enhanced: bool, raytrace: bool
199
- ) -> dict:
200
- """EnSight-side implementation.
201
-
202
- Parameters
203
- ----------
204
- width : int
205
- Width of the image in pixels.
206
- height : int
207
- Height of the image in pixels.
208
- passes : int
209
- Number of antialiasing passes.
210
- enhanced : bool
211
- Whether to returned the image as a "deep pixel" TIFF image file.
212
- raytrace :
213
- Whether to render the image with the raytracing engine.
214
-
215
- Returns
216
- -------
217
- dict
218
- Dictionary of the various channels.
219
- """
220
- if not raytrace:
221
- img = ensight.render(x=width, y=height, num_samples=passes, enhanced=enhanced)
222
- else:
223
- with tempfile.TemporaryDirectory() as tmpdirname:
224
- tmpfilename = os.path.join(tmpdirname, str(uuid.uuid1()))
225
- ensight.file.image_format("png")
226
- ensight.file.image_file(tmpfilename)
227
- ensight.file.image_window_size("user_defined")
228
- ensight.file.image_window_xy(width, height)
229
- ensight.file.image_rend_offscreen("ON")
230
- ensight.file.image_numpasses(passes)
231
- ensight.file.image_stereo("current")
232
- ensight.file.image_screen_tiling(1, 1)
233
- ensight.file.raytracer_options("fgoverlay 1 imagedenoise 1 quality 5")
234
- ensight.file.image_raytrace_it("ON")
235
- ensight.file.save_image()
236
- img = enve.image()
237
- img.load(f"{tmpfilename}.png")
238
- # get the channels from the enve.image instance
239
- output = dict(width=width, height=height, metadata=img.metadata)
240
- # extract the channels from the image
241
- output["pixeldata"] = self._numpy_to_dict(img.pixeldata)
242
- output["variabledata"] = self._numpy_to_dict(img.variabledata)
243
- output["pickdata"] = self._numpy_to_dict(img.pickdata)
244
- return output
245
-
246
- ANIM_TYPE_SOLUTIONTIME: int = 0
247
- ANIM_TYPE_ANIMATEDTRACES: int = 1
248
- ANIM_TYPE_FLIPBOOK: int = 2
249
- ANIM_TYPE_KEYFRAME: int = 3
250
-
251
- def animation(
252
- self,
253
- filename: str,
254
- width: Optional[int] = None,
255
- height: Optional[int] = None,
256
- passes: int = 4,
257
- anim_type: int = ANIM_TYPE_SOLUTIONTIME,
258
- frames: Optional[int] = None,
259
- starting_frame: int = 0,
260
- frames_per_second: float = 60.0,
261
- format_options: Optional[str] = "",
262
- raytrace: bool = False,
263
- ) -> None:
264
- """Generate an MPEG4 animation file.
265
-
266
- An MPEG4 animation file can be generated from temporal data, flipbooks, keyframes,
267
- or animated traces.
268
-
269
- Parameters
270
- ----------
271
- filename : str
272
- Name for the MPEG4 file to save to local disk.
273
- width : int, optional
274
- Width of the image in pixels. The default is ``None``, in which case
275
- ``ensight.objs.core.WINDOWSIZE[0]`` is used.
276
- height : int, optional
277
- Height of the image in pixels. The default is ``None``, in which case
278
- ``ensight.objs.core.WINDOWSIZE[1]`` is used.
279
- passes : int, optional
280
- Number of antialiasing passes. The default is ``4``.
281
- anim_type : int, optional
282
- Type of the animation to render. The default is ``0``, in which case
283
- ``"ANIM_TYPE_SOLUTIONTIME"`` is used. This table provides descriptions
284
- by each option number and name:
285
-
286
- =========================== ========================================
287
- Name Animation type
288
- =========================== ========================================
289
- 0: ANIM_TYPE_SOLUTIONTIME Animation over all solution times
290
- 1: ANIM_TYPE_ANIMATEDTRACES Records animated rotations and traces
291
- 2: ANIM_TYPE_FLIPBOOK Records current flipbook animation
292
- 3: ANIM_TYPE_KEYFRAME Records current kKeyframe animation
293
- =========================== ========================================
294
-
295
- frames : int, optional
296
- Number of frames to save. The default is ``None``. The default for
297
- all but ``ANIM_TYPE_ANIMATEDTRACES`` covers all timesteps, flipbook
298
- pages, or keyframe steps. If ``ANIM_TYPE_ANIMATEDTRACES`` is specified,
299
- this keyword is required.
300
- starting_frame : int, optional
301
- Keyword for saving a subset of the complete collection of frames.
302
- The default is ``0``.
303
- frames_per_second : float, optional
304
- Number of frames per second for playback in the saved animation.
305
- The default is ``60.0``.
306
- format_options : str, optional
307
- More specific options for the MPEG4 encoder. The default is ``""``.
308
- raytrace : bool, optional
309
- Whether to render the image with the raytracing engine. The default is ``False``.
310
-
311
- Examples
312
- --------
313
- >>> s = LocalLauncher().start()
314
- >>> data = f"{s.cei_home}/ensight{s.cei_suffix}gui/demos/Crash Queries.ens"
315
- >>> s.ensight.objs.ensxml_restore_file(data)
316
- >>> quality = "Quality Best Type 1"
317
- >>> s.ensight.utils.export.animation("local_file.mp4", format_options=quality)
318
-
319
- """
320
- self._remote_support_check()
321
-
322
- win_size = self._ensight.objs.core.WINDOWSIZE
323
- if width is None:
324
- width = win_size[0]
325
- if height is None:
326
- height = win_size[1]
327
-
328
- if format_options is None:
329
- format_options = "Quality High Type 1"
330
-
331
- num_frames: int = 0
332
- if frames is None:
333
- if anim_type == self.ANIM_TYPE_SOLUTIONTIME:
334
- num_timesteps = self._ensight.objs.core.TIMESTEP_LIMITS[1]
335
- num_frames = num_timesteps - starting_frame
336
- elif anim_type == self.ANIM_TYPE_ANIMATEDTRACES:
337
- raise RuntimeError("frames is a required keyword with ANIMATEDTRACES animations")
338
- elif anim_type == self.ANIM_TYPE_FLIPBOOK:
339
- num_flip_pages = len(self._ensight.objs.core.FLIPBOOKS[0].PAGE_DETAILS)
340
- num_frames = num_flip_pages - starting_frame
341
- elif anim_type == self.ANIM_TYPE_KEYFRAME:
342
- num_keyframe_pages = self._ensight.objs.core.KEYFRAMEDATA["totalFrames"]
343
- num_frames = num_keyframe_pages - starting_frame
344
- else:
345
- num_frames = frames
346
-
347
- if num_frames < 1: # pragma: no cover
348
- raise RuntimeError( # pragma: no cover
349
- "No frames selected. Perhaps a static dataset SOLUTIONTIME request \
350
- or no FLIPBOOK/KEYFRAME defined."
351
- )
352
-
353
- if isinstance(self._ensight, ModuleType): # pragma: no cover
354
- raw_mpeg4 = self._animation_remote( # pragma: no cover
355
- width,
356
- height,
357
- passes,
358
- anim_type,
359
- starting_frame,
360
- num_frames,
361
- frames_per_second,
362
- format_options,
363
- raytrace,
364
- )
365
- else:
366
- cmd = f"ensight.utils.export._animation_remote({width}, {height}, {passes}, "
367
- cmd += f"{anim_type}, {starting_frame}, {num_frames}, "
368
- cmd += f"{frames_per_second}, '{format_options}', {raytrace})"
369
- raw_mpeg4 = self._ensight._session.cmd(cmd)
370
-
371
- with open(filename, "wb") as fp:
372
- fp.write(raw_mpeg4)
373
-
374
- def _animation_remote(
375
- self,
376
- width: int,
377
- height: int,
378
- passes: int,
379
- anim_type: int,
380
- start: int,
381
- frames: int,
382
- fps: float,
383
- options: str,
384
- raytrace: bool,
385
- ) -> bytes:
386
- """EnSight-side implementation.
387
-
388
- Parameters
389
- ----------
390
- width : int
391
- Width of the image in pixels.
392
- height : int
393
- Height of the image in pixels.
394
- passes : int
395
- Number of antialiasing passes.
396
- anim_type : int
397
- Type of animation to save.
398
- start : int
399
- First frame number to save.
400
- frames : int
401
- Number of frames to save.
402
- fps : float
403
- Output framerate.
404
- options : str
405
- MPEG4 configuration options.
406
- raytrace : bool
407
- Whether to render the image with the raytracing engine.
408
-
409
- Returns
410
- -------
411
- bytes
412
- MPEG4 stream in bytes.
413
- """
414
-
415
- with tempfile.TemporaryDirectory() as tmpdirname:
416
- tmpfilename = os.path.join(tmpdirname, str(uuid.uuid1()) + ".mp4")
417
- self._ensight.file.animation_rend_offscreen("ON")
418
- self._ensight.file.animation_screen_tiling(1, 1)
419
- self._ensight.file.animation_format("mpeg4")
420
- if options:
421
- self._ensight.file.animation_format_options(options)
422
- self._ensight.file.animation_frame_rate(fps)
423
- self._ensight.file.animation_rend_offscreen("ON")
424
- self._ensight.file.animation_numpasses(passes)
425
- self._ensight.file.animation_stereo("mono")
426
- self._ensight.file.animation_screen_tiling(1, 1)
427
- self._ensight.file.animation_file(tmpfilename)
428
- self._ensight.file.animation_window_size("user_defined")
429
- self._ensight.file.animation_window_xy(width, height)
430
- self._ensight.file.animation_frames(frames)
431
- self._ensight.file.animation_start_number(start)
432
- self._ensight.file.animation_multiple_images("OFF")
433
- if raytrace:
434
- self._ensight.file.animation_raytrace_it("ON")
435
- else:
436
- self._ensight.file.animation_raytrace_it("OFF")
437
- self._ensight.file.animation_raytrace_ext("OFF")
438
-
439
- self._ensight.file.animation_play_time("OFF")
440
- self._ensight.file.animation_play_flipbook("OFF")
441
- self._ensight.file.animation_play_keyframe("OFF")
442
-
443
- self._ensight.file.animation_reset_time("OFF")
444
- self._ensight.file.animation_reset_traces("OFF")
445
- self._ensight.file.animation_reset_flipbook("OFF")
446
- self._ensight.file.animation_reset_keyframe("OFF")
447
-
448
- if anim_type == self.ANIM_TYPE_SOLUTIONTIME:
449
- # playing over time
450
- self._ensight.file.animation_play_time("ON")
451
- self._ensight.file.animation_reset_time("ON")
452
- elif anim_type == self.ANIM_TYPE_ANIMATEDTRACES:
453
- # recording particle traces/etc
454
- self._ensight.file.animation_reset_traces("ON")
455
- elif anim_type == self.ANIM_TYPE_KEYFRAME:
456
- self._ensight.file.animation_reset_keyframe("ON")
457
- self._ensight.file.animation_play_keyframe("ON")
458
- elif anim_type == self.ANIM_TYPE_FLIPBOOK:
459
- self._ensight.file.animation_play_flipbook("ON")
460
- self._ensight.file.animation_reset_flipbook("ON")
461
-
462
- self._ensight.file.save_animation()
463
-
464
- with open(tmpfilename, "rb") as fp:
465
- mp4_data = fp.read()
466
-
467
- return mp4_data
468
-
469
- GEOM_EXPORT_GLTF = "gltf2"
470
- GEOM_EXPORT_AVZ = "avz"
471
- GEOM_EXPORT_PLY = "ply"
472
- GEOM_EXPORT_STL = "stl"
473
-
474
- extension_map = {
475
- GEOM_EXPORT_GLTF: ".glb",
476
- GEOM_EXPORT_AVZ: ".avz",
477
- GEOM_EXPORT_PLY: ".ply",
478
- GEOM_EXPORT_STL: ".stl",
479
- }
480
-
481
- def _geometry_remote( # pragma: no cover
482
- self, format: str, starting_timestep: int, frames: int, delta_timestep: int
483
- ) -> List[bytes]:
484
- """EnSight-side implementation.
485
-
486
- Parameters
487
- ----------
488
- format : str
489
- The format to export
490
- starting_timestep: int
491
- The first timestep to export. If None, defaults to the current timestep
492
- frames: int
493
- Number of timesteps to save. If None, defaults from the current timestep to the last
494
- delta_timestep: int
495
- The delta timestep to use when exporting
496
-
497
- Returns
498
- -------
499
- bytes
500
- Geometry export in bytes
501
- """
502
- rawdata = None
503
- extension = self.extension_map.get(format)
504
- rawdata_list = []
505
- if not extension:
506
- raise RuntimeError("The geometry export format provided is not supported.")
507
- with tempfile.TemporaryDirectory() as tmpdirname:
508
- self._ensight.part.select_all()
509
- self._ensight.savegeom.format(format)
510
- self._ensight.savegeom.begin_step(starting_timestep)
511
- # frames is 1-indexed, so I need to decrease of 1
512
- self._ensight.savegeom.end_step(starting_timestep + frames - 1)
513
- self._ensight.savegeom.step_by(delta_timestep)
514
- tmpfilename = os.path.join(tmpdirname, str(uuid.uuid1()))
515
- self._ensight.savegeom.save_geometric_entities(tmpfilename)
516
- files = glob.glob(f"{tmpfilename}*{extension}")
517
- for export_file in files:
518
- with open(export_file, "rb") as tmpfile:
519
- rawdata = tmpfile.read()
520
- rawdata_list.append(rawdata)
521
- return rawdata_list
522
-
523
- def geometry(
524
- self,
525
- filename: str,
526
- format: str = GEOM_EXPORT_GLTF,
527
- starting_timestep: Optional[int] = None,
528
- frames: Optional[int] = 1,
529
- delta_timestep: Optional[int] = None,
530
- ) -> None:
531
- """Export a geometry file.
532
-
533
- Parameters
534
- ----------
535
- filename: str
536
- The location where to export the geometry
537
- format : str
538
- The format to export
539
- starting_timestep: int
540
- The first timestep to export. If None, defaults to the current timestep
541
- frames: int
542
- Number of timesteps to save. If None, defaults from the current timestep to the last
543
- delta_timestep: int
544
- The delta timestep to use when exporting
545
-
546
- Examples
547
- --------
548
- >>> s = LocalLauncher().start()
549
- >>> data = f"{s.cei_home}/ensight{s.cei_suffix}gui/demos/Crash Queries.ens"
550
- >>> s.ensight.objs.ensxml_restore_file(data)
551
- >>> s.ensight.utils.export.geometry("local_file.glb", format=s.ensight.utils.export.GEOM_EXPORT_GLTF)
552
- """
553
- if starting_timestep is None:
554
- starting_timestep = int(self._ensight.objs.core.TIMESTEP)
555
- if frames is None or frames == -1:
556
- # Timesteps are 0-indexed so frames need to be increased of 1
557
- frames = int(self._ensight.objs.core.TIMESTEP_LIMITS[1]) + 1
558
- if not delta_timestep:
559
- delta_timestep = 1
560
- self._remote_support_check()
561
- raw_data_list = None
562
- if isinstance(self._ensight, ModuleType): # pragma: no cover
563
- raw_data_list = self._geometry_remote( # pragma: no cover
564
- format,
565
- starting_timestep=starting_timestep,
566
- frames=frames,
567
- delta_timestep=delta_timestep,
568
- )
569
- else:
570
- self._ensight._session.ensight_version_check("2024 R2")
571
- cmd = f"ensight.utils.export._geometry_remote('{format}', {starting_timestep}, {frames}, {delta_timestep})"
572
- raw_data_list = self._ensight._session.cmd(cmd)
573
- if raw_data_list: # pragma: no cover
574
- if len(raw_data_list) == 1:
575
- with open(filename, "wb") as fp:
576
- fp.write(raw_data_list[0])
577
- else:
578
- for idx, raw_data in enumerate(raw_data_list):
579
- filename_base, extension = os.path.splitext(filename)
580
- _filename = f"{filename_base}{str(idx).zfill(3)}{extension}"
581
- with open(_filename, "wb") as fp:
582
- fp.write(raw_data)
583
- else: # pragma: no cover
584
- raise IOError("Export was not successful") # pragma: no cover
1
+ import glob
2
+ import os
3
+ import tempfile
4
+ from types import ModuleType
5
+ from typing import Any, List, Optional, Union
6
+ import uuid
7
+
8
+ from PIL import Image
9
+ import numpy
10
+
11
+ try:
12
+ import ensight
13
+ import enve
14
+ except ImportError:
15
+ from ansys.api.pyensight import ensight_api
16
+
17
+
18
+ class Export:
19
+ """Provides the ``ensight.utils.export`` interface.
20
+
21
+ The methods in this class implement simplified interfaces to common
22
+ image and animation export operations.
23
+
24
+ This class is instantiated as ``ensight.utils.export`` in EnSight Python
25
+ and as ``Session.ensight.utils.export`` in PyEnSight. The constructor is
26
+ passed the interface, which serves as the ``ensight`` module for either
27
+ case. As a result, the methods can be accessed as ``ensight.utils.export.image()``
28
+ in EnSight Python or ``session.ensight.utils.export.animation()`` in PyEnSight.
29
+
30
+ Parameters
31
+ ----------
32
+ interface :
33
+ Entity that provides the ``ensight`` namespace. In the case of
34
+ EnSight Python, the ``ensight`` module is passed. In the case
35
+ of PyEnSight, ``Session.ensight`` is passed.
36
+ """
37
+
38
+ def __init__(self, interface: Union["ensight_api.ensight", "ensight"]):
39
+ self._ensight = interface
40
+
41
+ def _remote_support_check(self):
42
+ """Determine if ``ensight.utils.export`` exists on the remote system.
43
+
44
+ Before trying to use this module, use this method to determine if this
45
+ module is available in the EnSight instance.
46
+
47
+ Raises
48
+ ------
49
+ RuntimeError if the module is not present.
50
+ """
51
+ # if a module, then we are inside EnSight
52
+ if isinstance(self._ensight, ModuleType): # pragma: no cover
53
+ return # pragma: no cover
54
+ try:
55
+ _ = self._ensight._session.cmd("dir(ensight.utils.export)")
56
+ except RuntimeError: # pragma: no cover
57
+ import ansys.pyensight.core # pragma: no cover
58
+
59
+ raise RuntimeError( # pragma: no cover
60
+ f"Remote EnSight session must have PyEnsight version \
61
+ {ansys.pyensight.core.DEFAULT_ANSYS_VERSION} or higher installed to use this API."
62
+ )
63
+
64
+ TIFFTAG_IMAGEDESCRIPTION: int = 0x010E
65
+
66
+ def image(
67
+ self,
68
+ filename: str,
69
+ width: Optional[int] = None,
70
+ height: Optional[int] = None,
71
+ passes: int = 4,
72
+ enhanced: bool = False,
73
+ raytrace: bool = False,
74
+ ) -> None:
75
+ """Render an image of the current EnSight scene.
76
+
77
+ Parameters
78
+ ----------
79
+ filename : str
80
+ Name of the local file to save the image to.
81
+ width : int, optional
82
+ Width of the image in pixels. The default is ``None``, in which case
83
+ ```ensight.objs.core.WINDOWSIZE[0]`` is used.
84
+ height : int, optional
85
+ Height of the image in pixels. The default is ``None``, in which case
86
+ ``ensight.objs.core.WINDOWSIZE[1]`` is used.
87
+ passes : int, optional
88
+ Number of antialiasing passes. The default is ``4``.
89
+ enhanced : bool, optional
90
+ Whether to save the image to the filename specified in the TIFF format.
91
+ The default is ``False``. The TIFF format includes additional channels
92
+ for the per-pixel object and variable information.
93
+ raytrace : bool, optional
94
+ Whether to render the image with the raytracing engine. The default is ``False``.
95
+
96
+ Examples
97
+ --------
98
+ >>> s = LocalLauncher().start()
99
+ >>> s.load_data(f"{s.cei_home}/ensight{s.cei_suffix}/data/cube/cube.case")
100
+ >>> s.ensight.utils.export.image("example.png")
101
+
102
+ """
103
+ self._remote_support_check()
104
+
105
+ win_size = self._ensight.objs.core.WINDOWSIZE
106
+ if width is None:
107
+ width = win_size[0]
108
+ if height is None:
109
+ height = win_size[1]
110
+
111
+ if isinstance(self._ensight, ModuleType): # pragma: no cover
112
+ raw_image = self._image_remote(
113
+ width, height, passes, enhanced, raytrace
114
+ ) # pragma: no cover
115
+ else:
116
+ cmd = f"ensight.utils.export._image_remote({width}, {height}, {passes}, "
117
+ cmd += f"{enhanced}, {raytrace})"
118
+ raw_image = self._ensight._session.cmd(cmd)
119
+
120
+ pil_image = self._dict_to_pil(raw_image)
121
+ if enhanced:
122
+ tiffinfo_dir = {self.TIFFTAG_IMAGEDESCRIPTION: raw_image["metadata"]}
123
+ pil_image[0].save(
124
+ filename,
125
+ save_all=True,
126
+ append_images=[pil_image[1], pil_image[2]],
127
+ tiffinfo=tiffinfo_dir,
128
+ )
129
+ else:
130
+ pil_image[0].save(filename)
131
+
132
+ def _dict_to_pil(self, data: dict) -> list:
133
+ """Convert the contents of the dictionary into a PIL image.
134
+
135
+ Parameters
136
+ ----------
137
+ data : dict
138
+ Dictionary representation of the contents of the ``enve`` object.
139
+
140
+ Returns
141
+ -------
142
+ list
143
+ List of one or three image objects, [RGB {, pick, variable}].
144
+ """
145
+ images = [
146
+ Image.fromarray(self._numpy_from_dict(data["pixeldata"])).transpose(
147
+ Image.FLIP_TOP_BOTTOM
148
+ )
149
+ ]
150
+ if data.get("variabledata", None) and data.get("pickdata", None):
151
+ images.append(
152
+ Image.fromarray(self._numpy_from_dict(data["pickdata"])).transpose(
153
+ Image.FLIP_TOP_BOTTOM
154
+ )
155
+ )
156
+ images.append(
157
+ Image.fromarray(self._numpy_from_dict(data["variabledata"])).transpose(
158
+ Image.FLIP_TOP_BOTTOM
159
+ )
160
+ )
161
+ return images
162
+
163
+ @staticmethod
164
+ def _numpy_to_dict(array: Any) -> Optional[dict]:
165
+ """Convert a numpy array into a dictionary.
166
+
167
+ Parameters
168
+ ----------
169
+ array:
170
+ Numpy array or None.
171
+
172
+ Returns
173
+ -------
174
+ ``None`` or a dictionary that can be serialized.
175
+ """
176
+ if array is None:
177
+ return None
178
+ return dict(shape=array.shape, dtype=array.dtype.str, data=array.tostring())
179
+
180
+ @staticmethod
181
+ def _numpy_from_dict(obj: Optional[dict]) -> Any:
182
+ """Convert a dictionary into a numpy array.
183
+
184
+ Parameters
185
+ ----------
186
+ obj:
187
+ Dictionary generated by ``_numpy_to_dict`` or ``None``.
188
+
189
+ Returns
190
+ -------
191
+ ``None`` or a numpy array.
192
+ """
193
+ if obj is None:
194
+ return None
195
+ return numpy.frombuffer(obj["data"], dtype=obj["dtype"]).reshape(obj["shape"])
196
+
197
+ def _image_remote(
198
+ self, width: int, height: int, passes: int, enhanced: bool, raytrace: bool
199
+ ) -> dict:
200
+ """EnSight-side implementation.
201
+
202
+ Parameters
203
+ ----------
204
+ width : int
205
+ Width of the image in pixels.
206
+ height : int
207
+ Height of the image in pixels.
208
+ passes : int
209
+ Number of antialiasing passes.
210
+ enhanced : bool
211
+ Whether to returned the image as a "deep pixel" TIFF image file.
212
+ raytrace :
213
+ Whether to render the image with the raytracing engine.
214
+
215
+ Returns
216
+ -------
217
+ dict
218
+ Dictionary of the various channels.
219
+ """
220
+ if not raytrace:
221
+ img = ensight.render(x=width, y=height, num_samples=passes, enhanced=enhanced)
222
+ else:
223
+ with tempfile.TemporaryDirectory() as tmpdirname:
224
+ tmpfilename = os.path.join(tmpdirname, str(uuid.uuid1()))
225
+ ensight.file.image_format("png")
226
+ ensight.file.image_file(tmpfilename)
227
+ ensight.file.image_window_size("user_defined")
228
+ ensight.file.image_window_xy(width, height)
229
+ ensight.file.image_rend_offscreen("ON")
230
+ ensight.file.image_numpasses(passes)
231
+ ensight.file.image_stereo("current")
232
+ ensight.file.image_screen_tiling(1, 1)
233
+ ensight.file.raytracer_options("fgoverlay 1 imagedenoise 1 quality 5")
234
+ ensight.file.image_raytrace_it("ON")
235
+ ensight.file.save_image()
236
+ img = enve.image()
237
+ img.load(f"{tmpfilename}.png")
238
+ # get the channels from the enve.image instance
239
+ output = dict(width=width, height=height, metadata=img.metadata)
240
+ # extract the channels from the image
241
+ output["pixeldata"] = self._numpy_to_dict(img.pixeldata)
242
+ output["variabledata"] = self._numpy_to_dict(img.variabledata)
243
+ output["pickdata"] = self._numpy_to_dict(img.pickdata)
244
+ return output
245
+
246
+ ANIM_TYPE_SOLUTIONTIME: int = 0
247
+ ANIM_TYPE_ANIMATEDTRACES: int = 1
248
+ ANIM_TYPE_FLIPBOOK: int = 2
249
+ ANIM_TYPE_KEYFRAME: int = 3
250
+
251
+ def animation(
252
+ self,
253
+ filename: str,
254
+ width: Optional[int] = None,
255
+ height: Optional[int] = None,
256
+ passes: int = 4,
257
+ anim_type: int = ANIM_TYPE_SOLUTIONTIME,
258
+ frames: Optional[int] = None,
259
+ starting_frame: int = 0,
260
+ frames_per_second: float = 60.0,
261
+ format_options: Optional[str] = "",
262
+ raytrace: bool = False,
263
+ ) -> None:
264
+ """Generate an MPEG4 animation file.
265
+
266
+ An MPEG4 animation file can be generated from temporal data, flipbooks, keyframes,
267
+ or animated traces.
268
+
269
+ Parameters
270
+ ----------
271
+ filename : str
272
+ Name for the MPEG4 file to save to local disk.
273
+ width : int, optional
274
+ Width of the image in pixels. The default is ``None``, in which case
275
+ ``ensight.objs.core.WINDOWSIZE[0]`` is used.
276
+ height : int, optional
277
+ Height of the image in pixels. The default is ``None``, in which case
278
+ ``ensight.objs.core.WINDOWSIZE[1]`` is used.
279
+ passes : int, optional
280
+ Number of antialiasing passes. The default is ``4``.
281
+ anim_type : int, optional
282
+ Type of the animation to render. The default is ``0``, in which case
283
+ ``"ANIM_TYPE_SOLUTIONTIME"`` is used. This table provides descriptions
284
+ by each option number and name:
285
+
286
+ =========================== ========================================
287
+ Name Animation type
288
+ =========================== ========================================
289
+ 0: ANIM_TYPE_SOLUTIONTIME Animation over all solution times
290
+ 1: ANIM_TYPE_ANIMATEDTRACES Records animated rotations and traces
291
+ 2: ANIM_TYPE_FLIPBOOK Records current flipbook animation
292
+ 3: ANIM_TYPE_KEYFRAME Records current kKeyframe animation
293
+ =========================== ========================================
294
+
295
+ frames : int, optional
296
+ Number of frames to save. The default is ``None``. The default for
297
+ all but ``ANIM_TYPE_ANIMATEDTRACES`` covers all timesteps, flipbook
298
+ pages, or keyframe steps. If ``ANIM_TYPE_ANIMATEDTRACES`` is specified,
299
+ this keyword is required.
300
+ starting_frame : int, optional
301
+ Keyword for saving a subset of the complete collection of frames.
302
+ The default is ``0``.
303
+ frames_per_second : float, optional
304
+ Number of frames per second for playback in the saved animation.
305
+ The default is ``60.0``.
306
+ format_options : str, optional
307
+ More specific options for the MPEG4 encoder. The default is ``""``.
308
+ raytrace : bool, optional
309
+ Whether to render the image with the raytracing engine. The default is ``False``.
310
+
311
+ Examples
312
+ --------
313
+ >>> s = LocalLauncher().start()
314
+ >>> data = f"{s.cei_home}/ensight{s.cei_suffix}gui/demos/Crash Queries.ens"
315
+ >>> s.ensight.objs.ensxml_restore_file(data)
316
+ >>> quality = "Quality Best Type 1"
317
+ >>> s.ensight.utils.export.animation("local_file.mp4", format_options=quality)
318
+
319
+ """
320
+ self._remote_support_check()
321
+
322
+ win_size = self._ensight.objs.core.WINDOWSIZE
323
+ if width is None:
324
+ width = win_size[0]
325
+ if height is None:
326
+ height = win_size[1]
327
+
328
+ if format_options is None:
329
+ format_options = "Quality High Type 1"
330
+
331
+ num_frames: int = 0
332
+ if frames is None:
333
+ if anim_type == self.ANIM_TYPE_SOLUTIONTIME:
334
+ num_timesteps = self._ensight.objs.core.TIMESTEP_LIMITS[1]
335
+ num_frames = num_timesteps - starting_frame
336
+ elif anim_type == self.ANIM_TYPE_ANIMATEDTRACES:
337
+ raise RuntimeError("frames is a required keyword with ANIMATEDTRACES animations")
338
+ elif anim_type == self.ANIM_TYPE_FLIPBOOK:
339
+ num_flip_pages = len(self._ensight.objs.core.FLIPBOOKS[0].PAGE_DETAILS)
340
+ num_frames = num_flip_pages - starting_frame
341
+ elif anim_type == self.ANIM_TYPE_KEYFRAME:
342
+ num_keyframe_pages = self._ensight.objs.core.KEYFRAMEDATA["totalFrames"]
343
+ num_frames = num_keyframe_pages - starting_frame
344
+ else:
345
+ num_frames = frames
346
+
347
+ if num_frames < 1: # pragma: no cover
348
+ raise RuntimeError( # pragma: no cover
349
+ "No frames selected. Perhaps a static dataset SOLUTIONTIME request \
350
+ or no FLIPBOOK/KEYFRAME defined."
351
+ )
352
+
353
+ if isinstance(self._ensight, ModuleType): # pragma: no cover
354
+ raw_mpeg4 = self._animation_remote( # pragma: no cover
355
+ width,
356
+ height,
357
+ passes,
358
+ anim_type,
359
+ starting_frame,
360
+ num_frames,
361
+ frames_per_second,
362
+ format_options,
363
+ raytrace,
364
+ )
365
+ else:
366
+ cmd = f"ensight.utils.export._animation_remote({width}, {height}, {passes}, "
367
+ cmd += f"{anim_type}, {starting_frame}, {num_frames}, "
368
+ cmd += f"{frames_per_second}, '{format_options}', {raytrace})"
369
+ raw_mpeg4 = self._ensight._session.cmd(cmd)
370
+
371
+ with open(filename, "wb") as fp:
372
+ fp.write(raw_mpeg4)
373
+
374
+ def _animation_remote(
375
+ self,
376
+ width: int,
377
+ height: int,
378
+ passes: int,
379
+ anim_type: int,
380
+ start: int,
381
+ frames: int,
382
+ fps: float,
383
+ options: str,
384
+ raytrace: bool,
385
+ ) -> bytes:
386
+ """EnSight-side implementation.
387
+
388
+ Parameters
389
+ ----------
390
+ width : int
391
+ Width of the image in pixels.
392
+ height : int
393
+ Height of the image in pixels.
394
+ passes : int
395
+ Number of antialiasing passes.
396
+ anim_type : int
397
+ Type of animation to save.
398
+ start : int
399
+ First frame number to save.
400
+ frames : int
401
+ Number of frames to save.
402
+ fps : float
403
+ Output framerate.
404
+ options : str
405
+ MPEG4 configuration options.
406
+ raytrace : bool
407
+ Whether to render the image with the raytracing engine.
408
+
409
+ Returns
410
+ -------
411
+ bytes
412
+ MPEG4 stream in bytes.
413
+ """
414
+
415
+ with tempfile.TemporaryDirectory() as tmpdirname:
416
+ tmpfilename = os.path.join(tmpdirname, str(uuid.uuid1()) + ".mp4")
417
+ self._ensight.file.animation_rend_offscreen("ON")
418
+ self._ensight.file.animation_screen_tiling(1, 1)
419
+ self._ensight.file.animation_format("mpeg4")
420
+ if options:
421
+ self._ensight.file.animation_format_options(options)
422
+ self._ensight.file.animation_frame_rate(fps)
423
+ self._ensight.file.animation_rend_offscreen("ON")
424
+ self._ensight.file.animation_numpasses(passes)
425
+ self._ensight.file.animation_stereo("mono")
426
+ self._ensight.file.animation_screen_tiling(1, 1)
427
+ self._ensight.file.animation_file(tmpfilename)
428
+ self._ensight.file.animation_window_size("user_defined")
429
+ self._ensight.file.animation_window_xy(width, height)
430
+ self._ensight.file.animation_frames(frames)
431
+ self._ensight.file.animation_start_number(start)
432
+ self._ensight.file.animation_multiple_images("OFF")
433
+ if raytrace:
434
+ self._ensight.file.animation_raytrace_it("ON")
435
+ else:
436
+ self._ensight.file.animation_raytrace_it("OFF")
437
+ self._ensight.file.animation_raytrace_ext("OFF")
438
+
439
+ self._ensight.file.animation_play_time("OFF")
440
+ self._ensight.file.animation_play_flipbook("OFF")
441
+ self._ensight.file.animation_play_keyframe("OFF")
442
+
443
+ self._ensight.file.animation_reset_time("OFF")
444
+ self._ensight.file.animation_reset_traces("OFF")
445
+ self._ensight.file.animation_reset_flipbook("OFF")
446
+ self._ensight.file.animation_reset_keyframe("OFF")
447
+
448
+ if anim_type == self.ANIM_TYPE_SOLUTIONTIME:
449
+ # playing over time
450
+ self._ensight.file.animation_play_time("ON")
451
+ self._ensight.file.animation_reset_time("ON")
452
+ elif anim_type == self.ANIM_TYPE_ANIMATEDTRACES:
453
+ # recording particle traces/etc
454
+ self._ensight.file.animation_reset_traces("ON")
455
+ elif anim_type == self.ANIM_TYPE_KEYFRAME:
456
+ self._ensight.file.animation_reset_keyframe("ON")
457
+ self._ensight.file.animation_play_keyframe("ON")
458
+ elif anim_type == self.ANIM_TYPE_FLIPBOOK:
459
+ self._ensight.file.animation_play_flipbook("ON")
460
+ self._ensight.file.animation_reset_flipbook("ON")
461
+
462
+ self._ensight.file.save_animation()
463
+
464
+ with open(tmpfilename, "rb") as fp:
465
+ mp4_data = fp.read()
466
+
467
+ return mp4_data
468
+
469
+ GEOM_EXPORT_GLTF = "gltf2"
470
+ GEOM_EXPORT_AVZ = "avz"
471
+ GEOM_EXPORT_PLY = "ply"
472
+ GEOM_EXPORT_STL = "stl"
473
+
474
+ extension_map = {
475
+ GEOM_EXPORT_GLTF: ".glb",
476
+ GEOM_EXPORT_AVZ: ".avz",
477
+ GEOM_EXPORT_PLY: ".ply",
478
+ GEOM_EXPORT_STL: ".stl",
479
+ }
480
+
481
+ def _geometry_remote( # pragma: no cover
482
+ self, format: str, starting_timestep: int, frames: int, delta_timestep: int
483
+ ) -> List[bytes]:
484
+ """EnSight-side implementation.
485
+
486
+ Parameters
487
+ ----------
488
+ format : str
489
+ The format to export
490
+ starting_timestep: int
491
+ The first timestep to export. If None, defaults to the current timestep
492
+ frames: int
493
+ Number of timesteps to save. If None, defaults from the current timestep to the last
494
+ delta_timestep: int
495
+ The delta timestep to use when exporting
496
+
497
+ Returns
498
+ -------
499
+ bytes
500
+ Geometry export in bytes
501
+ """
502
+ rawdata = None
503
+ extension = self.extension_map.get(format)
504
+ rawdata_list = []
505
+ if not extension:
506
+ raise RuntimeError("The geometry export format provided is not supported.")
507
+ with tempfile.TemporaryDirectory() as tmpdirname:
508
+ self._ensight.part.select_all()
509
+ self._ensight.savegeom.format(format)
510
+ self._ensight.savegeom.begin_step(starting_timestep)
511
+ # frames is 1-indexed, so I need to decrease of 1
512
+ self._ensight.savegeom.end_step(starting_timestep + frames - 1)
513
+ self._ensight.savegeom.step_by(delta_timestep)
514
+ tmpfilename = os.path.join(tmpdirname, str(uuid.uuid1()))
515
+ self._ensight.savegeom.save_geometric_entities(tmpfilename)
516
+ files = glob.glob(f"{tmpfilename}*{extension}")
517
+ for export_file in files:
518
+ with open(export_file, "rb") as tmpfile:
519
+ rawdata = tmpfile.read()
520
+ rawdata_list.append(rawdata)
521
+ return rawdata_list
522
+
523
+ def geometry(
524
+ self,
525
+ filename: str,
526
+ format: str = GEOM_EXPORT_GLTF,
527
+ starting_timestep: Optional[int] = None,
528
+ frames: Optional[int] = 1,
529
+ delta_timestep: Optional[int] = None,
530
+ ) -> None:
531
+ """Export a geometry file.
532
+
533
+ Parameters
534
+ ----------
535
+ filename: str
536
+ The location where to export the geometry
537
+ format : str
538
+ The format to export
539
+ starting_timestep: int
540
+ The first timestep to export. If None, defaults to the current timestep
541
+ frames: int
542
+ Number of timesteps to save. If None, defaults from the current timestep to the last
543
+ delta_timestep: int
544
+ The delta timestep to use when exporting
545
+
546
+ Examples
547
+ --------
548
+ >>> s = LocalLauncher().start()
549
+ >>> data = f"{s.cei_home}/ensight{s.cei_suffix}gui/demos/Crash Queries.ens"
550
+ >>> s.ensight.objs.ensxml_restore_file(data)
551
+ >>> s.ensight.utils.export.geometry("local_file.glb", format=s.ensight.utils.export.GEOM_EXPORT_GLTF)
552
+ """
553
+ if starting_timestep is None:
554
+ starting_timestep = int(self._ensight.objs.core.TIMESTEP)
555
+ if frames is None or frames == -1:
556
+ # Timesteps are 0-indexed so frames need to be increased of 1
557
+ frames = int(self._ensight.objs.core.TIMESTEP_LIMITS[1]) + 1
558
+ if not delta_timestep:
559
+ delta_timestep = 1
560
+ self._remote_support_check()
561
+ raw_data_list = None
562
+ if isinstance(self._ensight, ModuleType): # pragma: no cover
563
+ raw_data_list = self._geometry_remote( # pragma: no cover
564
+ format,
565
+ starting_timestep=starting_timestep,
566
+ frames=frames,
567
+ delta_timestep=delta_timestep,
568
+ )
569
+ else:
570
+ self._ensight._session.ensight_version_check("2024 R2")
571
+ cmd = f"ensight.utils.export._geometry_remote('{format}', {starting_timestep}, {frames}, {delta_timestep})"
572
+ raw_data_list = self._ensight._session.cmd(cmd)
573
+ if raw_data_list: # pragma: no cover
574
+ if len(raw_data_list) == 1:
575
+ with open(filename, "wb") as fp:
576
+ fp.write(raw_data_list[0])
577
+ else:
578
+ for idx, raw_data in enumerate(raw_data_list):
579
+ filename_base, extension = os.path.splitext(filename)
580
+ _filename = f"{filename_base}{str(idx).zfill(3)}{extension}"
581
+ with open(_filename, "wb") as fp:
582
+ fp.write(raw_data)
583
+ else: # pragma: no cover
584
+ raise IOError("Export was not successful") # pragma: no cover