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.

Files changed (105) hide show
  1. {pymiesimx-0.2.2 → pymiesimx-0.3.0}/PKG-INFO +12 -3
  2. {pymiesimx-0.2.2 → pymiesimx-0.3.0}/PyMieSimX/gui/assets/dashboard.css +37 -3
  3. pymiesimx-0.3.0/PyMieSimX/gui/assets/simulation-sharing.js +85 -0
  4. {pymiesimx-0.2.2 → pymiesimx-0.3.0}/PyMieSimX/gui/callbacks.py +17 -11
  5. {pymiesimx-0.2.2 → pymiesimx-0.3.0}/PyMieSimX/gui/layout.py +1 -0
  6. pymiesimx-0.3.0/PyMieSimX/gui/pages/examples.py +140 -0
  7. {pymiesimx-0.2.2 → pymiesimx-0.3.0}/PyMieSimX/gui/pages/experiment/page.py +1 -2
  8. {pymiesimx-0.2.2 → pymiesimx-0.3.0}/PyMieSimX/gui/pages/population.py +1 -2
  9. {pymiesimx-0.2.2 → pymiesimx-0.3.0}/PyMieSimX/gui/pages/single/page.py +1 -2
  10. {pymiesimx-0.2.2 → pymiesimx-0.3.0}/PyMieSimX/gui/seo.py +4 -0
  11. {pymiesimx-0.2.2 → pymiesimx-0.3.0}/PyMieSimX/gui/sharing.py +23 -25
  12. {pymiesimx-0.2.2 → pymiesimx-0.3.0}/PyMieSimX.egg-info/PKG-INFO +12 -3
  13. {pymiesimx-0.2.2 → pymiesimx-0.3.0}/PyMieSimX.egg-info/SOURCES.txt +2 -0
  14. {pymiesimx-0.2.2 → pymiesimx-0.3.0}/PyMieSimX.egg-info/scm_file_list.json +2 -0
  15. {pymiesimx-0.2.2 → pymiesimx-0.3.0}/PyMieSimX.egg-info/scm_version.json +2 -2
  16. {pymiesimx-0.2.2 → pymiesimx-0.3.0}/README.rst +11 -2
  17. {pymiesimx-0.2.2 → pymiesimx-0.3.0}/tests/browser/test_dashboard.py +74 -12
  18. pymiesimx-0.3.0/tests/gui/test_examples.py +78 -0
  19. {pymiesimx-0.2.2 → pymiesimx-0.3.0}/tests/gui/test_sharing.py +8 -14
  20. pymiesimx-0.2.2/PyMieSimX/gui/assets/simulation-sharing.js +0 -36
  21. {pymiesimx-0.2.2 → pymiesimx-0.3.0}/.coveragerc +0 -0
  22. {pymiesimx-0.2.2 → pymiesimx-0.3.0}/.flake8 +0 -0
  23. {pymiesimx-0.2.2 → pymiesimx-0.3.0}/.github/dependabot.yml +0 -0
  24. {pymiesimx-0.2.2 → pymiesimx-0.3.0}/.github/workflows/deploy_PyPi.yml +0 -0
  25. {pymiesimx-0.2.2 → pymiesimx-0.3.0}/.github/workflows/deploy_anaconda.yml +0 -0
  26. {pymiesimx-0.2.2 → pymiesimx-0.3.0}/.github/workflows/deploy_coverage.yml +0 -0
  27. {pymiesimx-0.2.2 → pymiesimx-0.3.0}/.github/workflows/quality.yml +0 -0
  28. {pymiesimx-0.2.2 → pymiesimx-0.3.0}/.github/workflows/tests.yml +0 -0
  29. {pymiesimx-0.2.2 → pymiesimx-0.3.0}/.gitignore +0 -0
  30. {pymiesimx-0.2.2 → pymiesimx-0.3.0}/.pre-commit-config.yaml +0 -0
  31. {pymiesimx-0.2.2 → pymiesimx-0.3.0}/AGENTS.md +0 -0
  32. {pymiesimx-0.2.2 → pymiesimx-0.3.0}/CONTRIBUTING.md +0 -0
  33. {pymiesimx-0.2.2 → pymiesimx-0.3.0}/Dockerfile +0 -0
  34. {pymiesimx-0.2.2 → pymiesimx-0.3.0}/LICENSE +0 -0
  35. {pymiesimx-0.2.2 → pymiesimx-0.3.0}/PyMieSimX/__init__.py +0 -0
  36. {pymiesimx-0.2.2 → pymiesimx-0.3.0}/PyMieSimX/__main__.py +0 -0
  37. {pymiesimx-0.2.2 → pymiesimx-0.3.0}/PyMieSimX/api.py +0 -0
  38. {pymiesimx-0.2.2 → pymiesimx-0.3.0}/PyMieSimX/application/__init__.py +0 -0
  39. {pymiesimx-0.2.2 → pymiesimx-0.3.0}/PyMieSimX/application/wsgi.py +0 -0
  40. {pymiesimx-0.2.2 → pymiesimx-0.3.0}/PyMieSimX/gui/__init__.py +0 -0
  41. {pymiesimx-0.2.2 → pymiesimx-0.3.0}/PyMieSimX/gui/admin.py +0 -0
  42. {pymiesimx-0.2.2 → pymiesimx-0.3.0}/PyMieSimX/gui/assets/card-state.js +0 -0
  43. {pymiesimx-0.2.2 → pymiesimx-0.3.0}/PyMieSimX/gui/assets/default-settings.json +0 -0
  44. {pymiesimx-0.2.2 → pymiesimx-0.3.0}/PyMieSimX/gui/assets/home-spf-radial.gif +0 -0
  45. {pymiesimx-0.2.2 → pymiesimx-0.3.0}/PyMieSimX/gui/assets/home-spf-radial.png +0 -0
  46. {pymiesimx-0.2.2 → pymiesimx-0.3.0}/PyMieSimX/gui/assets/logo.png +0 -0
  47. {pymiesimx-0.2.2 → pymiesimx-0.3.0}/PyMieSimX/gui/assets/materials-stock.json +0 -0
  48. {pymiesimx-0.2.2 → pymiesimx-0.3.0}/PyMieSimX/gui/assets/pymiesim-favicon.svg +0 -0
  49. {pymiesimx-0.2.2 → pymiesimx-0.3.0}/PyMieSimX/gui/assets/pymiesim-logo-dark.svg +0 -0
  50. {pymiesimx-0.2.2 → pymiesimx-0.3.0}/PyMieSimX/gui/assets/pymiesim-logo.svg +0 -0
  51. {pymiesimx-0.2.2 → pymiesimx-0.3.0}/PyMieSimX/gui/callback_helpers.py +0 -0
  52. {pymiesimx-0.2.2 → pymiesimx-0.3.0}/PyMieSimX/gui/components/__init__.py +0 -0
  53. {pymiesimx-0.2.2 → pymiesimx-0.3.0}/PyMieSimX/gui/components/cards.py +0 -0
  54. {pymiesimx-0.2.2 → pymiesimx-0.3.0}/PyMieSimX/gui/computation.py +0 -0
  55. {pymiesimx-0.2.2 → pymiesimx-0.3.0}/PyMieSimX/gui/defaults.py +0 -0
  56. {pymiesimx-0.2.2 → pymiesimx-0.3.0}/PyMieSimX/gui/experiment_service.py +0 -0
  57. {pymiesimx-0.2.2 → pymiesimx-0.3.0}/PyMieSimX/gui/interface.py +0 -0
  58. {pymiesimx-0.2.2 → pymiesimx-0.3.0}/PyMieSimX/gui/jobs.py +0 -0
  59. {pymiesimx-0.2.2 → pymiesimx-0.3.0}/PyMieSimX/gui/material_catalog.py +0 -0
  60. {pymiesimx-0.2.2 → pymiesimx-0.3.0}/PyMieSimX/gui/models.py +0 -0
  61. {pymiesimx-0.2.2 → pymiesimx-0.3.0}/PyMieSimX/gui/pages/__init__.py +0 -0
  62. {pymiesimx-0.2.2 → pymiesimx-0.3.0}/PyMieSimX/gui/pages/citation.py +0 -0
  63. {pymiesimx-0.2.2 → pymiesimx-0.3.0}/PyMieSimX/gui/pages/documentation.py +0 -0
  64. {pymiesimx-0.2.2 → pymiesimx-0.3.0}/PyMieSimX/gui/pages/experiment/__init__.py +0 -0
  65. {pymiesimx-0.2.2 → pymiesimx-0.3.0}/PyMieSimX/gui/pages/experiment/sections/__init__.py +0 -0
  66. {pymiesimx-0.2.2 → pymiesimx-0.3.0}/PyMieSimX/gui/pages/experiment/sections/configuration.py +0 -0
  67. {pymiesimx-0.2.2 → pymiesimx-0.3.0}/PyMieSimX/gui/pages/field_syntax.py +0 -0
  68. {pymiesimx-0.2.2 → pymiesimx-0.3.0}/PyMieSimX/gui/pages/home.py +0 -0
  69. {pymiesimx-0.2.2 → pymiesimx-0.3.0}/PyMieSimX/gui/pages/install_local.py +0 -0
  70. {pymiesimx-0.2.2 → pymiesimx-0.3.0}/PyMieSimX/gui/pages/sellmeier.py +0 -0
  71. {pymiesimx-0.2.2 → pymiesimx-0.3.0}/PyMieSimX/gui/pages/settings.py +0 -0
  72. {pymiesimx-0.2.2 → pymiesimx-0.3.0}/PyMieSimX/gui/pages/single/__init__.py +0 -0
  73. {pymiesimx-0.2.2 → pymiesimx-0.3.0}/PyMieSimX/gui/pages/single/sections/__init__.py +0 -0
  74. {pymiesimx-0.2.2 → pymiesimx-0.3.0}/PyMieSimX/gui/pages/single/sections/representations.py +0 -0
  75. {pymiesimx-0.2.2 → pymiesimx-0.3.0}/PyMieSimX/gui/pages/single/sections/setup.py +0 -0
  76. {pymiesimx-0.2.2 → pymiesimx-0.3.0}/PyMieSimX/gui/parsing.py +0 -0
  77. {pymiesimx-0.2.2 → pymiesimx-0.3.0}/PyMieSimX/gui/plotting.py +0 -0
  78. {pymiesimx-0.2.2 → pymiesimx-0.3.0}/PyMieSimX/gui/population_service.py +0 -0
  79. {pymiesimx-0.2.2 → pymiesimx-0.3.0}/PyMieSimX/gui/schemas.py +0 -0
  80. {pymiesimx-0.2.2 → pymiesimx-0.3.0}/PyMieSimX/gui/services.py +0 -0
  81. {pymiesimx-0.2.2 → pymiesimx-0.3.0}/PyMieSimX/gui/single_service.py +0 -0
  82. {pymiesimx-0.2.2 → pymiesimx-0.3.0}/PyMieSimX/gui/usage_metrics.py +0 -0
  83. {pymiesimx-0.2.2 → pymiesimx-0.3.0}/PyMieSimX/gui/validation.py +0 -0
  84. {pymiesimx-0.2.2 → pymiesimx-0.3.0}/PyMieSimX.egg-info/dependency_links.txt +0 -0
  85. {pymiesimx-0.2.2 → pymiesimx-0.3.0}/PyMieSimX.egg-info/entry_points.txt +0 -0
  86. {pymiesimx-0.2.2 → pymiesimx-0.3.0}/PyMieSimX.egg-info/requires.txt +0 -0
  87. {pymiesimx-0.2.2 → pymiesimx-0.3.0}/PyMieSimX.egg-info/top_level.txt +0 -0
  88. {pymiesimx-0.2.2 → pymiesimx-0.3.0}/conda.recipe/meta.yaml +0 -0
  89. {pymiesimx-0.2.2 → pymiesimx-0.3.0}/makefile +0 -0
  90. {pymiesimx-0.2.2 → pymiesimx-0.3.0}/pyproject.toml +0 -0
  91. {pymiesimx-0.2.2 → pymiesimx-0.3.0}/pytest.ini +0 -0
  92. {pymiesimx-0.2.2 → pymiesimx-0.3.0}/setup.cfg +0 -0
  93. {pymiesimx-0.2.2 → pymiesimx-0.3.0}/tests/gui/cli.py +0 -0
  94. {pymiesimx-0.2.2 → pymiesimx-0.3.0}/tests/gui/test_admin.py +0 -0
  95. {pymiesimx-0.2.2 → pymiesimx-0.3.0}/tests/gui/test_api.py +0 -0
  96. {pymiesimx-0.2.2 → pymiesimx-0.3.0}/tests/gui/test_dash_callbacks.py +0 -0
  97. {pymiesimx-0.2.2 → pymiesimx-0.3.0}/tests/gui/test_home.py +0 -0
  98. {pymiesimx-0.2.2 → pymiesimx-0.3.0}/tests/gui/test_population.py +0 -0
  99. {pymiesimx-0.2.2 → pymiesimx-0.3.0}/tests/gui/test_seo.py +0 -0
  100. {pymiesimx-0.2.2 → pymiesimx-0.3.0}/tests/gui/test_services.py +0 -0
  101. {pymiesimx-0.2.2 → pymiesimx-0.3.0}/tests/gui/test_usage_metrics.py +0 -0
  102. {pymiesimx-0.2.2 → pymiesimx-0.3.0}/tools/generate_home_spf.py +0 -0
  103. {pymiesimx-0.2.2 → pymiesimx-0.3.0}/tools/next_release_version.py +0 -0
  104. {pymiesimx-0.2.2 → pymiesimx-0.3.0}/tools/release_tag.py +0 -0
  105. {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.2.2
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**. If clipboard access is unavailable, use **Show simulation
62
- link** and copy the displayed URL.
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-share-feedback:empty { display: none; }
4084
- .simulation-share-feedback .field-input { width: 100%; min-width: 0; }
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, sharing_feedback
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, sharing_feedback, shared_setup
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, sharing_feedback
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.Clipboard(id="simulation-clipboard", children="Copy simulation link", copied_children="Link copied",
258
- title="Copy a link to this simulation setup", className="toolbar-button toolbar-button-secondary", n_clicks=0),
259
- html.Button("Show simulation link", id="simulation-link-fallback", n_clicks=0,
260
- className="toolbar-button toolbar-button-secondary", style={"display": "none"}),
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
- """function(pathname) {
285
- return window.isSecureContext && navigator.clipboard && navigator.clipboard.writeText
286
- ? {display: 'none'} : {};
287
- }""",
288
- Output("simulation-link-fallback", "style"), Input("url", "pathname"),
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-clipboard", "content"), Output("simulation-clipboard", "copied_children"),
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, fallback_clicks, href, path, settings, field_ids, field_values, *values):
310
- if not (clicks or fallback_clicks) or path not in WORKSPACES:
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 as error:
325
- return "", "Unable to copy", str(error)
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
- feedback = []
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.2.2
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**. If clipboard access is unavailable, use **Show simulation
62
- link** and copy the displayed URL.
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",