PyMieSimX 0.2.2__tar.gz → 0.3.0__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.2 → pymiesimx-0.3.0}/PKG-INFO +12 -3
- {pymiesimx-0.2.2 → pymiesimx-0.3.0}/PyMieSimX/gui/assets/dashboard.css +37 -3
- pymiesimx-0.3.0/PyMieSimX/gui/assets/simulation-sharing.js +85 -0
- {pymiesimx-0.2.2 → pymiesimx-0.3.0}/PyMieSimX/gui/callbacks.py +17 -11
- {pymiesimx-0.2.2 → pymiesimx-0.3.0}/PyMieSimX/gui/layout.py +1 -0
- pymiesimx-0.3.0/PyMieSimX/gui/pages/examples.py +140 -0
- {pymiesimx-0.2.2 → pymiesimx-0.3.0}/PyMieSimX/gui/pages/experiment/page.py +1 -2
- {pymiesimx-0.2.2 → pymiesimx-0.3.0}/PyMieSimX/gui/pages/population.py +1 -2
- {pymiesimx-0.2.2 → pymiesimx-0.3.0}/PyMieSimX/gui/pages/single/page.py +1 -2
- {pymiesimx-0.2.2 → pymiesimx-0.3.0}/PyMieSimX/gui/seo.py +4 -0
- {pymiesimx-0.2.2 → pymiesimx-0.3.0}/PyMieSimX/gui/sharing.py +23 -25
- {pymiesimx-0.2.2 → pymiesimx-0.3.0}/PyMieSimX.egg-info/PKG-INFO +12 -3
- {pymiesimx-0.2.2 → pymiesimx-0.3.0}/PyMieSimX.egg-info/SOURCES.txt +2 -0
- {pymiesimx-0.2.2 → pymiesimx-0.3.0}/PyMieSimX.egg-info/scm_file_list.json +2 -0
- {pymiesimx-0.2.2 → pymiesimx-0.3.0}/PyMieSimX.egg-info/scm_version.json +2 -2
- {pymiesimx-0.2.2 → pymiesimx-0.3.0}/README.rst +11 -2
- {pymiesimx-0.2.2 → pymiesimx-0.3.0}/tests/browser/test_dashboard.py +74 -12
- pymiesimx-0.3.0/tests/gui/test_examples.py +78 -0
- {pymiesimx-0.2.2 → pymiesimx-0.3.0}/tests/gui/test_sharing.py +8 -14
- pymiesimx-0.2.2/PyMieSimX/gui/assets/simulation-sharing.js +0 -36
- {pymiesimx-0.2.2 → pymiesimx-0.3.0}/.coveragerc +0 -0
- {pymiesimx-0.2.2 → pymiesimx-0.3.0}/.flake8 +0 -0
- {pymiesimx-0.2.2 → pymiesimx-0.3.0}/.github/dependabot.yml +0 -0
- {pymiesimx-0.2.2 → pymiesimx-0.3.0}/.github/workflows/deploy_PyPi.yml +0 -0
- {pymiesimx-0.2.2 → pymiesimx-0.3.0}/.github/workflows/deploy_anaconda.yml +0 -0
- {pymiesimx-0.2.2 → pymiesimx-0.3.0}/.github/workflows/deploy_coverage.yml +0 -0
- {pymiesimx-0.2.2 → pymiesimx-0.3.0}/.github/workflows/quality.yml +0 -0
- {pymiesimx-0.2.2 → pymiesimx-0.3.0}/.github/workflows/tests.yml +0 -0
- {pymiesimx-0.2.2 → pymiesimx-0.3.0}/.gitignore +0 -0
- {pymiesimx-0.2.2 → pymiesimx-0.3.0}/.pre-commit-config.yaml +0 -0
- {pymiesimx-0.2.2 → pymiesimx-0.3.0}/AGENTS.md +0 -0
- {pymiesimx-0.2.2 → pymiesimx-0.3.0}/CONTRIBUTING.md +0 -0
- {pymiesimx-0.2.2 → pymiesimx-0.3.0}/Dockerfile +0 -0
- {pymiesimx-0.2.2 → pymiesimx-0.3.0}/LICENSE +0 -0
- {pymiesimx-0.2.2 → pymiesimx-0.3.0}/PyMieSimX/__init__.py +0 -0
- {pymiesimx-0.2.2 → pymiesimx-0.3.0}/PyMieSimX/__main__.py +0 -0
- {pymiesimx-0.2.2 → pymiesimx-0.3.0}/PyMieSimX/api.py +0 -0
- {pymiesimx-0.2.2 → pymiesimx-0.3.0}/PyMieSimX/application/__init__.py +0 -0
- {pymiesimx-0.2.2 → pymiesimx-0.3.0}/PyMieSimX/application/wsgi.py +0 -0
- {pymiesimx-0.2.2 → pymiesimx-0.3.0}/PyMieSimX/gui/__init__.py +0 -0
- {pymiesimx-0.2.2 → pymiesimx-0.3.0}/PyMieSimX/gui/admin.py +0 -0
- {pymiesimx-0.2.2 → pymiesimx-0.3.0}/PyMieSimX/gui/assets/card-state.js +0 -0
- {pymiesimx-0.2.2 → pymiesimx-0.3.0}/PyMieSimX/gui/assets/default-settings.json +0 -0
- {pymiesimx-0.2.2 → pymiesimx-0.3.0}/PyMieSimX/gui/assets/home-spf-radial.gif +0 -0
- {pymiesimx-0.2.2 → pymiesimx-0.3.0}/PyMieSimX/gui/assets/home-spf-radial.png +0 -0
- {pymiesimx-0.2.2 → pymiesimx-0.3.0}/PyMieSimX/gui/assets/logo.png +0 -0
- {pymiesimx-0.2.2 → pymiesimx-0.3.0}/PyMieSimX/gui/assets/materials-stock.json +0 -0
- {pymiesimx-0.2.2 → pymiesimx-0.3.0}/PyMieSimX/gui/assets/pymiesim-favicon.svg +0 -0
- {pymiesimx-0.2.2 → pymiesimx-0.3.0}/PyMieSimX/gui/assets/pymiesim-logo-dark.svg +0 -0
- {pymiesimx-0.2.2 → pymiesimx-0.3.0}/PyMieSimX/gui/assets/pymiesim-logo.svg +0 -0
- {pymiesimx-0.2.2 → pymiesimx-0.3.0}/PyMieSimX/gui/callback_helpers.py +0 -0
- {pymiesimx-0.2.2 → pymiesimx-0.3.0}/PyMieSimX/gui/components/__init__.py +0 -0
- {pymiesimx-0.2.2 → pymiesimx-0.3.0}/PyMieSimX/gui/components/cards.py +0 -0
- {pymiesimx-0.2.2 → pymiesimx-0.3.0}/PyMieSimX/gui/computation.py +0 -0
- {pymiesimx-0.2.2 → pymiesimx-0.3.0}/PyMieSimX/gui/defaults.py +0 -0
- {pymiesimx-0.2.2 → pymiesimx-0.3.0}/PyMieSimX/gui/experiment_service.py +0 -0
- {pymiesimx-0.2.2 → pymiesimx-0.3.0}/PyMieSimX/gui/interface.py +0 -0
- {pymiesimx-0.2.2 → pymiesimx-0.3.0}/PyMieSimX/gui/jobs.py +0 -0
- {pymiesimx-0.2.2 → pymiesimx-0.3.0}/PyMieSimX/gui/material_catalog.py +0 -0
- {pymiesimx-0.2.2 → pymiesimx-0.3.0}/PyMieSimX/gui/models.py +0 -0
- {pymiesimx-0.2.2 → pymiesimx-0.3.0}/PyMieSimX/gui/pages/__init__.py +0 -0
- {pymiesimx-0.2.2 → pymiesimx-0.3.0}/PyMieSimX/gui/pages/citation.py +0 -0
- {pymiesimx-0.2.2 → pymiesimx-0.3.0}/PyMieSimX/gui/pages/documentation.py +0 -0
- {pymiesimx-0.2.2 → pymiesimx-0.3.0}/PyMieSimX/gui/pages/experiment/__init__.py +0 -0
- {pymiesimx-0.2.2 → pymiesimx-0.3.0}/PyMieSimX/gui/pages/experiment/sections/__init__.py +0 -0
- {pymiesimx-0.2.2 → pymiesimx-0.3.0}/PyMieSimX/gui/pages/experiment/sections/configuration.py +0 -0
- {pymiesimx-0.2.2 → pymiesimx-0.3.0}/PyMieSimX/gui/pages/field_syntax.py +0 -0
- {pymiesimx-0.2.2 → pymiesimx-0.3.0}/PyMieSimX/gui/pages/home.py +0 -0
- {pymiesimx-0.2.2 → pymiesimx-0.3.0}/PyMieSimX/gui/pages/install_local.py +0 -0
- {pymiesimx-0.2.2 → pymiesimx-0.3.0}/PyMieSimX/gui/pages/sellmeier.py +0 -0
- {pymiesimx-0.2.2 → pymiesimx-0.3.0}/PyMieSimX/gui/pages/settings.py +0 -0
- {pymiesimx-0.2.2 → pymiesimx-0.3.0}/PyMieSimX/gui/pages/single/__init__.py +0 -0
- {pymiesimx-0.2.2 → pymiesimx-0.3.0}/PyMieSimX/gui/pages/single/sections/__init__.py +0 -0
- {pymiesimx-0.2.2 → pymiesimx-0.3.0}/PyMieSimX/gui/pages/single/sections/representations.py +0 -0
- {pymiesimx-0.2.2 → pymiesimx-0.3.0}/PyMieSimX/gui/pages/single/sections/setup.py +0 -0
- {pymiesimx-0.2.2 → pymiesimx-0.3.0}/PyMieSimX/gui/parsing.py +0 -0
- {pymiesimx-0.2.2 → pymiesimx-0.3.0}/PyMieSimX/gui/plotting.py +0 -0
- {pymiesimx-0.2.2 → pymiesimx-0.3.0}/PyMieSimX/gui/population_service.py +0 -0
- {pymiesimx-0.2.2 → pymiesimx-0.3.0}/PyMieSimX/gui/schemas.py +0 -0
- {pymiesimx-0.2.2 → pymiesimx-0.3.0}/PyMieSimX/gui/services.py +0 -0
- {pymiesimx-0.2.2 → pymiesimx-0.3.0}/PyMieSimX/gui/single_service.py +0 -0
- {pymiesimx-0.2.2 → pymiesimx-0.3.0}/PyMieSimX/gui/usage_metrics.py +0 -0
- {pymiesimx-0.2.2 → pymiesimx-0.3.0}/PyMieSimX/gui/validation.py +0 -0
- {pymiesimx-0.2.2 → pymiesimx-0.3.0}/PyMieSimX.egg-info/dependency_links.txt +0 -0
- {pymiesimx-0.2.2 → pymiesimx-0.3.0}/PyMieSimX.egg-info/entry_points.txt +0 -0
- {pymiesimx-0.2.2 → pymiesimx-0.3.0}/PyMieSimX.egg-info/requires.txt +0 -0
- {pymiesimx-0.2.2 → pymiesimx-0.3.0}/PyMieSimX.egg-info/top_level.txt +0 -0
- {pymiesimx-0.2.2 → pymiesimx-0.3.0}/conda.recipe/meta.yaml +0 -0
- {pymiesimx-0.2.2 → pymiesimx-0.3.0}/makefile +0 -0
- {pymiesimx-0.2.2 → pymiesimx-0.3.0}/pyproject.toml +0 -0
- {pymiesimx-0.2.2 → pymiesimx-0.3.0}/pytest.ini +0 -0
- {pymiesimx-0.2.2 → pymiesimx-0.3.0}/setup.cfg +0 -0
- {pymiesimx-0.2.2 → pymiesimx-0.3.0}/tests/gui/cli.py +0 -0
- {pymiesimx-0.2.2 → pymiesimx-0.3.0}/tests/gui/test_admin.py +0 -0
- {pymiesimx-0.2.2 → pymiesimx-0.3.0}/tests/gui/test_api.py +0 -0
- {pymiesimx-0.2.2 → pymiesimx-0.3.0}/tests/gui/test_dash_callbacks.py +0 -0
- {pymiesimx-0.2.2 → pymiesimx-0.3.0}/tests/gui/test_home.py +0 -0
- {pymiesimx-0.2.2 → pymiesimx-0.3.0}/tests/gui/test_population.py +0 -0
- {pymiesimx-0.2.2 → pymiesimx-0.3.0}/tests/gui/test_seo.py +0 -0
- {pymiesimx-0.2.2 → pymiesimx-0.3.0}/tests/gui/test_services.py +0 -0
- {pymiesimx-0.2.2 → pymiesimx-0.3.0}/tests/gui/test_usage_metrics.py +0 -0
- {pymiesimx-0.2.2 → pymiesimx-0.3.0}/tools/generate_home_spf.py +0 -0
- {pymiesimx-0.2.2 → pymiesimx-0.3.0}/tools/next_release_version.py +0 -0
- {pymiesimx-0.2.2 → pymiesimx-0.3.0}/tools/release_tag.py +0 -0
- {pymiesimx-0.2.2 → pymiesimx-0.3.0}/tools/verify_conda_artifact.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: PyMieSimX
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.3.0
|
|
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,15 @@ 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
|
+
Examples
|
|
54
|
+
--------
|
|
55
|
+
|
|
56
|
+
The **Examples** page provides six starting points: gold and silver nanoparticle
|
|
57
|
+
resonances, a silica size sweep, gold nanoshells, a bead's angular scattering,
|
|
58
|
+
and a polydisperse silica suspension. Each card links to its simulation
|
|
59
|
+
workspace, restores the complete setup, and runs automatically to show results.
|
|
60
|
+
The parameters can then be adjusted and the results exported as CSV.
|
|
61
|
+
|
|
53
62
|
Sharing simulations
|
|
54
63
|
-------------------
|
|
55
64
|
|
|
@@ -58,8 +67,8 @@ Optics to share the current setup. Links preserve the input values, sweep
|
|
|
58
67
|
ranges, selected measures, and plot settings without requiring an account.
|
|
59
68
|
Opening a link restores the setup and automatically runs the simulation once
|
|
60
69
|
the inputs and plot settings are ready. Later edits can be calculated with
|
|
61
|
-
**Run**.
|
|
62
|
-
|
|
70
|
+
**Run**. Copying puts the link directly on the clipboard and briefly shows
|
|
71
|
+
**Copied** on the button without displaying an additional card.
|
|
63
72
|
|
|
64
73
|
Links store the configuration rather than a result snapshot. Recalculation
|
|
65
74
|
uses the installed solver and optical material data, so results can change
|
|
@@ -4077,9 +4077,43 @@ html .single-workspace .representation-card {
|
|
|
4077
4077
|
.workspace-header .eyebrow { min-height: 2lh; }
|
|
4078
4078
|
.workspace-header .hero-text { min-height: 4lh; }
|
|
4079
4079
|
}
|
|
4080
|
+
@media (max-width: 380px) {
|
|
4081
|
+
.workspace-header .eyebrow { min-height: 3lh; }
|
|
4082
|
+
.workspace-header .hero-text { min-height: 6lh; }
|
|
4083
|
+
}
|
|
4080
4084
|
|
|
4081
|
-
.simulation-share-feedback { display: grid; gap: 8px; font-size: .86rem; color: var(--muted); }
|
|
4082
4085
|
.simulation-share-actions { display: contents; }
|
|
4083
|
-
.simulation-
|
|
4084
|
-
.simulation-
|
|
4086
|
+
.simulation-copy-button { display: inline-grid; }
|
|
4087
|
+
.simulation-copy-button > span { grid-area: 1 / 1; }
|
|
4088
|
+
.simulation-copy-pending-label,
|
|
4089
|
+
.simulation-copy-success-label,
|
|
4090
|
+
.simulation-copy-error-label { visibility: hidden; }
|
|
4091
|
+
.simulation-copy-pending .simulation-copy-label,
|
|
4092
|
+
.simulation-copy-success .simulation-copy-label,
|
|
4093
|
+
.simulation-copy-error .simulation-copy-label { visibility: hidden; }
|
|
4094
|
+
.simulation-copy-pending > .simulation-copy-pending-label,
|
|
4095
|
+
.simulation-copy-success > .simulation-copy-success-label,
|
|
4096
|
+
.simulation-copy-error > .simulation-copy-error-label { visibility: visible; }
|
|
4097
|
+
.simulation-copy-button:active { transform: translateY(1px); }
|
|
4098
|
+
.simulation-copy-button.simulation-copy-success { background: rgba(25, 135, 84, .12); border-color: #198754; color: #198754; }
|
|
4099
|
+
.simulation-copy-button.simulation-copy-error { background: rgba(190, 50, 50, .10); border-color: #be3232; color: #be3232; }
|
|
4085
4100
|
.simulation-link-notice { padding: 10px 14px; border: 1px solid var(--line); border-radius: 8px; color: var(--ink); }
|
|
4101
|
+
|
|
4102
|
+
.examples-grid { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 18px; }
|
|
4103
|
+
.example-card { display: flex; flex-direction: column; gap: 14px; padding: 22px; min-width: 0; }
|
|
4104
|
+
.example-card-heading { display: flex; align-items: center; justify-content: space-between; gap: 12px; }
|
|
4105
|
+
.example-number { font-size: 1.7rem; font-weight: 700; color: var(--accent); }
|
|
4106
|
+
.example-topic { font-size: .75rem; color: var(--muted); text-align: right; }
|
|
4107
|
+
.example-card h2 { margin: 0; font-size: 1.2rem; line-height: 1.3; }
|
|
4108
|
+
.example-description, .example-observe p { margin: 0; font-size: .88rem; line-height: 1.6; color: var(--muted); }
|
|
4109
|
+
.example-parameters { margin: 0; display: grid; gap: 8px; padding: 12px; background: var(--panel); border: 1px solid var(--line); border-radius: 8px; }
|
|
4110
|
+
.example-parameters dt { font-size: .72rem; color: var(--muted); }
|
|
4111
|
+
.example-parameters dd { margin: 2px 0 0; font-size: .84rem; overflow-wrap: anywhere; }
|
|
4112
|
+
.example-observe { flex: 1; }
|
|
4113
|
+
.example-observe strong { display: block; margin-bottom: 4px; font-size: .8rem; }
|
|
4114
|
+
.example-reference { font-size: .72rem; color: var(--muted); }
|
|
4115
|
+
.example-card-actions { display: grid; gap: 8px; padding-top: 12px; border-top: 1px solid var(--line); }
|
|
4116
|
+
.example-card-actions > span { font-size: .72rem; color: var(--muted); }
|
|
4117
|
+
.example-open { text-decoration: none; text-align: center; white-space: normal; }
|
|
4118
|
+
@media (max-width: 1250px) { .examples-grid { grid-template-columns: repeat(2, minmax(0, 1fr)); } }
|
|
4119
|
+
@media (max-width: 700px) { .examples-grid { grid-template-columns: minmax(0, 1fr); } }
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
window.dash_clientside = Object.assign({}, window.dash_clientside, {
|
|
2
|
+
simulation_sharing: {
|
|
3
|
+
copy_feedback: function(clicks, status) {
|
|
4
|
+
const sharing = window.dash_clientside.simulation_sharing;
|
|
5
|
+
const base = 'toolbar-button toolbar-button-secondary simulation-copy-button';
|
|
6
|
+
const title = 'Copy a link to this simulation setup';
|
|
7
|
+
window.clearTimeout(sharing.feedbackTimer);
|
|
8
|
+
if (!clicks) return [base, title];
|
|
9
|
+
if (!status || status.clicks !== clicks) {
|
|
10
|
+
return [base + ' simulation-copy-pending', 'Copying the simulation link…'];
|
|
11
|
+
}
|
|
12
|
+
const button = document.getElementById('simulation-clipboard');
|
|
13
|
+
sharing.feedbackTimer = window.setTimeout(function() {
|
|
14
|
+
if (document.getElementById('simulation-clipboard') === button) {
|
|
15
|
+
window.dash_clientside.set_props('simulation-clipboard', {className: base, title: title});
|
|
16
|
+
}
|
|
17
|
+
}, 2000);
|
|
18
|
+
return status.copied
|
|
19
|
+
? [base + ' simulation-copy-success', 'Simulation link copied to clipboard']
|
|
20
|
+
: [base + ' simulation-copy-error', 'Unable to copy. Try again or check clipboard permissions.'];
|
|
21
|
+
},
|
|
22
|
+
copy_link: async function(link) {
|
|
23
|
+
if (!link) return window.dash_clientside.no_update;
|
|
24
|
+
if (!link.url) return {clicks: link.clicks, copied: false};
|
|
25
|
+
if (window.isSecureContext && navigator.clipboard && navigator.clipboard.writeText) {
|
|
26
|
+
try {
|
|
27
|
+
await navigator.clipboard.writeText(link.url);
|
|
28
|
+
return {clicks: link.clicks, copied: true};
|
|
29
|
+
} catch (error) {
|
|
30
|
+
// Older browsers and denied clipboard permissions can
|
|
31
|
+
// still support copying through the document selection.
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
const field = document.createElement('textarea');
|
|
35
|
+
const focused = document.activeElement;
|
|
36
|
+
field.value = link.url;
|
|
37
|
+
field.setAttribute('readonly', '');
|
|
38
|
+
field.style.cssText = 'position:fixed;top:0;left:0;opacity:0;pointer-events:none;';
|
|
39
|
+
document.body.appendChild(field);
|
|
40
|
+
let copied = false;
|
|
41
|
+
try {
|
|
42
|
+
field.select();
|
|
43
|
+
copied = document.execCommand('copy');
|
|
44
|
+
} catch (error) {
|
|
45
|
+
copied = false;
|
|
46
|
+
} finally {
|
|
47
|
+
field.remove();
|
|
48
|
+
if (focused && focused.focus) focused.focus({preventScroll: true});
|
|
49
|
+
}
|
|
50
|
+
return {clicks: link.clicks, copied: copied};
|
|
51
|
+
},
|
|
52
|
+
auto_run: function(config, fieldIds, fieldValues, fieldClasses, settings) {
|
|
53
|
+
const unchanged = window.dash_clientside.no_update;
|
|
54
|
+
if (!config || config.started) return [unchanged, unchanged];
|
|
55
|
+
const values = Array.prototype.slice.call(arguments, 5);
|
|
56
|
+
const clicks = values.pop();
|
|
57
|
+
if (clicks) return [unchanged, Object.assign({}, config, {started: true})];
|
|
58
|
+
|
|
59
|
+
const actual = {};
|
|
60
|
+
config.control_names.forEach((name, index) => { actual[name] = values[index]; });
|
|
61
|
+
(fieldIds || []).forEach((id, index) => {
|
|
62
|
+
const ordered = {};
|
|
63
|
+
Object.keys(id).sort().forEach(key => { ordered[key] = id[key]; });
|
|
64
|
+
actual[JSON.stringify(ordered)] = fieldValues[index];
|
|
65
|
+
});
|
|
66
|
+
// Field classes depend on the existing validation callbacks; this
|
|
67
|
+
// also waits for dynamic fields and material-mode restoration.
|
|
68
|
+
if (!fieldClasses || fieldClasses.length < config.field_keys.length ||
|
|
69
|
+
!config.field_keys.every(key => Object.prototype.hasOwnProperty.call(actual, key))) {
|
|
70
|
+
return [unchanged, unchanged];
|
|
71
|
+
}
|
|
72
|
+
const same = (left, right) => JSON.stringify(left) === JSON.stringify(right);
|
|
73
|
+
if (!Object.entries(config.expected_controls).every(([key, value]) => same(actual[key], value))) {
|
|
74
|
+
return [unchanged, unchanged];
|
|
75
|
+
}
|
|
76
|
+
const plot = (settings || {})[config.workspace] || {};
|
|
77
|
+
if (!Object.entries(config.plot_settings).every(([key, value]) => same(plot[key], value))) {
|
|
78
|
+
return [unchanged, unchanged];
|
|
79
|
+
}
|
|
80
|
+
// Both outputs update together. Later edits and callback updates
|
|
81
|
+
// see the started flag and cannot launch a second automatic run.
|
|
82
|
+
return [1, Object.assign({}, config, {started: true})];
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
});
|
|
@@ -14,6 +14,7 @@ from PyMieSimX.gui.pages.sellmeier import build_sellmeier_page
|
|
|
14
14
|
from PyMieSimX.gui.pages.field_syntax import build_field_syntax_page
|
|
15
15
|
from PyMieSimX.gui.pages.citation import build_citation_page
|
|
16
16
|
from PyMieSimX.gui.pages.home import build_home_page
|
|
17
|
+
from PyMieSimX.gui.pages.examples import build_examples_page
|
|
17
18
|
from PyMieSimX.gui.pages.install_local import build_install_local_page
|
|
18
19
|
from PyMieSimX.gui.pages.experiment import build_experiment_page
|
|
19
20
|
from PyMieSimX.gui.pages.settings import build_settings_page
|
|
@@ -182,6 +183,7 @@ def register_callbacks(app: Dash, default_measure_options: list[str]) -> None:
|
|
|
182
183
|
Output("sidebar-link-single", "className"),
|
|
183
184
|
Output("sidebar-link-documentation", "className"),
|
|
184
185
|
Output("sidebar-link-settings", "className"),
|
|
186
|
+
Output("sidebar-link-examples", "className"),
|
|
185
187
|
Output("sidebar-link-population", "className"),
|
|
186
188
|
Output("home-visit-count", "data"),
|
|
187
189
|
Input("url", "pathname"),
|
|
@@ -216,6 +218,7 @@ def register_callbacks(app: Dash, default_measure_options: list[str]) -> None:
|
|
|
216
218
|
"single": "sidebar-link",
|
|
217
219
|
"documentation": "sidebar-link",
|
|
218
220
|
"settings": "sidebar-link",
|
|
221
|
+
"examples": "sidebar-link",
|
|
219
222
|
"population": "sidebar-link",
|
|
220
223
|
}
|
|
221
224
|
home_visits = _counter(home_visits)
|
|
@@ -228,36 +231,39 @@ def register_callbacks(app: Dash, default_measure_options: list[str]) -> None:
|
|
|
228
231
|
home_visits = float("nan")
|
|
229
232
|
if route == "/admin":
|
|
230
233
|
token = parse_qs((search or "").lstrip("?")).get("token", [None])[0]
|
|
231
|
-
return build_page_with_footer(build_admin_page(token)), *(active[key] for key in ("home", "experiment", "single", "documentation", "settings", "population")), home_visits
|
|
234
|
+
return build_page_with_footer(build_admin_page(token)), *(active[key] for key in ("home", "experiment", "single", "documentation", "settings", "examples", "population")), home_visits
|
|
235
|
+
if route == "/examples":
|
|
236
|
+
active["examples"] += " active"
|
|
237
|
+
return build_page_with_footer(build_examples_page()), *(active[key] for key in ("home", "experiment", "single", "documentation", "settings", "examples", "population")), home_visits
|
|
232
238
|
if route == "/documentation":
|
|
233
239
|
active["documentation"] += " active"
|
|
234
|
-
return build_page_with_footer(build_documentation_page()), *(active[key] for key in ("home", "experiment", "single", "documentation", "settings", "population")), home_visits
|
|
240
|
+
return build_page_with_footer(build_documentation_page()), *(active[key] for key in ("home", "experiment", "single", "documentation", "settings", "examples", "population")), home_visits
|
|
235
241
|
if route == "/citation":
|
|
236
242
|
active["home"] += " active"
|
|
237
|
-
return build_page_with_footer(build_citation_page()), *(active[key] for key in ("home", "experiment", "single", "documentation", "settings", "population")), home_visits
|
|
243
|
+
return build_page_with_footer(build_citation_page()), *(active[key] for key in ("home", "experiment", "single", "documentation", "settings", "examples", "population")), home_visits
|
|
238
244
|
if route == "/documentation/install-local":
|
|
239
245
|
active["documentation"] += " active"
|
|
240
|
-
return build_page_with_footer(build_install_local_page()), *(active[key] for key in ("home", "experiment", "single", "documentation", "settings", "population")), home_visits
|
|
246
|
+
return build_page_with_footer(build_install_local_page()), *(active[key] for key in ("home", "experiment", "single", "documentation", "settings", "examples", "population")), home_visits
|
|
241
247
|
if route == "/documentation/sellmeier":
|
|
242
248
|
active["documentation"] += " active"
|
|
243
|
-
return build_page_with_footer(build_sellmeier_page()), *(active[key] for key in ("home", "experiment", "single", "documentation", "settings", "population")), home_visits
|
|
249
|
+
return build_page_with_footer(build_sellmeier_page()), *(active[key] for key in ("home", "experiment", "single", "documentation", "settings", "examples", "population")), home_visits
|
|
244
250
|
if route == "/documentation/field-syntax":
|
|
245
251
|
active["documentation"] += " active"
|
|
246
|
-
return build_page_with_footer(build_field_syntax_page()), *(active[key] for key in ("home", "experiment", "single", "documentation", "settings", "population")), home_visits
|
|
252
|
+
return build_page_with_footer(build_field_syntax_page()), *(active[key] for key in ("home", "experiment", "single", "documentation", "settings", "examples", "population")), home_visits
|
|
247
253
|
if route == "/settings":
|
|
248
254
|
active["settings"] += " active"
|
|
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
|
|
255
|
+
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", "examples", "population")), home_visits
|
|
250
256
|
if route == "/single":
|
|
251
257
|
active["single"] += " active"
|
|
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
|
|
258
|
+
return simulation_page(build_single_page((plot_settings or {}).get("particle_explorer", {}), setup)), *(active[key] for key in ("home", "experiment", "single", "documentation", "settings", "examples", "population")), home_visits
|
|
253
259
|
if route == "/population":
|
|
254
260
|
active["population"] += " active"
|
|
255
|
-
return simulation_page(build_population_page()), *(active[key] for key in ("home", "experiment", "single", "documentation", "settings", "population")), home_visits
|
|
261
|
+
return simulation_page(build_population_page()), *(active[key] for key in ("home", "experiment", "single", "documentation", "settings", "examples", "population")), home_visits
|
|
256
262
|
if route == "/experiment":
|
|
257
263
|
active["experiment"] += " active"
|
|
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
|
|
264
|
+
return simulation_page(build_experiment_page(default_measure_options, plot_settings or {}, setup)), *(active[key] for key in ("home", "experiment", "single", "documentation", "settings", "examples", "population")), home_visits
|
|
259
265
|
active["home"] += " active"
|
|
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
|
|
266
|
+
return build_page_with_footer(build_home_page(home_visits=home_visits)), *(active[key] for key in ("home", "experiment", "single", "documentation", "settings", "examples", "population")), home_visits
|
|
261
267
|
|
|
262
268
|
@app.callback(
|
|
263
269
|
Output("admin-home-page-visits", "children"),
|
|
@@ -310,6 +310,7 @@ def _build_sidebar():
|
|
|
310
310
|
_sidebar_link("Single Scatterer", "/single"),
|
|
311
311
|
_sidebar_link("Parameter Scan", "/experiment"),
|
|
312
312
|
_sidebar_link("Ensemble Optics", "/population"),
|
|
313
|
+
_sidebar_link("Examples", "/examples"),
|
|
313
314
|
_sidebar_link("Documentation", "/documentation"),
|
|
314
315
|
_sidebar_link("Settings", "/settings"),
|
|
315
316
|
],
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
"""Curated examples that open reproducible, automatically run simulations."""
|
|
2
|
+
|
|
3
|
+
from urllib.parse import urlencode
|
|
4
|
+
|
|
5
|
+
from dash import html
|
|
6
|
+
|
|
7
|
+
from PyMieSimX.gui.components import Card
|
|
8
|
+
from PyMieSimX.gui.defaults import DEFAULT_PLOT_SETTINGS
|
|
9
|
+
from PyMieSimX.gui.schemas import SECTION_FIELDS, SINGLE_SOURCE_FIELDS, SINGLE_SCATTERER_FIELDS
|
|
10
|
+
from PyMieSimX.gui.sharing import control_key, encode_simulation
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
GOLD = "main/Au/Olmon-ev"
|
|
14
|
+
SILVER = "main/Ag/Johnson"
|
|
15
|
+
SILICA = "main/SiO2/Malitson"
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def _fields(section, specs, values):
|
|
19
|
+
return {control_key({"kind": "field", "section": section, "name": spec.name}):
|
|
20
|
+
values.get(spec.name, spec.default) for spec in specs}
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def _sweep(wavelength, particles, *, model="SphereSet", axis="wavelength", measures=None):
|
|
24
|
+
controls = {
|
|
25
|
+
"source-type": "PlaneWaveSet", "scatterer-type": model, "detector-type": "None",
|
|
26
|
+
"measure-select": measures or ["Qsca", "Qabs", "Qext"],
|
|
27
|
+
"x-axis-select": axis, "plot-experiment-projection": "cartesian",
|
|
28
|
+
**_fields("source", SECTION_FIELDS["source"]["PlaneWaveSet"], {"wavelength": wavelength}),
|
|
29
|
+
**_fields("scatterer", SECTION_FIELDS["scatterer"][model], particles),
|
|
30
|
+
}
|
|
31
|
+
return {"v": 1, "page": "/experiment", "controls": controls, "plot_settings": dict(DEFAULT_PLOT_SETTINGS)}
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def _angular():
|
|
35
|
+
controls = {
|
|
36
|
+
"single-source-type": "PlaneWave", "single-scatterer-type": "Sphere",
|
|
37
|
+
"single-representation": "s1s2", "single-projection": "polar_1d", "single-sampling": "180",
|
|
38
|
+
"single-nearfield-mode": ["absolute"], "single-include-incident-field": ["include"],
|
|
39
|
+
**_fields("single-source", SINGLE_SOURCE_FIELDS["PlaneWave"], {"wavelength": "633"}),
|
|
40
|
+
**_fields("single-scatterer", SINGLE_SCATTERER_FIELDS["Sphere"], {
|
|
41
|
+
"diameter": "1000", "material": "organic/polystyrene/Sultanova", "medium": "1.33"}),
|
|
42
|
+
}
|
|
43
|
+
return {"v": 1, "page": "/single", "controls": controls, "plot_settings": dict(DEFAULT_PLOT_SETTINGS)}
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def _suspension():
|
|
47
|
+
controls = {f"population-{name}": value for name, value in {
|
|
48
|
+
"wavelength": "633", "distribution": "lognormal", "diameter": "500", "width": "1.25",
|
|
49
|
+
"minimum": "100", "maximum": "1500", "sampling": "96", "concentration-basis": "number",
|
|
50
|
+
"concentration": "1e8",
|
|
51
|
+
}.items()}
|
|
52
|
+
controls.update({control_key({"kind": "field", "section": "population", "name": name}): value
|
|
53
|
+
for name, value in {"material": SILICA, "medium": "1.33"}.items()})
|
|
54
|
+
return {"v": 1, "page": "/population", "controls": controls, "plot_settings": {}}
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
EXAMPLES = (
|
|
58
|
+
{
|
|
59
|
+
"id": "gold-resonance", "title": "Gold nanoparticle resonances", "topic": "Plasmonics", "color": "yellow",
|
|
60
|
+
"description": "Compare the scattering, absorption, and extinction spectra of a gold nanosphere in water.",
|
|
61
|
+
"observe": "Find the extinction peak and compare the contributions of scattering and absorption.",
|
|
62
|
+
"parameters": [("Particle", "80 nm gold sphere"), ("Wavelength", "400–850 nm · 181 points"), ("Medium", "n = 1.33")],
|
|
63
|
+
"setup": _sweep("400:850:181", {"diameter": "80", "material": GOLD, "medium": "1.33"}),
|
|
64
|
+
"reference": ("Gold optical data · Olmon", "https://refractiveindex.info/?shelf=main&book=Au&page=Olmon-ev"),
|
|
65
|
+
},
|
|
66
|
+
{
|
|
67
|
+
"id": "silver-resonance", "title": "Silver nanoparticle resonances", "topic": "Plasmonics", "color": "cyan",
|
|
68
|
+
"description": "Explore the wavelength-dependent response of a silver nanosphere immersed in water.",
|
|
69
|
+
"observe": "Locate the resonance and compare its spectral shape with the gold example.",
|
|
70
|
+
"parameters": [("Particle", "60 nm silver sphere"), ("Wavelength", "300–650 nm · 141 points"), ("Medium", "n = 1.33")],
|
|
71
|
+
"setup": _sweep("300:650:141", {"diameter": "60", "material": SILVER, "medium": "1.33"}),
|
|
72
|
+
"reference": ("Silver optical data · Johnson & Christy", "https://refractiveindex.info/?shelf=main&book=Ag&page=Johnson"),
|
|
73
|
+
},
|
|
74
|
+
{
|
|
75
|
+
"id": "silica-size", "title": "From Rayleigh to Mie scattering", "topic": "Particle size", "color": "blue",
|
|
76
|
+
"description": "Sweep the diameter of a silica sphere at a fixed wavelength to explore how scattering changes with size.",
|
|
77
|
+
"observe": "Follow the transition from weak small-particle scattering to the oscillations of larger spheres.",
|
|
78
|
+
"parameters": [("Particle", "Silica · 30–1600 nm diameter"), ("Wavelength", "633 nm"), ("Medium", "n = 1.00")],
|
|
79
|
+
"setup": _sweep("633", {"diameter": "30:1600:160", "material": SILICA, "medium": "1.0"},
|
|
80
|
+
axis="diameter", measures=["Qsca", "Qext"]),
|
|
81
|
+
"reference": ("Silica optical data · Malitson", "https://refractiveindex.info/?shelf=main&book=SiO2&page=Malitson"),
|
|
82
|
+
},
|
|
83
|
+
{
|
|
84
|
+
"id": "gold-nanoshell", "title": "Tune a gold nanoshell", "topic": "Core–shell particles", "color": "purple",
|
|
85
|
+
"description": "Compare three gold shell thicknesses around the same silica core across a wavelength sweep.",
|
|
86
|
+
"observe": "Compare the extinction peaks as the shell thickness changes while the core diameter stays fixed.",
|
|
87
|
+
"parameters": [("Core", "80 nm silica diameter"), ("Gold shell", "5, 15, and 30 nm thick"), ("Wavelength", "450–1100 nm · n = 1.33")],
|
|
88
|
+
"setup": _sweep("450:1100:161", {"core_diameter": "80", "shell_thickness": "5,15,30",
|
|
89
|
+
"core_material": SILICA, "shell_material": GOLD, "medium": "1.33"}, model="CoreShellSet"),
|
|
90
|
+
"reference": ("Gold optical data · Olmon", "https://refractiveindex.info/?shelf=main&book=Au&page=Olmon-ev"),
|
|
91
|
+
},
|
|
92
|
+
{
|
|
93
|
+
"id": "bead-angular", "title": "Where does a bead scatter light?", "topic": "Angular scattering", "color": "green",
|
|
94
|
+
"description": "Inspect the two angular scattering amplitudes of a micrometre-sized polystyrene bead in water.",
|
|
95
|
+
"observe": "Compare |S1| and |S2| in the polar plot to explore angular lobes and polarization dependence.",
|
|
96
|
+
"parameters": [("Particle", "1 µm polystyrene bead"), ("Wavelength", "633 nm · linear polarization"), ("View", "Polar S1 / S2 amplitudes · n = 1.33")],
|
|
97
|
+
"setup": _angular(),
|
|
98
|
+
"reference": ("Polystyrene optical data · Sultanova", "https://refractiveindex.info/?shelf=organic&book=polystyrene&page=Sultanova"),
|
|
99
|
+
},
|
|
100
|
+
{
|
|
101
|
+
"id": "silica-suspension", "title": "Optics of a silica suspension", "topic": "Ensemble optics", "color": "blue",
|
|
102
|
+
"description": "Combine a lognormal size distribution and concentration to calculate the optical properties of a dilute suspension.",
|
|
103
|
+
"observe": "Inspect scattering and absorption coefficients, albedo, and the scattering-weighted anisotropy.",
|
|
104
|
+
"parameters": [("Distribution", "500 nm median · GSD 1.25"), ("Concentration", "10⁸ particles/mL"), ("Illumination", "633 nm · n = 1.33")],
|
|
105
|
+
"setup": _suspension(),
|
|
106
|
+
"reference": ("Silica optical data · Malitson", "https://refractiveindex.info/?shelf=main&book=SiO2&page=Malitson"),
|
|
107
|
+
},
|
|
108
|
+
)
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
def example_link(example):
|
|
112
|
+
"""Build a portable link using the same format as user-shared simulations."""
|
|
113
|
+
setup = example["setup"]
|
|
114
|
+
return setup["page"] + "?" + urlencode({"simulation": encode_simulation(setup)})
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
def build_examples_page():
|
|
118
|
+
"""Show six independent starting points without computing on page load."""
|
|
119
|
+
return html.Div(className="page-content-stack examples-page", children=[
|
|
120
|
+
html.Section(className="page-hero", children=[
|
|
121
|
+
html.P("Start with a physical question", className="eyebrow"),
|
|
122
|
+
html.H1("Examples"),
|
|
123
|
+
html.P("Six ready-to-run simulations. Open an example to restore its setup, run it automatically, and explore the results.", className="hero-text"),
|
|
124
|
+
]),
|
|
125
|
+
html.Div(className="examples-grid", children=[_example_card(example, index) for index, example in enumerate(EXAMPLES, 1)]),
|
|
126
|
+
])
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
def _example_card(example, index):
|
|
130
|
+
workspace = {"/experiment": "Parameter Scan", "/single": "Single Scatterer", "/population": "Ensemble Optics"}[example["setup"]["page"]]
|
|
131
|
+
reference_label, reference_url = example["reference"]
|
|
132
|
+
return html.Article(id=f"example-{example['id']}", className=Card.classes(color=example["color"], extra="example-card"), children=[
|
|
133
|
+
html.Div(className="example-card-heading", children=[html.Span(f"{index:02d}", className="example-number"), html.Span(example["topic"], className="example-topic")]),
|
|
134
|
+
html.H2(example["title"]),
|
|
135
|
+
html.P(example["description"], className="example-description"),
|
|
136
|
+
html.Dl(className="example-parameters", children=[html.Div([html.Dt(label), html.Dd(value)]) for label, value in example["parameters"]]),
|
|
137
|
+
html.Div(className="example-observe", children=[html.Strong("What to look for"), html.P(example["observe"])]),
|
|
138
|
+
html.A(reference_label, href=reference_url, target="_blank", rel="noopener noreferrer", className="example-reference"),
|
|
139
|
+
html.Div(className="example-card-actions", children=[html.Span(workspace), html.A("Open simulation & results →", href=example_link(example), className="example-open toolbar-button toolbar-button-primary")]),
|
|
140
|
+
])
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
|
|
4
4
|
from dash import dcc, html
|
|
5
5
|
|
|
6
|
-
from PyMieSimX.gui.sharing import sharing_controls
|
|
6
|
+
from PyMieSimX.gui.sharing import sharing_controls
|
|
7
7
|
from PyMieSimX.gui.components.cards import workspace_header
|
|
8
8
|
|
|
9
9
|
from PyMieSimX.gui.callback_helpers import status_banner
|
|
@@ -56,7 +56,6 @@ def build_experiment_page(default_measure_options: list[str], plot_settings: dic
|
|
|
56
56
|
className="experiment-run-feedback",
|
|
57
57
|
children=status_banner("idle", "Ready. Click Run to start a sweep."),
|
|
58
58
|
),
|
|
59
|
-
sharing_feedback(),
|
|
60
59
|
_x_axis_card(default_measure_options),
|
|
61
60
|
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))]),
|
|
62
61
|
],
|
|
@@ -5,7 +5,7 @@ import logging
|
|
|
5
5
|
from dash import Input, Output, State, dcc, html, no_update
|
|
6
6
|
from dash.exceptions import PreventUpdate
|
|
7
7
|
|
|
8
|
-
from PyMieSimX.gui.sharing import sharing_controls,
|
|
8
|
+
from PyMieSimX.gui.sharing import sharing_controls, shared_setup
|
|
9
9
|
import plotly.graph_objects as go
|
|
10
10
|
import numpy as np
|
|
11
11
|
|
|
@@ -104,7 +104,6 @@ def build_population_page():
|
|
|
104
104
|
id="population-export", disabled=True, className="toolbar-button toolbar-button-secondary"),
|
|
105
105
|
sharing_controls("/population"),
|
|
106
106
|
]),
|
|
107
|
-
sharing_feedback(),
|
|
108
107
|
html.Div(id="population-status", role="status", children="Ready. Click Run to calculate all properties."),
|
|
109
108
|
_panel("Distribution preview", [
|
|
110
109
|
dcc.Graph(id="population-preview", config={"displaylogo": False}),
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
from dash import dcc, html
|
|
4
4
|
|
|
5
|
-
from PyMieSimX.gui.sharing import sharing_controls
|
|
5
|
+
from PyMieSimX.gui.sharing import sharing_controls
|
|
6
6
|
from PyMieSimX.gui.components.cards import workspace_header
|
|
7
7
|
|
|
8
8
|
from PyMieSimX.gui.defaults import DEFAULT_WORKSPACE_SETTINGS
|
|
@@ -47,7 +47,6 @@ def build_single_page(plot_settings: dict | None = None, simulation: dict | None
|
|
|
47
47
|
sharing_controls("/single"),
|
|
48
48
|
],
|
|
49
49
|
),
|
|
50
|
-
sharing_feedback(),
|
|
51
50
|
build_representation_section(),
|
|
52
51
|
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))]),
|
|
53
52
|
],
|
|
@@ -8,6 +8,10 @@ from flask import has_request_context, request
|
|
|
8
8
|
|
|
9
9
|
|
|
10
10
|
PAGE_METADATA = {
|
|
11
|
+
"/examples": {
|
|
12
|
+
"title": "Mie Scattering Examples — Nanoparticle Resonances & More | PyMieSimX",
|
|
13
|
+
"description": "Explore six Mie scattering examples: gold and silver nanoparticle resonances, silica size sweeps, gold nanoshells, angular scattering, and suspension optics.",
|
|
14
|
+
},
|
|
11
15
|
"/": {
|
|
12
16
|
"title": "Mie Scattering Calculator & Mie Theory Simulator | PyMieSimX",
|
|
13
17
|
"description": "Calculate Mie scattering in your browser with PyMieSimX. Explore single particles, run parameter sweeps, and export results with the PyMieSim solver.",
|
|
@@ -254,17 +254,18 @@ def restore_controls(component, setup):
|
|
|
254
254
|
def sharing_controls(path):
|
|
255
255
|
return html.Div(className="simulation-share-actions", children=[
|
|
256
256
|
dcc.Store(id=f"{path.strip('/')}-auto-run", data=None),
|
|
257
|
-
dcc.
|
|
258
|
-
|
|
259
|
-
html.Button(
|
|
260
|
-
|
|
257
|
+
dcc.Store(id="simulation-link"),
|
|
258
|
+
dcc.Store(id="simulation-copy-status"),
|
|
259
|
+
html.Button([
|
|
260
|
+
html.Span("Copy simulation link", className="simulation-copy-label"),
|
|
261
|
+
html.Span("Copying…", className="simulation-copy-pending-label"),
|
|
262
|
+
html.Span("✓ Copied", className="simulation-copy-success-label"),
|
|
263
|
+
html.Span("Unable to copy", className="simulation-copy-error-label"),
|
|
264
|
+
], id="simulation-clipboard", n_clicks=0, title="Copy a link to this simulation setup",
|
|
265
|
+
className="toolbar-button toolbar-button-secondary simulation-copy-button", **{"aria-live": "polite"}),
|
|
261
266
|
])
|
|
262
267
|
|
|
263
268
|
|
|
264
|
-
def sharing_feedback():
|
|
265
|
-
return html.Div(id="simulation-share-feedback", role="status", className="simulation-share-feedback")
|
|
266
|
-
|
|
267
|
-
|
|
268
269
|
def register_sharing_callbacks(app):
|
|
269
270
|
from dash import ClientsideFunction
|
|
270
271
|
for path, button_id in RUN_BUTTONS.items():
|
|
@@ -281,11 +282,14 @@ def register_sharing_callbacks(app):
|
|
|
281
282
|
State(button_id, "n_clicks"),
|
|
282
283
|
)
|
|
283
284
|
app.clientside_callback(
|
|
284
|
-
"""
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
285
|
+
ClientsideFunction(namespace="simulation_sharing", function_name="copy_link"),
|
|
286
|
+
Output("simulation-copy-status", "data"), Input("simulation-link", "data"),
|
|
287
|
+
prevent_initial_call=True,
|
|
288
|
+
)
|
|
289
|
+
app.clientside_callback(
|
|
290
|
+
ClientsideFunction(namespace="simulation_sharing", function_name="copy_feedback"),
|
|
291
|
+
Output("simulation-clipboard", "className"), Output("simulation-clipboard", "title"),
|
|
292
|
+
Input("simulation-clipboard", "n_clicks"), Input("simulation-copy-status", "data"),
|
|
289
293
|
)
|
|
290
294
|
@app.callback(
|
|
291
295
|
Output("experiment-result", "clear_data"), Output("single-result", "clear_data"),
|
|
@@ -296,18 +300,16 @@ def register_sharing_callbacks(app):
|
|
|
296
300
|
return clear, clear
|
|
297
301
|
|
|
298
302
|
@app.callback(
|
|
299
|
-
Output("simulation-
|
|
300
|
-
Output("simulation-share-feedback", "children"),
|
|
303
|
+
Output("simulation-link", "data"),
|
|
301
304
|
Input("simulation-clipboard", "n_clicks"),
|
|
302
|
-
Input("simulation-link-fallback", "n_clicks"),
|
|
303
305
|
State("url", "href"), State("url", "pathname"), State("plot-settings-store", "data"),
|
|
304
306
|
State({"kind": "field", "section": ALL, "name": ALL}, "id"),
|
|
305
307
|
State({"kind": "field", "section": ALL, "name": ALL}, "value"),
|
|
306
308
|
*[State(name, "value", allow_optional=True) for name in CAPTURE_CONTROLS],
|
|
307
309
|
prevent_initial_call=True,
|
|
308
310
|
)
|
|
309
|
-
def _copy_simulation_link(clicks,
|
|
310
|
-
if not
|
|
311
|
+
def _copy_simulation_link(clicks, href, path, settings, field_ids, field_values, *values):
|
|
312
|
+
if not clicks or path not in WORKSPACES:
|
|
311
313
|
raise PreventUpdate
|
|
312
314
|
allowed = allowed_controls(path)
|
|
313
315
|
controls = {name: value for name, value in zip(CAPTURE_CONTROLS, values) if name in allowed and value is not None}
|
|
@@ -321,12 +323,8 @@ def register_sharing_callbacks(app):
|
|
|
321
323
|
payload = {"v": 1, "page": path, "controls": controls, "plot_settings": plot}
|
|
322
324
|
try:
|
|
323
325
|
token = encode_simulation(payload)
|
|
324
|
-
except ValueError
|
|
325
|
-
return "", "
|
|
326
|
+
except ValueError:
|
|
327
|
+
return {"clicks": clicks, "url": None}
|
|
326
328
|
parts = urlsplit(href)
|
|
327
329
|
link = urlunsplit((parts.scheme, parts.netloc, path, urlencode({"simulation": token}), ""))
|
|
328
|
-
|
|
329
|
-
if fallback_clicks and not clicks:
|
|
330
|
-
feedback = [html.Label("Simulation link", htmlFor="simulation-share-link"),
|
|
331
|
-
dcc.Input(id="simulation-share-link", value=link, readOnly=True, type="text", className="field-input")]
|
|
332
|
-
return link, "Link copied", feedback
|
|
330
|
+
return {"clicks": clicks, "url": link}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: PyMieSimX
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.3.0
|
|
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,15 @@ 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
|
+
Examples
|
|
54
|
+
--------
|
|
55
|
+
|
|
56
|
+
The **Examples** page provides six starting points: gold and silver nanoparticle
|
|
57
|
+
resonances, a silica size sweep, gold nanoshells, a bead's angular scattering,
|
|
58
|
+
and a polydisperse silica suspension. Each card links to its simulation
|
|
59
|
+
workspace, restores the complete setup, and runs automatically to show results.
|
|
60
|
+
The parameters can then be adjusted and the results exported as CSV.
|
|
61
|
+
|
|
53
62
|
Sharing simulations
|
|
54
63
|
-------------------
|
|
55
64
|
|
|
@@ -58,8 +67,8 @@ Optics to share the current setup. Links preserve the input values, sweep
|
|
|
58
67
|
ranges, selected measures, and plot settings without requiring an account.
|
|
59
68
|
Opening a link restores the setup and automatically runs the simulation once
|
|
60
69
|
the inputs and plot settings are ready. Later edits can be calculated with
|
|
61
|
-
**Run**.
|
|
62
|
-
|
|
70
|
+
**Run**. Copying puts the link directly on the clipboard and briefly shows
|
|
71
|
+
**Copied** on the button without displaying an additional card.
|
|
63
72
|
|
|
64
73
|
Links store the configuration rather than a result snapshot. Recalculation
|
|
65
74
|
uses the installed solver and optical material data, so results can change
|
|
@@ -67,6 +67,7 @@ PyMieSimX/gui/components/cards.py
|
|
|
67
67
|
PyMieSimX/gui/pages/__init__.py
|
|
68
68
|
PyMieSimX/gui/pages/citation.py
|
|
69
69
|
PyMieSimX/gui/pages/documentation.py
|
|
70
|
+
PyMieSimX/gui/pages/examples.py
|
|
70
71
|
PyMieSimX/gui/pages/field_syntax.py
|
|
71
72
|
PyMieSimX/gui/pages/home.py
|
|
72
73
|
PyMieSimX/gui/pages/install_local.py
|
|
@@ -88,6 +89,7 @@ tests/gui/cli.py
|
|
|
88
89
|
tests/gui/test_admin.py
|
|
89
90
|
tests/gui/test_api.py
|
|
90
91
|
tests/gui/test_dash_callbacks.py
|
|
92
|
+
tests/gui/test_examples.py
|
|
91
93
|
tests/gui/test_home.py
|
|
92
94
|
tests/gui/test_population.py
|
|
93
95
|
tests/gui/test_seo.py
|
|
@@ -47,6 +47,7 @@
|
|
|
47
47
|
"PyMieSimX/gui/pages/__init__.py",
|
|
48
48
|
"PyMieSimX/gui/pages/citation.py",
|
|
49
49
|
"PyMieSimX/gui/pages/documentation.py",
|
|
50
|
+
"PyMieSimX/gui/pages/examples.py",
|
|
50
51
|
"PyMieSimX/gui/pages/experiment/__init__.py",
|
|
51
52
|
"PyMieSimX/gui/pages/experiment/page.py",
|
|
52
53
|
"PyMieSimX/gui/pages/experiment/sections/__init__.py",
|
|
@@ -82,6 +83,7 @@
|
|
|
82
83
|
"tests/gui/test_admin.py",
|
|
83
84
|
"tests/gui/test_api.py",
|
|
84
85
|
"tests/gui/test_dash_callbacks.py",
|
|
86
|
+
"tests/gui/test_examples.py",
|
|
85
87
|
"tests/gui/test_home.py",
|
|
86
88
|
"tests/gui/test_population.py",
|
|
87
89
|
"tests/gui/test_seo.py",
|