PyMieSimX 0.1.6__tar.gz → 0.2.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.1.6 → pymiesimx-0.2.0}/.github/workflows/deploy_anaconda.yml +8 -1
- {pymiesimx-0.1.6 → pymiesimx-0.2.0}/PKG-INFO +50 -3
- {pymiesimx-0.1.6 → pymiesimx-0.2.0}/PyMieSimX/__init__.py +4 -0
- {pymiesimx-0.1.6 → pymiesimx-0.2.0}/PyMieSimX/api.py +4 -0
- {pymiesimx-0.1.6 → pymiesimx-0.2.0}/PyMieSimX/gui/assets/dashboard.css +18 -3
- {pymiesimx-0.1.6 → pymiesimx-0.2.0}/PyMieSimX/gui/callbacks.py +44 -13
- {pymiesimx-0.1.6 → pymiesimx-0.2.0}/PyMieSimX/gui/computation.py +2 -2
- {pymiesimx-0.1.6 → pymiesimx-0.2.0}/PyMieSimX/gui/interface.py +1 -1
- {pymiesimx-0.1.6 → pymiesimx-0.2.0}/PyMieSimX/gui/layout.py +5 -3
- {pymiesimx-0.1.6 → pymiesimx-0.2.0}/PyMieSimX/gui/pages/documentation.py +6 -5
- {pymiesimx-0.1.6 → pymiesimx-0.2.0}/PyMieSimX/gui/pages/experiment/__init__.py +1 -1
- {pymiesimx-0.1.6 → pymiesimx-0.2.0}/PyMieSimX/gui/pages/experiment/page.py +2 -2
- {pymiesimx-0.1.6 → pymiesimx-0.2.0}/PyMieSimX/gui/pages/field_syntax.py +1 -1
- {pymiesimx-0.1.6 → pymiesimx-0.2.0}/PyMieSimX/gui/pages/home.py +53 -11
- pymiesimx-0.2.0/PyMieSimX/gui/pages/population.py +221 -0
- {pymiesimx-0.1.6 → pymiesimx-0.2.0}/PyMieSimX/gui/pages/sellmeier.py +1 -1
- {pymiesimx-0.1.6 → pymiesimx-0.2.0}/PyMieSimX/gui/pages/settings.py +2 -2
- {pymiesimx-0.1.6 → pymiesimx-0.2.0}/PyMieSimX/gui/parsing.py +11 -0
- pymiesimx-0.2.0/PyMieSimX/gui/population_service.py +186 -0
- {pymiesimx-0.1.6 → pymiesimx-0.2.0}/PyMieSimX/gui/schemas.py +6 -0
- {pymiesimx-0.1.6 → pymiesimx-0.2.0}/PyMieSimX.egg-info/PKG-INFO +50 -3
- {pymiesimx-0.1.6 → pymiesimx-0.2.0}/PyMieSimX.egg-info/SOURCES.txt +5 -1
- {pymiesimx-0.1.6 → pymiesimx-0.2.0}/PyMieSimX.egg-info/requires.txt +1 -1
- {pymiesimx-0.1.6 → pymiesimx-0.2.0}/PyMieSimX.egg-info/scm_file_list.json +5 -1
- pymiesimx-0.2.0/PyMieSimX.egg-info/scm_version.json +8 -0
- {pymiesimx-0.1.6 → pymiesimx-0.2.0}/README.rst +48 -1
- {pymiesimx-0.1.6 → pymiesimx-0.2.0}/conda.recipe/meta.yaml +4 -1
- {pymiesimx-0.1.6 → pymiesimx-0.2.0}/pyproject.toml +1 -1
- pymiesimx-0.2.0/tests/browser/test_dashboard.py +364 -0
- pymiesimx-0.2.0/tests/gui/test_population.py +226 -0
- pymiesimx-0.2.0/tools/verify_conda_artifact.py +28 -0
- pymiesimx-0.1.6/PyMieSimX.egg-info/scm_version.json +0 -8
- pymiesimx-0.1.6/tests/browser/test_dashboard.py +0 -132
- {pymiesimx-0.1.6 → pymiesimx-0.2.0}/.coveragerc +0 -0
- {pymiesimx-0.1.6 → pymiesimx-0.2.0}/.flake8 +0 -0
- {pymiesimx-0.1.6 → pymiesimx-0.2.0}/.github/dependabot.yml +0 -0
- {pymiesimx-0.1.6 → pymiesimx-0.2.0}/.github/workflows/deploy_PyPi.yml +0 -0
- {pymiesimx-0.1.6 → pymiesimx-0.2.0}/.github/workflows/deploy_coverage.yml +0 -0
- {pymiesimx-0.1.6 → pymiesimx-0.2.0}/.github/workflows/quality.yml +0 -0
- {pymiesimx-0.1.6 → pymiesimx-0.2.0}/.github/workflows/tests.yml +0 -0
- {pymiesimx-0.1.6 → pymiesimx-0.2.0}/.gitignore +0 -0
- {pymiesimx-0.1.6 → pymiesimx-0.2.0}/.pre-commit-config.yaml +0 -0
- {pymiesimx-0.1.6 → pymiesimx-0.2.0}/AGENTS.md +0 -0
- {pymiesimx-0.1.6 → pymiesimx-0.2.0}/CONTRIBUTING.md +0 -0
- {pymiesimx-0.1.6 → pymiesimx-0.2.0}/Dockerfile +0 -0
- {pymiesimx-0.1.6 → pymiesimx-0.2.0}/LICENSE +0 -0
- {pymiesimx-0.1.6 → pymiesimx-0.2.0}/PyMieSimX/__main__.py +0 -0
- {pymiesimx-0.1.6 → pymiesimx-0.2.0}/PyMieSimX/application/__init__.py +0 -0
- {pymiesimx-0.1.6 → pymiesimx-0.2.0}/PyMieSimX/application/wsgi.py +0 -0
- {pymiesimx-0.1.6 → pymiesimx-0.2.0}/PyMieSimX/gui/__init__.py +0 -0
- {pymiesimx-0.1.6 → pymiesimx-0.2.0}/PyMieSimX/gui/admin.py +0 -0
- {pymiesimx-0.1.6 → pymiesimx-0.2.0}/PyMieSimX/gui/assets/card-state.js +0 -0
- {pymiesimx-0.1.6 → pymiesimx-0.2.0}/PyMieSimX/gui/assets/default-settings.json +0 -0
- {pymiesimx-0.1.6 → pymiesimx-0.2.0}/PyMieSimX/gui/assets/home-spf-radial.gif +0 -0
- {pymiesimx-0.1.6 → pymiesimx-0.2.0}/PyMieSimX/gui/assets/home-spf-radial.png +0 -0
- {pymiesimx-0.1.6 → pymiesimx-0.2.0}/PyMieSimX/gui/assets/logo.png +0 -0
- {pymiesimx-0.1.6 → pymiesimx-0.2.0}/PyMieSimX/gui/assets/materials-stock.json +0 -0
- {pymiesimx-0.1.6 → pymiesimx-0.2.0}/PyMieSimX/gui/assets/pymiesim-favicon.svg +0 -0
- {pymiesimx-0.1.6 → pymiesimx-0.2.0}/PyMieSimX/gui/assets/pymiesim-logo-dark.svg +0 -0
- {pymiesimx-0.1.6 → pymiesimx-0.2.0}/PyMieSimX/gui/assets/pymiesim-logo.svg +0 -0
- {pymiesimx-0.1.6 → pymiesimx-0.2.0}/PyMieSimX/gui/callback_helpers.py +0 -0
- {pymiesimx-0.1.6 → pymiesimx-0.2.0}/PyMieSimX/gui/components/__init__.py +0 -0
- {pymiesimx-0.1.6 → pymiesimx-0.2.0}/PyMieSimX/gui/components/cards.py +0 -0
- {pymiesimx-0.1.6 → pymiesimx-0.2.0}/PyMieSimX/gui/defaults.py +0 -0
- {pymiesimx-0.1.6 → pymiesimx-0.2.0}/PyMieSimX/gui/experiment_service.py +0 -0
- {pymiesimx-0.1.6 → pymiesimx-0.2.0}/PyMieSimX/gui/jobs.py +0 -0
- {pymiesimx-0.1.6 → pymiesimx-0.2.0}/PyMieSimX/gui/material_catalog.py +0 -0
- {pymiesimx-0.1.6 → pymiesimx-0.2.0}/PyMieSimX/gui/models.py +0 -0
- {pymiesimx-0.1.6 → pymiesimx-0.2.0}/PyMieSimX/gui/pages/__init__.py +0 -0
- {pymiesimx-0.1.6 → pymiesimx-0.2.0}/PyMieSimX/gui/pages/citation.py +0 -0
- {pymiesimx-0.1.6 → pymiesimx-0.2.0}/PyMieSimX/gui/pages/experiment/sections/__init__.py +0 -0
- {pymiesimx-0.1.6 → pymiesimx-0.2.0}/PyMieSimX/gui/pages/experiment/sections/configuration.py +0 -0
- {pymiesimx-0.1.6 → pymiesimx-0.2.0}/PyMieSimX/gui/pages/install_local.py +0 -0
- {pymiesimx-0.1.6 → pymiesimx-0.2.0}/PyMieSimX/gui/pages/single/__init__.py +0 -0
- {pymiesimx-0.1.6 → pymiesimx-0.2.0}/PyMieSimX/gui/pages/single/page.py +0 -0
- {pymiesimx-0.1.6 → pymiesimx-0.2.0}/PyMieSimX/gui/pages/single/sections/__init__.py +0 -0
- {pymiesimx-0.1.6 → pymiesimx-0.2.0}/PyMieSimX/gui/pages/single/sections/representations.py +0 -0
- {pymiesimx-0.1.6 → pymiesimx-0.2.0}/PyMieSimX/gui/pages/single/sections/setup.py +0 -0
- {pymiesimx-0.1.6 → pymiesimx-0.2.0}/PyMieSimX/gui/plotting.py +0 -0
- {pymiesimx-0.1.6 → pymiesimx-0.2.0}/PyMieSimX/gui/services.py +0 -0
- {pymiesimx-0.1.6 → pymiesimx-0.2.0}/PyMieSimX/gui/single_service.py +0 -0
- {pymiesimx-0.1.6 → pymiesimx-0.2.0}/PyMieSimX/gui/usage_metrics.py +0 -0
- {pymiesimx-0.1.6 → pymiesimx-0.2.0}/PyMieSimX/gui/validation.py +0 -0
- {pymiesimx-0.1.6 → pymiesimx-0.2.0}/PyMieSimX.egg-info/dependency_links.txt +0 -0
- {pymiesimx-0.1.6 → pymiesimx-0.2.0}/PyMieSimX.egg-info/entry_points.txt +0 -0
- {pymiesimx-0.1.6 → pymiesimx-0.2.0}/PyMieSimX.egg-info/top_level.txt +0 -0
- {pymiesimx-0.1.6 → pymiesimx-0.2.0}/makefile +0 -0
- {pymiesimx-0.1.6 → pymiesimx-0.2.0}/pytest.ini +0 -0
- {pymiesimx-0.1.6 → pymiesimx-0.2.0}/setup.cfg +0 -0
- {pymiesimx-0.1.6 → pymiesimx-0.2.0}/tests/gui/cli.py +0 -0
- {pymiesimx-0.1.6 → pymiesimx-0.2.0}/tests/gui/test_admin.py +0 -0
- {pymiesimx-0.1.6 → pymiesimx-0.2.0}/tests/gui/test_api.py +0 -0
- {pymiesimx-0.1.6 → pymiesimx-0.2.0}/tests/gui/test_dash_callbacks.py +0 -0
- {pymiesimx-0.1.6 → pymiesimx-0.2.0}/tests/gui/test_home.py +0 -0
- {pymiesimx-0.1.6 → pymiesimx-0.2.0}/tests/gui/test_services.py +0 -0
- {pymiesimx-0.1.6 → pymiesimx-0.2.0}/tests/gui/test_usage_metrics.py +0 -0
- {pymiesimx-0.1.6 → pymiesimx-0.2.0}/tools/generate_home_spf.py +0 -0
- {pymiesimx-0.1.6 → pymiesimx-0.2.0}/tools/next_release_version.py +0 -0
- {pymiesimx-0.1.6 → pymiesimx-0.2.0}/tools/release_tag.py +0 -0
|
@@ -17,6 +17,13 @@ jobs:
|
|
|
17
17
|
package_name: pymiesimx
|
|
18
18
|
recipe_path: conda.recipe
|
|
19
19
|
test_import: PyMieSimX
|
|
20
|
-
|
|
20
|
+
# Shell startup in the reusable workflow reactivates base, even through
|
|
21
|
+
# conda run. Invoke the installed environment's interpreter directly.
|
|
22
|
+
test_command: >-
|
|
23
|
+
if [ -f '${test_prefix}/python.exe' ]; then
|
|
24
|
+
'${test_prefix}/python.exe' '${GITHUB_WORKSPACE}/tools/verify_conda_artifact.py';
|
|
25
|
+
else
|
|
26
|
+
'${test_prefix}/bin/python' '${GITHUB_WORKSPACE}/tools/verify_conda_artifact.py';
|
|
27
|
+
fi
|
|
21
28
|
secrets:
|
|
22
29
|
ANACONDA_API_TOKEN: ${{ secrets.ANACONDA_API_TOKEN }}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: PyMieSimX
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.2.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
|
|
@@ -8,7 +8,7 @@ Project-URL: Repository, https://github.com/MartinPdeS/PyMieSimX
|
|
|
8
8
|
Requires-Python: >=3.11
|
|
9
9
|
Description-Content-Type: text/x-rst
|
|
10
10
|
License-File: LICENSE
|
|
11
|
-
Requires-Dist: PyMieSim<6,>=5.
|
|
11
|
+
Requires-Dist: PyMieSim<6,>=5.7.1
|
|
12
12
|
Requires-Dist: PyOptik<4,>=3.2.1
|
|
13
13
|
Requires-Dist: dash<5,>=4.1.0
|
|
14
14
|
Requires-Dist: gunicorn<24,>=23.0.0
|
|
@@ -50,6 +50,53 @@ 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
|
+
Ensemble Optics
|
|
54
|
+
---------------
|
|
55
|
+
|
|
56
|
+
Open **Ensemble Optics** to compute the properties of a homogeneous spherical
|
|
57
|
+
particle population at one vacuum wavelength, without a parameter sweep. The
|
|
58
|
+
collapsible Particle, Distribution, and Concentration side tabs hold the inputs;
|
|
59
|
+
the Model tab explains the assumptions and averaging. Choose a monodisperse,
|
|
60
|
+
truncated Gaussian, lognormal, or uniform number-based diameter distribution
|
|
61
|
+
and supply a number concentration (particles/mL) or volume fraction.
|
|
62
|
+
Gaussian width is a standard deviation in nm; lognormal width is a geometric
|
|
63
|
+
standard deviation (greater than 1 and at most 3). Gaussian bounds are positive
|
|
64
|
+
and the distribution is renormalized within them. Named optical materials are
|
|
65
|
+
resolved through PyOptik, or you can enter a particle index ``n+ik`` and a real
|
|
66
|
+
medium index directly. Both fields use the same RI/Material toggle as the other
|
|
67
|
+
workspaces; switching back to RI restores the previously entered index.
|
|
68
|
+
|
|
69
|
+
Results include scattering, particle absorption, extinction, reduced scattering
|
|
70
|
+
(in inverse millimeters), population single-scattering albedo, effective
|
|
71
|
+
anisotropy, averaged cross sections, and both number- and area-weighted
|
|
72
|
+
efficiencies. Cross sections are number averages; area efficiencies divide mean
|
|
73
|
+
cross section by mean projected area. Effective anisotropy is weighted by
|
|
74
|
+
scattering cross section. Population albedo is mean scattering cross section
|
|
75
|
+
divided by mean extinction cross section, rather than a number average of
|
|
76
|
+
individual albedos. Undefined ratios are displayed as undefined.
|
|
77
|
+
|
|
78
|
+
The calculation assumes independent scattering and excludes particle
|
|
79
|
+
interactions, multiple scattering, and host-medium absorption. Use dilute
|
|
80
|
+
populations. Preview the discrete number fractions, increase sampling to check
|
|
81
|
+
convergence, and export results with units, definitions, inputs, and weights.
|
|
82
|
+
PyMieSim 5.7.1 or newer supplies the distribution and averaging routines.
|
|
83
|
+
|
|
84
|
+
The same calculation is available without constructing the GUI::
|
|
85
|
+
|
|
86
|
+
from PyMieSimX import compute_population_optics, export_population_to_csv
|
|
87
|
+
|
|
88
|
+
result = compute_population_optics(
|
|
89
|
+
wavelength_nm=650,
|
|
90
|
+
material="1.5+0.01j",
|
|
91
|
+
medium="1.33",
|
|
92
|
+
distribution="lognormal",
|
|
93
|
+
diameter_nm=500,
|
|
94
|
+
width=1.2,
|
|
95
|
+
concentration=1e9,
|
|
96
|
+
concentration_basis="number",
|
|
97
|
+
)
|
|
98
|
+
csv_text = export_population_to_csv(result)
|
|
99
|
+
|
|
53
100
|
Source-model note
|
|
54
101
|
-----------------
|
|
55
102
|
|
|
@@ -129,7 +176,7 @@ available for applications that need the graphical interface.
|
|
|
129
176
|
Multiple measures
|
|
130
177
|
-----------------
|
|
131
178
|
|
|
132
|
-
The Parameter
|
|
179
|
+
The Parameter Scan Measures selector accepts several quantities in one run.
|
|
133
180
|
For example, select ``Qsca``, ``Qabs``, and ``Qext`` to compare scattering,
|
|
134
181
|
absorption, and extinction efficiencies. Measures with the same units share a
|
|
135
182
|
plot; different units use separate panels with a shared X axis. CSV exports
|
|
@@ -10,6 +10,8 @@ from PyMieSimX.api import (
|
|
|
10
10
|
ResultSizeEstimate,
|
|
11
11
|
ResultSizeLimitError,
|
|
12
12
|
ValidationIssue,
|
|
13
|
+
compute_population_optics,
|
|
14
|
+
export_population_to_csv,
|
|
13
15
|
available_measures,
|
|
14
16
|
build_figure,
|
|
15
17
|
build_single_figure,
|
|
@@ -27,6 +29,8 @@ except PackageNotFoundError:
|
|
|
27
29
|
__version__ = "0+unknown"
|
|
28
30
|
|
|
29
31
|
__all__ = [
|
|
32
|
+
"compute_population_optics",
|
|
33
|
+
"export_population_to_csv",
|
|
30
34
|
"MAX_SWEEP_COMBINATIONS",
|
|
31
35
|
"MAX_RESULT_FRAME_BYTES",
|
|
32
36
|
"MAX_RESULT_PAYLOAD_BYTES",
|
|
@@ -19,7 +19,11 @@ from PyMieSimX.gui.services import (
|
|
|
19
19
|
validate_experiment_inputs,
|
|
20
20
|
)
|
|
21
21
|
|
|
22
|
+
from PyMieSimX.gui.population_service import compute_population_optics, export_population_to_csv
|
|
23
|
+
|
|
22
24
|
__all__ = [
|
|
25
|
+
"compute_population_optics",
|
|
26
|
+
"export_population_to_csv",
|
|
23
27
|
"MAX_SWEEP_COMBINATIONS",
|
|
24
28
|
"MAX_RESULT_FRAME_BYTES",
|
|
25
29
|
"MAX_RESULT_PAYLOAD_BYTES",
|
|
@@ -3717,8 +3717,8 @@ html .plot-options-card .plot-option-control.dashboard-dropdown .Select-control
|
|
|
3717
3717
|
font-weight: 650;
|
|
3718
3718
|
}
|
|
3719
3719
|
|
|
3720
|
-
/* Match
|
|
3721
|
-
card used by Parameter
|
|
3720
|
+
/* Match Single Scatterer representation controls to the compact Graph Axes
|
|
3721
|
+
card used by Parameter Scan. */
|
|
3722
3722
|
.single-workspace .representation-card {
|
|
3723
3723
|
width: 100%;
|
|
3724
3724
|
max-width: none;
|
|
@@ -3910,12 +3910,17 @@ html .single-workspace .representation-card {
|
|
|
3910
3910
|
font-variant-numeric: tabular-nums;
|
|
3911
3911
|
}
|
|
3912
3912
|
|
|
3913
|
+
.home-support-actions {
|
|
3914
|
+
display: flex;
|
|
3915
|
+
flex-direction: column;
|
|
3916
|
+
gap: 9px;
|
|
3917
|
+
}
|
|
3918
|
+
|
|
3913
3919
|
.home-support-button {
|
|
3914
3920
|
display: inline-flex;
|
|
3915
3921
|
align-items: center;
|
|
3916
3922
|
gap: 9px;
|
|
3917
3923
|
flex: 0 0 auto;
|
|
3918
|
-
margin-left: auto;
|
|
3919
3924
|
padding: 10px 17px;
|
|
3920
3925
|
border: 1px solid #e8c56b;
|
|
3921
3926
|
border-radius: 9px;
|
|
@@ -3992,3 +3997,13 @@ html .single-workspace .representation-card {
|
|
|
3992
3997
|
grid-template-columns: 1fr;
|
|
3993
3998
|
}
|
|
3994
3999
|
}
|
|
4000
|
+
|
|
4001
|
+
/* Ensemble Optics shares the workspace toolbar and collapsible side panels. */
|
|
4002
|
+
.population-workspace, .population-output { display: grid; gap: 20px; min-width: 0; }
|
|
4003
|
+
.population-page .field-block { margin-bottom: 14px; }
|
|
4004
|
+
.population-page .field-block small { display: block; margin-top: 6px; opacity: .75; }
|
|
4005
|
+
.population-page .field-input { width: 100%; }
|
|
4006
|
+
.population-page .documentation-table { width: 100%; }
|
|
4007
|
+
.population-page .documentation-table td { overflow-wrap: anywhere; }
|
|
4008
|
+
#population-status, #population-preview-error { margin-bottom: 12px; }
|
|
4009
|
+
#population-results { overflow-x: auto; }
|
|
@@ -18,7 +18,10 @@ from PyMieSimX.gui.pages.install_local import build_install_local_page
|
|
|
18
18
|
from PyMieSimX.gui.pages.experiment import build_experiment_page
|
|
19
19
|
from PyMieSimX.gui.pages.settings import build_settings_page
|
|
20
20
|
from PyMieSimX.gui.pages.single import build_single_page
|
|
21
|
-
from PyMieSimX.gui.
|
|
21
|
+
from PyMieSimX.gui.pages.population import build_population_page, register_population_callbacks
|
|
22
|
+
from PyMieSimX.gui.parsing import is_numeric_material_value
|
|
23
|
+
from PyMieSimX.gui.population_service import parse_population_material
|
|
24
|
+
from PyMieSimX.gui.schemas import POPULATION_OPTICAL_FIELDS, DETECTOR_FIELDS, SCATTERER_FIELDS, SINGLE_SCATTERER_FIELDS, SINGLE_SOURCE_FIELDS, SOURCE_FIELDS
|
|
22
25
|
from PyMieSimX.gui.callback_helpers import (
|
|
23
26
|
execute_single_callback,
|
|
24
27
|
merge_local_plot_values as _merge_local_plot_values,
|
|
@@ -59,6 +62,7 @@ def _counter(value: object) -> int:
|
|
|
59
62
|
|
|
60
63
|
def register_callbacks(app: Dash, default_measure_options: list[str]) -> None:
|
|
61
64
|
"""Register all dashboard callbacks."""
|
|
65
|
+
register_population_callbacks(app)
|
|
62
66
|
LOGGER.debug("Registering dashboard callbacks")
|
|
63
67
|
|
|
64
68
|
@app.callback(
|
|
@@ -151,6 +155,25 @@ def register_callbacks(app: Dash, default_measure_options: list[str]) -> None:
|
|
|
151
155
|
return _handle_sidebar_tabs(tab_ids, ["yellow", "blue", "cyan", "purple"], current_class, current_active, "experiment-right-sidebar")
|
|
152
156
|
|
|
153
157
|
|
|
158
|
+
@app.callback(
|
|
159
|
+
Output("population-right-sidebar", "className"),
|
|
160
|
+
Output("population-right-sidebar-active", "data"),
|
|
161
|
+
*[Output(f"population-tab-{name}", "className") for name in ("particle", "distribution", "concentration", "model")],
|
|
162
|
+
*[Output(f"population-panel-{name}", "style") for name in ("particle", "distribution", "concentration", "model")],
|
|
163
|
+
*[Input(f"population-tab-{name}", "n_clicks") for name in ("particle", "distribution", "concentration", "model")],
|
|
164
|
+
Input("page-content", "n_clicks"),
|
|
165
|
+
Input("dashboard-sidebar", "n_clicks"),
|
|
166
|
+
Input("population-right-sidebar", "n_clicks"),
|
|
167
|
+
State("population-right-sidebar", "className"),
|
|
168
|
+
State("population-right-sidebar-active", "data"),
|
|
169
|
+
prevent_initial_call=True,
|
|
170
|
+
)
|
|
171
|
+
def _handle_population_sidebar_tabs(_particle_clicks, _distribution_clicks, _concentration_clicks, _model_clicks,
|
|
172
|
+
_page_clicks, _sidebar_clicks, _right_sidebar_clicks, current_class, current_active):
|
|
173
|
+
tab_ids = [f"population-tab-{name}" for name in ("particle", "distribution", "concentration", "model")]
|
|
174
|
+
return _handle_sidebar_tabs(tab_ids, ["blue", "yellow", "cyan", "purple"], current_class, current_active, "population-right-sidebar")
|
|
175
|
+
|
|
176
|
+
|
|
154
177
|
@app.callback(
|
|
155
178
|
Output("page-content", "children"),
|
|
156
179
|
Output("sidebar-link-home", "className"),
|
|
@@ -158,6 +181,7 @@ def register_callbacks(app: Dash, default_measure_options: list[str]) -> None:
|
|
|
158
181
|
Output("sidebar-link-single", "className"),
|
|
159
182
|
Output("sidebar-link-documentation", "className"),
|
|
160
183
|
Output("sidebar-link-settings", "className"),
|
|
184
|
+
Output("sidebar-link-population", "className"),
|
|
161
185
|
Output("home-visit-count", "data"),
|
|
162
186
|
Input("url", "pathname"),
|
|
163
187
|
Input("url", "search"),
|
|
@@ -176,6 +200,7 @@ def register_callbacks(app: Dash, default_measure_options: list[str]) -> None:
|
|
|
176
200
|
"single": "sidebar-link",
|
|
177
201
|
"documentation": "sidebar-link",
|
|
178
202
|
"settings": "sidebar-link",
|
|
203
|
+
"population": "sidebar-link",
|
|
179
204
|
}
|
|
180
205
|
home_visits = _counter(home_visits)
|
|
181
206
|
if route == "/":
|
|
@@ -187,33 +212,36 @@ def register_callbacks(app: Dash, default_measure_options: list[str]) -> None:
|
|
|
187
212
|
home_visits = float("nan")
|
|
188
213
|
if route == "/admin":
|
|
189
214
|
token = parse_qs((search or "").lstrip("?")).get("token", [None])[0]
|
|
190
|
-
return build_page_with_footer(build_admin_page(token)), *(active[key] for key in ("home", "experiment", "single", "documentation", "settings")), home_visits
|
|
215
|
+
return build_page_with_footer(build_admin_page(token)), *(active[key] for key in ("home", "experiment", "single", "documentation", "settings", "population")), home_visits
|
|
191
216
|
if route == "/documentation":
|
|
192
217
|
active["documentation"] += " active"
|
|
193
|
-
return build_page_with_footer(build_documentation_page()), *(active[key] for key in ("home", "experiment", "single", "documentation", "settings")), home_visits
|
|
218
|
+
return build_page_with_footer(build_documentation_page()), *(active[key] for key in ("home", "experiment", "single", "documentation", "settings", "population")), home_visits
|
|
194
219
|
if route == "/citation":
|
|
195
220
|
active["home"] += " active"
|
|
196
|
-
return build_page_with_footer(build_citation_page()), *(active[key] for key in ("home", "experiment", "single", "documentation", "settings")), home_visits
|
|
221
|
+
return build_page_with_footer(build_citation_page()), *(active[key] for key in ("home", "experiment", "single", "documentation", "settings", "population")), home_visits
|
|
197
222
|
if route == "/documentation/install-local":
|
|
198
223
|
active["documentation"] += " active"
|
|
199
|
-
return build_page_with_footer(build_install_local_page()), *(active[key] for key in ("home", "experiment", "single", "documentation", "settings")), home_visits
|
|
224
|
+
return build_page_with_footer(build_install_local_page()), *(active[key] for key in ("home", "experiment", "single", "documentation", "settings", "population")), home_visits
|
|
200
225
|
if route == "/documentation/sellmeier":
|
|
201
226
|
active["documentation"] += " active"
|
|
202
|
-
return build_page_with_footer(build_sellmeier_page()), *(active[key] for key in ("home", "experiment", "single", "documentation", "settings")), home_visits
|
|
227
|
+
return build_page_with_footer(build_sellmeier_page()), *(active[key] for key in ("home", "experiment", "single", "documentation", "settings", "population")), home_visits
|
|
203
228
|
if route == "/documentation/field-syntax":
|
|
204
229
|
active["documentation"] += " active"
|
|
205
|
-
return build_page_with_footer(build_field_syntax_page()), *(active[key] for key in ("home", "experiment", "single", "documentation", "settings")), home_visits
|
|
230
|
+
return build_page_with_footer(build_field_syntax_page()), *(active[key] for key in ("home", "experiment", "single", "documentation", "settings", "population")), home_visits
|
|
206
231
|
if route == "/settings":
|
|
207
232
|
active["settings"] += " active"
|
|
208
|
-
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")), home_visits
|
|
233
|
+
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
|
|
209
234
|
if route == "/single":
|
|
210
235
|
active["single"] += " active"
|
|
211
|
-
return build_page_with_footer(build_single_page((plot_settings or {}).get("particle_explorer", {}))), *(active[key] for key in ("home", "experiment", "single", "documentation", "settings")), home_visits
|
|
236
|
+
return build_page_with_footer(build_single_page((plot_settings or {}).get("particle_explorer", {}))), *(active[key] for key in ("home", "experiment", "single", "documentation", "settings", "population")), home_visits
|
|
237
|
+
if route == "/population":
|
|
238
|
+
active["population"] += " active"
|
|
239
|
+
return build_page_with_footer(build_population_page()), *(active[key] for key in ("home", "experiment", "single", "documentation", "settings", "population")), home_visits
|
|
212
240
|
if route == "/experiment":
|
|
213
241
|
active["experiment"] += " active"
|
|
214
|
-
return build_page_with_footer(build_experiment_page(default_measure_options, plot_settings or {})), *(active[key] for key in ("home", "experiment", "single", "documentation", "settings")), home_visits
|
|
242
|
+
return build_page_with_footer(build_experiment_page(default_measure_options, plot_settings or {})), *(active[key] for key in ("home", "experiment", "single", "documentation", "settings", "population")), home_visits
|
|
215
243
|
active["home"] += " active"
|
|
216
|
-
return build_page_with_footer(build_home_page(home_visits=home_visits)), *(active[key] for key in ("home", "experiment", "single", "documentation", "settings")), home_visits
|
|
244
|
+
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
|
|
217
245
|
|
|
218
246
|
@app.callback(
|
|
219
247
|
Output("admin-home-page-visits", "children"),
|
|
@@ -316,6 +344,7 @@ def register_callbacks(app: Dash, default_measure_options: list[str]) -> None:
|
|
|
316
344
|
"detector": detector_type,
|
|
317
345
|
"single-source": single_source_type,
|
|
318
346
|
"single-scatterer": single_scatterer_type,
|
|
347
|
+
"population": "Sphere",
|
|
319
348
|
}
|
|
320
349
|
schema_groups = {
|
|
321
350
|
"source": SOURCE_FIELDS,
|
|
@@ -323,6 +352,7 @@ def register_callbacks(app: Dash, default_measure_options: list[str]) -> None:
|
|
|
323
352
|
"detector": DETECTOR_FIELDS,
|
|
324
353
|
"single-source": SINGLE_SOURCE_FIELDS,
|
|
325
354
|
"single-scatterer": SINGLE_SCATTERER_FIELDS,
|
|
355
|
+
"population": {"Sphere": POPULATION_OPTICAL_FIELDS},
|
|
326
356
|
}
|
|
327
357
|
|
|
328
358
|
classes = []
|
|
@@ -340,7 +370,8 @@ def register_callbacks(app: Dash, default_measure_options: list[str]) -> None:
|
|
|
340
370
|
valid = field.optional and empty
|
|
341
371
|
if not empty:
|
|
342
372
|
try:
|
|
343
|
-
parsed_value =
|
|
373
|
+
parsed_value = (parse_population_material(raw_value, medium=field.name == "medium")
|
|
374
|
+
if section == "population" else _parse_field_value(field.kind, raw_value, field.unit))
|
|
344
375
|
if field.name in {"wavelength", "optical_power", "numerical_aperture", "amplitude", "diameter", "core_diameter", "shell_thickness", "sampling"}:
|
|
345
376
|
_validate_positive_field(field.name, parsed_value)
|
|
346
377
|
valid = True
|
|
@@ -403,7 +434,7 @@ def register_callbacks(app: Dash, default_measure_options: list[str]) -> None:
|
|
|
403
434
|
dropdown_style = {} if use_named_material else {"display": "none"}
|
|
404
435
|
ri_value = stored_ri_value or "1.4"
|
|
405
436
|
|
|
406
|
-
if current_value and
|
|
437
|
+
if current_value and is_numeric_material_value(current_value):
|
|
407
438
|
ri_value = current_value
|
|
408
439
|
|
|
409
440
|
if use_named_material and selected_material:
|
|
@@ -745,9 +745,9 @@ def _plotly_array(value: Any) -> np.ndarray:
|
|
|
745
745
|
|
|
746
746
|
|
|
747
747
|
def export_single_result_to_csv(result: dict[str, Any] | None) -> str:
|
|
748
|
-
"""Serialize a
|
|
748
|
+
"""Serialize a Single Scatterer figure payload to CSV text."""
|
|
749
749
|
if not result or not result.get("figure"):
|
|
750
|
-
LOGGER.debug("
|
|
750
|
+
LOGGER.debug("Single Scatterer CSV export requested without a result")
|
|
751
751
|
return ""
|
|
752
752
|
|
|
753
753
|
traces = result["figure"].get("data", [])
|
|
@@ -38,7 +38,7 @@ def create_dash_app() -> Dash:
|
|
|
38
38
|
"""Create and configure the experiment dashboard Dash application."""
|
|
39
39
|
application = Dash(
|
|
40
40
|
__name__,
|
|
41
|
-
title="PyMieSim
|
|
41
|
+
title="PyMieSim",
|
|
42
42
|
assets_folder=str(Path(__file__).with_name("assets")),
|
|
43
43
|
suppress_callback_exceptions=True,
|
|
44
44
|
)
|
|
@@ -5,6 +5,7 @@ from dash import dcc, html
|
|
|
5
5
|
|
|
6
6
|
from PyMieSimX.gui.defaults import DEFAULT_APPLICATION_SETTINGS, DEFAULT_PLOT_SETTINGS
|
|
7
7
|
from PyMieSimX.gui.material_catalog import material_dropdown_options
|
|
8
|
+
from PyMieSimX.gui.parsing import is_numeric_material_value
|
|
8
9
|
from PyMieSimX.gui.schemas import FieldSpec, SECTION_FIELDS, SINGLE_SCATTERER_FIELDS, SINGLE_SOURCE_FIELDS
|
|
9
10
|
|
|
10
11
|
|
|
@@ -305,8 +306,9 @@ def _build_sidebar():
|
|
|
305
306
|
className="sidebar-nav",
|
|
306
307
|
children=[
|
|
307
308
|
_sidebar_link("Home", "/"),
|
|
308
|
-
_sidebar_link("
|
|
309
|
-
_sidebar_link("Parameter
|
|
309
|
+
_sidebar_link("Single Scatterer", "/single"),
|
|
310
|
+
_sidebar_link("Parameter Scan", "/experiment"),
|
|
311
|
+
_sidebar_link("Ensemble Optics", "/population"),
|
|
310
312
|
_sidebar_link("Documentation", "/documentation"),
|
|
311
313
|
_sidebar_link("Settings", "/settings"),
|
|
312
314
|
],
|
|
@@ -360,7 +362,7 @@ def render_field(section: str, field_spec: FieldSpec):
|
|
|
360
362
|
if field_spec.kind in {"material", "medium"}:
|
|
361
363
|
is_medium = field_spec.kind == "medium"
|
|
362
364
|
default_text = str(field_spec.default)
|
|
363
|
-
default_is_named =
|
|
365
|
+
default_is_named = bool(default_text.strip()) and not is_numeric_material_value(default_text)
|
|
364
366
|
named_options = material_dropdown_options(medium=is_medium)
|
|
365
367
|
default_from_catalog = named_options[0]["value"] if named_options else None
|
|
366
368
|
named_values = {option["value"] for option in named_options}
|
|
@@ -19,8 +19,8 @@ def build_documentation_page():
|
|
|
19
19
|
html.Div(
|
|
20
20
|
className="documentation-hero-actions",
|
|
21
21
|
children=[
|
|
22
|
-
html.A("
|
|
23
|
-
html.A("Open
|
|
22
|
+
html.A("Open Parameter Scan →", href="/experiment", className="home-button home-button-primary"),
|
|
23
|
+
html.A("Open Single Scatterer →", href="/single", className="home-button home-button-outline"),
|
|
24
24
|
],
|
|
25
25
|
),
|
|
26
26
|
],
|
|
@@ -32,7 +32,7 @@ def build_documentation_page():
|
|
|
32
32
|
html.Div(
|
|
33
33
|
className="card-body documentation-quickstart-body",
|
|
34
34
|
children=[
|
|
35
|
-
_step("01", "Choose a workspace", "Use Parameter
|
|
35
|
+
_step("01", "Choose a workspace", "Use Parameter Scan for parameter sweeps and detector coupling. Use Single Scatterer to inspect one optical setup."),
|
|
36
36
|
_step("02", "Configure the objects", "Pick a source, scatterer, and—when needed—a detector. Each card exposes the fields supported by that model."),
|
|
37
37
|
_step("03", "Enter values or sweeps", "Use one value for a focused run, a comma-separated list for selected values, or start:end:count for an evenly spaced sweep."),
|
|
38
38
|
_step("04", "Render and inspect", "Run the calculation, select the X axis when several fields vary, and use Settings to tune the resulting plots."),
|
|
@@ -43,8 +43,9 @@ def build_documentation_page():
|
|
|
43
43
|
html.Div(
|
|
44
44
|
className="documentation-grid documentation-topic-grid",
|
|
45
45
|
children=[
|
|
46
|
-
_guide_card("yellow", "Parameter
|
|
47
|
-
_guide_card("blue", "
|
|
46
|
+
_guide_card("yellow", "Parameter Scan", "Sweep a parameter space", "Build a source, scatterer, and detector, then evaluate a measure over every combination.", ["X-axis is inferred from fields with multiple values.", "Detector-free runs are supported.", "Results can be exported as CSV."], "/experiment", "Open Parameter Scan"),
|
|
47
|
+
_guide_card("blue", "Single Scatterer", "Understand one setup", "Inspect angular amplitudes, polarization, phase functions, or far-field intensity for one source and scatterer.", ["Choose a representation.", "Control angular sampling.", "Render the figure on demand."], "/single", "Open Single Scatterer"),
|
|
48
|
+
_guide_card("orange", "Ensemble Optics", "Calculate population properties", "Define a particle size distribution and concentration at one wavelength.", ["Gaussian, lognormal, uniform, or monodisperse sizes.", "Scattering, absorption, albedo, and averaged efficiencies.", "Export results and distribution weights as CSV."], "/population", "Open Ensemble Optics"),
|
|
48
49
|
_guide_card("green", "Material model", "Sellmeier relation", "Understand dispersive refractive-index modeling and check the coefficient table used for material workflows.", ["See the Sellmeier equation.", "Review coefficient values.", "Map values to material inputs."], "/documentation/sellmeier", "Open Sellmeier reference"),
|
|
49
50
|
_guide_card("blue", "Input reference", "Field syntax & vocabulary", "Learn compact field notation, detector definitions, and the model terms used throughout the dashboard.", ["Enter scalars, lists, and ranges.", "Understand detector choices.", "Review supported model families."], "/documentation/field-syntax", "Open field reference"),
|
|
50
51
|
_guide_card("cyan", "Plot settings", "Make figures your own", "Set typography, line and marker sizes, grid visibility, legend visibility, and light or dark plot styling.", ["Preferences are saved in this browser.", "Settings apply to both workspaces.", "The application theme is controlled here."], "/settings", "Open Settings"),
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
"""Parameter
|
|
1
|
+
"""Parameter Scan page composition."""
|
|
2
2
|
|
|
3
3
|
|
|
4
4
|
from dash import dcc, html
|
|
@@ -10,7 +10,7 @@ from .sections import build_detector_section, build_scatterer_section, build_sou
|
|
|
10
10
|
|
|
11
11
|
|
|
12
12
|
def build_experiment_page(default_measure_options: list[str], plot_settings: dict | None = None):
|
|
13
|
-
"""Build the isolated Parameter
|
|
13
|
+
"""Build the isolated Parameter Scan workspace."""
|
|
14
14
|
settings = (plot_settings or {}).get("parameter_sweep", {})
|
|
15
15
|
return html.Div(
|
|
16
16
|
className="tab-content-stack experiment-tab-content",
|
|
@@ -39,7 +39,7 @@ def build_field_syntax_page():
|
|
|
39
39
|
className=Card.classes(color="blue", extra="panel documentation-note"),
|
|
40
40
|
children=[
|
|
41
41
|
html.Div(className="card-header panel-header", children=[html.H2("Continue exploring")]),
|
|
42
|
-
html.Div(className="card-body documentation-note-body", children=[html.P("Use these definitions while configuring a Parameter
|
|
42
|
+
html.Div(className="card-body documentation-note-body", children=[html.P("Use these definitions while configuring a Parameter Scan or Single Scatterer run."), html.A("Back to documentation →", href="/documentation", className="inline-action")]),
|
|
43
43
|
],
|
|
44
44
|
),
|
|
45
45
|
],
|
|
@@ -38,13 +38,26 @@ def build_home_page(*, home_visits: int | float | None = None):
|
|
|
38
38
|
html.Div(
|
|
39
39
|
[
|
|
40
40
|
_visit_metric(home_visits),
|
|
41
|
-
html.
|
|
42
|
-
[
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
41
|
+
html.Div(
|
|
42
|
+
[
|
|
43
|
+
html.A(
|
|
44
|
+
[html.Span("\u2665", className="home-support-icon", **{"aria-hidden": "true"}), "Support Developer"],
|
|
45
|
+
id="home-support-developer",
|
|
46
|
+
href="https://github.com/sponsors/MartinPdeS",
|
|
47
|
+
target="_blank",
|
|
48
|
+
rel="noopener noreferrer",
|
|
49
|
+
className="home-support-button",
|
|
50
|
+
),
|
|
51
|
+
html.A(
|
|
52
|
+
[html.Span("\u2605", className="home-support-icon", **{"aria-hidden": "true"}), "Star PyMieSim on GitHub"],
|
|
53
|
+
id="home-star-repository",
|
|
54
|
+
href="https://github.com/MartinPdeS/PyMieSim",
|
|
55
|
+
target="_blank",
|
|
56
|
+
rel="noopener noreferrer",
|
|
57
|
+
className="home-support-button",
|
|
58
|
+
),
|
|
59
|
+
],
|
|
60
|
+
className="home-support-actions",
|
|
48
61
|
),
|
|
49
62
|
],
|
|
50
63
|
className="home-hero-actions",
|
|
@@ -55,25 +68,35 @@ def build_home_page(*, home_visits: int | float | None = None):
|
|
|
55
68
|
className="home-capability-grid",
|
|
56
69
|
children=[
|
|
57
70
|
_capability_card(
|
|
58
|
-
"
|
|
71
|
+
"Single Scatterer",
|
|
59
72
|
"Inspect one optical setup through angular scattering, polarization, phase functions, and field representations.",
|
|
60
73
|
["Configure source", "Configure scatterer", "Render representations"],
|
|
61
|
-
"Open
|
|
74
|
+
"Open Single Scatterer",
|
|
62
75
|
"/single",
|
|
63
76
|
html.Img(src="/assets/home-spf-radial.gif", className="home-capability-preview home-capability-preview-image", alt="Rotating scattering phase function 3D radial surface"),
|
|
64
77
|
"Scattering phase function — rotating 3D radial surface",
|
|
65
78
|
"purple",
|
|
66
79
|
),
|
|
67
80
|
_capability_card(
|
|
68
|
-
"Parameter
|
|
81
|
+
"Parameter Scan",
|
|
69
82
|
"Run source, scatterer, and detector configurations across parameter sweeps and export structured results for analysis.",
|
|
70
83
|
["Configure source", "Configure scatterer and detector", "Run and export results"],
|
|
71
|
-
"Open Parameter
|
|
84
|
+
"Open Parameter Scan",
|
|
72
85
|
"/experiment",
|
|
73
86
|
_qsca_preview(),
|
|
74
87
|
"Scattering-efficiency sweep",
|
|
75
88
|
"green",
|
|
76
89
|
),
|
|
90
|
+
_capability_card(
|
|
91
|
+
"Ensemble Optics",
|
|
92
|
+
"Turn a particle size distribution and concentration into scattering, absorption, albedo, and averaged optical properties at one wavelength.",
|
|
93
|
+
["Define particle properties", "Choose a distribution and concentration", "Compute all properties and export"],
|
|
94
|
+
"Open Ensemble Optics",
|
|
95
|
+
"/population",
|
|
96
|
+
_distribution_preview(),
|
|
97
|
+
"Gaussian and lognormal size distributions",
|
|
98
|
+
"blue",
|
|
99
|
+
),
|
|
77
100
|
],
|
|
78
101
|
),
|
|
79
102
|
],
|
|
@@ -115,6 +138,7 @@ def _capability_card(
|
|
|
115
138
|
className="home-capability-preview",
|
|
116
139
|
)
|
|
117
140
|
return html.Section(
|
|
141
|
+
id=f"home-workflow-{href.strip('/')}",
|
|
118
142
|
className=Card.classes(color=color, extra="home-capability-card"),
|
|
119
143
|
children=[
|
|
120
144
|
html.Div(title, className="home-section-header"),
|
|
@@ -207,3 +231,21 @@ def _qsca_preview() -> go.Figure:
|
|
|
207
231
|
margin={"t": 32},
|
|
208
232
|
)
|
|
209
233
|
return figure
|
|
234
|
+
|
|
235
|
+
|
|
236
|
+
def _distribution_preview() -> go.Figure:
|
|
237
|
+
"""Illustrate Gaussian and lognormal particle-size probability densities."""
|
|
238
|
+
diameter = np.linspace(100, 1000, 240)
|
|
239
|
+
sigma_log = np.log(1.3)
|
|
240
|
+
lognormal = np.exp(-0.5 * (np.log(diameter / 450) / sigma_log) ** 2) / (diameter * sigma_log * np.sqrt(2 * np.pi))
|
|
241
|
+
gaussian = np.exp(-0.5 * ((diameter - 520) / 80) ** 2) / (80 * np.sqrt(2 * np.pi))
|
|
242
|
+
figure = go.Figure([
|
|
243
|
+
go.Scatter(x=diameter, y=lognormal, mode="lines", name="Lognormal", hoverinfo="skip",
|
|
244
|
+
line={"color": "#2672d6", "width": 3}, fill="tozeroy", fillcolor="rgba(38,114,214,.15)"),
|
|
245
|
+
go.Scatter(x=diameter, y=gaussian, mode="lines", name="Gaussian", hoverinfo="skip",
|
|
246
|
+
line={"color": "#b47b1d", "width": 3, "dash": "dot"}),
|
|
247
|
+
])
|
|
248
|
+
figure = _preview_layout(figure, x_title="Diameter (nm)", y_title="Probability density (nm⁻¹)")
|
|
249
|
+
figure.update_layout(showlegend=True, margin={"t": 32},
|
|
250
|
+
legend={"orientation": "h", "x": 0.5, "xanchor": "center", "y": 1.13, "font": {"size": 10}})
|
|
251
|
+
return figure
|