ansys-pyensight-core 0.11.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (37) hide show
  1. ansys/pyensight/core/__init__.py +41 -0
  2. ansys/pyensight/core/common.py +341 -0
  3. ansys/pyensight/core/deep_pixel_view.html +98 -0
  4. ansys/pyensight/core/dockerlauncher.py +1124 -0
  5. ansys/pyensight/core/dvs.py +872 -0
  6. ansys/pyensight/core/enscontext.py +345 -0
  7. ansys/pyensight/core/enshell_grpc.py +641 -0
  8. ansys/pyensight/core/ensight_grpc.py +874 -0
  9. ansys/pyensight/core/ensobj.py +515 -0
  10. ansys/pyensight/core/launch_ensight.py +296 -0
  11. ansys/pyensight/core/launcher.py +388 -0
  12. ansys/pyensight/core/libuserd.py +2110 -0
  13. ansys/pyensight/core/listobj.py +280 -0
  14. ansys/pyensight/core/locallauncher.py +579 -0
  15. ansys/pyensight/core/py.typed +0 -0
  16. ansys/pyensight/core/renderable.py +880 -0
  17. ansys/pyensight/core/session.py +1923 -0
  18. ansys/pyensight/core/sgeo_poll.html +24 -0
  19. ansys/pyensight/core/utils/__init__.py +21 -0
  20. ansys/pyensight/core/utils/adr.py +111 -0
  21. ansys/pyensight/core/utils/dsg_server.py +1220 -0
  22. ansys/pyensight/core/utils/export.py +606 -0
  23. ansys/pyensight/core/utils/omniverse.py +769 -0
  24. ansys/pyensight/core/utils/omniverse_cli.py +614 -0
  25. ansys/pyensight/core/utils/omniverse_dsg_server.py +1196 -0
  26. ansys/pyensight/core/utils/omniverse_glb_server.py +848 -0
  27. ansys/pyensight/core/utils/parts.py +1221 -0
  28. ansys/pyensight/core/utils/query.py +487 -0
  29. ansys/pyensight/core/utils/readers.py +300 -0
  30. ansys/pyensight/core/utils/resources/Materials/000_sky.exr +0 -0
  31. ansys/pyensight/core/utils/support.py +128 -0
  32. ansys/pyensight/core/utils/variables.py +2019 -0
  33. ansys/pyensight/core/utils/views.py +674 -0
  34. ansys_pyensight_core-0.11.0.dist-info/METADATA +309 -0
  35. ansys_pyensight_core-0.11.0.dist-info/RECORD +37 -0
  36. ansys_pyensight_core-0.11.0.dist-info/WHEEL +4 -0
  37. ansys_pyensight_core-0.11.0.dist-info/licenses/LICENSE +21 -0
@@ -0,0 +1,880 @@
1
+ # Copyright (C) 2022 - 2026 ANSYS, Inc. and/or its affiliates.
2
+ # SPDX-License-Identifier: MIT
3
+ #
4
+ #
5
+ # Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ # of this software and associated documentation files (the "Software"), to deal
7
+ # in the Software without restriction, including without limitation the rights
8
+ # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ # copies of the Software, and to permit persons to whom the Software is
10
+ # furnished to do so, subject to the following conditions:
11
+ #
12
+ # The above copyright notice and this permission notice shall be included in all
13
+ # copies or substantial portions of the Software.
14
+ #
15
+ # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ # SOFTWARE.
22
+
23
+ """Renderable module.
24
+
25
+ This module provides the interface for creating objects in the EnSight session
26
+ that can be displayed via HTML over the websocket server interface.
27
+ """
28
+ import hashlib
29
+ import os
30
+ import shutil
31
+ from typing import TYPE_CHECKING, Any, Dict, List, Optional, Tuple, Union, no_type_check
32
+ import uuid
33
+ import warnings
34
+ import webbrowser
35
+
36
+ import requests
37
+
38
+ if TYPE_CHECKING:
39
+ from ansys.pyensight.core import Session
40
+
41
+
42
+ def _get_ansysnexus_version(version: Union[int, str]) -> str:
43
+ if int(version) < 242:
44
+ return ""
45
+ return str(version)
46
+
47
+
48
+ class Renderable:
49
+ """Generates HTML pages for renderable entities.
50
+
51
+ This class provides the underlying HTML remote webpage generation for
52
+ the :func:`show<ansys.pyensight.core.Session.show>` method. The approach
53
+ is to generate the renderable in the EnSight session and make the
54
+ artifacts available via the websocket server. The artifacts are then
55
+ wrapped with simple HTML pages, which are also served up by the websocket
56
+ server. These HTML pages can then be used to populate iframes.
57
+
58
+ Parameters
59
+ ----------
60
+ session :
61
+ PyEnSight session to generate renderables for.
62
+ cell_handle :
63
+ Jupyter notebook cell handle (if any). The default is ``None``.
64
+ width : int, optional
65
+ Width of the renderable. The default is ``None``.
66
+ height : int, optional
67
+ Height of the renderable. The default is ``None``.
68
+ temporal : bool, optional
69
+ Whether to show data for all timesteps in an interactive
70
+ WebGL-based browser viewer. The default is ``False``.
71
+ aa : int, optional
72
+ Number of antialiasing passes to use when rendering images.
73
+ The default is ``1``.
74
+ fps : float, optional
75
+ Number of frames per second to use for animation playback. The
76
+ default is ``30.0``.
77
+ num_frames : int, optional
78
+ Number of frames of static timestep to record for animation playback.
79
+ The default is ``None``.
80
+
81
+ """
82
+
83
+ def __init__(
84
+ self,
85
+ session: "Session",
86
+ cell_handle: Optional[Any] = None,
87
+ width: Optional[int] = None,
88
+ height: Optional[int] = None,
89
+ temporal: bool = False,
90
+ aa: int = 1,
91
+ fps: float = 30.0,
92
+ num_frames: Optional[int] = None,
93
+ ) -> None:
94
+ self._session = session
95
+ self._filename_index: int = 0
96
+ self._guid: str = str(uuid.uuid1()).replace("-", "")
97
+ self._download_names: List[str] = []
98
+ # The Jupyter notebook cell handle (if any)
99
+ self._cell_handle = cell_handle
100
+ # the URL to the base HTML file for this entity
101
+ self._url: Optional[str] = None
102
+ # the pathname of the HTML file in the remote EnSight session
103
+ self._url_remote_pathname: Optional[str] = None
104
+ # the name passed to the pyensight session show() string
105
+ self._rendertype: str = ""
106
+ # Common attributes used by various subclasses
107
+ self._width: Optional[int] = width
108
+ self._height: Optional[int] = height
109
+ self._temporal: bool = temporal
110
+ self._aa: int = aa
111
+ self._fps: float = fps
112
+ self._num_frames: Optional[int] = num_frames
113
+ #
114
+ self._using_proxy = False
115
+ # if we're talking directly to WS, then use 'http' otherwise 'https' for the proxy
116
+ self._http_protocol = "http"
117
+ try:
118
+ if self._session.launcher._pim_instance is not None:
119
+ self._using_proxy = True
120
+ self._http_protocol = "https"
121
+ except Exception:
122
+ # the launcher may not be PIM aware; that's ok
123
+ pass
124
+
125
+ def __repr__(self) -> str:
126
+ name = self.__class__.__name__
127
+ return f"{name}( url='{self._url}' )"
128
+
129
+ @no_type_check
130
+ def _repr_pretty_(self, p: "pretty", cycle: bool) -> None:
131
+ """Support the pretty module for better IPython support.
132
+
133
+ Parameters
134
+ ----------
135
+ p : text, optional
136
+ Pretty flag. The default is ``"pretty"``.
137
+ cycle : bool, optional
138
+ Cycle flag. The default is ``None``.
139
+
140
+ """
141
+ name = self.__class__.__name__
142
+ p.text(f"{name}( url='{self._url}' )")
143
+
144
+ def _get_query_parameters_str(self, params: Optional[Dict[str, str]] = None) -> str:
145
+ """Generate any optional http query parameters.
146
+ Return a string formatted as a URL query to tack on to the
147
+ beginning part of the URL. The string may be empty if there
148
+ aren't any parameters. The method takes a dict
149
+ of parameters, possibly empty or None, and combines it with the
150
+ parameters from the launcher, which also may be empty.
151
+ """
152
+ qp_dict = self._session.launcher._get_query_parameters()
153
+ if qp_dict is None:
154
+ # just in case
155
+ qp_dict = {}
156
+ if params:
157
+ qp_dict.update(params)
158
+ query_parameter_str = ""
159
+ symbol = "?"
160
+ for p in qp_dict.items():
161
+ query_parameter_str += f"{symbol}{p[0]}={p[1]}"
162
+ symbol = "&"
163
+ return query_parameter_str
164
+
165
+ def _generate_filename(self, suffix: str) -> Tuple[str, str]:
166
+ """Create session-specific files and URLs.
167
+
168
+ Every time this method is called, a new filename (on the EnSight host)
169
+ and the associated URL for that file are generated. The caller
170
+ provides the suffix for the names.
171
+
172
+ Parameters
173
+ ----------
174
+ suffix: str
175
+ Suffix of the file.
176
+
177
+ Returns
178
+ -------
179
+ Tuple[str, str]
180
+ Filename to use on the host system and the URL that accesses the
181
+ file via REST calls to the websocket server.
182
+
183
+ """
184
+ filename = f"{self._session.secret_key}_{self._guid}_{self._filename_index}{suffix}"
185
+ # Note: cannot use os.path.join here as the OS of the EnSight session might not match
186
+ # the client OS.
187
+ pathname = f"{self._session.launcher.session_directory}/{filename}"
188
+ self._filename_index += 1
189
+ return pathname, filename
190
+
191
+ def _generate_url(self) -> None:
192
+ """Build the remote HTML filename and associated URL.
193
+
194
+ On the remote system the, pathname to the HTML file is
195
+ ``{session_directory}/{session}_{guid}_{index}_{type}.html``.
196
+ The URL to the file (through the session HTTP server) is
197
+ ``http://{system}:{websocketserverhtmlport}/{session}_{guid}_{index}_{type}.html``.
198
+
199
+ Note that there may be optional http query parameters at the end of the URL.
200
+
201
+ After this call, ``_url`` and ``_url_remote_pathname`` reflect these names.
202
+
203
+ """
204
+ suffix = f"_{self._rendertype}.html"
205
+ filename_index = self._filename_index
206
+ remote_pathname, _ = self._generate_filename(suffix)
207
+ simple_filename = f"{self._session.secret_key}_{self._guid}_{filename_index}{suffix}"
208
+ url = f"{self._http_protocol}://{self._session.html_hostname}:{self._session.html_port}"
209
+ self._url = f"{url}/{simple_filename}{self._get_query_parameters_str()}"
210
+ self._url_remote_pathname = remote_pathname
211
+
212
+ def _save_remote_html_page(self, html: str) -> None:
213
+ """Create an HTML webpage on the remote host.
214
+
215
+ Given a snippet of HTML, create a file on the remote server with the
216
+ name generated by the ``_generate_url()`` method.
217
+ The most common use is to generate an "iframe" wrapper around some HTML
218
+ snippet.
219
+
220
+ Parameters
221
+ ----------
222
+ html : str
223
+ HTML snippet to wrap remotely.
224
+
225
+ """
226
+ # save "html" into a file on the remote server with filename .html
227
+ cmd = f'open(r"""{self._url_remote_pathname}""", "w").write("""{html}""")'
228
+ self._session.grpc.command(cmd, do_eval=False)
229
+
230
+ def browser(self) -> None:
231
+ """Open a web browser page to display the renderable content."""
232
+ if self._url:
233
+ webbrowser.open(self._url)
234
+
235
+ @property
236
+ def url(self) -> Optional[str]:
237
+ """URL to the renderable content."""
238
+ return self._url
239
+
240
+ def _default_size(self, width: int, height: int) -> Tuple[int, int]:
241
+ """Propose and return a size for a rectangle.
242
+
243
+ The renderable may have been constructed with user-supplied width and height
244
+ information. If so, that information is returned. If not, the width and
245
+ height values passed to this method are returned.
246
+
247
+ Parameters
248
+ ----------
249
+ width : int
250
+ Width value to return if the renderable does not have a width.
251
+ height : int
252
+ Height value to return if the renderable does not have a height.
253
+
254
+ Returns
255
+ -------
256
+ Tuple[int, int]
257
+ Tuple (width, height) of the size values to use.
258
+
259
+ """
260
+ out_w = self._width
261
+ if out_w is None:
262
+ out_w = width
263
+ out_h = self._height
264
+ if out_h is None:
265
+ out_h = height
266
+ return out_w, out_h
267
+
268
+ def update(self) -> None:
269
+ """Update the visualization and display it.
270
+
271
+ When this method is called, the graphics content is updated to the
272
+ current EnSight instance state. For example, an image might be re-captured.
273
+ The URL of the content stays the same, but the content that the URL displays is
274
+ updated.
275
+
276
+ If the renderable was created in the context of a Jupyter notebook cell,
277
+ the original cell display is updated.
278
+
279
+ """
280
+ if self._cell_handle:
281
+ from IPython.display import IFrame
282
+
283
+ width, height = self._default_size(800, 600)
284
+ self._cell_handle.update(IFrame(src=self._url, width=width, height=height))
285
+
286
+ def delete(self) -> None:
287
+ """Delete all server resources for the renderable.
288
+
289
+ A renderable occupies resources in the EnSight :class:`Session<ansys.pyensight.core.Session>`
290
+ instance. This method releases those resources. Once this method is called, the renderable
291
+ can no longer be displayed.
292
+
293
+ Notes
294
+ -----
295
+ This method has not yet been implemented.
296
+
297
+ """
298
+ pass
299
+
300
+ def download(self, dirname: str) -> List[str]:
301
+ """Download the content files for the renderable.
302
+
303
+ A renderable saves files (such as images, mpegs, and geometry) in the EnSight instance.
304
+ Normally, these files are accessed via the webpage specified in the URL property.
305
+ This method allows for those files to be downloaded to a local directory so that they
306
+ can be used for other purposes.
307
+
308
+ .. note::
309
+ Any previously existing files with the same name are overwritten.
310
+
311
+ Parameters
312
+ ----------
313
+ dirname : str
314
+ Name of the existing directory to save the files to.
315
+
316
+
317
+ Returns
318
+ -------
319
+ list
320
+ List of names for the downloaded files.
321
+
322
+ Examples
323
+ --------
324
+ Download the PNG file generated by the image renderable.
325
+
326
+ >>> img = session.show('image", width=640, height=480, aa=4)
327
+ >>> names = img.download("/tmp")
328
+ >>> png_pathname = os.path.join("/tmp", names[0])
329
+
330
+ """
331
+ for filename in self._download_names:
332
+ url = f"{self._http_protocol}://{self._session.html_hostname}:{self._session.html_port}/{filename}{self._get_query_parameters_str()}"
333
+ outpath = os.path.join(dirname, filename)
334
+ with requests.get(url, stream=True) as r:
335
+ with open(outpath, "wb") as f:
336
+ shutil.copyfileobj(r.raw, f)
337
+ return self._download_names
338
+
339
+
340
+ class RenderableImage(Renderable):
341
+ """Renders an image on the EnSight host system and makes it available via a webpage."""
342
+
343
+ def __init__(self, *args, **kwargs) -> None:
344
+ """Initialize RenderableImage."""
345
+ super().__init__(*args, **kwargs)
346
+ self._rendertype = "image"
347
+ self._generate_url()
348
+ # the HTML serves up a PNG file
349
+ pathname, filename = self._generate_filename(".png")
350
+ self._png_pathname = pathname
351
+ self._png_filename = filename
352
+ # the download is the png file
353
+ self._download_names.append(self._png_filename)
354
+ self.update()
355
+
356
+ def update(self):
357
+ """Update the image and display it.
358
+
359
+ If the renderable is part of a Jupyter notebook cell, that cell is updated
360
+ as an iframe reference.
361
+
362
+ """
363
+ # save the image file on the remote host
364
+ w, h = self._default_size(1920, 1080)
365
+ cmd = f'ensight.render({w},{h},num_samples={self._aa}).save(r"""{self._png_pathname}""")'
366
+ self._session.cmd(cmd)
367
+ # generate HTML page with file references local to the websocket server root
368
+ html = '<body style="margin:0px;padding:0px;">\n'
369
+ html += f'<img src="/{self._png_filename}{self._get_query_parameters_str()}">\n'
370
+ html += "</body>\n"
371
+ # refresh the remote HTML
372
+ self._save_remote_html_page(html)
373
+ super().update()
374
+
375
+
376
+ class RenderableDeepPixel(Renderable):
377
+ """Renders a deep pixel image on the EnSight host system and makes it available via a webpage."""
378
+
379
+ def __init__(self, *args, **kwargs) -> None:
380
+ super().__init__(*args, **kwargs)
381
+ self._rendertype = "deep_pixel"
382
+ self._generate_url()
383
+ pathname, filename = self._generate_filename(".tif")
384
+ self._tif_pathname = pathname
385
+ self._tif_filename = filename
386
+ # the download is the tiff file
387
+ self._download_names.append(self._tif_filename)
388
+ self.update()
389
+
390
+ def update(self):
391
+ """Update the deep pixel image and display it.
392
+
393
+ If the renderable is part of a Jupyter notebook cell, that cell is updated as
394
+ an iframe reference.
395
+ """
396
+ # save the (deep) image file
397
+ # get the optional query parameters which may be an empty string
398
+ # needed for proxy servers like ansys lab
399
+ optional_query = self._get_query_parameters_str()
400
+ w, h = self._default_size(1920, 1080)
401
+ deep = f",num_samples={self._aa},enhanced=1"
402
+ cmd = f'ensight.render({w},{h}{deep}).save(r"""{self._tif_pathname}""")'
403
+ self._session.cmd(cmd)
404
+ html_source = os.path.join(os.path.dirname(__file__), "deep_pixel_view.html")
405
+ with open(html_source, "r") as fp:
406
+ html = fp.read()
407
+ # copy some files from Nexus
408
+ cmd = "import shutil, enve, ceiversion, os.path\n"
409
+ base_name = "os.path.join(enve.home(), f'nexus{ceiversion.nexus_suffix}', 'django', "
410
+ base_name += "'website', 'static', 'website', 'scripts', "
411
+ for script in ["geotiff.js", "geotiff_nexus.js", "bootstrap.min.js"]:
412
+ name = base_name + f"'{script}')"
413
+ cmd += f'shutil.copy({name}, r"""{self._session.launcher.session_directory}""")\n'
414
+ name = "os.path.join(enve.home(), f'nexus{ceiversion.nexus_suffix}', 'django', "
415
+ name += "'website', 'static', 'website', 'content', 'bootstrap.min.css')"
416
+ cmd += f'shutil.copy({name}, r"""{self._session.launcher.session_directory}""")\n'
417
+ self._session.cmd(cmd, do_eval=False)
418
+ # With Bootstrap 5 (2025 R1), class names have '-bs-' in them, e.g. 'data-bs-toggle' vs 'data-toggle'
419
+ bs_prefix = "bs-"
420
+ jquery_version = ""
421
+ if int(self._session._cei_suffix) < 251:
422
+ bs_prefix = ""
423
+ jquery_version = "-3.4.1"
424
+ jquery = f"jquery{jquery_version}.min.js"
425
+ cmd = "import shutil, enve, ceiversion, os.path\n"
426
+ name = base_name + f"'{jquery}')"
427
+ cmd += "try:"
428
+ cmd += f' shutil.copy({name}, r"""{self._session.launcher.session_directory}""")\n'
429
+ cmd += "except Exception:"
430
+ cmd += " pass"
431
+ name = "os.path.join(enve.home(), f'nexus{ceiversion.nexus_suffix}', 'django', "
432
+ name += "'website', 'static', 'website', 'content', 'bootstrap.min.css')"
433
+ cmd += f'shutil.copy({name}, r"""{self._session.launcher.session_directory}""")\n'
434
+ self._session.cmd(cmd, do_eval=False)
435
+ url = f"{self._http_protocol}://{self._session.html_hostname}:{self._session.html_port}"
436
+ tiff_url = f"{url}/{self._tif_filename}{optional_query}"
437
+ # replace some bits in the HTML
438
+ html = html.replace("TIFF_URL", tiff_url)
439
+ html = html.replace("ITEMID", self._guid)
440
+ html = html.replace("OPTIONAL_QUERY", optional_query)
441
+ html = html.replace("JQUERY_VERSION", jquery_version)
442
+ html = html.replace("BS_PREFIX", bs_prefix)
443
+ # refresh the remote HTML
444
+ self._save_remote_html_page(html)
445
+ super().update()
446
+
447
+
448
+ class RenderableMP4(Renderable):
449
+ """Renders the timesteps of the current dataset into an MP4 file and displays the results."""
450
+
451
+ def __init__(self, *args, **kwargs) -> None:
452
+ super().__init__(*args, **kwargs)
453
+ self._rendertype = "animation"
454
+ self._generate_url()
455
+ # the HTML serves up a PNG file
456
+ pathname, filename = self._generate_filename(".mp4")
457
+ self._mp4_pathname = pathname
458
+ self._mp4_filename = filename
459
+ # the download is the mp4 file
460
+ self._download_names.append(self._mp4_filename)
461
+ self.update()
462
+
463
+ def update(self):
464
+ """Update the animation and display it.
465
+
466
+ If the renderable is part of a Jupyter notebook cell, that cell is updated as an
467
+ iframe reference.
468
+
469
+ """
470
+ # save the image file on the remote host
471
+ w, h = self._default_size(1920, 1080)
472
+ # Assume this is a particle trace animation save...
473
+ num_frames = self._num_frames
474
+ st = 0
475
+ if self._num_frames is None:
476
+ # get the timestep limits, [0,0] is non-time varying
477
+ st, en = self._session.ensight.objs.core.TIMESTEP_LIMITS
478
+ num_frames = en - st + 1
479
+ self._session.ensight.file.animation_rend_offscreen("ON")
480
+ self._session.ensight.file.animation_screen_tiling(1, 1)
481
+ self._session.ensight.file.animation_format("mpeg4")
482
+ self._session.ensight.file.animation_format_options("Quality High Type 1")
483
+ self._session.ensight.file.animation_frame_rate(self._fps)
484
+ self._session.ensight.file.animation_rend_offscreen("ON")
485
+ self._session.ensight.file.animation_numpasses(self._aa)
486
+ self._session.ensight.file.animation_stereo("mono")
487
+ self._session.ensight.file.animation_screen_tiling(1, 1)
488
+ self._session.ensight.file.animation_file(self._mp4_pathname)
489
+ self._session.ensight.file.animation_window_size("user_defined")
490
+ self._session.ensight.file.animation_window_xy(w, h)
491
+ self._session.ensight.file.animation_frames(num_frames)
492
+ self._session.ensight.file.animation_start_number(st)
493
+ self._session.ensight.file.animation_multiple_images("OFF")
494
+ self._session.ensight.file.animation_raytrace_it("OFF")
495
+ self._session.ensight.file.animation_raytrace_ext("OFF")
496
+ self._session.ensight.file.animation_play_flipbook("OFF")
497
+ self._session.ensight.file.animation_play_keyframe("OFF")
498
+
499
+ if self._num_frames is None:
500
+ # playing over time
501
+ self._session.ensight.file.animation_play_time("ON")
502
+ self._session.ensight.file.animation_reset_traces("OFF")
503
+ self._session.ensight.file.animation_reset_time("ON")
504
+ else:
505
+ # recording particle traces/etc
506
+ self._session.ensight.file.animation_play_time("OFF")
507
+ self._session.ensight.file.animation_reset_traces("ON")
508
+ self._session.ensight.file.animation_reset_time("OFF")
509
+
510
+ self._session.ensight.file.animation_reset_flipbook("OFF")
511
+ self._session.ensight.file.animation_reset_keyframe("OFF")
512
+ self._session.ensight.file.save_animation()
513
+
514
+ # generate HTML page with file references local to the websocket server root
515
+ html = '<body style="margin:0px;padding:0px;">\n'
516
+ html += f'<video width="{w}" height="{h}" controls>\n'
517
+ html += f' <source src="/{self._mp4_filename}{self._get_query_parameters_str()}" type="video/mp4" />\n'
518
+ html += "</video>\n"
519
+ html += "</body>\n"
520
+
521
+ # refresh the remote HTML
522
+ self._save_remote_html_page(html)
523
+ super().update()
524
+
525
+
526
+ class RenderableWebGL(Renderable):
527
+ """Renders an AVZ file (WebGL renderable) on the EnSight host system and makes it available via
528
+ a webpage.
529
+ """
530
+
531
+ def __init__(self, *args, **kwargs) -> None:
532
+ super().__init__(*args, **kwargs)
533
+ self._rendertype = "webgl"
534
+ self._generate_url()
535
+ pathname, filename = self._generate_filename(".avz")
536
+ self._avz_pathname = pathname
537
+ self._avz_filename = filename
538
+ # the download is the avz file
539
+ self._download_names.append(self._avz_filename)
540
+ self.update()
541
+
542
+ def update(self):
543
+ """Update the WebGL geometry and display it.
544
+
545
+ If the renderable is part of a Jupyter notebook cell, that cell is updated as
546
+ an iframe reference.
547
+ """
548
+ # save the .avz file
549
+ self._session.ensight.part.select_all()
550
+ self._session.ensight.savegeom.format("avz")
551
+ # current timestep or all of the timesteps
552
+ ts = self._session.ensight.objs.core.TIMESTEP
553
+ st = ts
554
+ en = ts
555
+ if self._temporal:
556
+ st, en = self._session.ensight.objs.core.TIMESTEP_LIMITS
557
+ self._session.ensight.savegeom.begin_step(st)
558
+ self._session.ensight.savegeom.end_step(en)
559
+ self._session.ensight.savegeom.step_by(1)
560
+ # Save the file
561
+ self._session.ensight.savegeom.save_geometric_entities(self._avz_pathname)
562
+ # generate HTML page with file references local to the websocket server root
563
+ version = _get_ansysnexus_version(self._session._cei_suffix)
564
+ if self._using_proxy:
565
+ # if using pim we get the static content from the front end and not
566
+ # where ensight is running, thus we use a specific URI host and not relative.
567
+ html = f"<script src='{self._http_protocol}://{self._session.html_hostname}:{self._session.html_port}/ansys{version}/nexus/viewer-loader.js'></script>\n"
568
+ html += f"<ansys-nexus-viewer src='{self._http_protocol}://{self._session.html_hostname}:{self._session.html_port}/{self._avz_filename}"
569
+ html += f"{self._get_query_parameters_str()}'></ansys-nexus-viewer>\n"
570
+ else:
571
+ html = f"<script src='/ansys{version}/nexus/viewer-loader.js'></script>\n"
572
+ html += f"<ansys-nexus-viewer src='/{self._avz_filename}{self._get_query_parameters_str()}'></ansys-nexus-viewer>\n"
573
+ # refresh the remote HTML
574
+ self._save_remote_html_page(html)
575
+ super().update()
576
+
577
+
578
+ class RenderableVNC(Renderable):
579
+ """Generates an ansys-nexus-viewer component that can be used to connect to the EnSight VNC remote image renderer."""
580
+
581
+ def __init__(self, *args, **kwargs) -> None:
582
+ ui = kwargs.get("ui")
583
+ if kwargs.get("ui"):
584
+ kwargs.pop("ui")
585
+ super().__init__(*args, **kwargs)
586
+ self._ui = ui
587
+ self._generate_url()
588
+ self._rendertype = "remote"
589
+ self.update()
590
+
591
+ def _update_2023R2_or_less(self):
592
+ """Update the remote rendering widget and display it for
593
+ backend EnSight of version earlier than 2024R1
594
+ """
595
+ query_params = {
596
+ "autoconnect": "true",
597
+ "host": self._session.html_hostname,
598
+ "port": self._session.ws_port,
599
+ }
600
+ url = f"{self._http_protocol}://{self._session.html_hostname}:{self._session.html_port}"
601
+ url += "/ansys/nexus/novnc/vnc_envision.html"
602
+ url += self._get_query_parameters_str(query_params)
603
+ self._url = url
604
+
605
+ def update(self):
606
+ """Update the remote rendering widget and display it.
607
+
608
+ If the renderable is part of a Jupyter notebook cell, that cell is updated as an
609
+ iframe reference.
610
+
611
+ """
612
+ optional_query = self._get_query_parameters_str()
613
+ version = _get_ansysnexus_version(self._session._cei_suffix)
614
+ ui = "simple" if not self._ui else self._ui
615
+ if int(self._session._cei_suffix) < 242: # pragma: no cover
616
+ version = ""
617
+ self._update_2023R2_or_less() # pragma: no cover
618
+ else:
619
+ html = (
620
+ f"<script src='/ansys{version}/nexus/viewer-loader.js{optional_query}'></script>\n"
621
+ )
622
+ rest_uri = (
623
+ f"{self._http_protocol}://{self._session.html_hostname}:{self._session.html_port}"
624
+ )
625
+ ws_uri = (
626
+ f"{self._http_protocol}://{self._session.html_hostname}:{self._session.ws_port}"
627
+ )
628
+
629
+ query_args = ""
630
+ if self._using_proxy and optional_query: # pragma: no cover
631
+ query_args = f', "extra_query_args":"{optional_query[1:]}"' # pragma: no cover
632
+
633
+ attributes = ' renderer="envnc"'
634
+ attributes += f" ui={ui}"
635
+ attributes += ' active="true"'
636
+ attributes += (
637
+ " renderer_options='"
638
+ + f'{{ "ws":"{ws_uri}", "http":"{rest_uri}", "security_token":"{self._session.secret_key}", "connect_to_running_ens":true {query_args} }}'
639
+ + "'"
640
+ )
641
+
642
+ html += f"<ansys-nexus-viewer {attributes}></ansys-nexus-viewer>\n"
643
+
644
+ # refresh the remote HTML
645
+ self._save_remote_html_page(html)
646
+ super().update()
647
+
648
+
649
+ # Undocumented class
650
+ class RenderableVNCAngular(Renderable):
651
+ def __init__(self, *args, **kwargs) -> None:
652
+ super().__init__(*args, **kwargs)
653
+ self._generate_url()
654
+ self._rendertype = "remote"
655
+ self.update()
656
+
657
+ def update(self):
658
+ optional_query = self._get_query_parameters_str()
659
+ version = _get_ansysnexus_version(self._session._cei_suffix)
660
+ base_content = f"""
661
+ <!doctype html>
662
+ <html lang="en" class="dark">
663
+ <head><base href="/ansys{version}/nexus/angular/">
664
+ <meta charset="utf-8">
665
+ <title>WebEnSight</title>
666
+ <script src="/ansys{version}/nexus/viewer-loader.js"></script>
667
+ <meta name="viewport" content="width=device-width, initial-scale=1">
668
+ <link rel="icon" type="image/x-icon" href="ensight.ico">
669
+ <link rel="stylesheet" href="styles.css"></head>
670
+ <body>
671
+ """
672
+ module_with_attributes = "\n <web-en-sight "
673
+ module_with_attributes += f'wsPort="{self._session.ws_port}" '
674
+ module_with_attributes += f'secretKey="{self._session.secret_key}"'
675
+ if self._using_proxy and optional_query: # pragma: no cover
676
+ module_with_attributes += f' extraQueryArgs="{optional_query[1:]}"'
677
+ module_with_attributes += ">\n"
678
+ script_src = '<script src="runtime.js" type="module"></script><script src="polyfills.js" type="module"></script><script src="main.js" type="module"></script></body>\n</html>'
679
+ content = base_content + module_with_attributes + script_src
680
+ self._save_remote_html_page(content)
681
+ super().update()
682
+
683
+
684
+ class RenderableEVSN(Renderable):
685
+ """Generates a URL that can be used to connect to the EnVision VNC remote image renderer."""
686
+
687
+ def __init__(self, *args, **kwargs) -> None:
688
+ super().__init__(*args, **kwargs)
689
+ self._rendertype = "remote_scene"
690
+ self._generate_url()
691
+ pathname, filename = self._generate_filename(".evsn")
692
+ self._evsn_pathname = pathname
693
+ self._evsn_filename = filename
694
+ pathname, filename = self._generate_filename(".png")
695
+ self._proxy_pathname = pathname
696
+ self._proxy_filename = filename
697
+ # the download is the evsn file
698
+ self._download_names.append(self._evsn_filename)
699
+ self.update()
700
+
701
+ def update(self):
702
+ """Update the remote rendering widget and display it.
703
+
704
+ If the renderable is part of a Jupyter notebook cell, that cell is updated as an
705
+ iframe reference.
706
+
707
+ """
708
+ # Save the proxy image
709
+ w, h = self._default_size(1920, 1080)
710
+ cmd = f'ensight.render({w},{h},num_samples={self._aa}).save(r"""{self._proxy_pathname}""")'
711
+ self._session.cmd(cmd)
712
+ # save the .evsn file
713
+ self._session.ensight.file.save_scenario_which_parts("all")
714
+ self._session.ensight.file.scenario_format("envision")
715
+ # current timestep or all of the timesteps
716
+ if self._temporal:
717
+ st, en = self._session.ensight.objs.core.TIMESTEP_LIMITS
718
+ self._session.ensight.file.scenario_steptime_anim(1, st, en, 1.0)
719
+ else:
720
+ self._session.ensight.file.scenario_steptime_anim(0, 1, 1, 1)
721
+ varlist = self._session.ensight.objs.core.VARIABLES.find(True, "ACTIVE")
722
+ vars = [x.DESCRIPTION for x in varlist]
723
+ self._session.ensight.variables.select_byname_begin(vars)
724
+ # Save the file
725
+ self._session.ensight.file.save_scenario_fileslct(self._evsn_pathname)
726
+
727
+ # generate HTML page with file references local to the websocketserver root
728
+ optional_query = self._get_query_parameters_str()
729
+ version = _get_ansysnexus_version(self._session._cei_suffix)
730
+ html = f"<script src='/ansys{version}/nexus/viewer-loader.js{optional_query}'></script>\n"
731
+ server = f"{self._http_protocol}://{self._session.html_hostname}:{self._session.html_port}"
732
+
733
+ # FIXME: This method doesn't work with Ansys Lab since the viewer seems to require
734
+ # a full pathname to the file being generated by EnSight on a shared file system.
735
+ # The following commented out line should replace the two after that, but that
736
+ # prevents running locally from working since it's not using the full pathname to
737
+ # the shared file. -MFK
738
+ cleanname = self._evsn_filename.replace("\\", "/")
739
+ attributes = f"src='{cleanname}'"
740
+ # attributes = f"src='{cleanname}{optional_query}'"
741
+
742
+ attributes += f" proxy_img='/{self._proxy_filename}{optional_query}'"
743
+ attributes += " aspect_ratio='proxy'"
744
+ attributes += " renderer='envnc'"
745
+ http_uri = f'"http":"{server}"'
746
+ ws_uri = (
747
+ f'"ws":"{self._http_protocol}://{self._session.html_hostname}:{self._session.ws_port}"'
748
+ )
749
+ secrets = f'"security_token":"{self._session.secret_key}"'
750
+ if not self._using_proxy or not optional_query: # pragma: no cover
751
+ attributes += f" renderer_options='{{ {http_uri}, {ws_uri}, {secrets} }}'"
752
+ elif self._using_proxy and optional_query: # pragma: no cover
753
+ query_args = f'"extra_query_args":"{optional_query[1:]}"' # pragma: no cover
754
+ attributes += f" renderer_options='{{ {http_uri}, {ws_uri}, {secrets}, {query_args} }}'" # pragma: no cover
755
+ html += f"<ansys-nexus-viewer {attributes}></ansys-nexus-viewer>\n"
756
+ # refresh the remote HTML
757
+ self._save_remote_html_page(html)
758
+ super().update()
759
+
760
+
761
+ class RenderableSGEO(Renderable): # pragma: no cover
762
+ """Generates a WebGL-based renderable that leverages the SGEO format/viewer interface for progressive geometry transport."""
763
+
764
+ def __init__(self, *args, **kwargs) -> None: # pragma: no cover
765
+ super().__init__(*args, **kwargs)
766
+ self._generate_url()
767
+ pathname, filename = self._generate_filename("")
768
+ # on the server, a JSON block can be accessed via:
769
+ # {_sgeo_base_pathname}/geometry.sgeo
770
+ # and the update files:
771
+ # {_sgeo_base_pathname}/{names}.bin
772
+ self._sgeo_base_pathname = pathname
773
+ self._sgeo_base_filename = filename
774
+ # Create the directory where the sgeo files will go '/{filename}/' URL base
775
+ cmd = f'import os\nos.mkdir(r"""{self._sgeo_base_pathname}""")\n'
776
+ self._session.cmd(cmd, do_eval=False)
777
+ # get a stream ID
778
+ self._stream_id = self._session.ensight.dsg_new_stream(sgeo=1)
779
+ #
780
+ self._revision = 0
781
+ self.update()
782
+
783
+ def update(self): # pragma: no cover
784
+ """Generate a SGEO geometry file.
785
+
786
+ This method causes the EnSight session to generate an updated geometry SGEO
787
+ file and content and then display the results in any attached WebGL viewer.
788
+
789
+ If the renderable is part of a Jupyter notebook cell, that cell is updated as an
790
+ iframe reference.
791
+
792
+ """
793
+ # Ask for an update to be generated
794
+ remote_filename = f"{self._sgeo_base_pathname}/geometry.sgeo"
795
+ self._session.ensight.dsg_save_update(
796
+ remote_filename,
797
+ urlprefix=f"/{self._sgeo_base_filename}/",
798
+ stream=self._stream_id,
799
+ )
800
+
801
+ # Update the proxy image
802
+ self._update_proxy()
803
+
804
+ # If the first update, generate the HTML
805
+ if self._revision == 0:
806
+ # generate HTML page with file references local to the websocketserver root
807
+ attributes = (
808
+ f"src='/{self._sgeo_base_filename}/geometry.sgeo{self._get_query_parameters_str()}'"
809
+ )
810
+ attributes += f" proxy_img='/{self._sgeo_base_filename}/proxy.png{self._get_query_parameters_str()}'"
811
+ attributes += " aspect_ratio='proxy'"
812
+ attributes += " renderer='sgeo'"
813
+ version = _get_ansysnexus_version(self._session._cei_suffix)
814
+ html = f"<script src='/ansys{version}/nexus/viewer-loader.js{self._get_query_parameters_str()}'></script>\n"
815
+ html += f"<ansys-nexus-viewer id='{self._guid}' {attributes}></ansys-nexus-viewer>\n"
816
+ html += self._periodic_script()
817
+ # refresh the remote HTML
818
+ self._save_remote_html_page(html)
819
+ # Subsequent updates are handled by the component itself
820
+ super().update()
821
+
822
+ # update the revision file
823
+ rev_filename = f"{self._sgeo_base_pathname}/geometry.rev"
824
+ cmd = f'with open(r"""{rev_filename}""", "w") as fp:\n'
825
+ cmd += f' fp.write("{self._revision}")\n'
826
+ self._session.cmd(cmd, do_eval=False)
827
+
828
+ self._revision += 1
829
+
830
+ def _update_proxy(self):
831
+ """Replace the current proxy image with the current view."""
832
+ # save a proxy image
833
+ w, h = self._default_size(1920, 1080)
834
+ remote_filename = f"{self._sgeo_base_pathname}/proxy.png"
835
+ cmd = f'ensight.render({w},{h},num_samples={self._aa}).save(r"""{remote_filename}""")'
836
+ self._session.cmd(cmd, do_eval=False)
837
+
838
+ def delete(self) -> None:
839
+ try:
840
+ _ = self._session.ensight.dsg_close_stream(self._stream_id)
841
+ except Exception:
842
+ pass
843
+ super().delete()
844
+
845
+ def _periodic_script(self) -> str:
846
+ html_source = os.path.join(os.path.dirname(__file__), "sgeo_poll.html")
847
+ with open(html_source, "r") as fp:
848
+ html = fp.read()
849
+ revision_uri = f"/{self._sgeo_base_filename}/geometry.rev{self._get_query_parameters_str()}"
850
+ html = html.replace("REVURL_ITEMID", revision_uri)
851
+ html = html.replace("ITEMID", self._guid)
852
+ return html
853
+
854
+
855
+ class RenderableFluidsWebUI(Renderable):
856
+ def __init__(self, *args, **kwargs) -> None:
857
+ super().__init__(*args, **kwargs)
858
+ self._session.ensight_version_check("2025 R1")
859
+ warnings.warn("The webUI is still under active development and should be considered beta.")
860
+ self._rendertype = "webui"
861
+ self._generate_url()
862
+ self.update()
863
+
864
+ def _generate_url(self) -> None:
865
+ sha256_hash = hashlib.sha256()
866
+ sha256_hash.update(self._session._secret_key.encode())
867
+ token = sha256_hash.hexdigest()
868
+ optional_query = self._get_query_parameters_str()
869
+ port = self._session._webui_port
870
+ if "instance_name" in self._session._launcher._get_query_parameters():
871
+ instance_name = self._session._launcher._get_query_parameters()["instance_name"]
872
+ # If using PIM, the port needs to be the 443 HTTPS Port;
873
+ port = self._session.html_port
874
+ # In the webUI code there's already a workflow to pass down the query parameter
875
+ # ans_instance_id, just use it
876
+ instance_name = self._session._launcher._get_query_parameters()["instance_name"]
877
+ optional_query = f"?ans_instance_id={instance_name}"
878
+ url = f"{self._http_protocol}://{self._session.html_hostname}:{port}"
879
+ url += f"{optional_query}#{token}"
880
+ self._url = url