Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

73 make attributes of multi cif accessible in report templates #80

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -215,3 +215,5 @@ test-data/DK_Zucker2_0m-finalcif_changes.cif
olex2
.vs
x64
test-data/1000007-multi-multitable.docx
test-data/report_1000007-multi-finalcif.docx
4 changes: 2 additions & 2 deletions .run/make documentation Windows.run.xml
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
<component name="ProjectRunConfigurationManager">
<configuration default="false" name="make documentation Windows" type="ShConfigurationType">
<option name="SCRIPT_TEXT" value="" />
<option name="INDEPENDENT_SCRIPT_PATH" value="true" />
<option name="SCRIPT_PATH" value="docs/make.bat" />
<option name="INDEPENDENT_SCRIPT_PATH" value="false" />
<option name="SCRIPT_PATH" value="$PROJECT_DIR$/docs/make.bat" />
<option name="SCRIPT_OPTIONS" value="singlehtml" />
<option name="INDEPENDENT_SCRIPT_WORKING_DIRECTORY" value="true" />
<option name="SCRIPT_WORKING_DIRECTORY" value="$PROJECT_DIR$" />
Expand Down
43 changes: 43 additions & 0 deletions docs/report.rst
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,9 @@ Data Available for the Report

'cif' : Gives you access to the full CIF information, use it like
{{ cif._exptl_crystal_density_diffrn }} or the variables in the next table.
'name' : Name of the current CIF block.
'block' : The context of all CIF blocks of a multi-CIF usable as attribute, e.g. block.name.foo
'blocklist' : A list of all CIF blocks of a multi-CIF usable for iteration over blocks.
'atomic_coordinates' : The atomic coordinates as ('label', 'x', 'y', 'z', 'u_eq') for each atom.
'displacement_parameters': The atomic displacement parameters as ('label', 'U11', 'U22', 'U33',
'U23', 'U13', 'U12') for each atom.
Expand Down Expand Up @@ -150,6 +153,8 @@ Data Available for the Report
'restraints' : The value of '_refine_ls_number_restraints'.
'parameters' : The value of '_refine_ls_number_parameters'.
'goof' : The value of '_refine_ls_goodness_of_fit_ref'.
't_min' : The value of '_exptl_absorpt_correction_T_min'.
't_max' : The value of '_exptl_absorpt_correction_T_max'.
'ls_R_factor_gt' : The value of '_refine_ls_R_factor_gt'.
'ls_wR_factor_gt' : The value of '_refine_ls_wR_factor_gt'.
'ls_R_factor_all' : The value of '_refine_ls_R_factor_all'.
Expand Down Expand Up @@ -181,6 +186,9 @@ no real minus sign in front. The former values hav hyphens replaced with minus s
'cif.disorder_present' : Is true if atoms in parts are present in the structure.
'cif.cell' : The unit cell as 'a', 'b', 'c', 'alpha', 'beta', 'gamma', 'volume'.
'cif.bonds' : The list of bonds as 'label1', 'label2', 'dist', 'symm'.
'cif.bond_dist("atom1-atom2")' : The bond distance between two atoms.
'cif.angle("atom1-atom2-atom3")' : The angle between three atoms.
'cif.torsion("atom1-atom2-atom3-atom4")' : The torsion angle between four atoms.
'angles' : The list of angles as 'label1', 'label2', 'label3', 'angle_val',
'symm1', 'symm2'.
'torsion_angles' : The list of torsion angles as 'label1', 'label2', 'label3', 'label4',
Expand All @@ -195,6 +203,41 @@ no real minus sign in front. The former values hav hyphens replaced with minus s
The above is not limited to the templates of FinalCif. It is also possible to insert template tags
into any other Word document and replace them with values from a CIF file. There are no limits to
the imagination.
Sine version 130, it is possible to address the values of individual blocks of a multi-CIF. For example,

.. code-block:: jinja

{% for block in blocklist %}
{{ block.name }}
{% enfor %}

prints all block names of a multi-CIF.

Another way is to use the 'block' variable in the template. It contains the respective block data.
To access the values of the block, you have to use the block name in square brackets. This prevents
conflicts with the Jinja2 syntax.
For example, the chemical formula of the block 'compound1' of a multi-CIF is:

.. code-block:: jinja

{{ block['compound1']._chemical_formula_sum }}

Special methods allow you to access the values of the atoms, bonds, angles, torsion angles of single- and
multi-CIF files:

.. code-block:: jinja

{{ block['p-1'].cif.bond_dist('C1-C2') }}
{{ block['p-1'].cif.angle('C1-C2-C3') }}

Prints out the distance between C1 and C2 as well as the angle between C1, C2 and C3.
This can be used to render specific bond distances of a multi-CIF
file to a publication without the need to change the values by hand every time a refinement changes.
Be aware that the atom labels must be given in the order they have in the respective CIF loop. When a
an atom combination is not present in a CIF loop, the value 'None' will appear.

For a single-CIF, leave out the "block['block name']." part.



Further information for programmers:
Expand Down
31 changes: 21 additions & 10 deletions finalcif/appwindow.py
Original file line number Diff line number Diff line change
Expand Up @@ -1113,14 +1113,16 @@ def make_report_tables(self) -> None:
print('Report without templates')
make_report_from(options=self.options, cif=self.cif,
output_filename=str(report_filename), picfile=picfile)
if self.cif.is_multi_cif and self.cif.doc[0].name != 'global':
make_multi_tables(cif=self.cif, output_filename=str(multi_table_document))
else:
print('Report with templates')
t = TemplatedReport()
t.make_templated_report(options=self.options, cif=self.cif,
output_filename=str(report_filename), picfile=picfile,
template_path=Path(self.get_checked_templates_list_text()))
if self.cif.is_multi_cif and self.cif.doc[0].name != 'global':
make_multi_tables(cif=self.cif, output_filename=str(multi_table_document))
ok = t.make_templated_report(options=self.options, cif=self.cif,
output_filename=str(report_filename), picfile=picfile,
template_path=Path(self.get_checked_templates_list_text()))
if not ok:
return None
except FileNotFoundError as e:
if DEBUG:
raise
Expand All @@ -1137,8 +1139,9 @@ def make_report_tables(self) -> None:
return
if not self.running_inside_unit_test:
self.open_report_document(report_filename, multi_table_document)
print('dbg> disabled temporarily!')
# Save report and other files to a zip file:
self.zip_report(report_filename)
# self.zip_report(report_filename)

def report_without_template(self) -> bool:
"""Check whether the report is generated from a template or hard-coded"""
Expand All @@ -1165,7 +1168,7 @@ def zip_report(self, report_filename: Path) -> None:
arc.zip.write(filename=multitable, arcname=multitable.name)

def open_report_document(self, report_filename: Path, multi_table_document: Path) -> None:
if self.cif.is_multi_cif:
if self.cif.is_multi_cif and self.report_without_template():
open_file(multi_table_document)
open_file(report_filename)

Expand Down Expand Up @@ -1444,8 +1447,16 @@ def load_cif_file(self, filepath: Path, block=0, load_changes: bool = True) -> N
self.ui.cif_main_table.resizeRowsToContents()
self.ui.datanameComboBox.blockSignals(False)
if self.cif.is_multi_cif:
# short after start, because window size is not finished
QtCore.QTimer(self).singleShot(1000, self.ui.datanameComboBox.showPopup)
self._flash_block_combobox()

def _flash_block_combobox(self):
orig_pal = self.ui.datanameComboBox.palette()
pal = QtGui.QPalette()
pal.setColor(QtGui.QPalette.Base, light_blue)
self.ui.datanameComboBox.setAutoFillBackground(True)
# short after start, because window size is not finished before:
QtCore.QTimer(self).singleShot(1500, lambda: self.ui.datanameComboBox.setPalette(pal))
QtCore.QTimer(self).singleShot(2600, lambda: self.ui.datanameComboBox.setPalette(orig_pal))

def add_data_names_to_combobox(self) -> None:
self.ui.datanameComboBox.clear()
Expand Down Expand Up @@ -1489,7 +1500,7 @@ def _load_block(self, index: int, load_changes: bool = True) -> None:
self.finalcif_changes_filename.unlink(missing_ok=True)
elif self.finalcif_changes_filename.exists() and self.changes_cif_has_values():
changes_exist = True
if changes_exist and not self.running_inside_unit_test and self.changes_answer == 0:
if changes_exist and not self.running_inside_unit_test and self.changes_answer == 0 and not self.cif.is_multi_cif:
self.changes_answer = QMessageBox.question(self, 'Previous changes found',
f'Previous changes from a former FinalCif session '
f'were found in\n{self.finalcif_changes_filename.name}.\n\n'
Expand Down
49 changes: 33 additions & 16 deletions finalcif/cif/cif_file_io.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,8 @@
import re
from collections import namedtuple
from contextlib import suppress
from functools import cache
from pathlib import Path
from typing import Dict, List, Tuple, Union, Generator, Type
from typing import Dict, List, Tuple, Union, Generator

import gemmi
from gemmi.cif import as_string, Document, Loop
Expand Down Expand Up @@ -141,7 +140,6 @@ def _on_load(self) -> None:
self.check_hkl_min_max()

@property
@cache
def hkl_extra_info(self):
return self._abs_hkl_details()

Expand Down Expand Up @@ -595,10 +593,10 @@ def calc_checksum(input_str: str) -> int:
"""
crc_sum = 0
try:
input_str = input_str.encode('cp1250', 'ignore')
bytes_str = input_str.encode('cp1250', 'ignore')
except Exception:
input_str = input_str.encode('ascii', 'ignore')
for char in input_str:
bytes_str = input_str.encode('ascii', 'ignore')
for char in bytes_str:
# print(char)
if char > 32: # ascii 32 is space character
crc_sum += char
Expand Down Expand Up @@ -663,7 +661,7 @@ def atoms(self, without_h: bool = False) -> Generator:
part = self.block.find_loop('_atom_site_disorder_group')
occ = self.block.find_loop('_atom_site_occupancy')
u_eq = self.block.find_loop('_atom_site_U_iso_or_equiv')
atom = namedtuple('Atom', ('label', 'type', 'x', 'y', 'z', 'part', 'occ', 'u_eq'))
atom = namedtuple('atom', ('label', 'type', 'x', 'y', 'z', 'part', 'occ', 'u_eq'))
for label, type, x, y, z, part, occ, u_eq in zip(labels, types, x, y, z,
part if part else ('0',) * len(labels),
occ if occ else ('1.000000',) * len(labels),
Expand All @@ -682,7 +680,7 @@ def atoms_fract(self) -> Generator:

@property
def atoms_orth(self) -> Generator:
atom = namedtuple('Atom', ('label', 'type', 'x', 'y', 'z', 'part', 'occ', 'u_eq'))
atom = namedtuple('atom', ('label', 'type', 'x', 'y', 'z', 'part', 'occ', 'u_eq'))
cell = self.atomic_struct.cell
for at in self.atomic_struct.sites:
x, y, z = at.orth(cell)
Expand Down Expand Up @@ -719,10 +717,10 @@ def disorder_present(self) -> bool:
return False

@property
def cell(self) -> Type['cell']:
def cell(self):
c = self.atomic_struct.cell
nt = namedtuple('cell', 'a, b, c, alpha, beta, gamma, volume')
return nt(c.a, c.b, c.c, c.alpha, c.beta, c.gamma, c.volume)
cell = namedtuple('cell', 'a, b, c, alpha, beta, gamma, volume')
return cell(c.a, c.b, c.c, c.alpha, c.beta, c.gamma, c.volume)

def ishydrogen(self, label: str) -> bool:
"""
Expand All @@ -749,14 +747,32 @@ def bonds(self, without_h: bool = False) -> Generator:
dist = self.block.find_loop('_geom_bond_distance')
symm = self.block.find_loop('_geom_bond_site_symmetry_2')
publ_loop = self.block.find_loop('_geom_bond_publ_flag')
bond = namedtuple('Bond', ('label1', 'label2', 'dist', 'symm'))
bond = namedtuple('bond', ('label1', 'label2', 'dist', 'symm'))
for label1, label2, dist, symm, publ in zip(label1, label2, dist, symm, publ_loop):
if without_h and (self.ishydrogen(label1) or self.ishydrogen(label2)) or \
self.yes_not_set(publ, self._has_publ_flag_set(publ_loop)):
continue
else:
yield bond(label1=label1, label2=label2, dist=dist, symm=self.checksymm(symm))

def bond_dist(self, pair: str) -> Union[float, None]:
for p in self.bonds():
if f'{p.label1}-{p.label2}' == pair:
return p.dist
return None

def angle(self, atoms: str) -> Union[float, None]:
for p in self.angles():
if f'{p.label1}-{p.label2}-{p.label3}' == atoms:
return p.angle_val
return None

def torsion(self, atoms: str) -> Union[float, None]:
for p in self.torsion_angles():
if f'{p.label1}-{p.label2}-{p.label3}-{p.label4}' == atoms:
return p.torsang
return None

def _has_publ_flag_set(self, publ_loop: gemmi.cif.Column) -> bool:
return any([x[0].lower() == 'y' for x in list(publ_loop) if x])

Expand All @@ -768,7 +784,7 @@ def angles(self, without_H: bool = False) -> Generator:
symm1 = self.block.find_loop('_geom_angle_site_symmetry_1')
symm2 = self.block.find_loop('_geom_angle_site_symmetry_3')
publ_loop = self.block.find_loop('_geom_angle_publ_flag')
angle = namedtuple('Angle', ('label1', 'label2', 'label3', 'angle_val', 'symm1', 'symm2'))
angle = namedtuple('angle', ('label1', 'label2', 'label3', 'angle_val', 'symm1', 'symm2'))
for label1, label2, label3, angle_val, symm1, symm2, publ in \
zip(label1, label2, label3, angle_val, symm1, symm2, publ_loop):
if without_H and (
Expand Down Expand Up @@ -876,10 +892,10 @@ def keys_with_essentials(self) -> Tuple[List[Tuple[str, str]], List[Tuple[str, s
for item in self.block:
if item.pair is not None:
key, value = item.pair
#if key.startswith('_shelx'):
# if key.startswith('_shelx'):
# # No-one should edit shelx values:
# continue
#if key == '_iucr_refine_instructions_details':
# if key == '_iucr_refine_instructions_details':
# continue
if self._is_centrokey(key):
continue
Expand Down Expand Up @@ -945,7 +961,8 @@ def test_hkl_checksum(self) -> bool:

c = CifContainer('test-data/DK_Zucker2_0m.cif')
c.load_this_block(len(c.doc) - 1)
pp(list(c.torsion_angles()))
# pp(list(c.torsion_angles()))
print(c.bond_dist('C1-C2'))
# pp(c.hkl_extra_info)
# print(c.hkl_file)
# print(c.hkl_as_cif)
Expand Down
3 changes: 3 additions & 0 deletions finalcif/gui/dialogs.py
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,9 @@ def show_general_warning(parent, warn_text: str = '', info_text: str = '', windo
"""
if not warn_text:
return None
if "PYTEST_CURRENT_TEST" in os.environ:
print(f'DBG> Running inside a pytest -> not showing error message:\n{warn_text}\n{info_text}')
return None
box = QMessageBox(parent=parent)
box.setTextFormat(Qt.AutoText)
box.setWindowTitle(window_title)
Expand Down
7 changes: 5 additions & 2 deletions finalcif/report/report_text.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,11 @@

import gemmi
from docx import Document
from docx.oxml.xmlchemy import BaseOxmlElement
from docx.text.paragraph import Paragraph
from docx.text.run import Run
from lxml import etree
from lxml.etree import XSLTAccessControl
from shelxfile.atoms.atoms import Atom as SHXAtom

from finalcif.app_path import application_path
Expand All @@ -19,11 +21,12 @@
from finalcif.tools.misc import protected_space, angstrom, zero_width_space, remove_line_endings, flatten


def math_to_word(eq: str) -> str:
def math_to_word(eq: str) -> BaseOxmlElement:
"""Transform a sympy equation to be printed in word document."""
tree = etree.fromstring(eq)
xslt = etree.parse(os.path.join(application_path, 'template/mathml2omml.xsl'))
transform = etree.XSLT(xslt)
acess_control = XSLTAccessControl(read_network=False, write_network=False)
transform = etree.XSLT(xslt, access_control=acess_control)
new_dom = transform(tree)
return new_dom.getroot()

Expand Down
Loading