PyMieSimX 0.2.1__tar.gz → 0.2.2__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.
Potentially problematic release.
This version of PyMieSimX might be problematic. Click here for more details.
- {pymiesimx-0.2.1 → pymiesimx-0.2.2}/PKG-INFO +16 -1
- {pymiesimx-0.2.1 → pymiesimx-0.2.2}/PyMieSimX/gui/assets/dashboard.css +28 -0
- pymiesimx-0.2.2/PyMieSimX/gui/assets/simulation-sharing.js +36 -0
- {pymiesimx-0.2.1 → pymiesimx-0.2.2}/PyMieSimX/gui/callbacks.py +102 -25
- {pymiesimx-0.2.1 → pymiesimx-0.2.2}/PyMieSimX/gui/components/cards.py +8 -0
- {pymiesimx-0.2.1 → pymiesimx-0.2.2}/PyMieSimX/gui/interface.py +5 -2
- {pymiesimx-0.2.1 → pymiesimx-0.2.2}/PyMieSimX/gui/layout.py +1 -0
- {pymiesimx-0.2.1 → pymiesimx-0.2.2}/PyMieSimX/gui/pages/experiment/page.py +12 -3
- {pymiesimx-0.2.1 → pymiesimx-0.2.2}/PyMieSimX/gui/pages/home.py +1 -1
- {pymiesimx-0.2.1 → pymiesimx-0.2.2}/PyMieSimX/gui/pages/population.py +23 -13
- {pymiesimx-0.2.1 → pymiesimx-0.2.2}/PyMieSimX/gui/pages/single/page.py +13 -3
- pymiesimx-0.2.2/PyMieSimX/gui/seo.py +91 -0
- pymiesimx-0.2.2/PyMieSimX/gui/sharing.py +332 -0
- {pymiesimx-0.2.1 → pymiesimx-0.2.2}/PyMieSimX.egg-info/PKG-INFO +16 -1
- {pymiesimx-0.2.1 → pymiesimx-0.2.2}/PyMieSimX.egg-info/SOURCES.txt +5 -0
- {pymiesimx-0.2.1 → pymiesimx-0.2.2}/PyMieSimX.egg-info/scm_file_list.json +5 -0
- pymiesimx-0.2.2/PyMieSimX.egg-info/scm_version.json +8 -0
- {pymiesimx-0.2.1 → pymiesimx-0.2.2}/README.rst +15 -0
- {pymiesimx-0.2.1 → pymiesimx-0.2.2}/tests/browser/test_dashboard.py +286 -1
- pymiesimx-0.2.2/tests/gui/test_seo.py +67 -0
- pymiesimx-0.2.2/tests/gui/test_sharing.py +171 -0
- pymiesimx-0.2.1/PyMieSimX.egg-info/scm_version.json +0 -8
- {pymiesimx-0.2.1 → pymiesimx-0.2.2}/.coveragerc +0 -0
- {pymiesimx-0.2.1 → pymiesimx-0.2.2}/.flake8 +0 -0
- {pymiesimx-0.2.1 → pymiesimx-0.2.2}/.github/dependabot.yml +0 -0
- {pymiesimx-0.2.1 → pymiesimx-0.2.2}/.github/workflows/deploy_PyPi.yml +0 -0
- {pymiesimx-0.2.1 → pymiesimx-0.2.2}/.github/workflows/deploy_anaconda.yml +0 -0
- {pymiesimx-0.2.1 → pymiesimx-0.2.2}/.github/workflows/deploy_coverage.yml +0 -0
- {pymiesimx-0.2.1 → pymiesimx-0.2.2}/.github/workflows/quality.yml +0 -0
- {pymiesimx-0.2.1 → pymiesimx-0.2.2}/.github/workflows/tests.yml +0 -0
- {pymiesimx-0.2.1 → pymiesimx-0.2.2}/.gitignore +0 -0
- {pymiesimx-0.2.1 → pymiesimx-0.2.2}/.pre-commit-config.yaml +0 -0
- {pymiesimx-0.2.1 → pymiesimx-0.2.2}/AGENTS.md +0 -0
- {pymiesimx-0.2.1 → pymiesimx-0.2.2}/CONTRIBUTING.md +0 -0
- {pymiesimx-0.2.1 → pymiesimx-0.2.2}/Dockerfile +0 -0
- {pymiesimx-0.2.1 → pymiesimx-0.2.2}/LICENSE +0 -0
- {pymiesimx-0.2.1 → pymiesimx-0.2.2}/PyMieSimX/__init__.py +0 -0
- {pymiesimx-0.2.1 → pymiesimx-0.2.2}/PyMieSimX/__main__.py +0 -0
- {pymiesimx-0.2.1 → pymiesimx-0.2.2}/PyMieSimX/api.py +0 -0
- {pymiesimx-0.2.1 → pymiesimx-0.2.2}/PyMieSimX/application/__init__.py +0 -0
- {pymiesimx-0.2.1 → pymiesimx-0.2.2}/PyMieSimX/application/wsgi.py +0 -0
- {pymiesimx-0.2.1 → pymiesimx-0.2.2}/PyMieSimX/gui/__init__.py +0 -0
- {pymiesimx-0.2.1 → pymiesimx-0.2.2}/PyMieSimX/gui/admin.py +0 -0
- {pymiesimx-0.2.1 → pymiesimx-0.2.2}/PyMieSimX/gui/assets/card-state.js +0 -0
- {pymiesimx-0.2.1 → pymiesimx-0.2.2}/PyMieSimX/gui/assets/default-settings.json +0 -0
- {pymiesimx-0.2.1 → pymiesimx-0.2.2}/PyMieSimX/gui/assets/home-spf-radial.gif +0 -0
- {pymiesimx-0.2.1 → pymiesimx-0.2.2}/PyMieSimX/gui/assets/home-spf-radial.png +0 -0
- {pymiesimx-0.2.1 → pymiesimx-0.2.2}/PyMieSimX/gui/assets/logo.png +0 -0
- {pymiesimx-0.2.1 → pymiesimx-0.2.2}/PyMieSimX/gui/assets/materials-stock.json +0 -0
- {pymiesimx-0.2.1 → pymiesimx-0.2.2}/PyMieSimX/gui/assets/pymiesim-favicon.svg +0 -0
- {pymiesimx-0.2.1 → pymiesimx-0.2.2}/PyMieSimX/gui/assets/pymiesim-logo-dark.svg +0 -0
- {pymiesimx-0.2.1 → pymiesimx-0.2.2}/PyMieSimX/gui/assets/pymiesim-logo.svg +0 -0
- {pymiesimx-0.2.1 → pymiesimx-0.2.2}/PyMieSimX/gui/callback_helpers.py +0 -0
- {pymiesimx-0.2.1 → pymiesimx-0.2.2}/PyMieSimX/gui/components/__init__.py +0 -0
- {pymiesimx-0.2.1 → pymiesimx-0.2.2}/PyMieSimX/gui/computation.py +0 -0
- {pymiesimx-0.2.1 → pymiesimx-0.2.2}/PyMieSimX/gui/defaults.py +0 -0
- {pymiesimx-0.2.1 → pymiesimx-0.2.2}/PyMieSimX/gui/experiment_service.py +0 -0
- {pymiesimx-0.2.1 → pymiesimx-0.2.2}/PyMieSimX/gui/jobs.py +0 -0
- {pymiesimx-0.2.1 → pymiesimx-0.2.2}/PyMieSimX/gui/material_catalog.py +0 -0
- {pymiesimx-0.2.1 → pymiesimx-0.2.2}/PyMieSimX/gui/models.py +0 -0
- {pymiesimx-0.2.1 → pymiesimx-0.2.2}/PyMieSimX/gui/pages/__init__.py +0 -0
- {pymiesimx-0.2.1 → pymiesimx-0.2.2}/PyMieSimX/gui/pages/citation.py +0 -0
- {pymiesimx-0.2.1 → pymiesimx-0.2.2}/PyMieSimX/gui/pages/documentation.py +0 -0
- {pymiesimx-0.2.1 → pymiesimx-0.2.2}/PyMieSimX/gui/pages/experiment/__init__.py +0 -0
- {pymiesimx-0.2.1 → pymiesimx-0.2.2}/PyMieSimX/gui/pages/experiment/sections/__init__.py +0 -0
- {pymiesimx-0.2.1 → pymiesimx-0.2.2}/PyMieSimX/gui/pages/experiment/sections/configuration.py +0 -0
- {pymiesimx-0.2.1 → pymiesimx-0.2.2}/PyMieSimX/gui/pages/field_syntax.py +0 -0
- {pymiesimx-0.2.1 → pymiesimx-0.2.2}/PyMieSimX/gui/pages/install_local.py +0 -0
- {pymiesimx-0.2.1 → pymiesimx-0.2.2}/PyMieSimX/gui/pages/sellmeier.py +0 -0
- {pymiesimx-0.2.1 → pymiesimx-0.2.2}/PyMieSimX/gui/pages/settings.py +0 -0
- {pymiesimx-0.2.1 → pymiesimx-0.2.2}/PyMieSimX/gui/pages/single/__init__.py +0 -0
- {pymiesimx-0.2.1 → pymiesimx-0.2.2}/PyMieSimX/gui/pages/single/sections/__init__.py +0 -0
- {pymiesimx-0.2.1 → pymiesimx-0.2.2}/PyMieSimX/gui/pages/single/sections/representations.py +0 -0
- {pymiesimx-0.2.1 → pymiesimx-0.2.2}/PyMieSimX/gui/pages/single/sections/setup.py +0 -0
- {pymiesimx-0.2.1 → pymiesimx-0.2.2}/PyMieSimX/gui/parsing.py +0 -0
- {pymiesimx-0.2.1 → pymiesimx-0.2.2}/PyMieSimX/gui/plotting.py +0 -0
- {pymiesimx-0.2.1 → pymiesimx-0.2.2}/PyMieSimX/gui/population_service.py +0 -0
- {pymiesimx-0.2.1 → pymiesimx-0.2.2}/PyMieSimX/gui/schemas.py +0 -0
- {pymiesimx-0.2.1 → pymiesimx-0.2.2}/PyMieSimX/gui/services.py +0 -0
- {pymiesimx-0.2.1 → pymiesimx-0.2.2}/PyMieSimX/gui/single_service.py +0 -0
- {pymiesimx-0.2.1 → pymiesimx-0.2.2}/PyMieSimX/gui/usage_metrics.py +0 -0
- {pymiesimx-0.2.1 → pymiesimx-0.2.2}/PyMieSimX/gui/validation.py +0 -0
- {pymiesimx-0.2.1 → pymiesimx-0.2.2}/PyMieSimX.egg-info/dependency_links.txt +0 -0
- {pymiesimx-0.2.1 → pymiesimx-0.2.2}/PyMieSimX.egg-info/entry_points.txt +0 -0
- {pymiesimx-0.2.1 → pymiesimx-0.2.2}/PyMieSimX.egg-info/requires.txt +0 -0
- {pymiesimx-0.2.1 → pymiesimx-0.2.2}/PyMieSimX.egg-info/top_level.txt +0 -0
- {pymiesimx-0.2.1 → pymiesimx-0.2.2}/conda.recipe/meta.yaml +0 -0
- {pymiesimx-0.2.1 → pymiesimx-0.2.2}/makefile +0 -0
- {pymiesimx-0.2.1 → pymiesimx-0.2.2}/pyproject.toml +0 -0
- {pymiesimx-0.2.1 → pymiesimx-0.2.2}/pytest.ini +0 -0
- {pymiesimx-0.2.1 → pymiesimx-0.2.2}/setup.cfg +0 -0
- {pymiesimx-0.2.1 → pymiesimx-0.2.2}/tests/gui/cli.py +0 -0
- {pymiesimx-0.2.1 → pymiesimx-0.2.2}/tests/gui/test_admin.py +0 -0
- {pymiesimx-0.2.1 → pymiesimx-0.2.2}/tests/gui/test_api.py +0 -0
- {pymiesimx-0.2.1 → pymiesimx-0.2.2}/tests/gui/test_dash_callbacks.py +0 -0
- {pymiesimx-0.2.1 → pymiesimx-0.2.2}/tests/gui/test_home.py +0 -0
- {pymiesimx-0.2.1 → pymiesimx-0.2.2}/tests/gui/test_population.py +0 -0
- {pymiesimx-0.2.1 → pymiesimx-0.2.2}/tests/gui/test_services.py +0 -0
- {pymiesimx-0.2.1 → pymiesimx-0.2.2}/tests/gui/test_usage_metrics.py +0 -0
- {pymiesimx-0.2.1 → pymiesimx-0.2.2}/tools/generate_home_spf.py +0 -0
- {pymiesimx-0.2.1 → pymiesimx-0.2.2}/tools/next_release_version.py +0 -0
- {pymiesimx-0.2.1 → pymiesimx-0.2.2}/tools/release_tag.py +0 -0
- {pymiesimx-0.2.1 → pymiesimx-0.2.2}/tools/verify_conda_artifact.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: PyMieSimX
|
|
3
|
-
Version: 0.2.
|
|
3
|
+
Version: 0.2.2
|
|
4
4
|
Summary: Graphical Dash interface for PyMieSim.
|
|
5
5
|
Author-email: Martin Poinsinet de Sivry-Houle <martin.poinsinet.de.sivry@gmail.com>
|
|
6
6
|
License-Expression: MIT
|
|
@@ -50,6 +50,21 @@ optical setups, run parameter sweeps, explore individual particles, and download
|
|
|
50
50
|
your results as CSV files. The hosted service provides direct access to the
|
|
51
51
|
graphical interface; local installation is also available below.
|
|
52
52
|
|
|
53
|
+
Sharing simulations
|
|
54
|
+
-------------------
|
|
55
|
+
|
|
56
|
+
Use **Copy simulation link** in Single Scatterer, Parameter Scan, or Ensemble
|
|
57
|
+
Optics to share the current setup. Links preserve the input values, sweep
|
|
58
|
+
ranges, selected measures, and plot settings without requiring an account.
|
|
59
|
+
Opening a link restores the setup and automatically runs the simulation once
|
|
60
|
+
the inputs and plot settings are ready. Later edits can be calculated with
|
|
61
|
+
**Run**. If clipboard access is unavailable, use **Show simulation
|
|
62
|
+
link** and copy the displayed URL.
|
|
63
|
+
|
|
64
|
+
Links store the configuration rather than a result snapshot. Recalculation
|
|
65
|
+
uses the installed solver and optical material data, so results can change
|
|
66
|
+
after those dependencies are updated.
|
|
67
|
+
|
|
53
68
|
Ensemble Optics
|
|
54
69
|
---------------
|
|
55
70
|
|
|
@@ -4055,3 +4055,31 @@ html .single-workspace .representation-card {
|
|
|
4055
4055
|
.population-page .documentation-table td { overflow-wrap: anywhere; }
|
|
4056
4056
|
#population-status, #population-preview-error { margin-bottom: 12px; }
|
|
4057
4057
|
#population-results { overflow-x: auto; }
|
|
4058
|
+
.workspace-intro {
|
|
4059
|
+
margin: 0;
|
|
4060
|
+
color: var(--muted);
|
|
4061
|
+
font-size: .86rem;
|
|
4062
|
+
line-height: 1.6;
|
|
4063
|
+
}
|
|
4064
|
+
|
|
4065
|
+
.single-tab-content, .experiment-tab-content, .population-page { gap: 12px; }
|
|
4066
|
+
.page-hero.workspace-header {
|
|
4067
|
+
box-sizing: border-box;
|
|
4068
|
+
min-height: 76px;
|
|
4069
|
+
display: flex;
|
|
4070
|
+
flex-direction: column;
|
|
4071
|
+
justify-content: center;
|
|
4072
|
+
gap: 4px;
|
|
4073
|
+
}
|
|
4074
|
+
.workspace-header .hero-text { max-width: none; line-height: 1.6; min-height: 2lh; }
|
|
4075
|
+
@media (max-width: 700px) {
|
|
4076
|
+
.page-hero.workspace-header { min-height: 148px; }
|
|
4077
|
+
.workspace-header .eyebrow { min-height: 2lh; }
|
|
4078
|
+
.workspace-header .hero-text { min-height: 4lh; }
|
|
4079
|
+
}
|
|
4080
|
+
|
|
4081
|
+
.simulation-share-feedback { display: grid; gap: 8px; font-size: .86rem; color: var(--muted); }
|
|
4082
|
+
.simulation-share-actions { display: contents; }
|
|
4083
|
+
.simulation-share-feedback:empty { display: none; }
|
|
4084
|
+
.simulation-share-feedback .field-input { width: 100%; min-width: 0; }
|
|
4085
|
+
.simulation-link-notice { padding: 10px 14px; border: 1px solid var(--line); border-radius: 8px; color: var(--ink); }
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
window.dash_clientside = Object.assign({}, window.dash_clientside, {
|
|
2
|
+
simulation_sharing: {
|
|
3
|
+
auto_run: function(config, fieldIds, fieldValues, fieldClasses, settings) {
|
|
4
|
+
const unchanged = window.dash_clientside.no_update;
|
|
5
|
+
if (!config || config.started) return [unchanged, unchanged];
|
|
6
|
+
const values = Array.prototype.slice.call(arguments, 5);
|
|
7
|
+
const clicks = values.pop();
|
|
8
|
+
if (clicks) return [unchanged, Object.assign({}, config, {started: true})];
|
|
9
|
+
|
|
10
|
+
const actual = {};
|
|
11
|
+
config.control_names.forEach((name, index) => { actual[name] = values[index]; });
|
|
12
|
+
(fieldIds || []).forEach((id, index) => {
|
|
13
|
+
const ordered = {};
|
|
14
|
+
Object.keys(id).sort().forEach(key => { ordered[key] = id[key]; });
|
|
15
|
+
actual[JSON.stringify(ordered)] = fieldValues[index];
|
|
16
|
+
});
|
|
17
|
+
// Field classes depend on the existing validation callbacks; this
|
|
18
|
+
// also waits for dynamic fields and material-mode restoration.
|
|
19
|
+
if (!fieldClasses || fieldClasses.length < config.field_keys.length ||
|
|
20
|
+
!config.field_keys.every(key => Object.prototype.hasOwnProperty.call(actual, key))) {
|
|
21
|
+
return [unchanged, unchanged];
|
|
22
|
+
}
|
|
23
|
+
const same = (left, right) => JSON.stringify(left) === JSON.stringify(right);
|
|
24
|
+
if (!Object.entries(config.expected_controls).every(([key, value]) => same(actual[key], value))) {
|
|
25
|
+
return [unchanged, unchanged];
|
|
26
|
+
}
|
|
27
|
+
const plot = (settings || {})[config.workspace] || {};
|
|
28
|
+
if (!Object.entries(config.plot_settings).every(([key, value]) => same(plot[key], value))) {
|
|
29
|
+
return [unchanged, unchanged];
|
|
30
|
+
}
|
|
31
|
+
// Both outputs update together. Later edits and callback updates
|
|
32
|
+
// see the started flag and cannot launch a second automatic run.
|
|
33
|
+
return [1, Object.assign({}, config, {started: true})];
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
});
|
|
@@ -31,6 +31,7 @@ from PyMieSimX.gui.callback_helpers import (
|
|
|
31
31
|
from PyMieSimX.gui.jobs import experiment_jobs
|
|
32
32
|
from PyMieSimX.gui.validation import normalize_measures
|
|
33
33
|
from PyMieSimX.gui import usage_metrics
|
|
34
|
+
from PyMieSimX.gui.sharing import register_sharing_callbacks, read_simulation, shared_setup, shared_plot_settings, restore_controls
|
|
34
35
|
from PyMieSimX.gui.admin import build_admin_page, collect_admin_dashboard_data, dashboard_values, is_admin_access_granted
|
|
35
36
|
from PyMieSimX.gui.services import (
|
|
36
37
|
available_measures,
|
|
@@ -194,6 +195,21 @@ def register_callbacks(app: Dash, default_measure_options: list[str]) -> None:
|
|
|
194
195
|
def _route_pages(pathname: str | None, search: str | None, home_visits: int, experiment_runs: int, single_runs: int, theme_store: dict | None, plot_settings: dict | None):
|
|
195
196
|
"""Render only the selected route page inside the persistent shell."""
|
|
196
197
|
route = pathname or "/"
|
|
198
|
+
try:
|
|
199
|
+
setup = read_simulation(search, route)
|
|
200
|
+
share_message = "Shared setup loaded. This simulation runs automatically." if setup else None
|
|
201
|
+
except ValueError as error:
|
|
202
|
+
setup = None
|
|
203
|
+
share_message = str(error)
|
|
204
|
+
plot_settings = shared_plot_settings(plot_settings, setup)
|
|
205
|
+
|
|
206
|
+
def simulation_page(page):
|
|
207
|
+
from dash import html
|
|
208
|
+
restore_controls(page, setup)
|
|
209
|
+
if share_message:
|
|
210
|
+
page.children.insert(0, html.Div(share_message, role="status", className="simulation-link-notice"))
|
|
211
|
+
return build_page_with_footer(page)
|
|
212
|
+
|
|
197
213
|
active = {
|
|
198
214
|
"home": "sidebar-link",
|
|
199
215
|
"experiment": "sidebar-link",
|
|
@@ -233,13 +249,13 @@ def register_callbacks(app: Dash, default_measure_options: list[str]) -> None:
|
|
|
233
249
|
return build_page_with_footer(build_settings_page((theme_store or {}).get("theme", "light"), plot_settings or {})), *(active[key] for key in ("home", "experiment", "single", "documentation", "settings", "population")), home_visits
|
|
234
250
|
if route == "/single":
|
|
235
251
|
active["single"] += " active"
|
|
236
|
-
return
|
|
252
|
+
return simulation_page(build_single_page((plot_settings or {}).get("particle_explorer", {}), setup)), *(active[key] for key in ("home", "experiment", "single", "documentation", "settings", "population")), home_visits
|
|
237
253
|
if route == "/population":
|
|
238
254
|
active["population"] += " active"
|
|
239
|
-
return
|
|
255
|
+
return simulation_page(build_population_page()), *(active[key] for key in ("home", "experiment", "single", "documentation", "settings", "population")), home_visits
|
|
240
256
|
if route == "/experiment":
|
|
241
257
|
active["experiment"] += " active"
|
|
242
|
-
return
|
|
258
|
+
return simulation_page(build_experiment_page(default_measure_options, plot_settings or {}, setup)), *(active[key] for key in ("home", "experiment", "single", "documentation", "settings", "population")), home_visits
|
|
243
259
|
active["home"] += " active"
|
|
244
260
|
return build_page_with_footer(build_home_page(home_visits=home_visits)), *(active[key] for key in ("home", "experiment", "single", "documentation", "settings", "population")), home_visits
|
|
245
261
|
|
|
@@ -292,9 +308,12 @@ def register_callbacks(app: Dash, default_measure_options: list[str]) -> None:
|
|
|
292
308
|
Input("plot-experiment-legend", "value", allow_optional=True),
|
|
293
309
|
Input("plot-experiment-grid", "value", allow_optional=True),
|
|
294
310
|
State("plot-settings-store", "data"),
|
|
311
|
+
State("url", "search"),
|
|
312
|
+
State("url", "pathname"),
|
|
295
313
|
)
|
|
296
314
|
def _sync_plot_settings(*values):
|
|
297
|
-
|
|
315
|
+
search, path = values[-2:]
|
|
316
|
+
stored_settings = shared_plot_settings(values[-3], shared_setup(search, path))
|
|
298
317
|
particle_values = values[:9]
|
|
299
318
|
sweep_values = values[9:18]
|
|
300
319
|
single_local_values = values[18:24]
|
|
@@ -387,28 +406,43 @@ def register_callbacks(app: Dash, default_measure_options: list[str]) -> None:
|
|
|
387
406
|
|
|
388
407
|
return classes, errors
|
|
389
408
|
|
|
390
|
-
@app.callback(Output("source-fields", "children"), Input("source-type", "value"))
|
|
391
|
-
def _render_source_fields(source_type: str):
|
|
409
|
+
@app.callback(Output("source-fields", "children"), Input("source-type", "value"), State("url", "search"))
|
|
410
|
+
def _render_source_fields(source_type: str, search: str = ""):
|
|
392
411
|
LOGGER.debug("Rendering source fields for %s", source_type)
|
|
393
|
-
|
|
412
|
+
setup = shared_setup(search, "/experiment")
|
|
413
|
+
if setup and setup["controls"].get("source-type") != source_type:
|
|
414
|
+
setup = None
|
|
415
|
+
return restore_controls(render_fields("source", source_type), setup)
|
|
394
416
|
|
|
395
|
-
@app.callback(Output("scatterer-fields", "children"), Input("scatterer-type", "value"))
|
|
396
|
-
def _render_scatterer_fields(scatterer_type: str):
|
|
417
|
+
@app.callback(Output("scatterer-fields", "children"), Input("scatterer-type", "value"), State("url", "search"))
|
|
418
|
+
def _render_scatterer_fields(scatterer_type: str, search: str = ""):
|
|
397
419
|
LOGGER.debug("Rendering scatterer fields for %s", scatterer_type)
|
|
398
|
-
|
|
420
|
+
setup = shared_setup(search, "/experiment")
|
|
421
|
+
if setup and setup["controls"].get("scatterer-type") != scatterer_type:
|
|
422
|
+
setup = None
|
|
423
|
+
return restore_controls(render_fields("scatterer", scatterer_type), setup)
|
|
399
424
|
|
|
400
|
-
@app.callback(Output("detector-fields", "children"), Input("detector-type", "value"))
|
|
401
|
-
def _render_detector_fields(detector_type: str):
|
|
425
|
+
@app.callback(Output("detector-fields", "children"), Input("detector-type", "value"), State("url", "search"))
|
|
426
|
+
def _render_detector_fields(detector_type: str, search: str = ""):
|
|
402
427
|
LOGGER.debug("Rendering detector fields for %s", detector_type)
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
428
|
+
setup = shared_setup(search, "/experiment")
|
|
429
|
+
if setup and setup["controls"].get("detector-type") != detector_type:
|
|
430
|
+
setup = None
|
|
431
|
+
return restore_controls(render_fields("detector", detector_type), setup)
|
|
432
|
+
|
|
433
|
+
@app.callback(Output("single-source-fields", "children"), Input("single-source-type", "value"), State("url", "search"))
|
|
434
|
+
def _render_single_source_fields(source_type: str, search: str = ""):
|
|
435
|
+
setup = shared_setup(search, "/single")
|
|
436
|
+
if setup and setup["controls"].get("single-source-type") != source_type:
|
|
437
|
+
setup = None
|
|
438
|
+
return restore_controls(render_fields("single-source", source_type), setup)
|
|
439
|
+
|
|
440
|
+
@app.callback(Output("single-scatterer-fields", "children"), Input("single-scatterer-type", "value"), State("url", "search"))
|
|
441
|
+
def _render_single_scatterer_fields(scatterer_type: str, search: str = ""):
|
|
442
|
+
setup = shared_setup(search, "/single")
|
|
443
|
+
if setup and setup["controls"].get("single-scatterer-type") != scatterer_type:
|
|
444
|
+
setup = None
|
|
445
|
+
return restore_controls(render_fields("single-scatterer", scatterer_type), setup)
|
|
412
446
|
|
|
413
447
|
@app.callback(
|
|
414
448
|
Output({"kind": "material-toggle", "section": MATCH, "name": MATCH}, "className"),
|
|
@@ -478,6 +512,7 @@ def register_callbacks(app: Dash, default_measure_options: list[str]) -> None:
|
|
|
478
512
|
Input({"kind": "field", "section": "detector", "name": ALL}, "value"),
|
|
479
513
|
Input({"kind": "field", "section": "detector", "name": ALL}, "id"),
|
|
480
514
|
State("x-axis-select", "value"),
|
|
515
|
+
State("url", "search"),
|
|
481
516
|
)
|
|
482
517
|
def _update_x_axis_options(
|
|
483
518
|
source_type: str,
|
|
@@ -490,6 +525,7 @@ def register_callbacks(app: Dash, default_measure_options: list[str]) -> None:
|
|
|
490
525
|
detector_values: list[str],
|
|
491
526
|
detector_ids: list[dict[str, str]],
|
|
492
527
|
current_x_axis: str | None,
|
|
528
|
+
search: str = "",
|
|
493
529
|
):
|
|
494
530
|
LOGGER.debug(
|
|
495
531
|
"Updating x-axis options from form state source=%s scatterer=%s detector=%s current=%s",
|
|
@@ -509,6 +545,16 @@ def register_callbacks(app: Dash, default_measure_options: list[str]) -> None:
|
|
|
509
545
|
)
|
|
510
546
|
|
|
511
547
|
options = [{"label": field_name, "value": field_name} for field_name in variable_fields]
|
|
548
|
+
setup = shared_setup(search, "/experiment")
|
|
549
|
+
if setup:
|
|
550
|
+
desired_axis = setup["controls"].get("x-axis-select")
|
|
551
|
+
complete = (len(source_ids) == len(SOURCE_FIELDS[source_type])
|
|
552
|
+
and len(scatterer_ids) == len(SCATTERER_FIELDS[scatterer_type])
|
|
553
|
+
and len(detector_ids) == len(DETECTOR_FIELDS[detector_type]))
|
|
554
|
+
if current_x_axis is None or current_x_axis == desired_axis:
|
|
555
|
+
current_x_axis = desired_axis
|
|
556
|
+
if not complete:
|
|
557
|
+
return options, current_x_axis
|
|
512
558
|
value = current_x_axis if current_x_axis in variable_fields else (variable_fields[0] if variable_fields else None)
|
|
513
559
|
return options, value
|
|
514
560
|
|
|
@@ -835,12 +881,25 @@ def register_callbacks(app: Dash, default_measure_options: list[str]) -> None:
|
|
|
835
881
|
Input("single-representation", "value"),
|
|
836
882
|
Input("single-projection", "value"),
|
|
837
883
|
State("plot-settings-store", "data"),
|
|
884
|
+
State("url", "search"),
|
|
885
|
+
State("plot-single-x-scale", "value", allow_optional=True),
|
|
886
|
+
State("plot-single-y-scale", "value", allow_optional=True),
|
|
887
|
+
State("plot-single-font-size", "value", allow_optional=True),
|
|
888
|
+
State("plot-single-line-width", "value", allow_optional=True),
|
|
889
|
+
State("plot-single-legend", "value", allow_optional=True),
|
|
890
|
+
State("plot-single-grid", "value", allow_optional=True),
|
|
838
891
|
)
|
|
839
|
-
def _update_single_plot_options(representation: str, projection: str, plot_settings: dict | None):
|
|
892
|
+
def _update_single_plot_options(representation: str, projection: str, plot_settings: dict | None, search: str = "", *local_values):
|
|
893
|
+
setup = shared_setup(search, "/single")
|
|
894
|
+
plot_settings = shared_plot_settings(plot_settings, setup)
|
|
840
895
|
particle_settings = (plot_settings or {}).get("particle_explorer", plot_settings or {})
|
|
896
|
+
particle_settings = _merge_local_plot_values(particle_settings, local_values)
|
|
841
897
|
from PyMieSimX.gui.layout import _plot_options_card
|
|
842
898
|
|
|
843
|
-
|
|
899
|
+
card = _plot_options_card("single", particle_settings, representation, projection)
|
|
900
|
+
if setup:
|
|
901
|
+
restore_controls(card, {**setup, "controls": {}})
|
|
902
|
+
return card
|
|
844
903
|
|
|
845
904
|
@app.callback(
|
|
846
905
|
Output("experiment-plot-options-container", "children"),
|
|
@@ -848,22 +907,38 @@ def register_callbacks(app: Dash, default_measure_options: list[str]) -> None:
|
|
|
848
907
|
Input("experiment-result", "data"),
|
|
849
908
|
State("plot-settings-store", "data"),
|
|
850
909
|
State("plot-experiment-projection", "value", allow_optional=True),
|
|
910
|
+
State("url", "search"),
|
|
911
|
+
State("plot-experiment-x-scale", "value", allow_optional=True),
|
|
912
|
+
State("plot-experiment-y-scale", "value", allow_optional=True),
|
|
913
|
+
State("plot-experiment-font-size", "value", allow_optional=True),
|
|
914
|
+
State("plot-experiment-line-width", "value", allow_optional=True),
|
|
915
|
+
State("plot-experiment-legend", "value", allow_optional=True),
|
|
916
|
+
State("plot-experiment-grid", "value", allow_optional=True),
|
|
851
917
|
)
|
|
852
|
-
def _update_experiment_plot_options(x_axis: str | None, result: dict | None, plot_settings: dict | None, current_projection: str | None):
|
|
918
|
+
def _update_experiment_plot_options(x_axis: str | None, result: dict | None, plot_settings: dict | None, current_projection: str | None, search: str = "", *local_values):
|
|
919
|
+
setup = shared_setup(search, "/experiment")
|
|
920
|
+
plot_settings = shared_plot_settings(plot_settings, setup)
|
|
853
921
|
sweep_settings = (plot_settings or {}).get("parameter_sweep", plot_settings or {})
|
|
854
922
|
units = (result or {}).get("units", {})
|
|
855
923
|
xaxis_unit = units.get(x_axis)
|
|
856
924
|
if xaxis_unit is None and x_axis:
|
|
857
925
|
matching_units = [value for key, value in units.items() if key.rsplit(":", 1)[-1] == x_axis]
|
|
858
926
|
xaxis_unit = matching_units[0] if matching_units else None
|
|
927
|
+
if xaxis_unit is None and x_axis:
|
|
928
|
+
xaxis_unit = next((str(spec.unit) for group in (SOURCE_FIELDS, SCATTERER_FIELDS, DETECTOR_FIELDS)
|
|
929
|
+
for specs in group.values() for spec in specs if spec.name == x_axis and spec.unit is not None), None)
|
|
859
930
|
is_angle = _is_angular_unit(xaxis_unit)
|
|
860
931
|
options = [{"label": "Cartesian", "value": "cartesian"}]
|
|
861
932
|
if is_angle:
|
|
862
933
|
options.append({"label": "Polar", "value": "polar"})
|
|
863
934
|
projection = current_projection if is_angle and current_projection == "polar" else "cartesian"
|
|
935
|
+
sweep_settings = _merge_local_plot_values(sweep_settings, local_values)
|
|
864
936
|
from PyMieSimX.gui.layout import _plot_options_card
|
|
865
937
|
|
|
866
|
-
|
|
938
|
+
card = _plot_options_card("experiment", sweep_settings, projection=projection, projection_options=options)
|
|
939
|
+
if setup:
|
|
940
|
+
restore_controls(card, {**setup, "controls": {}})
|
|
941
|
+
return card
|
|
867
942
|
|
|
868
943
|
@app.callback(
|
|
869
944
|
Output("single-graph", "figure"),
|
|
@@ -894,6 +969,8 @@ def register_callbacks(app: Dash, default_measure_options: list[str]) -> None:
|
|
|
894
969
|
"""Keep Export CSV greyed out until Run has produced a result."""
|
|
895
970
|
return not result
|
|
896
971
|
|
|
972
|
+
register_sharing_callbacks(app)
|
|
973
|
+
|
|
897
974
|
|
|
898
975
|
def build_single_empty_figure(plot_settings: dict | None = None, theme: str = "light"):
|
|
899
976
|
"""Return the initial representation canvas."""
|
|
@@ -6,6 +6,14 @@ from collections.abc import Sequence
|
|
|
6
6
|
from dash import html
|
|
7
7
|
|
|
8
8
|
|
|
9
|
+
def workspace_header(title: str, description: str):
|
|
10
|
+
"""Build the same compact introduction card for each simulation workspace."""
|
|
11
|
+
return html.Section(className="page-hero workspace-header", children=[
|
|
12
|
+
html.P(title, className="eyebrow"),
|
|
13
|
+
html.P(description, className="hero-text workspace-intro"),
|
|
14
|
+
])
|
|
15
|
+
|
|
16
|
+
|
|
9
17
|
class Card:
|
|
10
18
|
"""Build a RosettaX-style card with an optional colored left accent."""
|
|
11
19
|
|
|
@@ -14,6 +14,7 @@ from PyMieSimX.gui.callbacks import register_callbacks
|
|
|
14
14
|
from PyMieSimX.gui.layout import create_layout
|
|
15
15
|
from PyMieSimX.gui.material_catalog import ensure_material_catalog
|
|
16
16
|
from PyMieSimX.gui.services import available_measures
|
|
17
|
+
from PyMieSimX.gui.seo import PAGE_METADATA, SearchDash, register_page_metadata
|
|
17
18
|
|
|
18
19
|
LOGGER = logging.getLogger(__name__)
|
|
19
20
|
GITHUB_RELEASES_URL = "https://github.com/MartinPdeS/PyMieSimX/releases"
|
|
@@ -36,9 +37,10 @@ def _initialize_material_catalog() -> None:
|
|
|
36
37
|
|
|
37
38
|
def create_dash_app() -> Dash:
|
|
38
39
|
"""Create and configure the experiment dashboard Dash application."""
|
|
39
|
-
application =
|
|
40
|
+
application = SearchDash(
|
|
40
41
|
__name__,
|
|
41
|
-
title="
|
|
42
|
+
title=PAGE_METADATA["/"]["title"],
|
|
43
|
+
update_title=None,
|
|
42
44
|
assets_folder=str(Path(__file__).with_name("assets")),
|
|
43
45
|
suppress_callback_exceptions=True,
|
|
44
46
|
)
|
|
@@ -51,6 +53,7 @@ def create_dash_app() -> Dash:
|
|
|
51
53
|
initial_measures = available_measures("SphereSet", "PhotodiodeSet")
|
|
52
54
|
application.layout = create_layout(initial_measures)
|
|
53
55
|
register_callbacks(application, initial_measures)
|
|
56
|
+
register_page_metadata(application)
|
|
54
57
|
LOGGER.debug("Dash application initialized with %d callbacks", len(application.callback_map))
|
|
55
58
|
return application
|
|
56
59
|
|
|
@@ -217,6 +217,7 @@ def create_layout(default_measure_options: list[str]):
|
|
|
217
217
|
className="app-shell",
|
|
218
218
|
children=[
|
|
219
219
|
dcc.Location(id="url", refresh=False),
|
|
220
|
+
dcc.Store(id="page-metadata"),
|
|
220
221
|
dcc.Store(id="experiment-result"),
|
|
221
222
|
dcc.Store(id="single-result"),
|
|
222
223
|
dcc.Download(id="csv-download"),
|
|
@@ -3,18 +3,25 @@
|
|
|
3
3
|
|
|
4
4
|
from dash import dcc, html
|
|
5
5
|
|
|
6
|
+
from PyMieSimX.gui.sharing import sharing_controls, sharing_feedback
|
|
7
|
+
from PyMieSimX.gui.components.cards import workspace_header
|
|
8
|
+
|
|
6
9
|
from PyMieSimX.gui.callback_helpers import status_banner
|
|
7
10
|
from PyMieSimX.gui.layout import PLOT_CONFIG, _plot_options_card, _x_axis_card, build_plot_options_sidebar, build_tabbed_sidebar
|
|
8
11
|
|
|
9
12
|
from .sections import build_detector_section, build_scatterer_section, build_source_section
|
|
10
13
|
|
|
11
14
|
|
|
12
|
-
def build_experiment_page(default_measure_options: list[str], plot_settings: dict | None = None):
|
|
15
|
+
def build_experiment_page(default_measure_options: list[str], plot_settings: dict | None = None, simulation: dict | None = None):
|
|
13
16
|
"""Build the isolated Parameter Scan workspace."""
|
|
14
17
|
settings = (plot_settings or {}).get("parameter_sweep", {})
|
|
18
|
+
projection = (simulation or {}).get("controls", {}).get("plot-experiment-projection", "cartesian")
|
|
19
|
+
projection_options = [{"label": "Cartesian", "value": "cartesian"}, {"label": "Polar", "value": "polar"}] if projection == "polar" else None
|
|
20
|
+
initial_plot_options = _plot_options_card("experiment", settings, projection=projection, projection_options=projection_options)
|
|
15
21
|
return html.Div(
|
|
16
22
|
className="tab-content-stack experiment-tab-content",
|
|
17
23
|
children=[
|
|
24
|
+
workspace_header("Compare scattering across a parameter sweep", "Run a Mie parameter sweep across source, scatterer, and detector settings. Compare scattering measures and export the results as CSV."),
|
|
18
25
|
html.Section(
|
|
19
26
|
id="configure",
|
|
20
27
|
className="workspace",
|
|
@@ -24,7 +31,6 @@ def build_experiment_page(default_measure_options: list[str], plot_settings: dic
|
|
|
24
31
|
html.Section(
|
|
25
32
|
className="result-column",
|
|
26
33
|
children=[
|
|
27
|
-
_x_axis_card(default_measure_options),
|
|
28
34
|
html.Div(
|
|
29
35
|
className="graph-toolbar",
|
|
30
36
|
children=[
|
|
@@ -42,6 +48,7 @@ def build_experiment_page(default_measure_options: list[str], plot_settings: dic
|
|
|
42
48
|
disabled=True,
|
|
43
49
|
className="toolbar-button toolbar-button-secondary",
|
|
44
50
|
),
|
|
51
|
+
sharing_controls("/experiment"),
|
|
45
52
|
],
|
|
46
53
|
),
|
|
47
54
|
html.Div(
|
|
@@ -49,6 +56,8 @@ def build_experiment_page(default_measure_options: list[str], plot_settings: dic
|
|
|
49
56
|
className="experiment-run-feedback",
|
|
50
57
|
children=status_banner("idle", "Ready. Click Run to start a sweep."),
|
|
51
58
|
),
|
|
59
|
+
sharing_feedback(),
|
|
60
|
+
_x_axis_card(default_measure_options),
|
|
52
61
|
html.Section(className="panel graph-panel", children=[dcc.Loading(id="result-graph-loading", type="circle", color="#4f8df7", custom_spinner=html.Div("Computing…", className="plot-computing-indicator"), delay_show=150, delay_hide=150, children=dcc.Graph(id="result-graph", config=PLOT_CONFIG))]),
|
|
53
62
|
],
|
|
54
63
|
),
|
|
@@ -61,7 +70,7 @@ def build_experiment_page(default_measure_options: list[str], plot_settings: dic
|
|
|
61
70
|
{"tab_id": "experiment-tab-source", "panel_id": "experiment-panel-source", "label": "Source", "color": "yellow", "content": build_source_section(), "active": False},
|
|
62
71
|
{"tab_id": "experiment-tab-scatterer", "panel_id": "experiment-panel-scatterer", "label": "Scatterer", "color": "blue", "content": build_scatterer_section(), "active": False},
|
|
63
72
|
{"tab_id": "experiment-tab-detector", "panel_id": "experiment-panel-detector", "label": "Detector", "color": "cyan", "content": build_detector_section(), "active": False},
|
|
64
|
-
{"tab_id": "experiment-tab-plot-options", "panel_id": "experiment-panel-plot-options", "label": "Plot options", "color": "purple", "content": build_plot_options_sidebar("experiment-plot-options-container",
|
|
73
|
+
{"tab_id": "experiment-tab-plot-options", "panel_id": "experiment-panel-plot-options", "label": "Plot options", "color": "purple", "content": build_plot_options_sidebar("experiment-plot-options-container", initial_plot_options), "active": False},
|
|
65
74
|
],
|
|
66
75
|
),
|
|
67
76
|
],
|
|
@@ -29,7 +29,7 @@ def build_home_page(*, home_visits: int | float | None = None):
|
|
|
29
29
|
[
|
|
30
30
|
html.H1("PyMieSim"),
|
|
31
31
|
html.P(
|
|
32
|
-
"An open-source
|
|
32
|
+
"An open-source Mie theory simulator and scattering calculator. Explore individual particles, run parameter sweeps, and export results with PyMieSim.",
|
|
33
33
|
className="hero-text",
|
|
34
34
|
),
|
|
35
35
|
],
|
|
@@ -4,10 +4,13 @@ import logging
|
|
|
4
4
|
|
|
5
5
|
from dash import Input, Output, State, dcc, html, no_update
|
|
6
6
|
from dash.exceptions import PreventUpdate
|
|
7
|
+
|
|
8
|
+
from PyMieSimX.gui.sharing import sharing_controls, sharing_feedback, shared_setup
|
|
7
9
|
import plotly.graph_objects as go
|
|
8
10
|
import numpy as np
|
|
9
11
|
|
|
10
12
|
from PyMieSimX.gui.components import Card
|
|
13
|
+
from PyMieSimX.gui.components.cards import workspace_header
|
|
11
14
|
from PyMieSimX.gui.layout import build_tabbed_sidebar, render_field
|
|
12
15
|
from PyMieSimX.gui.material_catalog import material_dropdown_options
|
|
13
16
|
from PyMieSimX.gui.schemas import POPULATION_OPTICAL_FIELDS
|
|
@@ -91,19 +94,18 @@ def build_population_page():
|
|
|
91
94
|
return html.Div(className="page-content-stack population-page", children=[
|
|
92
95
|
dcc.Store(id="population-result"), dcc.Download(id="population-download"),
|
|
93
96
|
dcc.Store(id="population-right-sidebar-active", data=None),
|
|
94
|
-
|
|
95
|
-
html.P("From particles to population properties", className="eyebrow"),
|
|
96
|
-
html.P("Define particle properties, size distribution, and concentration in the side panels, then compute all optical properties at one wavelength.", className="hero-text"),
|
|
97
|
-
]),
|
|
97
|
+
workspace_header("From particles to population properties", "Define particle properties, size distribution, and concentration in the side panels, then compute all optical properties at one wavelength."),
|
|
98
98
|
html.Section(className="population-workspace", children=[
|
|
99
99
|
html.Div(className="result-column population-output", children=[
|
|
100
100
|
html.Div(className="graph-toolbar", children=[
|
|
101
|
-
html.Button(_compute_label("
|
|
101
|
+
html.Button(_compute_label("Run"), id="population-compute", n_clicks=0,
|
|
102
102
|
className="toolbar-button toolbar-button-primary"),
|
|
103
103
|
html.Button([html.Span("⤓", className="toolbar-button-icon", **{"aria-hidden": "true"}), "Export CSV"],
|
|
104
104
|
id="population-export", disabled=True, className="toolbar-button toolbar-button-secondary"),
|
|
105
|
+
sharing_controls("/population"),
|
|
105
106
|
]),
|
|
106
|
-
|
|
107
|
+
sharing_feedback(),
|
|
108
|
+
html.Div(id="population-status", role="status", children="Ready. Click Run to calculate all properties."),
|
|
107
109
|
_panel("Distribution preview", [
|
|
108
110
|
dcc.Graph(id="population-preview", config={"displaylogo": False}),
|
|
109
111
|
html.Div(id="population-preview-error", role="status"),
|
|
@@ -138,20 +140,28 @@ def register_population_callbacks(app):
|
|
|
138
140
|
Output("population-minimum-field", "style"), Output("population-maximum-field", "style"),
|
|
139
141
|
Output("population-sampling-field", "style"), Output("population-width", "value"),
|
|
140
142
|
Input("population-distribution", "value"),
|
|
143
|
+
State("url", "search"),
|
|
141
144
|
)
|
|
142
|
-
def _population_distribution_controls(distribution):
|
|
145
|
+
def _population_distribution_controls(distribution, search=""):
|
|
146
|
+
from dash import ctx
|
|
147
|
+
setup = shared_setup(search, "/population")
|
|
148
|
+
width = (setup or {}).get("controls", {}).get("population-width") if setup and not ctx.triggered else None
|
|
143
149
|
hidden = {"display": "none"}
|
|
144
150
|
diameter_label = {"monodisperse": "Diameter [nm]", "gaussian": "Gaussian mean diameter [nm]"}.get(distribution, "Geometric median diameter [nm]")
|
|
145
151
|
return (diameter_label, "Standard deviation [nm]" if distribution == "gaussian" else "Geometric standard deviation",
|
|
146
152
|
hidden if distribution == "uniform" else {}, {} if distribution in {"gaussian", "lognormal"} else hidden,
|
|
147
153
|
{} if distribution in {"gaussian", "uniform"} else hidden,
|
|
148
154
|
{} if distribution in {"gaussian", "uniform"} else hidden,
|
|
149
|
-
hidden if distribution == "monodisperse" else {}, "50" if distribution == "gaussian" else "1.2")
|
|
155
|
+
hidden if distribution == "monodisperse" else {}, width if width is not None else "50" if distribution == "gaussian" else "1.2")
|
|
150
156
|
|
|
151
157
|
@app.callback(Output("population-concentration-label", "children"), Output("population-concentration", "value"),
|
|
152
|
-
Input("population-concentration-basis", "value"))
|
|
153
|
-
def _population_concentration_controls(basis):
|
|
154
|
-
|
|
158
|
+
Input("population-concentration-basis", "value"), State("url", "search"))
|
|
159
|
+
def _population_concentration_controls(basis, search=""):
|
|
160
|
+
from dash import ctx
|
|
161
|
+
setup = shared_setup(search, "/population")
|
|
162
|
+
concentration = (setup or {}).get("controls", {}).get("population-concentration") if setup and not ctx.triggered else None
|
|
163
|
+
label, default = ("Particle volume fraction [0–1]", "0.001") if basis == "volume" else ("Number concentration [particles/mL]", "1e9")
|
|
164
|
+
return label, concentration if concentration is not None else default
|
|
155
165
|
|
|
156
166
|
@app.callback(Output("population-preview", "figure"), Output("population-preview-error", "children"),
|
|
157
167
|
*[Input(f"population-{name}", "value") for name in ("distribution", "diameter", "width", "minimum", "maximum", "sampling")],
|
|
@@ -183,7 +193,7 @@ def register_population_callbacks(app):
|
|
|
183
193
|
Input("population-compute", "n_clicks"), *[State(_control_id(name), "value") for name in FIELDS],
|
|
184
194
|
prevent_initial_call=True, running=[
|
|
185
195
|
(Output("population-compute", "disabled"), True, False),
|
|
186
|
-
(Output("population-compute", "children"), _compute_label("
|
|
196
|
+
(Output("population-compute", "children"), _compute_label("Running…"), _compute_label("Run")),
|
|
187
197
|
*[(Output(_control_id(name), "disabled"), True, False) for name in FIELDS],
|
|
188
198
|
*[(Output({"kind": kind, "section": "population", "name": name}, "disabled"),
|
|
189
199
|
True, not material_dropdown_options(medium=name == "medium"))
|
|
@@ -211,7 +221,7 @@ def register_population_callbacks(app):
|
|
|
211
221
|
Output("population-export", "disabled", allow_duplicate=True),
|
|
212
222
|
*[Input(_control_id(name), "value") for name in FIELDS], prevent_initial_call=True)
|
|
213
223
|
def _invalidate_population(*values):
|
|
214
|
-
return None, [], "Ready. Click
|
|
224
|
+
return None, [], "Ready. Click Run to calculate all properties.", True
|
|
215
225
|
|
|
216
226
|
@app.callback(Output("population-download", "data"), Input("population-export", "n_clicks"),
|
|
217
227
|
State("population-result", "data"), prevent_initial_call=True)
|
|
@@ -2,24 +2,31 @@
|
|
|
2
2
|
|
|
3
3
|
from dash import dcc, html
|
|
4
4
|
|
|
5
|
+
from PyMieSimX.gui.sharing import sharing_controls, sharing_feedback
|
|
6
|
+
from PyMieSimX.gui.components.cards import workspace_header
|
|
7
|
+
|
|
5
8
|
from PyMieSimX.gui.defaults import DEFAULT_WORKSPACE_SETTINGS
|
|
6
9
|
from PyMieSimX.gui.layout import PLOT_CONFIG, _plot_options_card, build_plot_options_sidebar, build_tabbed_sidebar
|
|
7
10
|
|
|
8
11
|
from .sections import build_representation_section, build_scatterer_section, build_source_section
|
|
9
12
|
|
|
10
13
|
|
|
11
|
-
def build_single_page(plot_settings: dict | None = None):
|
|
14
|
+
def build_single_page(plot_settings: dict | None = None, simulation: dict | None = None):
|
|
12
15
|
"""Build the single page from independent setup and plot sections."""
|
|
16
|
+
controls = (simulation or {}).get("controls", {})
|
|
17
|
+
representation = controls.get("single-representation", DEFAULT_WORKSPACE_SETTINGS["particle_explorer"]["representation"])
|
|
18
|
+
projection = controls.get("single-projection", DEFAULT_WORKSPACE_SETTINGS["particle_explorer"]["projection"])
|
|
19
|
+
initial_plot_options = _plot_options_card("single", plot_settings or {}, representation=representation, projection=projection)
|
|
13
20
|
return html.Div(
|
|
14
21
|
className="tab-content-stack single-tab-content",
|
|
15
22
|
children=[
|
|
23
|
+
workspace_header("Explore scattering from a single particle", "Configure the source and scatterer to explore nanoparticle Mie scattering, angular patterns, polarization, and fields."),
|
|
16
24
|
html.Section(
|
|
17
25
|
className="single-workspace",
|
|
18
26
|
children=[
|
|
19
27
|
html.Section(
|
|
20
28
|
className="result-column",
|
|
21
29
|
children=[
|
|
22
|
-
build_representation_section(),
|
|
23
30
|
html.Div(
|
|
24
31
|
className="graph-toolbar",
|
|
25
32
|
children=[
|
|
@@ -37,8 +44,11 @@ def build_single_page(plot_settings: dict | None = None):
|
|
|
37
44
|
disabled=True,
|
|
38
45
|
className="toolbar-button toolbar-button-secondary",
|
|
39
46
|
),
|
|
47
|
+
sharing_controls("/single"),
|
|
40
48
|
],
|
|
41
49
|
),
|
|
50
|
+
sharing_feedback(),
|
|
51
|
+
build_representation_section(),
|
|
42
52
|
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=dcc.Graph(id="single-graph", config=PLOT_CONFIG))]),
|
|
43
53
|
],
|
|
44
54
|
),
|
|
@@ -50,7 +60,7 @@ def build_single_page(plot_settings: dict | None = None):
|
|
|
50
60
|
[
|
|
51
61
|
{"tab_id": "single-tab-source", "panel_id": "single-panel-source", "label": "Source", "color": "yellow", "content": build_source_section(), "active": False},
|
|
52
62
|
{"tab_id": "single-tab-scatterer", "panel_id": "single-panel-scatterer", "label": "Scatterer", "color": "blue", "content": build_scatterer_section(), "active": False},
|
|
53
|
-
{"tab_id": "single-tab-plot-options", "panel_id": "single-panel-plot-options", "label": "Plot options", "color": "purple", "content": build_plot_options_sidebar("single-plot-options-container",
|
|
63
|
+
{"tab_id": "single-tab-plot-options", "panel_id": "single-panel-plot-options", "label": "Plot options", "color": "purple", "content": build_plot_options_sidebar("single-plot-options-container", initial_plot_options), "active": False},
|
|
54
64
|
],
|
|
55
65
|
),
|
|
56
66
|
],
|