witwin-maxwell 0.3.0__py3-none-win_amd64.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 (107) hide show
  1. witwin/maxwell/__init__.py +164 -0
  2. witwin/maxwell/adapters/__init__.py +1 -0
  3. witwin/maxwell/adapters/gds.py +203 -0
  4. witwin/maxwell/adapters/tidy3d.py +1669 -0
  5. witwin/maxwell/adjoint_inputs.py +70 -0
  6. witwin/maxwell/compiler/__init__.py +11 -0
  7. witwin/maxwell/compiler/materials.py +1355 -0
  8. witwin/maxwell/compiler/monitors.py +164 -0
  9. witwin/maxwell/compiler/sources.py +222 -0
  10. witwin/maxwell/fdfd/__init__.py +3 -0
  11. witwin/maxwell/fdfd/adjoint/__init__.py +3 -0
  12. witwin/maxwell/fdfd/adjoint/bridge.py +180 -0
  13. witwin/maxwell/fdfd/ams.py +300 -0
  14. witwin/maxwell/fdfd/krylov.py +280 -0
  15. witwin/maxwell/fdfd/plotting.py +110 -0
  16. witwin/maxwell/fdfd/postprocess.py +33 -0
  17. witwin/maxwell/fdfd/solver.py +1057 -0
  18. witwin/maxwell/fdtd/__init__.py +4 -0
  19. witwin/maxwell/fdtd/adjoint/__init__.py +21 -0
  20. witwin/maxwell/fdtd/adjoint/bridge.py +746 -0
  21. witwin/maxwell/fdtd/adjoint/capabilities.py +29 -0
  22. witwin/maxwell/fdtd/adjoint/core.py +3614 -0
  23. witwin/maxwell/fdtd/adjoint/dispatch.py +684 -0
  24. witwin/maxwell/fdtd/adjoint/native.py +2561 -0
  25. witwin/maxwell/fdtd/adjoint/profiler.py +195 -0
  26. witwin/maxwell/fdtd/adjoint/reverse_common.py +207 -0
  27. witwin/maxwell/fdtd/adjoint/seeds.py +524 -0
  28. witwin/maxwell/fdtd/boundary/__init__.py +37 -0
  29. witwin/maxwell/fdtd/boundary/bloch.py +90 -0
  30. witwin/maxwell/fdtd/boundary/common.py +41 -0
  31. witwin/maxwell/fdtd/boundary/cpml.py +527 -0
  32. witwin/maxwell/fdtd/boundary/runtime.py +209 -0
  33. witwin/maxwell/fdtd/checkpoint.py +227 -0
  34. witwin/maxwell/fdtd/coords.py +32 -0
  35. witwin/maxwell/fdtd/cuda/__init__.py +11 -0
  36. witwin/maxwell/fdtd/cuda/backend.py +3532 -0
  37. witwin/maxwell/fdtd/cuda/build.py +292 -0
  38. witwin/maxwell/fdtd/cuda/extension.cpp +2053 -0
  39. witwin/maxwell/fdtd/cuda/kernels/adjoint.cu +5290 -0
  40. witwin/maxwell/fdtd/cuda/kernels/aniso.cu +1704 -0
  41. witwin/maxwell/fdtd/cuda/kernels/boundary.cu +424 -0
  42. witwin/maxwell/fdtd/cuda/kernels/common.cu +74 -0
  43. witwin/maxwell/fdtd/cuda/kernels/common.cuh +82 -0
  44. witwin/maxwell/fdtd/cuda/kernels/dispersive.cu +782 -0
  45. witwin/maxwell/fdtd/cuda/kernels/electric.cu +3613 -0
  46. witwin/maxwell/fdtd/cuda/kernels/magnetic.cu +781 -0
  47. witwin/maxwell/fdtd/cuda/kernels/observers.cu +347 -0
  48. witwin/maxwell/fdtd/cuda/kernels/projection.cu +211 -0
  49. witwin/maxwell/fdtd/cuda/kernels/sources.cu +1482 -0
  50. witwin/maxwell/fdtd/cuda/kernels/spectral.cu +297 -0
  51. witwin/maxwell/fdtd/cuda/launch.h +23 -0
  52. witwin/maxwell/fdtd/cuda/prebuilt/witwin_maxwell_fdtd_cuda.pyd +0 -0
  53. witwin/maxwell/fdtd/cuda/runtime/__init__.py +6 -0
  54. witwin/maxwell/fdtd/cuda/runtime/dispatcher.py +6 -0
  55. witwin/maxwell/fdtd/cuda/runtime/graph.py +33 -0
  56. witwin/maxwell/fdtd/cuda/tensors.h +35 -0
  57. witwin/maxwell/fdtd/dispersion.py +48 -0
  58. witwin/maxwell/fdtd/evaluation.py +290 -0
  59. witwin/maxwell/fdtd/excitation/__init__.py +27 -0
  60. witwin/maxwell/fdtd/excitation/injection.py +1102 -0
  61. witwin/maxwell/fdtd/excitation/modes.py +1675 -0
  62. witwin/maxwell/fdtd/excitation/spatial.py +469 -0
  63. witwin/maxwell/fdtd/excitation/temporal.py +510 -0
  64. witwin/maxwell/fdtd/excitation/tfsf_apply.py +264 -0
  65. witwin/maxwell/fdtd/excitation/tfsf_common.py +421 -0
  66. witwin/maxwell/fdtd/excitation/tfsf_specs.py +490 -0
  67. witwin/maxwell/fdtd/excitation/tfsf_state.py +778 -0
  68. witwin/maxwell/fdtd/io.py +31 -0
  69. witwin/maxwell/fdtd/material_pullback.py +371 -0
  70. witwin/maxwell/fdtd/meshing/__init__.py +3 -0
  71. witwin/maxwell/fdtd/meshing/autogrid.py +335 -0
  72. witwin/maxwell/fdtd/observers.py +1453 -0
  73. witwin/maxwell/fdtd/plotting.py +121 -0
  74. witwin/maxwell/fdtd/postprocess.py +205 -0
  75. witwin/maxwell/fdtd/runtime/__init__.py +119 -0
  76. witwin/maxwell/fdtd/runtime/initialization.py +236 -0
  77. witwin/maxwell/fdtd/runtime/materials.py +1563 -0
  78. witwin/maxwell/fdtd/runtime/module_cache.py +31 -0
  79. witwin/maxwell/fdtd/runtime/spectral.py +581 -0
  80. witwin/maxwell/fdtd/runtime/stepping.py +1987 -0
  81. witwin/maxwell/fdtd/solver.py +502 -0
  82. witwin/maxwell/geometry/__init__.py +5 -0
  83. witwin/maxwell/geometry/polyslab.py +299 -0
  84. witwin/maxwell/materials.py +3 -0
  85. witwin/maxwell/media.py +1467 -0
  86. witwin/maxwell/monitors.py +743 -0
  87. witwin/maxwell/ports.py +113 -0
  88. witwin/maxwell/postprocess/RCS.py +139 -0
  89. witwin/maxwell/postprocess/__init__.py +42 -0
  90. witwin/maxwell/postprocess/diffraction.py +219 -0
  91. witwin/maxwell/postprocess/directivity.py +152 -0
  92. witwin/maxwell/postprocess/emission.py +67 -0
  93. witwin/maxwell/postprocess/modal.py +324 -0
  94. witwin/maxwell/postprocess/nfft.py +266 -0
  95. witwin/maxwell/postprocess/scattering_parameters.py +358 -0
  96. witwin/maxwell/postprocess/stratton_chu.py +1193 -0
  97. witwin/maxwell/result.py +967 -0
  98. witwin/maxwell/scene.py +1338 -0
  99. witwin/maxwell/simulation.py +659 -0
  100. witwin/maxwell/sources.py +917 -0
  101. witwin/maxwell/visualization/__init__.py +28 -0
  102. witwin/maxwell/visualization/interactive.py +484 -0
  103. witwin/maxwell/visualization/plots.py +568 -0
  104. witwin_maxwell-0.3.0.dist-info/METADATA +184 -0
  105. witwin_maxwell-0.3.0.dist-info/RECORD +107 -0
  106. witwin_maxwell-0.3.0.dist-info/WHEEL +4 -0
  107. witwin_maxwell-0.3.0.dist-info/licenses/LICENSE +13 -0
@@ -0,0 +1,28 @@
1
+ from .plots import (
2
+ extract_orthogonal_slice,
3
+ plot_cross_section_panels,
4
+ plot_orthogonal_views,
5
+ plot_slice_image,
6
+ visualize_material_slice,
7
+ visualize_slice,
8
+ )
9
+
10
+ __all__ = [
11
+ "build_fdtd_pyvista_grid",
12
+ "extract_orthogonal_slice",
13
+ "plot_cross_section_panels",
14
+ "plot_orthogonal_views",
15
+ "plot_slice_image",
16
+ "show_pyvista_solution",
17
+ "visualize_material_slice",
18
+ "visualize_slice",
19
+ ]
20
+
21
+
22
+ def __getattr__(name):
23
+ if name in ("build_fdtd_pyvista_grid", "show_pyvista_solution"):
24
+ from .interactive import build_fdtd_pyvista_grid, show_pyvista_solution
25
+ globals()["build_fdtd_pyvista_grid"] = build_fdtd_pyvista_grid
26
+ globals()["show_pyvista_solution"] = show_pyvista_solution
27
+ return globals()[name]
28
+ raise AttributeError(f"module {__name__!r} has no attribute {name!r}")
@@ -0,0 +1,484 @@
1
+ import numpy as np
2
+
3
+
4
+ ZINC = {
5
+ "950": "#09090b",
6
+ "900": "#18181b",
7
+ "800": "#27272a",
8
+ "700": "#3f3f46",
9
+ "600": "#52525b",
10
+ "400": "#a1a1aa",
11
+ "200": "#e4e4e7",
12
+ "100": "#f4f4f5",
13
+ }
14
+
15
+
16
+ def build_fdtd_pyvista_grid(fdtd, freq_solution):
17
+ import pyvista as pv
18
+
19
+ field_mag, _, _, _ = fdtd._interpolate_yee_to_center(freq_solution)
20
+ permittivity = fdtd._get_centered_permittivity().cpu().numpy()
21
+ x0, _, y0, _, z0, _ = fdtd.scene.domain_range
22
+
23
+ image = pv.ImageData()
24
+ image.dimensions = (
25
+ field_mag.shape[0] + 1,
26
+ field_mag.shape[1] + 1,
27
+ field_mag.shape[2] + 1,
28
+ )
29
+ image.origin = (x0, y0, z0)
30
+ image.spacing = (fdtd.dx, fdtd.dy, fdtd.dz)
31
+ image.cell_data["field_mag"] = field_mag.ravel(order="F")
32
+ image.cell_data["permittivity"] = permittivity.ravel(order="F")
33
+ return image.cell_data_to_point_data(pass_cell_data=True)
34
+
35
+
36
+ def _iso_value_from_frac(field_max, iso_frac):
37
+ return max(float(field_max) * float(iso_frac), 1e-12)
38
+
39
+
40
+ def _build_view_state(grid, iso_frac):
41
+ field_mag = np.asarray(grid["field_mag"])
42
+ permittivity = np.asarray(grid["permittivity"])
43
+ field_min = float(np.nanmin(field_mag))
44
+ field_max = float(np.nanmax(field_mag))
45
+ eps_min = float(np.nanmin(permittivity))
46
+ eps_max = float(np.nanmax(permittivity))
47
+ iso_frac = float(np.clip(iso_frac, 0.05, 0.95))
48
+ dielectric_threshold = float(np.clip(1.05, eps_min, eps_max))
49
+ return {
50
+ "field_min": field_min,
51
+ "field_max": field_max,
52
+ "eps_min": eps_min,
53
+ "eps_max": eps_max,
54
+ "mode": "field",
55
+ "iso_frac": iso_frac,
56
+ "iso_value": _iso_value_from_frac(field_max, iso_frac),
57
+ "dielectric_threshold": dielectric_threshold,
58
+ "volume_opacity": 0.18,
59
+ "show_outline": True,
60
+ "show_slices": True,
61
+ "show_contour": True,
62
+ }
63
+
64
+
65
+ def _safe_remove_actor(plotter, actor):
66
+ if actor is None:
67
+ return
68
+ if isinstance(actor, (list, tuple)):
69
+ for item in actor:
70
+ _safe_remove_actor(plotter, item)
71
+ return
72
+ try:
73
+ plotter.remove_actor(actor, render=False)
74
+ except (RuntimeError, TypeError, ValueError):
75
+ return
76
+
77
+
78
+ def _clear_scene_actors(plotter, actors):
79
+ for key, actor in list(actors.items()):
80
+ _safe_remove_actor(plotter, actor)
81
+ actors[key] = None
82
+ if hasattr(plotter, "clear_plane_widgets"):
83
+ plotter.clear_plane_widgets()
84
+
85
+
86
+ def _format_status_panel(state):
87
+ mode_label = "Field" if state["mode"] == "field" else "Dielectric"
88
+ return "\n".join(
89
+ [
90
+ "Status",
91
+ f"Mode: {mode_label}",
92
+ f"Field max: {state['field_max']:.3e}",
93
+ f"Eps range: {state['eps_min']:.3f} .. {state['eps_max']:.3f}",
94
+ f"Iso / cutoff: {state['iso_frac']:.2f} / {state['dielectric_threshold']:.3f}",
95
+ ]
96
+ )
97
+
98
+
99
+ def _update_status_panel(plotter, state):
100
+ plotter.subplot(0, 1)
101
+ plotter.add_text(
102
+ _format_status_panel(state),
103
+ position=(0.08, 0.015),
104
+ viewport=True,
105
+ font_size=8,
106
+ color=ZINC["100"],
107
+ name="status_panel",
108
+ render=False,
109
+ )
110
+
111
+
112
+ def _update_scene_title(plotter, state):
113
+ mode_label = "Field" if state["mode"] == "field" else "Dielectric"
114
+ plotter.subplot(0, 0)
115
+ plotter.add_text(
116
+ f"Scene View: {mode_label}",
117
+ position="upper_left",
118
+ font_size=12,
119
+ color=ZINC["100"],
120
+ name="scene_title",
121
+ render=False,
122
+ )
123
+
124
+
125
+ def _add_outline(plotter, grid):
126
+ plotter.subplot(0, 0)
127
+ return plotter.add_mesh(
128
+ grid.outline(),
129
+ color=ZINC["200"],
130
+ line_width=1,
131
+ name="outline",
132
+ render=False,
133
+ )
134
+
135
+
136
+ def _add_field_volume(plotter, grid, state):
137
+ plotter.subplot(0, 0)
138
+ return plotter.add_volume(
139
+ grid,
140
+ scalars="field_mag",
141
+ cmap="inferno",
142
+ opacity=state["volume_opacity"],
143
+ shade=False,
144
+ mapper="smart",
145
+ show_scalar_bar=False,
146
+ clim=(state["field_min"], state["field_max"]),
147
+ name="field_volume",
148
+ render=False,
149
+ )
150
+
151
+
152
+ def _add_field_contour(plotter, grid, state):
153
+ plotter.subplot(0, 0)
154
+ contour = grid.contour(isosurfaces=[state["iso_value"]], scalars="field_mag")
155
+ if contour.n_cells == 0:
156
+ return None
157
+ return plotter.add_mesh(
158
+ contour,
159
+ scalars="field_mag",
160
+ cmap="inferno",
161
+ opacity=0.9,
162
+ smooth_shading=True,
163
+ show_scalar_bar=False,
164
+ clim=(state["field_min"], state["field_max"]),
165
+ specular=0.2,
166
+ name="field_contour",
167
+ render=False,
168
+ )
169
+
170
+
171
+ def _add_field_slices(plotter, grid, state):
172
+ plotter.subplot(0, 0)
173
+ return plotter.add_mesh_slice_orthogonal(
174
+ grid,
175
+ scalars="field_mag",
176
+ cmap="inferno",
177
+ line_width=2,
178
+ widget_color=ZINC["400"],
179
+ show_scalar_bar=False,
180
+ clim=(state["field_min"], state["field_max"]),
181
+ render=False,
182
+ )
183
+
184
+
185
+ def _add_dielectric_mesh(plotter, grid, state):
186
+ plotter.subplot(0, 0)
187
+ dielectric = grid.threshold(
188
+ value=state["dielectric_threshold"],
189
+ scalars="permittivity",
190
+ )
191
+ if dielectric.n_cells == 0:
192
+ return None
193
+ return plotter.add_mesh(
194
+ dielectric,
195
+ scalars="permittivity",
196
+ cmap="viridis",
197
+ opacity=0.28,
198
+ smooth_shading=True,
199
+ show_scalar_bar=False,
200
+ clim=(state["eps_min"], state["eps_max"]),
201
+ specular=0.12,
202
+ name="dielectric_mesh",
203
+ render=False,
204
+ )
205
+
206
+
207
+ def _add_permittivity_slices(plotter, grid, state):
208
+ plotter.subplot(0, 0)
209
+ return plotter.add_mesh_slice_orthogonal(
210
+ grid,
211
+ scalars="permittivity",
212
+ cmap="viridis",
213
+ line_width=2,
214
+ widget_color=ZINC["400"],
215
+ show_scalar_bar=False,
216
+ clim=(state["eps_min"], state["eps_max"]),
217
+ render=False,
218
+ )
219
+
220
+
221
+ def _setup_scene_view(plotter, state):
222
+ plotter.subplot(0, 0)
223
+ plotter.set_background(ZINC["900"], top=ZINC["800"], all_renderers=False)
224
+ plotter.add_axes(line_width=2, color=ZINC["200"])
225
+ plotter.show_grid(color=ZINC["600"])
226
+ plotter.camera_position = "iso"
227
+ _update_scene_title(plotter, state)
228
+
229
+
230
+ def _setup_control_panel(plotter, state):
231
+ plotter.subplot(0, 1)
232
+ plotter.set_background(ZINC["800"], top=ZINC["700"], all_renderers=False)
233
+ plotter.add_text(
234
+ "Controls",
235
+ position=(0.08, 0.94),
236
+ viewport=True,
237
+ font_size=16,
238
+ color=ZINC["100"],
239
+ name="panel_title",
240
+ render=False,
241
+ )
242
+ plotter.add_text(
243
+ "Switch the scene between Field and Dielectric.",
244
+ position=(0.08, 0.89),
245
+ viewport=True,
246
+ font_size=10,
247
+ color=ZINC["200"],
248
+ name="panel_subtitle",
249
+ render=False,
250
+ )
251
+ plotter.add_text(
252
+ "Display Mode",
253
+ position=(0.08, 0.82),
254
+ viewport=True,
255
+ font_size=11,
256
+ color=ZINC["100"],
257
+ name="panel_mode_label",
258
+ render=False,
259
+ )
260
+ plotter.add_text(
261
+ "Scene Toggles",
262
+ position=(0.08, 0.66),
263
+ viewport=True,
264
+ font_size=11,
265
+ color=ZINC["100"],
266
+ name="panel_toggle_label",
267
+ render=False,
268
+ )
269
+ plotter.add_text(
270
+ "Parameters",
271
+ position=(0.08, 0.49),
272
+ viewport=True,
273
+ font_size=11,
274
+ color=ZINC["100"],
275
+ name="panel_param_label",
276
+ render=False,
277
+ )
278
+ _update_status_panel(plotter, state)
279
+
280
+
281
+ def _rebuild_scene(plotter, grid, state, actors):
282
+ plotter.subplot(0, 0)
283
+ _clear_scene_actors(plotter, actors)
284
+ _update_scene_title(plotter, state)
285
+
286
+ if state["show_outline"]:
287
+ actors["outline"] = _add_outline(plotter, grid)
288
+
289
+ if state["mode"] == "field":
290
+ actors["field_volume"] = _add_field_volume(plotter, grid, state)
291
+ actors["field_contour"] = (
292
+ _add_field_contour(plotter, grid, state) if state["show_contour"] else None
293
+ )
294
+ actors["field_slices"] = (
295
+ _add_field_slices(plotter, grid, state) if state["show_slices"] else None
296
+ )
297
+ return
298
+
299
+ actors["dielectric_mesh"] = _add_dielectric_mesh(plotter, grid, state)
300
+ actors["dielectric_slices"] = (
301
+ _add_permittivity_slices(plotter, grid, state) if state["show_slices"] else None
302
+ )
303
+
304
+
305
+ def _add_labeled_checkbox(plotter, callback, value, label, button_position, text_position):
306
+ plotter.add_checkbox_button_widget(
307
+ callback,
308
+ value=value,
309
+ position=button_position,
310
+ size=26,
311
+ border_size=2,
312
+ color_on="#f97316",
313
+ color_off=ZINC["600"],
314
+ background_color=ZINC["100"],
315
+ )
316
+ plotter.add_text(
317
+ label,
318
+ position=text_position,
319
+ viewport=True,
320
+ font_size=10,
321
+ color=ZINC["100"],
322
+ render=False,
323
+ )
324
+
325
+
326
+ def _add_control_widgets(plotter, grid, state, actors, window_size):
327
+ plotter.subplot(0, 1)
328
+ checkbox_x = 24
329
+ y0 = int(window_size[1] * 0.56)
330
+ gap = int(window_size[1] * 0.055)
331
+
332
+ def refresh_and_render():
333
+ _rebuild_scene(plotter, grid, state, actors)
334
+ _update_status_panel(plotter, state)
335
+ plotter.render()
336
+
337
+ def update_mode(value):
338
+ state["mode"] = "field" if str(value).lower().startswith("field") else "dielectric"
339
+ refresh_and_render()
340
+
341
+ def toggle_slices(value):
342
+ state["show_slices"] = bool(value)
343
+ refresh_and_render()
344
+
345
+ def toggle_outline(value):
346
+ state["show_outline"] = bool(value)
347
+ refresh_and_render()
348
+
349
+ def toggle_contour(value):
350
+ state["show_contour"] = bool(value)
351
+ refresh_and_render()
352
+
353
+ def update_volume_opacity(value):
354
+ state["volume_opacity"] = float(value)
355
+ refresh_and_render()
356
+
357
+ def update_iso_frac(value):
358
+ state["iso_frac"] = float(np.clip(value, 0.05, 0.95))
359
+ state["iso_value"] = _iso_value_from_frac(state["field_max"], state["iso_frac"])
360
+ refresh_and_render()
361
+
362
+ def update_dielectric_threshold(value):
363
+ state["dielectric_threshold"] = float(
364
+ np.clip(value, state["eps_min"], state["eps_max"])
365
+ )
366
+ refresh_and_render()
367
+
368
+ plotter.add_text_slider_widget(
369
+ update_mode,
370
+ data=["Field", "Dielectric"],
371
+ value=0,
372
+ pointa=(0.12, 0.77),
373
+ pointb=(0.88, 0.77),
374
+ color=ZINC["100"],
375
+ interaction_event="end",
376
+ )
377
+
378
+ _add_labeled_checkbox(
379
+ plotter,
380
+ toggle_slices,
381
+ state["show_slices"],
382
+ "Show slices",
383
+ (checkbox_x, y0),
384
+ (0.20, 0.57),
385
+ )
386
+ _add_labeled_checkbox(
387
+ plotter,
388
+ toggle_outline,
389
+ state["show_outline"],
390
+ "Show outline",
391
+ (checkbox_x, y0 - gap),
392
+ (0.20, 0.515),
393
+ )
394
+ _add_labeled_checkbox(
395
+ plotter,
396
+ toggle_contour,
397
+ state["show_contour"],
398
+ "Field contour",
399
+ (checkbox_x, y0 - 2 * gap),
400
+ (0.20, 0.46),
401
+ )
402
+
403
+ plotter.add_slider_widget(
404
+ update_volume_opacity,
405
+ rng=(0.03, 0.45),
406
+ value=state["volume_opacity"],
407
+ title="Field volume opacity",
408
+ pointa=(0.10, 0.42),
409
+ pointb=(0.90, 0.42),
410
+ fmt="%.2f",
411
+ interaction_event="end",
412
+ color=ZINC["100"],
413
+ )
414
+ plotter.add_slider_widget(
415
+ update_iso_frac,
416
+ rng=(0.05, 0.95),
417
+ value=state["iso_frac"],
418
+ title="Field isosurface fraction",
419
+ pointa=(0.10, 0.34),
420
+ pointb=(0.90, 0.34),
421
+ fmt="%.2f",
422
+ interaction_event="end",
423
+ color=ZINC["100"],
424
+ )
425
+ plotter.add_slider_widget(
426
+ update_dielectric_threshold,
427
+ rng=(state["eps_min"], max(state["eps_max"], state["eps_min"] + 1e-6)),
428
+ value=state["dielectric_threshold"],
429
+ title="Dielectric cutoff",
430
+ pointa=(0.10, 0.18),
431
+ pointb=(0.90, 0.18),
432
+ fmt="%.3f",
433
+ interaction_event="end",
434
+ color=ZINC["100"],
435
+ )
436
+
437
+
438
+ def show_pyvista_solution(grid, iso_frac=0.35, headless=False, screenshot=None):
439
+ import pyvista as pv
440
+
441
+ pv.global_theme.allow_empty_mesh = True
442
+
443
+ off_screen = headless or screenshot is not None
444
+ state = _build_view_state(grid, iso_frac)
445
+ window_size = (1920, 980)
446
+ plotter = pv.Plotter(
447
+ shape=(1, 2),
448
+ col_weights=[4, 2],
449
+ border=True,
450
+ border_color=ZINC["600"],
451
+ window_size=window_size,
452
+ off_screen=off_screen,
453
+ )
454
+ if hasattr(plotter, "enable_depth_peeling"):
455
+ plotter.enable_depth_peeling()
456
+
457
+ actors = {
458
+ "outline": None,
459
+ "field_volume": None,
460
+ "field_contour": None,
461
+ "field_slices": None,
462
+ "dielectric_mesh": None,
463
+ "dielectric_slices": None,
464
+ }
465
+
466
+ _setup_scene_view(plotter, state)
467
+ _setup_control_panel(plotter, state)
468
+ _rebuild_scene(plotter, grid, state, actors)
469
+ plotter.subplot(0, 0)
470
+ if hasattr(plotter, "add_camera_orientation_widget"):
471
+ plotter.add_camera_orientation_widget()
472
+ _add_control_widgets(plotter, grid, state, actors, window_size)
473
+
474
+ if screenshot is not None:
475
+ plotter.show(screenshot=str(screenshot), auto_close=True)
476
+ return
477
+
478
+ if headless:
479
+ plotter.show(auto_close=True)
480
+ return
481
+
482
+ print("PyVista controls: left-drag rotate, right-drag zoom, middle-drag pan.")
483
+ print("Use the zinc control panel on the right to switch between Field and Dielectric.")
484
+ plotter.show()