PyMieSimX 0.1.0__tar.gz → 0.1.3__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.
Files changed (106) hide show
  1. {pymiesimx-0.1.0 → pymiesimx-0.1.3}/.coveragerc +1 -0
  2. {pymiesimx-0.1.0 → pymiesimx-0.1.3}/.github/workflows/deploy_PyPi.yml +5 -7
  3. {pymiesimx-0.1.0 → pymiesimx-0.1.3}/.github/workflows/deploy_anaconda.yml +8 -9
  4. {pymiesimx-0.1.0 → pymiesimx-0.1.3}/.github/workflows/deploy_coverage.yml +5 -2
  5. pymiesimx-0.1.3/.github/workflows/quality.yml +47 -0
  6. pymiesimx-0.1.3/.github/workflows/tests.yml +22 -0
  7. {pymiesimx-0.1.0 → pymiesimx-0.1.3}/.gitignore +7 -0
  8. pymiesimx-0.1.3/CONTRIBUTING.md +7 -0
  9. {pymiesimx-0.1.0 → pymiesimx-0.1.3}/Dockerfile +2 -1
  10. pymiesimx-0.1.3/PKG-INFO +153 -0
  11. pymiesimx-0.1.3/PyMieSimX/__init__.py +58 -0
  12. {pymiesimx-0.1.0 → pymiesimx-0.1.3}/PyMieSimX/__main__.py +19 -5
  13. pymiesimx-0.1.3/PyMieSimX/api.py +39 -0
  14. pymiesimx-0.1.3/PyMieSimX/application/__init__.py +1 -0
  15. pymiesimx-0.1.3/PyMieSimX/application/wsgi.py +9 -0
  16. pymiesimx-0.1.3/PyMieSimX/gui/__init__.py +12 -0
  17. pymiesimx-0.1.3/PyMieSimX/gui/admin.py +122 -0
  18. {pymiesimx-0.1.0 → pymiesimx-0.1.3}/PyMieSimX/gui/assets/dashboard.css +572 -28
  19. {pymiesimx-0.1.0 → pymiesimx-0.1.3}/PyMieSimX/gui/assets/default-settings.json +3 -3
  20. pymiesimx-0.1.3/PyMieSimX/gui/assets/home-spf-radial.gif +0 -0
  21. pymiesimx-0.1.3/PyMieSimX/gui/assets/home-spf-radial.png +0 -0
  22. pymiesimx-0.1.3/PyMieSimX/gui/assets/materials-stock.json +142 -0
  23. pymiesimx-0.1.3/PyMieSimX/gui/callback_helpers.py +129 -0
  24. pymiesimx-0.1.0/PyMieSimX/gui/interface.py → pymiesimx-0.1.3/PyMieSimX/gui/callbacks.py +314 -157
  25. {pymiesimx-0.1.0 → pymiesimx-0.1.3}/PyMieSimX/gui/components/cards.py +0 -1
  26. pymiesimx-0.1.0/PyMieSimX/gui/services.py → pymiesimx-0.1.3/PyMieSimX/gui/computation.py +396 -26
  27. {pymiesimx-0.1.0 → pymiesimx-0.1.3}/PyMieSimX/gui/defaults.py +0 -1
  28. pymiesimx-0.1.3/PyMieSimX/gui/experiment_service.py +23 -0
  29. pymiesimx-0.1.3/PyMieSimX/gui/interface.py +105 -0
  30. pymiesimx-0.1.3/PyMieSimX/gui/jobs.py +118 -0
  31. {pymiesimx-0.1.0 → pymiesimx-0.1.3}/PyMieSimX/gui/layout.py +164 -7
  32. pymiesimx-0.1.3/PyMieSimX/gui/material_catalog.py +127 -0
  33. pymiesimx-0.1.3/PyMieSimX/gui/models.py +64 -0
  34. {pymiesimx-0.1.0 → pymiesimx-0.1.3}/PyMieSimX/gui/pages/__init__.py +3 -1
  35. {pymiesimx-0.1.0 → pymiesimx-0.1.3}/PyMieSimX/gui/pages/documentation.py +3 -60
  36. pymiesimx-0.1.3/PyMieSimX/gui/pages/experiment/page.py +62 -0
  37. pymiesimx-0.1.3/PyMieSimX/gui/pages/experiment/sections/configuration.py +47 -0
  38. pymiesimx-0.1.3/PyMieSimX/gui/pages/field_syntax.py +77 -0
  39. pymiesimx-0.1.3/PyMieSimX/gui/pages/home.py +175 -0
  40. {pymiesimx-0.1.0 → pymiesimx-0.1.3}/PyMieSimX/gui/pages/install_local.py +7 -4
  41. pymiesimx-0.1.3/PyMieSimX/gui/pages/sellmeier.py +169 -0
  42. {pymiesimx-0.1.0 → pymiesimx-0.1.3}/PyMieSimX/gui/pages/settings.py +0 -1
  43. pymiesimx-0.1.3/PyMieSimX/gui/pages/single/page.py +57 -0
  44. pymiesimx-0.1.3/PyMieSimX/gui/pages/single/sections/representations.py +37 -0
  45. pymiesimx-0.1.3/PyMieSimX/gui/pages/single/sections/setup.py +31 -0
  46. {pymiesimx-0.1.0 → pymiesimx-0.1.3}/PyMieSimX/gui/parsing.py +44 -7
  47. pymiesimx-0.1.3/PyMieSimX/gui/plotting.py +19 -0
  48. {pymiesimx-0.1.0 → pymiesimx-0.1.3}/PyMieSimX/gui/schemas.py +7 -7
  49. pymiesimx-0.1.3/PyMieSimX/gui/services.py +17 -0
  50. pymiesimx-0.1.3/PyMieSimX/gui/single_service.py +14 -0
  51. pymiesimx-0.1.3/PyMieSimX/gui/usage_metrics.py +173 -0
  52. pymiesimx-0.1.3/PyMieSimX/gui/validation.py +18 -0
  53. pymiesimx-0.1.3/PyMieSimX.egg-info/PKG-INFO +153 -0
  54. {pymiesimx-0.1.0 → pymiesimx-0.1.3}/PyMieSimX.egg-info/SOURCES.txt +34 -2
  55. pymiesimx-0.1.3/PyMieSimX.egg-info/requires.txt +21 -0
  56. pymiesimx-0.1.3/PyMieSimX.egg-info/scm_file_list.json +85 -0
  57. pymiesimx-0.1.3/PyMieSimX.egg-info/scm_version.json +8 -0
  58. pymiesimx-0.1.3/README.rst +123 -0
  59. {pymiesimx-0.1.0 → pymiesimx-0.1.3/conda.recipe}/meta.yaml +8 -6
  60. {pymiesimx-0.1.0 → pymiesimx-0.1.3}/makefile +14 -2
  61. {pymiesimx-0.1.0 → pymiesimx-0.1.3}/pyproject.toml +20 -9
  62. {pymiesimx-0.1.0 → pymiesimx-0.1.3}/pytest.ini +2 -0
  63. pymiesimx-0.1.3/tests/browser/test_dashboard.py +28 -0
  64. pymiesimx-0.1.3/tests/gui/cli.py +57 -0
  65. pymiesimx-0.1.3/tests/gui/test_admin.py +77 -0
  66. pymiesimx-0.1.3/tests/gui/test_api.py +59 -0
  67. pymiesimx-0.1.3/tests/gui/test_dash_callbacks.py +161 -0
  68. pymiesimx-0.1.3/tests/gui/test_services.py +240 -0
  69. pymiesimx-0.1.3/tests/gui/test_usage_metrics.py +31 -0
  70. pymiesimx-0.1.3/tools/generate_home_spf.py +100 -0
  71. pymiesimx-0.1.3/tools/next_release_version.py +24 -0
  72. pymiesimx-0.1.3/tools/release_tag.py +27 -0
  73. pymiesimx-0.1.0/.github/workflows/quality.yml +0 -30
  74. pymiesimx-0.1.0/PKG-INFO +0 -90
  75. pymiesimx-0.1.0/PyMieSimX/__init__.py +0 -5
  76. pymiesimx-0.1.0/PyMieSimX/gui/__init__.py +0 -5
  77. pymiesimx-0.1.0/PyMieSimX/gui/pages/experiment/page.py +0 -49
  78. pymiesimx-0.1.0/PyMieSimX/gui/pages/experiment/sections/configuration.py +0 -42
  79. pymiesimx-0.1.0/PyMieSimX/gui/pages/home.py +0 -152
  80. pymiesimx-0.1.0/PyMieSimX/gui/pages/single/page.py +0 -28
  81. pymiesimx-0.1.0/PyMieSimX/gui/pages/single/sections/representations.py +0 -38
  82. pymiesimx-0.1.0/PyMieSimX/gui/pages/single/sections/setup.py +0 -27
  83. pymiesimx-0.1.0/PyMieSimX.egg-info/PKG-INFO +0 -90
  84. pymiesimx-0.1.0/PyMieSimX.egg-info/requires.txt +0 -16
  85. pymiesimx-0.1.0/README.rst +0 -43
  86. pymiesimx-0.1.0/tests/gui/cli.py +0 -31
  87. pymiesimx-0.1.0/tests/gui/test_services.py +0 -87
  88. {pymiesimx-0.1.0 → pymiesimx-0.1.3}/.flake8 +0 -0
  89. {pymiesimx-0.1.0 → pymiesimx-0.1.3}/.github/dependabot.yml +0 -0
  90. {pymiesimx-0.1.0 → pymiesimx-0.1.3}/.pre-commit-config.yaml +0 -0
  91. {pymiesimx-0.1.0 → pymiesimx-0.1.3}/LICENSE +0 -0
  92. {pymiesimx-0.1.0 → pymiesimx-0.1.3}/PyMieSimX/gui/assets/card-state.js +0 -0
  93. {pymiesimx-0.1.0 → pymiesimx-0.1.3}/PyMieSimX/gui/assets/logo.png +0 -0
  94. {pymiesimx-0.1.0 → pymiesimx-0.1.3}/PyMieSimX/gui/assets/pymiesim-favicon.svg +0 -0
  95. {pymiesimx-0.1.0 → pymiesimx-0.1.3}/PyMieSimX/gui/assets/pymiesim-logo-dark.svg +0 -0
  96. {pymiesimx-0.1.0 → pymiesimx-0.1.3}/PyMieSimX/gui/assets/pymiesim-logo.svg +0 -0
  97. {pymiesimx-0.1.0 → pymiesimx-0.1.3}/PyMieSimX/gui/components/__init__.py +0 -0
  98. {pymiesimx-0.1.0 → pymiesimx-0.1.3}/PyMieSimX/gui/pages/citation.py +0 -0
  99. {pymiesimx-0.1.0 → pymiesimx-0.1.3}/PyMieSimX/gui/pages/experiment/__init__.py +0 -0
  100. {pymiesimx-0.1.0 → pymiesimx-0.1.3}/PyMieSimX/gui/pages/experiment/sections/__init__.py +0 -0
  101. {pymiesimx-0.1.0 → pymiesimx-0.1.3}/PyMieSimX/gui/pages/single/__init__.py +0 -0
  102. {pymiesimx-0.1.0 → pymiesimx-0.1.3}/PyMieSimX/gui/pages/single/sections/__init__.py +0 -0
  103. {pymiesimx-0.1.0 → pymiesimx-0.1.3}/PyMieSimX.egg-info/dependency_links.txt +0 -0
  104. {pymiesimx-0.1.0 → pymiesimx-0.1.3}/PyMieSimX.egg-info/entry_points.txt +0 -0
  105. {pymiesimx-0.1.0 → pymiesimx-0.1.3}/PyMieSimX.egg-info/top_level.txt +0 -0
  106. {pymiesimx-0.1.0 → pymiesimx-0.1.3}/setup.cfg +0 -0
@@ -8,6 +8,7 @@ omit =
8
8
  PyMieSimX/**/__main__.py
9
9
 
10
10
  [report]
11
+ fail_under = 52
11
12
  skip_covered = True
12
13
  show_missing = True
13
14
  precision = 2
@@ -1,19 +1,17 @@
1
- name: Deploy to PyPI
1
+ name: Publish to PyPI
2
2
 
3
3
  on:
4
4
  push:
5
- branches: ["master"]
6
- tags: ["*"]
7
- pull_request:
8
- branches: ["master"]
5
+ tags: ["v*"]
9
6
 
10
7
  permissions:
11
8
  contents: read
12
9
 
13
10
  jobs:
14
- PyPI:
15
- uses: MartinPdeS/MPSActions/.github/workflows/publish_pure_package_to_PyPi.yml@master
11
+ publish:
12
+ uses: MartinPdeS/MPSActions/.github/workflows/publish_pure_package_to_PyPi.yml@v5
16
13
  with:
17
14
  python-version: "3.11"
15
+ test-command: python -c 'import PyMieSimX'
18
16
  secrets:
19
17
  PYPI_API_TOKEN: ${{ secrets.PYPI_API_TOKEN }}
@@ -1,21 +1,20 @@
1
- name: Deploy Conda Packages
1
+ name: Publish Conda package
2
2
 
3
3
  on:
4
4
  push:
5
- branches: [ "master" ]
6
- tags: '*'
7
- pull_request:
8
- branches: [ "master" ]
5
+ tags: ["v*"]
9
6
 
10
7
  permissions:
11
- contents: write
8
+ contents: read
12
9
 
13
10
  jobs:
14
- linux:
15
- uses: MartinPdeS/MPSActions/.github/workflows/publish_compiled_package_to_anaconda.yml@master
11
+ publish:
12
+ uses: MartinPdeS/MPSActions/.github/workflows/publish_compiled_package_to_anaconda.yml@v5
16
13
  with:
17
14
  python_versions: '["3.11", "3.12", "3.13"]'
18
- apt_package: ""
19
15
  os_list: '["ubuntu-latest", "macos-latest", "windows-latest"]'
16
+ package_name: pymiesimx
17
+ recipe_path: conda.recipe
18
+ test_import: PyMieSimX
20
19
  secrets:
21
20
  ANACONDA_API_TOKEN: ${{ secrets.ANACONDA_API_TOKEN }}
@@ -16,11 +16,11 @@ jobs:
16
16
  name: Run GUI tests and publish coverage
17
17
  runs-on: ubuntu-latest
18
18
  steps:
19
- - uses: actions/checkout@v4
19
+ - uses: actions/checkout@fbc6f3992d24b796d5a048ff273f7fcc4a7b6c09
20
20
  with:
21
21
  fetch-depth: 0
22
22
 
23
- - uses: actions/setup-python@v5
23
+ - uses: actions/setup-python@ece7cb06caefa5fff74198d8649806c4678c61a1
24
24
  with:
25
25
  python-version: "3.11"
26
26
 
@@ -29,6 +29,9 @@ jobs:
29
29
  python -m pip install --upgrade pip wheel build
30
30
  python -m pip install -e ".[testing]"
31
31
 
32
+ - name: Initialize material catalog
33
+ run: python -m PyOptik setup --no-progress
34
+
32
35
  - name: Run GUI tests with coverage
33
36
  run: |
34
37
  coverage erase
@@ -0,0 +1,47 @@
1
+ name: Quality and GUI tests
2
+
3
+ on:
4
+ push:
5
+ branches: ["master"]
6
+ pull_request:
7
+ branches: ["master"]
8
+
9
+ permissions:
10
+ contents: read
11
+
12
+ jobs:
13
+ python-quality:
14
+ runs-on: ubuntu-latest
15
+ strategy:
16
+ matrix:
17
+ python-version: ["3.11", "3.13"]
18
+ steps:
19
+ - uses: actions/checkout@fbc6f3992d24b796d5a048ff273f7fcc4a7b6c09
20
+ - uses: actions/setup-python@ece7cb06caefa5fff74198d8649806c4678c61a1
21
+ with:
22
+ python-version: ${{ matrix.python-version }}
23
+ - name: Install package and quality tools
24
+ run: python -m pip install --upgrade pip && python -m pip install -e ".[testing,dev]"
25
+ - name: Initialize material catalog
26
+ run: python -m PyOptik setup --no-progress
27
+ - name: Ruff
28
+ run: python -m ruff check PyMieSimX tests
29
+ - name: Mypy
30
+ run: python -m mypy PyMieSimX/gui/parsing.py PyMieSimX/gui/schemas.py PyMieSimX/gui/models.py PyMieSimX/gui/jobs.py PyMieSimX/gui/callback_helpers.py PyMieSimX/gui/admin.py
31
+ - name: GUI tests
32
+ run: python -m pytest
33
+
34
+ browser-tests:
35
+ runs-on: ubuntu-latest
36
+ steps:
37
+ - uses: actions/checkout@fbc6f3992d24b796d5a048ff273f7fcc4a7b6c09
38
+ - uses: actions/setup-python@ece7cb06caefa5fff74198d8649806c4678c61a1
39
+ with:
40
+ python-version: "3.11"
41
+ - name: Install package and browser test tools
42
+ run: python -m pip install --upgrade pip && python -m pip install -e ".[testing,browser]"
43
+ - name: Initialize material catalog
44
+ run: python -m PyOptik setup --no-progress
45
+ - name: Browser workflow tests
46
+ # The full GUI suite enforces the repository-wide coverage threshold.
47
+ run: python -m pytest tests/browser --config-file=pytest.ini --headless --cov=PyMieSimX --cov-report=term --cov-fail-under=0
@@ -0,0 +1,22 @@
1
+ name: Tests
2
+
3
+ on:
4
+ push:
5
+ branches: [master]
6
+ pull_request:
7
+ branches: [master]
8
+
9
+ permissions:
10
+ contents: read
11
+
12
+ jobs:
13
+ tests:
14
+ runs-on: ubuntu-latest
15
+ steps:
16
+ - uses: actions/checkout@fbc6f3992d24b796d5a048ff273f7fcc4a7b6c09
17
+ - uses: actions/setup-python@ece7cb06caefa5fff74198d8649806c4678c61a1
18
+ with:
19
+ python-version: "3.11"
20
+ - run: python -m pip install --upgrade pip && python -m pip install -e ".[testing]"
21
+ - run: python -m PyOptik setup --no-progress
22
+ - run: python -m pytest -q
@@ -10,3 +10,10 @@ dist/
10
10
  .coverage
11
11
  htmlcov/
12
12
  .DS_Store
13
+ !conda.recipe/
14
+ !conda.recipe/meta.yaml
15
+ !CONTRIBUTING.md
16
+ !makefile
17
+ !Makefile
18
+ !tools/
19
+ !tools/*.py
@@ -0,0 +1,7 @@
1
+ # Contributing to PyMieSimX
2
+
3
+ Use Python 3.10 or newer. Install development dependencies with `python -m pip install -e ".[testing,dev]"`, then run `make quality` and `make test`.
4
+
5
+ Releases use semantic version tags. Run `make patch`, `make minor`, or `make major` from a clean `master` checkout; the command creates the release commit, tags it, and pushes both. Publishing workflows run only for `v*` tags.
6
+
7
+ The Conda recipe is at `conda.recipe/meta.yaml`. Do not commit build artefacts, virtual environments, or caches.
@@ -7,6 +7,7 @@ WORKDIR /app
7
7
  COPY . .
8
8
 
9
9
  RUN python -m pip install --upgrade pip \
10
- && python -m pip install --no-cache-dir .
10
+ && python -m pip install --no-cache-dir . \
11
+ && python -m PyOptik setup --no-progress
11
12
 
12
13
  CMD ["sh", "-c", "exec gunicorn PyMieSimX.gui.interface:app --bind 0.0.0.0:${PORT:-8050} --workers 1 --threads 2 --timeout 3600"]
@@ -0,0 +1,153 @@
1
+ Metadata-Version: 2.4
2
+ Name: PyMieSimX
3
+ Version: 0.1.3
4
+ Summary: Graphical Dash interface for PyMieSim.
5
+ Author-email: Martin Poinsinet de Sivry-Houle <martin.poinsinet.de.sivry@gmail.com>
6
+ License-Expression: MIT
7
+ Project-URL: Repository, https://github.com/MartinPdeS/PyMieSimX
8
+ Requires-Python: >=3.11
9
+ Description-Content-Type: text/x-rst
10
+ License-File: LICENSE
11
+ Requires-Dist: PyMieSim<6,>=5.6.4
12
+ Requires-Dist: PyOptik<4,>=3.2.1
13
+ Requires-Dist: dash<5,>=4.1.0
14
+ Requires-Dist: gunicorn<24,>=23.0.0
15
+ Requires-Dist: numpy<3,>=2.0
16
+ Requires-Dist: pandas<3,>=2.2
17
+ Requires-Dist: plotly<7,>=5.24
18
+ Requires-Dist: psycopg[binary]<4,>=3.2.9
19
+ Provides-Extra: testing
20
+ Requires-Dist: pytest>=8.0; extra == "testing"
21
+ Requires-Dist: pytest-cov>=5.0; extra == "testing"
22
+ Requires-Dist: coverage>=7.0; extra == "testing"
23
+ Provides-Extra: browser
24
+ Requires-Dist: dash[testing]<5,>=4.1; extra == "browser"
25
+ Provides-Extra: dev
26
+ Requires-Dist: build>=1.2; extra == "dev"
27
+ Requires-Dist: mypy==1.15.0; extra == "dev"
28
+ Requires-Dist: ruff==0.11.2; extra == "dev"
29
+ Dynamic: license-file
30
+
31
+ PyMieSimX
32
+ =========
33
+
34
+ PyMieSimX is the standalone graphical interface for `PyMieSim
35
+ <https://github.com/MartinPdeS/PyMieSim>`_. It provides a Dash application for
36
+ configuring optical setups, running parameter sweeps, exploring individual
37
+ particles, and exporting results.
38
+
39
+ Source-model note
40
+ -----------------
41
+
42
+ The Gaussian source exposed by PyMieSimX is not a generalized Lorenz--Mie
43
+ theory (GLMT) implementation. It is a convenience object for defining a
44
+ Gaussian illumination through its numerical aperture and optical power in
45
+ watts. The ``Gaussian`` and ``GaussianSet`` source options should therefore be
46
+ interpreted as a practical source parameterization, not as a separate GLMT
47
+ solver.
48
+
49
+ Installation
50
+ ------------
51
+
52
+ Install the GUI with PyMieSim and PyOptik with::
53
+
54
+ pip install PyMieSimX
55
+
56
+ The first ``pymiesimx`` launch downloads PyOptik's complete
57
+ RefractiveIndex.INFO snapshot. It can also be initialized explicitly with::
58
+
59
+ python -m PyOptik setup --no-progress
60
+
61
+ Launch the dashboard with::
62
+
63
+ pymiesimx
64
+
65
+ Use ``pymiesimx --help`` to see the available host, port, browser, and debug
66
+ options.
67
+
68
+ Development
69
+ -----------
70
+
71
+ Install an editable checkout with::
72
+
73
+ pip install -e .
74
+
75
+ The GUI source lives in ``PyMieSimX/gui``. Scientific calculations are provided
76
+ by the installed ``PyMieSim`` package rather than duplicated here.
77
+
78
+ Testing and automation
79
+ ----------------------
80
+
81
+ Run the GUI test suite with::
82
+
83
+ pip install -e ".[testing]"
84
+ python -m pytest
85
+
86
+ GitHub Actions includes quality checks, GUI tests, PyPI publishing, Conda
87
+ recipe publishing, and coverage deployment. The Conda recipe is in
88
+ ``meta.yaml`` and the container entry point is defined in ``Dockerfile``.
89
+
90
+ Python API
91
+ ----------
92
+
93
+ The computational API can be used without constructing the Dash application::
94
+
95
+ from PyMieSimX import run_experiment
96
+
97
+ result = run_experiment(
98
+ source_type="GaussianSet",
99
+ source_values={
100
+ "wavelength": "650",
101
+ "polarization": "0",
102
+ "optical_power": "1e-3",
103
+ "numerical_aperture": "0.2",
104
+ },
105
+ scatterer_type="SphereSet",
106
+ scatterer_values={"diameter": "500", "material": "1.4", "medium": "1.0"},
107
+ detector_type="None",
108
+ detector_values={},
109
+ measure="Qsca",
110
+ )
111
+
112
+ ``PyMieSimX.create_dash_app`` and ``PyMieSimX.OpticalSetupGUI`` remain
113
+ available for applications that need the graphical interface.
114
+
115
+ Background calculations and limits
116
+ -----------------------------------
117
+
118
+ Parameter sweeps are submitted to a bounded background worker and reported to
119
+ the dashboard through a polling status indicator. Results are guarded by both
120
+ an in-memory dataframe limit and a serialized Dash-payload limit. Large-result
121
+ warnings are logged before execution; oversized results are rejected with an
122
+ actionable message asking you to reduce the sweep.
123
+
124
+ Run the command-line launcher with ``--debug`` to enable detailed logs. Log
125
+ messages use the format ``timestamp | level | logger | message`` and include
126
+ experiment job identifiers, input configuration, queue transitions, dataframe
127
+ memory usage, and serialized result sizes.
128
+
129
+ Server-side usage metrics
130
+ -------------------------
131
+
132
+ The dashboard can share a PostgreSQL metrics database with RosettaX. Configure
133
+ the Render service with::
134
+
135
+ PYMIESIMX_USAGE_METRICS_BACKEND=postgres
136
+ DATABASE_URL=<the shared PostgreSQL URL>
137
+
138
+ PyMieSimX writes namespaced counters to the shared ``metrics_counters`` table:
139
+ ``pymiesimx_home_page_visit_count``, ``pymiesimx_experiment_run_count``, and
140
+ ``pymiesimx_single_run_count``. Metrics are collected only when the PostgreSQL
141
+ backend is explicitly configured and available. Local installations do not
142
+ persist usage counters.
143
+
144
+ Administration page
145
+ -------------------
146
+
147
+ Usage counters are shown only on the hidden, token-protected administration
148
+ page. Configure a server-side token with::
149
+
150
+ PYMIESIMX_ADMIN_TOKEN=<a long random value>
151
+
152
+ Then open ``/admin?token=<the same value>``. The page is intentionally absent
153
+ from the public sidebar and refreshes its counters every 30 seconds.
@@ -0,0 +1,58 @@
1
+ """Python API and optional Dash graphical interface for PyMieSim."""
2
+
3
+ from importlib.metadata import PackageNotFoundError, version
4
+
5
+ from PyMieSimX.api import (
6
+ MAX_SWEEP_COMBINATIONS,
7
+ MAX_RESULT_FRAME_BYTES,
8
+ MAX_RESULT_PAYLOAD_BYTES,
9
+ ExperimentValidationError,
10
+ ResultSizeEstimate,
11
+ ResultSizeLimitError,
12
+ ValidationIssue,
13
+ available_measures,
14
+ build_figure,
15
+ build_single_figure,
16
+ estimate_sweep_size,
17
+ export_result_to_csv,
18
+ export_single_result_to_csv,
19
+ estimate_result_size,
20
+ run_experiment,
21
+ validate_experiment_inputs,
22
+ )
23
+
24
+ try:
25
+ __version__ = version("PyMieSimX")
26
+ except PackageNotFoundError:
27
+ __version__ = "0+unknown"
28
+
29
+ __all__ = [
30
+ "MAX_SWEEP_COMBINATIONS",
31
+ "MAX_RESULT_FRAME_BYTES",
32
+ "MAX_RESULT_PAYLOAD_BYTES",
33
+ "ExperimentValidationError",
34
+ "ResultSizeEstimate",
35
+ "ResultSizeLimitError",
36
+ "ValidationIssue",
37
+ "available_measures",
38
+ "build_figure",
39
+ "build_single_figure",
40
+ "estimate_sweep_size",
41
+ "export_result_to_csv",
42
+ "export_single_result_to_csv",
43
+ "estimate_result_size",
44
+ "run_experiment",
45
+ "validate_experiment_inputs",
46
+ "__version__",
47
+ "OpticalSetupGUI",
48
+ "create_dash_app",
49
+ ]
50
+
51
+
52
+ def __getattr__(name: str):
53
+ """Load the optional Dash interface only when it is explicitly requested."""
54
+ if name in {"OpticalSetupGUI", "create_dash_app"}:
55
+ from PyMieSimX.gui.interface import OpticalSetupGUI, create_dash_app
56
+
57
+ return {"OpticalSetupGUI": OpticalSetupGUI, "create_dash_app": create_dash_app}[name]
58
+ raise AttributeError(f"module {__name__!r} has no attribute {name!r}")
@@ -2,8 +2,10 @@
2
2
 
3
3
  import argparse
4
4
  import logging
5
+ import sys
5
6
 
6
- from PyMieSimX.gui.interface import OpticalSetupGUI
7
+
8
+ LOGGER = logging.getLogger(__name__)
7
9
 
8
10
 
9
11
  def _build_argument_parser() -> argparse.ArgumentParser:
@@ -16,13 +18,25 @@ def _build_argument_parser() -> argparse.ArgumentParser:
16
18
  return parser
17
19
 
18
20
 
19
- def main(argv: list[str] | None = None) -> None:
20
- """Launch the PyMieSimX dashboard from a console entry point."""
21
- args = _build_argument_parser().parse_args(argv)
21
+ def configure_logging(*, debug: bool, log_level: str = "INFO") -> int:
22
+ """Configure application logging using RosettaX's console format."""
23
+ resolved_level = logging.DEBUG if debug else getattr(logging, str(log_level).upper(), logging.INFO)
22
24
  logging.basicConfig(
23
- level=logging.DEBUG if args.debug else logging.INFO,
25
+ level=resolved_level,
24
26
  format="%(asctime)s | %(levelname)s | %(name)s | %(message)s",
27
+ stream=sys.stdout,
28
+ force=True,
25
29
  )
30
+ logging.getLogger("PyMieSimX").setLevel(resolved_level)
31
+ return resolved_level
32
+
33
+
34
+ def main(argv: list[str] | None = None) -> None:
35
+ """Launch the PyMieSimX dashboard from a console entry point."""
36
+ args = _build_argument_parser().parse_args(argv)
37
+ configure_logging(debug=args.debug)
38
+ from PyMieSimX.gui.interface import OpticalSetupGUI
39
+
26
40
  OpticalSetupGUI().run(
27
41
  host=args.host,
28
42
  port=args.port,
@@ -0,0 +1,39 @@
1
+ """Stable, GUI-independent Python API for PyMieSimX computations."""
2
+
3
+ from PyMieSimX.gui.services import (
4
+ MAX_SWEEP_COMBINATIONS,
5
+ MAX_RESULT_FRAME_BYTES,
6
+ MAX_RESULT_PAYLOAD_BYTES,
7
+ ExperimentValidationError,
8
+ ResultSizeEstimate,
9
+ ResultSizeLimitError,
10
+ ValidationIssue,
11
+ available_measures,
12
+ build_figure,
13
+ build_single_figure,
14
+ estimate_sweep_size,
15
+ export_result_to_csv,
16
+ export_single_result_to_csv,
17
+ estimate_result_size,
18
+ run_experiment,
19
+ validate_experiment_inputs,
20
+ )
21
+
22
+ __all__ = [
23
+ "MAX_SWEEP_COMBINATIONS",
24
+ "MAX_RESULT_FRAME_BYTES",
25
+ "MAX_RESULT_PAYLOAD_BYTES",
26
+ "ExperimentValidationError",
27
+ "ResultSizeEstimate",
28
+ "ResultSizeLimitError",
29
+ "ValidationIssue",
30
+ "available_measures",
31
+ "build_figure",
32
+ "build_single_figure",
33
+ "estimate_sweep_size",
34
+ "export_result_to_csv",
35
+ "export_single_result_to_csv",
36
+ "estimate_result_size",
37
+ "run_experiment",
38
+ "validate_experiment_inputs",
39
+ ]
@@ -0,0 +1 @@
1
+ """WSGI compatibility entry points for the PyMieSimX dashboard."""
@@ -0,0 +1,9 @@
1
+ """WSGI entry point for deployments using Gunicorn or another WSGI server."""
2
+
3
+ from PyMieSimX.__main__ import configure_logging
4
+
5
+ configure_logging(debug=False)
6
+
7
+ from PyMieSimX.gui.interface import server # noqa: E402
8
+
9
+ __all__ = ["server"]
@@ -0,0 +1,12 @@
1
+ """Dash dashboard components for PyMieSimX."""
2
+
3
+ __all__ = ["OpticalSetupGUI", "create_dash_app"]
4
+
5
+
6
+ def __getattr__(name: str):
7
+ """Load the Dash application lazily for computational-only imports."""
8
+ if name in __all__:
9
+ from PyMieSimX.gui.interface import OpticalSetupGUI, create_dash_app
10
+
11
+ return {"OpticalSetupGUI": OpticalSetupGUI, "create_dash_app": create_dash_app}[name]
12
+ raise AttributeError(f"module {__name__!r} has no attribute {name!r}")
@@ -0,0 +1,122 @@
1
+ """Token-gated administration dashboard for server-side usage counters."""
2
+
3
+ import hmac
4
+ import os
5
+ from dataclasses import dataclass, field
6
+ from datetime import datetime, timezone
7
+ from typing import Any
8
+
9
+ from dash import dcc, html
10
+
11
+ from PyMieSimX.gui import usage_metrics
12
+ from PyMieSimX.gui.components import Card
13
+
14
+
15
+ ADMIN_TOKEN_ENV_VAR = "PYMIESIMX_ADMIN_TOKEN"
16
+ REFRESH_INTERVAL_MILLISECONDS = 30_000
17
+
18
+
19
+ @dataclass(frozen=True)
20
+ class AdminDashboardData:
21
+ """Snapshot displayed by the administration dashboard."""
22
+
23
+ metrics: usage_metrics.UsageMetrics = field(default_factory=usage_metrics.UsageMetrics)
24
+ generated_at: str = ""
25
+
26
+
27
+ def is_admin_access_granted(provided_token: Any) -> bool:
28
+ """Return whether the provided token exactly matches server configuration."""
29
+ configured_token = os.getenv(ADMIN_TOKEN_ENV_VAR, "").strip()
30
+ candidate_token = str(provided_token or "").strip()
31
+ return bool(configured_token and candidate_token and hmac.compare_digest(configured_token, candidate_token))
32
+
33
+
34
+ def collect_admin_dashboard_data() -> AdminDashboardData:
35
+ """Load the current server-side usage counters."""
36
+ return AdminDashboardData(
37
+ metrics=usage_metrics.load_usage_metrics(),
38
+ generated_at=f"{datetime.now(timezone.utc).strftime('%Y-%m-%d %H:%M:%S')} UTC",
39
+ )
40
+
41
+
42
+ def metric_text(value: object) -> str:
43
+ """Format a metric for display, including unavailable local values."""
44
+ if isinstance(value, float) and value != value:
45
+ return "NaN"
46
+ return str(value)
47
+
48
+
49
+ def dashboard_values(data: AdminDashboardData) -> tuple[str, str, str, str]:
50
+ """Return callback-ready metric values and update timestamp."""
51
+ metrics = data.metrics
52
+ return (
53
+ metric_text(metrics.home_page_visit_count),
54
+ metric_text(metrics.experiment_run_count),
55
+ metric_text(metrics.single_run_count),
56
+ f"Last updated: {data.generated_at}",
57
+ )
58
+
59
+
60
+ def build_admin_page(token: str | None) -> html.Div:
61
+ """Build the hidden dashboard or an intentionally generic denial page."""
62
+ if not is_admin_access_granted(token):
63
+ return html.Div(
64
+ html.Section(
65
+ className=Card.classes(color="blue", extra="admin-access-card"),
66
+ children=[
67
+ html.H2("Page not available"),
68
+ html.P("The requested page is not available on this server."),
69
+ ],
70
+ ),
71
+ className="page-content-stack admin-page",
72
+ )
73
+
74
+ values = dashboard_values(collect_admin_dashboard_data())
75
+ tiles = (
76
+ ("admin-home-page-visits", values[0], "Home page visits"),
77
+ ("admin-experiment-runs", values[1], "Parameter sweeps"),
78
+ ("admin-single-runs", values[2], "Particle explorations"),
79
+ )
80
+ return html.Div(
81
+ className="page-content-stack admin-page",
82
+ children=[
83
+ dcc.Interval(id="admin-refresh-interval", interval=REFRESH_INTERVAL_MILLISECONDS, n_intervals=0),
84
+ html.Section(
85
+ className="page-hero",
86
+ children=[
87
+ html.H1("Administration"),
88
+ html.P("Server-side PyMieSimX usage metrics.", className="hero-text"),
89
+ html.Div(
90
+ [
91
+ html.Span(values[3], id="admin-last-updated"),
92
+ html.Button("Refresh", id="admin-refresh-button", n_clicks=0, className="run-button"),
93
+ ],
94
+ className="admin-toolbar",
95
+ ),
96
+ ],
97
+ ),
98
+ html.Div(
99
+ [
100
+ html.Section(
101
+ className=Card.classes(color="blue", extra="admin-metric-card"),
102
+ children=[
103
+ html.Div(value, id=value_id, className="admin-metric-value"),
104
+ html.Div(label, className="admin-metric-label"),
105
+ ],
106
+ )
107
+ for value_id, value, label in tiles
108
+ ],
109
+ className="admin-metric-grid",
110
+ ),
111
+ ],
112
+ )
113
+
114
+
115
+ __all__ = [
116
+ "ADMIN_TOKEN_ENV_VAR",
117
+ "AdminDashboardData",
118
+ "build_admin_page",
119
+ "collect_admin_dashboard_data",
120
+ "dashboard_values",
121
+ "is_admin_access_granted",
122
+ ]