finalcif 175__tar.gz → 177__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-175 → finalcif-177}/PKG-INFO +1 -1
  2. finalcif-177/finalcif/__init__.py +3 -0
  3. {finalcif-175 → finalcif-177}/finalcif/appwindow.py +9 -9
  4. {finalcif-175 → finalcif-177}/finalcif/cif/hkl.py +1 -4
  5. {finalcif-175 → finalcif-177}/finalcif/gui/finalcif_gui_ui.py +1 -1
  6. {finalcif-175 → finalcif-177}/finalcif/gui/finalcif_gui_ui.ui +1 -1
  7. {finalcif-175 → finalcif-177}/finalcif/gui/squeeze_dialog.py +53 -9
  8. {finalcif-175 → finalcif-177}/finalcif/gui/validators.py +10 -1
  9. {finalcif-175 → finalcif-177}/finalcif/tools/shred.py +1 -1
  10. finalcif-177/finalcif/tools/solvents.py +183 -0
  11. {finalcif-175 → finalcif-177}/finalcif/tools/squeeze.py +41 -3
  12. finalcif-177/uv.lock +1096 -0
  13. finalcif-175/finalcif/__init__.py +0 -3
  14. finalcif-175/uv.lock +0 -1056
  15. {finalcif-175 → finalcif-177}/.gitattributes +0 -0
  16. {finalcif-175 → finalcif-177}/.githooks/pre-push +0 -0
  17. {finalcif-175 → finalcif-177}/.gitignore +0 -0
  18. {finalcif-175 → finalcif-177}/.python-version +0 -0
  19. {finalcif-175 → finalcif-177}/.ruff.toml +0 -0
  20. {finalcif-175 → finalcif-177}/.run/Make Documentation MAC.run.xml +0 -0
  21. {finalcif-175 → finalcif-177}/.run/compile_ui_files.run.xml +0 -0
  22. {finalcif-175 → finalcif-177}/.run/make documentation Windows.run.xml +0 -0
  23. {finalcif-175 → finalcif-177}/AGENTS.md +0 -0
  24. {finalcif-175 → finalcif-177}/CODE_OF_CONDUCT.md +0 -0
  25. {finalcif-175 → finalcif-177}/CONTRIBUTING.md +0 -0
  26. {finalcif-175 → finalcif-177}/Finalcif_installer_win.spec +0 -0
  27. {finalcif-175 → finalcif-177}/Finalcif_linux_onefile.spec +0 -0
  28. {finalcif-175 → finalcif-177}/Finalcif_mac.spec +0 -0
  29. {finalcif-175 → finalcif-177}/LICENSE +0 -0
  30. {finalcif-175 → finalcif-177}/README.md +0 -0
  31. {finalcif-175 → finalcif-177}/finalcif/app_path.py +0 -0
  32. {finalcif-175 → finalcif-177}/finalcif/cif/__init__.py +0 -0
  33. {finalcif-175 → finalcif-177}/finalcif/cif/all_cif_dicts.py +0 -0
  34. {finalcif-175 → finalcif-177}/finalcif/cif/atoms.py +0 -0
  35. {finalcif-175 → finalcif-177}/finalcif/cif/checkcif/__init__.py +0 -0
  36. {finalcif-175 → finalcif-177}/finalcif/cif/checkcif/checkcif.py +0 -0
  37. {finalcif-175 → finalcif-177}/finalcif/cif/cif_dict_foo.py +0 -0
  38. {finalcif-175 → finalcif-177}/finalcif/cif/cif_file_io.py +0 -0
  39. {finalcif-175 → finalcif-177}/finalcif/cif/cif_keyword_mapping.py +0 -0
  40. {finalcif-175 → finalcif-177}/finalcif/cif/cif_order.py +0 -0
  41. {finalcif-175 → finalcif-177}/finalcif/cif/cod/__init__.py +0 -0
  42. {finalcif-175 → finalcif-177}/finalcif/cif/cod/deposit.py +0 -0
  43. {finalcif-175 → finalcif-177}/finalcif/cif/cod/deposit_check.py +0 -0
  44. {finalcif-175 → finalcif-177}/finalcif/cif/cod/deposition_list.py +0 -0
  45. {finalcif-175 → finalcif-177}/finalcif/cif/cod/doi.py +0 -0
  46. {finalcif-175 → finalcif-177}/finalcif/cif/cod/upload.py +0 -0
  47. {finalcif-175 → finalcif-177}/finalcif/cif/cod/website_parser.py +0 -0
  48. {finalcif-175 → finalcif-177}/finalcif/cif/core_dict.py +0 -0
  49. {finalcif-175 → finalcif-177}/finalcif/cif/modulation_dict.py +0 -0
  50. {finalcif-175 → finalcif-177}/finalcif/cif/powder_dict.py +0 -0
  51. {finalcif-175 → finalcif-177}/finalcif/cif/reference.py +0 -0
  52. {finalcif-175 → finalcif-177}/finalcif/cif/restraints_dict.py +0 -0
  53. {finalcif-175 → finalcif-177}/finalcif/cif/text.py +0 -0
  54. {finalcif-175 → finalcif-177}/finalcif/cif/twin_dict.py +0 -0
  55. {finalcif-175 → finalcif-177}/finalcif/cif/vrf_entry.py +0 -0
  56. {finalcif-175 → finalcif-177}/finalcif/ciforder/__init__.py +0 -0
  57. {finalcif-175 → finalcif-177}/finalcif/ciforder/order.py +0 -0
  58. {finalcif-175 → finalcif-177}/finalcif/ciforder/order_ui.py +0 -0
  59. {finalcif-175 → finalcif-177}/finalcif/ciforder/order_ui.ui +0 -0
  60. {finalcif-175 → finalcif-177}/finalcif/datafiles/__init__.py +0 -0
  61. {finalcif-175 → finalcif-177}/finalcif/datafiles/bruker_data.py +0 -0
  62. {finalcif-175 → finalcif-177}/finalcif/datafiles/bruker_frame.py +0 -0
  63. {finalcif-175 → finalcif-177}/finalcif/datafiles/ccdc_mail.py +0 -0
  64. {finalcif-175 → finalcif-177}/finalcif/datafiles/data.py +0 -0
  65. {finalcif-175 → finalcif-177}/finalcif/datafiles/p4p_reader.py +0 -0
  66. {finalcif-175 → finalcif-177}/finalcif/datafiles/sadabs.py +0 -0
  67. {finalcif-175 → finalcif-177}/finalcif/datafiles/saint.py +0 -0
  68. {finalcif-175 → finalcif-177}/finalcif/datafiles/shelx_lst.py +0 -0
  69. {finalcif-175 → finalcif-177}/finalcif/datafiles/utils.py +0 -0
  70. {finalcif-175 → finalcif-177}/finalcif/equip_property/__init__.py +0 -0
  71. {finalcif-175 → finalcif-177}/finalcif/equip_property/author_loop_templates.py +0 -0
  72. {finalcif-175 → finalcif-177}/finalcif/equip_property/equipment.py +0 -0
  73. {finalcif-175 → finalcif-177}/finalcif/equip_property/properties.py +0 -0
  74. {finalcif-175 → finalcif-177}/finalcif/equip_property/tools.py +0 -0
  75. {finalcif-175 → finalcif-177}/finalcif/finalcif_start.py +0 -0
  76. {finalcif-175 → finalcif-177}/finalcif/gui/__init__.py +0 -0
  77. {finalcif-175 → finalcif-177}/finalcif/gui/block_combobox.py +0 -0
  78. {finalcif-175 → finalcif-177}/finalcif/gui/checkcif_browser.py +0 -0
  79. {finalcif-175 → finalcif-177}/finalcif/gui/cif_table_model.py +0 -0
  80. {finalcif-175 → finalcif-177}/finalcif/gui/cif_table_view.py +0 -0
  81. {finalcif-175 → finalcif-177}/finalcif/gui/combobox.py +0 -0
  82. {finalcif-175 → finalcif-177}/finalcif/gui/custom_classes.py +0 -0
  83. {finalcif-175 → finalcif-177}/finalcif/gui/dialogs.py +0 -0
  84. {finalcif-175 → finalcif-177}/finalcif/gui/edit_button.py +0 -0
  85. {finalcif-175 → finalcif-177}/finalcif/gui/equipmenttable.py +0 -0
  86. {finalcif-175 → finalcif-177}/finalcif/gui/file_editor.py +0 -0
  87. {finalcif-175 → finalcif-177}/finalcif/gui/import_selector.py +0 -0
  88. {finalcif-175 → finalcif-177}/finalcif/gui/import_selector_ui.py +0 -0
  89. {finalcif-175 → finalcif-177}/finalcif/gui/import_selector_ui.ui +0 -0
  90. {finalcif-175 → finalcif-177}/finalcif/gui/loop_creator.py +0 -0
  91. {finalcif-175 → finalcif-177}/finalcif/gui/loop_creator_ui.py +0 -0
  92. {finalcif-175 → finalcif-177}/finalcif/gui/loop_creator_ui.ui +0 -0
  93. {finalcif-175 → finalcif-177}/finalcif/gui/loops.py +0 -0
  94. {finalcif-175 → finalcif-177}/finalcif/gui/mainstackwidget.py +0 -0
  95. {finalcif-175 → finalcif-177}/finalcif/gui/mixins.py +0 -0
  96. {finalcif-175 → finalcif-177}/finalcif/gui/new_key_dialog.py +0 -0
  97. {finalcif-175 → finalcif-177}/finalcif/gui/new_key_dialog_ui.py +0 -0
  98. {finalcif-175 → finalcif-177}/finalcif/gui/new_key_dialog_ui.ui +0 -0
  99. {finalcif-175 → finalcif-177}/finalcif/gui/plaintextedit.py +0 -0
  100. {finalcif-175 → finalcif-177}/finalcif/gui/playground.py +0 -0
  101. {finalcif-175 → finalcif-177}/finalcif/gui/propertytable.py +0 -0
  102. {finalcif-175 → finalcif-177}/finalcif/gui/shelx_navigation.py +0 -0
  103. {finalcif-175 → finalcif-177}/finalcif/gui/single_line_textedit.py +0 -0
  104. {finalcif-175 → finalcif-177}/finalcif/gui/spell_check_edit.py +0 -0
  105. {finalcif-175 → finalcif-177}/finalcif/gui/syntax_highlighter.py +0 -0
  106. {finalcif-175 → finalcif-177}/finalcif/gui/text_templates_ui.py +0 -0
  107. {finalcif-175 → finalcif-177}/finalcif/gui/text_templates_ui.ui +0 -0
  108. {finalcif-175 → finalcif-177}/finalcif/gui/text_value_editor.py +0 -0
  109. {finalcif-175 → finalcif-177}/finalcif/gui/timers.py +0 -0
  110. {finalcif-175 → finalcif-177}/finalcif/gui/vrf_classes.py +0 -0
  111. {finalcif-175 → finalcif-177}/finalcif/gui/vzs_viewer.py +0 -0
  112. {finalcif-175 → finalcif-177}/finalcif/icon/__init__.py +0 -0
  113. {finalcif-175 → finalcif-177}/finalcif/icon/finalcif.png +0 -0
  114. {finalcif-175 → finalcif-177}/finalcif/icon/finalcif2.ico +0 -0
  115. {finalcif-175 → finalcif-177}/finalcif/icon/finalcif2.png +0 -0
  116. {finalcif-175 → finalcif-177}/finalcif/icon/multitable.ico +0 -0
  117. {finalcif-175 → finalcif-177}/finalcif/icon/multitable.png +0 -0
  118. {finalcif-175 → finalcif-177}/finalcif/icon/multitable.xcf +0 -0
  119. {finalcif-175 → finalcif-177}/finalcif/report/__init__.py +0 -0
  120. {finalcif-175 → finalcif-177}/finalcif/report/archive_report.py +0 -0
  121. {finalcif-175 → finalcif-177}/finalcif/report/gui/__init__.py +0 -0
  122. {finalcif-175 → finalcif-177}/finalcif/report/gui/mainwindow.py +0 -0
  123. {finalcif-175 → finalcif-177}/finalcif/report/gui/mainwindow.ui +0 -0
  124. {finalcif-175 → finalcif-177}/finalcif/report/mtools.py +0 -0
  125. {finalcif-175 → finalcif-177}/finalcif/report/references.py +0 -0
  126. {finalcif-175 → finalcif-177}/finalcif/report/report_text.py +0 -0
  127. {finalcif-175 → finalcif-177}/finalcif/report/symm.py +0 -0
  128. {finalcif-175 → finalcif-177}/finalcif/report/tables.py +0 -0
  129. {finalcif-175 → finalcif-177}/finalcif/report/templated_report.py +0 -0
  130. {finalcif-175 → finalcif-177}/finalcif/template/__init__.py +0 -0
  131. {finalcif-175 → finalcif-177}/finalcif/template/bootstrap/bootstrap.min.css +0 -0
  132. {finalcif-175 → finalcif-177}/finalcif/template/machines/D8_VENTURE.cif +0 -0
  133. {finalcif-175 → finalcif-177}/finalcif/template/mathml2omml.xsl +0 -0
  134. {finalcif-175 → finalcif-177}/finalcif/template/miew/LICENSES +0 -0
  135. {finalcif-175 → finalcif-177}/finalcif/template/miew/Miew.css +0 -0
  136. {finalcif-175 → finalcif-177}/finalcif/template/miew/Miew.min.js +0 -0
  137. {finalcif-175 → finalcif-177}/finalcif/template/miew/lodash.min.js +0 -0
  138. {finalcif-175 → finalcif-177}/finalcif/template/miew/three.js +0 -0
  139. {finalcif-175 → finalcif-177}/finalcif/template/report.tex +0 -0
  140. {finalcif-175 → finalcif-177}/finalcif/template/report.tmpl +0 -0
  141. {finalcif-175 → finalcif-177}/finalcif/template/report2.tex +0 -0
  142. {finalcif-175 → finalcif-177}/finalcif/template/report_default.docx +0 -0
  143. {finalcif-175 → finalcif-177}/finalcif/template/report_fastmolwidget.tmpl +0 -0
  144. {finalcif-175 → finalcif-177}/finalcif/template/template1.docx +0 -0
  145. {finalcif-175 → finalcif-177}/finalcif/template/template_for_multitable.docx +0 -0
  146. {finalcif-175 → finalcif-177}/finalcif/template/template_text.docx +0 -0
  147. {finalcif-175 → finalcif-177}/finalcif/template/template_without_text.docx +0 -0
  148. {finalcif-175 → finalcif-177}/finalcif/template/templates.py +0 -0
  149. {finalcif-175 → finalcif-177}/finalcif/template/unicode2latex/FakePlasTeX/FakeContext.py +0 -0
  150. {finalcif-175 → finalcif-177}/finalcif/template/unicode2latex/FakePlasTeX/FakeTokenizer.py +0 -0
  151. {finalcif-175 → finalcif-177}/finalcif/template/unicode2latex/FakePlasTeX/README.md +0 -0
  152. {finalcif-175 → finalcif-177}/finalcif/template/unicode2latex/FakePlasTeX/TokenizerPassThru.py +0 -0
  153. {finalcif-175 → finalcif-177}/finalcif/template/unicode2latex/LICENSE +0 -0
  154. {finalcif-175 → finalcif-177}/finalcif/template/unicode2latex/__init__.py +0 -0
  155. {finalcif-175 → finalcif-177}/finalcif/template/unicode2latex/u2l.py +0 -0
  156. {finalcif-175 → finalcif-177}/finalcif/template/unicode2latex/unicode-math-table.tex +0 -0
  157. {finalcif-175 → finalcif-177}/finalcif/template/unicode2latex/unicode-math-xetex.sty +0 -0
  158. {finalcif-175 → finalcif-177}/finalcif/template/xsl/convert.py +0 -0
  159. {finalcif-175 → finalcif-177}/finalcif/template/xsl/xmltohtml.xsl +0 -0
  160. {finalcif-175 → finalcif-177}/finalcif/template/xsl/xmltolatex.xsl +0 -0
  161. {finalcif-175 → finalcif-177}/finalcif/tools/__init__.py +0 -0
  162. {finalcif-175 → finalcif-177}/finalcif/tools/chemparse.py +0 -0
  163. {finalcif-175 → finalcif-177}/finalcif/tools/download.py +0 -0
  164. {finalcif-175 → finalcif-177}/finalcif/tools/dsrmath.py +0 -0
  165. {finalcif-175 → finalcif-177}/finalcif/tools/formal_charge.py +0 -0
  166. {finalcif-175 → finalcif-177}/finalcif/tools/misc.py +0 -0
  167. {finalcif-175 → finalcif-177}/finalcif/tools/mol_file_writer.py +0 -0
  168. {finalcif-175 → finalcif-177}/finalcif/tools/options.py +0 -0
  169. {finalcif-175 → finalcif-177}/finalcif/tools/platon.py +0 -0
  170. {finalcif-175 → finalcif-177}/finalcif/tools/pupdate.py +0 -0
  171. {finalcif-175 → finalcif-177}/finalcif/tools/selfupdate.py +0 -0
  172. {finalcif-175 → finalcif-177}/finalcif/tools/settings.py +0 -0
  173. {finalcif-175 → finalcif-177}/finalcif/tools/space_groups.py +0 -0
  174. {finalcif-175 → finalcif-177}/finalcif/tools/spgr_format.py +0 -0
  175. {finalcif-175 → finalcif-177}/finalcif/tools/statusbar.py +0 -0
  176. {finalcif-175 → finalcif-177}/finalcif/tools/sumformula.py +0 -0
  177. {finalcif-175 → finalcif-177}/finalcif/tools/z_from_packing.py +0 -0
  178. {finalcif-175 → finalcif-177}/finalcif.exe +0 -0
  179. {finalcif-175 → finalcif-177}/platon/README.txt +0 -0
  180. {finalcif-175 → finalcif-177}/platon/compile_platon.bat +0 -0
  181. {finalcif-175 → finalcif-177}/platon/platon_special.exe +0 -0
  182. {finalcif-175 → finalcif-177}/platon/platon_special.f +0 -0
  183. {finalcif-175 → finalcif-177}/pylock.toml +0 -0
  184. {finalcif-175 → finalcif-177}/pyproject.toml +0 -0
  185. {finalcif-175 → finalcif-177}/run_and_capture.py +0 -0
  186. {finalcif-175 → finalcif-177}/run_finalcif +0 -0
  187. {finalcif-175 → finalcif-177}/run_finalcif.bat +0 -0
  188. {finalcif-175 → finalcif-177}/screenshots/finalcif_checkcif.png +0 -0
  189. {finalcif-175 → finalcif-177}/screenshots/finalcif_details.png +0 -0
  190. {finalcif-175 → finalcif-177}/screenshots/finalcif_loops.png +0 -0
  191. {finalcif-175 → finalcif-177}/screenshots/finalcif_loops2.png +0 -0
  192. {finalcif-175 → finalcif-177}/screenshots/finalcif_main.png +0 -0
  193. {finalcif-175 → finalcif-177}/screenshots/finalcif_report.png +0 -0
  194. {finalcif-175 → finalcif-177}/screenshots/finalcif_responses.png +0 -0
  195. {finalcif-175 → finalcif-177}/screenshots/loops_column_buttons.png +0 -0
  196. {finalcif-175 → finalcif-177}/work/compare_z_platon.py +0 -0
  197. {finalcif-175 → finalcif-177}/work/diagnose_1519506.py +0 -0
  198. {finalcif-175 → finalcif-177}/work/diagnose_z.py +0 -0
  199. {finalcif-175 → finalcif-177}/work/investigate_failures.py +0 -0
  200. {finalcif-175 → finalcif-177}/work/validate_cod.py +0 -0
  201. {finalcif-175 → finalcif-177}/work/validate_zprime.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: finalcif
3
- Version: 175
3
+ Version: 177
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
@@ -0,0 +1,3 @@
1
+
2
+ VERSION = 177
3
+ __version__ = VERSION
@@ -1311,15 +1311,15 @@ class AppWindow(QMainWindow):
1311
1311
  self.validation_response_forms_list.append(vrf)
1312
1312
  table.place_vrf_widget(insert_pos, vrf)
1313
1313
 
1314
- if html_entries:
1315
- n = len(html_entries)
1316
- form_noun = 'form' if n == 1 else 'forms'
1317
- self.ui.CheckCifLogPlainTextEdit.appendHtml(
1318
- f'<br><b>{n} validation response forms {form_noun} added to the main table.</b>')
1319
- else:
1320
- levels = 'A, B or C' if self.ui.explainCandGAlertsCheckBox.isChecked() else 'A or B'
1321
- self.ui.CheckCifLogPlainTextEdit.appendHtml(
1322
- f'<br><b>No level {levels} alerts require a response.</b>')
1314
+ self.ui.CheckCifLogPlainTextEdit.appendHtml(self._vrf_summary_message(len(html_entries)))
1315
+
1316
+ def _vrf_summary_message(self, count: int) -> str:
1317
+ """Return the log message summarizing how many VRFs a CheckCIF run produced."""
1318
+ if count:
1319
+ form_noun = 'form' if count == 1 else 'forms'
1320
+ return f'<br><b>{count} validation response {form_noun} added to the main table.</b>'
1321
+ levels = 'A, B or C' if self.ui.explainCAlertsCheckBox.isChecked() else 'A or B'
1322
+ return f'<br><b>No level {levels} alerts require a response.</b>'
1323
1323
 
1324
1324
  def _vrf_insert_position(self) -> int:
1325
1325
  """Return the row index just after the last existing ``_vrf_*`` row in the main table.
@@ -34,10 +34,7 @@ class HKL:
34
34
 
35
35
  @property
36
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))
37
+ return self._doc.as_string(options=gemmi.cif.WriteOptions(Style.Simple))
41
38
 
42
39
  def _add_hkl_as_loop(self) -> None:
43
40
  """
@@ -2881,7 +2881,7 @@ class Ui_FinalCifWindow(object):
2881
2881
  self.CancelPropertiesButton.setText(QCoreApplication.translate("FinalCifWindow", u"Cancel", None))
2882
2882
  self.ExportPropertyButton.setText(QCoreApplication.translate("FinalCifWindow", u"Export", None))
2883
2883
  self.BackFromLoopsPushButton.setText(QCoreApplication.translate("FinalCifWindow", u"Back to CIF Table", None))
2884
- self.add_squeeze_pushButton.setText(QCoreApplication.translate("FinalCifWindow", u"Add Squeeze Info", None))
2884
+ self.add_squeeze_pushButton.setText(QCoreApplication.translate("FinalCifWindow", u"Add/Edit Squeeze Info", None))
2885
2885
  self.deleteLoopButton.setText(QCoreApplication.translate("FinalCifWindow", u"Delete Loop", None))
2886
2886
  self.newLoopPushButton.setText(QCoreApplication.translate("FinalCifWindow", u"Add New Loop", None))
2887
2887
  self.revertLoopsPushButton.setText(QCoreApplication.translate("FinalCifWindow", u"Revert Changes", None))
@@ -2861,7 +2861,7 @@ The data source will be ignored until next program restart.</string>
2861
2861
  <item row="0" column="2">
2862
2862
  <widget class="QPushButton" name="add_squeeze_pushButton">
2863
2863
  <property name="text">
2864
- <string>Add Squeeze Info</string>
2864
+ <string>Add/Edit Squeeze Info</string>
2865
2865
  </property>
2866
2866
  </widget>
2867
2867
  </item>
@@ -18,14 +18,15 @@ from pathlib import Path
18
18
  from qtpy import QtCore, compat
19
19
  from qtpy.QtGui import QColor
20
20
  from qtpy.QtWidgets import (
21
- QDialog, QDialogButtonBox, QHBoxLayout, QLabel, QMessageBox,
22
- QPlainTextEdit, QPushButton, QSizePolicy, QTableWidget, QTableWidgetItem,
21
+ QAbstractItemView, QCompleter, QDialog, QDialogButtonBox, QHBoxLayout, QLabel, QLineEdit, QMessageBox,
22
+ QPlainTextEdit, QPushButton, QSizePolicy, QStyledItemDelegate, QTableWidget, QTableWidgetItem,
23
23
  QVBoxLayout, QWidget, QApplication
24
24
  )
25
25
 
26
26
  from finalcif.cif.cif_file_io import CifContainer
27
- from finalcif.cif.text import quote
28
- from finalcif.tools.squeeze import build_details_text, electrons_from_formula
27
+ from finalcif.cif.text import quote, retranslate_delimiter
28
+ from finalcif.tools.solvents import solvent_names
29
+ from finalcif.tools.squeeze import build_details_text, electrons_from_formula, resolve_formula
29
30
 
30
31
 
31
32
  class SqueezeMode(Enum):
@@ -78,7 +79,9 @@ _MODE_CONFIG: dict[SqueezeMode, dict] = {
78
79
  'details_key' : '_platon_squeeze_details',
79
80
  'title' : 'PLATON SQUEEZE \u2013 Assign Solvent Content',
80
81
  'info' : ('PLATON/SQUEEZE was used. '
81
- 'Assign the solvent formula per void <b>per unit cell</b>, e.g. <tt>2(H2O)</tt>.'),
82
+ 'Assign the solvent formula per void <b>per unit cell</b>, e.g. <tt>2(H2O)</tt>.\n'
83
+ 'Common solvent names like <tt>2 thf</tt> or <tt>toluene</tt> are '
84
+ 'converted to their formula.'),
82
85
  'details_label' : 'SQUEEZE details (<i>_platon_squeeze_details</i>):',
83
86
  'electrons_col_header': 'Electrons\n(PLATON)',
84
87
  },
@@ -91,7 +94,9 @@ _MODE_CONFIG: dict[SqueezeMode, dict] = {
91
94
  'details_key' : '_smtbx_masks_special_details',
92
95
  'title' : 'Olex2/SMTBX Masks \u2013 Assign Solvent Content',
93
96
  'info' : ('Olex2/SMTBX solvent masks were used. '
94
- 'Assign the solvent formula per void <b>per unit cell</b>, e.g. <tt>2(H2O)</tt>.'),
97
+ 'Assign the solvent formula per void <b>per unit cell</b>, e.g. <tt>2(H2O)</tt>.\n'
98
+ 'Common solvent names like <tt>2 thf</tt> or <tt>toluene</tt> are '
99
+ 'converted to their formula.'),
95
100
  'details_label' : 'Masks details (<i>_smtbx_masks_special_details</i>):',
96
101
  'electrons_col_header': 'Electrons\n(Masks)',
97
102
  },
@@ -102,6 +107,19 @@ _COLOR_WARN = QColor(255, 200, 200) # light red
102
107
  _COLOR_NEUTRAL = QColor(255, 255, 255) # white
103
108
 
104
109
 
110
+ class SolventNameDelegate(QStyledItemDelegate):
111
+ """Line edit delegate that autocompletes common solvent names."""
112
+
113
+ def createEditor(self, parent: QWidget, option, index) -> QWidget:
114
+ editor = QLineEdit(parent)
115
+ completer = QCompleter(solvent_names(), editor)
116
+ completer.setCaseSensitivity(QtCore.Qt.CaseSensitivity.CaseInsensitive)
117
+ completer.setFilterMode(QtCore.Qt.MatchFlag.MatchContains)
118
+ completer.setCompletionMode(QCompleter.CompletionMode.PopupCompletion)
119
+ editor.setCompleter(completer)
120
+ return editor
121
+
122
+
105
123
  class SqueezeSolventDialog(QDialog):
106
124
  """
107
125
  Modal dialog for entering PLATON SQUEEZE or Olex2/SMTBX solvent mask content.
@@ -212,6 +230,15 @@ class SqueezeSolventDialog(QDialog):
212
230
  self.table.setColumnWidth(_COL_FORMULA, 160)
213
231
  self.table.setColumnWidth(_COL_ELEC_CALC, 85)
214
232
  self.table.verticalHeader().setVisible(False)
233
+ self.table.setEditTriggers(
234
+ QAbstractItemView.EditTrigger.CurrentChanged
235
+ | QAbstractItemView.EditTrigger.SelectedClicked
236
+ | QAbstractItemView.EditTrigger.DoubleClicked
237
+ | QAbstractItemView.EditTrigger.AnyKeyPressed
238
+ | QAbstractItemView.EditTrigger.EditKeyPressed
239
+ )
240
+ self._solvent_delegate = SolventNameDelegate(self.table)
241
+ self.table.setItemDelegateForColumn(_COL_FORMULA, self._solvent_delegate)
215
242
  self.table.itemChanged.connect(self._on_formula_changed)
216
243
  layout.addWidget(self.table)
217
244
 
@@ -242,7 +269,7 @@ class SqueezeSolventDialog(QDialog):
242
269
  existing_details = self.cif[self._cfg['details_key']]
243
270
  if existing_details:
244
271
  self._details_user_modified = True
245
- self.details_edit.setPlainText(existing_details)
272
+ self.details_edit.setPlainText(retranslate_delimiter(existing_details))
246
273
  self.details_edit.textChanged.connect(self._on_details_text_changed)
247
274
 
248
275
  # button box
@@ -408,9 +435,25 @@ class SqueezeSolventDialog(QDialog):
408
435
  def _on_formula_changed(self, item: QTableWidgetItem) -> None:
409
436
  if item.column() != _COL_FORMULA:
410
437
  return
438
+ self._resolve_item_formula(item)
411
439
  self._update_row_calculations(item.row())
412
440
  self._regenerate_details()
413
441
 
442
+ @staticmethod
443
+ def _resolve_item_formula(item: QTableWidgetItem) -> None:
444
+ """Replace a solvent name in *item* by its sum formula."""
445
+ text = item.text()
446
+ resolved = resolve_formula(text)
447
+ if resolved == text:
448
+ return
449
+ table = item.tableWidget()
450
+ blocked = table.signalsBlocked() if table else True
451
+ if table:
452
+ table.blockSignals(True)
453
+ item.setText(resolved)
454
+ if table:
455
+ table.blockSignals(blocked)
456
+
414
457
  def _update_row_calculations(self, row_idx: int) -> None:
415
458
  """Recalculate Electrons (calc.) and Δ for a given row."""
416
459
  formula_item = self.table.item(row_idx, _COL_FORMULA)
@@ -458,6 +501,7 @@ class SqueezeSolventDialog(QDialog):
458
501
  item = self.table.item(row_idx, _COL_FORMULA)
459
502
  if item:
460
503
  item.setText(formula)
504
+ self._resolve_item_formula(item)
461
505
  self._update_row_calculations(row_idx)
462
506
  self.table.blockSignals(False)
463
507
  self._regenerate_details()
@@ -475,7 +519,7 @@ class SqueezeSolventDialog(QDialog):
475
519
  'nr' : row_idx + 1,
476
520
  'volume' : vol_item.text() if vol_item else '?',
477
521
  'electrons_platon': elec_item.text() if elec_item else '?',
478
- 'formula' : formula_item.text() if formula_item else '',
522
+ 'formula' : resolve_formula(formula_item.text()) if formula_item else '',
479
523
  })
480
524
  text = build_details_text(void_rows, method=self._loop_mode.value)
481
525
  # Block textChanged so our programmatic update doesn't set _details_user_modified
@@ -513,7 +557,7 @@ class SqueezeSolventDialog(QDialog):
513
557
  new_formulae: list[str] = []
514
558
  for row_idx in range(n_rows):
515
559
  item = self.table.item(row_idx, _COL_FORMULA)
516
- formula = item.text().strip() if item else '?'
560
+ formula = resolve_formula(item.text()).strip() if item else '?'
517
561
  new_formulae.append(quote(formula) if formula else '?')
518
562
 
519
563
  # Reconstruct column data with the updated content column
@@ -147,6 +147,15 @@ class Textlimits:
147
147
  return False
148
148
 
149
149
 
150
+ class AnyTextLimits:
151
+ def __init__(self, help_text: str | None = None):
152
+ self.valid = self.validate_cif_key
153
+ self.help_text = help_text or 'Any text is allowed.'
154
+
155
+ def validate_cif_key(self, value: str) -> bool:
156
+ return True
157
+
158
+
150
159
  validators: dict[str, BaseLimits] = {
151
160
  '_database_code_depnum_ccdc_archive' : Integerlimits(lower=0, upper=inf),
152
161
  '_cell_measurement_reflns_used' : Integerlimits(lower=0, upper=inf),
@@ -279,7 +288,7 @@ validators: dict[str, BaseLimits] = {
279
288
  'DGRS']),
280
289
  '_atom_site_refinement_flags_adp' : Textlimits(options=['None', 'T', 'U', 'TU']),
281
290
  '_atom_site_refinement_flags_occupancy': Textlimits(options=['None', 'P']),
282
- '_atom_site_disorder_assembly' : Textlimits(options=['A', 'B', 'S']),
291
+ '_atom_site_disorder_assembly' : AnyTextLimits(),
283
292
  '_atom_site_aniso_U_11' : Floatlimits(),
284
293
  '_atom_site_aniso_U_22' : Floatlimits(),
285
294
  '_atom_site_aniso_U_33' : Floatlimits(),
@@ -68,7 +68,7 @@ class ShredCIF:
68
68
  if line.startswith(')'):
69
69
  line = ';' + line[1:]
70
70
  lines.append(line)
71
- return '\n'.join(lines).lstrip('\n')
71
+ return '\n'.join(lines).lstrip('\n\r')
72
72
 
73
73
  def _show_info(self, resname: Path, hklname: Path, resdata: str | None, hkldata: str | None) -> None:
74
74
  if resdata and not hkldata:
@@ -0,0 +1,183 @@
1
+ # ----------------------------------------------------------------------------
2
+ # "THE BEER-WARE LICENSE" (Revision 42):
3
+ # <dkratzert@gmx.de> wrote this file. As long as you retain
4
+ # this notice you can do whatever you want with this stuff. If we meet some day,
5
+ # and you think this stuff is worth it, you can buy me a beer in return.
6
+ # Dr. Daniel Kratzert
7
+ # ----------------------------------------------------------------------------
8
+ """
9
+ Lookup table that translates common solvent names into chemical formulae.
10
+
11
+ This allows users to type ``thf``, ``water`` or ``2 toluene`` instead of the
12
+ corresponding sum formula when assigning solvent content to voids.
13
+ """
14
+ from __future__ import annotations
15
+
16
+ import re
17
+
18
+ # Canonical solvent name -> sum formula.
19
+ SOLVENTS: dict[str, str] = {
20
+ 'water' : 'H2O',
21
+ 'methanol' : 'CH4O',
22
+ 'ethanol' : 'C2H6O',
23
+ '1-propanol' : 'C3H8O',
24
+ '2-propanol' : 'C3H8O',
25
+ '1-butanol' : 'C4H10O',
26
+ 'tert-butanol' : 'C4H10O',
27
+ 'ethylene glycol' : 'C2H6O2',
28
+ 'acetone' : 'C3H6O',
29
+ 'acetonitrile' : 'C2H3N',
30
+ 'acetic acid' : 'C2H4O2',
31
+ 'ethyl acetate' : 'C4H8O2',
32
+ 'formic acid' : 'CH2O2',
33
+ 'tetrahydrofuran' : 'C4H8O',
34
+ '2-methyltetrahydrofuran': 'C5H10O',
35
+ 'tetrahydropyran' : 'C5H10O',
36
+ '1,4-dioxane' : 'C4H8O2',
37
+ 'diethyl ether' : 'C4H10O',
38
+ 'diisopropyl ether' : 'C6H14O',
39
+ 'tert-butyl methyl ether': 'C5H12O',
40
+ 'dimethoxyethane' : 'C4H10O2',
41
+ 'diglyme' : 'C6H14O3',
42
+ 'anisole' : 'C7H8O',
43
+ 'dichloromethane' : 'CH2Cl2',
44
+ 'chloroform' : 'CHCl3',
45
+ 'carbon tetrachloride' : 'CCl4',
46
+ '1,2-dichloroethane' : 'C2H4Cl2',
47
+ 'carbon disulfide' : 'CS2',
48
+ 'benzene' : 'C6H6',
49
+ 'toluene' : 'C7H8',
50
+ 'xylene' : 'C8H10',
51
+ 'mesitylene' : 'C9H12',
52
+ 'chlorobenzene' : 'C6H5Cl',
53
+ '1,2-dichlorobenzene' : 'C6H4Cl2',
54
+ 'bromobenzene' : 'C6H5Br',
55
+ 'fluorobenzene' : 'C6H5F',
56
+ '1,2-difluorobenzene' : 'C6H4F2',
57
+ 'nitrobenzene' : 'C6H5NO2',
58
+ 'nitromethane' : 'CH3NO2',
59
+ 'pentane' : 'C5H12',
60
+ 'hexane' : 'C6H14',
61
+ 'heptane' : 'C7H16',
62
+ 'octane' : 'C8H18',
63
+ 'cyclohexane' : 'C6H12',
64
+ 'cyclopentane' : 'C5H10',
65
+ 'pyridine' : 'C5H5N',
66
+ 'triethylamine' : 'C6H15N',
67
+ 'dimethylformamide' : 'C3H7NO',
68
+ 'dimethylacetamide' : 'C4H9NO',
69
+ 'dimethyl sulfoxide' : 'C2H6OS',
70
+ 'n-methylpyrrolidone' : 'C5H9NO',
71
+ 'hexamethylphosphoramide': 'C6H18N3OP',
72
+ 'sulfolane' : 'C4H8O2S',
73
+ 'aniline' : 'C6H7N',
74
+ 'ammonia' : 'H3N',
75
+ 'hydrazine' : 'H4N2',
76
+ }
77
+
78
+ # Alias -> canonical solvent name. Aliases must not look like a valid sum
79
+ # formula, otherwise a formula would silently be reinterpreted as a name.
80
+ ALIASES: dict[str, str] = {
81
+ 'h2o' : 'water',
82
+ 'aqua' : 'water',
83
+ 'meoh' : 'methanol',
84
+ 'etoh' : 'ethanol',
85
+ 'cyclohexan' : 'cyclohexane',
86
+ 'n-propanol' : '1-propanol',
87
+ 'propanol' : '2-propanol',
88
+ 'isopropanol' : '2-propanol',
89
+ 'iso-propanol' : '2-propanol',
90
+ 'ipa' : '2-propanol',
91
+ 'iproh' : '2-propanol',
92
+ 'n-butanol' : '1-butanol',
93
+ 'butanol' : '1-butanol',
94
+ 't-butanol' : 'tert-butanol',
95
+ 'tert-butyl alcohol' : 'tert-butanol',
96
+ 'tbuoh' : 'tert-butanol',
97
+ 'glycol' : 'ethylene glycol',
98
+ 'aceton' : 'acetone',
99
+ 'mecn' : 'acetonitrile',
100
+ 'ch3cn' : 'acetonitrile',
101
+ 'etoac' : 'ethyl acetate',
102
+ 'ethylacetate' : 'ethyl acetate',
103
+ 'thf' : 'tetrahydrofuran',
104
+ 'me-thf' : '2-methyltetrahydrofuran',
105
+ '2-me-thf' : '2-methyltetrahydrofuran',
106
+ '2-methyl-thf' : '2-methyltetrahydrofuran',
107
+ 'methf' : '2-methyltetrahydrofuran',
108
+ 'thp' : 'tetrahydropyran',
109
+ 'dioxane' : '1,4-dioxane',
110
+ 'ether' : 'diethyl ether',
111
+ 'et2o' : 'diethyl ether',
112
+ 'diethylether' : 'diethyl ether',
113
+ 'ipr2o' : 'diisopropyl ether',
114
+ 'mtbe' : 'tert-butyl methyl ether',
115
+ 'tbme' : 'tert-butyl methyl ether',
116
+ 'dme' : 'dimethoxyethane',
117
+ 'glyme' : 'dimethoxyethane',
118
+ '1,2-dimethoxyethane' : 'dimethoxyethane',
119
+ 'methoxybenzene' : 'anisole',
120
+ 'dcm' : 'dichloromethane',
121
+ 'methylene chloride' : 'dichloromethane',
122
+ 'methylenechloride' : 'dichloromethane',
123
+ 'dce' : '1,2-dichloroethane',
124
+ 'benzol' : 'benzene',
125
+ 'toluol' : 'toluene',
126
+ 'tol' : 'toluene',
127
+ 'p-xylene' : 'xylene',
128
+ 'o-xylene' : 'xylene',
129
+ 'm-xylene' : 'xylene',
130
+ 'xylol' : 'xylene',
131
+ '1,3,5-trimethylbenzene': 'mesitylene',
132
+ 'odcb' : '1,2-dichlorobenzene',
133
+ 'dfb' : '1,2-difluorobenzene',
134
+ 'n-pentane' : 'pentane',
135
+ 'n-hexane' : 'hexane',
136
+ 'n-heptane' : 'heptane',
137
+ 'n-octane' : 'octane',
138
+ 'pyr' : 'pyridine',
139
+ 'py' : 'pyridine',
140
+ 'net3' : 'triethylamine',
141
+ 'et3n' : 'triethylamine',
142
+ 'dmf' : 'dimethylformamide',
143
+ 'n,n-dimethylformamide' : 'dimethylformamide',
144
+ 'dma' : 'dimethylacetamide',
145
+ 'n,n-dimethylacetamide' : 'dimethylacetamide',
146
+ 'dmso' : 'dimethyl sulfoxide',
147
+ 'dimethylsulfoxide' : 'dimethyl sulfoxide',
148
+ 'nmp' : 'n-methylpyrrolidone',
149
+ 'n-methyl-2-pyrrolidone': 'n-methylpyrrolidone',
150
+ 'hmpa' : 'hexamethylphosphoramide',
151
+ 'tetramethylene sulfone': 'sulfolane',
152
+ 'aminobenzene' : 'aniline',
153
+ }
154
+
155
+ _NORMALIZE_RE = re.compile(r'[\s_]+')
156
+
157
+
158
+ def _norm(name: str) -> str:
159
+ """Normalize a solvent name for case- and whitespace-insensitive lookup."""
160
+ return _NORMALIZE_RE.sub(' ', name.strip().lower()).replace(' ', '')
161
+
162
+
163
+ _LOOKUP: dict[str, str] = {_norm(name): formula for name, formula in SOLVENTS.items()}
164
+ _LOOKUP.update({_norm(alias): SOLVENTS[canonical] for alias, canonical in ALIASES.items()})
165
+
166
+
167
+ def resolve_solvent(name: str) -> str | None:
168
+ """Return the sum formula for a solvent name or alias.
169
+
170
+ Args:
171
+ name: Solvent name such as ``'thf'``, ``'Water'`` or ``'diethyl ether'``.
172
+
173
+ Returns:
174
+ The sum formula, or ``None`` when *name* is not a known solvent.
175
+ """
176
+ if not name:
177
+ return None
178
+ return _LOOKUP.get(_norm(name))
179
+
180
+
181
+ def solvent_names() -> list[str]:
182
+ """Return all canonical solvent names and aliases, sorted alphabetically."""
183
+ return sorted(set(SOLVENTS) | set(ALIASES))
@@ -15,6 +15,7 @@ from typing import Literal, TYPE_CHECKING
15
15
 
16
16
  from finalcif.cif.atoms import element2num
17
17
  from finalcif.tools.chemparse import parse_formula, ChemparseError
18
+ from finalcif.tools.solvents import resolve_solvent
18
19
 
19
20
  if TYPE_CHECKING:
20
21
  from finalcif.cif.cif_file_io import CifContainer
@@ -22,6 +23,42 @@ if TYPE_CHECKING:
22
23
  # Atomic numbers (= electron count for neutral atoms) for elements common in crystallography.
23
24
  ATOMIC_NUMBERS: dict[str, int] = element2num
24
25
 
26
+ # Splits an optional leading multiplier from the rest, e.g. '2(thf)' or '2 thf'.
27
+ _MULTIPLIER_RE = re.compile(r'^(\d+(?:\.\d+)?)\s*(.*)$', re.DOTALL)
28
+
29
+
30
+ def resolve_formula(text: str) -> str:
31
+ """Translate a solvent name into its sum formula.
32
+
33
+ Accepts an optional leading multiplier, so ``'thf'``, ``'2 thf'`` and
34
+ ``'2(thf)'`` all resolve. Text that does not name a known solvent is
35
+ returned unchanged, which keeps ordinary formulae such as ``'2(H2O)'``
36
+ working.
37
+
38
+ Args:
39
+ text: User input, e.g. ``'2 toluene'``.
40
+
41
+ Returns:
42
+ A formula string such as ``'2(C7H8)'``, or *text* unchanged.
43
+ """
44
+ stripped = text.strip()
45
+ if not stripped:
46
+ return text
47
+ multiplier = ''
48
+ remainder = stripped
49
+ match = _MULTIPLIER_RE.match(stripped)
50
+ if match:
51
+ multiplier, remainder = match.group(1), match.group(2).strip()
52
+ remainder = remainder.strip()
53
+ if remainder.startswith('(') and remainder.endswith(')'):
54
+ remainder = remainder[1:-1].strip()
55
+ formula = resolve_solvent(remainder)
56
+ if formula is None:
57
+ return stripped
58
+ if not multiplier or float(multiplier) == 1:
59
+ return formula
60
+ return f'{multiplier}({formula})'
61
+
25
62
 
26
63
  def _normalize_squeeze_formula(formula_str: str) -> str:
27
64
  """
@@ -80,11 +117,12 @@ def electrons_from_formula(formula_str: str) -> int:
80
117
  """
81
118
  Returns the total electron count for a chemical formula string.
82
119
 
83
- Supports standard crystallographic formulae such as 'C4H8O', '2(H2O)', 'CHCl3'.
120
+ Supports standard crystallographic formulae such as 'C4H8O', '2(H2O)', 'CHCl3'
121
+ as well as common solvent names like 'thf' or '2 toluene'.
84
122
  Unknown elements contribute 0 electrons. Returns 0 for empty / placeholder strings.
85
123
 
86
124
  Args:
87
- formula_str: Chemical formula, e.g. '2(C4H8O)' or 'H2O'.
125
+ formula_str: Chemical formula, e.g. '2(C4H8O)', 'H2O' or '2 thf'.
88
126
 
89
127
  Returns:
90
128
  Integer electron count, or 0 when the formula cannot be parsed.
@@ -92,7 +130,7 @@ def electrons_from_formula(formula_str: str) -> int:
92
130
  if not formula_str or formula_str.strip() in ('?', '.', ''):
93
131
  return 0
94
132
  try:
95
- normalised = _normalize_squeeze_formula(formula_str.strip())
133
+ normalised = _normalize_squeeze_formula(resolve_formula(formula_str))
96
134
  atom_dict = parse_formula(normalised)
97
135
  return round(sum(ATOMIC_NUMBERS.get(el, 0) * count for el, count in atom_dict.items()))
98
136
  except ChemparseError: