syncmoss 0.4.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 (178) hide show
  1. syncmoss-0.4.1/.github/workflows/_MacOS.yml +451 -0
  2. {syncmoss-0.4.0 → syncmoss-0.4.1}/PKG-INFO +1 -1
  3. {syncmoss-0.4.0 → syncmoss-0.4.1}/syncmoss/Library_window.py +1 -1
  4. {syncmoss-0.4.0 → syncmoss-0.4.1}/syncmoss/instrumental_io.py +21 -22
  5. {syncmoss-0.4.0 → syncmoss-0.4.1}/syncmoss/main.py +1 -1
  6. {syncmoss-0.4.0 → syncmoss-0.4.1}/syncmoss/model_io.py +1 -1
  7. {syncmoss-0.4.0 → syncmoss-0.4.1}/syncmoss/sms_theory.py +6 -1
  8. {syncmoss-0.4.0 → syncmoss-0.4.1}/syncmoss/supp_menu.py +5 -4
  9. {syncmoss-0.4.0 → syncmoss-0.4.1}/syncmoss/syncmoss_main.py +58 -15
  10. {syncmoss-0.4.0 → syncmoss-0.4.1}/syncmoss.egg-info/PKG-INFO +1 -1
  11. {syncmoss-0.4.0 → syncmoss-0.4.1}/syncmoss.egg-info/SOURCES.txt +1 -0
  12. {syncmoss-0.4.0 → syncmoss-0.4.1}/syncmoss.egg-info/scm_file_list.json +1 -0
  13. {syncmoss-0.4.0 → syncmoss-0.4.1}/tests/syncmoss_test.py +272 -3
  14. {syncmoss-0.4.0 → syncmoss-0.4.1}/tests/test_instrumental_recentre.py +21 -5
  15. syncmoss-0.4.1/tests/test_library_dir.py +239 -0
  16. syncmoss-0.4.0/.github/workflows/_MacOS.yml +0 -329
  17. {syncmoss-0.4.0 → syncmoss-0.4.1}/.github/workflows/Tests.yml +0 -0
  18. {syncmoss-0.4.0 → syncmoss-0.4.1}/.github/workflows/_PyPI.yml +0 -0
  19. {syncmoss-0.4.0 → syncmoss-0.4.1}/.github/workflows/_TagRelease.yml +0 -0
  20. {syncmoss-0.4.0 → syncmoss-0.4.1}/.github/workflows/_Tests.yml +0 -0
  21. {syncmoss-0.4.0 → syncmoss-0.4.1}/.github/workflows/_UploadRelease.yml +0 -0
  22. {syncmoss-0.4.0 → syncmoss-0.4.1}/.github/workflows/_Version.yml +0 -0
  23. {syncmoss-0.4.0 → syncmoss-0.4.1}/.github/workflows/_Wheels.yml +0 -0
  24. {syncmoss-0.4.0 → syncmoss-0.4.1}/.github/workflows/_WindowsExe.yml +0 -0
  25. {syncmoss-0.4.0 → syncmoss-0.4.1}/.github/workflows/release.yml +0 -0
  26. {syncmoss-0.4.0 → syncmoss-0.4.1}/.gitignore +0 -0
  27. {syncmoss-0.4.0 → syncmoss-0.4.1}/.vscode/settings.json +0 -0
  28. {syncmoss-0.4.0 → syncmoss-0.4.1}/COPYING.txt +0 -0
  29. {syncmoss-0.4.0 → syncmoss-0.4.1}/LICENSE +0 -0
  30. {syncmoss-0.4.0 → syncmoss-0.4.1}/Lib_test/test.mdl +0 -0
  31. {syncmoss-0.4.0 → syncmoss-0.4.1}/NOTICE.txt +0 -0
  32. {syncmoss-0.4.0 → syncmoss-0.4.1}/README.md +0 -0
  33. {syncmoss-0.4.0 → syncmoss-0.4.1}/bundle/MacOS.spec +0 -0
  34. {syncmoss-0.4.0 → syncmoss-0.4.1}/bundle/Windows.spec +0 -0
  35. {syncmoss-0.4.0 → syncmoss-0.4.1}/bundle/bundle.py +0 -0
  36. {syncmoss-0.4.0 → syncmoss-0.4.1}/bundle/macos_bundle.py +0 -0
  37. {syncmoss-0.4.0 → syncmoss-0.4.1}/docs/thick_models.md +0 -0
  38. {syncmoss-0.4.0 → syncmoss-0.4.1}/docs/thick_vs_thin.md +0 -0
  39. {syncmoss-0.4.0 → syncmoss-0.4.1}/pyproject.toml +0 -0
  40. {syncmoss-0.4.0 → syncmoss-0.4.1}/requirements.txt +0 -0
  41. {syncmoss-0.4.0 → syncmoss-0.4.1}/setup.cfg +0 -0
  42. {syncmoss-0.4.0 → syncmoss-0.4.1}/syncmoss/Calibration.py +0 -0
  43. {syncmoss-0.4.0 → syncmoss-0.4.1}/syncmoss/Hamiltonian_helper.py +0 -0
  44. {syncmoss-0.4.0 → syncmoss-0.4.1}/syncmoss/Library/Aegirine.mdl +0 -0
  45. {syncmoss-0.4.0 → syncmoss-0.4.1}/syncmoss/Library/Akaganeite.mdl +0 -0
  46. {syncmoss-0.4.0 → syncmoss-0.4.1}/syncmoss/Library/Almandine.mdl +0 -0
  47. {syncmoss-0.4.0 → syncmoss-0.4.1}/syncmoss/Library/Antitaenite.mdl +0 -0
  48. {syncmoss-0.4.0 → syncmoss-0.4.1}/syncmoss/Library/Arsenopyrite.mdl +0 -0
  49. {syncmoss-0.4.0 → syncmoss-0.4.1}/syncmoss/Library/Bornite.mdl +0 -0
  50. {syncmoss-0.4.0 → syncmoss-0.4.1}/syncmoss/Library/Bridgmanite (Fe,Al-bearing) @100 GPa.mdl +0 -0
  51. {syncmoss-0.4.0 → syncmoss-0.4.1}/syncmoss/Library/Bridgmanite (Fe,Al-bearing).mdl +0 -0
  52. {syncmoss-0.4.0 → syncmoss-0.4.1}/syncmoss/Library/Celadonite.mdl +0 -0
  53. {syncmoss-0.4.0 → syncmoss-0.4.1}/syncmoss/Library/Cementite.mdl +0 -0
  54. {syncmoss-0.4.0 → syncmoss-0.4.1}/syncmoss/Library/Chalcopyrite.mdl +0 -0
  55. {syncmoss-0.4.0 → syncmoss-0.4.1}/syncmoss/Library/Clinopyroxene (augite).mdl +0 -0
  56. {syncmoss-0.4.0 → syncmoss-0.4.1}/syncmoss/Library/Copiapite.mdl +0 -0
  57. {syncmoss-0.4.0 → syncmoss-0.4.1}/syncmoss/Library/Cubanite.mdl +0 -0
  58. {syncmoss-0.4.0 → syncmoss-0.4.1}/syncmoss/Library/Epidote.mdl +0 -0
  59. {syncmoss-0.4.0 → syncmoss-0.4.1}/syncmoss/Library/Fayalite (Fe-olivine).mdl +0 -0
  60. {syncmoss-0.4.0 → syncmoss-0.4.1}/syncmoss/Library/Ferrihydrite (2-line).mdl +0 -0
  61. {syncmoss-0.4.0 → syncmoss-0.4.1}/syncmoss/Library/Ferropericlase.mdl +0 -0
  62. {syncmoss-0.4.0 → syncmoss-0.4.1}/syncmoss/Library/Goethite.mdl +0 -0
  63. {syncmoss-0.4.0 → syncmoss-0.4.1}/syncmoss/Library/Green rust (fougerite, carbonate).mdl +0 -0
  64. {syncmoss-0.4.0 → syncmoss-0.4.1}/syncmoss/Library/Haegg carbide.mdl +0 -0
  65. {syncmoss-0.4.0 → syncmoss-0.4.1}/syncmoss/Library/Hedenbergite.mdl +0 -0
  66. {syncmoss-0.4.0 → syncmoss-0.4.1}/syncmoss/Library/Hematite.mdl +0 -0
  67. {syncmoss-0.4.0 → syncmoss-0.4.1}/syncmoss/Library/Heterosite (FePO4).mdl +0 -0
  68. {syncmoss-0.4.0 → syncmoss-0.4.1}/syncmoss/Library/Ilmenite.mdl +0 -0
  69. {syncmoss-0.4.0 → syncmoss-0.4.1}/syncmoss/Library/Iron phosphide.mdl +0 -0
  70. {syncmoss-0.4.0 → syncmoss-0.4.1}/syncmoss/Library/Isocubanite.mdl +0 -0
  71. {syncmoss-0.4.0 → syncmoss-0.4.1}/syncmoss/Library/Jarosite.mdl +0 -0
  72. {syncmoss-0.4.0 → syncmoss-0.4.1}/syncmoss/Library/Kamacite.mdl +0 -0
  73. {syncmoss-0.4.0 → syncmoss-0.4.1}/syncmoss/Library/Lepidocrocite.mdl +0 -0
  74. {syncmoss-0.4.0 → syncmoss-0.4.1}/syncmoss/Library/Maghemite.mdl +0 -0
  75. {syncmoss-0.4.0 → syncmoss-0.4.1}/syncmoss/Library/Magnetite.mdl +0 -0
  76. {syncmoss-0.4.0 → syncmoss-0.4.1}/syncmoss/Library/Marcasite.mdl +0 -0
  77. {syncmoss-0.4.0 → syncmoss-0.4.1}/syncmoss/Library/Melanterite.mdl +0 -0
  78. {syncmoss-0.4.0 → syncmoss-0.4.1}/syncmoss/Library/Orthopyroxene (enstatite-ferrosilite).mdl +0 -0
  79. {syncmoss-0.4.0 → syncmoss-0.4.1}/syncmoss/Library/Prussian blue (ferric ferrocyanide).mdl +0 -0
  80. {syncmoss-0.4.0 → syncmoss-0.4.1}/syncmoss/Library/Pyrite.mdl +0 -0
  81. {syncmoss-0.4.0 → syncmoss-0.4.1}/syncmoss/Library/Pyrrhotite.mdl +0 -0
  82. {syncmoss-0.4.0 → syncmoss-0.4.1}/syncmoss/Library/Rhomboclase.mdl +0 -0
  83. {syncmoss-0.4.0 → syncmoss-0.4.1}/syncmoss/Library/Roemerite.mdl +0 -0
  84. {syncmoss-0.4.0 → syncmoss-0.4.1}/syncmoss/Library/Siderite.mdl +0 -0
  85. {syncmoss-0.4.0 → syncmoss-0.4.1}/syncmoss/Library/Szomolnokite.mdl +0 -0
  86. {syncmoss-0.4.0 → syncmoss-0.4.1}/syncmoss/Library/Taenite.mdl +0 -0
  87. {syncmoss-0.4.0 → syncmoss-0.4.1}/syncmoss/Library/Talnakhite.mdl +0 -0
  88. {syncmoss-0.4.0 → syncmoss-0.4.1}/syncmoss/Library/Tetrataenite.mdl +0 -0
  89. {syncmoss-0.4.0 → syncmoss-0.4.1}/syncmoss/Library/Triphylite (LiFePO4).mdl +0 -0
  90. {syncmoss-0.4.0 → syncmoss-0.4.1}/syncmoss/Library/Troilite.mdl +0 -0
  91. {syncmoss-0.4.0 → syncmoss-0.4.1}/syncmoss/Library/Vivianite.mdl +0 -0
  92. {syncmoss-0.4.0 → syncmoss-0.4.1}/syncmoss/Library/Voltaite.mdl +0 -0
  93. {syncmoss-0.4.0 → syncmoss-0.4.1}/syncmoss/Library/alpha-Fe (metallic iron, bcc).mdl +0 -0
  94. {syncmoss-0.4.0 → syncmoss-0.4.1}/syncmoss/Library_io.py +0 -0
  95. {syncmoss-0.4.0 → syncmoss-0.4.1}/syncmoss/constants.py +0 -0
  96. {syncmoss-0.4.0 → syncmoss-0.4.1}/syncmoss/fitting_io.py +0 -0
  97. {syncmoss-0.4.0 → syncmoss-0.4.1}/syncmoss/icons/CheckBox.png +0 -0
  98. {syncmoss-0.4.0 → syncmoss-0.4.1}/syncmoss/icons/CheckBox_.png +0 -0
  99. {syncmoss-0.4.0 → syncmoss-0.4.1}/syncmoss/icons/CheckBox_L.png +0 -0
  100. {syncmoss-0.4.0 → syncmoss-0.4.1}/syncmoss/icons/CheckBox_L2.png +0 -0
  101. {syncmoss-0.4.0 → syncmoss-0.4.1}/syncmoss/icons/DU.png +0 -0
  102. {syncmoss-0.4.0 → syncmoss-0.4.1}/syncmoss/icons/Switch.png +0 -0
  103. {syncmoss-0.4.0 → syncmoss-0.4.1}/syncmoss/icons/UD.png +0 -0
  104. {syncmoss-0.4.0 → syncmoss-0.4.1}/syncmoss/icons/icon_r.ico +0 -0
  105. {syncmoss-0.4.0 → syncmoss-0.4.1}/syncmoss/instrumental_window.py +0 -0
  106. {syncmoss-0.4.0 → syncmoss-0.4.1}/syncmoss/legacy.py +0 -0
  107. {syncmoss-0.4.0 → syncmoss-0.4.1}/syncmoss/minimi_lib.py +0 -0
  108. {syncmoss-0.4.0 → syncmoss-0.4.1}/syncmoss/models.py +0 -0
  109. {syncmoss-0.4.0 → syncmoss-0.4.1}/syncmoss/models_description_window.py +0 -0
  110. {syncmoss-0.4.0 → syncmoss-0.4.1}/syncmoss/models_positions.py +0 -0
  111. {syncmoss-0.4.0 → syncmoss-0.4.1}/syncmoss/parameters/ABSorber3.txt +0 -0
  112. {syncmoss-0.4.0 → syncmoss-0.4.1}/syncmoss/parameters/Be.txt +0 -0
  113. {syncmoss-0.4.0 → syncmoss-0.4.1}/syncmoss/parameters/Calibration.dat +0 -0
  114. {syncmoss-0.4.0 → syncmoss-0.4.1}/syncmoss/parameters/GCMS.txt +0 -0
  115. {syncmoss-0.4.0 → syncmoss-0.4.1}/syncmoss/parameters/INS_APS.txt +0 -0
  116. {syncmoss-0.4.0 → syncmoss-0.4.1}/syncmoss/parameters/INSexp.txt +0 -0
  117. {syncmoss-0.4.0 → syncmoss-0.4.1}/syncmoss/parameters/INSint.txt +0 -0
  118. {syncmoss-0.4.0 → syncmoss-0.4.1}/syncmoss/parameters/INSth.txt +0 -0
  119. {syncmoss-0.4.0 → syncmoss-0.4.1}/syncmoss/parameters/KB.txt +0 -0
  120. {syncmoss-0.4.0 → syncmoss-0.4.1}/syncmoss/parameters/NFS.txt +0 -0
  121. {syncmoss-0.4.0 → syncmoss-0.4.1}/syncmoss/parameters/calibr.png +0 -0
  122. {syncmoss-0.4.0 → syncmoss-0.4.1}/syncmoss/parameters/help.md +0 -0
  123. {syncmoss-0.4.0 → syncmoss-0.4.1}/syncmoss/parameters/models_description.md +0 -0
  124. {syncmoss-0.4.0 → syncmoss-0.4.1}/syncmoss/parameters_table.py +0 -0
  125. {syncmoss-0.4.0 → syncmoss-0.4.1}/syncmoss/results_table.py +0 -0
  126. {syncmoss-0.4.0 → syncmoss-0.4.1}/syncmoss/spectrum_io.py +0 -0
  127. {syncmoss-0.4.0 → syncmoss-0.4.1}/syncmoss/spectrum_plotter.py +0 -0
  128. {syncmoss-0.4.0 → syncmoss-0.4.1}/syncmoss/support_math.py +0 -0
  129. {syncmoss-0.4.0 → syncmoss-0.4.1}/syncmoss/test_graf.txt +0 -0
  130. {syncmoss-0.4.0 → syncmoss-0.4.1}/syncmoss/test_param.txt +0 -0
  131. {syncmoss-0.4.0 → syncmoss-0.4.1}/syncmoss/theme_dark.json +0 -0
  132. {syncmoss-0.4.0 → syncmoss-0.4.1}/syncmoss/theme_light.json +0 -0
  133. {syncmoss-0.4.0 → syncmoss-0.4.1}/syncmoss.egg-info/dependency_links.txt +0 -0
  134. {syncmoss-0.4.0 → syncmoss-0.4.1}/syncmoss.egg-info/entry_points.txt +0 -0
  135. {syncmoss-0.4.0 → syncmoss-0.4.1}/syncmoss.egg-info/requires.txt +0 -0
  136. {syncmoss-0.4.0 → syncmoss-0.4.1}/syncmoss.egg-info/top_level.txt +0 -0
  137. {syncmoss-0.4.0 → syncmoss-0.4.1}/tests/conftest.py +0 -0
  138. {syncmoss-0.4.0 → syncmoss-0.4.1}/tests/data/Distr_corr.mdl +0 -0
  139. {syncmoss-0.4.0 → syncmoss-0.4.1}/tests/data/Distr_distr.mdl +0 -0
  140. {syncmoss-0.4.0 → syncmoss-0.4.1}/tests/data/alpha_fe_sms_000.mca +0 -0
  141. {syncmoss-0.4.0 → syncmoss-0.4.1}/tests/data/calibration_000_golden.json +0 -0
  142. {syncmoss-0.4.0 → syncmoss-0.4.1}/tests/data/generate_cal_fixtures.py +0 -0
  143. {syncmoss-0.4.0 → syncmoss-0.4.1}/tests/data/synthetic_alpha_fe_cms_linear.mca +0 -0
  144. {syncmoss-0.4.0 → syncmoss-0.4.1}/tests/golden_models.json +0 -0
  145. {syncmoss-0.4.0 → syncmoss-0.4.1}/tests/test_calibration.py +0 -0
  146. {syncmoss-0.4.0 → syncmoss-0.4.1}/tests/test_calibration_helpers.py +0 -0
  147. {syncmoss-0.4.0 → syncmoss-0.4.1}/tests/test_calibration_path.py +0 -0
  148. {syncmoss-0.4.0 → syncmoss-0.4.1}/tests/test_distr_composition.py +0 -0
  149. {syncmoss-0.4.0 → syncmoss-0.4.1}/tests/test_distr_expansion.py +0 -0
  150. {syncmoss-0.4.0 → syncmoss-0.4.1}/tests/test_distr_mdl.py +0 -0
  151. {syncmoss-0.4.0 → syncmoss-0.4.1}/tests/test_distr_targets.py +0 -0
  152. {syncmoss-0.4.0 → syncmoss-0.4.1}/tests/test_expression_validation.py +0 -0
  153. {syncmoss-0.4.0 → syncmoss-0.4.1}/tests/test_golden_models.py +0 -0
  154. {syncmoss-0.4.0 → syncmoss-0.4.1}/tests/test_gui_smoke.py +0 -0
  155. {syncmoss-0.4.0 → syncmoss-0.4.1}/tests/test_hamiltonian_texture.py +0 -0
  156. {syncmoss-0.4.0 → syncmoss-0.4.1}/tests/test_instrumental_metadata.py +0 -0
  157. {syncmoss-0.4.0 → syncmoss-0.4.1}/tests/test_instrumental_method_choice.py +0 -0
  158. {syncmoss-0.4.0 → syncmoss-0.4.1}/tests/test_instrumental_window.py +0 -0
  159. {syncmoss-0.4.0 → syncmoss-0.4.1}/tests/test_integral_points.py +0 -0
  160. {syncmoss-0.4.0 → syncmoss-0.4.1}/tests/test_legacy.py +0 -0
  161. {syncmoss-0.4.0 → syncmoss-0.4.1}/tests/test_library_io.py +0 -0
  162. {syncmoss-0.4.0 → syncmoss-0.4.1}/tests/test_model_append.py +0 -0
  163. {syncmoss-0.4.0 → syncmoss-0.4.1}/tests/test_model_io.py +0 -0
  164. {syncmoss-0.4.0 → syncmoss-0.4.1}/tests/test_model_only_mode.py +0 -0
  165. {syncmoss-0.4.0 → syncmoss-0.4.1}/tests/test_nbaseline_results.py +0 -0
  166. {syncmoss-0.4.0 → syncmoss-0.4.1}/tests/test_parameter_links.py +0 -0
  167. {syncmoss-0.4.0 → syncmoss-0.4.1}/tests/test_polarization.py +0 -0
  168. {syncmoss-0.4.0 → syncmoss-0.4.1}/tests/test_recon_composition.py +0 -0
  169. {syncmoss-0.4.0 → syncmoss-0.4.1}/tests/test_recon_mdl.py +0 -0
  170. {syncmoss-0.4.0 → syncmoss-0.4.1}/tests/test_recon_results.py +0 -0
  171. {syncmoss-0.4.0 → syncmoss-0.4.1}/tests/test_reference_shift.py +0 -0
  172. {syncmoss-0.4.0 → syncmoss-0.4.1}/tests/test_sdw_grid.py +0 -0
  173. {syncmoss-0.4.0 → syncmoss-0.4.1}/tests/test_search_guards.py +0 -0
  174. {syncmoss-0.4.0 → syncmoss-0.4.1}/tests/test_sms_theory.py +0 -0
  175. {syncmoss-0.4.0 → syncmoss-0.4.1}/tests/test_support_math.py +0 -0
  176. {syncmoss-0.4.0 → syncmoss-0.4.1}/tests/test_texture_s1.py +0 -0
  177. {syncmoss-0.4.0 → syncmoss-0.4.1}/tests/test_theory_search.py +0 -0
  178. {syncmoss-0.4.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
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: syncmoss
3
- Version: 0.4.0
3
+ Version: 0.4.1
4
4
  Summary: SYNCmoss — Mössbauer spectroscopy analysis application
5
5
  Author: Sergey Yaroslavtsev
6
6
  License: MIT
@@ -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
@@ -1258,40 +1258,39 @@ THEORY_FREE_FIELDS = ('theta_urad', 'B_s', 'dEQ', 'shift',
1258
1258
  # over (0.19-2.4 T across the published temperature series), i.e. T ~ 75.93 C by
1259
1259
  # the calibrated law.
1260
1260
  #
1261
- # dEQ = -0.4056 +- 0.0030 mm/s comes from the 83-spectrum global fit. It is a
1262
- # property of FeBO3, so it is a starting value, not a guess: the independent
1263
- # laboratory measurement of Lyubutin et al. (2022) gives 2q = -0.3815 mm/s in
1264
- # this parametrisation, 6 % away.
1261
+ # dEQ = -0.4216 +- 0.0038 mm/s (2026-09-29 global fit of the 83-spectrum ESRF
1262
+ # series: 123 parameters, shape chi2 1.317, and the independent intensity
1263
+ # cross-check agreeing to 0.91-1.05 across all nine temperatures).
1265
1264
  #
1266
- # It read -0.3228 until the model was corrected in several places at once -- the
1267
- # baseline constrained to a pure scale, ONE B_s per temperature instead of one
1268
- # per spectrum, the Gaussian imperfection released, and f_LM changed from 0.70 to
1269
- # 0.774. Several of those had been letting dEQ absorb an error that was not its
1270
- # own. Because they moved together, this value is NOT attributable to any single
1271
- # one of them -- and note that f_LM was changed on theoretical grounds, not
1272
- # because it was measured to improve anything (see sms_study/test_flm_refine.py,
1273
- # where the data prefer 0.55-0.65).
1265
+ # QUOTE THE ERROR AS +-0.03, NOT +-0.004. dEQ tracks how much freedom the model
1266
+ # is given rather than converging on a value: -0.3228, then -0.4056, then
1267
+ # -0.3900, now -0.4216, each time the source model changed. The +-0.0038 is the
1268
+ # covariance of one fit; the spread ACROSS defensible fits is ten times that. So
1269
+ # the laboratory value of Lyubutin et al. (2022), 2q = -0.3815 mm/s in this
1270
+ # parametrisation, is consistent with this one -- do not report a disagreement.
1274
1271
  #
1275
- # mosaic_urad and gauss_fwhm are the two incoherent broadenings, both measured
1276
- # by the same global fit (14.49 +- 0.11 urad and 0.1222 +- 0.0007 mm/s).
1272
+ # f_LM is the opposite and IS well determined: 0.7718, 0.7661, 0.7714, 0.7711
1273
+ # across those same four models. It is pinned by the width of the saturated core
1274
+ # (see the dynamical-saturation argument in sms_theory), which the model gets
1275
+ # right, while dEQ trades against whatever is absorbing line asymmetry.
1277
1276
  #
1278
1277
  # mosaic_urad starts at ZERO: the angle deviation is off by default (see
1279
1278
  # THEORY_PASSES), and setting_urad = 3 urad is applied regardless, so the source
1280
- # is never completely unsmeared. f_LM is the nuclear amplitude scale; 0.774 is
1281
- # both the Debye value and where the 83-spectrum profile puts it.
1279
+ # is never completely unsmeared. Released only at the rocking minimum, where the
1280
+ # 83-spectrum fit puts it at 20-28 urad (it is a property of one measurement's
1281
+ # setting, not a constant -- so this is a start, not a value).
1282
1282
  #
1283
1283
  # theta and B_s are the OPERATING POINT: spectrum 008 of the ESRF series
1284
1284
  # (T = 75.825 C, the angle the source is actually used at), refitted under these
1285
1285
  # very conventions -- setting_urad = 3, n_angle = 11 -- at chi2 = 0.97. Starting
1286
1286
  # a search at a guessed point instead is what the B_s scan exists to repair.
1287
1287
  #
1288
- # dEQ, gauss_fwhm and f_LM come from the GLOBAL 83-spectrum fit, not from 008:
1289
- # they are properties of the crystal and the instrument, and one spectrum
1290
- # determines dEQ only to about half a sigma (008 alone prefers -0.24, the global
1291
- # fit gives -0.3900 +- 0.0027).
1292
- THEORY_START = {'theta_urad': 87.3, 'B_s': 0.7898, 'dEQ': -0.3900,
1288
+ # dEQ, gauss_fwhm and f_LM come from the GLOBAL fit, not from 008: they are
1289
+ # properties of the crystal and the instrument, and one spectrum determines dEQ
1290
+ # only to about +-0.04 (008 alone prefers -0.24).
1291
+ THEORY_START = {'theta_urad': 87.3, 'B_s': 0.7898, 'dEQ': -0.4216,
1293
1292
  'shift': -0.2882, 'mosaic_urad': 0.0, 'dBs_rel_fwhm': 0.0,
1294
- 'gauss_fwhm': 0.1303, 'f_LM': 0.7718}
1293
+ 'gauss_fwhm': 0.1303, 'f_LM': 0.7711}
1295
1294
 
1296
1295
  # Levenberg-Marquardt budget for the SCHEDULE passes. The final polish keeps
1297
1296
  # the full budget; the intermediate passes do not need it and each iteration is
@@ -3,7 +3,7 @@ import sys
3
3
  import multiprocessing as mp
4
4
  from PySide6.QtWidgets import QApplication
5
5
  from syncmoss.syncmoss_main import PhysicsApp
6
- __VERSION__ = "0.4.0"
6
+ __VERSION__ = "0.4.1"
7
7
 
8
8
 
9
9
  def _run_gui_smoke():
@@ -570,7 +570,7 @@ def save_model_to_library(main_window, title, comment=None, metadata=None, notif
570
570
  main_window.set_status("Library title is empty", "orange")
571
571
  return False
572
572
 
573
- library_dir = os.path.join(main_window.dir_path, 'Library')
573
+ library_dir = main_window.library_dir
574
574
  os.makedirs(library_dir, exist_ok=True)
575
575
  final_title, version_idx = compute_versioned_title_if_needed(library_dir, title)
576
576
  file_path = os.path.join(library_dir, f"{final_title}.mdl")
@@ -1225,7 +1225,12 @@ PHYS_FIELDS = ('theta_urad', 'B_s', 'dEQ', 'shift', 'thickness_um',
1225
1225
  'gauss_fwhm', 'n_gauss', 'dT_mK')
1226
1226
 
1227
1227
  PHYS_DEFAULTS = {
1228
- 'theta_urad': 70.0, 'B_s': 0.50, 'dEQ': -0.3228, 'shift': 0.0,
1228
+ # dEQ and f_LM must match instrumental_io.THEORY_START, which is where they
1229
+ # are documented. They were allowed to drift apart once (this dict kept
1230
+ # -0.3228 for three revisions after the fitted value had moved) and nothing
1231
+ # caught it, because every production call passes dEQ explicitly -- so the
1232
+ # stale number only ever surfaces in a new caller that does not.
1233
+ 'theta_urad': 70.0, 'B_s': 0.50, 'dEQ': -0.4216, 'shift': 0.0,
1229
1234
  'thickness_um': DEFAULT_THICKNESS_UM, 'mosaic_urad': DEFAULT_MOSAIC_URAD,
1230
1235
  'setting_urad': DEFAULT_SETTING_URAD, 'phi_m_deg': 0.0,
1231
1236
  'f_LM': DEFAULT_F_LM, 'dBs_rel_fwhm': 0.0, 'n_angle': 11.0,
@@ -580,13 +580,14 @@ def open_hamiltonian_helper(main_window):
580
580
 
581
581
  def export_library_pressed(main_window):
582
582
  """Export internal Library folder to a selected destination."""
583
+ # The Library dialogs open in the Library folder itself, not the work folder.
583
584
  destination = QFileDialog.getExistingDirectory(
584
- main_window, "Select destination folder", main_window.workfolder or main_window.dir_path)
585
+ main_window, "Select destination folder", main_window.library_dir)
585
586
  if not destination:
586
587
  main_window.set_status("Export Library canceled", "orange")
587
588
  return
588
589
  try:
589
- library_dir = os.path.join(main_window.dir_path, 'Library')
590
+ library_dir = main_window.library_dir
590
591
  target = export_library(library_dir, destination)
591
592
  main_window.set_status(f"Library exported to: {target}", "green")
592
593
  except Exception as e:
@@ -596,12 +597,12 @@ def export_library_pressed(main_window):
596
597
  def import_library_pressed(main_window):
597
598
  """Import .mdl files from selected folder into internal Library folder."""
598
599
  source = QFileDialog.getExistingDirectory(
599
- main_window, "Select source folder", main_window.workfolder or main_window.dir_path)
600
+ main_window, "Select source folder", main_window.library_dir)
600
601
  if not source:
601
602
  main_window.set_status("Import Library canceled", "orange")
602
603
  return
603
604
  try:
604
- library_dir = os.path.join(main_window.dir_path, 'Library')
605
+ library_dir = main_window.library_dir
605
606
  result = import_library(source, library_dir)
606
607
  if isinstance(result, dict):
607
608
  copied = int(result.get('copied', 0))