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.
- {finalcif-172 → finalcif-175}/.gitignore +1 -0
- {finalcif-172 → finalcif-175}/.ruff.toml +3 -1
- {finalcif-172 → finalcif-175}/.run/compile_ui_files.run.xml +1 -1
- {finalcif-172 → finalcif-175}/PKG-INFO +6 -6
- finalcif-175/finalcif/__init__.py +3 -0
- {finalcif-172 → finalcif-175}/finalcif/appwindow.py +120 -9
- {finalcif-172 → finalcif-175}/finalcif/cif/checkcif/checkcif.py +7 -5
- finalcif-175/finalcif/cif/hkl.py +288 -0
- {finalcif-172 → finalcif-175}/finalcif/datafiles/bruker_data.py +177 -44
- {finalcif-172 → finalcif-175}/finalcif/datafiles/data.py +3 -0
- finalcif-175/finalcif/datafiles/sadabs.py +545 -0
- {finalcif-172 → finalcif-175}/finalcif/datafiles/saint.py +52 -53
- {finalcif-172 → finalcif-175}/finalcif/gui/dialogs.py +1 -1
- {finalcif-172 → finalcif-175}/finalcif/gui/finalcif_gui_ui.py +63 -54
- {finalcif-172 → finalcif-175}/finalcif/gui/finalcif_gui_ui.ui +117 -105
- {finalcif-172 → finalcif-175}/finalcif/gui/validators.py +5 -1
- {finalcif-172 → finalcif-175}/finalcif/template/report_fastmolwidget.tmpl +11 -1
- {finalcif-172 → finalcif-175}/finalcif/tools/selfupdate.py +129 -16
- {finalcif-172 → finalcif-175}/finalcif/tools/squeeze.py +76 -1
- {finalcif-172 → finalcif-175}/finalcif/tools/sumformula.py +27 -3
- {finalcif-172 → finalcif-175}/finalcif/tools/z_from_packing.py +701 -281
- {finalcif-172 → finalcif-175}/pyproject.toml +4 -4
- {finalcif-172 → finalcif-175}/uv.lock +215 -135
- finalcif-172/finalcif/__init__.py +0 -3
- finalcif-172/finalcif/cif/hkl.py +0 -94
- finalcif-172/finalcif/datafiles/sadabs.py +0 -191
- {finalcif-172 → finalcif-175}/.gitattributes +0 -0
- {finalcif-172 → finalcif-175}/.githooks/pre-push +0 -0
- {finalcif-172 → finalcif-175}/.python-version +0 -0
- {finalcif-172 → finalcif-175}/.run/Make Documentation MAC.run.xml +0 -0
- {finalcif-172 → finalcif-175}/.run/make documentation Windows.run.xml +0 -0
- {finalcif-172 → finalcif-175}/AGENTS.md +0 -0
- {finalcif-172 → finalcif-175}/CODE_OF_CONDUCT.md +0 -0
- {finalcif-172 → finalcif-175}/CONTRIBUTING.md +0 -0
- {finalcif-172 → finalcif-175}/Finalcif_installer_win.spec +0 -0
- {finalcif-172 → finalcif-175}/Finalcif_linux_onefile.spec +0 -0
- {finalcif-172 → finalcif-175}/Finalcif_mac.spec +0 -0
- {finalcif-172 → finalcif-175}/LICENSE +0 -0
- {finalcif-172 → finalcif-175}/README.md +0 -0
- {finalcif-172 → finalcif-175}/finalcif/app_path.py +0 -0
- {finalcif-172 → finalcif-175}/finalcif/cif/__init__.py +0 -0
- {finalcif-172 → finalcif-175}/finalcif/cif/all_cif_dicts.py +0 -0
- {finalcif-172 → finalcif-175}/finalcif/cif/atoms.py +0 -0
- {finalcif-172 → finalcif-175}/finalcif/cif/checkcif/__init__.py +0 -0
- {finalcif-172 → finalcif-175}/finalcif/cif/cif_dict_foo.py +0 -0
- {finalcif-172 → finalcif-175}/finalcif/cif/cif_file_io.py +0 -0
- {finalcif-172 → finalcif-175}/finalcif/cif/cif_keyword_mapping.py +0 -0
- {finalcif-172 → finalcif-175}/finalcif/cif/cif_order.py +0 -0
- {finalcif-172 → finalcif-175}/finalcif/cif/cod/__init__.py +0 -0
- {finalcif-172 → finalcif-175}/finalcif/cif/cod/deposit.py +0 -0
- {finalcif-172 → finalcif-175}/finalcif/cif/cod/deposit_check.py +0 -0
- {finalcif-172 → finalcif-175}/finalcif/cif/cod/deposition_list.py +0 -0
- {finalcif-172 → finalcif-175}/finalcif/cif/cod/doi.py +0 -0
- {finalcif-172 → finalcif-175}/finalcif/cif/cod/upload.py +0 -0
- {finalcif-172 → finalcif-175}/finalcif/cif/cod/website_parser.py +0 -0
- {finalcif-172 → finalcif-175}/finalcif/cif/core_dict.py +0 -0
- {finalcif-172 → finalcif-175}/finalcif/cif/modulation_dict.py +0 -0
- {finalcif-172 → finalcif-175}/finalcif/cif/powder_dict.py +0 -0
- {finalcif-172 → finalcif-175}/finalcif/cif/reference.py +0 -0
- {finalcif-172 → finalcif-175}/finalcif/cif/restraints_dict.py +0 -0
- {finalcif-172 → finalcif-175}/finalcif/cif/text.py +0 -0
- {finalcif-172 → finalcif-175}/finalcif/cif/twin_dict.py +0 -0
- {finalcif-172 → finalcif-175}/finalcif/cif/vrf_entry.py +0 -0
- {finalcif-172 → finalcif-175}/finalcif/ciforder/__init__.py +0 -0
- {finalcif-172 → finalcif-175}/finalcif/ciforder/order.py +0 -0
- {finalcif-172 → finalcif-175}/finalcif/ciforder/order_ui.py +0 -0
- {finalcif-172 → finalcif-175}/finalcif/ciforder/order_ui.ui +0 -0
- {finalcif-172 → finalcif-175}/finalcif/datafiles/__init__.py +0 -0
- {finalcif-172 → finalcif-175}/finalcif/datafiles/bruker_frame.py +0 -0
- {finalcif-172 → finalcif-175}/finalcif/datafiles/ccdc_mail.py +0 -0
- {finalcif-172 → finalcif-175}/finalcif/datafiles/p4p_reader.py +0 -0
- {finalcif-172 → finalcif-175}/finalcif/datafiles/shelx_lst.py +0 -0
- {finalcif-172 → finalcif-175}/finalcif/datafiles/utils.py +0 -0
- {finalcif-172 → finalcif-175}/finalcif/equip_property/__init__.py +0 -0
- {finalcif-172 → finalcif-175}/finalcif/equip_property/author_loop_templates.py +0 -0
- {finalcif-172 → finalcif-175}/finalcif/equip_property/equipment.py +0 -0
- {finalcif-172 → finalcif-175}/finalcif/equip_property/properties.py +0 -0
- {finalcif-172 → finalcif-175}/finalcif/equip_property/tools.py +0 -0
- {finalcif-172 → finalcif-175}/finalcif/finalcif_start.py +0 -0
- {finalcif-172 → finalcif-175}/finalcif/gui/__init__.py +0 -0
- {finalcif-172 → finalcif-175}/finalcif/gui/block_combobox.py +0 -0
- {finalcif-172 → finalcif-175}/finalcif/gui/checkcif_browser.py +0 -0
- {finalcif-172 → finalcif-175}/finalcif/gui/cif_table_model.py +0 -0
- {finalcif-172 → finalcif-175}/finalcif/gui/cif_table_view.py +0 -0
- {finalcif-172 → finalcif-175}/finalcif/gui/combobox.py +0 -0
- {finalcif-172 → finalcif-175}/finalcif/gui/custom_classes.py +0 -0
- {finalcif-172 → finalcif-175}/finalcif/gui/edit_button.py +0 -0
- {finalcif-172 → finalcif-175}/finalcif/gui/equipmenttable.py +0 -0
- {finalcif-172 → finalcif-175}/finalcif/gui/file_editor.py +0 -0
- {finalcif-172 → finalcif-175}/finalcif/gui/import_selector.py +0 -0
- {finalcif-172 → finalcif-175}/finalcif/gui/import_selector_ui.py +0 -0
- {finalcif-172 → finalcif-175}/finalcif/gui/import_selector_ui.ui +0 -0
- {finalcif-172 → finalcif-175}/finalcif/gui/loop_creator.py +0 -0
- {finalcif-172 → finalcif-175}/finalcif/gui/loop_creator_ui.py +0 -0
- {finalcif-172 → finalcif-175}/finalcif/gui/loop_creator_ui.ui +0 -0
- {finalcif-172 → finalcif-175}/finalcif/gui/loops.py +0 -0
- {finalcif-172 → finalcif-175}/finalcif/gui/mainstackwidget.py +0 -0
- {finalcif-172 → finalcif-175}/finalcif/gui/mixins.py +0 -0
- {finalcif-172 → finalcif-175}/finalcif/gui/new_key_dialog.py +0 -0
- {finalcif-172 → finalcif-175}/finalcif/gui/new_key_dialog_ui.py +0 -0
- {finalcif-172 → finalcif-175}/finalcif/gui/new_key_dialog_ui.ui +0 -0
- {finalcif-172 → finalcif-175}/finalcif/gui/plaintextedit.py +0 -0
- {finalcif-172 → finalcif-175}/finalcif/gui/playground.py +0 -0
- {finalcif-172 → finalcif-175}/finalcif/gui/propertytable.py +0 -0
- {finalcif-172 → finalcif-175}/finalcif/gui/shelx_navigation.py +0 -0
- {finalcif-172 → finalcif-175}/finalcif/gui/single_line_textedit.py +0 -0
- {finalcif-172 → finalcif-175}/finalcif/gui/spell_check_edit.py +0 -0
- {finalcif-172 → finalcif-175}/finalcif/gui/squeeze_dialog.py +0 -0
- {finalcif-172 → finalcif-175}/finalcif/gui/syntax_highlighter.py +0 -0
- {finalcif-172 → finalcif-175}/finalcif/gui/text_templates_ui.py +0 -0
- {finalcif-172 → finalcif-175}/finalcif/gui/text_templates_ui.ui +0 -0
- {finalcif-172 → finalcif-175}/finalcif/gui/text_value_editor.py +0 -0
- {finalcif-172 → finalcif-175}/finalcif/gui/timers.py +0 -0
- {finalcif-172 → finalcif-175}/finalcif/gui/vrf_classes.py +0 -0
- {finalcif-172 → finalcif-175}/finalcif/gui/vzs_viewer.py +0 -0
- {finalcif-172 → finalcif-175}/finalcif/icon/__init__.py +0 -0
- {finalcif-172 → finalcif-175}/finalcif/icon/finalcif.png +0 -0
- {finalcif-172 → finalcif-175}/finalcif/icon/finalcif2.ico +0 -0
- {finalcif-172 → finalcif-175}/finalcif/icon/finalcif2.png +0 -0
- {finalcif-172 → finalcif-175}/finalcif/icon/multitable.ico +0 -0
- {finalcif-172 → finalcif-175}/finalcif/icon/multitable.png +0 -0
- {finalcif-172 → finalcif-175}/finalcif/icon/multitable.xcf +0 -0
- {finalcif-172 → finalcif-175}/finalcif/report/__init__.py +0 -0
- {finalcif-172 → finalcif-175}/finalcif/report/archive_report.py +0 -0
- {finalcif-172 → finalcif-175}/finalcif/report/gui/__init__.py +0 -0
- {finalcif-172 → finalcif-175}/finalcif/report/gui/mainwindow.py +0 -0
- {finalcif-172 → finalcif-175}/finalcif/report/gui/mainwindow.ui +0 -0
- {finalcif-172 → finalcif-175}/finalcif/report/mtools.py +0 -0
- {finalcif-172 → finalcif-175}/finalcif/report/references.py +0 -0
- {finalcif-172 → finalcif-175}/finalcif/report/report_text.py +0 -0
- {finalcif-172 → finalcif-175}/finalcif/report/symm.py +0 -0
- {finalcif-172 → finalcif-175}/finalcif/report/tables.py +0 -0
- {finalcif-172 → finalcif-175}/finalcif/report/templated_report.py +0 -0
- {finalcif-172 → finalcif-175}/finalcif/template/__init__.py +0 -0
- {finalcif-172 → finalcif-175}/finalcif/template/bootstrap/bootstrap.min.css +0 -0
- {finalcif-172 → finalcif-175}/finalcif/template/machines/D8_VENTURE.cif +0 -0
- {finalcif-172 → finalcif-175}/finalcif/template/mathml2omml.xsl +0 -0
- {finalcif-172 → finalcif-175}/finalcif/template/miew/LICENSES +0 -0
- {finalcif-172 → finalcif-175}/finalcif/template/miew/Miew.css +0 -0
- {finalcif-172 → finalcif-175}/finalcif/template/miew/Miew.min.js +0 -0
- {finalcif-172 → finalcif-175}/finalcif/template/miew/lodash.min.js +0 -0
- {finalcif-172 → finalcif-175}/finalcif/template/miew/three.js +0 -0
- {finalcif-172 → finalcif-175}/finalcif/template/report.tex +0 -0
- {finalcif-172 → finalcif-175}/finalcif/template/report.tmpl +0 -0
- {finalcif-172 → finalcif-175}/finalcif/template/report2.tex +0 -0
- {finalcif-172 → finalcif-175}/finalcif/template/report_default.docx +0 -0
- {finalcif-172 → finalcif-175}/finalcif/template/template1.docx +0 -0
- {finalcif-172 → finalcif-175}/finalcif/template/template_for_multitable.docx +0 -0
- {finalcif-172 → finalcif-175}/finalcif/template/template_text.docx +0 -0
- {finalcif-172 → finalcif-175}/finalcif/template/template_without_text.docx +0 -0
- {finalcif-172 → finalcif-175}/finalcif/template/templates.py +0 -0
- {finalcif-172 → finalcif-175}/finalcif/template/unicode2latex/FakePlasTeX/FakeContext.py +0 -0
- {finalcif-172 → finalcif-175}/finalcif/template/unicode2latex/FakePlasTeX/FakeTokenizer.py +0 -0
- {finalcif-172 → finalcif-175}/finalcif/template/unicode2latex/FakePlasTeX/README.md +0 -0
- {finalcif-172 → finalcif-175}/finalcif/template/unicode2latex/FakePlasTeX/TokenizerPassThru.py +0 -0
- {finalcif-172 → finalcif-175}/finalcif/template/unicode2latex/LICENSE +0 -0
- {finalcif-172 → finalcif-175}/finalcif/template/unicode2latex/__init__.py +0 -0
- {finalcif-172 → finalcif-175}/finalcif/template/unicode2latex/u2l.py +0 -0
- {finalcif-172 → finalcif-175}/finalcif/template/unicode2latex/unicode-math-table.tex +0 -0
- {finalcif-172 → finalcif-175}/finalcif/template/unicode2latex/unicode-math-xetex.sty +0 -0
- {finalcif-172 → finalcif-175}/finalcif/template/xsl/convert.py +0 -0
- {finalcif-172 → finalcif-175}/finalcif/template/xsl/xmltohtml.xsl +0 -0
- {finalcif-172 → finalcif-175}/finalcif/template/xsl/xmltolatex.xsl +0 -0
- {finalcif-172 → finalcif-175}/finalcif/tools/__init__.py +0 -0
- {finalcif-172 → finalcif-175}/finalcif/tools/chemparse.py +0 -0
- {finalcif-172 → finalcif-175}/finalcif/tools/download.py +0 -0
- {finalcif-172 → finalcif-175}/finalcif/tools/dsrmath.py +0 -0
- {finalcif-172 → finalcif-175}/finalcif/tools/formal_charge.py +0 -0
- {finalcif-172 → finalcif-175}/finalcif/tools/misc.py +0 -0
- {finalcif-172 → finalcif-175}/finalcif/tools/mol_file_writer.py +0 -0
- {finalcif-172 → finalcif-175}/finalcif/tools/options.py +0 -0
- {finalcif-172 → finalcif-175}/finalcif/tools/platon.py +0 -0
- {finalcif-172 → finalcif-175}/finalcif/tools/pupdate.py +0 -0
- {finalcif-172 → finalcif-175}/finalcif/tools/settings.py +0 -0
- {finalcif-172 → finalcif-175}/finalcif/tools/shred.py +0 -0
- {finalcif-172 → finalcif-175}/finalcif/tools/space_groups.py +0 -0
- {finalcif-172 → finalcif-175}/finalcif/tools/spgr_format.py +0 -0
- {finalcif-172 → finalcif-175}/finalcif/tools/statusbar.py +0 -0
- {finalcif-172 → finalcif-175}/finalcif.exe +0 -0
- {finalcif-172 → finalcif-175}/platon/README.txt +0 -0
- {finalcif-172 → finalcif-175}/platon/compile_platon.bat +0 -0
- {finalcif-172 → finalcif-175}/platon/platon_special.exe +0 -0
- {finalcif-172 → finalcif-175}/platon/platon_special.f +0 -0
- {finalcif-172 → finalcif-175}/pylock.toml +0 -0
- {finalcif-172 → finalcif-175}/run_and_capture.py +0 -0
- {finalcif-172 → finalcif-175}/run_finalcif +0 -0
- {finalcif-172 → finalcif-175}/run_finalcif.bat +0 -0
- {finalcif-172 → finalcif-175}/screenshots/finalcif_checkcif.png +0 -0
- {finalcif-172 → finalcif-175}/screenshots/finalcif_details.png +0 -0
- {finalcif-172 → finalcif-175}/screenshots/finalcif_loops.png +0 -0
- {finalcif-172 → finalcif-175}/screenshots/finalcif_loops2.png +0 -0
- {finalcif-172 → finalcif-175}/screenshots/finalcif_main.png +0 -0
- {finalcif-172 → finalcif-175}/screenshots/finalcif_report.png +0 -0
- {finalcif-172 → finalcif-175}/screenshots/finalcif_responses.png +0 -0
- {finalcif-172 → finalcif-175}/screenshots/loops_column_buttons.png +0 -0
- {finalcif-172 → finalcif-175}/work/compare_z_platon.py +0 -0
- {finalcif-172 → finalcif-175}/work/diagnose_1519506.py +0 -0
- {finalcif-172 → finalcif-175}/work/diagnose_z.py +0 -0
- {finalcif-172 → finalcif-175}/work/investigate_failures.py +0 -0
- {finalcif-172 → finalcif-175}/work/validate_cod.py +0 -0
- {finalcif-172 → finalcif-175}/work/validate_zprime.py +0 -0
|
@@ -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",
|
|
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="
|
|
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.
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
2
|
Name: finalcif
|
|
3
|
-
Version:
|
|
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
|
|
18
|
+
Requires-Dist: docxcompose>=2.2.0
|
|
19
19
|
Requires-Dist: docxtpl[subdoc]>=0.20.2
|
|
20
|
-
Requires-Dist: fastmolwidget>=1.
|
|
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.
|
|
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
|
|
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
|
|
@@ -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
|
|
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
|
-
|
|
2089
|
-
|
|
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(
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
239
|
-
|
|
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)
|