finalcif 172__tar.gz → 175__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 (201) hide show
  1. {finalcif-172 → finalcif-175}/.gitignore +1 -0
  2. {finalcif-172 → finalcif-175}/.ruff.toml +3 -1
  3. {finalcif-172 → finalcif-175}/.run/compile_ui_files.run.xml +1 -1
  4. {finalcif-172 → finalcif-175}/PKG-INFO +6 -6
  5. finalcif-175/finalcif/__init__.py +3 -0
  6. {finalcif-172 → finalcif-175}/finalcif/appwindow.py +120 -9
  7. {finalcif-172 → finalcif-175}/finalcif/cif/checkcif/checkcif.py +7 -5
  8. finalcif-175/finalcif/cif/hkl.py +288 -0
  9. {finalcif-172 → finalcif-175}/finalcif/datafiles/bruker_data.py +177 -44
  10. {finalcif-172 → finalcif-175}/finalcif/datafiles/data.py +3 -0
  11. finalcif-175/finalcif/datafiles/sadabs.py +545 -0
  12. {finalcif-172 → finalcif-175}/finalcif/datafiles/saint.py +52 -53
  13. {finalcif-172 → finalcif-175}/finalcif/gui/dialogs.py +1 -1
  14. {finalcif-172 → finalcif-175}/finalcif/gui/finalcif_gui_ui.py +63 -54
  15. {finalcif-172 → finalcif-175}/finalcif/gui/finalcif_gui_ui.ui +117 -105
  16. {finalcif-172 → finalcif-175}/finalcif/gui/validators.py +5 -1
  17. {finalcif-172 → finalcif-175}/finalcif/template/report_fastmolwidget.tmpl +11 -1
  18. {finalcif-172 → finalcif-175}/finalcif/tools/selfupdate.py +129 -16
  19. {finalcif-172 → finalcif-175}/finalcif/tools/squeeze.py +76 -1
  20. {finalcif-172 → finalcif-175}/finalcif/tools/sumformula.py +27 -3
  21. {finalcif-172 → finalcif-175}/finalcif/tools/z_from_packing.py +701 -281
  22. {finalcif-172 → finalcif-175}/pyproject.toml +4 -4
  23. {finalcif-172 → finalcif-175}/uv.lock +215 -135
  24. finalcif-172/finalcif/__init__.py +0 -3
  25. finalcif-172/finalcif/cif/hkl.py +0 -94
  26. finalcif-172/finalcif/datafiles/sadabs.py +0 -191
  27. {finalcif-172 → finalcif-175}/.gitattributes +0 -0
  28. {finalcif-172 → finalcif-175}/.githooks/pre-push +0 -0
  29. {finalcif-172 → finalcif-175}/.python-version +0 -0
  30. {finalcif-172 → finalcif-175}/.run/Make Documentation MAC.run.xml +0 -0
  31. {finalcif-172 → finalcif-175}/.run/make documentation Windows.run.xml +0 -0
  32. {finalcif-172 → finalcif-175}/AGENTS.md +0 -0
  33. {finalcif-172 → finalcif-175}/CODE_OF_CONDUCT.md +0 -0
  34. {finalcif-172 → finalcif-175}/CONTRIBUTING.md +0 -0
  35. {finalcif-172 → finalcif-175}/Finalcif_installer_win.spec +0 -0
  36. {finalcif-172 → finalcif-175}/Finalcif_linux_onefile.spec +0 -0
  37. {finalcif-172 → finalcif-175}/Finalcif_mac.spec +0 -0
  38. {finalcif-172 → finalcif-175}/LICENSE +0 -0
  39. {finalcif-172 → finalcif-175}/README.md +0 -0
  40. {finalcif-172 → finalcif-175}/finalcif/app_path.py +0 -0
  41. {finalcif-172 → finalcif-175}/finalcif/cif/__init__.py +0 -0
  42. {finalcif-172 → finalcif-175}/finalcif/cif/all_cif_dicts.py +0 -0
  43. {finalcif-172 → finalcif-175}/finalcif/cif/atoms.py +0 -0
  44. {finalcif-172 → finalcif-175}/finalcif/cif/checkcif/__init__.py +0 -0
  45. {finalcif-172 → finalcif-175}/finalcif/cif/cif_dict_foo.py +0 -0
  46. {finalcif-172 → finalcif-175}/finalcif/cif/cif_file_io.py +0 -0
  47. {finalcif-172 → finalcif-175}/finalcif/cif/cif_keyword_mapping.py +0 -0
  48. {finalcif-172 → finalcif-175}/finalcif/cif/cif_order.py +0 -0
  49. {finalcif-172 → finalcif-175}/finalcif/cif/cod/__init__.py +0 -0
  50. {finalcif-172 → finalcif-175}/finalcif/cif/cod/deposit.py +0 -0
  51. {finalcif-172 → finalcif-175}/finalcif/cif/cod/deposit_check.py +0 -0
  52. {finalcif-172 → finalcif-175}/finalcif/cif/cod/deposition_list.py +0 -0
  53. {finalcif-172 → finalcif-175}/finalcif/cif/cod/doi.py +0 -0
  54. {finalcif-172 → finalcif-175}/finalcif/cif/cod/upload.py +0 -0
  55. {finalcif-172 → finalcif-175}/finalcif/cif/cod/website_parser.py +0 -0
  56. {finalcif-172 → finalcif-175}/finalcif/cif/core_dict.py +0 -0
  57. {finalcif-172 → finalcif-175}/finalcif/cif/modulation_dict.py +0 -0
  58. {finalcif-172 → finalcif-175}/finalcif/cif/powder_dict.py +0 -0
  59. {finalcif-172 → finalcif-175}/finalcif/cif/reference.py +0 -0
  60. {finalcif-172 → finalcif-175}/finalcif/cif/restraints_dict.py +0 -0
  61. {finalcif-172 → finalcif-175}/finalcif/cif/text.py +0 -0
  62. {finalcif-172 → finalcif-175}/finalcif/cif/twin_dict.py +0 -0
  63. {finalcif-172 → finalcif-175}/finalcif/cif/vrf_entry.py +0 -0
  64. {finalcif-172 → finalcif-175}/finalcif/ciforder/__init__.py +0 -0
  65. {finalcif-172 → finalcif-175}/finalcif/ciforder/order.py +0 -0
  66. {finalcif-172 → finalcif-175}/finalcif/ciforder/order_ui.py +0 -0
  67. {finalcif-172 → finalcif-175}/finalcif/ciforder/order_ui.ui +0 -0
  68. {finalcif-172 → finalcif-175}/finalcif/datafiles/__init__.py +0 -0
  69. {finalcif-172 → finalcif-175}/finalcif/datafiles/bruker_frame.py +0 -0
  70. {finalcif-172 → finalcif-175}/finalcif/datafiles/ccdc_mail.py +0 -0
  71. {finalcif-172 → finalcif-175}/finalcif/datafiles/p4p_reader.py +0 -0
  72. {finalcif-172 → finalcif-175}/finalcif/datafiles/shelx_lst.py +0 -0
  73. {finalcif-172 → finalcif-175}/finalcif/datafiles/utils.py +0 -0
  74. {finalcif-172 → finalcif-175}/finalcif/equip_property/__init__.py +0 -0
  75. {finalcif-172 → finalcif-175}/finalcif/equip_property/author_loop_templates.py +0 -0
  76. {finalcif-172 → finalcif-175}/finalcif/equip_property/equipment.py +0 -0
  77. {finalcif-172 → finalcif-175}/finalcif/equip_property/properties.py +0 -0
  78. {finalcif-172 → finalcif-175}/finalcif/equip_property/tools.py +0 -0
  79. {finalcif-172 → finalcif-175}/finalcif/finalcif_start.py +0 -0
  80. {finalcif-172 → finalcif-175}/finalcif/gui/__init__.py +0 -0
  81. {finalcif-172 → finalcif-175}/finalcif/gui/block_combobox.py +0 -0
  82. {finalcif-172 → finalcif-175}/finalcif/gui/checkcif_browser.py +0 -0
  83. {finalcif-172 → finalcif-175}/finalcif/gui/cif_table_model.py +0 -0
  84. {finalcif-172 → finalcif-175}/finalcif/gui/cif_table_view.py +0 -0
  85. {finalcif-172 → finalcif-175}/finalcif/gui/combobox.py +0 -0
  86. {finalcif-172 → finalcif-175}/finalcif/gui/custom_classes.py +0 -0
  87. {finalcif-172 → finalcif-175}/finalcif/gui/edit_button.py +0 -0
  88. {finalcif-172 → finalcif-175}/finalcif/gui/equipmenttable.py +0 -0
  89. {finalcif-172 → finalcif-175}/finalcif/gui/file_editor.py +0 -0
  90. {finalcif-172 → finalcif-175}/finalcif/gui/import_selector.py +0 -0
  91. {finalcif-172 → finalcif-175}/finalcif/gui/import_selector_ui.py +0 -0
  92. {finalcif-172 → finalcif-175}/finalcif/gui/import_selector_ui.ui +0 -0
  93. {finalcif-172 → finalcif-175}/finalcif/gui/loop_creator.py +0 -0
  94. {finalcif-172 → finalcif-175}/finalcif/gui/loop_creator_ui.py +0 -0
  95. {finalcif-172 → finalcif-175}/finalcif/gui/loop_creator_ui.ui +0 -0
  96. {finalcif-172 → finalcif-175}/finalcif/gui/loops.py +0 -0
  97. {finalcif-172 → finalcif-175}/finalcif/gui/mainstackwidget.py +0 -0
  98. {finalcif-172 → finalcif-175}/finalcif/gui/mixins.py +0 -0
  99. {finalcif-172 → finalcif-175}/finalcif/gui/new_key_dialog.py +0 -0
  100. {finalcif-172 → finalcif-175}/finalcif/gui/new_key_dialog_ui.py +0 -0
  101. {finalcif-172 → finalcif-175}/finalcif/gui/new_key_dialog_ui.ui +0 -0
  102. {finalcif-172 → finalcif-175}/finalcif/gui/plaintextedit.py +0 -0
  103. {finalcif-172 → finalcif-175}/finalcif/gui/playground.py +0 -0
  104. {finalcif-172 → finalcif-175}/finalcif/gui/propertytable.py +0 -0
  105. {finalcif-172 → finalcif-175}/finalcif/gui/shelx_navigation.py +0 -0
  106. {finalcif-172 → finalcif-175}/finalcif/gui/single_line_textedit.py +0 -0
  107. {finalcif-172 → finalcif-175}/finalcif/gui/spell_check_edit.py +0 -0
  108. {finalcif-172 → finalcif-175}/finalcif/gui/squeeze_dialog.py +0 -0
  109. {finalcif-172 → finalcif-175}/finalcif/gui/syntax_highlighter.py +0 -0
  110. {finalcif-172 → finalcif-175}/finalcif/gui/text_templates_ui.py +0 -0
  111. {finalcif-172 → finalcif-175}/finalcif/gui/text_templates_ui.ui +0 -0
  112. {finalcif-172 → finalcif-175}/finalcif/gui/text_value_editor.py +0 -0
  113. {finalcif-172 → finalcif-175}/finalcif/gui/timers.py +0 -0
  114. {finalcif-172 → finalcif-175}/finalcif/gui/vrf_classes.py +0 -0
  115. {finalcif-172 → finalcif-175}/finalcif/gui/vzs_viewer.py +0 -0
  116. {finalcif-172 → finalcif-175}/finalcif/icon/__init__.py +0 -0
  117. {finalcif-172 → finalcif-175}/finalcif/icon/finalcif.png +0 -0
  118. {finalcif-172 → finalcif-175}/finalcif/icon/finalcif2.ico +0 -0
  119. {finalcif-172 → finalcif-175}/finalcif/icon/finalcif2.png +0 -0
  120. {finalcif-172 → finalcif-175}/finalcif/icon/multitable.ico +0 -0
  121. {finalcif-172 → finalcif-175}/finalcif/icon/multitable.png +0 -0
  122. {finalcif-172 → finalcif-175}/finalcif/icon/multitable.xcf +0 -0
  123. {finalcif-172 → finalcif-175}/finalcif/report/__init__.py +0 -0
  124. {finalcif-172 → finalcif-175}/finalcif/report/archive_report.py +0 -0
  125. {finalcif-172 → finalcif-175}/finalcif/report/gui/__init__.py +0 -0
  126. {finalcif-172 → finalcif-175}/finalcif/report/gui/mainwindow.py +0 -0
  127. {finalcif-172 → finalcif-175}/finalcif/report/gui/mainwindow.ui +0 -0
  128. {finalcif-172 → finalcif-175}/finalcif/report/mtools.py +0 -0
  129. {finalcif-172 → finalcif-175}/finalcif/report/references.py +0 -0
  130. {finalcif-172 → finalcif-175}/finalcif/report/report_text.py +0 -0
  131. {finalcif-172 → finalcif-175}/finalcif/report/symm.py +0 -0
  132. {finalcif-172 → finalcif-175}/finalcif/report/tables.py +0 -0
  133. {finalcif-172 → finalcif-175}/finalcif/report/templated_report.py +0 -0
  134. {finalcif-172 → finalcif-175}/finalcif/template/__init__.py +0 -0
  135. {finalcif-172 → finalcif-175}/finalcif/template/bootstrap/bootstrap.min.css +0 -0
  136. {finalcif-172 → finalcif-175}/finalcif/template/machines/D8_VENTURE.cif +0 -0
  137. {finalcif-172 → finalcif-175}/finalcif/template/mathml2omml.xsl +0 -0
  138. {finalcif-172 → finalcif-175}/finalcif/template/miew/LICENSES +0 -0
  139. {finalcif-172 → finalcif-175}/finalcif/template/miew/Miew.css +0 -0
  140. {finalcif-172 → finalcif-175}/finalcif/template/miew/Miew.min.js +0 -0
  141. {finalcif-172 → finalcif-175}/finalcif/template/miew/lodash.min.js +0 -0
  142. {finalcif-172 → finalcif-175}/finalcif/template/miew/three.js +0 -0
  143. {finalcif-172 → finalcif-175}/finalcif/template/report.tex +0 -0
  144. {finalcif-172 → finalcif-175}/finalcif/template/report.tmpl +0 -0
  145. {finalcif-172 → finalcif-175}/finalcif/template/report2.tex +0 -0
  146. {finalcif-172 → finalcif-175}/finalcif/template/report_default.docx +0 -0
  147. {finalcif-172 → finalcif-175}/finalcif/template/template1.docx +0 -0
  148. {finalcif-172 → finalcif-175}/finalcif/template/template_for_multitable.docx +0 -0
  149. {finalcif-172 → finalcif-175}/finalcif/template/template_text.docx +0 -0
  150. {finalcif-172 → finalcif-175}/finalcif/template/template_without_text.docx +0 -0
  151. {finalcif-172 → finalcif-175}/finalcif/template/templates.py +0 -0
  152. {finalcif-172 → finalcif-175}/finalcif/template/unicode2latex/FakePlasTeX/FakeContext.py +0 -0
  153. {finalcif-172 → finalcif-175}/finalcif/template/unicode2latex/FakePlasTeX/FakeTokenizer.py +0 -0
  154. {finalcif-172 → finalcif-175}/finalcif/template/unicode2latex/FakePlasTeX/README.md +0 -0
  155. {finalcif-172 → finalcif-175}/finalcif/template/unicode2latex/FakePlasTeX/TokenizerPassThru.py +0 -0
  156. {finalcif-172 → finalcif-175}/finalcif/template/unicode2latex/LICENSE +0 -0
  157. {finalcif-172 → finalcif-175}/finalcif/template/unicode2latex/__init__.py +0 -0
  158. {finalcif-172 → finalcif-175}/finalcif/template/unicode2latex/u2l.py +0 -0
  159. {finalcif-172 → finalcif-175}/finalcif/template/unicode2latex/unicode-math-table.tex +0 -0
  160. {finalcif-172 → finalcif-175}/finalcif/template/unicode2latex/unicode-math-xetex.sty +0 -0
  161. {finalcif-172 → finalcif-175}/finalcif/template/xsl/convert.py +0 -0
  162. {finalcif-172 → finalcif-175}/finalcif/template/xsl/xmltohtml.xsl +0 -0
  163. {finalcif-172 → finalcif-175}/finalcif/template/xsl/xmltolatex.xsl +0 -0
  164. {finalcif-172 → finalcif-175}/finalcif/tools/__init__.py +0 -0
  165. {finalcif-172 → finalcif-175}/finalcif/tools/chemparse.py +0 -0
  166. {finalcif-172 → finalcif-175}/finalcif/tools/download.py +0 -0
  167. {finalcif-172 → finalcif-175}/finalcif/tools/dsrmath.py +0 -0
  168. {finalcif-172 → finalcif-175}/finalcif/tools/formal_charge.py +0 -0
  169. {finalcif-172 → finalcif-175}/finalcif/tools/misc.py +0 -0
  170. {finalcif-172 → finalcif-175}/finalcif/tools/mol_file_writer.py +0 -0
  171. {finalcif-172 → finalcif-175}/finalcif/tools/options.py +0 -0
  172. {finalcif-172 → finalcif-175}/finalcif/tools/platon.py +0 -0
  173. {finalcif-172 → finalcif-175}/finalcif/tools/pupdate.py +0 -0
  174. {finalcif-172 → finalcif-175}/finalcif/tools/settings.py +0 -0
  175. {finalcif-172 → finalcif-175}/finalcif/tools/shred.py +0 -0
  176. {finalcif-172 → finalcif-175}/finalcif/tools/space_groups.py +0 -0
  177. {finalcif-172 → finalcif-175}/finalcif/tools/spgr_format.py +0 -0
  178. {finalcif-172 → finalcif-175}/finalcif/tools/statusbar.py +0 -0
  179. {finalcif-172 → finalcif-175}/finalcif.exe +0 -0
  180. {finalcif-172 → finalcif-175}/platon/README.txt +0 -0
  181. {finalcif-172 → finalcif-175}/platon/compile_platon.bat +0 -0
  182. {finalcif-172 → finalcif-175}/platon/platon_special.exe +0 -0
  183. {finalcif-172 → finalcif-175}/platon/platon_special.f +0 -0
  184. {finalcif-172 → finalcif-175}/pylock.toml +0 -0
  185. {finalcif-172 → finalcif-175}/run_and_capture.py +0 -0
  186. {finalcif-172 → finalcif-175}/run_finalcif +0 -0
  187. {finalcif-172 → finalcif-175}/run_finalcif.bat +0 -0
  188. {finalcif-172 → finalcif-175}/screenshots/finalcif_checkcif.png +0 -0
  189. {finalcif-172 → finalcif-175}/screenshots/finalcif_details.png +0 -0
  190. {finalcif-172 → finalcif-175}/screenshots/finalcif_loops.png +0 -0
  191. {finalcif-172 → finalcif-175}/screenshots/finalcif_loops2.png +0 -0
  192. {finalcif-172 → finalcif-175}/screenshots/finalcif_main.png +0 -0
  193. {finalcif-172 → finalcif-175}/screenshots/finalcif_report.png +0 -0
  194. {finalcif-172 → finalcif-175}/screenshots/finalcif_responses.png +0 -0
  195. {finalcif-172 → finalcif-175}/screenshots/loops_column_buttons.png +0 -0
  196. {finalcif-172 → finalcif-175}/work/compare_z_platon.py +0 -0
  197. {finalcif-172 → finalcif-175}/work/diagnose_1519506.py +0 -0
  198. {finalcif-172 → finalcif-175}/work/diagnose_z.py +0 -0
  199. {finalcif-172 → finalcif-175}/work/investigate_failures.py +0 -0
  200. {finalcif-172 → finalcif-175}/work/validate_cod.py +0 -0
  201. {finalcif-172 → finalcif-175}/work/validate_zprime.py +0 -0
@@ -243,3 +243,4 @@ finalcif_keys_order.cif
243
243
  /work/dicts/ddl.dic
244
244
  /test-data/4060314-finalcif.zip
245
245
  /test-data/report_4060314-finalcif.html
246
+ Z-value_examples
@@ -51,7 +51,9 @@ select = ["E", "F", "B", "UP", "ANN", "A", "C4", "DTZ", "NPY", "PL", "FURB", "RU
51
51
  ignore = ["E501", "F401", "E401", "B904", "E402", "C901",
52
52
  "PLR0912", "PLR0915", "PLR0913", "ANN201", "ANN201", "ANN204", "ANN001", "ANN002",
53
53
  "D102", "D103", "D205", "D212", "PLR2004", "PLW2901", "DTZ005", "DTZ006", "UP015",
54
- "RUF012", "DTZ001",
54
+ "RUF012",
55
+ "DTZ001",
56
+ "PLR0911",
55
57
  ]
56
58
 
57
59
  # Avoid trying to fix flake8-bugbear (`B`) violations.
@@ -14,7 +14,7 @@
14
14
  <option name="ADD_SOURCE_ROOTS" value="true" />
15
15
  <option name="DEBUG_JUST_MY_CODE" value="true" />
16
16
  <EXTENSION ID="PythonCoverageRunConfigurationExtension" runner="coverage.py" />
17
- <option name="RUN_TOOL" value="true" />
17
+ <option name="RUN_TOOL" value="false" />
18
18
  <option name="SCRIPT_NAME" value="scripts/compile_ui_files.py" />
19
19
  <option name="PARAMETERS" value="" />
20
20
  <option name="SHOW_COMMAND_LINE" value="false" />
@@ -1,6 +1,6 @@
1
- Metadata-Version: 2.4
1
+ Metadata-Version: 2.5
2
2
  Name: finalcif
3
- Version: 172
3
+ Version: 175
4
4
  Summary: CIF file editor
5
5
  Project-URL: Homepage, https://dkratzert.de/finalcif.html
6
6
  Project-URL: Bug Tracker, https://github.com/dkratzert/FinalCif/issues
@@ -15,14 +15,14 @@ Requires-Python: >=3.12
15
15
  Requires-Dist: certifi==2026.6.17
16
16
  Requires-Dist: chardet
17
17
  Requires-Dist: charset-normalizer
18
- Requires-Dist: docxcompose==2.2.0
18
+ Requires-Dist: docxcompose>=2.2.0
19
19
  Requires-Dist: docxtpl[subdoc]>=0.20.2
20
- Requires-Dist: fastmolwidget>=1.0.0
20
+ Requires-Dist: fastmolwidget>=1.2.0
21
21
  Requires-Dist: gemmi>=0.7.5
22
22
  Requires-Dist: html2text
23
23
  Requires-Dist: lxml>=6.1.1
24
24
  Requires-Dist: numpy==2.4.6
25
- Requires-Dist: packaging==26.2
25
+ Requires-Dist: packaging==26.3
26
26
  Requires-Dist: pefile
27
27
  Requires-Dist: pybind11>=3
28
28
  Requires-Dist: pyenchant
@@ -31,7 +31,7 @@ Requires-Dist: python-docx
31
31
  Requires-Dist: qtawesome
32
32
  Requires-Dist: qtpy
33
33
  Requires-Dist: requests
34
- Requires-Dist: shelxfile==24
34
+ Requires-Dist: shelxfile>=24
35
35
  Requires-Dist: urllib3==2.7.0
36
36
  Requires-Dist: wheel==0.47.0
37
37
  Description-Content-Type: text/markdown
@@ -0,0 +1,3 @@
1
+
2
+ VERSION = 175
3
+ __version__ = VERSION
@@ -26,6 +26,8 @@ if TYPE_CHECKING:
26
26
  from finalcif.cif.checkcif.checkcif import MyHTMLParser
27
27
 
28
28
  from fastmolwidget import Atomtuple
29
+ from fastmolwidget.density_controls import ResidualDensityControls
30
+ from fastmolwidget.hkl_io import find_reflection_file, has_reflections
29
31
  from fastmolwidget.part_combo import PartFilterWidget
30
32
 
31
33
  from finalcif.gui.shelx_navigation import find_shelx_line_for_atom, scroll_and_highlight_shelx_atom
@@ -86,10 +88,12 @@ from finalcif.tools.platon import PlatonRunner
86
88
  from finalcif.tools.settings import FinalCifSettings
87
89
  from finalcif.tools.shred import ShredCIF
88
90
  from finalcif.tools.space_groups import SpaceGroups
91
+ from finalcif.tools.squeeze import append_unknown_solvent, has_unassigned_solvent
89
92
  from finalcif.tools.z_from_packing import count_z_and_zprime, ZResult
90
93
  from finalcif.tools.spgr_format import spgrps
91
94
  from finalcif.tools.statusbar import StatusBar
92
- from finalcif.tools.sumformula import formula_str_to_dict, formula_to_html, sum_formula_to_html
95
+ from finalcif.tools.sumformula import (formula_str_to_dict, formula_to_html, sum_formula_to_html,
96
+ with_solvent_marker)
93
97
 
94
98
  DEBUG = False
95
99
  app = QApplication.instance()
@@ -194,6 +198,14 @@ class AppWindow(QMainWindow):
194
198
  self.parts_combo = PartFilterWidget(parent=self.ui.molGroupBox, label=' ', min_combo_width=90)
195
199
  self.parts_combo.setObjectName('partsFilterWidget')
196
200
  self.ui.horizontalLayout_7.insertWidget(3, self.parts_combo)
201
+ # Residual density button and level spin box, next to the parts selector.
202
+ # The file dialog is switched off: without reflection data the structure
203
+ # is simply shown without density.
204
+ self.density_controls = ResidualDensityControls(render_widget=self.ui.render_widget,
205
+ parent=self.ui.molGroupBox,
206
+ allow_reflection_dialog=False)
207
+ self.density_controls.setObjectName('residualDensityControls')
208
+ self.ui.horizontalLayout_7.insertWidget(4, self.density_controls)
197
209
  # Inject the Author Editor tab (defined in the .ui file) into LoopsPage
198
210
  self.ui.loops_page.set_author_editor_tab(self.ui.author_editor_widget)
199
211
  self.fixfont = QtGui.QFontDatabase.systemFont(QtGui.QFontDatabase.SystemFont.FixedFont)
@@ -1305,8 +1317,9 @@ class AppWindow(QMainWindow):
1305
1317
  self.ui.CheckCifLogPlainTextEdit.appendHtml(
1306
1318
  f'<br><b>{n} validation response forms {form_noun} added to the main table.</b>')
1307
1319
  else:
1320
+ levels = 'A, B or C' if self.ui.explainCandGAlertsCheckBox.isChecked() else 'A or B'
1308
1321
  self.ui.CheckCifLogPlainTextEdit.appendHtml(
1309
- '<br><b>No level A, B or C alerts require a response.</b>')
1322
+ f'<br><b>No level {levels} alerts require a response.</b>')
1310
1323
 
1311
1324
  def _vrf_insert_position(self) -> int:
1312
1325
  """Return the row index just after the last existing ``_vrf_*`` row in the main table.
@@ -1358,7 +1371,8 @@ class AppWindow(QMainWindow):
1358
1371
  hkl_upload=(not self.ui.structfactCheckBox.isChecked()), pdf=False,
1359
1372
  url=self.options.checkcif_url,
1360
1373
  full_iucr=self.ui.fullIucrCheckBox.isChecked(),
1361
- check_duplicates=self.ui.checkDuplicatesCheckBox.isChecked())
1374
+ check_duplicates=self.ui.checkDuplicatesCheckBox.isChecked(),
1375
+ explain_c=self.ui.explainCAlertsCheckBox.isChecked())
1362
1376
  self.ckf.progress.connect(self._ckf_progress)
1363
1377
  self.ckf.failed.connect(self._checkcif_failed)
1364
1378
  self.ckf.finished.connect(self._checkcif_finished)
@@ -1506,6 +1520,50 @@ class AppWindow(QMainWindow):
1506
1520
  self.ui.cif_main_table.setText(key='_chemical_formula_moiety', txt=formula_moiety, column=Column.EDIT)
1507
1521
  self.show_moiety_formula_tooltip(moiety if moiety not in ['', '?'] else formula_moiety)
1508
1522
 
1523
+ def _with_squeezed_solvent(self, formula_moiety: str) -> str:
1524
+ """Mark a generated moiety formula with ``[+ solvent]`` for squeezed structures.
1525
+
1526
+ The unmodelled solvent of a PLATON SQUEEZE / Olex2-SMTBX structure is
1527
+ missing from the atom model, so the moiety derived from the bond graph
1528
+ is incomplete unless the void content was assigned a formula.
1529
+ """
1530
+ try:
1531
+ if has_unassigned_solvent(self.cif):
1532
+ return append_unknown_solvent(formula_moiety)
1533
+ except Exception:
1534
+ pass
1535
+ return formula_moiety
1536
+
1537
+ def mark_squeezed_solvent_in_formulas(self) -> None:
1538
+ """Append ``[+ solvent]`` to the sum and moiety formula of a squeezed structure.
1539
+
1540
+ PLATON marks both ``_chemical_formula_sum`` and
1541
+ ``_chemical_formula_moiety`` this way whenever solvent was removed by
1542
+ SQUEEZE or an Olex2/SMTBX mask, because the formulas then describe less
1543
+ than the real content of the unit cell. Nothing is marked when the void
1544
+ content was assigned a formula, or for multi-CIF files.
1545
+ """
1546
+ try:
1547
+ if self.cif.is_multi_cif or not has_unassigned_solvent(self.cif):
1548
+ return
1549
+ except Exception:
1550
+ return
1551
+ for key in ('_chemical_formula_sum', '_chemical_formula_moiety'):
1552
+ self._append_solvent_marker_to(key)
1553
+
1554
+ def _append_solvent_marker_to(self, key: str) -> None:
1555
+ """Write *key* back with the unknown-solvent marker unless it is empty.
1556
+
1557
+ ``append_unknown_solvent`` normalises a marker that a previous save
1558
+ wrapped across two CIF lines, so the comparison has to be made against
1559
+ the normalised value to avoid writing the row over and over.
1560
+ """
1561
+ current = (self.ui.cif_main_table.getTextFromKey(key=key, col=Column.EDIT)
1562
+ or self.ui.cif_main_table.getTextFromKey(key=key, col=Column.CIF))
1563
+ marked = append_unknown_solvent(current)
1564
+ if marked and marked != current.strip():
1565
+ self.ui.cif_main_table.setText(key=key, txt=marked, column=Column.EDIT)
1566
+
1509
1567
  def append_to_ciflog_without_newline(self, text: str = '') -> None:
1510
1568
  self.ui.CheckCifLogPlainTextEdit.moveCursor(QtGui.QTextCursor.MoveOperation.End)
1511
1569
  self.ui.CheckCifLogPlainTextEdit.insertPlainText(text)
@@ -2046,6 +2104,7 @@ class AppWindow(QMainWindow):
2046
2104
  self.enable_buttons()
2047
2105
  self.fill_space_group_lineedit()
2048
2106
  self.fill_sum_formula_lineedit()
2107
+ self.mark_squeezed_solvent_in_formulas()
2049
2108
  self.ui.CCDCNumLineEdit.setText(self.cif['_database_code_depnum_ccdc_archive'])
2050
2109
  self.ui.CheckcifPlaintextEdit.clear()
2051
2110
  self.ui.TemplatesStackedWidget.setCurrentIndex(0)
@@ -2084,11 +2143,12 @@ class AppWindow(QMainWindow):
2084
2143
  return False
2085
2144
 
2086
2145
  def fill_sum_formula_lineedit(self) -> None:
2146
+ formula = self.cif['_chemical_formula_sum'].strip(" '")
2087
2147
  try:
2088
- self.ui.SumFormMainLineEdit.setText(sum_formula_to_html(formula_str_to_dict(
2089
- self.cif['_chemical_formula_sum'].strip(" '"))))
2148
+ html = sum_formula_to_html(formula_str_to_dict(formula))
2149
+ self.ui.SumFormMainLineEdit.setText(with_solvent_marker(html, formula))
2090
2150
  except Exception:
2091
- self.ui.SumFormMainLineEdit.setText(self.cif['_chemical_formula_sum'].strip(" '"))
2151
+ self.ui.SumFormMainLineEdit.setText(formula)
2092
2152
  self.show_moiety_formula_tooltip()
2093
2153
 
2094
2154
  def show_moiety_formula_tooltip(self, formula_moiety: str = '') -> None:
@@ -2255,6 +2315,7 @@ class AppWindow(QMainWindow):
2255
2315
  if peak:
2256
2316
  self.ui.peakLineEdit.setText("{} / {}".format(peak, self.cif['_refine_diff_density_min']))
2257
2317
  self._show_shelx_file()
2318
+ self._update_residual_density_source()
2258
2319
  try:
2259
2320
  single_shot(self, 0, lambda: self.view_molecule(reset_view=True))
2260
2321
  except Exception:
@@ -2316,6 +2377,29 @@ class AppWindow(QMainWindow):
2316
2377
  self.ui.render_widget.grow_molecule(atom_tuples,
2317
2378
  cell=self.cif.cell[:6])
2318
2379
 
2380
+ def _update_residual_density_source(self) -> None:
2381
+ """Tell the viewer which model and reflections the shown atoms come from.
2382
+
2383
+ The block in memory is used, not the file on disk, so that the density
2384
+ follows unsaved edits and the block the user selected in a multi-block
2385
+ CIF. Without any reflection data the density button stays disabled and
2386
+ the structure is simply displayed without density.
2387
+ """
2388
+ with suppress(Exception):
2389
+ self.ui.render_widget.set_model_source(self.cif.block,
2390
+ reflections=self._reflection_source())
2391
+ self.density_controls.update_density_availability()
2392
+
2393
+ def _reflection_source(self) -> gemmi.cif.Block | Path | None:
2394
+ """Where the reflections of the current structure are, if anywhere.
2395
+
2396
+ Self-contained SHELXL CIFs carry the whole ``.hkl`` in the block itself;
2397
+ otherwise a sibling ``.hkl``/``.fcf`` of the CIF on disk is used.
2398
+ """
2399
+ if has_reflections(self.cif.block):
2400
+ return self.cif.block
2401
+ return find_reflection_file(self.cif.fileobj)
2402
+
2319
2403
  def _update_z_label(self) -> None:
2320
2404
  """Start a background computation of Z and Z′ and show a placeholder.
2321
2405
 
@@ -2371,7 +2455,7 @@ class AppWindow(QMainWindow):
2371
2455
  return
2372
2456
  # Propagate moiety formula to the CIF table if not already filled.
2373
2457
  if result.moiety_formula:
2374
- self.add_moiety_furmula(result.moiety_formula)
2458
+ self.add_moiety_furmula(self._with_squeezed_solvent(result.moiety_formula))
2375
2459
  confidence_icon = {'high': '✓', 'medium': '~', 'formula': '≡', 'low': '?'}[result.confidence]
2376
2460
  self.ui.zEstimateLabel.setText(f"Z = {result.z} Z′ = {result.z_prime:.3g} {confidence_icon}")
2377
2461
  self.ui.zEstimateLabel2.setText(f"Z = {result.z} Z′ = {result.z_prime:.3g} {confidence_icon}")
@@ -2499,7 +2583,8 @@ class AppWindow(QMainWindow):
2499
2583
  Tries to determine the sources of missing data in the cif file, e.g. Tmin/Tmax from SADABS.
2500
2584
  """
2501
2585
  self.check_Z()
2502
- self.sources = BrukerData(self, self.cif).sources
2586
+ bruker_data = BrukerData(self, self.cif)
2587
+ self.sources = bruker_data.sources
2503
2588
  squeeze_dialog_shown = False
2504
2589
  if self.sources:
2505
2590
  # Add the CCDC number in case we have a deposition mail lying around:
@@ -2540,6 +2625,32 @@ class AppWindow(QMainWindow):
2540
2625
  # TypeError my originate from incomplete self.missing_data list!
2541
2626
  # print(e, '##', miss_key)
2542
2627
  pass
2628
+ self._apply_source_overrides(bruker_data.overrides)
2629
+ self._add_source_tooltips()
2630
+
2631
+ def _add_source_tooltips(self) -> None:
2632
+ """
2633
+ Shows the file a value of the data source column came from as a tooltip.
2634
+ """
2635
+ for key, source in self.sources.items():
2636
+ if not source or not source[1] or not self.ui.cif_main_table.has_key(key):
2637
+ continue
2638
+ widget = self.ui.cif_main_table.widget_from_key(key, Column.DATA)
2639
+ if widget:
2640
+ widget.setToolTip(f'Source: {source[1]}')
2641
+
2642
+ def _apply_source_overrides(self, overrides: set[str]) -> None:
2643
+ """
2644
+ Replaces values that are known to be wrong in the CIF (SHELXL writes for example
2645
+ wrong reflection numbers for twins) by the value of the corresponding data source.
2646
+ """
2647
+ for key in overrides:
2648
+ source = self.sources.get(key)
2649
+ if not source or not source[0] or key in self.missing_data:
2650
+ continue
2651
+ if not self.ui.cif_main_table.has_key(key):
2652
+ continue
2653
+ self.ui.cif_main_table.setText(key=key, column=Column.DATA, txt=str(source[0]), color=light_green)
2543
2654
 
2544
2655
  def add_ccdc_number(self):
2545
2656
  ccdc = CCDCMail(self.cif)
@@ -2550,7 +2661,7 @@ class AppWindow(QMainWindow):
2550
2661
  self.add_row('_database_code_depnum_ccdc_archive', '', at_start=True)
2551
2662
  txt = self.ui.cif_main_table.getTextFromKey('_database_code_depnum_ccdc_archive', Column.EDIT).strip()
2552
2663
  if not txt or (txt == '?'):
2553
- self.sources['_database_code_depnum_ccdc_archive'] = (str(ccdc.depnum), str(ccdc.emlfile.name))
2664
+ self.sources['_database_code_depnum_ccdc_archive'] = (str(ccdc.depnum), str(ccdc.emlfile))
2554
2665
  self.missing_data.add('_database_code_depnum_ccdc_archive')
2555
2666
 
2556
2667
  def refresh_combo_boxes(self) -> None:
@@ -215,7 +215,8 @@ class CheckCif(QThread):
215
215
  failed = Signal(str)
216
216
 
217
217
  def __init__(self, parent, cif: CifContainer, outfile: Path, hkl_upload: bool = True,
218
- pdf: bool = False, url: str = '', full_iucr: bool = False, check_duplicates: bool = True):
218
+ pdf: bool = False, url: str = '', full_iucr: bool = False, check_duplicates: bool = True,
219
+ explain_c: bool = False):
219
220
  # hkl == False means no hkl upload
220
221
  super().__init__(parent=parent)
221
222
  self.hkl_upload = hkl_upload
@@ -225,6 +226,7 @@ class CheckCif(QThread):
225
226
  self.checkcif_url = url
226
227
  self.full_iucr = full_iucr
227
228
  self.check_duplicates = check_duplicates
229
+ self.explain_c = explain_c
228
230
 
229
231
  def _html_check(self) -> None:
230
232
  if self.hkl_upload:
@@ -232,12 +234,12 @@ class CheckCif(QThread):
232
234
  else:
233
235
  self.progress.emit('Running Checkcif with no hkl data')
234
236
 
235
- def get_vrf(self):
237
+ def get_vrf(self) -> str:
238
+ """The 'valout' request value defining which alert levels get a response form."""
236
239
  if self.pdf:
237
240
  return 'vrfno'
238
- else:
239
- # Currently, the vrfabc option misses some validation response forms. Only vrfab gives correct results.
240
- return 'vrfabc'
241
+ # The IUCr server offers no response forms for level G alerts.
242
+ return 'vrfabc' if self.explain_c else 'vrfab'
241
243
 
242
244
  def run(self) -> None:
243
245
  """
@@ -0,0 +1,288 @@
1
+ from __future__ import annotations
2
+
3
+ import re
4
+ from collections import namedtuple
5
+ from collections.abc import Sequence
6
+ from pathlib import Path
7
+
8
+ import gemmi
9
+ import numpy as np
10
+ from gemmi.cif import Loop, Document, Style
11
+ from packaging import version
12
+
13
+ Limit = namedtuple('Limit', 'h_max, h_min, k_max, k_min, l_max, l_min')
14
+
15
+
16
+ class HKL:
17
+ """
18
+ loop_
19
+ _refln_index_h
20
+ _refln_index_k
21
+ _refln_index_l
22
+ _refln_F_squared_meas
23
+ _refln_F_squared_sigma
24
+ _refln_scale_group_code
25
+ """
26
+
27
+ def __init__(self, hkl_file: str, block_name: str, hklf_type: int = 4):
28
+ self._hkl_file = hkl_file
29
+ self.hklf_type = hklf_type
30
+ self._doc: Document = gemmi.cif.Document()
31
+ self._doc.add_new_block(block_name)
32
+ self.block = self._doc.sole_block()
33
+ self._add_hkl_as_loop()
34
+
35
+ @property
36
+ def hkl_as_cif(self) -> str:
37
+ if version.parse(gemmi.__version__) < version.parse('0.5.1'):
38
+ return self._doc.as_string(style=Style.Simple)
39
+ else:
40
+ return self._doc.as_string(options=gemmi.cif.WriteOptions(Style.Simple))
41
+
42
+ def _add_hkl_as_loop(self) -> None:
43
+ """
44
+ Adds the hkl data from a SHELX hkl file as local loop.
45
+ """
46
+ hkl_width = self._get_hkl_width()
47
+ loop_header = ['index_h',
48
+ 'index_k',
49
+ 'index_l',
50
+ 'F_squared_meas' if self.hklf_type != 3 else 'F_meas',
51
+ 'F_squared_sigma' if self.hklf_type != 3 else 'F_sigma',
52
+ 'scale_group_code']
53
+ loop: Loop = self.block.init_loop('_refln_', self._trim_header_to_hkl_width(loop_header))
54
+ zero_reflection_pattern = re.compile(r'^\s+0\s+0\s+0\s+0.*')
55
+ for line in self._hkl_file.splitlines(keepends=False):
56
+ splitline = line.split()
57
+ if not splitline:
58
+ continue
59
+ # Do not use data after the 0 0 0 reflection
60
+ if zero_reflection_pattern.match(line):
61
+ # Need to truncate, because some programs add the scale group even if
62
+ # there is no scale group in other reflections
63
+ loop.add_row(splitline[:hkl_width])
64
+ break
65
+ try:
66
+ loop.add_row(splitline[:len(loop_header)])
67
+ # RuntimeError ist from gemmi.cif.add_row:
68
+ except (IndexError, RuntimeError):
69
+ continue
70
+
71
+ def __repr__(self) -> str:
72
+ return self.hkl_as_cif[:250]
73
+
74
+ def _trim_header_to_hkl_width(self, loop_header: list[str]) -> list[str]:
75
+ hkl_with = self._get_hkl_width()
76
+ trimmed_header = loop_header[:hkl_with]
77
+ return trimmed_header
78
+
79
+ def _get_hkl_width(self) -> int:
80
+ first_lines = self._hkl_file[:150].strip().splitlines(keepends=False)
81
+ if len(first_lines) > 1:
82
+ return len(first_lines[1].split())
83
+ return len(first_lines[0].split())
84
+
85
+ def get_hkl_min_max(self) -> Limit:
86
+ hkl: gemmi.ReflnBlock = gemmi.hkl_cif_as_refln_block(self.block)
87
+ miller = hkl.make_miller_array()
88
+ h_max, k_max, l_max = np.max(miller, axis=0)
89
+ h_min, k_min, l_min = np.min(miller, axis=0)
90
+ return Limit(h_max=h_max, h_min=h_min, k_max=k_max, k_min=k_min, l_max=l_max, l_min=l_min)
91
+
92
+
93
+ def calculate_rint(hkl_file: str, space_group: str, *, cell: Sequence[float] | None = None,
94
+ resolution: tuple[float, float] | None = None, hklf: int = 4,
95
+ twst: int | None = None) -> float | None:
96
+ """
97
+ R(int) of an unmerged SHELX hkl file, calculated the way SHELXL and Olex2 do it:
98
+
99
+ R(int) = sum|Fo^2 - Fo^2(mean)| / sum[Fo^2]
100
+
101
+ Systematically absent reflections are rejected and Friedel opposites are only merged in
102
+ centrosymmetric space groups. Fo^2(mean) is the mean weighted with 1/sigma^2.
103
+
104
+ For HKLF 5 data only the singly indexed reflections of the reference domain are used,
105
+ because composite (overlapped) reflections have no symmetry equivalents to compare with.
106
+
107
+ SADABS writes no overall R(int) into its listing file, only the wR2(int) of the parameter
108
+ refinement and the R(int) values of the individual runs. The R(int) of a TWINABS listing
109
+ file describes a data treatment that a HKLF 5 refinement does not use.
110
+
111
+ Args:
112
+ hkl_file: Content of a SHELX hkl file.
113
+ space_group: Space group name, e.g. 'P n a 21'.
114
+ cell: Unit cell parameters, needed to apply a resolution limit.
115
+ resolution: Lowest and highest resolution in Angstrom (a SHELX SHEL instruction).
116
+ hklf: The SHELX HKLF file type. Data with negative batch numbers are always treated as
117
+ HKLF 5 data.
118
+ twst: The domain of a SHELX TWST instruction, only used for HKLF 5 data.
119
+ """
120
+ miller, intensities, sigmas, batches = _reflections_from_shelx_hkl(hkl_file)
121
+ if not len(miller) or not space_group:
122
+ return None
123
+ if hklf >= 5 or bool((batches < 0).any()):
124
+ # Only an HKLF 5 file has negative batch numbers:
125
+ singles = _singles_of_reference_domain(batches, twst)
126
+ miller, intensities, sigmas = miller[singles], intensities[singles], sigmas[singles]
127
+ if not len(miller):
128
+ return None
129
+ try:
130
+ operations = gemmi.SpaceGroup(space_group).operations()
131
+ except (RuntimeError, ValueError):
132
+ return None
133
+ used = ~_systematically_absent(miller, operations)
134
+ used &= _within_resolution(miller, cell, resolution)
135
+ miller, intensities, sigmas = miller[used], intensities[used], sigmas[used]
136
+ if not len(miller):
137
+ return None
138
+ groups, counts = _group_equivalents(miller, operations)
139
+ if len(counts) >= len(miller):
140
+ # Merged data have no equivalent reflections to compare:
141
+ return None
142
+ return _rint_of_groups(groups, counts, intensities, sigmas)
143
+
144
+
145
+ def reference_domain(hkl_file: str, twst: int | None = None) -> int | None:
146
+ """
147
+ The twin domain whose singly indexed reflections are used for the R(int) of HKLF 5 data.
148
+ """
149
+ *_, batches = _reflections_from_shelx_hkl(hkl_file)
150
+ if not len(batches):
151
+ return None
152
+ return _largest_domain(batches[_singles(batches)]) or None
153
+
154
+
155
+ def _singles_of_reference_domain(batches: np.ndarray, twst: int | None) -> np.ndarray:
156
+ """
157
+ The singly indexed reflections of the reference domain of an HKLF 5 file.
158
+
159
+ The reference domain is the domain of a SHELX TWST instruction or, like in Olex2, the
160
+ domain with the most singly indexed reflections.
161
+ """
162
+ singles = _singles(batches)
163
+ domain = twst if twst and twst > 0 else _largest_domain(batches[singles])
164
+ return singles & (batches == domain)
165
+
166
+
167
+ def _singles(batches: np.ndarray) -> np.ndarray:
168
+ """
169
+ In an HKLF 5 file all members of a composite reflection except the last one have a negative
170
+ batch number, thus a reflection is a single if its own batch number and that of its
171
+ predecessor are positive.
172
+ """
173
+ previous = np.concatenate(([1], batches[:-1]))
174
+ return (batches > 0) & (previous > 0)
175
+
176
+
177
+ def _largest_domain(domains: np.ndarray) -> int:
178
+ if not len(domains):
179
+ return 0
180
+ values, counts = np.unique(domains, return_counts=True)
181
+ return int(values[np.argmax(counts)])
182
+
183
+
184
+ def _within_resolution(miller: np.ndarray, cell: Sequence[float] | None,
185
+ resolution: tuple[float, float] | None) -> np.ndarray:
186
+ """
187
+ Applies the resolution limits of a SHELX SHEL instruction.
188
+ """
189
+ if not resolution or not cell:
190
+ return np.ones(len(miller), dtype=bool)
191
+ lowest, highest = max(resolution), min(resolution)
192
+ try:
193
+ spacing = gemmi.UnitCell(*cell[:6]).calculate_d_array(miller)
194
+ except (RuntimeError, TypeError, ValueError):
195
+ return np.ones(len(miller), dtype=bool)
196
+ return (spacing <= lowest) & (spacing >= highest)
197
+
198
+
199
+ def _rint_of_groups(groups: np.ndarray, counts: np.ndarray, intensities: np.ndarray,
200
+ sigmas: np.ndarray) -> float | None:
201
+ weights = np.where(sigmas > 0.0, 1.0 / np.square(np.where(sigmas > 0.0, sigmas, 1.0)), 1.0)
202
+ weight_sum = np.bincount(groups, weights=weights, minlength=len(counts))
203
+ mean = np.bincount(groups, weights=weights * intensities, minlength=len(counts)) / weight_sum
204
+ with_equivalents = counts[groups] > 1
205
+ numerator = np.abs(intensities - mean[groups])[with_equivalents].sum()
206
+ denominator = intensities[with_equivalents].sum()
207
+ if not denominator:
208
+ return None
209
+ return round(float(numerator / denominator), 4)
210
+
211
+
212
+ def _group_equivalents(miller: np.ndarray, operations: gemmi.GroupOps) -> tuple[np.ndarray, np.ndarray]:
213
+ """
214
+ Assigns a group number to every reflection that is symmetry equivalent to another one.
215
+ """
216
+ equivalents = [_encode(miller @ _rotation(op)) for op in operations.sym_ops]
217
+ if operations.is_centrosymmetric():
218
+ equivalents += [_encode(-(miller @ _rotation(op))) for op in operations.sym_ops]
219
+ representative = np.max(np.stack(equivalents), axis=0)
220
+ _, groups, counts = np.unique(representative, return_inverse=True, return_counts=True)
221
+ return groups, counts
222
+
223
+
224
+ def _systematically_absent(miller: np.ndarray, operations: gemmi.GroupOps) -> np.ndarray:
225
+ absent = np.zeros(len(miller), dtype=bool)
226
+ for op in operations.sym_ops:
227
+ translation = np.array(op.tran, dtype=np.float64) / op.DEN
228
+ if not translation.any():
229
+ continue
230
+ unchanged = (miller @ _rotation(op) == miller).all(axis=1)
231
+ phase = miller @ translation
232
+ absent |= unchanged & (np.abs(phase - np.round(phase)) > 1e-6)
233
+ for centering in operations.cen_ops:
234
+ translation = np.array(centering, dtype=np.float64) / gemmi.Op.DEN
235
+ if not translation.any():
236
+ continue
237
+ phase = miller @ translation
238
+ absent |= np.abs(phase - np.round(phase)) > 1e-6
239
+ return absent
240
+
241
+
242
+ def _rotation(op: gemmi.Op) -> np.ndarray:
243
+ """The rotation part of a symmetry operation, applicable as ``miller @ rotation``."""
244
+ return np.array(op.rot, dtype=np.int64) // op.DEN
245
+
246
+
247
+ def _encode(miller: np.ndarray) -> np.ndarray:
248
+ """Packs Miller indices into single numbers that keep their lexicographic order."""
249
+ shifted = miller.astype(np.int64) + 2 ** 20
250
+ return (shifted[:, 0] << 42) | (shifted[:, 1] << 21) | shifted[:, 2]
251
+
252
+
253
+ def _reflections_from_shelx_hkl(hkl_file: str) -> tuple[np.ndarray, np.ndarray, np.ndarray, np.ndarray]:
254
+ miller, intensities, sigmas, batches = [], [], [], []
255
+ for line in hkl_file.splitlines(keepends=False):
256
+ if len(line) < 28:
257
+ continue
258
+ try:
259
+ index = (int(line[0:4]), int(line[4:8]), int(line[8:12]))
260
+ if not any(index):
261
+ break
262
+ intensity, sigma = float(line[12:20]), float(line[20:28])
263
+ except ValueError:
264
+ continue
265
+ miller.append(index)
266
+ intensities.append(intensity)
267
+ sigmas.append(sigma)
268
+ batches.append(_batch_number(line))
269
+ return (np.array(miller, dtype=np.int32).reshape(-1, 3),
270
+ np.array(intensities, dtype=np.float64),
271
+ np.array(sigmas, dtype=np.float64),
272
+ np.array(batches, dtype=np.int32))
273
+
274
+
275
+ def _batch_number(line: str) -> int:
276
+ """The batch number of an HKLF 5 reflection, negative for all but the last member of a
277
+ composite reflection."""
278
+ try:
279
+ return int(line[28:32])
280
+ except ValueError:
281
+ return 1
282
+
283
+
284
+ if __name__ == '__main__':
285
+ h = HKL(Path('tests/examples/work/test_hkl_file.txt').read_text(), '123234')
286
+ # print(h.hkl_as_cif[:250])
287
+ m = h.get_hkl_min_max()
288
+ print(m)