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