Skip to content

bf.viewer

view

view(stack: Any, save_dir: Path | str | None = None) -> Viewer

Returns the viewer for a device (spec D7).

Parameters:

Name Type Description Default
stack Any

a :class:~bifrost.stack.StackDevice.

required
save_dir Path | str | None

where a server-side save writes its PNG (spec D14). Defaults to the process working directory.

None

Returns:

Name Type Description
The Viewer

class:Viewer app-shell widget.

Source code in bifrost/viewer/widget.py
def view(stack: Any, save_dir: Path | str | None = None) -> Viewer:
    """Returns the viewer for a device (spec D7).

    Args:
        stack: a :class:`~bifrost.stack.StackDevice`.
        save_dir: where a server-side save writes its PNG (spec D14). Defaults
            to the process working directory.

    Returns:
        The :class:`Viewer` app-shell widget.
    """
    return Viewer(stack, save_dir=save_dir)

Viewer

Bases: AnyWidget

The app-shell widget returned by :func:view (spec D7, D9).

Source code in bifrost/viewer/widget.py
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
class Viewer(anywidget.AnyWidget):
    """The app-shell widget returned by :func:`view` (spec D7, D9)."""

    _esm = _STATIC / "viewer.js"
    _css = _STATIC / "viewer.css"

    #: set once: the mesh buffers and the box (spec D5, C5)
    geometry = traitlets.Dict(default_value={}).tag(sync=True)
    #: set once: the level and port metadata (spec D5)
    meta = traitlets.Dict(default_value={}).tag(sync=True)
    #: the shared state, re-sent on every edit (spec D10)
    state = traitlets.Dict(default_value={}).tag(sync=True)
    #: the 2D cross-section rings, rebuilt when the plane changes (spec R4)
    section = traitlets.Dict(default_value={}).tag(sync=True)
    #: the last run's outcome summary, for the browser to show (spec R14, Q20)
    run_result = traitlets.Dict(default_value={}).tag(sync=True)
    #: every detector's recorded field from the current result, keyed by spec id
    #: (spec R15, D16): the raw complex field as float32 buffers, so a transform
    #: or color edit re-shades in the browser with no kernel round-trip
    fields = traitlets.Dict(default_value={}).tag(sync=True)
    #: the in-flight run's progress as ``{"step": int, "total": int}`` (spec D15);
    #: ``{}`` when no run is in flight or the total is not yet known
    progress = traitlets.Dict(default_value={}).tag(sync=True)
    #: the in-flight run's phase — ``"placing"``, ``"compiling"`` or
    #: ``"stepping"`` (spec D15) — or ``""`` when no run is in flight
    phase = traitlets.Unicode(default_value="").tag(sync=True)
    #: whether the user has asked the in-flight run to stop (spec D17); the
    #: browser greys the button and shows "stopping…" while it is true
    stopping = traitlets.Bool(default_value=False).tag(sync=True)
    #: the last kernel-side save's outcome, for the browser to confirm (spec
    #: D14): ``{"path": str}`` on success, ``{"folder": str}`` when the save
    #: folder changed, or ``{"error": str}`` on failure
    save_result = traitlets.Dict(default_value={}).tag(sync=True)
    #: the absolute folder a server-side save writes into (spec D14). Settable
    #: from the browser, so the settings screen can offer a folder field.
    save_dir = traitlets.Unicode(default_value="").tag(sync=True)
    #: the launch code — the script tail that rebuilds this setup (spec R14
    #: mode 3, D12) — refreshed on every setup edit, so the browser's export
    #: button copies the current text with no kernel round trip
    launch_code = traitlets.Unicode(default_value="").tag(sync=True)

    def __init__(self, stack: Any, save_dir: Path | str | None = None, **kwargs: Any) -> None:
        """Builds the device's geometry and prepares the widget.

        Args:
            stack: a :class:`~bifrost.stack.StackDevice`.
            save_dir: where the save button writes a PNG when the browser
                prefers a server-side save (spec D14). Defaults to the process
                working directory, resolved once at construction so a later
                ``os.chdir`` does not move the target.
            **kwargs: passed to :class:`anywidget.AnyWidget`.
        """
        self._viewer_state = ViewerState(stack)
        #: the result's own world (spec R17): a separate :class:`ViewerState`
        #: built from the result's stack, holding the adopted specs and the
        #: styles the Result tab edits. ``None`` until the first result arrives
        #: (a run or a ``load_result``); while it is ``None`` the Result tab
        #: shows its empty note. The editor world is never touched by a result.
        self._result_state: ViewerState | None = None
        #: the result world's cached geometry/section payloads, rebuilt only
        #: when the result changes (they do not track editor edits)
        self._result_geometry: dict[str, Any] = {}
        self._result_meta: dict[str, Any] = {}
        self._result_section: dict[str, Any] = {}
        #: the result world the ``fields`` trait was last built from — so a
        #: fresh result republishes its payload once, even from an editor tab
        self._fields_state: ViewerState | None = None
        #: the editor world's cached payloads (see :meth:`_cached_payloads`)
        self._editor_geometry: dict[str, Any] | None = None
        self._editor_meta: dict[str, Any] | None = None
        self._editor_section: dict[str, Any] | None = None
        initial_save_dir = Path(save_dir).expanduser() if save_dir is not None else Path.cwd()
        self._run_result: Any = None
        self._run_error: BaseException | None = None
        self._run_thread: threading.Thread | None = None
        #: the stop flag for the in-flight run (spec D17); ``None`` when no run
        #: is in flight. Set by :meth:`stop` and read by the worker's condition.
        self._run_cancel: threading.Event | None = None
        #: the result in place before the current run started, so a *discarded*
        #: stop can leave ``last_result`` as it was rather than as ``None`` (D17)
        self._previous_result: Any = None
        #: cache of the prepared ``(objects, config, constraints)``, keyed by the
        #: state revision, so the three properties share one build (spec D11)
        self._prepared: tuple[int, tuple[list[Any], Any, list[Any]]] | None = None
        super().__init__(**kwargs)
        geometry, meta = build_payload(self._viewer_state)
        self.geometry = geometry
        self.meta = meta
        self.state = self._viewer_state.to_state()
        self.section = build_section_payload(self._viewer_state)
        self.save_dir = str(initial_save_dir.resolve())
        self.launch_code = _launch_code_text(self.stack, self._viewer_state)
        self.on_msg(self._on_msg)

    # ------------------------------------------------------------------ worlds

    def _install_result(self, result: RunResult, stack: Any, settings: Any = None) -> None:
        """Installs a run result into the result world (spec R17).

        The result gets its own :class:`ViewerState` — built from ``stack``,
        holding the adopted specs and the Result tab's own styles — so the
        editor's setup and level styles are untouched. The Result tab's level
        styles start from the editor's, with the opacity scaled — 1/3 for a
        non-fill level, 2/3 for a fill level — so the fields show through
        (:func:`export_styles`).
        Called by ``run``'s worker (the widget's own stack) and by
        ``load_result`` (a foreign one).
        """
        from .adopt import adopt_result

        new_state = ViewerState(stack, settings=settings)
        export_styles(self._viewer_state, new_state)
        adopt_result(new_state, result.objects, result.config)
        self._result_state = new_state
        geometry, meta = build_payload(new_state)
        self._result_geometry = geometry
        self._result_meta = meta
        self._result_section = build_section_payload(new_state)

    def _list_in_editor(self, result: RunResult, stack: Any) -> None:
        """Makes the editor's sources and detectors the result's (spec R16, R17).

        A result loaded from user code has no setup in the editor, so the Sources /
        Detectors tab would be empty while the Result tab lists them. The result's
        sources and detectors are adopted into the editor too: the same read-only
        specs, drawn on the same planes. Nothing is added when the editor already
        holds them (the widget's own run loaded back) or when the result is on another
        device, whose box the editor does not share.
        """
        from .adopt import adopt_result

        state = self._viewer_state
        state.clear_adopted()
        if stack is not state.stack:
            return
        placed = [
            (category, str(obj.name))
            for category, objects in (
                ("source", getattr(result.objects, "sources", ())), ("detector", result.objects.detectors)
            )
            for obj in objects
        ]
        own = {(spec.category, spec.name) for spec in state.specs}
        if not set(placed) <= own:
            adopt_result(state, result.objects, result.config)

    def _active_state(self) -> ViewerState:
        """The state whose traits are on the wire (spec R17).

        The Result tab shows the result world's geometry and state; every other
        tab shows the editor's. The result world falls back to the editor while
        no result exists (its tab then only shows the empty note).
        """
        if self._viewer_state.active_tab == "result" and self._result_state is not None:
            return self._result_state
        return self._viewer_state

    def _wire_state(self, active: ViewerState) -> dict[str, Any]:
        """The state for the wire: the active world's, with the tab echoed.

        ``active_tab`` lives on the editor state (it routes the worlds, spec
        R17), so when the result world is on the wire its own ``active_tab``
        — a fresh default — must not leak out: the browser shades only while
        the wire says the Result tab is active.
        """
        state = active.to_state()
        if active is not self._viewer_state:
            state["active_tab"] = self._viewer_state.active_tab
        return state

    def _sync_active(self, *, force: bool = False) -> None:
        """Sends the active world's traits (spec R17).

        A tab switch swaps which world the single ``geometry``/``meta``/
        ``section``/``state`` traits carry, so on a switch (or ``force``) all
        four are re-sent. ``state`` goes first so a rebuild sees the styles the
        new world ships with; ``fields`` — the result world's whenever one
        exists — rides along, so switching away from Result clears the
        overlays (the browser shades only while the Result tab is active).
        """
        active = self._active_state()
        if force or active is self._result_state:
            self.state = self._wire_state(active)
            self.geometry = self._cached_payloads(active)[0]
            self.meta = self._cached_payloads(active)[1]
            self.section = (
                self._result_section if active is self._result_state
                else self._cached_section(self._viewer_state)
            )
            self.fields = (
                build_field_payload(self._result_state, self._run_result)
                if self._result_state is not None else {}
            )
            self._fields_state = self._result_state
            return
        # a fresh result world (run/show_result/load_result) republishes the
        # field payload even from an editor tab — the browser shades only
        # while the Result tab is active, so this is inert there (R17)
        if self._result_state is not None and self._fields_state is not self._result_state:
            self.fields = build_field_payload(self._result_state, self._run_result)
            self._fields_state = self._result_state
        self.state = self._wire_state(active)

    def _cached_payloads(self, state: ViewerState) -> tuple[dict[str, Any], dict[str, Any]]:
        """The state's ``(geometry, meta)`` payloads, cached per world (spec R17).

        A tab switch re-sends the whole set every time, so the mesh buffers are
        built once per world; the editor's cache is dropped when a re-box moves
        the box (``geometry_changed``).
        """
        if state is self._result_state:
            return self._result_geometry, self._result_meta
        if self._viewer_state.geometry_changed or self._editor_geometry is None:
            self._viewer_state.geometry_changed = False
            self._editor_geometry, self._editor_meta = build_payload(self._viewer_state)
            self._editor_section = None
        return self._editor_geometry, self._editor_meta

    def _cached_section(self, state: ViewerState) -> dict[str, Any]:
        """The editor world's section payload, cached between plane moves.

        The result world's section is rebuilt once per result; the editor's
        changes only when the plane or its position moves, which the caller
        (``_on_msg``) detects — the cache here just avoids a re-slice on every
        tab switch.
        """
        if state is not self._viewer_state:
            return self._result_section
        if self._editor_section is None:
            self._editor_section = build_section_payload(self._viewer_state)
        return self._editor_section

    # ------------------------------------------------------------------ handle

    @property
    def stack(self) -> Any:
        """The :class:`~bifrost.stack.StackDevice` being viewed (spec D7)."""
        return self._viewer_state.stack

    @property
    def specs(self) -> list[Any]:
        """The source/detector specs (spec R12, D11)."""
        return self._viewer_state.specs

    @property
    def settings(self) -> Any:
        """The simulation settings (spec R13)."""
        return self._viewer_state.settings

    @property
    def running(self) -> bool:
        """Whether a mode-1 run is in flight (spec D15)."""
        return self._viewer_state.running

    def _prepare(self) -> tuple[list[Any], Any, list[Any]]:
        """The prepared objects, config and constraints (spec R14 mode 2, D11).

        Memoized on the state revision: the three properties (``objects``,
        ``config``, ``constraints``) share one build, so the constraints'
        object names always match the objects they were built with. A setup
        edit bumps the revision and forces a rebuild on next access.
        """
        revision = self._viewer_state.revision
        if self._prepared is None or self._prepared[0] != revision:
            self._prepared = (revision, build_objects(self.stack, self._viewer_state))
        return self._prepared[1]

    @property
    def objects(self) -> list[Any]:
        """The prepared simulation objects, computed on access (spec D11)."""
        return self._prepare()[0]

    @property
    def config(self) -> Any:
        """The prepared ``fdtdx.SimulationConfig``, computed on access (spec D11)."""
        return self._prepare()[1]

    @property
    def constraints(self) -> list[Any]:
        """The prepared placement constraints, computed on access (spec D11)."""
        return self._prepare()[2]

    def run(self, on_done: Callable[[Any], None] | None = None, key: Any = None) -> None:
        """Places and runs the simulation on a background thread (spec D15).

        Returns immediately; the widget stays interactive. While the run is in
        flight the setup is locked (``state['running']`` is true and setup edits
        are rejected). On completion ``run_result`` carries a summary and
        ``state['running']`` returns to false; detector results are on
        ``self.last_result.arrays.detector_states``.

        Args:
            on_done: called with the :class:`~bifrost.viewer.sim.RunResult`
                (or None on error) when the run finishes, on the worker thread.
            key: an optional ``jax`` PRNG key.

        Raises:
            RuntimeError: if a run is already in flight.
        """
        if self._viewer_state.running:
            raise RuntimeError("A run is already in flight")
        # snapshot the previous result before clearing, so a discarded stop can
        # restore it (spec D17)
        self._previous_result = self._run_result
        self._run_result = None
        self._run_error = None
        self._viewer_state.running = True
        self.state = self._viewer_state.to_state()
        self.run_result = {"status": "running"}
        self.progress = {}
        self.phase = "placing"
        self.stopping = False
        self._run_cancel = threading.Event()

        # capture the marimo runtime context here, on the cell's thread; the
        # worker must install it or its trait updates never reach the browser
        runtime_context = _marimo_runtime_context()

        def _on_progress(step: int, total: int) -> None:
            self.progress = {"step": int(step), "total": int(total)}

        def _on_phase(name: str) -> None:
            # once a stop is requested the phase must stay "stopping", so a later
            # "compiling" from the same run cannot overwrite it (spec D17)
            if self._run_cancel is not None and self._run_cancel.is_set():
                return
            self.phase = name

        def _work() -> None:
            if runtime_context is not None:
                with runtime_context.install():
                    _work_inner()
            else:
                _work_inner()

        def _work_inner() -> None:
            try:
                result = _run_sim(
                    self.stack,
                    self._viewer_state,
                    key=key,
                    progress=_on_progress,
                    phase=_on_phase,
                    cancel=self._run_cancel,
                    keep_partial=self._viewer_state.settings.keep_partial,
                )
                self._run_result = result
                self.run_result = {"status": "done", "detectors": _detector_names(result)}
                # the result lands in the result world (spec R17): the editor's
                # setup and styles are untouched, the Result tab shades from it;
                # the editor's own setup is what ran, so a loaded result's copies go
                self._viewer_state.clear_adopted()
                self._install_result(result, self.stack)
                # the field buffers ride the same worker-thread context install
                self._sync_active()
            except RunStopped:
                # a stopped run whose partial result is discarded: keep the
                # previous result and its overlays, report the stop (spec D17)
                self._run_result = self._previous_result
                self._run_error = None
                self.run_result = {"status": "stopped"}
            except BaseException as exc:
                self._run_error = exc
                self.run_result = {"status": "error", "message": str(exc)}
            finally:
                self._viewer_state.running = False
                self._sync_active()
                self.progress = {}
                self.phase = ""
                self.stopping = False
                self._run_cancel = None
                if on_done is not None:
                    on_done(self._run_result)

        self._run_thread = threading.Thread(target=_work, name="viewer-run", daemon=True)
        self._run_thread.start()

    def stop(self) -> bool:
        """Asks the in-flight run to stop (spec D17).

        Sets the run's cancel flag; the worker notices it at the next step or
        phase boundary and unwinds. Idempotent, and a no-op when no run is in
        flight. Does not clear ``running`` — the worker's ``finally`` owns that,
        so the widget never claims a run has ended before it has.

        Returns:
            True if a run was in flight and the stop was requested, False
            otherwise.
        """
        cancel = self._run_cancel
        if cancel is None or not self._viewer_state.running:
            return False
        cancel.set()
        self.stopping = True
        return True

    @property
    def last_result(self) -> Any:
        """The last completed run's :class:`~bifrost.viewer.sim.RunResult` (spec Q20)."""
        return self._run_result

    @property
    def last_error(self) -> BaseException | None:
        """The last run's exception, if it failed (spec Q20)."""
        return self._run_error

    def show_result(self, result: Any) -> None:
        """Adopts a result the user produced elsewhere as the current result (R15).

        The common case is a user who ran the simulation under ``srun`` and
        wants to look at the fields in the widget: this adopts that result's
        detector arrays and redraws every overlay, with no re-run and no
        re-send of the geometry. It does **not** adopt the result's geometry or
        objects — the widget's own stack and specs stay authoritative.

        Args:
            result: a :class:`~bifrost.viewer.sim.RunResult`, or any object
                with ``.arrays.detector_states``, ``.objects`` and ``.config``.

        Raises:
            ValueError: if the result's detectors do not correspond to the
                widget's current setup by name, grid shape, grid spacing and
                plane position (spec Q23). The previous result and its overlays
                are left untouched.
        """
        validate_result(self._viewer_state, result)
        self._run_result = result
        self._run_error = None
        self.run_result = {"status": "done", "detectors": _detector_names(result)}
        # the result lands in the result world too (spec R17): same stack as the
        # editor here, but the Result tab shades from the result world's specs
        self._viewer_state.clear_adopted()
        self._install_result(result, self.stack)
        self._sync_active()

    def load_result(
        self,
        objects: Any,
        arrays: Any,
        config: Any,
        stack: Any = None,
        *,
        settings: Any = None,
    ) -> None:
        """Displays a completed simulation from arbitrary user code (spec R16, D18).

        Takes the three pieces fdtdx hands out directly —
        :func:`fdtdx.place_objects` returns ``(objects, config)`` and
        :func:`fdtdx.run_fdtd` returns ``(time_step, arrays)`` — so nothing is
        assembled for it; the widget's own run result passes as
        ``load_result(*result)``. The result lands in the viewer's **result
        world** (spec R17): a separate state built from ``stack`` (defaulting
        to the stack the widget was built from) that the Result tab shows —
        its own geometry, one read-only adopted spec per source and detector in
        ``objects`` (spec R16), and the phasor-family detectors shaded from
        ``arrays.detector_states``. The editor's geometry and styles are
        untouched. When the result is on the widget's own stack, the editor
        lists the same sources and detectors, as read-only copies, so the two
        tabs agree.

        The adopted sources and detectors are excluded from a later
        :meth:`run`, and a run (or a second ``load_result``) replaces them: the
        setup you add yourself is what runs, and is never re-created from them.

        Args:
            objects: the result's placed :class:`fdtdx.objects.container.ObjectContainer`.
            arrays: the result's fdtdx ``ArrayContainer``.
            config: the result's :class:`fdtdx.config.SimulationConfig`.
            stack: the :class:`~bifrost.stack.StackDevice` the result
                was recorded on; defaults to the widget's current stack.
            settings: kernel-side :class:`~bifrost.viewer.sim.SimSettings`
                for the result world; defaults to fresh default settings.

        Raises:
            RuntimeError: if a run is in flight (spec D15).
            ValueError: if the result lacks a required piece, a phasor detector
                could never shade, or ``stack`` disagrees with the result's
                grid. The previous result and overlays are left untouched.
        """
        if self._viewer_state.running:
            raise RuntimeError("a run is in flight; load_result is refused")
        from .fields import validate_foreign_result

        if stack is None:
            stack = self._viewer_state.stack
        validate_foreign_result(objects, arrays, config, stack=stack)
        result = RunResult(objects, arrays, config)
        self._install_result(result, stack, settings=settings)
        self._list_in_editor(result, stack)
        self._run_result = result
        self._run_error = None
        self._previous_result = None
        self.run_result = {"status": "done", "detectors": _detector_names(result)}
        # switching to the result world re-sends its traits; the editor's stay
        # untouched and come back when the user leaves the Result tab (spec R17)
        self._sync_active(force=True)

    # ------------------------------------------------------------------ messages

    def _on_msg(self, _widget: Any, content: dict[str, Any], buffers: list) -> None:
        """Applies a browser message to the kernel state and re-syncs (spec D10).

        Handles a state ``patch`` (the general path) and the setup events
        (add/update/remove a source or detector, edit the settings). While a run
        is in flight the setup events and any setup-editing patch are refused
        (spec D15): the state is re-synced unchanged, so the browser cannot
        diverge from the kernel's authority.
        """
        event = content.get("event")
        if event == "run":
            # mode 1 from the widget's Run action (spec R14, D15); a second
            # request while a run is in flight is ignored, not an error
            try:
                self.run()
            except RuntimeError:
                pass
            return
        if event == "stop":
            # the Run button's stop toggle (spec D17): flag the in-flight run so
            # its worker unwinds at the next step or phase boundary
            self.stop()
            return
        if event == "save_image":
            # the browser's save button, routed to the kernel (spec D14): write
            # the PNG beside the notebook and report the path back
            self._save_image(content)
            return
        if event == "save_video":
            # a multi-frame detector's "save video": the browser encoded the
            # frames; wrap them into an MP4 and save or hand it back
            self._save_video(content, buffers)
            return
        if event == "set_save_dir":
            # the settings screen's folder field (spec D14): point server-side
            # saves at a new directory, creating it if the write is legal
            self._set_save_dir(content.get("dir"))
            return
        before = (self._viewer_state.plane, self._viewer_state.plane_position)
        result_before = (
            (self._result_state.plane, self._result_state.plane_position)
            if self._result_state is not None else None
        )
        tab_before = self._viewer_state.active_tab
        revision_before = self._viewer_state.revision
        patch = dict(content.get("patch") or {}) if event == "set_state" else None
        if patch is not None and "active_tab" in patch:
            # the tab itself routes the worlds (spec R17), so it always edits
            # the editor state, whichever world is on the wire
            try:
                self._viewer_state.apply_patch({"active_tab": patch.pop("active_tab")})
            except (ValueError, TypeError, KeyError):
                patch = None
        active = self._active_state()
        try:
            if patch is not None:
                if patch:
                    active.apply_patch(patch)
            elif event in _SETUP_EVENTS:
                # setup events author the editor's setup (spec R17) — except a
                # display edit from the Result tab (a field setting or an
                # adopted row's visibility, spec R17): the result world's
                # adopted specs own those, so while its tab is active the edit
                # is routed to it
                if event == "update_spec" and self._result_state is not None \
                        and self._viewer_state.active_tab == "result":
                    self._result_state.apply_patch(
                        {f"{content.get('kind', 'detector')}s": [
                            {"op": "update", **content.get("spec", {})},
                        ]}
                    )
                    self.state = self._wire_state(self._result_state)
                    return
                self._apply_setup_event(event, content)
            else:
                return
        except (ValueError, TypeError, KeyError):
            pass  # a bad or refused message leaves the state unchanged
        if tab_before != self._viewer_state.active_tab:
            # a tab switch swaps which world the traits carry (spec R17): the
            # Result tab shows the result world's geometry and state, the other
            # tabs the editor's, so re-send the now-active world's set
            self._sync_active(force=True)
            return
        if self._viewer_state.revision != revision_before:
            # a setup edit landed (spec R12/R13): refresh the launch code so
            # the browser's export button copies the current tail (spec D12)
            self.launch_code = _launch_code_text(self.stack, self._viewer_state)
        self.state = self._wire_state(active)
        if active is self._result_state:
            # the result world tracks no editor edits; styles ride `state`. Its
            # own plane and position still drive the Result tab's 2D view, so a
            # move re-slices the result's section (the axes follow the plane)
            if (active.plane, active.plane_position) != result_before:
                self._result_section = build_section_payload(active)
                self.section = self._result_section
            return
        if self._viewer_state.geometry_changed:
            # a boundary_offset edit re-boxed the device (spec R13): re-send the
            # mesh buffers and metadata so the browser rebuilds its viewports
            geometry, meta = self._cached_payloads(self._viewer_state)
            self.geometry = geometry
            self.meta = meta
            self._editor_section = None
            self.section = self._cached_section(self._viewer_state)
            return
        after = (self._viewer_state.plane, self._viewer_state.plane_position)
        if after != before:
            self._editor_section = build_section_payload(self._viewer_state)
            self.section = self._editor_section

    def _apply_setup_event(self, event: str, content: dict[str, Any]) -> None:
        """Turns a setup event into a state patch (spec R12, R13, D15).

        Refused while a run is in flight, so the message can only touch the
        setup when the lock is clear.
        """
        if self._viewer_state.running:
            raise ValueError("The setup is locked while a run is in flight")
        if event == "add_source":
            self._viewer_state.apply_patch({"sources": [{"op": "add", "port": content["port"],
                                                         **({"spec": content["spec"]} if "spec" in content else {})}]})
        elif event == "add_detector":
            self._viewer_state.apply_patch({"detectors": [{"op": "add", "port": content["port"],
                                                           **({"spec": content["spec"]} if "spec" in content else {})}]})
        elif event == "update_spec":
            key = "sources" if content["kind"] == "source" else "detectors"
            self._viewer_state.apply_patch({key: [{"op": "update", **content["spec"]}]})
        elif event == "remove_spec":
            key = "sources" if content["kind"] == "source" else "detectors"
            self._viewer_state.apply_patch({key: [{"op": "remove", "id": content["id"]}]})
        elif event == "set_settings":
            self._viewer_state.apply_patch({"simulation": content["settings"]})

    def _set_save_dir(self, folder: Any) -> None:
        """Points server-side saves at ``folder``, creating it (spec D14).

        The browser cannot browse the server's filesystem, so the folder comes
        in as a path string. A relative path resolves against the current
        :attr:`save_dir`, so a short name like ``shots`` nests under the
        existing target. The directory is created here — so a typo or an
        unwritable path is reported at once, not on the next save.
        """
        if not isinstance(folder, str) or not folder.strip():
            self.save_result = {"error": "enter a folder path"}
            return
        try:
            target = Path(folder).expanduser()
            if not target.is_absolute():
                target = Path(self.save_dir) / target
            target.mkdir(parents=True, exist_ok=True)
        except OSError as exc:
            self.save_result = {"error": str(exc)}
        else:
            self.save_dir = str(target.resolve())
            self.save_result = {"folder": self.save_dir}

    def _save_image(self, content: dict[str, Any]) -> None:
        """Writes a browser PNG under :attr:`save_dir` (spec D14).

        The browser sends a ``data:image/png;base64,...`` URL and a filename;
        the file lands in ``save_dir`` (created if missing) and ``save_result``
        carries the absolute path, or an ``error`` string if the write failed.
        An existing file is kept: a ``-1``, ``-2``, … suffix is appended until
        the name is free, so repeated saves never clobber an earlier one.
        """
        data = content.get("data")
        filename = content.get("filename") or "viewer.png"
        try:
            raw = _decode_data_url(data)
            target = _free_path(Path(self.save_dir) / Path(filename).name)
            target.parent.mkdir(parents=True, exist_ok=True)
            target.write_bytes(raw)
        except (ValueError, OSError, binascii.Error) as exc:
            self.save_result = {"error": str(exc)}
        else:
            self.save_result = {"path": str(target.resolve())}

    def _save_video(self, content: dict[str, Any], buffers: list) -> None:
        """Saves a browser-encoded video of a detector's frames as an MP4.

        The browser renders and encodes every frame of a multi-frame detector
        (WebCodecs) and sends the stream as one binary buffer with its
        ``codec`` and ``fps``; :func:`wrap_video` makes the MP4. With
        ``target == "server"`` the file lands in :attr:`save_dir` (never
        clobbering, like :meth:`_save_image`) and ``save_result`` carries the
        path; otherwise the MP4 goes back to the browser as a ``video_ready``
        message, which downloads it. A failure sets ``save_result["error"]``.
        """
        name = Path(str(content.get("filename") or "detector")).name
        name = name if name.endswith(".mp4") else f"{name}.mp4"
        server = content.get("target") == "server"
        if not buffers:
            self.save_result = {"error": "video: the browser sent no video data"}
            return
        try:
            mp4 = wrap_video(bytes(buffers[0]), str(content.get("codec")), int(content.get("fps") or 20))
            if server:
                target = _free_path(Path(self.save_dir) / name)
                target.parent.mkdir(parents=True, exist_ok=True)
                target.write_bytes(mp4)
            else:
                self.send({"event": "video_ready", "filename": name}, buffers=[mp4])
        except (ValueError, OSError, RuntimeError, subprocess.SubprocessError) as exc:
            self.save_result = {"error": f"video: {exc}"}
        else:
            if server:
                self.save_result = {"path": str(target.resolve())}

config property

config: Any

The prepared fdtdx.SimulationConfig, computed on access (spec D11).

constraints property

constraints: list[Any]

The prepared placement constraints, computed on access (spec D11).

last_error property

last_error: BaseException | None

The last run's exception, if it failed (spec Q20).

last_result property

last_result: Any

The last completed run's :class:~bifrost.viewer.sim.RunResult (spec Q20).

objects property

objects: list[Any]

The prepared simulation objects, computed on access (spec D11).

running property

running: bool

Whether a mode-1 run is in flight (spec D15).

settings property

settings: Any

The simulation settings (spec R13).

specs property

specs: list[Any]

The source/detector specs (spec R12, D11).

stack property

stack: Any

The :class:~bifrost.stack.StackDevice being viewed (spec D7).

__init__

__init__(stack: Any, save_dir: Path | str | None = None, **kwargs: Any) -> None

Builds the device's geometry and prepares the widget.

Parameters:

Name Type Description Default
stack Any

a :class:~bifrost.stack.StackDevice.

required
save_dir Path | str | None

where the save button writes a PNG when the browser prefers a server-side save (spec D14). Defaults to the process working directory, resolved once at construction so a later os.chdir does not move the target.

None
**kwargs Any

passed to :class:anywidget.AnyWidget.

{}
Source code in bifrost/viewer/widget.py
def __init__(self, stack: Any, save_dir: Path | str | None = None, **kwargs: Any) -> None:
    """Builds the device's geometry and prepares the widget.

    Args:
        stack: a :class:`~bifrost.stack.StackDevice`.
        save_dir: where the save button writes a PNG when the browser
            prefers a server-side save (spec D14). Defaults to the process
            working directory, resolved once at construction so a later
            ``os.chdir`` does not move the target.
        **kwargs: passed to :class:`anywidget.AnyWidget`.
    """
    self._viewer_state = ViewerState(stack)
    #: the result's own world (spec R17): a separate :class:`ViewerState`
    #: built from the result's stack, holding the adopted specs and the
    #: styles the Result tab edits. ``None`` until the first result arrives
    #: (a run or a ``load_result``); while it is ``None`` the Result tab
    #: shows its empty note. The editor world is never touched by a result.
    self._result_state: ViewerState | None = None
    #: the result world's cached geometry/section payloads, rebuilt only
    #: when the result changes (they do not track editor edits)
    self._result_geometry: dict[str, Any] = {}
    self._result_meta: dict[str, Any] = {}
    self._result_section: dict[str, Any] = {}
    #: the result world the ``fields`` trait was last built from — so a
    #: fresh result republishes its payload once, even from an editor tab
    self._fields_state: ViewerState | None = None
    #: the editor world's cached payloads (see :meth:`_cached_payloads`)
    self._editor_geometry: dict[str, Any] | None = None
    self._editor_meta: dict[str, Any] | None = None
    self._editor_section: dict[str, Any] | None = None
    initial_save_dir = Path(save_dir).expanduser() if save_dir is not None else Path.cwd()
    self._run_result: Any = None
    self._run_error: BaseException | None = None
    self._run_thread: threading.Thread | None = None
    #: the stop flag for the in-flight run (spec D17); ``None`` when no run
    #: is in flight. Set by :meth:`stop` and read by the worker's condition.
    self._run_cancel: threading.Event | None = None
    #: the result in place before the current run started, so a *discarded*
    #: stop can leave ``last_result`` as it was rather than as ``None`` (D17)
    self._previous_result: Any = None
    #: cache of the prepared ``(objects, config, constraints)``, keyed by the
    #: state revision, so the three properties share one build (spec D11)
    self._prepared: tuple[int, tuple[list[Any], Any, list[Any]]] | None = None
    super().__init__(**kwargs)
    geometry, meta = build_payload(self._viewer_state)
    self.geometry = geometry
    self.meta = meta
    self.state = self._viewer_state.to_state()
    self.section = build_section_payload(self._viewer_state)
    self.save_dir = str(initial_save_dir.resolve())
    self.launch_code = _launch_code_text(self.stack, self._viewer_state)
    self.on_msg(self._on_msg)

load_result

load_result(objects: Any, arrays: Any, config: Any, stack: Any = None, *, settings: Any = None) -> None

Displays a completed simulation from arbitrary user code (spec R16, D18).

Takes the three pieces fdtdx hands out directly — :func:fdtdx.place_objects returns (objects, config) and :func:fdtdx.run_fdtd returns (time_step, arrays) — so nothing is assembled for it; the widget's own run result passes as load_result(*result). The result lands in the viewer's result world (spec R17): a separate state built from stack (defaulting to the stack the widget was built from) that the Result tab shows — its own geometry, one read-only adopted spec per source and detector in objects (spec R16), and the phasor-family detectors shaded from arrays.detector_states. The editor's geometry and styles are untouched. When the result is on the widget's own stack, the editor lists the same sources and detectors, as read-only copies, so the two tabs agree.

The adopted sources and detectors are excluded from a later :meth:run, and a run (or a second load_result) replaces them: the setup you add yourself is what runs, and is never re-created from them.

Parameters:

Name Type Description Default
objects Any

the result's placed :class:fdtdx.objects.container.ObjectContainer.

required
arrays Any

the result's fdtdx ArrayContainer.

required
config Any

the result's :class:fdtdx.config.SimulationConfig.

required
stack Any

the :class:~bifrost.stack.StackDevice the result was recorded on; defaults to the widget's current stack.

None
settings Any

kernel-side :class:~bifrost.viewer.sim.SimSettings for the result world; defaults to fresh default settings.

None

Raises:

Type Description
RuntimeError

if a run is in flight (spec D15).

ValueError

if the result lacks a required piece, a phasor detector could never shade, or stack disagrees with the result's grid. The previous result and overlays are left untouched.

Source code in bifrost/viewer/widget.py
def load_result(
    self,
    objects: Any,
    arrays: Any,
    config: Any,
    stack: Any = None,
    *,
    settings: Any = None,
) -> None:
    """Displays a completed simulation from arbitrary user code (spec R16, D18).

    Takes the three pieces fdtdx hands out directly —
    :func:`fdtdx.place_objects` returns ``(objects, config)`` and
    :func:`fdtdx.run_fdtd` returns ``(time_step, arrays)`` — so nothing is
    assembled for it; the widget's own run result passes as
    ``load_result(*result)``. The result lands in the viewer's **result
    world** (spec R17): a separate state built from ``stack`` (defaulting
    to the stack the widget was built from) that the Result tab shows —
    its own geometry, one read-only adopted spec per source and detector in
    ``objects`` (spec R16), and the phasor-family detectors shaded from
    ``arrays.detector_states``. The editor's geometry and styles are
    untouched. When the result is on the widget's own stack, the editor
    lists the same sources and detectors, as read-only copies, so the two
    tabs agree.

    The adopted sources and detectors are excluded from a later
    :meth:`run`, and a run (or a second ``load_result``) replaces them: the
    setup you add yourself is what runs, and is never re-created from them.

    Args:
        objects: the result's placed :class:`fdtdx.objects.container.ObjectContainer`.
        arrays: the result's fdtdx ``ArrayContainer``.
        config: the result's :class:`fdtdx.config.SimulationConfig`.
        stack: the :class:`~bifrost.stack.StackDevice` the result
            was recorded on; defaults to the widget's current stack.
        settings: kernel-side :class:`~bifrost.viewer.sim.SimSettings`
            for the result world; defaults to fresh default settings.

    Raises:
        RuntimeError: if a run is in flight (spec D15).
        ValueError: if the result lacks a required piece, a phasor detector
            could never shade, or ``stack`` disagrees with the result's
            grid. The previous result and overlays are left untouched.
    """
    if self._viewer_state.running:
        raise RuntimeError("a run is in flight; load_result is refused")
    from .fields import validate_foreign_result

    if stack is None:
        stack = self._viewer_state.stack
    validate_foreign_result(objects, arrays, config, stack=stack)
    result = RunResult(objects, arrays, config)
    self._install_result(result, stack, settings=settings)
    self._list_in_editor(result, stack)
    self._run_result = result
    self._run_error = None
    self._previous_result = None
    self.run_result = {"status": "done", "detectors": _detector_names(result)}
    # switching to the result world re-sends its traits; the editor's stay
    # untouched and come back when the user leaves the Result tab (spec R17)
    self._sync_active(force=True)

run

run(on_done: Callable[[Any], None] | None = None, key: Any = None) -> None

Places and runs the simulation on a background thread (spec D15).

Returns immediately; the widget stays interactive. While the run is in flight the setup is locked (state['running'] is true and setup edits are rejected). On completion run_result carries a summary and state['running'] returns to false; detector results are on self.last_result.arrays.detector_states.

Parameters:

Name Type Description Default
on_done Callable[[Any], None] | None

called with the :class:~bifrost.viewer.sim.RunResult (or None on error) when the run finishes, on the worker thread.

None
key Any

an optional jax PRNG key.

None

Raises:

Type Description
RuntimeError

if a run is already in flight.

Source code in bifrost/viewer/widget.py
def run(self, on_done: Callable[[Any], None] | None = None, key: Any = None) -> None:
    """Places and runs the simulation on a background thread (spec D15).

    Returns immediately; the widget stays interactive. While the run is in
    flight the setup is locked (``state['running']`` is true and setup edits
    are rejected). On completion ``run_result`` carries a summary and
    ``state['running']`` returns to false; detector results are on
    ``self.last_result.arrays.detector_states``.

    Args:
        on_done: called with the :class:`~bifrost.viewer.sim.RunResult`
            (or None on error) when the run finishes, on the worker thread.
        key: an optional ``jax`` PRNG key.

    Raises:
        RuntimeError: if a run is already in flight.
    """
    if self._viewer_state.running:
        raise RuntimeError("A run is already in flight")
    # snapshot the previous result before clearing, so a discarded stop can
    # restore it (spec D17)
    self._previous_result = self._run_result
    self._run_result = None
    self._run_error = None
    self._viewer_state.running = True
    self.state = self._viewer_state.to_state()
    self.run_result = {"status": "running"}
    self.progress = {}
    self.phase = "placing"
    self.stopping = False
    self._run_cancel = threading.Event()

    # capture the marimo runtime context here, on the cell's thread; the
    # worker must install it or its trait updates never reach the browser
    runtime_context = _marimo_runtime_context()

    def _on_progress(step: int, total: int) -> None:
        self.progress = {"step": int(step), "total": int(total)}

    def _on_phase(name: str) -> None:
        # once a stop is requested the phase must stay "stopping", so a later
        # "compiling" from the same run cannot overwrite it (spec D17)
        if self._run_cancel is not None and self._run_cancel.is_set():
            return
        self.phase = name

    def _work() -> None:
        if runtime_context is not None:
            with runtime_context.install():
                _work_inner()
        else:
            _work_inner()

    def _work_inner() -> None:
        try:
            result = _run_sim(
                self.stack,
                self._viewer_state,
                key=key,
                progress=_on_progress,
                phase=_on_phase,
                cancel=self._run_cancel,
                keep_partial=self._viewer_state.settings.keep_partial,
            )
            self._run_result = result
            self.run_result = {"status": "done", "detectors": _detector_names(result)}
            # the result lands in the result world (spec R17): the editor's
            # setup and styles are untouched, the Result tab shades from it;
            # the editor's own setup is what ran, so a loaded result's copies go
            self._viewer_state.clear_adopted()
            self._install_result(result, self.stack)
            # the field buffers ride the same worker-thread context install
            self._sync_active()
        except RunStopped:
            # a stopped run whose partial result is discarded: keep the
            # previous result and its overlays, report the stop (spec D17)
            self._run_result = self._previous_result
            self._run_error = None
            self.run_result = {"status": "stopped"}
        except BaseException as exc:
            self._run_error = exc
            self.run_result = {"status": "error", "message": str(exc)}
        finally:
            self._viewer_state.running = False
            self._sync_active()
            self.progress = {}
            self.phase = ""
            self.stopping = False
            self._run_cancel = None
            if on_done is not None:
                on_done(self._run_result)

    self._run_thread = threading.Thread(target=_work, name="viewer-run", daemon=True)
    self._run_thread.start()

show_result

show_result(result: Any) -> None

Adopts a result the user produced elsewhere as the current result (R15).

The common case is a user who ran the simulation under srun and wants to look at the fields in the widget: this adopts that result's detector arrays and redraws every overlay, with no re-run and no re-send of the geometry. It does not adopt the result's geometry or objects — the widget's own stack and specs stay authoritative.

Parameters:

Name Type Description Default
result Any

a :class:~bifrost.viewer.sim.RunResult, or any object with .arrays.detector_states, .objects and .config.

required

Raises:

Type Description
ValueError

if the result's detectors do not correspond to the widget's current setup by name, grid shape, grid spacing and plane position (spec Q23). The previous result and its overlays are left untouched.

Source code in bifrost/viewer/widget.py
def show_result(self, result: Any) -> None:
    """Adopts a result the user produced elsewhere as the current result (R15).

    The common case is a user who ran the simulation under ``srun`` and
    wants to look at the fields in the widget: this adopts that result's
    detector arrays and redraws every overlay, with no re-run and no
    re-send of the geometry. It does **not** adopt the result's geometry or
    objects — the widget's own stack and specs stay authoritative.

    Args:
        result: a :class:`~bifrost.viewer.sim.RunResult`, or any object
            with ``.arrays.detector_states``, ``.objects`` and ``.config``.

    Raises:
        ValueError: if the result's detectors do not correspond to the
            widget's current setup by name, grid shape, grid spacing and
            plane position (spec Q23). The previous result and its overlays
            are left untouched.
    """
    validate_result(self._viewer_state, result)
    self._run_result = result
    self._run_error = None
    self.run_result = {"status": "done", "detectors": _detector_names(result)}
    # the result lands in the result world too (spec R17): same stack as the
    # editor here, but the Result tab shades from the result world's specs
    self._viewer_state.clear_adopted()
    self._install_result(result, self.stack)
    self._sync_active()

stop

stop() -> bool

Asks the in-flight run to stop (spec D17).

Sets the run's cancel flag; the worker notices it at the next step or phase boundary and unwinds. Idempotent, and a no-op when no run is in flight. Does not clear running — the worker's finally owns that, so the widget never claims a run has ended before it has.

Returns:

Type Description
bool

True if a run was in flight and the stop was requested, False

bool

otherwise.

Source code in bifrost/viewer/widget.py
def stop(self) -> bool:
    """Asks the in-flight run to stop (spec D17).

    Sets the run's cancel flag; the worker notices it at the next step or
    phase boundary and unwinds. Idempotent, and a no-op when no run is in
    flight. Does not clear ``running`` — the worker's ``finally`` owns that,
    so the widget never claims a run has ended before it has.

    Returns:
        True if a run was in flight and the stop was requested, False
        otherwise.
    """
    cancel = self._run_cancel
    if cancel is None or not self._viewer_state.running:
        return False
    cancel.set()
    self.stopping = True
    return True

SimSettings dataclass

The setup-wide simulation settings (spec R13).

Attributes:

Name Type Description
wavelength_um float

the default wavelength, in µm, for new sources/detectors.

pulse_width_frac float

the Gaussian pulse's spectral width as a fraction of the center frequency; 0 means a single-frequency (CW) run.

port_padding tuple[float, float]

(lateral, vertical) — a new plane extends this far past the port's width and the level's height on each side, in units of the mean wavelength of the source or detector (the settings' wavelength for one with none of its own); negative values shrink the plane.

pml_thickness int

the PML thickness, in grid cells.

resolution_um float

the grid resolution, in µm.

time_s float

the simulation time, in seconds.

boundary_offset tuple[float, float, float, float, float, float]

(x+, x-, y+, y-, z+, z-) in µm — how far the simulation box's faces move off the geometry's bounding box (the StackDevice box, spec R13). Each value moves its face along the axis, positive towards +x, +y or +z, so (1, -1, 0, 0, 0, 0) grows the box by 1 µm on each x side; the geometry itself does not change.

Source code in bifrost/viewer/sim.py
@dataclass
class SimSettings:
    """The setup-wide simulation settings (spec R13).

    Attributes:
        wavelength_um: the default wavelength, in µm, for new sources/detectors.
        pulse_width_frac: the Gaussian pulse's spectral width as a fraction of
            the center frequency; ``0`` means a single-frequency (CW) run.
        port_padding: ``(lateral, vertical)`` — a new plane extends this far past
            the port's width and the level's height on each side, in units of the
            mean wavelength of the source or detector (the settings' wavelength
            for one with none of its own); negative values shrink the plane.
        pml_thickness: the PML thickness, in grid cells.
        resolution_um: the grid resolution, in µm.
        time_s: the simulation time, in seconds.
        boundary_offset: ``(x+, x-, y+, y-, z+, z-)`` in µm — how far the
            simulation box's faces move off the geometry's bounding box (the
            ``StackDevice`` box, spec R13). Each value moves its face along the
            axis, positive towards +x, +y or +z, so ``(1, -1, 0, 0, 0, 0)``
            grows the box by 1 µm on each x side; the geometry itself does not
            change.
    """

    wavelength_um: float = 1.55
    pulse_width_frac: float = 0.0
    port_padding: tuple[float, float] = (3.0, 2.0)
    pml_thickness: int = 10
    resolution_um: float = 0.02
    time_s: float = 400e-15
    boundary_offset: tuple[float, float, float, float, float, float] = (
        0.0, 0.0, 0.0, 0.0, 0.0, 0.0,
    )
    #: the number of decades below a plane's maximum that ``logabs`` floors at
    #: (spec D16, Q22). One value for every detector's overlay; the bottom of the
    #: ``logabs`` color ramp, so a true zero maps to the floor, not to ``-inf``.
    log_range: float = 6.0
    #: how the device averages the permittivity at material interfaces (spec
    #: R13), one of :data:`AVERAGINGS`: ``"subpixel"`` (Kottke, the diagonal of
    #: the averaged tensor), ``"subpixel_full"`` (the full tensor, with the
    #: off-diagonal entries oblique and sloped walls need; about 5x the time
    #: per step and 1.8x the memory) or ``"inverse"`` (fdtdx's isotropic
    #: inverse average). The viewer seeds it from the device's own
    #: ``averaging`` and places the device with it.
    averaging: str = "subpixel"
    #: whether a run the user stops keeps its partial result (spec D17). When
    #: False (the default) a stopped run is discarded and the previous result
    #: stays on screen; when True its detectors' partial fields are shown.
    keep_partial: bool = False

    def to_wire(self) -> dict[str, Any]:
        """The settings as JSON for the browser (spec R13)."""
        return {
            "wavelength_um": float(self.wavelength_um),
            "pulse_width_frac": float(self.pulse_width_frac),
            "port_padding": [float(self.port_padding[0]), float(self.port_padding[1])],
            "pml_thickness": int(self.pml_thickness),
            "resolution_um": float(self.resolution_um),
            "time_s": float(self.time_s),
            "boundary_offset": [float(v) for v in self.boundary_offset],
            "log_range": float(self.log_range),
            "averaging": str(self.averaging),
            "keep_partial": bool(self.keep_partial),
        }

    @classmethod
    def from_wire(cls, data: dict[str, Any]) -> SimSettings:
        """Rebuilds settings from a browser patch, validating each field."""
        defaults = cls()
        padding = data.get("port_padding", defaults.port_padding)
        if not (
            isinstance(padding, (list, tuple))
            and len(padding) == 2
            and all(math.isfinite(float(v)) for v in padding)
        ):
            raise ValueError(f"port_padding must be two finite numbers, got {padding!r}")
        wavelength = float(data.get("wavelength_um", defaults.wavelength_um))
        resolution = float(data.get("resolution_um", defaults.resolution_um))
        time_s = float(data.get("time_s", defaults.time_s))
        pml = int(data.get("pml_thickness", defaults.pml_thickness))
        if wavelength <= 0 or resolution <= 0 or time_s <= 0 or pml < 0:
            raise ValueError("Settings must be positive (wavelength, resolution, time) or non-negative (PML)")
        offset = data.get("boundary_offset", defaults.boundary_offset)
        if not (isinstance(offset, (list, tuple)) and len(offset) == 6):
            raise ValueError(f"boundary_offset must be 6 numbers (x+, x-, y+, y-, z+, z-), got {offset!r}")
        offset = tuple(float(v) for v in offset)
        if not all(math.isfinite(v) for v in offset):
            raise ValueError(f"boundary_offset must be finite, got {offset!r}")
        log_range = float(data.get("log_range", defaults.log_range))
        if not math.isfinite(log_range) or log_range <= 0:
            raise ValueError(f"log_range must be a positive number of decades, got {log_range!r}")
        averaging = str(data.get("averaging", defaults.averaging))
        if averaging not in AVERAGINGS:
            raise ValueError(f"averaging must be one of {AVERAGINGS}, got {averaging!r}")
        return cls(
            wavelength_um=wavelength,
            pulse_width_frac=float(data.get("pulse_width_frac", defaults.pulse_width_frac)),
            port_padding=(float(padding[0]), float(padding[1])),
            pml_thickness=pml,
            resolution_um=resolution,
            time_s=time_s,
            boundary_offset=offset,
            log_range=log_range,
            averaging=averaging,
            keep_partial=bool(data.get("keep_partial", defaults.keep_partial)),
        )

from_wire classmethod

from_wire(data: dict[str, Any]) -> SimSettings

Rebuilds settings from a browser patch, validating each field.

Source code in bifrost/viewer/sim.py
@classmethod
def from_wire(cls, data: dict[str, Any]) -> SimSettings:
    """Rebuilds settings from a browser patch, validating each field."""
    defaults = cls()
    padding = data.get("port_padding", defaults.port_padding)
    if not (
        isinstance(padding, (list, tuple))
        and len(padding) == 2
        and all(math.isfinite(float(v)) for v in padding)
    ):
        raise ValueError(f"port_padding must be two finite numbers, got {padding!r}")
    wavelength = float(data.get("wavelength_um", defaults.wavelength_um))
    resolution = float(data.get("resolution_um", defaults.resolution_um))
    time_s = float(data.get("time_s", defaults.time_s))
    pml = int(data.get("pml_thickness", defaults.pml_thickness))
    if wavelength <= 0 or resolution <= 0 or time_s <= 0 or pml < 0:
        raise ValueError("Settings must be positive (wavelength, resolution, time) or non-negative (PML)")
    offset = data.get("boundary_offset", defaults.boundary_offset)
    if not (isinstance(offset, (list, tuple)) and len(offset) == 6):
        raise ValueError(f"boundary_offset must be 6 numbers (x+, x-, y+, y-, z+, z-), got {offset!r}")
    offset = tuple(float(v) for v in offset)
    if not all(math.isfinite(v) for v in offset):
        raise ValueError(f"boundary_offset must be finite, got {offset!r}")
    log_range = float(data.get("log_range", defaults.log_range))
    if not math.isfinite(log_range) or log_range <= 0:
        raise ValueError(f"log_range must be a positive number of decades, got {log_range!r}")
    averaging = str(data.get("averaging", defaults.averaging))
    if averaging not in AVERAGINGS:
        raise ValueError(f"averaging must be one of {AVERAGINGS}, got {averaging!r}")
    return cls(
        wavelength_um=wavelength,
        pulse_width_frac=float(data.get("pulse_width_frac", defaults.pulse_width_frac)),
        port_padding=(float(padding[0]), float(padding[1])),
        pml_thickness=pml,
        resolution_um=resolution,
        time_s=time_s,
        boundary_offset=offset,
        log_range=log_range,
        averaging=averaging,
        keep_partial=bool(data.get("keep_partial", defaults.keep_partial)),
    )

to_wire

to_wire() -> dict[str, Any]

The settings as JSON for the browser (spec R13).

Source code in bifrost/viewer/sim.py
def to_wire(self) -> dict[str, Any]:
    """The settings as JSON for the browser (spec R13)."""
    return {
        "wavelength_um": float(self.wavelength_um),
        "pulse_width_frac": float(self.pulse_width_frac),
        "port_padding": [float(self.port_padding[0]), float(self.port_padding[1])],
        "pml_thickness": int(self.pml_thickness),
        "resolution_um": float(self.resolution_um),
        "time_s": float(self.time_s),
        "boundary_offset": [float(v) for v in self.boundary_offset],
        "log_range": float(self.log_range),
        "averaging": str(self.averaging),
        "keep_partial": bool(self.keep_partial),
    }

ObjectSpec dataclass

A source or detector as data, not a live fdtdx object (spec D11).

Attributes:

Name Type Description
type str

a key of :data:TYPES.

port str

the port name the object sits on, or "None" for absolute placement (spec R18).

params dict[str, Any]

the editable parameters (port_offset, port_padding, level, direction, wavelengths, the absolute-placement keys and any type-specific extras). Absent keys fall back to the settings (spec R13).

id str

a stable identity for update/remove/selection. Assigned by :class:~bifrost.viewer.state.ViewerState; empty until then.

glyph dict[str, Any] | None

an override for the wire glyph (spec D18). Set on adopted foreign detectors, whose glyph is derived from their placed grid_slice rather than a port frame; never sent through to_wire — the wire glyph travels in state["glyphs"].

Source code in bifrost/viewer/sim.py
@dataclass
class ObjectSpec:
    """A source or detector as data, not a live fdtdx object (spec D11).

    Attributes:
        type: a key of :data:`TYPES`.
        port: the port name the object sits on, or ``"None"`` for absolute
            placement (spec R18).
        params: the editable parameters (``port_offset``, ``port_padding``,
            ``level``, ``direction``, ``wavelengths``, the absolute-placement
            keys and any type-specific extras). Absent keys fall back to the
            settings (spec R13).
        id: a stable identity for update/remove/selection. Assigned by
            :class:`~bifrost.viewer.state.ViewerState`; empty until then.
        glyph: an override for the wire glyph (spec D18). Set on adopted
            foreign detectors, whose glyph is derived from their placed
            ``grid_slice`` rather than a port frame; never sent through
            ``to_wire`` — the wire glyph travels in ``state["glyphs"]``.
    """

    type: str
    port: str
    params: dict[str, Any] = field(default_factory=dict)
    id: str = ""
    glyph: dict[str, Any] | None = None

    @property
    def category(self) -> str:
        """``"source"`` or ``"detector"``."""
        return TYPES[self.type].category

    @property
    def name(self) -> str:
        """The object's name (``params['name']`` or ``<port>_<type>``)."""
        return str(self.params.get("name") or f"{self.port}_{self.type}")

    @property
    def absolute(self) -> bool:
        """Whether this spec is placed absolutely (``port == "None"``, R18)."""
        return self.port == "None"

    def field_params(self) -> dict[str, Any]:
        """The detector's display settings with defaults filled in (spec R15).

        Only a detector has these; a source returns an empty dict. Absent keys
        fall back to the defaults, so the browser always gets a complete set.
        An unpicked component defaults to ``|E|`` (see
        :func:`_default_component`); one set to None — "(none)" picked — stays
        None. The outline defaults off.
        """
        if self.category != "detector":
            return {}
        params = self.params
        return {
            "field_component": params["field_component"] if "field_component" in params
            else _default_component(params),
            "field_transform": str(self.params.get("field_transform") or "abs"),
            "field_color_low": str(self.params.get("field_color_low") or FIELD_COLOR_LOW),
            "field_color_high": str(self.params.get("field_color_high") or FIELD_COLOR_HIGH),
            "field_opacity": _field_opacity(self.params.get("field_opacity", 1.0)),
            "field_visible": bool(self.params.get("field_visible", True)),
            "field_freq": int(self.params.get("field_freq", 0)),
            "field_outline": bool(self.params.get("field_outline", False)),
            # the colour scale's fixed [min, max], in the transform's units; None
            # means the browser spans the whole recording (every frame)
            "field_range": _field_range(self.params.get("field_range")),
        }

    def to_wire(self) -> dict[str, Any]:
        """The spec as JSON for the browser (spec D5)."""
        return {"id": self.id, "type": self.type, "port": self.port, "params": dict(self.params)}

    @classmethod
    def from_wire(cls, data: dict[str, Any]) -> ObjectSpec:
        """Rebuilds a spec from a browser patch, validating the type and port."""
        type_name = str(data.get("type", ""))
        if type_name not in TYPES:
            raise ValueError(f"Unknown source/detector type {type_name!r}")
        port = str(data.get("port", ""))
        if not port:
            raise ValueError("A source/detector needs a port")
        params = dict(data.get("params") or {})
        allowed = set(_COMMON) - ({"port_offset", "level", "port_padding"} if port == "None" else set())
        if port == "None":
            allowed |= set(_ABSOLUTE)
        unknown = set(params) - allowed - set(TYPES[type_name].extra)
        if unknown:
            raise ValueError(f"Unknown parameters for {type_name!r}: {sorted(unknown)}")
        _validate_absolute(port, params)
        validate_sampling(params)
        return cls(type=type_name, port=port, params=params, id=str(data.get("id") or ""))

absolute property

absolute: bool

Whether this spec is placed absolutely (port == "None", R18).

category property

category: str

"source" or "detector".

name property

name: str

The object's name (params['name'] or <port>_<type>).

field_params

field_params() -> dict[str, Any]

The detector's display settings with defaults filled in (spec R15).

Only a detector has these; a source returns an empty dict. Absent keys fall back to the defaults, so the browser always gets a complete set. An unpicked component defaults to |E| (see :func:_default_component); one set to None — "(none)" picked — stays None. The outline defaults off.

Source code in bifrost/viewer/sim.py
def field_params(self) -> dict[str, Any]:
    """The detector's display settings with defaults filled in (spec R15).

    Only a detector has these; a source returns an empty dict. Absent keys
    fall back to the defaults, so the browser always gets a complete set.
    An unpicked component defaults to ``|E|`` (see
    :func:`_default_component`); one set to None — "(none)" picked — stays
    None. The outline defaults off.
    """
    if self.category != "detector":
        return {}
    params = self.params
    return {
        "field_component": params["field_component"] if "field_component" in params
        else _default_component(params),
        "field_transform": str(self.params.get("field_transform") or "abs"),
        "field_color_low": str(self.params.get("field_color_low") or FIELD_COLOR_LOW),
        "field_color_high": str(self.params.get("field_color_high") or FIELD_COLOR_HIGH),
        "field_opacity": _field_opacity(self.params.get("field_opacity", 1.0)),
        "field_visible": bool(self.params.get("field_visible", True)),
        "field_freq": int(self.params.get("field_freq", 0)),
        "field_outline": bool(self.params.get("field_outline", False)),
        # the colour scale's fixed [min, max], in the transform's units; None
        # means the browser spans the whole recording (every frame)
        "field_range": _field_range(self.params.get("field_range")),
    }

from_wire classmethod

from_wire(data: dict[str, Any]) -> ObjectSpec

Rebuilds a spec from a browser patch, validating the type and port.

Source code in bifrost/viewer/sim.py
@classmethod
def from_wire(cls, data: dict[str, Any]) -> ObjectSpec:
    """Rebuilds a spec from a browser patch, validating the type and port."""
    type_name = str(data.get("type", ""))
    if type_name not in TYPES:
        raise ValueError(f"Unknown source/detector type {type_name!r}")
    port = str(data.get("port", ""))
    if not port:
        raise ValueError("A source/detector needs a port")
    params = dict(data.get("params") or {})
    allowed = set(_COMMON) - ({"port_offset", "level", "port_padding"} if port == "None" else set())
    if port == "None":
        allowed |= set(_ABSOLUTE)
    unknown = set(params) - allowed - set(TYPES[type_name].extra)
    if unknown:
        raise ValueError(f"Unknown parameters for {type_name!r}: {sorted(unknown)}")
    _validate_absolute(port, params)
    validate_sampling(params)
    return cls(type=type_name, port=port, params=params, id=str(data.get("id") or ""))

to_wire

to_wire() -> dict[str, Any]

The spec as JSON for the browser (spec D5).

Source code in bifrost/viewer/sim.py
def to_wire(self) -> dict[str, Any]:
    """The spec as JSON for the browser (spec D5)."""
    return {"id": self.id, "type": self.type, "port": self.port, "params": dict(self.params)}

RunResult

Bases: NamedTuple

The outcome of a mode-1 run (spec R14, Q20).

arrays is the fdtdx :class:ArrayContainer; detector results are read from arrays.detector_states keyed by detector name. objects is the placed :class:ObjectContainer, so objects[name].compute_overlap(...) works as in the reference example.

Source code in bifrost/viewer/sim.py
class RunResult(NamedTuple):
    """The outcome of a mode-1 run (spec R14, Q20).

    ``arrays`` is the fdtdx :class:`ArrayContainer`; detector results are read
    from ``arrays.detector_states`` keyed by detector name. ``objects`` is the
    placed :class:`ObjectContainer`, so ``objects[name].compute_overlap(...)``
    works as in the reference example.
    """

    objects: Any
    arrays: Any
    config: Any

export_demo

export_demo(viewer: Any, out_dir: str | Path, *, section_step_um: float = 0.1, field_defaults: dict[str, dict[str, Any]] | None = None, unchecked: Sequence[str] = (), manifest: dict[str, Any] | None = None) -> dict[str, Any]

Writes a viewer that holds a result (load_result or run) as demo files.

Parameters:

Name Type Description Default
viewer Any

a :class:~bifrost.viewer.widget.Viewer whose result world exists.

required
out_dir str | Path

the directory to write; created if missing.

required
section_step_um float

the spacing of the stored 2D slices along each plane's normal. The positions include the box center, which is where the field plane of a device centered on its core sits.

0.1
field_defaults dict[str, dict[str, Any]] | None

how each detector's field is shown when the demo opens, by detector name: any of the field_* display settings (field_component, field_transform, field_visible, ...). A time-domain detector opens on the frame with the most field energy (set field_freq to override).

None
unchecked Sequence[str]

names of sources and detectors that open with their checkbox off (hidden, with their field), in the editor's tabs and the Result tab alike.

()
manifest dict[str, Any] | None

extra entries for manifest.json (source revision, package versions, simulation parameters).

None

Returns:

Type Description
dict[str, Any]

The manifest, also written to manifest.json.

Raises:

Type Description
ValueError

if the viewer holds no result.

Source code in bifrost/viewer/export.py
def export_demo(
    viewer: Any,
    out_dir: str | Path,
    *,
    section_step_um: float = 0.1,
    field_defaults: dict[str, dict[str, Any]] | None = None,
    unchecked: Sequence[str] = (),
    manifest: dict[str, Any] | None = None,
) -> dict[str, Any]:
    """Writes a viewer that holds a result (``load_result`` or ``run``) as demo files.

    Args:
        viewer: a :class:`~bifrost.viewer.widget.Viewer` whose result world exists.
        out_dir: the directory to write; created if missing.
        section_step_um: the spacing of the stored 2D slices along each plane's normal. The
            positions include the box center, which is where the field plane of a device
            centered on its core sits.
        field_defaults: how each detector's field is shown when the demo opens, by detector
            name: any of the ``field_*`` display settings (``field_component``,
            ``field_transform``, ``field_visible``, ...). A time-domain detector opens on the
            frame with the most field energy (set ``field_freq`` to override).
        unchecked: names of sources and detectors that open with their checkbox off (hidden, with
            their field), in the editor's tabs and the Result tab alike.
        manifest: extra entries for ``manifest.json`` (source revision, package versions,
            simulation parameters).

    Returns:
        The manifest, also written to ``manifest.json``.

    Raises:
        ValueError: if the viewer holds no result.
    """
    if viewer._result_state is None:
        raise ValueError("the viewer holds no result: call viewer.load_result(...) or viewer.run() first")
    out = Path(out_dir)
    out.mkdir(parents=True, exist_ok=True)
    editor, result = viewer._viewer_state, viewer._result_state
    worlds: dict[str, Any] = {}
    total = 0

    fields = {
        spec_id: _with_magnitude(entry) for spec_id, entry in build_field_payload(result, viewer._run_result).items()
    }
    shown: dict[str, dict[str, Any]] = {}  # the opening display settings, by spec id
    for spec_id, entry in fields.items():
        settings: dict[str, Any] = {}
        if entry.get("frame_kind") == "time":
            settings["field_freq"] = _peak_frame(entry)
        settings.update((field_defaults or {}).get(entry["name"], {}))
        shown[spec_id] = settings
    for name, state, payloads in (
        ("editor", editor, viewer._cached_payloads(editor)),
        ("result", result, (viewer._result_geometry, viewer._result_meta)),
    ):
        geometry, meta = payloads
        mesh_index, mesh_bytes = _export_meshes(geometry, out / f"{name}.meshes.bin")
        wire_state = state.to_state()
        wire_state["active_tab"] = "result" if name == "result" else "layout"
        wire_state["readonly"] = {"reason": READONLY_REASON}
        _uncheck(wire_state, set(unchecked))
        if name == "result":
            wire_state["field_settings"] = {
                spec_id: {**wire_state["field_settings"].get(spec_id, {}), **settings}
                for spec_id, settings in shown.items()
            } | {k: v for k, v in wire_state["field_settings"].items() if k not in shown}
        size = _write_json(
            out / f"{name}.json", {"state": wire_state, "meta": meta, "meshes": mesh_index}
        )
        worlds[name] = {"json_bytes": size, "mesh_bytes": mesh_bytes}
        total += size + mesh_bytes

    field_index = {}
    for spec_id, entry in fields.items():
        field_index[spec_id] = _export_field(entry, out)
        total += sum(field_index[spec_id]["bytes"].values())

    sections = _export_sections(result, out, section_step_um)
    total += sum(item["bytes"] for item in sections.values())

    data = {
        "format": FORMAT,
        "worlds": worlds,
        "fields": field_index,
        "sections": sections,
        "total_bytes": total,
        **(manifest or {}),
    }
    _write_json(out / "manifest.json", data)
    return data