PyMieSimX 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- PyMieSimX/__init__.py +5 -0
- PyMieSimX/__main__.py +35 -0
- PyMieSimX/gui/__init__.py +5 -0
- PyMieSimX/gui/assets/card-state.js +41 -0
- PyMieSimX/gui/assets/dashboard.css +3352 -0
- PyMieSimX/gui/assets/default-settings.json +32 -0
- PyMieSimX/gui/assets/logo.png +0 -0
- PyMieSimX/gui/assets/pymiesim-favicon.svg +6 -0
- PyMieSimX/gui/assets/pymiesim-logo-dark.svg +15 -0
- PyMieSimX/gui/assets/pymiesim-logo.svg +39 -0
- PyMieSimX/gui/components/__init__.py +5 -0
- PyMieSimX/gui/components/cards.py +64 -0
- PyMieSimX/gui/defaults.py +18 -0
- PyMieSimX/gui/interface.py +688 -0
- PyMieSimX/gui/layout.py +329 -0
- PyMieSimX/gui/pages/__init__.py +9 -0
- PyMieSimX/gui/pages/citation.py +66 -0
- PyMieSimX/gui/pages/documentation.py +132 -0
- PyMieSimX/gui/pages/experiment/__init__.py +5 -0
- PyMieSimX/gui/pages/experiment/page.py +49 -0
- PyMieSimX/gui/pages/experiment/sections/__init__.py +5 -0
- PyMieSimX/gui/pages/experiment/sections/configuration.py +42 -0
- PyMieSimX/gui/pages/home.py +152 -0
- PyMieSimX/gui/pages/install_local.py +110 -0
- PyMieSimX/gui/pages/settings.py +87 -0
- PyMieSimX/gui/pages/single/__init__.py +5 -0
- PyMieSimX/gui/pages/single/page.py +28 -0
- PyMieSimX/gui/pages/single/sections/__init__.py +6 -0
- PyMieSimX/gui/pages/single/sections/representations.py +38 -0
- PyMieSimX/gui/pages/single/sections/setup.py +27 -0
- PyMieSimX/gui/parsing.py +149 -0
- PyMieSimX/gui/schemas.py +118 -0
- PyMieSimX/gui/services.py +735 -0
- pymiesimx-0.1.0.dist-info/METADATA +90 -0
- pymiesimx-0.1.0.dist-info/RECORD +39 -0
- pymiesimx-0.1.0.dist-info/WHEEL +5 -0
- pymiesimx-0.1.0.dist-info/entry_points.txt +2 -0
- pymiesimx-0.1.0.dist-info/licenses/LICENSE +21 -0
- pymiesimx-0.1.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
"""Home page composition."""
|
|
2
|
+
|
|
3
|
+
from importlib.metadata import PackageNotFoundError, version
|
|
4
|
+
|
|
5
|
+
from dash import html
|
|
6
|
+
|
|
7
|
+
from PyMieSimX.gui.components import Card
|
|
8
|
+
|
|
9
|
+
def build_home_page(metrics: dict[str, int] | None = None):
|
|
10
|
+
"""Build the landing page and its workflow cards."""
|
|
11
|
+
metrics = metrics or {}
|
|
12
|
+
return html.Div(
|
|
13
|
+
className="page-content-stack",
|
|
14
|
+
children=[
|
|
15
|
+
html.Div(
|
|
16
|
+
[
|
|
17
|
+
html.Span("Version:", className="version-widget-label"),
|
|
18
|
+
html.Span(_package_version(), className="version-widget-value"),
|
|
19
|
+
],
|
|
20
|
+
className="version-widget",
|
|
21
|
+
),
|
|
22
|
+
html.Section(
|
|
23
|
+
className="page-hero home-page-hero",
|
|
24
|
+
children=[
|
|
25
|
+
html.H1("PyMieSim"),
|
|
26
|
+
html.P(
|
|
27
|
+
"An open-source library for fast and flexible far-field Mie scattering simulations.",
|
|
28
|
+
className="hero-text",
|
|
29
|
+
),
|
|
30
|
+
],
|
|
31
|
+
),
|
|
32
|
+
_citation_card(),
|
|
33
|
+
html.Div(
|
|
34
|
+
className="home-capability-grid",
|
|
35
|
+
children=[
|
|
36
|
+
_capability_card(
|
|
37
|
+
"Particle Explorer",
|
|
38
|
+
"Inspect one optical setup through angular scattering, polarization, phase functions, and field representations.",
|
|
39
|
+
["Configure source", "Configure scatterer", "Render representations"],
|
|
40
|
+
"Open Particle Explorer",
|
|
41
|
+
"/single",
|
|
42
|
+
),
|
|
43
|
+
_capability_card(
|
|
44
|
+
"Parameter Sweep",
|
|
45
|
+
"Run source, scatterer, and detector sets across parameter sweeps and export structured results for analysis.",
|
|
46
|
+
["Configure source", "Configure scatterer and detector", "Run and export results"],
|
|
47
|
+
"Open Parameter Sweep",
|
|
48
|
+
"/experiment",
|
|
49
|
+
),
|
|
50
|
+
_capability_card(
|
|
51
|
+
"Documentation",
|
|
52
|
+
"Learn the PyMieSimX vocabulary, field syntax, supported objects, and recommended workflows.",
|
|
53
|
+
["Learn the model vocabulary", "Explore field syntax", "Follow recommended workflows"],
|
|
54
|
+
"Read documentation",
|
|
55
|
+
"/documentation",
|
|
56
|
+
),
|
|
57
|
+
],
|
|
58
|
+
),
|
|
59
|
+
_metrics_card(metrics),
|
|
60
|
+
],
|
|
61
|
+
)
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def _package_version() -> str:
|
|
65
|
+
try:
|
|
66
|
+
return version("PyMieSim")
|
|
67
|
+
except PackageNotFoundError:
|
|
68
|
+
return "development"
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def _citation_card():
|
|
72
|
+
"""Build the support and citation panel shown on the landing page."""
|
|
73
|
+
return html.Section(
|
|
74
|
+
className=Card.classes(color="blue", extra="home-info-card"),
|
|
75
|
+
children=[
|
|
76
|
+
html.Div("Support, citation, and lab", className="home-section-header"),
|
|
77
|
+
html.Div(
|
|
78
|
+
className="home-info-body",
|
|
79
|
+
children=[
|
|
80
|
+
html.P(
|
|
81
|
+
"Support PyMieSim development, cite the underlying work in publications, and learn more about the project.",
|
|
82
|
+
className="home-info-copy",
|
|
83
|
+
),
|
|
84
|
+
html.Div(
|
|
85
|
+
className="home-button-row",
|
|
86
|
+
children=[
|
|
87
|
+
html.A("Support Developer", href="https://github.com/sponsors/MartinPdeS", target="_blank", rel="noopener noreferrer", className="home-button home-button-primary"),
|
|
88
|
+
html.A("Citing this work", href="/citation", className="home-button home-button-outline"),
|
|
89
|
+
html.A("PyMieSim documentation", href="https://martinpdes.github.io/PyMieSim/", target="_blank", rel="noopener noreferrer", className="home-button home-button-info"),
|
|
90
|
+
html.A("GitHub repository", href="https://github.com/MartinPdeS/PyMieSim", target="_blank", rel="noopener noreferrer", className="home-button home-button-muted"),
|
|
91
|
+
html.A("Install locally (Releases)", href="/documentation/install-local", className="home-button home-button-muted"),
|
|
92
|
+
],
|
|
93
|
+
),
|
|
94
|
+
],
|
|
95
|
+
),
|
|
96
|
+
],
|
|
97
|
+
)
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
def _capability_card(title: str, description: str, steps: list[str], button_text: str, href: str):
|
|
101
|
+
return html.Section(
|
|
102
|
+
className=Card.classes(color="blue", extra="home-capability-card"),
|
|
103
|
+
children=[
|
|
104
|
+
html.Div(title, className="home-section-header"),
|
|
105
|
+
html.Div(
|
|
106
|
+
className="home-capability-body",
|
|
107
|
+
children=[
|
|
108
|
+
html.P(description),
|
|
109
|
+
html.Div(
|
|
110
|
+
[html.Div([html.Span(str(index), className="home-step-number"), html.Span(step)]) for index, step in enumerate(steps, start=1)],
|
|
111
|
+
className="home-step-list",
|
|
112
|
+
),
|
|
113
|
+
html.A(button_text, href=href, className="home-workflow-button"),
|
|
114
|
+
],
|
|
115
|
+
),
|
|
116
|
+
],
|
|
117
|
+
)
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
def _metrics_card(metrics: dict[str, int]):
|
|
121
|
+
return html.Section(
|
|
122
|
+
className=Card.classes(color="blue", extra="home-info-card home-metrics-card"),
|
|
123
|
+
children=[
|
|
124
|
+
html.Div(className="card-header panel-header", children=[html.Div("PyMieSim usage metrics.", className="card-title")]),
|
|
125
|
+
html.Div(
|
|
126
|
+
className="card-body",
|
|
127
|
+
children=[
|
|
128
|
+
html.Div(
|
|
129
|
+
[
|
|
130
|
+
html.Div(str(metrics.get("home_page_visits", 0)), className="home-metric-value"),
|
|
131
|
+
html.Div("Home page visits", className="home-metric-label"),
|
|
132
|
+
],
|
|
133
|
+
className="home-metric-tile",
|
|
134
|
+
),
|
|
135
|
+
html.Div(
|
|
136
|
+
[
|
|
137
|
+
html.Div(str(metrics.get("experiment_runs", 0)), className="home-metric-value"),
|
|
138
|
+
html.Div("Parameter sweeps", className="home-metric-label"),
|
|
139
|
+
],
|
|
140
|
+
className="home-metric-tile",
|
|
141
|
+
),
|
|
142
|
+
html.Div(
|
|
143
|
+
[
|
|
144
|
+
html.Div(str(metrics.get("single_runs", 0)), className="home-metric-value"),
|
|
145
|
+
html.Div("Particle explorations", className="home-metric-label"),
|
|
146
|
+
],
|
|
147
|
+
className="home-metric-tile",
|
|
148
|
+
),
|
|
149
|
+
],
|
|
150
|
+
),
|
|
151
|
+
],
|
|
152
|
+
)
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
"""Local installation documentation page."""
|
|
2
|
+
|
|
3
|
+
from dash import html
|
|
4
|
+
|
|
5
|
+
from PyMieSimX.gui.components import Card
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
RELEASES_URL = "https://github.com/MartinPdeS/PyMieSim/releases"
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def _documentation_card(title: str, subtitle: str, body):
|
|
12
|
+
"""Build a RosettaX-style documentation card."""
|
|
13
|
+
return html.Section(
|
|
14
|
+
className=Card.classes(color="blue", extra="panel documentation-card"),
|
|
15
|
+
children=[
|
|
16
|
+
html.Div(
|
|
17
|
+
className="card-header panel-header",
|
|
18
|
+
children=[
|
|
19
|
+
html.Div(title, className="card-title"),
|
|
20
|
+
html.Div(subtitle, className="card-subtitle"),
|
|
21
|
+
],
|
|
22
|
+
),
|
|
23
|
+
html.Div(className="card-body", children=body),
|
|
24
|
+
],
|
|
25
|
+
)
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def _link_chip(label: str, href: str):
|
|
29
|
+
return html.A(label, href=href, className="documentation-link-chip")
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def build_install_local_page():
|
|
33
|
+
"""Build the local-installation page linked from the home card."""
|
|
34
|
+
return html.Div(
|
|
35
|
+
className="page-content-stack",
|
|
36
|
+
children=[
|
|
37
|
+
html.Section(
|
|
38
|
+
className="page-hero documentation-page-hero",
|
|
39
|
+
children=[
|
|
40
|
+
html.P("PyMieSim reference", className="eyebrow"),
|
|
41
|
+
html.H1("Install PyMieSim Locally"),
|
|
42
|
+
html.P(
|
|
43
|
+
"This page explains how to download and run PyMieSim from its release distribution without building from source.",
|
|
44
|
+
className="hero-text",
|
|
45
|
+
),
|
|
46
|
+
],
|
|
47
|
+
),
|
|
48
|
+
_documentation_card(
|
|
49
|
+
"Related pages",
|
|
50
|
+
"Use the documentation hub for workflow and model details after installation.",
|
|
51
|
+
[
|
|
52
|
+
html.Div(
|
|
53
|
+
[_link_chip("Back to documentation hub", "/documentation"), _link_chip("Quick start", "/documentation")],
|
|
54
|
+
className="documentation-link-chips",
|
|
55
|
+
)
|
|
56
|
+
],
|
|
57
|
+
),
|
|
58
|
+
html.Div(
|
|
59
|
+
className="install-local-grid",
|
|
60
|
+
children=[
|
|
61
|
+
_documentation_card(
|
|
62
|
+
"Download latest release bundle",
|
|
63
|
+
"Choose the archive for your operating system.",
|
|
64
|
+
[
|
|
65
|
+
html.Div(
|
|
66
|
+
[
|
|
67
|
+
html.A("Download Windows", href=RELEASES_URL, target="_blank", rel="noopener noreferrer", className="install-download-button"),
|
|
68
|
+
html.A("Download macOS", href=RELEASES_URL, target="_blank", rel="noopener noreferrer", className="install-download-button"),
|
|
69
|
+
html.A("Download Linux", href=RELEASES_URL, target="_blank", rel="noopener noreferrer", className="install-download-button"),
|
|
70
|
+
],
|
|
71
|
+
className="install-download-buttons",
|
|
72
|
+
),
|
|
73
|
+
html.Div(
|
|
74
|
+
[
|
|
75
|
+
"Need a different version? Use the full ",
|
|
76
|
+
html.A("Releases page", href=RELEASES_URL, target="_blank", rel="noopener noreferrer"),
|
|
77
|
+
".",
|
|
78
|
+
],
|
|
79
|
+
className="install-release-note",
|
|
80
|
+
),
|
|
81
|
+
],
|
|
82
|
+
),
|
|
83
|
+
_documentation_card(
|
|
84
|
+
"Install and run",
|
|
85
|
+
"Run PyMieSim after installing the package.",
|
|
86
|
+
[
|
|
87
|
+
html.Ol(
|
|
88
|
+
[
|
|
89
|
+
html.Li("Download the archive for your operating system."),
|
|
90
|
+
html.Li("Extract it to a folder where you have write permission."),
|
|
91
|
+
html.Li("Install PyMieSim in your Python environment."),
|
|
92
|
+
html.Li("Launch the PyMieSim GUI and open the local dashboard."),
|
|
93
|
+
],
|
|
94
|
+
className="install-steps",
|
|
95
|
+
)
|
|
96
|
+
],
|
|
97
|
+
),
|
|
98
|
+
],
|
|
99
|
+
),
|
|
100
|
+
_documentation_card(
|
|
101
|
+
"Validation and provenance",
|
|
102
|
+
"Recommended checks before using a local installation.",
|
|
103
|
+
[
|
|
104
|
+
html.Div("Confirm you downloaded the expected version from the release page."),
|
|
105
|
+
html.Div("Keep the installed version together with reports for traceability."),
|
|
106
|
+
html.Div("For source-based installation, follow the instructions in the main documentation."),
|
|
107
|
+
],
|
|
108
|
+
),
|
|
109
|
+
],
|
|
110
|
+
)
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
"""Settings page composition and independent plotting preferences."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from dash import dcc, html
|
|
6
|
+
|
|
7
|
+
from PyMieSimX.gui.components import Card
|
|
8
|
+
from PyMieSimX.gui.defaults import DEFAULT_APPLICATION_SETTINGS, DEFAULT_PARTICLE_PLOT_SETTINGS, DEFAULT_SWEEP_PLOT_SETTINGS
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def build_settings_page(theme: str = DEFAULT_APPLICATION_SETTINGS["theme"], plot_settings: dict | None = None):
|
|
12
|
+
"""Build the settings page with separate controls for each workspace."""
|
|
13
|
+
stored = plot_settings or {}
|
|
14
|
+
particle = _workspace_settings(stored, "particle_explorer", DEFAULT_PARTICLE_PLOT_SETTINGS)
|
|
15
|
+
sweep = _workspace_settings(stored, "parameter_sweep", DEFAULT_SWEEP_PLOT_SETTINGS)
|
|
16
|
+
return html.Div(
|
|
17
|
+
className="page-content-stack settings-page",
|
|
18
|
+
children=[
|
|
19
|
+
html.Section(
|
|
20
|
+
className="page-hero documentation-page-hero",
|
|
21
|
+
children=[
|
|
22
|
+
html.P("Application preferences", className="eyebrow"),
|
|
23
|
+
html.H1("Settings"),
|
|
24
|
+
html.P("Tune the dashboard appearance and configure each plotting workspace independently. Changes are saved automatically in this browser.", className="hero-text"),
|
|
25
|
+
],
|
|
26
|
+
),
|
|
27
|
+
html.Div(className="settings-grid", children=[_appearance_card(theme), _plot_card("particle", "Particle Explorer", "blue", particle), _plot_card("sweep", "Parameter Sweep", "yellow", sweep)]),
|
|
28
|
+
html.Section(
|
|
29
|
+
className=Card.classes(color="blue", extra="panel settings-note"),
|
|
30
|
+
children=[
|
|
31
|
+
html.Div(className="card-header panel-header", children=[html.H2("How settings are applied")]),
|
|
32
|
+
html.Div(className="card-body", children=[html.P("Particle Explorer and Parameter Sweep remember separate plotting preferences. Polar coordinates are applied to trace-based plots when the representation supports them; heatmaps remain Cartesian."), html.Div(id="settings-save-status", className="settings-save-status", children="All settings are saved automatically.")]),
|
|
33
|
+
],
|
|
34
|
+
),
|
|
35
|
+
],
|
|
36
|
+
)
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def _workspace_settings(stored: dict, key: str, defaults: dict) -> dict:
|
|
40
|
+
"""Read nested preferences while tolerating the previous flat format."""
|
|
41
|
+
values = stored.get(key, stored if key == "particle_explorer" else {})
|
|
42
|
+
return {**defaults, **(values or {})}
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def _appearance_card(theme: str):
|
|
46
|
+
return html.Section(
|
|
47
|
+
className=Card.classes(color="blue", extra="panel settings-card"),
|
|
48
|
+
children=[
|
|
49
|
+
html.Div(className="card-header panel-header", children=[html.H2("Appearance")]),
|
|
50
|
+
html.Div(className="card-body settings-card-body", children=[html.Label("Theme", htmlFor="settings-theme-mode", className="settings-label"), _dropdown("settings-theme-mode", [{"label": "Light", "value": "light"}, {"label": "Dark", "value": "dark"}], theme if theme in {"light", "dark"} else DEFAULT_APPLICATION_SETTINGS["theme"]), html.P("The theme changes the dashboard shell and its controls.", className="settings-help")]),
|
|
51
|
+
],
|
|
52
|
+
)
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def _plot_card(prefix: str, title: str, color: str, settings: dict):
|
|
56
|
+
return html.Section(
|
|
57
|
+
className=Card.classes(color=color, extra=f"panel settings-card settings-{prefix}-card"),
|
|
58
|
+
children=[
|
|
59
|
+
html.Div(className="card-header panel-header", children=[html.H2(title), html.Span("Independent plotting preferences", className="settings-card-subtitle")]),
|
|
60
|
+
html.Div(
|
|
61
|
+
className="card-body settings-card-body settings-form-grid",
|
|
62
|
+
children=[
|
|
63
|
+
_number_setting("Font size", f"settings-{prefix}-font-size", settings["font_size"], 8, 32, 1),
|
|
64
|
+
_number_setting("Line width", f"settings-{prefix}-line-width", settings["line_width"], 0.5, 8, 0.5),
|
|
65
|
+
_number_setting("Graph height (px)", f"settings-{prefix}-graph-height", settings["graph_height"], 300, 1400, 10),
|
|
66
|
+
_dropdown_field("Plot theme", f"settings-{prefix}-template", [{"label": "Match application theme", "value": "match-theme"}, {"label": "Light plot", "value": "plotly_white"}, {"label": "Dark plot", "value": "plotly_dark"}], settings["template"]),
|
|
67
|
+
_dropdown_field("Coordinates", f"settings-{prefix}-coordinates", [{"label": "Cartesian", "value": "cartesian"}, {"label": "Polar (when supported)", "value": "polar"}], settings["coordinate_system"]),
|
|
68
|
+
_dropdown_field("X-axis scale", f"settings-{prefix}-x-scale", [{"label": "Linear", "value": "linear"}, {"label": "Logarithmic", "value": "log"}], settings["x_scale"]),
|
|
69
|
+
_dropdown_field("Legend", f"settings-{prefix}-legend", [{"label": "Show", "value": True}, {"label": "Hide", "value": False}], bool(settings["show_legend"])),
|
|
70
|
+
_dropdown_field("Grid", f"settings-{prefix}-grid", [{"label": "Show", "value": True}, {"label": "Hide", "value": False}], bool(settings["show_grid"])),
|
|
71
|
+
_dropdown_field("Y-axis scale", f"settings-{prefix}-log-y", [{"label": "Linear", "value": False}, {"label": "Logarithmic", "value": True}], bool(settings["log_y"])),
|
|
72
|
+
],
|
|
73
|
+
),
|
|
74
|
+
],
|
|
75
|
+
)
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def _dropdown_field(label: str, component_id: str, options: list[dict], value):
|
|
79
|
+
return html.Div(className="settings-field", children=[html.Label(label, htmlFor=component_id, className="settings-label"), _dropdown(component_id, options, value)])
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def _dropdown(component_id: str, options: list[dict], value):
|
|
83
|
+
return dcc.Dropdown(id=component_id, options=options, value=value, clearable=False, searchable=False, optionHeight=38, maxHeight=200, persistence=True, persistence_type="session", className="dashboard-dropdown")
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def _number_setting(label: str, component_id: str, value: int | float, minimum: int | float, maximum: int | float, step: int | float):
|
|
87
|
+
return html.Div(className="settings-field", children=[html.Label(label, htmlFor=component_id, className="settings-label"), dcc.Input(id=component_id, type="number", value=value, min=minimum, max=maximum, step=step, placeholder=str(value), persistence=True, persistence_type="session", className="settings-number-input")])
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
"""Single-particle page composition."""
|
|
2
|
+
|
|
3
|
+
from dash import dcc, html
|
|
4
|
+
|
|
5
|
+
from PyMieSimX.gui.components import HeaderCard
|
|
6
|
+
from PyMieSimX.gui.layout import PLOT_CONFIG, _plot_options_card
|
|
7
|
+
|
|
8
|
+
from .sections import build_representation_section, build_scatterer_section, build_source_section
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def build_single_page(plot_settings: dict | None = None):
|
|
12
|
+
"""Build the single page from independent setup and plot sections."""
|
|
13
|
+
return html.Div(
|
|
14
|
+
className="tab-content-stack single-tab-content",
|
|
15
|
+
children=[
|
|
16
|
+
HeaderCard(
|
|
17
|
+
"Particle Explorer",
|
|
18
|
+
"Inspect angular scattering, polarization, and field patterns for one source–scatterer setup.",
|
|
19
|
+
[
|
|
20
|
+
("01", "Configure source", "Choose a source and set its optical parameters.", "yellow"),
|
|
21
|
+
("02", "Configure scatterer", "Define the particle geometry and refractive indices.", "blue"),
|
|
22
|
+
("03", "Choose representation", "Inspect amplitudes, polarization, phase functions, or far fields.", "orange"),
|
|
23
|
+
],
|
|
24
|
+
color="green",
|
|
25
|
+
).render(),
|
|
26
|
+
html.Section(className="single-workspace", children=[html.Section(className="control-column", children=[build_source_section(), build_scatterer_section()]), html.Section(className="result-column", children=[html.Section(className="panel graph-panel single-graph-panel", children=[dcc.Loading(id="single-graph-loading", type="circle", color="#4f8df7", custom_spinner=html.Div("Computing…", className="plot-computing-indicator"), delay_show=150, delay_hide=150, children=html.Div(className="plot-loading-target", children=[dcc.Graph(id="single-graph", config=PLOT_CONFIG), html.Div(id="single-computation-status", style={"display": "none"})]))]), build_representation_section(), html.Div(id="single-plot-options-container", children=[_plot_options_card("single", plot_settings or {})]), html.Div(className="export-actions", children=[html.Button("Export CSV", id="export-single-csv", n_clicks=0, className="run-button export-button")])])]),
|
|
27
|
+
],
|
|
28
|
+
)
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
"""Sections composing the single-particle page."""
|
|
2
|
+
|
|
3
|
+
from .representations import build_representation_section
|
|
4
|
+
from .setup import build_scatterer_section, build_source_section
|
|
5
|
+
|
|
6
|
+
__all__ = ["build_representation_section", "build_scatterer_section", "build_source_section"]
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
"""Representation controls section."""
|
|
2
|
+
|
|
3
|
+
from dash import dcc, html
|
|
4
|
+
|
|
5
|
+
from PyMieSimX.gui.components import Card
|
|
6
|
+
from PyMieSimX.gui.defaults import DEFAULT_WORKSPACE_SETTINGS
|
|
7
|
+
|
|
8
|
+
def build_representation_section():
|
|
9
|
+
return html.Section(
|
|
10
|
+
id="single-representation-card",
|
|
11
|
+
className=Card.classes(color="blue", extra="workflow-card representation-card"),
|
|
12
|
+
children=[
|
|
13
|
+
html.Div(className="representation-card-header", children=[html.Span("Representation Controls")]),
|
|
14
|
+
html.Div(
|
|
15
|
+
id="single-representation-body",
|
|
16
|
+
className="workflow-card-body representation-card-body",
|
|
17
|
+
children=[
|
|
18
|
+
html.Div(id="single-nearfield-availability-alert", className="nearfield-availability-alert", style={"display": "none"}),
|
|
19
|
+
html.Div(
|
|
20
|
+
className="representation-controls-row",
|
|
21
|
+
children=[
|
|
22
|
+
html.Div(className="field-block", children=[html.Label("Representation", htmlFor="single-representation"), dcc.Dropdown(id="single-representation", className="dashboard-dropdown", options=[{"label": "S1 / S2 amplitudes", "value": "s1s2"}, {"label": "Stokes I intensity", "value": "stokes"}, {"label": "Stokes Q", "value": "stokes_q"}, {"label": "Stokes U", "value": "stokes_u"}, {"label": "Stokes V", "value": "stokes_v"}, {"label": "Scattering phase function", "value": "spf"}, {"label": "Far-field intensity", "value": "farfields"}, {"label": "Near-field E", "value": "nearfields"}, {"label": "Near-field Ex", "value": "nearfields_ex"}, {"label": "Near-field Ey", "value": "nearfields_ey"}, {"label": "Near-field Ez", "value": "nearfields_ez"}], value=DEFAULT_WORKSPACE_SETTINGS["particle_explorer"]["representation"], clearable=False, searchable=False, optionHeight=38, maxHeight=400, persistence=True, persistence_type="session")]),
|
|
23
|
+
html.Div(className="field-block", children=[html.Label("Projection", htmlFor="single-projection"), dcc.Dropdown(id="single-projection", className="dashboard-dropdown", options=[{"label": "2D plot", "value": "2d"}, {"label": "Polar 1D", "value": "polar_1d"}], value=DEFAULT_WORKSPACE_SETTINGS["particle_explorer"]["projection"], clearable=False, searchable=False, optionHeight=38, maxHeight=400, persistence=True, persistence_type="session")]),
|
|
24
|
+
html.Div(className="field-block sampling-field", children=[html.Label("Angular sampling", htmlFor="single-sampling"), dcc.Input(id="single-sampling", type="text", inputMode="numeric", value=str(DEFAULT_WORKSPACE_SETTINGS["particle_explorer"]["sampling"]), placeholder=str(DEFAULT_WORKSPACE_SETTINGS["particle_explorer"]["sampling"]), className="field-input", persistence=True, persistence_type="session")]),
|
|
25
|
+
],
|
|
26
|
+
),
|
|
27
|
+
html.Div(
|
|
28
|
+
id="single-nearfield-controls-row",
|
|
29
|
+
className="nearfield-controls-row",
|
|
30
|
+
children=[
|
|
31
|
+
html.Div(id="single-nearfield-mode-field", className="field-block", children=[html.Label("Abs", htmlFor="single-nearfield-mode"), dcc.Checklist(id="single-nearfield-mode", options=[{"label": "", "value": "absolute"}], value=["absolute"], className="nearfield-switch", persistence=True, persistence_type="session")]),
|
|
32
|
+
html.Div(id="single-nearfield-incident-field", className="field-block", children=[html.Label("Incident field", htmlFor="single-include-incident-field"), dcc.Checklist(id="single-include-incident-field", options=[{"label": "", "value": "include"}], value=["include"], className="nearfield-switch", persistence=True, persistence_type="session")]),
|
|
33
|
+
],
|
|
34
|
+
),
|
|
35
|
+
],
|
|
36
|
+
),
|
|
37
|
+
],
|
|
38
|
+
)
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
"""Source and scatterer setup section."""
|
|
2
|
+
|
|
3
|
+
from dash import dcc, html
|
|
4
|
+
|
|
5
|
+
from PyMieSimX.gui.components import Card
|
|
6
|
+
from PyMieSimX.gui.defaults import DEFAULT_WORKSPACE_SETTINGS
|
|
7
|
+
from PyMieSimX.gui.schemas import SINGLE_SCATTERER_FIELDS, SINGLE_SOURCE_FIELDS
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def build_source_section():
|
|
11
|
+
return _setup_card("Source Set", "single-source-type", "single-source-fields", SINGLE_SOURCE_FIELDS, DEFAULT_WORKSPACE_SETTINGS["particle_explorer"]["source_type"], color="yellow", info="Configure the optical source and its wavelength, polarization, power, and aperture values.")
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def build_scatterer_section():
|
|
15
|
+
return _setup_card("Scatterer Set", "single-scatterer-type", "single-scatterer-fields", SINGLE_SCATTERER_FIELDS, DEFAULT_WORKSPACE_SETTINGS["particle_explorer"]["scatterer_type"], color="blue", info="Configure particle geometry, material, and surrounding medium values.")
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def _setup_card(title, selector_id, fields_id, choices, default, color="blue", info=""):
|
|
19
|
+
return html.Details(
|
|
20
|
+
id=f"{selector_id}-card",
|
|
21
|
+
className=Card.classes(color=color, extra="workflow-card"),
|
|
22
|
+
open=True,
|
|
23
|
+
children=[
|
|
24
|
+
html.Summary(children=[html.Span(title), html.Span("i", className="workflow-info-button", title=info, **{"aria-label": info})]),
|
|
25
|
+
html.Div(className="workflow-card-body", children=[dcc.Dropdown(id=selector_id, className="dashboard-dropdown", options=[{"label": key, "value": key} for key in choices], value=default, clearable=False, searchable=False, optionHeight=38, maxHeight=200, persistence="particle-explorer-defaults-v3", persistence_type="session"), html.Div(id=fields_id, className="panel-body")]),
|
|
26
|
+
],
|
|
27
|
+
)
|
PyMieSimX/gui/parsing.py
ADDED
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
"""Parsing helpers for dashboard form values."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import Any
|
|
6
|
+
|
|
7
|
+
import numpy as np
|
|
8
|
+
|
|
9
|
+
from PyMieSim.experiment.polarization_set import PolarizationSet
|
|
10
|
+
from PyMieSim.material import SellmeierMaterial, SellmeierMedium, TabulatedMaterial
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def parse_expression(raw_value: Any) -> Any:
|
|
14
|
+
"""Parse a scalar, CSV sequence, or ``start:end:count`` specification."""
|
|
15
|
+
if raw_value is None:
|
|
16
|
+
return None
|
|
17
|
+
|
|
18
|
+
if isinstance(raw_value, (int, float, complex, np.ndarray, list, tuple)):
|
|
19
|
+
return raw_value
|
|
20
|
+
|
|
21
|
+
text = str(raw_value).strip()
|
|
22
|
+
|
|
23
|
+
if text == "":
|
|
24
|
+
return None
|
|
25
|
+
|
|
26
|
+
if text.count(":") == 2:
|
|
27
|
+
start_text, stop_text, count_text = [part.strip() for part in text.split(":")]
|
|
28
|
+
count = int(float(count_text))
|
|
29
|
+
if count <= 0:
|
|
30
|
+
raise ValueError("Range count must be positive.")
|
|
31
|
+
return np.linspace(float(start_text), float(stop_text), count)
|
|
32
|
+
|
|
33
|
+
if "," in text:
|
|
34
|
+
tokens = [_parse_scalar_or_text(token.strip()) for token in text.split(",") if token.strip()]
|
|
35
|
+
|
|
36
|
+
if all(not isinstance(token, str) for token in tokens):
|
|
37
|
+
dtype = complex if any(isinstance(token, complex) and not isinstance(token, bool) for token in tokens) else float
|
|
38
|
+
return np.asarray(tokens, dtype=dtype)
|
|
39
|
+
|
|
40
|
+
return [str(token) for token in tokens]
|
|
41
|
+
|
|
42
|
+
return _parse_scalar_or_text(text)
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def parse_numeric_expression(raw_value: Any, *, integer: bool = False) -> Any:
|
|
46
|
+
"""Parse a numeric scalar or numeric sequence."""
|
|
47
|
+
parsed = parse_expression(raw_value)
|
|
48
|
+
|
|
49
|
+
if parsed is None:
|
|
50
|
+
return None
|
|
51
|
+
|
|
52
|
+
if isinstance(parsed, str):
|
|
53
|
+
raise ValueError(f"Expected numeric values, received '{parsed}'.")
|
|
54
|
+
|
|
55
|
+
if isinstance(parsed, np.ndarray):
|
|
56
|
+
return parsed.astype(int if integer else float)
|
|
57
|
+
|
|
58
|
+
if isinstance(parsed, list):
|
|
59
|
+
if any(isinstance(value, str) for value in parsed):
|
|
60
|
+
raise ValueError(f"Expected numeric values, received '{parsed}'.")
|
|
61
|
+
return np.asarray(parsed, dtype=int if integer else float)
|
|
62
|
+
|
|
63
|
+
return int(parsed) if integer else float(parsed)
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def parse_quantity_expression(raw_value: Any, unit: Any) -> Any:
|
|
67
|
+
"""Parse a numeric expression and attach a Pint unit."""
|
|
68
|
+
parsed = parse_numeric_expression(raw_value)
|
|
69
|
+
|
|
70
|
+
if parsed is None:
|
|
71
|
+
return None
|
|
72
|
+
|
|
73
|
+
return parsed * unit
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def parse_polarization(raw_value: Any, unit: Any) -> PolarizationSet:
|
|
77
|
+
"""Parse a polarization angle expression into a ``PolarizationSet``."""
|
|
78
|
+
angles = parse_quantity_expression(raw_value, unit)
|
|
79
|
+
return PolarizationSet(angles=angles)
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def parse_mode_numbers(raw_value: Any) -> Any:
|
|
83
|
+
"""Parse one or more coherent mode labels."""
|
|
84
|
+
parsed = parse_expression(raw_value)
|
|
85
|
+
|
|
86
|
+
if isinstance(parsed, list):
|
|
87
|
+
return parsed
|
|
88
|
+
|
|
89
|
+
return parsed
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
def parse_material_values(raw_value: Any, *, medium: bool = False) -> Any:
|
|
93
|
+
"""Parse numeric or named material or medium definitions."""
|
|
94
|
+
parsed = parse_expression(raw_value)
|
|
95
|
+
|
|
96
|
+
if parsed is None:
|
|
97
|
+
return None
|
|
98
|
+
|
|
99
|
+
if isinstance(parsed, np.ndarray):
|
|
100
|
+
return parsed
|
|
101
|
+
|
|
102
|
+
if isinstance(parsed, list):
|
|
103
|
+
return [_resolve_material_entry(value, medium=medium) for value in parsed]
|
|
104
|
+
|
|
105
|
+
return _resolve_material_entry(parsed, medium=medium)
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
def serialize_value(value: Any) -> Any:
|
|
109
|
+
"""Convert NumPy and object values into JSON-friendly primitives."""
|
|
110
|
+
if isinstance(value, np.generic):
|
|
111
|
+
return value.item()
|
|
112
|
+
|
|
113
|
+
if isinstance(value, complex):
|
|
114
|
+
return str(value)
|
|
115
|
+
|
|
116
|
+
if isinstance(value, (int, float, str, bool)) or value is None:
|
|
117
|
+
return value
|
|
118
|
+
|
|
119
|
+
return str(value)
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
def _parse_scalar_or_text(text: str) -> Any:
|
|
123
|
+
"""Parse one scalar token as float, complex, or raw text."""
|
|
124
|
+
if text == "":
|
|
125
|
+
return None
|
|
126
|
+
|
|
127
|
+
try:
|
|
128
|
+
if "j" in text.lower():
|
|
129
|
+
return complex(text.replace("i", "j").replace("I", "j"))
|
|
130
|
+
return float(text)
|
|
131
|
+
except ValueError:
|
|
132
|
+
return text
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
def _resolve_material_entry(value: Any, *, medium: bool) -> Any:
|
|
136
|
+
"""Resolve one material token into either a numeric value or material object."""
|
|
137
|
+
if not isinstance(value, str):
|
|
138
|
+
return value
|
|
139
|
+
|
|
140
|
+
constructors = (SellmeierMedium,) if medium else (SellmeierMaterial, TabulatedMaterial)
|
|
141
|
+
last_error = None
|
|
142
|
+
|
|
143
|
+
for constructor in constructors:
|
|
144
|
+
try:
|
|
145
|
+
return constructor(value)
|
|
146
|
+
except Exception as error: # pragma: no cover
|
|
147
|
+
last_error = error
|
|
148
|
+
|
|
149
|
+
raise ValueError(f"Unknown {'medium' if medium else 'material'} '{value}'.") from last_error
|