syncmoss 0.3.0__tar.gz → 0.4.1__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 (185) hide show
  1. syncmoss-0.4.1/.github/workflows/_MacOS.yml +451 -0
  2. {syncmoss-0.3.0 → syncmoss-0.4.1}/.github/workflows/_Tests.yml +6 -3
  3. {syncmoss-0.3.0 → syncmoss-0.4.1}/.gitignore +8 -0
  4. {syncmoss-0.3.0 → syncmoss-0.4.1}/PKG-INFO +1 -1
  5. {syncmoss-0.3.0 → syncmoss-0.4.1}/pyproject.toml +15 -1
  6. {syncmoss-0.3.0 → syncmoss-0.4.1}/syncmoss/Calibration.py +10 -4
  7. {syncmoss-0.3.0 → syncmoss-0.4.1}/syncmoss/Library_window.py +1 -1
  8. {syncmoss-0.3.0 → syncmoss-0.4.1}/syncmoss/constants.py +20 -0
  9. {syncmoss-0.3.0 → syncmoss-0.4.1}/syncmoss/fitting_io.py +4 -2
  10. syncmoss-0.4.1/syncmoss/instrumental_io.py +2001 -0
  11. syncmoss-0.4.1/syncmoss/instrumental_window.py +89 -0
  12. {syncmoss-0.3.0 → syncmoss-0.4.1}/syncmoss/main.py +1 -1
  13. {syncmoss-0.3.0 → syncmoss-0.4.1}/syncmoss/model_io.py +1 -1
  14. {syncmoss-0.3.0 → syncmoss-0.4.1}/syncmoss/models.py +27 -5
  15. {syncmoss-0.3.0 → syncmoss-0.4.1}/syncmoss/models_positions.py +10 -2
  16. syncmoss-0.4.1/syncmoss/parameters/Calibration.dat +513 -0
  17. syncmoss-0.4.1/syncmoss/parameters/INSexp.txt +1 -0
  18. syncmoss-0.4.1/syncmoss/parameters/INSint.txt +1 -0
  19. syncmoss-0.4.1/syncmoss/parameters/INSth.txt +1 -0
  20. syncmoss-0.4.1/syncmoss/parameters/calibr.png +0 -0
  21. {syncmoss-0.3.0 → syncmoss-0.4.1}/syncmoss/parameters/help.md +23 -0
  22. syncmoss-0.4.1/syncmoss/sms_theory.py +2264 -0
  23. {syncmoss-0.3.0 → syncmoss-0.4.1}/syncmoss/spectrum_io.py +37 -11
  24. {syncmoss-0.3.0 → syncmoss-0.4.1}/syncmoss/spectrum_plotter.py +52 -0
  25. syncmoss-0.4.1/syncmoss/supp_menu.py +684 -0
  26. {syncmoss-0.3.0 → syncmoss-0.4.1}/syncmoss/syncmoss_main.py +244 -53
  27. {syncmoss-0.3.0 → syncmoss-0.4.1}/syncmoss.egg-info/PKG-INFO +1 -1
  28. {syncmoss-0.3.0 → syncmoss-0.4.1}/syncmoss.egg-info/SOURCES.txt +13 -0
  29. syncmoss-0.4.1/syncmoss.egg-info/scm_file_list.json +172 -0
  30. {syncmoss-0.3.0 → syncmoss-0.4.1}/tests/conftest.py +72 -0
  31. syncmoss-0.4.1/tests/syncmoss_test.py +628 -0
  32. syncmoss-0.4.1/tests/test_calibration_path.py +95 -0
  33. {syncmoss-0.3.0 → syncmoss-0.4.1}/tests/test_instrumental_metadata.py +62 -0
  34. syncmoss-0.4.1/tests/test_instrumental_method_choice.py +155 -0
  35. syncmoss-0.4.1/tests/test_instrumental_recentre.py +136 -0
  36. syncmoss-0.4.1/tests/test_instrumental_window.py +121 -0
  37. syncmoss-0.4.1/tests/test_integral_points.py +104 -0
  38. syncmoss-0.4.1/tests/test_library_dir.py +239 -0
  39. syncmoss-0.4.1/tests/test_search_guards.py +129 -0
  40. syncmoss-0.4.1/tests/test_sms_theory.py +519 -0
  41. syncmoss-0.4.1/tests/test_theory_search.py +152 -0
  42. syncmoss-0.3.0/.github/workflows/_MacOS.yml +0 -329
  43. syncmoss-0.3.0/syncmoss/instrumental_io.py +0 -800
  44. syncmoss-0.3.0/syncmoss/parameters/Calibration.dat +0 -513
  45. syncmoss-0.3.0/syncmoss/parameters/INSexp.txt +0 -1
  46. syncmoss-0.3.0/syncmoss/parameters/INSint.txt +0 -1
  47. syncmoss-0.3.0/syncmoss/parameters/calibr.png +0 -0
  48. syncmoss-0.3.0/syncmoss/supp_menu.py +0 -295
  49. syncmoss-0.3.0/tests/syncmoss_test.py +0 -342
  50. {syncmoss-0.3.0 → syncmoss-0.4.1}/.github/workflows/Tests.yml +0 -0
  51. {syncmoss-0.3.0 → syncmoss-0.4.1}/.github/workflows/_PyPI.yml +0 -0
  52. {syncmoss-0.3.0 → syncmoss-0.4.1}/.github/workflows/_TagRelease.yml +0 -0
  53. {syncmoss-0.3.0 → syncmoss-0.4.1}/.github/workflows/_UploadRelease.yml +0 -0
  54. {syncmoss-0.3.0 → syncmoss-0.4.1}/.github/workflows/_Version.yml +0 -0
  55. {syncmoss-0.3.0 → syncmoss-0.4.1}/.github/workflows/_Wheels.yml +0 -0
  56. {syncmoss-0.3.0 → syncmoss-0.4.1}/.github/workflows/_WindowsExe.yml +0 -0
  57. {syncmoss-0.3.0 → syncmoss-0.4.1}/.github/workflows/release.yml +0 -0
  58. {syncmoss-0.3.0 → syncmoss-0.4.1}/.vscode/settings.json +0 -0
  59. {syncmoss-0.3.0 → syncmoss-0.4.1}/COPYING.txt +0 -0
  60. {syncmoss-0.3.0 → syncmoss-0.4.1}/LICENSE +0 -0
  61. {syncmoss-0.3.0 → syncmoss-0.4.1}/Lib_test/test.mdl +0 -0
  62. {syncmoss-0.3.0 → syncmoss-0.4.1}/NOTICE.txt +0 -0
  63. {syncmoss-0.3.0 → syncmoss-0.4.1}/README.md +0 -0
  64. {syncmoss-0.3.0 → syncmoss-0.4.1}/bundle/MacOS.spec +0 -0
  65. {syncmoss-0.3.0 → syncmoss-0.4.1}/bundle/Windows.spec +0 -0
  66. {syncmoss-0.3.0 → syncmoss-0.4.1}/bundle/bundle.py +0 -0
  67. {syncmoss-0.3.0 → syncmoss-0.4.1}/bundle/macos_bundle.py +0 -0
  68. {syncmoss-0.3.0 → syncmoss-0.4.1}/docs/thick_models.md +0 -0
  69. {syncmoss-0.3.0 → syncmoss-0.4.1}/docs/thick_vs_thin.md +0 -0
  70. {syncmoss-0.3.0 → syncmoss-0.4.1}/requirements.txt +0 -0
  71. {syncmoss-0.3.0 → syncmoss-0.4.1}/setup.cfg +0 -0
  72. {syncmoss-0.3.0 → syncmoss-0.4.1}/syncmoss/Hamiltonian_helper.py +0 -0
  73. {syncmoss-0.3.0 → syncmoss-0.4.1}/syncmoss/Library/Aegirine.mdl +0 -0
  74. {syncmoss-0.3.0 → syncmoss-0.4.1}/syncmoss/Library/Akaganeite.mdl +0 -0
  75. {syncmoss-0.3.0 → syncmoss-0.4.1}/syncmoss/Library/Almandine.mdl +0 -0
  76. {syncmoss-0.3.0 → syncmoss-0.4.1}/syncmoss/Library/Antitaenite.mdl +0 -0
  77. {syncmoss-0.3.0 → syncmoss-0.4.1}/syncmoss/Library/Arsenopyrite.mdl +0 -0
  78. {syncmoss-0.3.0 → syncmoss-0.4.1}/syncmoss/Library/Bornite.mdl +0 -0
  79. {syncmoss-0.3.0 → syncmoss-0.4.1}/syncmoss/Library/Bridgmanite (Fe,Al-bearing) @100 GPa.mdl +0 -0
  80. {syncmoss-0.3.0 → syncmoss-0.4.1}/syncmoss/Library/Bridgmanite (Fe,Al-bearing).mdl +0 -0
  81. {syncmoss-0.3.0 → syncmoss-0.4.1}/syncmoss/Library/Celadonite.mdl +0 -0
  82. {syncmoss-0.3.0 → syncmoss-0.4.1}/syncmoss/Library/Cementite.mdl +0 -0
  83. {syncmoss-0.3.0 → syncmoss-0.4.1}/syncmoss/Library/Chalcopyrite.mdl +0 -0
  84. {syncmoss-0.3.0 → syncmoss-0.4.1}/syncmoss/Library/Clinopyroxene (augite).mdl +0 -0
  85. {syncmoss-0.3.0 → syncmoss-0.4.1}/syncmoss/Library/Copiapite.mdl +0 -0
  86. {syncmoss-0.3.0 → syncmoss-0.4.1}/syncmoss/Library/Cubanite.mdl +0 -0
  87. {syncmoss-0.3.0 → syncmoss-0.4.1}/syncmoss/Library/Epidote.mdl +0 -0
  88. {syncmoss-0.3.0 → syncmoss-0.4.1}/syncmoss/Library/Fayalite (Fe-olivine).mdl +0 -0
  89. {syncmoss-0.3.0 → syncmoss-0.4.1}/syncmoss/Library/Ferrihydrite (2-line).mdl +0 -0
  90. {syncmoss-0.3.0 → syncmoss-0.4.1}/syncmoss/Library/Ferropericlase.mdl +0 -0
  91. {syncmoss-0.3.0 → syncmoss-0.4.1}/syncmoss/Library/Goethite.mdl +0 -0
  92. {syncmoss-0.3.0 → syncmoss-0.4.1}/syncmoss/Library/Green rust (fougerite, carbonate).mdl +0 -0
  93. {syncmoss-0.3.0 → syncmoss-0.4.1}/syncmoss/Library/Haegg carbide.mdl +0 -0
  94. {syncmoss-0.3.0 → syncmoss-0.4.1}/syncmoss/Library/Hedenbergite.mdl +0 -0
  95. {syncmoss-0.3.0 → syncmoss-0.4.1}/syncmoss/Library/Hematite.mdl +0 -0
  96. {syncmoss-0.3.0 → syncmoss-0.4.1}/syncmoss/Library/Heterosite (FePO4).mdl +0 -0
  97. {syncmoss-0.3.0 → syncmoss-0.4.1}/syncmoss/Library/Ilmenite.mdl +0 -0
  98. {syncmoss-0.3.0 → syncmoss-0.4.1}/syncmoss/Library/Iron phosphide.mdl +0 -0
  99. {syncmoss-0.3.0 → syncmoss-0.4.1}/syncmoss/Library/Isocubanite.mdl +0 -0
  100. {syncmoss-0.3.0 → syncmoss-0.4.1}/syncmoss/Library/Jarosite.mdl +0 -0
  101. {syncmoss-0.3.0 → syncmoss-0.4.1}/syncmoss/Library/Kamacite.mdl +0 -0
  102. {syncmoss-0.3.0 → syncmoss-0.4.1}/syncmoss/Library/Lepidocrocite.mdl +0 -0
  103. {syncmoss-0.3.0 → syncmoss-0.4.1}/syncmoss/Library/Maghemite.mdl +0 -0
  104. {syncmoss-0.3.0 → syncmoss-0.4.1}/syncmoss/Library/Magnetite.mdl +0 -0
  105. {syncmoss-0.3.0 → syncmoss-0.4.1}/syncmoss/Library/Marcasite.mdl +0 -0
  106. {syncmoss-0.3.0 → syncmoss-0.4.1}/syncmoss/Library/Melanterite.mdl +0 -0
  107. {syncmoss-0.3.0 → syncmoss-0.4.1}/syncmoss/Library/Orthopyroxene (enstatite-ferrosilite).mdl +0 -0
  108. {syncmoss-0.3.0 → syncmoss-0.4.1}/syncmoss/Library/Prussian blue (ferric ferrocyanide).mdl +0 -0
  109. {syncmoss-0.3.0 → syncmoss-0.4.1}/syncmoss/Library/Pyrite.mdl +0 -0
  110. {syncmoss-0.3.0 → syncmoss-0.4.1}/syncmoss/Library/Pyrrhotite.mdl +0 -0
  111. {syncmoss-0.3.0 → syncmoss-0.4.1}/syncmoss/Library/Rhomboclase.mdl +0 -0
  112. {syncmoss-0.3.0 → syncmoss-0.4.1}/syncmoss/Library/Roemerite.mdl +0 -0
  113. {syncmoss-0.3.0 → syncmoss-0.4.1}/syncmoss/Library/Siderite.mdl +0 -0
  114. {syncmoss-0.3.0 → syncmoss-0.4.1}/syncmoss/Library/Szomolnokite.mdl +0 -0
  115. {syncmoss-0.3.0 → syncmoss-0.4.1}/syncmoss/Library/Taenite.mdl +0 -0
  116. {syncmoss-0.3.0 → syncmoss-0.4.1}/syncmoss/Library/Talnakhite.mdl +0 -0
  117. {syncmoss-0.3.0 → syncmoss-0.4.1}/syncmoss/Library/Tetrataenite.mdl +0 -0
  118. {syncmoss-0.3.0 → syncmoss-0.4.1}/syncmoss/Library/Triphylite (LiFePO4).mdl +0 -0
  119. {syncmoss-0.3.0 → syncmoss-0.4.1}/syncmoss/Library/Troilite.mdl +0 -0
  120. {syncmoss-0.3.0 → syncmoss-0.4.1}/syncmoss/Library/Vivianite.mdl +0 -0
  121. {syncmoss-0.3.0 → syncmoss-0.4.1}/syncmoss/Library/Voltaite.mdl +0 -0
  122. {syncmoss-0.3.0 → syncmoss-0.4.1}/syncmoss/Library/alpha-Fe (metallic iron, bcc).mdl +0 -0
  123. {syncmoss-0.3.0 → syncmoss-0.4.1}/syncmoss/Library_io.py +0 -0
  124. {syncmoss-0.3.0 → syncmoss-0.4.1}/syncmoss/icons/CheckBox.png +0 -0
  125. {syncmoss-0.3.0 → syncmoss-0.4.1}/syncmoss/icons/CheckBox_.png +0 -0
  126. {syncmoss-0.3.0 → syncmoss-0.4.1}/syncmoss/icons/CheckBox_L.png +0 -0
  127. {syncmoss-0.3.0 → syncmoss-0.4.1}/syncmoss/icons/CheckBox_L2.png +0 -0
  128. {syncmoss-0.3.0 → syncmoss-0.4.1}/syncmoss/icons/DU.png +0 -0
  129. {syncmoss-0.3.0 → syncmoss-0.4.1}/syncmoss/icons/Switch.png +0 -0
  130. {syncmoss-0.3.0 → syncmoss-0.4.1}/syncmoss/icons/UD.png +0 -0
  131. {syncmoss-0.3.0 → syncmoss-0.4.1}/syncmoss/icons/icon_r.ico +0 -0
  132. {syncmoss-0.3.0 → syncmoss-0.4.1}/syncmoss/legacy.py +0 -0
  133. {syncmoss-0.3.0 → syncmoss-0.4.1}/syncmoss/minimi_lib.py +0 -0
  134. {syncmoss-0.3.0 → syncmoss-0.4.1}/syncmoss/models_description_window.py +0 -0
  135. {syncmoss-0.3.0 → syncmoss-0.4.1}/syncmoss/parameters/ABSorber3.txt +0 -0
  136. {syncmoss-0.3.0 → syncmoss-0.4.1}/syncmoss/parameters/Be.txt +0 -0
  137. {syncmoss-0.3.0 → syncmoss-0.4.1}/syncmoss/parameters/GCMS.txt +0 -0
  138. {syncmoss-0.3.0 → syncmoss-0.4.1}/syncmoss/parameters/INS_APS.txt +0 -0
  139. {syncmoss-0.3.0 → syncmoss-0.4.1}/syncmoss/parameters/KB.txt +0 -0
  140. {syncmoss-0.3.0 → syncmoss-0.4.1}/syncmoss/parameters/NFS.txt +0 -0
  141. {syncmoss-0.3.0 → syncmoss-0.4.1}/syncmoss/parameters/models_description.md +0 -0
  142. {syncmoss-0.3.0 → syncmoss-0.4.1}/syncmoss/parameters_table.py +0 -0
  143. {syncmoss-0.3.0 → syncmoss-0.4.1}/syncmoss/results_table.py +0 -0
  144. {syncmoss-0.3.0 → syncmoss-0.4.1}/syncmoss/support_math.py +0 -0
  145. {syncmoss-0.3.0 → syncmoss-0.4.1}/syncmoss/test_graf.txt +0 -0
  146. {syncmoss-0.3.0 → syncmoss-0.4.1}/syncmoss/test_param.txt +0 -0
  147. {syncmoss-0.3.0 → syncmoss-0.4.1}/syncmoss/theme_dark.json +0 -0
  148. {syncmoss-0.3.0 → syncmoss-0.4.1}/syncmoss/theme_light.json +0 -0
  149. {syncmoss-0.3.0 → syncmoss-0.4.1}/syncmoss.egg-info/dependency_links.txt +0 -0
  150. {syncmoss-0.3.0 → syncmoss-0.4.1}/syncmoss.egg-info/entry_points.txt +0 -0
  151. {syncmoss-0.3.0 → syncmoss-0.4.1}/syncmoss.egg-info/requires.txt +0 -0
  152. {syncmoss-0.3.0 → syncmoss-0.4.1}/syncmoss.egg-info/top_level.txt +0 -0
  153. {syncmoss-0.3.0 → syncmoss-0.4.1}/tests/data/Distr_corr.mdl +0 -0
  154. {syncmoss-0.3.0 → syncmoss-0.4.1}/tests/data/Distr_distr.mdl +0 -0
  155. {syncmoss-0.3.0 → syncmoss-0.4.1}/tests/data/alpha_fe_sms_000.mca +0 -0
  156. {syncmoss-0.3.0 → syncmoss-0.4.1}/tests/data/calibration_000_golden.json +0 -0
  157. {syncmoss-0.3.0 → syncmoss-0.4.1}/tests/data/generate_cal_fixtures.py +0 -0
  158. {syncmoss-0.3.0 → syncmoss-0.4.1}/tests/data/synthetic_alpha_fe_cms_linear.mca +0 -0
  159. {syncmoss-0.3.0 → syncmoss-0.4.1}/tests/golden_models.json +0 -0
  160. {syncmoss-0.3.0 → syncmoss-0.4.1}/tests/test_calibration.py +0 -0
  161. {syncmoss-0.3.0 → syncmoss-0.4.1}/tests/test_calibration_helpers.py +0 -0
  162. {syncmoss-0.3.0 → syncmoss-0.4.1}/tests/test_distr_composition.py +0 -0
  163. {syncmoss-0.3.0 → syncmoss-0.4.1}/tests/test_distr_expansion.py +0 -0
  164. {syncmoss-0.3.0 → syncmoss-0.4.1}/tests/test_distr_mdl.py +0 -0
  165. {syncmoss-0.3.0 → syncmoss-0.4.1}/tests/test_distr_targets.py +0 -0
  166. {syncmoss-0.3.0 → syncmoss-0.4.1}/tests/test_expression_validation.py +0 -0
  167. {syncmoss-0.3.0 → syncmoss-0.4.1}/tests/test_golden_models.py +0 -0
  168. {syncmoss-0.3.0 → syncmoss-0.4.1}/tests/test_gui_smoke.py +0 -0
  169. {syncmoss-0.3.0 → syncmoss-0.4.1}/tests/test_hamiltonian_texture.py +0 -0
  170. {syncmoss-0.3.0 → syncmoss-0.4.1}/tests/test_legacy.py +0 -0
  171. {syncmoss-0.3.0 → syncmoss-0.4.1}/tests/test_library_io.py +0 -0
  172. {syncmoss-0.3.0 → syncmoss-0.4.1}/tests/test_model_append.py +0 -0
  173. {syncmoss-0.3.0 → syncmoss-0.4.1}/tests/test_model_io.py +0 -0
  174. {syncmoss-0.3.0 → syncmoss-0.4.1}/tests/test_model_only_mode.py +0 -0
  175. {syncmoss-0.3.0 → syncmoss-0.4.1}/tests/test_nbaseline_results.py +0 -0
  176. {syncmoss-0.3.0 → syncmoss-0.4.1}/tests/test_parameter_links.py +0 -0
  177. {syncmoss-0.3.0 → syncmoss-0.4.1}/tests/test_polarization.py +0 -0
  178. {syncmoss-0.3.0 → syncmoss-0.4.1}/tests/test_recon_composition.py +0 -0
  179. {syncmoss-0.3.0 → syncmoss-0.4.1}/tests/test_recon_mdl.py +0 -0
  180. {syncmoss-0.3.0 → syncmoss-0.4.1}/tests/test_recon_results.py +0 -0
  181. {syncmoss-0.3.0 → syncmoss-0.4.1}/tests/test_reference_shift.py +0 -0
  182. {syncmoss-0.3.0 → syncmoss-0.4.1}/tests/test_sdw_grid.py +0 -0
  183. {syncmoss-0.3.0 → syncmoss-0.4.1}/tests/test_support_math.py +0 -0
  184. {syncmoss-0.3.0 → syncmoss-0.4.1}/tests/test_texture_s1.py +0 -0
  185. {syncmoss-0.3.0 → syncmoss-0.4.1}/tests/test_thick_gui.py +0 -0
@@ -0,0 +1,451 @@
1
+ name: _macOS (PyInstaller universal2 app)
2
+
3
+ # Modelled on crispy's release workflow (github.com/mretegan/crispy):
4
+ # 1. build the app natively on an Apple Silicon AND an Intel runner,
5
+ # 2. fuse the two bundles into one universal2 SYNCmoss.app,
6
+ # 3. sign it (Developer ID + hardened runtime), pack the DMG, notarize, staple.
7
+ #
8
+ # Signing and notarization run only when the Apple secrets are set (repository
9
+ # Settings -> Secrets and variables -> Actions; release.yml forwards them).
10
+ # Without them the job still ships a universal2 DMG, ad-hoc signed and not
11
+ # notarized, named SYNCmoss-macOS-universal2.dmg. A notarized one is named
12
+ # SYNCmoss-macOS-universal2-signed.dmg; _TagRelease.yml / _UploadRelease.yml
13
+ # tell the two apart by that name.
14
+ #
15
+ # APPLE_DEVELOPER_ID_APPLICATION_CERTIFICATE_BASE64
16
+ # the "Developer ID Application" certificate WITH its private key,
17
+ # exported from Keychain Access as .p12, then: base64 -i cert.p12 | pbcopy
18
+ # APPLE_DEVELOPER_ID_APPLICATION_CERTIFICATE_PASSWORD password of that .p12
19
+ # APPLE_ID Apple Account e-mail of the developer
20
+ # APPLE_TEAM_ID 10-character Team ID (developer.apple.com -> Membership)
21
+ # APPLE_APPLICATION_SPECIFIC_PASSWORD app-specific password (account.apple.com -> Sign-In and Security)
22
+ # APPLE_KEYCHAIN_PASSWORD optional: password of the throwaway CI keychain
23
+ # (a random one is used when unset)
24
+
25
+ on:
26
+ workflow_call:
27
+ secrets:
28
+ APPLE_ID:
29
+ required: false
30
+ APPLE_TEAM_ID:
31
+ required: false
32
+ APPLE_DEVELOPER_ID_APPLICATION_CERTIFICATE_PASSWORD:
33
+ required: false
34
+ APPLE_DEVELOPER_ID_APPLICATION_CERTIFICATE_BASE64:
35
+ required: false
36
+ APPLE_KEYCHAIN_PASSWORD:
37
+ required: false
38
+ APPLE_APPLICATION_SPECIFIC_PASSWORD:
39
+ required: false
40
+
41
+ jobs:
42
+ build:
43
+ name: Build macOS app (${{ matrix.arch }})
44
+ runs-on: ${{ matrix.runner }}
45
+ timeout-minutes: 60
46
+ strategy:
47
+ fail-fast: false
48
+ matrix:
49
+ include:
50
+ - runner: macos-15
51
+ arch: arm64
52
+ - runner: macos-15-intel
53
+ arch: x86_64
54
+ env:
55
+ MACOSX_DEPLOYMENT_TARGET: "11"
56
+ CFLAGS: "-mmacosx-version-min=11"
57
+ LDFLAGS: "-mmacosx-version-min=11"
58
+ steps:
59
+ - uses: actions/checkout@v6
60
+
61
+ - uses: actions/setup-python@v6
62
+ with:
63
+ python-version: "3.13"
64
+ cache: "pip"
65
+
66
+ - name: Check the runner architecture
67
+ run: |
68
+ MACHINE="$(python -c 'import platform; print(platform.machine())')"
69
+ echo "Python runs as ${MACHINE}"
70
+ if [ "${MACHINE}" != "${{ matrix.arch }}" ]; then
71
+ echo "::error::expected a ${{ matrix.arch }} runner, got ${MACHINE}"
72
+ exit 1
73
+ fi
74
+
75
+ - name: Download wheel artifact
76
+ uses: actions/download-artifact@v6
77
+ with:
78
+ name: wheels
79
+ path: wheels
80
+
81
+ # numba 0.62.x (llvmlite 0.45.x) is the last release with macOS x86_64
82
+ # wheels. The pin applies to BOTH architectures: the universal2 app holds
83
+ # one copy of the pure-Python code, so the two builds must resolve the
84
+ # same versions (checked again before the merge). --only-binary stops pip
85
+ # from quietly building an sdist on one architecture only.
86
+ - name: Install dependencies
87
+ run: |
88
+ python -m pip install --upgrade pip setuptools wheel
89
+ python -m pip install --only-binary=:all: "numba==0.62.1" -r requirements.txt pyinstaller wheels/*.whl
90
+ python -m pip list --format=freeze
91
+
92
+ - name: Build the app
93
+ run: python bundle/macos_bundle.py --dist-dir bundle/MacOS/${{ matrix.arch }} --work-dir bundle/build/MacOS/${{ matrix.arch }}
94
+
95
+ # tar keeps the symlinks (Qt frameworks) and executable bits that
96
+ # upload-artifact would drop.
97
+ - name: Archive the app bundle
98
+ run: |
99
+ set -euo pipefail
100
+ DIST="bundle/MacOS/${{ matrix.arch }}"
101
+ APP_PATH=""
102
+ for candidate in "${DIST}/SYNCmoss.app" "${DIST}/SYNCmoss/SYNCmoss.app"; do
103
+ if [ -d "${candidate}" ]; then
104
+ APP_PATH="${candidate}"
105
+ break
106
+ fi
107
+ done
108
+ if [ -z "${APP_PATH}" ]; then
109
+ echo "::error::SYNCmoss.app not found in the ${{ matrix.arch }} build output"
110
+ find "${DIST}" -maxdepth 3 -print
111
+ exit 1
112
+ fi
113
+ echo "Executable architectures: $(lipo -archs "${APP_PATH}/Contents/MacOS/SYNCmoss")"
114
+ mkdir -p macos-artifact
115
+ tar -cpzf macos-artifact/SYNCmoss.app.tar.gz -C "$(dirname "${APP_PATH}")" SYNCmoss.app
116
+ python -m pip list --format=freeze > macos-artifact/pip-freeze.txt
117
+ echo "APP_PATH=${APP_PATH}" >> "${GITHUB_ENV}"
118
+
119
+ - name: Upload the app bundle
120
+ uses: actions/upload-artifact@v5
121
+ with:
122
+ name: macos-app-${{ matrix.arch }}
123
+ path: macos-artifact/
124
+ if-no-files-found: error
125
+ retention-days: 1
126
+
127
+ # Runs each architecture natively (the x86_64 slice is never exercised
128
+ # later). After archiving, so nothing the test writes reaches the artifact.
129
+ - name: Smoke-test the frozen app
130
+ run: |
131
+ "${APP_PATH}/Contents/MacOS/SYNCmoss" --test
132
+
133
+ universal2:
134
+ name: Merge, sign and notarize the universal2 app
135
+ needs: build
136
+ runs-on: macos-15
137
+ timeout-minutes: 90
138
+ steps:
139
+ - uses: actions/setup-python@v6
140
+ with:
141
+ python-version: "3.13"
142
+
143
+ - name: Download the arm64 app
144
+ uses: actions/download-artifact@v6
145
+ with:
146
+ name: macos-app-arm64
147
+ path: bundles/arm64
148
+
149
+ - name: Download the x86_64 app
150
+ uses: actions/download-artifact@v6
151
+ with:
152
+ name: macos-app-x86_64
153
+ path: bundles/x86_64
154
+
155
+ - name: Check both builds used the same packages
156
+ run: |
157
+ if ! diff -u bundles/arm64/pip-freeze.txt bundles/x86_64/pip-freeze.txt; then
158
+ echo "::error::arm64 and x86_64 resolved different package versions, so the universal2 app would mix them; pin the differing packages in the build job's install step"
159
+ exit 1
160
+ fi
161
+
162
+ - name: Extract the app bundles
163
+ run: |
164
+ set -euo pipefail
165
+ mkdir -p arm64 x86_64
166
+ tar -xpf bundles/arm64/SYNCmoss.app.tar.gz -C arm64
167
+ tar -xpf bundles/x86_64/SYNCmoss.app.tar.gz -C x86_64
168
+
169
+ # Adapted from crispy's package/merge-universal.py: walk both bundles,
170
+ # fuse every pair of thin Mach-O files with lipo, copy everything else
171
+ # (identical files, already-universal binaries, data files, symlinks),
172
+ # then require every Mach-O file of the result to carry both slices.
173
+ - name: Merge into a universal2 app
174
+ shell: python
175
+ run: |
176
+ import filecmp
177
+ import os
178
+ import shutil
179
+ import subprocess
180
+ import sys
181
+
182
+ ARM_APP = "arm64/SYNCmoss.app"
183
+ X86_APP = "x86_64/SYNCmoss.app"
184
+ OUT_APP = "dist/universal2/SYNCmoss.app"
185
+
186
+ THIN_MAGICS = {b"\xfe\xed\xfa\xce", b"\xfe\xed\xfa\xcf", b"\xce\xfa\xed\xfe", b"\xcf\xfa\xed\xfe"}
187
+ FAT_MAGICS = {b"\xca\xfe\xba\xbe", b"\xbe\xba\xfe\xca"}
188
+
189
+
190
+ def macho_kind(path):
191
+ """'thin', 'fat' or None, from the magic bytes: PyInstaller ships
192
+ extension-less Mach-O files. Java .class files share the fat magic,
193
+ but a real universal binary has only a few architectures."""
194
+ with open(path, "rb") as f:
195
+ header = f.read(8)
196
+ if len(header) < 8:
197
+ return None
198
+ if header[:4] in THIN_MAGICS:
199
+ return "thin"
200
+ if header[:4] in FAT_MAGICS:
201
+ byteorder = "big" if header[:4] == b"\xca\xfe\xba\xbe" else "little"
202
+ return "fat" if int.from_bytes(header[4:8], byteorder) < 30 else None
203
+ return None
204
+
205
+
206
+ def collect(root):
207
+ """Relative path -> 'dir', 'symlink' or 'file' (symlinked dirs are not descended)."""
208
+ entries = {}
209
+ for current, dirnames, filenames in os.walk(root):
210
+ for name in dirnames + filenames:
211
+ path = os.path.join(current, name)
212
+ if os.path.islink(path):
213
+ kind = "symlink"
214
+ elif os.path.isdir(path):
215
+ kind = "dir"
216
+ else:
217
+ kind = "file"
218
+ entries[os.path.relpath(path, root)] = kind
219
+ return entries
220
+
221
+
222
+ arm = collect(ARM_APP)
223
+ x86 = collect(X86_APP)
224
+ os.makedirs(OUT_APP)
225
+ single_arch = []
226
+ fused = 0
227
+
228
+ for rel in sorted(set(arm) | set(x86)):
229
+ arm_kind, x86_kind = arm.get(rel), x86.get(rel)
230
+ arm_path = os.path.join(ARM_APP, rel)
231
+ x86_path = os.path.join(X86_APP, rel)
232
+ out_path = os.path.join(OUT_APP, rel)
233
+
234
+ if "dir" in (arm_kind, x86_kind):
235
+ os.makedirs(out_path, exist_ok=True)
236
+ continue
237
+ os.makedirs(os.path.dirname(out_path), exist_ok=True)
238
+
239
+ if "symlink" in (arm_kind, x86_kind):
240
+ if arm_kind == x86_kind and os.readlink(arm_path) != os.readlink(x86_path):
241
+ print(f"WARNING: symlink target differs, using arm64: {rel}")
242
+ os.symlink(os.readlink(arm_path if arm_kind == "symlink" else x86_path), out_path)
243
+ continue
244
+
245
+ if arm_kind is None or x86_kind is None:
246
+ present = arm_path if arm_kind else x86_path
247
+ if macho_kind(present):
248
+ single_arch.append(rel)
249
+ else:
250
+ print(f"WARNING: data file present in one build only: {rel}")
251
+ shutil.copy2(present, out_path)
252
+ continue
253
+
254
+ if filecmp.cmp(arm_path, x86_path, shallow=False):
255
+ shutil.copy2(arm_path, out_path)
256
+ continue
257
+
258
+ arm_macho, x86_macho = macho_kind(arm_path), macho_kind(x86_path)
259
+ if arm_macho == x86_macho == "thin":
260
+ subprocess.run(["lipo", "-create", arm_path, x86_path, "-output", out_path], check=True)
261
+ shutil.copymode(arm_path, out_path)
262
+ fused += 1
263
+ elif "fat" in (arm_macho, x86_macho):
264
+ # Already universal; the two copies differ only in their signature.
265
+ shutil.copy2(arm_path if arm_macho == "fat" else x86_path, out_path)
266
+ elif arm_macho or x86_macho:
267
+ single_arch.append(rel)
268
+ else:
269
+ print(f"WARNING: data file differs, using arm64: {rel}")
270
+ shutil.copy2(arm_path, out_path)
271
+
272
+ if single_arch:
273
+ sys.exit(
274
+ "ERROR: these Mach-O binaries exist for one architecture only, so the "
275
+ "app would crash on the other:\n" + "\n".join(f" - {p}" for p in single_arch)
276
+ )
277
+
278
+ not_universal = []
279
+ checked = 0
280
+ for rel, kind in sorted(collect(OUT_APP).items()):
281
+ path = os.path.join(OUT_APP, rel)
282
+ if kind != "file" or not macho_kind(path):
283
+ continue
284
+ checked += 1
285
+ archs = subprocess.run(["lipo", "-archs", path], capture_output=True, text=True).stdout.split()
286
+ if not {"arm64", "x86_64"} <= set(archs):
287
+ not_universal.append(f" - {rel}: {' '.join(archs) or 'unreadable'}")
288
+
289
+ print(f"Fused {fused} binaries, checked {checked} Mach-O files")
290
+ if not_universal:
291
+ sys.exit("ERROR: these binaries are not universal2:\n" + "\n".join(not_universal))
292
+ print(f"Universal2 app written to {OUT_APP}")
293
+
294
+ # The merge leaves the bundle's seal stale, so the app is always re-signed:
295
+ # with the Developer ID when the certificate secrets are set, otherwise
296
+ # ad-hoc (so macOS reports "unverified developer", not "damaged").
297
+ - name: Sign the app
298
+ id: sign
299
+ env:
300
+ CERTIFICATE_BASE64: ${{ secrets.APPLE_DEVELOPER_ID_APPLICATION_CERTIFICATE_BASE64 }}
301
+ CERTIFICATE_PASSWORD: ${{ secrets.APPLE_DEVELOPER_ID_APPLICATION_CERTIFICATE_PASSWORD }}
302
+ KEYCHAIN_PASSWORD: ${{ secrets.APPLE_KEYCHAIN_PASSWORD }}
303
+ run: |
304
+ set -euo pipefail
305
+ APP="dist/universal2/SYNCmoss.app"
306
+
307
+ # The hardened runtime needs these for the numba/llvmlite JIT (writable
308
+ # + executable memory) and for loading the bundled Python extensions.
309
+ ENTITLEMENTS="${RUNNER_TEMP}/entitlements.plist"
310
+ cat > "${ENTITLEMENTS}" <<'EOF'
311
+ <?xml version="1.0" encoding="UTF-8"?>
312
+ <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
313
+ <plist version="1.0">
314
+ <dict>
315
+ <key>com.apple.security.cs.allow-jit</key>
316
+ <true/>
317
+ <key>com.apple.security.cs.allow-unsigned-executable-memory</key>
318
+ <true/>
319
+ <key>com.apple.security.cs.disable-library-validation</key>
320
+ <true/>
321
+ </dict>
322
+ </plist>
323
+ EOF
324
+
325
+ # Extended attributes would invalidate the signature.
326
+ xattr -cr "${APP}"
327
+
328
+ if [ -n "${CERTIFICATE_BASE64}" ] && [ -n "${CERTIFICATE_PASSWORD}" ]; then
329
+ KEYCHAIN_PATH="${RUNNER_TEMP}/signing.keychain-db"
330
+ CERTIFICATE_PATH="${RUNNER_TEMP}/certificate.p12"
331
+ if [ -z "${KEYCHAIN_PASSWORD}" ]; then
332
+ KEYCHAIN_PASSWORD="$(openssl rand -base64 24)"
333
+ echo "::add-mask::${KEYCHAIN_PASSWORD}"
334
+ fi
335
+
336
+ echo "Creating a temporary keychain and importing the certificate"
337
+ echo "${CERTIFICATE_BASE64}" | base64 --decode -o "${CERTIFICATE_PATH}"
338
+ security create-keychain -p "${KEYCHAIN_PASSWORD}" "${KEYCHAIN_PATH}"
339
+ security set-keychain-settings -lut 21600 "${KEYCHAIN_PATH}"
340
+ security unlock-keychain -p "${KEYCHAIN_PASSWORD}" "${KEYCHAIN_PATH}"
341
+ security import "${CERTIFICATE_PATH}" -P "${CERTIFICATE_PASSWORD}" -A -t cert -f pkcs12 -k "${KEYCHAIN_PATH}"
342
+ rm -f "${CERTIFICATE_PATH}"
343
+ security set-key-partition-list -S apple-tool:,apple:,codesign: -s -k "${KEYCHAIN_PASSWORD}" "${KEYCHAIN_PATH}" > /dev/null
344
+ security list-keychains -d user -s "${KEYCHAIN_PATH}"
345
+
346
+ IDENTITY="$(security find-identity -v -p codesigning "${KEYCHAIN_PATH}" | awk '/"Developer ID Application/ && !found { print $2; found = 1 }')"
347
+ if [ -z "${IDENTITY}" ]; then
348
+ echo "::error::the certificate holds no 'Developer ID Application' identity"
349
+ security find-identity -v -p codesigning "${KEYCHAIN_PATH}"
350
+ exit 1
351
+ fi
352
+
353
+ echo "Signing with the Developer ID (hardened runtime)"
354
+ codesign --force --deep --timestamp --options runtime \
355
+ --entitlements "${ENTITLEMENTS}" \
356
+ --keychain "${KEYCHAIN_PATH}" \
357
+ --sign "${IDENTITY}" \
358
+ "${APP}"
359
+ security delete-keychain "${KEYCHAIN_PATH}"
360
+ echo "developer_id=true" >> "${GITHUB_OUTPUT}"
361
+ else
362
+ echo "::warning::Apple certificate secrets are not set: the macOS DMG is ad-hoc signed and not notarized"
363
+ codesign --force --deep --sign - "${APP}"
364
+ echo "developer_id=false" >> "${GITHUB_OUTPUT}"
365
+ fi
366
+
367
+ codesign --verify --deep --strict --verbose=2 "${APP}"
368
+ echo "Universal executable architectures: $(lipo -archs "${APP}/Contents/MacOS/SYNCmoss")"
369
+
370
+ # On a copy, so files the test writes never reach the DMG. For a Developer
371
+ # ID build this runs under the hardened runtime, i.e. it also checks that
372
+ # the entitlements above are enough for numba.
373
+ - name: Smoke-test the signed universal2 app
374
+ run: |
375
+ set -euo pipefail
376
+ mkdir -p "${RUNNER_TEMP}/smoke"
377
+ ditto dist/universal2/SYNCmoss.app "${RUNNER_TEMP}/smoke/SYNCmoss.app"
378
+ "${RUNNER_TEMP}/smoke/SYNCmoss.app/Contents/MacOS/SYNCmoss" --test
379
+
380
+ - name: Create the DMG
381
+ run: |
382
+ set -euo pipefail
383
+ STAGING="${RUNNER_TEMP}/dmg"
384
+ mkdir -p "${STAGING}"
385
+ ditto dist/universal2/SYNCmoss.app "${STAGING}/SYNCmoss.app"
386
+ ln -s /Applications "${STAGING}/Applications"
387
+ # hdiutil is occasionally "Resource busy" on hosted runners: retry.
388
+ for attempt in 1 2 3; do
389
+ if hdiutil create -volname SYNCmoss -srcfolder "${STAGING}" -ov -format UDZO -imagekey zlib-level=9 dist/SYNCmoss.dmg; then
390
+ break
391
+ fi
392
+ if [ "${attempt}" -eq 3 ]; then
393
+ exit 1
394
+ fi
395
+ echo "hdiutil failed, retrying in 10 s"
396
+ sleep 10
397
+ done
398
+
399
+ - name: Notarize and staple the DMG
400
+ id: notarize
401
+ if: steps.sign.outputs.developer_id == 'true'
402
+ env:
403
+ APPLE_ID: ${{ secrets.APPLE_ID }}
404
+ APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }}
405
+ APPLICATION_SPECIFIC_PASSWORD: ${{ secrets.APPLE_APPLICATION_SPECIFIC_PASSWORD }}
406
+ run: |
407
+ set -euo pipefail
408
+ if [ -z "${APPLE_ID}" ] || [ -z "${APPLE_TEAM_ID}" ] || [ -z "${APPLICATION_SPECIFIC_PASSWORD}" ]; then
409
+ echo "::warning::APPLE_ID / APPLE_TEAM_ID / APPLE_APPLICATION_SPECIFIC_PASSWORD are not set: the DMG is signed but not notarized, so Gatekeeper will still block it"
410
+ exit 0
411
+ fi
412
+ DMG="dist/SYNCmoss.dmg"
413
+
414
+ echo "Submitting ${DMG} for notarization"
415
+ RESULT="$(xcrun notarytool submit "${DMG}" \
416
+ --apple-id "${APPLE_ID}" --team-id "${APPLE_TEAM_ID}" --password "${APPLICATION_SPECIFIC_PASSWORD}" \
417
+ --wait --timeout 1h --output-format json)" || true
418
+ echo "${RESULT}"
419
+ SUBMISSION_ID="$(python -c 'import json, sys; print(json.loads(sys.argv[1]).get("id", ""))' "${RESULT}" 2> /dev/null || true)"
420
+ STATUS="$(python -c 'import json, sys; print(json.loads(sys.argv[1]).get("status", ""))' "${RESULT}" 2> /dev/null || true)"
421
+ if [ "${STATUS}" != "Accepted" ]; then
422
+ echo "::error::notarization did not succeed (status: '${STATUS:-none}')"
423
+ if [ -n "${SUBMISSION_ID}" ]; then
424
+ xcrun notarytool log "${SUBMISSION_ID}" \
425
+ --apple-id "${APPLE_ID}" --team-id "${APPLE_TEAM_ID}" --password "${APPLICATION_SPECIFIC_PASSWORD}" || true
426
+ fi
427
+ exit 1
428
+ fi
429
+
430
+ xcrun stapler staple "${DMG}"
431
+ xcrun stapler validate "${DMG}"
432
+ echo "notarized=true" >> "${GITHUB_OUTPUT}"
433
+
434
+ - name: Name the DMG
435
+ env:
436
+ NOTARIZED: ${{ steps.notarize.outputs.notarized }}
437
+ run: |
438
+ if [ "${NOTARIZED}" = "true" ]; then
439
+ mv dist/SYNCmoss.dmg dist/SYNCmoss-macOS-universal2-signed.dmg
440
+ else
441
+ mv dist/SYNCmoss.dmg dist/SYNCmoss-macOS-universal2.dmg
442
+ fi
443
+ ls -lh dist/*.dmg
444
+
445
+ - name: Upload the universal2 DMG
446
+ uses: actions/upload-artifact@v5
447
+ with:
448
+ name: macos-universal2
449
+ path: dist/SYNCmoss-macOS-universal2*.dmg
450
+ if-no-files-found: error
451
+ retention-days: 5
@@ -74,7 +74,7 @@ jobs:
74
74
  run: |
75
75
  python -m pip install --upgrade pip
76
76
  pip install wheels/*.whl
77
- pip install pytest
77
+ pip install pytest pytest-xdist
78
78
 
79
79
  - name: Install runtime dependencies + pytest (source checkout)
80
80
  if: inputs.install == 'source'
@@ -82,7 +82,7 @@ jobs:
82
82
  run: |
83
83
  python -m pip install --upgrade pip
84
84
  pip install -r requirements.txt
85
- pip install pytest
85
+ pip install pytest pytest-xdist
86
86
 
87
87
  - name: Run the test-suite
88
88
  shell: bash
@@ -103,4 +103,7 @@ jobs:
103
103
  if [ "$INSTALL" = "wheel" ]; then cd "$(mktemp -d)"; fi
104
104
 
105
105
  python -c "import syncmoss.syncmoss_main as m; print('syncmoss from:', m.__file__)"
106
- python -m pytest "$GITHUB_WORKSPACE/tests" "${args[@]}"
106
+ # -n 0 forces a SERIAL run: pyproject sets -n auto for local use,
107
+ # which oversubscribes a small CI runner. xdist is installed above
108
+ # only so that this flag parses.
109
+ python -m pytest "$GITHUB_WORKSPACE/tests" -n 0 "${args[@]}"
@@ -162,3 +162,11 @@ minimi_optimization_notes.txt
162
162
  # suite still covers the model end-to-end via the SCDW golden case, plus its
163
163
  # param-count and locked-angle checks (test_model_io / test_thick_gui).
164
164
  tests/test_sdw_grid.py
165
+
166
+ # SMS instrumental-function study against the ESRF (temperature, angle) dataset
167
+ # on N:\SL_studies: analysis scripts, cached intermediate fits and the figures
168
+ # they produce. Local-only -- it needs a network share that is not part of the
169
+ # repo, and its outputs are large. The conclusions that matter are tracked in
170
+ # docs/sms_instrumental_function.md; the calibration it produced is in
171
+ # sms_theory.FEBO3_BS_CALIBRATION and instrumental_io.ESRF_STANDARD_LINE_WIDTH.
172
+ sms_study/
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: syncmoss
3
- Version: 0.3.0
3
+ Version: 0.4.1
4
4
  Summary: SYNCmoss — Mössbauer spectroscopy analysis application
5
5
  Author: Sergey Yaroslavtsev
6
6
  License: MIT
@@ -73,7 +73,21 @@ local_scheme = "no-local-version"
73
73
 
74
74
  [tool.pytest.ini_options]
75
75
  testpaths = ["tests"]
76
- addopts = "-q --basetemp=./.pytest_tmp"
76
+ # -n 6: almost every test here spends its time in ONE process doing physics
77
+ # (a fit, a calibration, an instrumental-function search), so a serial run
78
+ # leaves most of the machine idle -- measured 351 s serial against 116 s on 8
79
+ # workers for the calibration + instrumental set.
80
+ #
81
+ # A FIXED 6, not "auto". Each worker builds its own QApplication, PhysicsApp
82
+ # and numba-compiled model, so the suite is limited by MEMORY, not by cores:
83
+ # "auto" resolves to one worker per core (22 here) and dies during start-up
84
+ # with an execnet EOFError once the machine runs out. The same ceiling bit the
85
+ # 83-spectrum study, where 10 parallel fits OOM'd and 6 did not.
86
+ #
87
+ # CI overrides this with -n 0 (see .github/workflows/_Tests.yml): the runners
88
+ # are small, where even 6 oversubscribes. Use -n 0 locally too when debugging,
89
+ # since xdist swallows live output.
90
+ addopts = "-q --basetemp=./.pytest_tmp -n 6"
77
91
  markers = [
78
92
  "gui: builds the PySide6 GUI; runs head-less via QT_QPA_PLATFORM=offscreen (set in tests/conftest.py)",
79
93
  "slow: slow tests (numba compilation / fitting)",
@@ -102,6 +102,7 @@ import numpy as np
102
102
 
103
103
  import syncmoss.minimi_lib as mi
104
104
  import syncmoss.models as m5
105
+ import syncmoss.sms_theory as smst
105
106
  from syncmoss.constants import (number_of_baseline_parameters, ALPHA_FE_FIELD,
106
107
  TESLA_PER_MMS, LINE_RATIO_25, LINE_RATIO_34,
107
108
  NAT_WIDTH)
@@ -532,11 +533,16 @@ def Calibration(dir_path, Cal_file, pool, VVV, INS, JN, x0, MulCo, Vel_start=1,
532
533
  return m5.TI(x, p, model, JN, pool, x0, MulCo, INS, [], [], Norm=Norm)
533
534
 
534
535
  # The multi-line instrumental function displaces the apparent line
535
- # positions by the intensity-weighted sum of the squared line shifts;
536
- # the folded velocity axis is corrected by this at the very end.
536
+ # positions by its first moment (for the legacy sum of unit-area
537
+ # Gaussians: the intensity-weighted sum of the line shifts); the folded
538
+ # velocity axis is corrected by this at the very end. The theoretical
539
+ # SMS shapes compute the same moment in sms_theory.ins_centroid.
537
540
  INS_shift = 0
538
- for i in range(0, int(len(INS) / 3)):
539
- INS_shift += INS[i * 3 + 1] * INS[i * 3 + 2] ** 2
541
+ if smst.ins_kind(INS) == smst.KIND_GAUSS:
542
+ for i in range(0, int(len(INS) / 3)):
543
+ INS_shift += INS[i * 3 + 1] * INS[i * 3 + 2] ** 2
544
+ else:
545
+ INS_shift = smst.ins_centroid(INS)
540
546
  else:
541
547
  raise ValueError(f"unknown experimental method VVV={VVV!r} (expected 1 or 3)")
542
548
 
@@ -119,7 +119,7 @@ def save_to_library_via_dialog(main_window):
119
119
 
120
120
  def open_library_model_dialog(main_window, parent_widget, insert_row, model_options=None):
121
121
  """Open internal Library browser with filters and detailed preview."""
122
- library_dir = os.path.join(main_window.dir_path, 'Library')
122
+ library_dir = main_window.library_dir
123
123
  if not os.path.isdir(library_dir):
124
124
  main_window.set_status(f"Library folder not found: {library_dir}", "red")
125
125
  return
@@ -56,6 +56,26 @@ TESLA_PER_MMS = 1 / (2 * LINE_SHIFT_16) # 3.1097641 T per mm/s o
56
56
  LINE_RATIO_25 = LINE_SHIFT_25 / LINE_SHIFT_16 # 0.5789752
57
57
  LINE_RATIO_34 = LINE_SHIFT_34 / LINE_SHIFT_16 # 0.1579504
58
58
 
59
+ # --- scattering-side 57Fe quantities (sms_theory.py) -----------------------
60
+ # Everything below is DERIVED from the primitives above plus two new inputs,
61
+ # ALPHA_IC and the universal hc; no 57Fe literal is repeated.
62
+ # ALPHA_IC -- internal conversion coefficient of the 14.4 keV level.
63
+ # HC_EV_A -- h*c = 1239.8419843320026 eV nm (exact in SI since 2019).
64
+ ALPHA_IC = 8.56
65
+ HC_EV_A = 12398.419843320026 # eV * Angstrom
66
+
67
+ MMPS_TO_NEV = E0 / c * 10**9 # 48.0733 neV per (mm/s): E = E0 v/c
68
+ GAMMA0_NEV = NAT_WIDTH * MMPS_TO_NEV # 4.7112 neV, the natural width in neV
69
+ MU_N_NEV_PER_T = mun / (E0_J / E0) * 10**9 # 31.4331 neV/T
70
+ LAMBDA_A = HC_EV_A / E0 # 0.8603 A
71
+ K_VEC_A = 2 * 3.141592653589793 / LAMBDA_A # 7.3035 1/A
72
+ R_E_A = 2.8179403262 * 10**-5 # classical electron radius, A
73
+ # Maximum resonant cross-section from the level scheme:
74
+ # sigma_0 = 2 pi lambdabar^2 (2 I_e + 1)/(2 I_g + 1) / (1 + alpha_ic)
75
+ # equals `sigma` above (2.464e-22 m^2) to 0.02 %; sms_theory.selftest() checks it.
76
+ SIGMA0_A2 = (2 * 3.141592653589793 * (LAMBDA_A / (2 * 3.141592653589793))**2
77
+ * (2 * 1.5 + 1) / (2 * 0.5 + 1) / (1 + ALPHA_IC)) # 0.024644 A^2
78
+
59
79
  # Linear polarization degree of the SMS (synchrotron) beam, 0..1. Only a default:
60
80
  # it is GUI-editable via Supp -> "Set polarization" and travels to the models as
61
81
  # the `pol` / `sms_pol` argument. 0.98 is a realistic synchrotron beam.
@@ -343,7 +343,8 @@ def fit_single_spectrum(app, spectrum_file, pool, background=None, sequence_para
343
343
  # Load and concatenate all spectra
344
344
  A_list, B_list = [], []
345
345
  for i, spec_file in enumerate(spectrum_files):
346
- A_temp, B_temp = load_spectrum(app, spec_file)
346
+ A_temp, B_temp = load_spectrum(
347
+ app, spec_file, calibration_path=app.calibration_path)
347
348
  A_temp, B_temp = A_temp[0], B_temp[0]
348
349
  A_list.append(A_temp)
349
350
  B_list.append(B_temp)
@@ -357,7 +358,8 @@ def fit_single_spectrum(app, spectrum_file, pool, background=None, sequence_para
357
358
 
358
359
  else:
359
360
  # Single spectrum fitting mode
360
- A, B = load_spectrum(app, spectrum_file)
361
+ A, B = load_spectrum(app, spectrum_file,
362
+ calibration_path=app.calibration_path)
361
363
  A, B = A[0], B[0] # Unpack from list
362
364
  A_list, B_list = [A], [B]
363
365