PyMieSimX 0.1.0__tar.gz → 0.1.1__tar.gz

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 (79) hide show
  1. {pymiesimx-0.1.0 → pymiesimx-0.1.1}/.github/workflows/quality.yml +1 -1
  2. pymiesimx-0.1.1/PKG-INFO +133 -0
  3. pymiesimx-0.1.1/PyMieSimX/__init__.py +58 -0
  4. {pymiesimx-0.1.0 → pymiesimx-0.1.1}/PyMieSimX/__main__.py +17 -6
  5. pymiesimx-0.1.1/PyMieSimX/api.py +39 -0
  6. pymiesimx-0.1.1/PyMieSimX/application/__init__.py +1 -0
  7. pymiesimx-0.1.1/PyMieSimX/application/wsgi.py +9 -0
  8. pymiesimx-0.1.1/PyMieSimX/gui/__init__.py +12 -0
  9. {pymiesimx-0.1.0 → pymiesimx-0.1.1}/PyMieSimX/gui/assets/dashboard.css +112 -1
  10. pymiesimx-0.1.1/PyMieSimX/gui/assets/materials-stock.json +142 -0
  11. pymiesimx-0.1.1/PyMieSimX/gui/callback_helpers.py +112 -0
  12. {pymiesimx-0.1.0 → pymiesimx-0.1.1}/PyMieSimX/gui/interface.py +212 -68
  13. pymiesimx-0.1.1/PyMieSimX/gui/jobs.py +118 -0
  14. {pymiesimx-0.1.0 → pymiesimx-0.1.1}/PyMieSimX/gui/layout.py +72 -3
  15. pymiesimx-0.1.1/PyMieSimX/gui/material_catalog.py +90 -0
  16. {pymiesimx-0.1.0 → pymiesimx-0.1.1}/PyMieSimX/gui/pages/__init__.py +3 -1
  17. {pymiesimx-0.1.0 → pymiesimx-0.1.1}/PyMieSimX/gui/pages/documentation.py +3 -60
  18. {pymiesimx-0.1.0 → pymiesimx-0.1.1}/PyMieSimX/gui/pages/experiment/page.py +2 -2
  19. {pymiesimx-0.1.0 → pymiesimx-0.1.1}/PyMieSimX/gui/pages/experiment/sections/configuration.py +3 -3
  20. pymiesimx-0.1.1/PyMieSimX/gui/pages/field_syntax.py +77 -0
  21. {pymiesimx-0.1.0 → pymiesimx-0.1.1}/PyMieSimX/gui/pages/home.py +3 -3
  22. {pymiesimx-0.1.0 → pymiesimx-0.1.1}/PyMieSimX/gui/pages/install_local.py +7 -4
  23. pymiesimx-0.1.1/PyMieSimX/gui/pages/sellmeier.py +169 -0
  24. {pymiesimx-0.1.0 → pymiesimx-0.1.1}/PyMieSimX/gui/pages/single/page.py +1 -1
  25. {pymiesimx-0.1.0 → pymiesimx-0.1.1}/PyMieSimX/gui/pages/single/sections/representations.py +1 -1
  26. {pymiesimx-0.1.0 → pymiesimx-0.1.1}/PyMieSimX/gui/pages/single/sections/setup.py +2 -2
  27. {pymiesimx-0.1.0 → pymiesimx-0.1.1}/PyMieSimX/gui/parsing.py +41 -6
  28. {pymiesimx-0.1.0 → pymiesimx-0.1.1}/PyMieSimX/gui/schemas.py +7 -7
  29. {pymiesimx-0.1.0 → pymiesimx-0.1.1}/PyMieSimX/gui/services.py +255 -9
  30. pymiesimx-0.1.1/PyMieSimX/gui/usage_metrics.py +225 -0
  31. pymiesimx-0.1.1/PyMieSimX.egg-info/PKG-INFO +133 -0
  32. {pymiesimx-0.1.0 → pymiesimx-0.1.1}/PyMieSimX.egg-info/SOURCES.txt +15 -1
  33. {pymiesimx-0.1.0 → pymiesimx-0.1.1}/PyMieSimX.egg-info/requires.txt +6 -5
  34. pymiesimx-0.1.1/PyMieSimX.egg-info/scm_file_list.json +67 -0
  35. pymiesimx-0.1.1/PyMieSimX.egg-info/scm_version.json +8 -0
  36. pymiesimx-0.1.1/README.rst +106 -0
  37. {pymiesimx-0.1.0 → pymiesimx-0.1.1}/meta.yaml +6 -5
  38. {pymiesimx-0.1.0 → pymiesimx-0.1.1}/pyproject.toml +12 -7
  39. pymiesimx-0.1.1/tests/gui/test_api.py +59 -0
  40. pymiesimx-0.1.1/tests/gui/test_services.py +221 -0
  41. pymiesimx-0.1.1/tests/gui/test_usage_metrics.py +30 -0
  42. pymiesimx-0.1.0/PKG-INFO +0 -90
  43. pymiesimx-0.1.0/PyMieSimX/__init__.py +0 -5
  44. pymiesimx-0.1.0/PyMieSimX/gui/__init__.py +0 -5
  45. pymiesimx-0.1.0/PyMieSimX.egg-info/PKG-INFO +0 -90
  46. pymiesimx-0.1.0/README.rst +0 -43
  47. pymiesimx-0.1.0/tests/gui/test_services.py +0 -87
  48. {pymiesimx-0.1.0 → pymiesimx-0.1.1}/.coveragerc +0 -0
  49. {pymiesimx-0.1.0 → pymiesimx-0.1.1}/.flake8 +0 -0
  50. {pymiesimx-0.1.0 → pymiesimx-0.1.1}/.github/dependabot.yml +0 -0
  51. {pymiesimx-0.1.0 → pymiesimx-0.1.1}/.github/workflows/deploy_PyPi.yml +0 -0
  52. {pymiesimx-0.1.0 → pymiesimx-0.1.1}/.github/workflows/deploy_anaconda.yml +0 -0
  53. {pymiesimx-0.1.0 → pymiesimx-0.1.1}/.github/workflows/deploy_coverage.yml +0 -0
  54. {pymiesimx-0.1.0 → pymiesimx-0.1.1}/.gitignore +0 -0
  55. {pymiesimx-0.1.0 → pymiesimx-0.1.1}/.pre-commit-config.yaml +0 -0
  56. {pymiesimx-0.1.0 → pymiesimx-0.1.1}/Dockerfile +0 -0
  57. {pymiesimx-0.1.0 → pymiesimx-0.1.1}/LICENSE +0 -0
  58. {pymiesimx-0.1.0 → pymiesimx-0.1.1}/PyMieSimX/gui/assets/card-state.js +0 -0
  59. {pymiesimx-0.1.0 → pymiesimx-0.1.1}/PyMieSimX/gui/assets/default-settings.json +0 -0
  60. {pymiesimx-0.1.0 → pymiesimx-0.1.1}/PyMieSimX/gui/assets/logo.png +0 -0
  61. {pymiesimx-0.1.0 → pymiesimx-0.1.1}/PyMieSimX/gui/assets/pymiesim-favicon.svg +0 -0
  62. {pymiesimx-0.1.0 → pymiesimx-0.1.1}/PyMieSimX/gui/assets/pymiesim-logo-dark.svg +0 -0
  63. {pymiesimx-0.1.0 → pymiesimx-0.1.1}/PyMieSimX/gui/assets/pymiesim-logo.svg +0 -0
  64. {pymiesimx-0.1.0 → pymiesimx-0.1.1}/PyMieSimX/gui/components/__init__.py +0 -0
  65. {pymiesimx-0.1.0 → pymiesimx-0.1.1}/PyMieSimX/gui/components/cards.py +0 -0
  66. {pymiesimx-0.1.0 → pymiesimx-0.1.1}/PyMieSimX/gui/defaults.py +0 -0
  67. {pymiesimx-0.1.0 → pymiesimx-0.1.1}/PyMieSimX/gui/pages/citation.py +0 -0
  68. {pymiesimx-0.1.0 → pymiesimx-0.1.1}/PyMieSimX/gui/pages/experiment/__init__.py +0 -0
  69. {pymiesimx-0.1.0 → pymiesimx-0.1.1}/PyMieSimX/gui/pages/experiment/sections/__init__.py +0 -0
  70. {pymiesimx-0.1.0 → pymiesimx-0.1.1}/PyMieSimX/gui/pages/settings.py +0 -0
  71. {pymiesimx-0.1.0 → pymiesimx-0.1.1}/PyMieSimX/gui/pages/single/__init__.py +0 -0
  72. {pymiesimx-0.1.0 → pymiesimx-0.1.1}/PyMieSimX/gui/pages/single/sections/__init__.py +0 -0
  73. {pymiesimx-0.1.0 → pymiesimx-0.1.1}/PyMieSimX.egg-info/dependency_links.txt +0 -0
  74. {pymiesimx-0.1.0 → pymiesimx-0.1.1}/PyMieSimX.egg-info/entry_points.txt +0 -0
  75. {pymiesimx-0.1.0 → pymiesimx-0.1.1}/PyMieSimX.egg-info/top_level.txt +0 -0
  76. {pymiesimx-0.1.0 → pymiesimx-0.1.1}/makefile +0 -0
  77. {pymiesimx-0.1.0 → pymiesimx-0.1.1}/pytest.ini +0 -0
  78. {pymiesimx-0.1.0 → pymiesimx-0.1.1}/setup.cfg +0 -0
  79. {pymiesimx-0.1.0 → pymiesimx-0.1.1}/tests/gui/cli.py +0 -0
@@ -14,7 +14,7 @@ jobs:
14
14
  runs-on: ubuntu-latest
15
15
  strategy:
16
16
  matrix:
17
- python-version: ["3.11", "3.13"]
17
+ python-version: ["3.10", "3.11", "3.13"]
18
18
  steps:
19
19
  - uses: actions/checkout@v4
20
20
  - uses: actions/setup-python@v5
@@ -0,0 +1,133 @@
1
+ Metadata-Version: 2.4
2
+ Name: PyMieSimX
3
+ Version: 0.1.1
4
+ Summary: Graphical Dash interface for PyMieSim.
5
+ Author-email: Martin Poinsinet de Sivry-Houle <martin.poinsinet.de.sivry@gmail.com>
6
+ License-Expression: MIT
7
+ Project-URL: Repository, https://github.com/MartinPdeS/PyMieSimX
8
+ Requires-Python: >=3.10
9
+ Description-Content-Type: text/x-rst
10
+ License-File: LICENSE
11
+ Requires-Dist: PyMieSim<6,>=5.1.2
12
+ Requires-Dist: dash<5,>=4.1.0
13
+ Requires-Dist: gunicorn<24,>=23.0.0
14
+ Requires-Dist: numpy<3,>=2.0
15
+ Requires-Dist: pandas<3,>=2.2
16
+ Requires-Dist: plotly<7,>=5.24
17
+ Requires-Dist: psycopg[binary]<4,>=3.2.9
18
+ Provides-Extra: testing
19
+ Requires-Dist: pytest>=8.0; extra == "testing"
20
+ Requires-Dist: pytest-cov>=5.0; extra == "testing"
21
+ Requires-Dist: coverage>=7.0; extra == "testing"
22
+ Provides-Extra: dev
23
+ Requires-Dist: build>=1.2; extra == "dev"
24
+ Requires-Dist: mypy==1.15.0; extra == "dev"
25
+ Requires-Dist: ruff==0.11.2; extra == "dev"
26
+ Dynamic: license-file
27
+
28
+ PyMieSimX
29
+ =========
30
+
31
+ PyMieSimX is the standalone graphical interface for `PyMieSim
32
+ <https://github.com/MartinPdeS/PyMieSim>`_. It provides a Dash application for
33
+ configuring optical setups, running parameter sweeps, exploring individual
34
+ particles, and exporting results.
35
+
36
+ Source-model note
37
+ -----------------
38
+
39
+ The Gaussian source exposed by PyMieSimX is not a generalized Lorenz--Mie
40
+ theory (GLMT) implementation. It is a convenience object for defining a
41
+ Gaussian illumination through its numerical aperture and optical power in
42
+ watts. The ``Gaussian`` and ``GaussianSet`` source options should therefore be
43
+ interpreted as a practical source parameterization, not as a separate GLMT
44
+ solver.
45
+
46
+ Installation
47
+ ------------
48
+
49
+ Install the GUI and its PyMieSim dependency with::
50
+
51
+ pip install PyMieSimX
52
+
53
+ Launch the dashboard with::
54
+
55
+ pymiesimx
56
+
57
+ Use ``pymiesimx --help`` to see the available host, port, browser, and debug
58
+ options.
59
+
60
+ Development
61
+ -----------
62
+
63
+ Install an editable checkout with::
64
+
65
+ pip install -e .
66
+
67
+ The GUI source lives in ``PyMieSimX/gui``. Scientific calculations are provided
68
+ by the installed ``PyMieSim`` package rather than duplicated here.
69
+
70
+ Testing and automation
71
+ ----------------------
72
+
73
+ Run the GUI test suite with::
74
+
75
+ pip install -e ".[testing]"
76
+ python -m pytest
77
+
78
+ GitHub Actions includes quality checks, GUI tests, PyPI publishing, Conda
79
+ recipe publishing, and coverage deployment. The Conda recipe is in
80
+ ``meta.yaml`` and the container entry point is defined in ``Dockerfile``.
81
+
82
+ Python API
83
+ ----------
84
+
85
+ The computational API can be used without constructing the Dash application::
86
+
87
+ from PyMieSimX import run_experiment
88
+
89
+ result = run_experiment(
90
+ source_type="GaussianSet",
91
+ source_values={
92
+ "wavelength": "650",
93
+ "polarization": "0",
94
+ "optical_power": "1e-3",
95
+ "numerical_aperture": "0.2",
96
+ },
97
+ scatterer_type="SphereSet",
98
+ scatterer_values={"diameter": "500", "material": "1.4", "medium": "1.0"},
99
+ detector_type="None",
100
+ detector_values={},
101
+ measure="Qsca",
102
+ )
103
+
104
+ ``PyMieSimX.create_dash_app`` and ``PyMieSimX.OpticalSetupGUI`` remain
105
+ available for applications that need the graphical interface.
106
+
107
+ Background calculations and limits
108
+ -----------------------------------
109
+
110
+ Parameter sweeps are submitted to a bounded background worker and reported to
111
+ the dashboard through a polling status indicator. Results are guarded by both
112
+ an in-memory dataframe limit and a serialized Dash-payload limit. Large-result
113
+ warnings are logged before execution; oversized results are rejected with an
114
+ actionable message asking you to reduce the sweep.
115
+
116
+ Run the command-line launcher with ``--debug`` to enable detailed logs. Log
117
+ messages use the format ``timestamp | level | logger | message`` and include
118
+ experiment job identifiers, input configuration, queue transitions, dataframe
119
+ memory usage, and serialized result sizes.
120
+
121
+ Server-side usage metrics
122
+ -------------------------
123
+
124
+ The dashboard can share a PostgreSQL metrics database with RosettaX. Configure
125
+ the Render service with::
126
+
127
+ PYMIESIMX_USAGE_METRICS_BACKEND=postgres
128
+ DATABASE_URL=<the shared PostgreSQL URL>
129
+
130
+ PyMieSimX writes namespaced counters to the shared ``metrics_counters`` table:
131
+ ``pymiesimx_home_page_visit_count``, ``pymiesimx_experiment_run_count``, and
132
+ ``pymiesimx_single_run_count``. If PostgreSQL is unavailable, local development
133
+ falls back to a JSON file under the platform's application-data directory.
@@ -0,0 +1,58 @@
1
+ """Python API and optional Dash graphical interface for PyMieSim."""
2
+
3
+ from importlib.metadata import PackageNotFoundError, version
4
+
5
+ from PyMieSimX.api import (
6
+ MAX_SWEEP_COMBINATIONS,
7
+ MAX_RESULT_FRAME_BYTES,
8
+ MAX_RESULT_PAYLOAD_BYTES,
9
+ ExperimentValidationError,
10
+ ResultSizeEstimate,
11
+ ResultSizeLimitError,
12
+ ValidationIssue,
13
+ available_measures,
14
+ build_figure,
15
+ build_single_figure,
16
+ estimate_sweep_size,
17
+ export_result_to_csv,
18
+ export_single_result_to_csv,
19
+ estimate_result_size,
20
+ run_experiment,
21
+ validate_experiment_inputs,
22
+ )
23
+
24
+ try:
25
+ __version__ = version("PyMieSimX")
26
+ except PackageNotFoundError:
27
+ __version__ = "0+unknown"
28
+
29
+ __all__ = [
30
+ "MAX_SWEEP_COMBINATIONS",
31
+ "MAX_RESULT_FRAME_BYTES",
32
+ "MAX_RESULT_PAYLOAD_BYTES",
33
+ "ExperimentValidationError",
34
+ "ResultSizeEstimate",
35
+ "ResultSizeLimitError",
36
+ "ValidationIssue",
37
+ "available_measures",
38
+ "build_figure",
39
+ "build_single_figure",
40
+ "estimate_sweep_size",
41
+ "export_result_to_csv",
42
+ "export_single_result_to_csv",
43
+ "estimate_result_size",
44
+ "run_experiment",
45
+ "validate_experiment_inputs",
46
+ "__version__",
47
+ "OpticalSetupGUI",
48
+ "create_dash_app",
49
+ ]
50
+
51
+
52
+ def __getattr__(name: str):
53
+ """Load the optional Dash interface only when it is explicitly requested."""
54
+ if name in {"OpticalSetupGUI", "create_dash_app"}:
55
+ from PyMieSimX.gui.interface import OpticalSetupGUI, create_dash_app
56
+
57
+ return {"OpticalSetupGUI": OpticalSetupGUI, "create_dash_app": create_dash_app}[name]
58
+ raise AttributeError(f"module {__name__!r} has no attribute {name!r}")
@@ -2,8 +2,7 @@
2
2
 
3
3
  import argparse
4
4
  import logging
5
-
6
- from PyMieSimX.gui.interface import OpticalSetupGUI
5
+ import sys
7
6
 
8
7
 
9
8
  def _build_argument_parser() -> argparse.ArgumentParser:
@@ -16,13 +15,25 @@ def _build_argument_parser() -> argparse.ArgumentParser:
16
15
  return parser
17
16
 
18
17
 
19
- def main(argv: list[str] | None = None) -> None:
20
- """Launch the PyMieSimX dashboard from a console entry point."""
21
- args = _build_argument_parser().parse_args(argv)
18
+ def configure_logging(*, debug: bool, log_level: str = "INFO") -> int:
19
+ """Configure application logging using RosettaX's console format."""
20
+ resolved_level = logging.DEBUG if debug else getattr(logging, str(log_level).upper(), logging.INFO)
22
21
  logging.basicConfig(
23
- level=logging.DEBUG if args.debug else logging.INFO,
22
+ level=resolved_level,
24
23
  format="%(asctime)s | %(levelname)s | %(name)s | %(message)s",
24
+ stream=sys.stdout,
25
+ force=True,
25
26
  )
27
+ logging.getLogger("PyMieSimX").setLevel(resolved_level)
28
+ return resolved_level
29
+
30
+
31
+ def main(argv: list[str] | None = None) -> None:
32
+ """Launch the PyMieSimX dashboard from a console entry point."""
33
+ args = _build_argument_parser().parse_args(argv)
34
+ configure_logging(debug=args.debug)
35
+ from PyMieSimX.gui.interface import OpticalSetupGUI
36
+
26
37
  OpticalSetupGUI().run(
27
38
  host=args.host,
28
39
  port=args.port,
@@ -0,0 +1,39 @@
1
+ """Stable, GUI-independent Python API for PyMieSimX computations."""
2
+
3
+ from PyMieSimX.gui.services import (
4
+ MAX_SWEEP_COMBINATIONS,
5
+ MAX_RESULT_FRAME_BYTES,
6
+ MAX_RESULT_PAYLOAD_BYTES,
7
+ ExperimentValidationError,
8
+ ResultSizeEstimate,
9
+ ResultSizeLimitError,
10
+ ValidationIssue,
11
+ available_measures,
12
+ build_figure,
13
+ build_single_figure,
14
+ estimate_sweep_size,
15
+ export_result_to_csv,
16
+ export_single_result_to_csv,
17
+ estimate_result_size,
18
+ run_experiment,
19
+ validate_experiment_inputs,
20
+ )
21
+
22
+ __all__ = [
23
+ "MAX_SWEEP_COMBINATIONS",
24
+ "MAX_RESULT_FRAME_BYTES",
25
+ "MAX_RESULT_PAYLOAD_BYTES",
26
+ "ExperimentValidationError",
27
+ "ResultSizeEstimate",
28
+ "ResultSizeLimitError",
29
+ "ValidationIssue",
30
+ "available_measures",
31
+ "build_figure",
32
+ "build_single_figure",
33
+ "estimate_sweep_size",
34
+ "export_result_to_csv",
35
+ "export_single_result_to_csv",
36
+ "estimate_result_size",
37
+ "run_experiment",
38
+ "validate_experiment_inputs",
39
+ ]
@@ -0,0 +1 @@
1
+ """WSGI compatibility entry points for the PyMieSimX dashboard."""
@@ -0,0 +1,9 @@
1
+ """WSGI entry point for deployments using Gunicorn or another WSGI server."""
2
+
3
+ from PyMieSimX.__main__ import configure_logging
4
+
5
+ configure_logging(debug=False)
6
+
7
+ from PyMieSimX.gui.interface import server # noqa: E402
8
+
9
+ __all__ = ["server"]
@@ -0,0 +1,12 @@
1
+ """Dash dashboard components for PyMieSimX."""
2
+
3
+ __all__ = ["OpticalSetupGUI", "create_dash_app"]
4
+
5
+
6
+ def __getattr__(name: str):
7
+ """Load the Dash application lazily for computational-only imports."""
8
+ if name in __all__:
9
+ from PyMieSimX.gui.interface import OpticalSetupGUI, create_dash_app
10
+
11
+ return {"OpticalSetupGUI": OpticalSetupGUI, "create_dash_app": create_dash_app}[name]
12
+ raise AttributeError(f"module {__name__!r} has no attribute {name!r}")
@@ -208,7 +208,7 @@ body {
208
208
  .plot-options-grid {
209
209
  display: grid;
210
210
  grid-template-columns: repeat(8, minmax(104px, 1fr));
211
- gap: 10px 12px;
211
+ gap: 6px 8px;
212
212
  }
213
213
 
214
214
  .plot-option-field {
@@ -806,6 +806,43 @@ html .rosettax-workflow-step-card--purple { border-left: 4px solid #8d6bd8 !impo
806
806
  line-height: 1.5;
807
807
  }
808
808
 
809
+ .documentation-equation {
810
+ margin: 10px 0;
811
+ padding: 9px 12px;
812
+ border: 1px solid rgba(13, 110, 253, .16);
813
+ border-radius: 8px;
814
+ background: rgba(13, 110, 253, .05);
815
+ color: #204a74;
816
+ font-family: "IBM Plex Mono", ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace;
817
+ font-size: .84rem;
818
+ line-height: 1.45;
819
+ overflow-x: auto;
820
+ }
821
+
822
+ .documentation-table {
823
+ width: 100%;
824
+ border-collapse: collapse;
825
+ margin: 8px 0 4px;
826
+ font-size: .82rem;
827
+ }
828
+
829
+ .documentation-table th,
830
+ .documentation-table td {
831
+ border: 1px solid rgba(13, 110, 253, .16);
832
+ padding: 7px 8px;
833
+ text-align: left;
834
+ }
835
+
836
+ .documentation-table th {
837
+ background: rgba(13, 110, 253, .08);
838
+ color: #204a74;
839
+ font-weight: 700;
840
+ }
841
+
842
+ .documentation-table td {
843
+ color: #52697a;
844
+ }
845
+
809
846
  @media (max-width: 1000px) {
810
847
  .documentation-quickstart-body,
811
848
  .documentation-model-grid {
@@ -2429,6 +2466,80 @@ html .field-input {
2429
2466
  overflow-wrap: anywhere;
2430
2467
  }
2431
2468
 
2469
+ .material-field-controls {
2470
+ display: grid;
2471
+ grid-template-columns: minmax(0, 1fr);
2472
+ gap: 8px;
2473
+ align-items: stretch;
2474
+ }
2475
+
2476
+ .material-mode-toggle {
2477
+ border: 1px solid var(--line);
2478
+ background: var(--panel-strong);
2479
+ border-radius: 999px;
2480
+ padding: 4px 6px;
2481
+ display: inline-flex;
2482
+ align-items: center;
2483
+ gap: 8px;
2484
+ width: fit-content;
2485
+ cursor: pointer;
2486
+ margin-bottom: 8px;
2487
+ }
2488
+
2489
+ .material-mode-label {
2490
+ font-size: .84rem;
2491
+ font-weight: 700;
2492
+ line-height: 1;
2493
+ color: var(--muted);
2494
+ min-width: 56px;
2495
+ text-align: center;
2496
+ }
2497
+
2498
+ .material-mode-knob {
2499
+ width: 26px;
2500
+ height: 26px;
2501
+ border-radius: 999px;
2502
+ background: #d8e1e8;
2503
+ transform: translateX(-5px);
2504
+ transition: background .16s ease, transform .16s ease;
2505
+ }
2506
+
2507
+ .material-mode-toggle.is-index .material-mode-label-ri {
2508
+ color: #0d6efd;
2509
+ }
2510
+
2511
+ .material-mode-toggle.is-material .material-mode-label-material {
2512
+ color: #0d6efd;
2513
+ }
2514
+
2515
+ .material-mode-toggle.is-material .material-mode-knob {
2516
+ transform: translateX(5px);
2517
+ background: #0d6efd;
2518
+ }
2519
+
2520
+ .material-mode-toggle:focus-visible {
2521
+ outline: 0;
2522
+ border-color: rgba(44, 120, 180, 0.65);
2523
+ box-shadow: 0 0 0 2px rgba(44, 120, 180, 0.18);
2524
+ }
2525
+
2526
+ .material-name-select .Select-control {
2527
+ min-height: 48px;
2528
+ border-radius: 12px;
2529
+ }
2530
+
2531
+ .material-name-select .Select-placeholder,
2532
+ .material-name-select .Select-value-label,
2533
+ .material-name-select .Select-input > input {
2534
+ line-height: 46px;
2535
+ }
2536
+
2537
+ @media (max-width: 760px) {
2538
+ .material-mode-label {
2539
+ min-width: 48px;
2540
+ }
2541
+ }
2542
+
2432
2543
  .dashboard-dropdown .Select-control,
2433
2544
  .dashboard-dropdown .Select-menu-outer,
2434
2545
  .dashboard-dropdown .Select-menu,
@@ -0,0 +1,142 @@
1
+ {
2
+ "materials": [
3
+ "fused_silica",
4
+ "BK7",
5
+ "water",
6
+ "air",
7
+ "polystyren",
8
+ "soda_lime_glass",
9
+ "crown",
10
+ "flint",
11
+ "BAK1",
12
+ "BAF10",
13
+ "gold",
14
+ "silver"
15
+ ],
16
+ "sellmeier_reference": [
17
+ {
18
+ "material": "fused_silica",
19
+ "formula": "Formula 1",
20
+ "B1": "0.6961663",
21
+ "B2": "0.4079426",
22
+ "B3": "0.8974794",
23
+ "C1_um2": "0.004679148",
24
+ "C2_um2": "0.013512063",
25
+ "C3_um2": "97.9340025"
26
+ },
27
+ {
28
+ "material": "BK7",
29
+ "formula": "Formula 2",
30
+ "B1": "1.18318503",
31
+ "B2": "0.0871756426",
32
+ "B3": "1.03133701",
33
+ "C1_um2": "0.00722141956",
34
+ "C2_um2": "0.0268216805",
35
+ "C3_um2": "101.702362"
36
+ },
37
+ {
38
+ "material": "water",
39
+ "formula": "Formula 2",
40
+ "B1": "0.5672526103",
41
+ "B2": "0.1736581125",
42
+ "B3": "0.02121531502",
43
+ "C1_um2": "0.005085550461",
44
+ "C2_um2": "0.01814938654",
45
+ "C3_um2": "0.02617260739"
46
+ },
47
+ {
48
+ "material": "air",
49
+ "formula": "Formula 6",
50
+ "B1": "0.05792105",
51
+ "B2": "0.00167917",
52
+ "B3": "—",
53
+ "C1_um2": "238.0185",
54
+ "C2_um2": "57.362",
55
+ "C3_um2": "—",
56
+ "parameters": "B1=0.05792105, C1=238.0185; B2=0.00167917, C2=57.362"
57
+ },
58
+ {
59
+ "material": "polystyren",
60
+ "formula": "Formula 2",
61
+ "B1": "1.4435",
62
+ "B2": "—",
63
+ "B3": "—",
64
+ "C1_um2": "0.020216",
65
+ "C2_um2": "—",
66
+ "C3_um2": "—"
67
+ },
68
+ {
69
+ "material": "soda_lime_glass",
70
+ "formula": "Formula 5",
71
+ "B1": "—",
72
+ "B2": "—",
73
+ "B3": "—",
74
+ "C1_um2": "—",
75
+ "C2_um2": "—",
76
+ "C3_um2": "—",
77
+ "parameters": "A=1.5130; B1=-0.003169, C1=2; B2=0.003962, C2=-2"
78
+ },
79
+ {
80
+ "material": "crown",
81
+ "formula": "Formula 2",
82
+ "B1": "1.1273555",
83
+ "B2": "0.124412303",
84
+ "B3": "0.827100531",
85
+ "C1_um2": "0.00720341707",
86
+ "C2_um2": "0.0269835916",
87
+ "C3_um2": "100.384588"
88
+ },
89
+ {
90
+ "material": "flint",
91
+ "formula": "Formula 2",
92
+ "B1": "1.34533359",
93
+ "B2": "0.209073176",
94
+ "B3": "0.937357162",
95
+ "C1_um2": "0.00997743871",
96
+ "C2_um2": "0.0470450767",
97
+ "C3_um2": "111.886764"
98
+ },
99
+ {
100
+ "material": "BAK1",
101
+ "formula": "Formula 2",
102
+ "B1": "1.12365662",
103
+ "B2": "0.309276848",
104
+ "B3": "0.881511957",
105
+ "C1_um2": "0.00644742752",
106
+ "C2_um2": "0.0222284402",
107
+ "C3_um2": "107.297751"
108
+ },
109
+ {
110
+ "material": "BAF10",
111
+ "formula": "Formula 2",
112
+ "B1": "1.5851495",
113
+ "B2": "0.143559385",
114
+ "B3": "1.08521269",
115
+ "C1_um2": "0.00926681282",
116
+ "C2_um2": "0.0424489805",
117
+ "C3_um2": "105.613573"
118
+ },
119
+ {
120
+ "material": "gold",
121
+ "formula": "Tabulated",
122
+ "B1": "—",
123
+ "B2": "—",
124
+ "B3": "—",
125
+ "C1_um2": "—",
126
+ "C2_um2": "—",
127
+ "C3_um2": "—",
128
+ "parameters": "Tabulated n(lambda) and k(lambda) data"
129
+ },
130
+ {
131
+ "material": "silver",
132
+ "formula": "Tabulated",
133
+ "B1": "—",
134
+ "B2": "—",
135
+ "B3": "—",
136
+ "C1_um2": "—",
137
+ "C2_um2": "—",
138
+ "C3_um2": "—",
139
+ "parameters": "Tabulated n(lambda) and k(lambda) data"
140
+ }
141
+ ]
142
+ }
@@ -0,0 +1,112 @@
1
+ """Pure orchestration helpers used by the Dash callbacks.
2
+
3
+ Keeping state conversion and callback error handling here makes the expensive
4
+ parts of the dashboard testable without invoking Dash's callback machinery.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ from dataclasses import dataclass
10
+ from typing import Any
11
+
12
+ from PyMieSimX.gui.services import ExperimentValidationError, build_single_figure, run_experiment
13
+
14
+
15
+ @dataclass(frozen=True)
16
+ class CallbackExecution:
17
+ """Serializable outcome of a computation requested by a Dash callback."""
18
+
19
+ result: dict[str, Any] | None
20
+ run_count: int
21
+ message: str
22
+ level: str
23
+
24
+
25
+ def pair_ids_with_values(ids: list[dict[str, str]], values: list[str]) -> dict[str, str]:
26
+ """Convert dynamic Dash field IDs and values into a flat mapping."""
27
+ return {field_id["name"]: value for field_id, value in zip(ids, values)}
28
+
29
+
30
+ def merge_local_plot_values(settings: dict | None, values: tuple[Any, ...]) -> dict:
31
+ """Overlay values from a plot-local options card onto stored preferences."""
32
+ merged = dict(settings or {})
33
+ names = ("x_scale", "log_y", "font_size", "line_width", "show_legend", "show_grid")
34
+ for name, value in zip(names, values):
35
+ if value is not None:
36
+ merged[name] = value
37
+ return merged
38
+
39
+
40
+ def execute_experiment_callback(
41
+ *,
42
+ source_type: str,
43
+ source_values: list[str],
44
+ source_ids: list[dict[str, str]],
45
+ scatterer_type: str,
46
+ scatterer_values: list[str],
47
+ scatterer_ids: list[dict[str, str]],
48
+ detector_type: str,
49
+ detector_values: list[str],
50
+ detector_ids: list[dict[str, str]],
51
+ measure: str,
52
+ run_count: int | None,
53
+ ) -> CallbackExecution:
54
+ """Execute a parameter sweep and convert errors into UI-ready state."""
55
+ next_count = int(run_count or 0)
56
+ try:
57
+ result = run_experiment(
58
+ source_type=source_type,
59
+ source_values=pair_ids_with_values(source_ids, source_values),
60
+ scatterer_type=scatterer_type,
61
+ scatterer_values=pair_ids_with_values(scatterer_ids, scatterer_values),
62
+ detector_type=detector_type,
63
+ detector_values=pair_ids_with_values(detector_ids, detector_values),
64
+ measure=measure,
65
+ )
66
+ except ExperimentValidationError as error:
67
+ return CallbackExecution(None, next_count, " ".join(issue.message for issue in error.issues), "error")
68
+ except (TypeError, ValueError, KeyError) as error:
69
+ return CallbackExecution(None, next_count, str(error), "error")
70
+
71
+ return CallbackExecution(result, next_count + 1, f"Completed {result['row_count']:,} result rows.", "success")
72
+
73
+
74
+ def execute_single_callback(
75
+ *,
76
+ source_type: str,
77
+ source_values: list[str],
78
+ source_ids: list[dict[str, str]],
79
+ scatterer_type: str,
80
+ scatterer_values: list[str],
81
+ scatterer_ids: list[dict[str, str]],
82
+ representation: str,
83
+ projection: str,
84
+ sampling: int,
85
+ nearfield_mode: list[str] | str | None,
86
+ include_incident_field: list[str] | None,
87
+ run_count: int | None,
88
+ ) -> CallbackExecution:
89
+ """Execute a particle representation and convert errors into UI state."""
90
+ next_count = int(run_count or 0)
91
+ nearfield_values = nearfield_mode if isinstance(nearfield_mode, list) else ([nearfield_mode] if nearfield_mode else [])
92
+ try:
93
+ figure, summary = build_single_figure(
94
+ source_type=source_type,
95
+ source_values=pair_ids_with_values(source_ids, source_values),
96
+ scatterer_type=scatterer_type,
97
+ scatterer_values=pair_ids_with_values(scatterer_ids, scatterer_values),
98
+ representation=representation,
99
+ projection=projection,
100
+ sampling=sampling or 120,
101
+ nearfield_mode="absolute" if "absolute" in nearfield_values else "real",
102
+ include_incident_field="include" in (include_incident_field or []),
103
+ )
104
+ except (TypeError, ValueError, KeyError) as error:
105
+ return CallbackExecution(None, next_count, str(error), "error")
106
+
107
+ return CallbackExecution(
108
+ {"figure": figure.to_plotly_json(), "summary": summary},
109
+ next_count + 1,
110
+ "Representation calculated.",
111
+ "success",
112
+ )