syncmoss 0.2.0__tar.gz → 0.2.2__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (85) hide show
  1. {syncmoss-0.2.0 → syncmoss-0.2.2}/.gitignore +10 -0
  2. {syncmoss-0.2.0/syncmoss.egg-info → syncmoss-0.2.2}/PKG-INFO +1 -1
  3. {syncmoss-0.2.0 → syncmoss-0.2.2}/bundle/MacOS.spec +47 -17
  4. {syncmoss-0.2.0 → syncmoss-0.2.2}/bundle/Windows.spec +32 -12
  5. {syncmoss-0.2.0 → syncmoss-0.2.2}/bundle/bundle.py +1 -2
  6. {syncmoss-0.2.0 → syncmoss-0.2.2}/bundle/macos_bundle.py +1 -1
  7. {syncmoss-0.2.0 → syncmoss-0.2.2}/pyproject.toml +2 -0
  8. {syncmoss-0.2.0 → syncmoss-0.2.2}/syncmoss/Calibration.py +3 -2
  9. {syncmoss-0.2.0 → syncmoss-0.2.2}/syncmoss/fitting_io.py +117 -118
  10. {syncmoss-0.2.0 → syncmoss-0.2.2}/syncmoss/instrumental_io.py +241 -33
  11. {syncmoss-0.2.0 → syncmoss-0.2.2}/syncmoss/main.py +2 -1
  12. syncmoss-0.2.2/syncmoss/minimi_lib.py +676 -0
  13. {syncmoss-0.2.0 → syncmoss-0.2.2}/syncmoss/model_io.py +63 -9
  14. {syncmoss-0.2.0 → syncmoss-0.2.2}/syncmoss/models.py +20 -3
  15. syncmoss-0.2.2/syncmoss/parameters/Calibration.dat +514 -0
  16. syncmoss-0.2.2/syncmoss/parameters/INSexp.txt +1 -0
  17. syncmoss-0.2.2/syncmoss/parameters/INSint.txt +1 -0
  18. {syncmoss-0.2.0 → syncmoss-0.2.2}/syncmoss/parameters_table.py +59 -3
  19. {syncmoss-0.2.0 → syncmoss-0.2.2}/syncmoss/results_table.py +2 -2
  20. {syncmoss-0.2.0 → syncmoss-0.2.2}/syncmoss/spectrum_io.py +38 -35
  21. {syncmoss-0.2.0 → syncmoss-0.2.2}/syncmoss/spectrum_plotter.py +1 -1
  22. {syncmoss-0.2.0 → syncmoss-0.2.2}/syncmoss/syncmoss_main.py +275 -109
  23. {syncmoss-0.2.0 → syncmoss-0.2.2/syncmoss.egg-info}/PKG-INFO +1 -1
  24. {syncmoss-0.2.0 → syncmoss-0.2.2}/syncmoss.egg-info/SOURCES.txt +3 -0
  25. {syncmoss-0.2.0 → syncmoss-0.2.2}/tests/syncmoss_test.py +0 -33
  26. syncmoss-0.2.2/tests/test_expression_validation.py +103 -0
  27. {syncmoss-0.2.0 → syncmoss-0.2.2}/tests/test_gui_smoke.py +30 -1
  28. syncmoss-0.2.2/tests/test_instrumental_metadata.py +649 -0
  29. syncmoss-0.2.0/syncmoss/minimi_lib.py +0 -607
  30. syncmoss-0.2.0/syncmoss/parameters/INSexp.txt +0 -1
  31. syncmoss-0.2.0/syncmoss/parameters/INSint.txt +0 -1
  32. {syncmoss-0.2.0 → syncmoss-0.2.2}/.github/workflows/_MacOS.yml +0 -0
  33. {syncmoss-0.2.0 → syncmoss-0.2.2}/.github/workflows/_PyPI.yml +0 -0
  34. {syncmoss-0.2.0 → syncmoss-0.2.2}/.github/workflows/_TagRelease.yml +0 -0
  35. {syncmoss-0.2.0 → syncmoss-0.2.2}/.github/workflows/_Tests.yml +0 -0
  36. {syncmoss-0.2.0 → syncmoss-0.2.2}/.github/workflows/_UploadRelease.yml +0 -0
  37. {syncmoss-0.2.0 → syncmoss-0.2.2}/.github/workflows/_Version.yml +0 -0
  38. {syncmoss-0.2.0 → syncmoss-0.2.2}/.github/workflows/_Wheels.yml +0 -0
  39. {syncmoss-0.2.0 → syncmoss-0.2.2}/.github/workflows/_WindowsExe.yml +0 -0
  40. {syncmoss-0.2.0 → syncmoss-0.2.2}/.github/workflows/release.yml +0 -0
  41. {syncmoss-0.2.0 → syncmoss-0.2.2}/.vscode/settings.json +0 -0
  42. {syncmoss-0.2.0 → syncmoss-0.2.2}/COPYING.txt +0 -0
  43. {syncmoss-0.2.0 → syncmoss-0.2.2}/LICENSE +0 -0
  44. {syncmoss-0.2.0 → syncmoss-0.2.2}/Lib_test/test.mdl +0 -0
  45. {syncmoss-0.2.0 → syncmoss-0.2.2}/NOTICE.txt +0 -0
  46. {syncmoss-0.2.0 → syncmoss-0.2.2}/README.md +0 -0
  47. {syncmoss-0.2.0 → syncmoss-0.2.2}/requirements.txt +0 -0
  48. {syncmoss-0.2.0 → syncmoss-0.2.2}/setup.cfg +0 -0
  49. {syncmoss-0.2.0 → syncmoss-0.2.2}/syncmoss/Calibration.dat +0 -0
  50. {syncmoss-0.2.0 → syncmoss-0.2.2}/syncmoss/Hamiltonian_helper.py +0 -0
  51. {syncmoss-0.2.0 → syncmoss-0.2.2}/syncmoss/Library/Hematite mineral test.mdl +0 -0
  52. {syncmoss-0.2.0 → syncmoss-0.2.2}/syncmoss/Library/Magnetite mineral bulk test.mdl +0 -0
  53. {syncmoss-0.2.0 → syncmoss-0.2.2}/syncmoss/Library/Wustite mineral test.mdl +0 -0
  54. {syncmoss-0.2.0 → syncmoss-0.2.2}/syncmoss/Library/alfa iron test.mdl +0 -0
  55. {syncmoss-0.2.0 → syncmoss-0.2.2}/syncmoss/Library_io.py +0 -0
  56. {syncmoss-0.2.0 → syncmoss-0.2.2}/syncmoss/Library_window.py +0 -0
  57. {syncmoss-0.2.0 → syncmoss-0.2.2}/syncmoss/constants.py +0 -0
  58. {syncmoss-0.2.0 → syncmoss-0.2.2}/syncmoss/icons/CheckBox.png +0 -0
  59. {syncmoss-0.2.0 → syncmoss-0.2.2}/syncmoss/icons/CheckBox_.png +0 -0
  60. {syncmoss-0.2.0 → syncmoss-0.2.2}/syncmoss/icons/CheckBox_L.png +0 -0
  61. {syncmoss-0.2.0 → syncmoss-0.2.2}/syncmoss/icons/CheckBox_L2.png +0 -0
  62. {syncmoss-0.2.0 → syncmoss-0.2.2}/syncmoss/icons/DU.png +0 -0
  63. {syncmoss-0.2.0 → syncmoss-0.2.2}/syncmoss/icons/Switch.png +0 -0
  64. {syncmoss-0.2.0 → syncmoss-0.2.2}/syncmoss/icons/UD.png +0 -0
  65. {syncmoss-0.2.0 → syncmoss-0.2.2}/syncmoss/icons/icon_r.ico +0 -0
  66. {syncmoss-0.2.0 → syncmoss-0.2.2}/syncmoss/models_positions.py +0 -0
  67. {syncmoss-0.2.0 → syncmoss-0.2.2}/syncmoss/parameters/ABSorber3.txt +0 -0
  68. {syncmoss-0.2.0 → syncmoss-0.2.2}/syncmoss/parameters/Be.txt +0 -0
  69. {syncmoss-0.2.0 → syncmoss-0.2.2}/syncmoss/parameters/GCMS.txt +0 -0
  70. {syncmoss-0.2.0 → syncmoss-0.2.2}/syncmoss/parameters/INS_APS.txt +0 -0
  71. {syncmoss-0.2.0 → syncmoss-0.2.2}/syncmoss/parameters/KB.txt +0 -0
  72. {syncmoss-0.2.0 → syncmoss-0.2.2}/syncmoss/parameters/NFS.txt +0 -0
  73. {syncmoss-0.2.0 → syncmoss-0.2.2}/syncmoss/support_math.py +0 -0
  74. {syncmoss-0.2.0 → syncmoss-0.2.2}/syncmoss/test_graf.txt +0 -0
  75. {syncmoss-0.2.0 → syncmoss-0.2.2}/syncmoss/test_param.txt +0 -0
  76. {syncmoss-0.2.0 → syncmoss-0.2.2}/syncmoss/theme_dark.json +0 -0
  77. {syncmoss-0.2.0 → syncmoss-0.2.2}/syncmoss/theme_light.json +0 -0
  78. {syncmoss-0.2.0 → syncmoss-0.2.2}/syncmoss.egg-info/dependency_links.txt +0 -0
  79. {syncmoss-0.2.0 → syncmoss-0.2.2}/syncmoss.egg-info/entry_points.txt +0 -0
  80. {syncmoss-0.2.0 → syncmoss-0.2.2}/syncmoss.egg-info/requires.txt +0 -0
  81. {syncmoss-0.2.0 → syncmoss-0.2.2}/syncmoss.egg-info/top_level.txt +0 -0
  82. {syncmoss-0.2.0 → syncmoss-0.2.2}/tests/conftest.py +0 -0
  83. {syncmoss-0.2.0 → syncmoss-0.2.2}/tests/test_library_io.py +0 -0
  84. {syncmoss-0.2.0 → syncmoss-0.2.2}/tests/test_model_io.py +0 -0
  85. {syncmoss-0.2.0 → syncmoss-0.2.2}/tests/test_support_math.py +0 -0
@@ -139,3 +139,13 @@ bundle/Windows/
139
139
  syncmoss/*.mdl
140
140
  syncmoss/*.png
141
141
  syncmoss/*.svg
142
+
143
+ # Deprecated original minimiser, kept locally for reference / equivalence checks
144
+ # (the active implementation is now the optimized minimi_lib.py).
145
+ syncmoss/minimi_lib_old.py
146
+
147
+ # minimi_lib optimization: local-only test tooling and notes (depend on the
148
+ # git-ignored minimi_lib_old.py; kept out of the tracked repo).
149
+ verify_minimi.py
150
+ bench_coldstart.py
151
+ minimi_optimization_notes.txt
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: syncmoss
3
- Version: 0.2.0
3
+ Version: 0.2.2
4
4
  Summary: SYNCmoss — Mössbauer spectroscopy analysis application
5
5
  Author: Sergey Yaroslavtsev
6
6
  License: MIT
@@ -2,30 +2,49 @@
2
2
  from PyInstaller.utils.hooks import collect_data_files, collect_submodules
3
3
  import os
4
4
 
5
- # Hidden imports
6
- hiddenimports = collect_submodules('scipy')
7
- hiddenimports += collect_submodules('PySide6')
8
- hiddenimports += collect_submodules('numba')
5
+ # ── Hidden imports ──────────────────────────────────────────────
6
+ # numba/llvmlite/scipy do a lot of dynamic importing, so collect their submodules
7
+ # (this keeps core modules like scipy._cyutility that the hooks miss). The big
8
+ # unused scipy subpackages are pruned again via scipy_excludes below; numba's own
9
+ # test-suite is filtered out (not needed at runtime, and it emits a wall of
10
+ # "hidden import not found" warnings for its CUDA tests).
11
+ hiddenimports = collect_submodules('numba', filter=lambda name: 'tests' not in name)
9
12
  hiddenimports += collect_submodules('llvmlite')
13
+ hiddenimports += collect_submodules('scipy')
14
+ # Only the Qt modules SYNCmoss actually uses. We deliberately do NOT
15
+ # collect_submodules('PySide6'): that drags in the QtQuick / QtQml / Qt3D /
16
+ # WebEngine frameworks (100+ MB) the app never uses. The PySide6 PyInstaller
17
+ # hook bundles the Qt frameworks/plugins matching just these imports. This
18
+ # matters most on macOS, where the build is universal2 (two architectures) and
19
+ # UPX is not applied.
20
+ hiddenimports += ['PySide6.QtCore', 'PySide6.QtGui', 'PySide6.QtWidgets']
10
21
  # GUI smoke driver lives in tests/syncmoss_test.py; bundling it lets the frozen
11
22
  # binary self-test via `SYNCmoss --test` (see syncmoss/main.py).
12
23
  hiddenimports += ['syncmoss_test']
13
24
 
14
- scipy_data = collect_data_files('scipy')
25
+ # llvmlite carries native data/bitcode that must be collected.
15
26
  llvmlite_data = collect_data_files('llvmlite')
27
+ # On macOS, missing platform input context plugins can lead to non-editable
28
+ # QLineEdit/QTable widgets in frozen apps. Bundle these explicitly.
29
+ qt_plugin_data = collect_data_files(
30
+ 'PySide6',
31
+ includes=[
32
+ 'Qt/plugins/platforms/*',
33
+ 'Qt/plugins/platforminputcontexts/*',
34
+ 'Qt/plugins/styles/*',
35
+ ],
36
+ )
16
37
 
17
38
  syncmoss_dir = os.path.abspath(os.path.join(SPECPATH, '..', 'syncmoss'))
18
39
  project_dir = os.path.abspath(os.path.join(SPECPATH, '..'))
19
40
  tests_dir = os.path.abspath(os.path.join(SPECPATH, '..', 'tests'))
20
41
 
21
- datas = scipy_data + llvmlite_data
42
+ # Application resources (icons/, parameters/, themes, etc.) are copied into the
43
+ # app bundle by macos_bundle.py after the build.
44
+ datas = llvmlite_data + qt_plugin_data
22
45
 
23
- # SYNCmoss only uses PySide6.QtWidgets / QtGui / QtCore. Excluding the large,
24
- # unused Qt frameworks (WebEngine, Quick/QML, 3D, Multimedia, Charts, ...) keeps
25
- # excludes win over collect_submodules above, so they are pruned from the bundle.
26
- # This matters most on macOS, where the build is universal2 (two architectures)
27
- # and UPX compression is not applied. The CI "--test" smoke run guards against
28
- # accidentally excluding something the app actually needs.
46
+ # Unused-Qt Python module excludes (belt-and-suspenders, since PySide6 is no
47
+ # longer force-collected). The CI "--test" smoke run guards against over-excluding.
29
48
  qt_excludes = [
30
49
  'PySide6.QtWebEngineCore', 'PySide6.QtWebEngineWidgets', 'PySide6.QtWebEngineQuick',
31
50
  'PySide6.QtWebChannel', 'PySide6.QtWebSockets', 'PySide6.QtWebView',
@@ -42,6 +61,15 @@ qt_excludes = [
42
61
  'PySide6.QtUiTools', 'PySide6.QtNetworkAuth', 'PySide6.QtRemoteObjects',
43
62
  'PySide6.QtTextToSpeech', 'PySide6.QtScxml', 'PySide6.QtStateMachine',
44
63
  ]
64
+ # scipy is only used for scipy.linalg.eig; exclude the big unused subpackages.
65
+ # (linalg, _lib, special and sparse are kept: scipy.linalg's import chain may
66
+ # touch special/sparse.)
67
+ scipy_excludes = [
68
+ 'scipy.stats', 'scipy.optimize', 'scipy.spatial', 'scipy.interpolate',
69
+ 'scipy.integrate', 'scipy.fft', 'scipy.fftpack', 'scipy.ndimage',
70
+ 'scipy.signal', 'scipy.io', 'scipy.cluster', 'scipy.odr',
71
+ 'scipy.datasets', 'scipy.misc',
72
+ ]
45
73
  # pytest is only used by the source test-suite; it must not be pulled into the
46
74
  # frozen binary (the smoke driver imports it lazily / guarded).
47
75
  other_excludes = ['tkinter', 'PyQt5', 'PyQt6', 'PySide2', 'pytest', '_pytest', 'pluggy']
@@ -53,7 +81,9 @@ a = Analysis(
53
81
  pathex=[project_dir, tests_dir],
54
82
  hooksconfig={
55
83
  'matplotlib': {
56
- 'backends': 'all',
84
+ # Only the backends SYNCmoss uses (Qt for the GUI, Agg/SVG/PDF for
85
+ # saving). 'all' pulls every backend incl. the tk one.
86
+ 'backends': ['Agg', 'QtAgg', 'SVG', 'PDF'],
57
87
  },
58
88
  },
59
89
  binaries=[],
@@ -61,7 +91,7 @@ a = Analysis(
61
91
  hiddenimports=hiddenimports,
62
92
  hookspath=[],
63
93
  runtime_hooks=[],
64
- excludes=qt_excludes + other_excludes,
94
+ excludes=qt_excludes + scipy_excludes + other_excludes,
65
95
  cipher=block_cipher,
66
96
  noarchive=False,
67
97
  )
@@ -76,9 +106,9 @@ exe_gui = EXE(
76
106
  name='SYNCmoss',
77
107
  debug=False,
78
108
  bootloader_ignore_signals=False,
79
- strip=True,
109
+ strip=False,
80
110
  upx=False,
81
- console=True,
111
+ console=False,
82
112
  )
83
113
 
84
114
  coll = COLLECT(
@@ -86,7 +116,7 @@ coll = COLLECT(
86
116
  a.binaries,
87
117
  a.zipfiles,
88
118
  a.datas,
89
- strip=True,
119
+ strip=False,
90
120
  upx=False,
91
121
  upx_exclude=[],
92
122
  name='SYNCmoss',
@@ -3,26 +3,35 @@ from PyInstaller.utils.hooks import collect_data_files, collect_submodules
3
3
  import os
4
4
 
5
5
  # ── Hidden imports ──────────────────────────────────────────────
6
- hiddenimports = collect_submodules('scipy')
7
- hiddenimports += collect_submodules('PySide6')
8
- # Ensure numba and llvmlite submodules are included in the frozen app
9
- hiddenimports += collect_submodules('numba')
6
+ # numba/llvmlite/scipy do a lot of dynamic importing, so collect their submodules
7
+ # (this keeps core modules like scipy._cyutility that the hooks miss). The big
8
+ # unused scipy subpackages are pruned again via scipy_excludes below; numba's own
9
+ # test-suite is filtered out (not needed at runtime, and it emits a wall of
10
+ # "hidden import not found" warnings for its CUDA tests).
11
+ hiddenimports = collect_submodules('numba', filter=lambda name: 'tests' not in name)
10
12
  hiddenimports += collect_submodules('llvmlite')
13
+ hiddenimports += collect_submodules('scipy')
14
+ # Only the Qt modules SYNCmoss actually uses. We deliberately do NOT
15
+ # collect_submodules('PySide6'): that drags in the QtQuick / QtQml / Qt3D /
16
+ # WebEngine frameworks (100+ MB) the app never uses. The PySide6 PyInstaller
17
+ # hook bundles the Qt frameworks/plugins matching just these imports.
18
+ hiddenimports += ['PySide6.QtCore', 'PySide6.QtGui', 'PySide6.QtWidgets']
11
19
  # GUI smoke driver lives in tests/syncmoss_test.py; bundling it lets the frozen
12
20
  # binary self-test via `SYNCmoss --test` (see syncmoss/main.py).
13
21
  hiddenimports += ['syncmoss_test']
14
- scipy_data = collect_data_files('scipy')
15
- # llvmlite carries native data/bitcode that should be collected
22
+
23
+ # scipy is only needed for scipy.linalg.eig (the import-time "dummy" in
24
+ # models.py); scipy_excludes below drops the big unused subpackages.
25
+ # llvmlite carries native data/bitcode that must be collected.
16
26
  llvmlite_data = collect_data_files('llvmlite')
17
27
 
18
28
  syncmoss_dir = os.path.abspath(os.path.join(SPECPATH, '..', 'syncmoss'))
19
29
  project_dir = os.path.abspath(os.path.join(SPECPATH, '..'))
20
30
  tests_dir = os.path.abspath(os.path.join(SPECPATH, '..', 'tests'))
21
31
 
22
- # Only Python library data (scipy, etc.) goes through PyInstaller.
23
- # Application resources (icons/, parameters/, themes, etc.) are copied
24
- # next to the .exe by bundle.py after the build.
25
- datas = scipy_data + llvmlite_data
32
+ # Application resources (icons/, parameters/, themes, etc.) are copied next to
33
+ # the .exe by bundle.py after the build.
34
+ datas = llvmlite_data
26
35
 
27
36
  # SYNCmoss only uses PySide6.QtWidgets / QtGui / QtCore. Excluding the large,
28
37
  # unused Qt frameworks keeps the bundle small; excludes win over the
@@ -44,6 +53,15 @@ qt_excludes = [
44
53
  'PySide6.QtUiTools', 'PySide6.QtNetworkAuth', 'PySide6.QtRemoteObjects',
45
54
  'PySide6.QtTextToSpeech', 'PySide6.QtScxml', 'PySide6.QtStateMachine',
46
55
  ]
56
+ # scipy is only used for scipy.linalg.eig; exclude the big unused subpackages.
57
+ # (linalg, _lib, special and sparse are kept: scipy.linalg's import chain may
58
+ # touch special/sparse.)
59
+ scipy_excludes = [
60
+ 'scipy.stats', 'scipy.optimize', 'scipy.spatial', 'scipy.interpolate',
61
+ 'scipy.integrate', 'scipy.fft', 'scipy.fftpack', 'scipy.ndimage',
62
+ 'scipy.signal', 'scipy.io', 'scipy.cluster', 'scipy.odr',
63
+ 'scipy.datasets', 'scipy.misc',
64
+ ]
47
65
  # pytest is only used by the source test-suite; it must not be pulled into the
48
66
  # frozen binary (the smoke driver imports it lazily / guarded).
49
67
  other_excludes = ['tkinter', 'PyQt5', 'PyQt6', 'PySide2', 'pytest', '_pytest', 'pluggy']
@@ -55,7 +73,9 @@ a = Analysis(
55
73
  pathex=[project_dir, tests_dir],
56
74
  hooksconfig={
57
75
  'matplotlib': {
58
- 'backends': 'all',
76
+ # Only the backends SYNCmoss uses (Qt for the GUI, Agg/SVG/PDF for
77
+ # saving). 'all' pulls every backend incl. the tk one.
78
+ 'backends': ['Agg', 'QtAgg', 'SVG', 'PDF'],
59
79
  },
60
80
  },
61
81
  binaries=[],
@@ -63,7 +83,7 @@ a = Analysis(
63
83
  hiddenimports=hiddenimports,
64
84
  hookspath=[],
65
85
  runtime_hooks=[],
66
- excludes=qt_excludes + other_excludes,
86
+ excludes=qt_excludes + scipy_excludes + other_excludes,
67
87
  cipher=block_cipher,
68
88
  noarchive=False,
69
89
  )
@@ -66,7 +66,6 @@ def copy_resources():
66
66
  # from syncmoss/
67
67
  os.path.join(syncmoss_DIR, "theme_dark.json"),
68
68
  os.path.join(syncmoss_DIR, "theme_light.json"),
69
- os.path.join(syncmoss_DIR, "Calibration.dat"),
70
69
  # from repo root
71
70
  os.path.join(ROOT_DIR, "COPYING.txt"),
72
71
  os.path.join(ROOT_DIR, "NOTICE.txt"),
@@ -117,7 +116,7 @@ def verify():
117
116
  "parameters/INSint.txt",
118
117
  "theme_dark.json",
119
118
  "theme_light.json",
120
- "Calibration.dat",
119
+ "parameters/Calibration.dat",
121
120
  "COPYING.txt",
122
121
  "NOTICE.txt",
123
122
  "LICENSE",
@@ -69,7 +69,6 @@ def copy_resources(app_dir: str):
69
69
  root_files = [
70
70
  os.path.join(SYNCMOSS_DIR, "theme_dark.json"),
71
71
  os.path.join(SYNCMOSS_DIR, "theme_light.json"),
72
- os.path.join(SYNCMOSS_DIR, "Calibration.dat"),
73
72
  os.path.join(ROOT_DIR, "COPYING.txt"),
74
73
  os.path.join(ROOT_DIR, "NOTICE.txt"),
75
74
  os.path.join(ROOT_DIR, "LICENSE"),
@@ -113,6 +112,7 @@ def verify(app_dir: str):
113
112
  "Contents/MacOS/icons/CheckBox.png",
114
113
  "Contents/MacOS/parameters/Be.txt",
115
114
  "Contents/MacOS/parameters/KB.txt",
115
+ "Contents/MacOS/parameters/Calibration.dat",
116
116
  "Contents/MacOS/theme_dark.json",
117
117
  "Contents/MacOS/theme_light.json",
118
118
  ]
@@ -58,6 +58,8 @@ syncmoss = [
58
58
  "icons/*.ico",
59
59
  "icons/*.svg",
60
60
  "parameters/*.txt",
61
+ "parameters/*.dat",
62
+ "parameters/*.png",
61
63
  "*.json",
62
64
  "*.dat",
63
65
  ]
@@ -31,6 +31,7 @@ import syncmoss.minimi_lib as mi
31
31
  import syncmoss.models as m5
32
32
  import multiprocessing as mp
33
33
  import matplotlib.pyplot as plt
34
+ import os
34
35
  import platform
35
36
  import re
36
37
  import time
@@ -541,7 +542,7 @@ def Calibration(dir_path, Cal_file, pool, VVV, INS, JN, x0, MulCo, Vel_start = 1
541
542
  model = ['Sextet', 'Sextet', 'Doublet']
542
543
 
543
544
  try:
544
- Be_param = np.genfromtxt(str(dir_path) + str('\\\\parameters\\\\Be.txt')*(platform.system() == 'Windows') + str('/parameters/Be.txt')*(platform.system() != 'Windows'), delimiter='\t', skip_footer=0)
545
+ Be_param = np.genfromtxt(os.path.join(dir_path, 'Be.txt'), delimiter='\t', skip_footer=0)
545
546
  print('file was read')
546
547
  except:
547
548
  Be_param = np.array([0.057, 0.066, -0.261, 0.098, 0.375, 0.772, 1])
@@ -742,7 +743,7 @@ def Calibration(dir_path, Cal_file, pool, VVV, INS, JN, x0, MulCo, Vel_start = 1
742
743
 
743
744
  ax.text(len(xn2)/2, max(id[0]) + 10 * np.sqrt(max(id[0])), str('lin ')*(method==1) + str('sin ')*(method==0) + str(n1) + str(' ') + str(n2), color='r', fontsize=8, horizontalalignment='center')
744
745
 
745
- fig.savefig('calibr.png', bbox_inches='tight')
746
+ fig.savefig(os.path.join(dir_path, 'calibr.png'), bbox_inches='tight')
746
747
  plt.close()
747
748
 
748
749
  print('Shift due to instrumental function ', INS_shift)
@@ -14,7 +14,11 @@ from syncmoss.constants import number_of_baseline_parameters, numco
14
14
  from syncmoss.model_io import mod_len_def as mod_len_def_full, read_model as read_model_full
15
15
  from syncmoss.models_positions import mod_pos
16
16
  from syncmoss.spectrum_io import load_spectrum
17
- from syncmoss.instrumental_io import resolve_sms_instrumental_for_file
17
+ from syncmoss.instrumental_io import (
18
+ resolve_instrumental_for_file,
19
+ compute_norm,
20
+ same_method_params,
21
+ )
18
22
 
19
23
 
20
24
 
@@ -296,62 +300,95 @@ def fit_single_spectrum(app, spectrum_file, pool, background=None, sequence_para
296
300
  print(f"[Fitting] X range: {A[0]:.2f} to {A[-1]:.2f}")
297
301
  print(f"[Fitting] Y range: {B.min():.2f} to {B.max():.2f}")
298
302
 
299
- # Determine fitting method
300
- if app.MS_fit.isChecked():
301
- VVV = 1 # MS method
302
- experimental_method = 1
303
- elif app.SMS_fit.isChecked():
304
- VVV = 3 # SMS method
305
- experimental_method = 3
306
- else:
303
+ # A method checkbox must be selected (it is the fallback when a spectrum
304
+ # carries no .dat instrumental metadata)
305
+ if not app.MS_fit.isChecked() and not app.SMS_fit.isChecked():
307
306
  return {
308
307
  'success': False,
309
308
  'message': 'No fitting method selected (MS or SMS)'
310
309
  }
311
-
312
- # Get instrumental function parameters
310
+
311
+ # Resolve instrumental parameters per spectrum. Each spectrum may be CMS
312
+ # or SMS depending on its own .dat metadata (#@GCMS vs #@INSexp/#@INSint)
313
+ # when the "use instrumental function from .dat file" option is enabled;
314
+ # otherwise the UI-selected method with the internal values is used.
313
315
  JN = int(app.JN0)
314
-
315
- if VVV == 1: # MS method
316
- # CMS method with MulCoCMS
317
- INS = float(app.L0.text())
318
- x0_val = 0.0
319
- MulCo_val = app.MulCoCMS
320
-
321
- # Calculate normalization integral
322
- pNorm = np.array([float(0)] * number_of_baseline_parameters)
323
- pNorm[0] = 1
324
- Norm = m5.TI(np.array([float(1000)]), pNorm, [], JN, pool, 0.0, MulCo_val, INS, [0], [0], Met=1)[0]
325
- print('Normalization integral equal to', Norm)
326
-
316
+ use_dat_metadata = bool(getattr(app, 'use_dat_instrumental_metadata', True))
317
+ files_for_ins = list(spectrum_files) if is_simultaneous else [spectrum_file]
318
+
319
+ method_params_list = []
320
+ for ins_file in files_for_ins:
321
+ mp_i = resolve_instrumental_for_file(app, ins_file, use_dat_metadata=use_dat_metadata)
322
+ mp_i['Norm'] = compute_norm(pool, JN, mp_i)
323
+ print('Normalization integral equal to', mp_i['Norm'])
324
+ method_params_list.append(mp_i)
325
+ mp0 = method_params_list[0]
326
+
327
+ note_lines = [mp_i['note'] for mp_i in method_params_list]
328
+ if len({mp_i['method'] for mp_i in method_params_list}) > 1:
329
+ note_lines.insert(0, "Mixed-method simultaneous fit: CMS and SMS spectra are fitted together.")
330
+ instrumental_note = '\n'.join(note_lines)
331
+ print(f"[Fitting] {instrumental_note}")
332
+
333
+ if is_simultaneous:
334
+ # Split model at Nbaseline boundaries
335
+ model_separate = []
336
+ startM = 0
337
+ for i in range(len(model)):
338
+ if model[i] == 'Nbaseline':
339
+ model_separate.append(model[startM:i])
340
+ startM = i + 1
341
+ model_separate.append(model[startM:])
342
+
343
+ # Calculate parameter indices for each spectrum
344
+ begining_spc = [0]
345
+ start_cont_par = number_of_baseline_parameters
346
+ param_names_full = app.params_table.get_parameter_names()
347
+ for i in range(1, len(param_names_full)):
348
+ param_names = param_names_full[i]
349
+ if len(param_names) > 0 and param_names[0] == 'Ns': # Start of new spectrum
350
+ begining_spc.append(start_cont_par)
351
+ for j in range(len(param_names)):
352
+ if param_names[j] != '':
353
+ start_cont_par += 1
354
+
355
+ print(f"[Fitting] Simultaneous - model_separate: {model_separate}")
356
+ print(f"[Fitting] Simultaneous - begining_spc: {begining_spc}")
357
+
358
+ # Per-section slices of the Distri/Cor expression lists (used after the
359
+ # fit to rebuild each section's sub-spectra for plotting)
360
+ distr_bounds = np.cumsum([0] + [ms.count('Distr') for ms in model_separate])
361
+ corr_bounds = np.cumsum([0] + [ms.count('Corr') for ms in model_separate])
362
+
363
+ def section_parameters(p_full, idx):
364
+ if idx < len(begining_spc) - 1:
365
+ return p_full[begining_spc[idx]:begining_spc[idx + 1]]
366
+ return p_full[begining_spc[idx]:]
367
+
368
+ uniform_method = all(same_method_params(mp0, mp_i) for mp_i in method_params_list[1:])
369
+
370
+ if not is_simultaneous or uniform_method:
371
+ # Uniform instrumental settings: a single TI call over the whole model
372
+ # (TI splits Nbaseline sections internally) — the original code path.
327
373
  def func(x, p):
328
- return m5.TI(x, p, model, JN, pool, 0.0, MulCo_val, INS, Distri, Cor, Met=1, Norm=Norm)
329
-
330
- elif VVV == 3: # SMS method
331
- # SMS method: optionally read INS metadata from .dat, otherwise fallback to global files
332
- if is_simultaneous:
333
- selected_files = app.parse_process_path()
334
- spectrum_for_ins = selected_files[0] if selected_files else spectrum_file
335
- else:
336
- spectrum_for_ins = spectrum_file
337
-
338
- use_dat_metadata = bool(getattr(app, 'use_dat_instrumental_metadata', True))
339
- INS, MulCo_val, x0_val, instrumental_note = resolve_sms_instrumental_for_file(
340
- app,
341
- spectrum_for_ins,
342
- use_dat_metadata=use_dat_metadata,
343
- )
344
- print(f"[Fitting] {instrumental_note}")
345
-
346
- # Calculate normalization integral
347
- pNorm = np.array([float(0)] * number_of_baseline_parameters)
348
- pNorm[0] = 1
349
- Norm = m5.TI(np.array([float(1000)]), pNorm, [], JN, pool, x0_val, MulCo_val, INS, [0], [0])[0]
350
- print('Normalization integral equal to', Norm)
351
-
374
+ return m5.TI(x, p, model, JN, pool, mp0['x0'], mp0['MulCo'], mp0['INS'],
375
+ Distri, Cor, Met=mp0['Met'], Norm=mp0['Norm'])
376
+ else:
377
+ # Dedicated per-section instrumental parameters (e.g. mixing CMS and
378
+ # SMS): TI receives one value per section as lists. The full model and
379
+ # full p are still passed, so cross-spectrum links and Distr/Cor p[i]
380
+ # references resolve exactly as in the uniform path — no per-section
381
+ # bookkeeping leaks into this module.
382
+ x0_list = [mp_i['x0'] for mp_i in method_params_list]
383
+ mulco_list = [mp_i['MulCo'] for mp_i in method_params_list]
384
+ ins_list = [mp_i['INS'] for mp_i in method_params_list]
385
+ met_list = [mp_i['Met'] for mp_i in method_params_list]
386
+ norm_list = [mp_i['Norm'] for mp_i in method_params_list]
387
+
352
388
  def func(x, p):
353
- return m5.TI(x, p, model, JN, pool, x0_val, MulCo_val, INS, Distri, Cor, Norm=Norm)
354
-
389
+ return m5.TI(x, p, model, JN, pool, x0_list, mulco_list, ins_list,
390
+ Distri, Cor, Met=met_list, Norm=norm_list)
391
+
355
392
  # Set up bounds and fixed parameters
356
393
  # For now, use unbounded optimization
357
394
  bounds = np.array([[-np.inf] * len(p), [np.inf] * len(p)], dtype=float)
@@ -480,56 +517,28 @@ def fit_single_spectrum(app, spectrum_file, pool, background=None, sequence_para
480
517
  SPC_f = func(A, p)
481
518
 
482
519
  # For simultaneous fitting, we need to separate results for each spectrum
520
+ # (model_separate and begining_spc were computed before the minimization)
483
521
  if is_simultaneous:
484
- # Separate model and parameters for each spectrum
485
- model_separate = []
486
- startM = 0
487
- for i in range(len(model)):
488
- if model[i] == 'Nbaseline':
489
- model_separate.append(model[startM:i])
490
- startM = i + 1
491
- model_separate.append(model[startM:])
492
-
493
- # Calculate parameter indices for each spectrum
494
- begining_spc = [0]
495
- start_cont_par = number_of_baseline_parameters
496
- param_names_full = app.params_table.get_parameter_names()
497
- for i in range(1, len(param_names_full)):
498
- param_names = param_names_full[i]
499
- if len(param_names) > 0 and param_names[0] == 'Ns': # Start of new spectrum
500
- begining_spc.append(start_cont_par)
501
- for j in range(len(param_names)):
502
- if param_names[j] != '':
503
- start_cont_par += 1
504
-
505
- print(f"[Fitting] Simultaneous - model_separate: {model_separate}")
506
- print(f"[Fitting] Simultaneous - begining_spc: {begining_spc}")
507
-
508
522
  # Substitute Distri and Cor parameter values ONCE using full model and full p
509
523
  # This ensures constrained parameters are correctly substituted
510
524
  Distri_substituted = np.copy(Distri)
511
525
  Cor_substituted = np.copy(Cor)
512
526
  if len(Distri) > 0 or len(Cor) > 0:
513
527
  _, _, Distri_substituted, Cor_substituted, _, _ = create_subspectra(app, model, Distri, Cor, p)
514
-
515
- # Calculate fitted spectrum for each section separately
516
- # Use model_separate (without Nbaseline) and parameter subset for each
528
+
529
+ # Calculate fitted spectrum for each section separately, each with the
530
+ # instrumental parameters resolved for that section's spectrum
517
531
  SPC_f_list = []
518
532
  for NumSpc in range(number_of_spectra):
519
- # Get parameters for this spectrum
520
- if NumSpc < len(begining_spc) - 1:
521
- p_separate = p[begining_spc[NumSpc]:begining_spc[NumSpc + 1]]
522
- else:
523
- p_separate = p[begining_spc[NumSpc]:]
524
-
525
- # Calculate spectrum using model_separate and p_separate
526
- # Use pre-substituted Distri and Cor
527
- model_for_spectrum = model_separate[NumSpc]
528
- if VVV == 1: # MS method
529
- SPC_f_separate = m5.TI(A_list[NumSpc], p_separate, model_for_spectrum, JN, pool, 0.0, MulCo_val, INS, Distri_substituted, Cor_substituted, Met=1, Norm=Norm)
530
- elif VVV == 3: # SMS method
531
- SPC_f_separate = m5.TI(A_list[NumSpc], p_separate, model_for_spectrum, JN, pool, x0_val, MulCo_val, INS, Distri_substituted, Cor_substituted, Norm=Norm)
532
-
533
+ p_separate = section_parameters(p, NumSpc)
534
+ mp_i = method_params_list[NumSpc]
535
+ d_slice = list(Distri_substituted[distr_bounds[NumSpc]:distr_bounds[NumSpc + 1]])
536
+ c_slice = list(Cor_substituted[corr_bounds[NumSpc]:corr_bounds[NumSpc + 1]])
537
+ SPC_f_separate = m5.TI(A_list[NumSpc], p_separate, model_separate[NumSpc], JN, pool,
538
+ mp_i['x0'], mp_i['MulCo'], mp_i['INS'],
539
+ d_slice if len(d_slice) > 0 else [0],
540
+ c_slice if len(c_slice) > 0 else [0],
541
+ Met=mp_i['Met'], Norm=mp_i['Norm'])
533
542
  SPC_f_list.append(SPC_f_separate)
534
543
 
535
544
  # Now calculate subspectra for plotting
@@ -547,15 +556,12 @@ def fit_single_spectrum(app, spectrum_file, pool, background=None, sequence_para
547
556
  Cor_work = np.copy(Cor)
548
557
 
549
558
  for NumSpc in range(number_of_spectra):
550
- # Get parameters for this spectrum
551
- if NumSpc < len(begining_spc) - 1:
552
- p_separate = p[begining_spc[NumSpc]:begining_spc[NumSpc + 1]]
553
- else:
554
- p_separate = p[begining_spc[NumSpc]:]
555
-
559
+ p_separate = section_parameters(p, NumSpc)
560
+ mp_i = method_params_list[NumSpc]
561
+
556
562
  # Calculate subspectra
557
563
  Ps, Psm, Distri_t, Cor_t, Di, Co = create_subspectra(app, model_separate[NumSpc], Distri_work, Cor_work, p_separate)
558
-
564
+
559
565
  FS = []
560
566
  FS_pos = []
561
567
  for i in range(len(Ps)):
@@ -563,16 +569,13 @@ def fit_single_spectrum(app, spectrum_file, pool, background=None, sequence_para
563
569
  DiSt = sum([Psm[j].count('Distr') for j in range(i)])
564
570
  CoEn = CoSt + Psm[i].count('Corr')
565
571
  DiEn = DiSt + Psm[i].count('Distr')
566
-
567
- if VVV == 1: # MS method
568
- subspectrum = m5.TI(A_list[NumSpc], Ps[i], Psm[i], JN, pool, 0.0, MulCo_val, INS,
569
- Distri_t[DiSt:DiEn], Cor_t[CoSt:CoEn], Met=1, Norm=Norm)
570
- positions = mod_pos(Ps[i], Psm[i], INS, Met=1)
571
- elif VVV == 3: # SMS method
572
- subspectrum = m5.TI(A_list[NumSpc], Ps[i], Psm[i], JN, pool, x0_val, MulCo_val, INS,
573
- Distri_t[DiSt:DiEn], Cor_t[CoSt:CoEn], Norm=Norm)
574
- positions = mod_pos(Ps[i], Psm[i], INS)
575
-
572
+
573
+ subspectrum = m5.TI(A_list[NumSpc], Ps[i], Psm[i], JN, pool,
574
+ mp_i['x0'], mp_i['MulCo'], mp_i['INS'],
575
+ Distri_t[DiSt:DiEn], Cor_t[CoSt:CoEn],
576
+ Met=mp_i['Met'], Norm=mp_i['Norm'])
577
+ positions = mod_pos(Ps[i], Psm[i], mp_i['INS'], Met=mp_i['Met'])
578
+
576
579
  FS.append(subspectrum)
577
580
  FS_pos.append(positions)
578
581
 
@@ -623,16 +626,12 @@ def fit_single_spectrum(app, spectrum_file, pool, background=None, sequence_para
623
626
  CoEn = CoSt + Psm[i].count('Corr')
624
627
  DiEn = DiSt + Psm[i].count('Distr')
625
628
  print(Ps[i], Psm[i])
626
- if VVV == 1: # MS method
627
- subspectrum = m5.TI(A, Ps[i], Psm[i], JN, pool, 0.0, MulCo_val, INS,
628
- Distri_t[DiSt:DiEn], Cor_t[CoSt:CoEn], Met=1, Norm=Norm)
629
- # Calculate positions for this subspectrum
630
- positions = mod_pos(Ps[i], Psm[i], INS, Met=1)
631
- elif VVV == 3: # SMS method
632
- subspectrum = m5.TI(A, Ps[i], Psm[i], JN, pool, x0_val, MulCo_val, INS,
633
- Distri_t[DiSt:DiEn], Cor_t[CoSt:CoEn], Norm=Norm)
634
- # Calculate positions for this subspectrum
635
- positions = mod_pos(Ps[i], Psm[i], INS)
629
+ subspectrum = m5.TI(A, Ps[i], Psm[i], JN, pool,
630
+ mp0['x0'], mp0['MulCo'], mp0['INS'],
631
+ Distri_t[DiSt:DiEn], Cor_t[CoSt:CoEn],
632
+ Met=mp0['Met'], Norm=mp0['Norm'])
633
+ # Calculate positions for this subspectrum
634
+ positions = mod_pos(Ps[i], Psm[i], mp0['INS'], Met=mp0['Met'])
636
635
  FS.append(subspectrum)
637
636
  FS_pos.append(positions)
638
637