diff --git a/TCutility/__init__.py b/TCutility/__init__.py index 53bd5da4..6d07545b 100644 --- a/TCutility/__init__.py +++ b/TCutility/__init__.py @@ -14,4 +14,4 @@ def ensure_2d(x, transposed=False): return x -from TCutility import analysis, results, constants, log, molecule # noqa: F401, E402 +from TCutility import analysis, results, constants, log, molecule, formula # noqa: F401, E402 diff --git a/TCutility/formula.py b/TCutility/formula.py new file mode 100644 index 00000000..f3d5ca1c --- /dev/null +++ b/TCutility/formula.py @@ -0,0 +1,43 @@ +def molecule(molstring: str, mode: str = 'html') -> str: + ''' + Parse and return a string containing a molecular formula that will show up properly in LaTeX or HTML. + + Args: + molstring: the string that contains the molecular formula to be parsed. It can be either single molecule or a reaction. Molecules should be separated by '+' or '->'. + mode: the formatter to convert the string to. Should be 'html' or 'latex'. + + Returns: + A string that is formatted to be rendered nicely in either HTML or LaTeX. + ''' + # to take care of plus-signs used to denote reactions we have to first split + # the molstring into its parts. + for part in molstring.split(): + # if part is only a plus-sign we skip this part. This is only true when the plus-sign + # is used to denote a reaction + if part in ['+', '->']: + continue + + # parse the part + partret = part + # numbers should be subscript + for num in '0123456789': + if mode == 'latex': + partret = partret.replace(num, f'_{num}') + if mode == 'html': + partret = partret.replace(num, f'{num}') + + # signs should be superscript + for sign in '+-': + # negative charges should be denoted by em dash and not a normal dash + if mode == 'latex': + partret = partret.replace(sign, f'^{sign.replace("-", "—")}') + if mode == 'html': + partret = partret.replace(sign, f'{sign.replace("-", "—")}') + # replace the part in the original string + molstring = molstring.replace(part, partret) + + return molstring + + +if __name__ == '__main__': + print(molecule('F- + CH3Cl', 'html')) diff --git a/TCutility/report.py b/TCutility/report.py new file mode 100644 index 00000000..966e8bb3 --- /dev/null +++ b/TCutility/report.py @@ -0,0 +1,77 @@ +from TCutility import results +import docx +from htmldocx import HtmlToDocx + + +class SI: + def __init__(self, path: str): + ''' + Class for creating supporting information (SI) files in Microsoft Word. + + Args: + path: the location of the Word file. Does not have to have a file-extension. + append_mode: whether to append to or overwrite the file. + ''' + + self.path = path.removesuffix('.docx') + '.docx' + self.doc = docx.Document() + + # set the font to Calibri + self.doc.styles['Normal'].font.name = 'Calibri' + + def __enter__(self): + return self + + def __exit__(self, *args, **kwargs): + self.doc.save(self.path) + + def add_xyz(self, obj: str or dict, title: str): + ''' + Add the coordinates and information about a calculation to the SI. + It will add the electronic bond energy, Gibb's free energy, enthalpy and imaginary mode, as well as the coordinates of the molecule. + + Args: + obj: a string specifying a calculation directory or a `TCutility.results.Result` object from a calculation. + title: title to be written before the coordinates and information. + ''' + if isinstance(obj, str): + obj = results.read(obj) + + # title is always bold + s = f'{title}
' + + parser = HtmlToDocx() + + # add electronic energy. E should be bold and italics. Unit will be kcal mol^-1 + E = str(round(obj.properties.energy.bond, 1)).replace('-', '—') + s += f'E = {E} kcal mol—1
' + + # add Gibbs and enthalpy if we have them + if obj.properties.energy.gibbs: + G = str(round(obj.properties.energy.gibbs, 1)).replace('-', '—') + s += f'G = {G} kcal mol—1
' + if obj.properties.energy.enthalpy: + H = str(round(obj.properties.energy.enthalpy, 1)).replace('-', '—') + s += f'H = {H} kcal mol—1
' + + # add imaginary frequency if we have one + if obj.properties.vibrations: + if obj.properties.vibrations.number_of_imag_modes == 1: + freq = abs(round(obj.properties.vibrations.frequencies[0])) + s += f'νimag = {freq}i cm—1' + + # remove trailing line breaks + s = s.removesuffix('
') + + # coords should be written in mono-type font with 8 decimals and 4 spaces between each coordinate + s += '
'
+		for atom in obj.molecule.output:
+			s += f'{atom.symbol:2}    {atom.coords[0]: .8f}    {atom.coords[1]: .8f}    {atom.coords[2]: .8f}
' + s += '
' + parser.add_html_to_document(s, self.doc) + + def add_heading(self, text: str, level: int = 1): + ''' + Add a heading to the file. This method has the same arguments and functionality as docx.Document.add_heading. + ''' + self.doc.add_heading(text, level) diff --git a/examples/write_SI.py b/examples/write_SI.py new file mode 100644 index 00000000..05b1e830 --- /dev/null +++ b/examples/write_SI.py @@ -0,0 +1,10 @@ +from TCutility import report, formula +import os + +j = os.path.join + +pwd = os.path.split(__file__)[0] +with report.SI('test.docx') as si: + si.add_heading('Test molecules:') + si.add_xyz(j(pwd, '..', 'test', 'fixtures', 'level_of_theory', 'M06_2X'), 'Cyclo-octatriene') + si.add_xyz(j(pwd, '..', 'test', 'fixtures', 'ethanol'), formula.molecule('C2H5OH')) diff --git a/test/test_formula.py b/test/test_formula.py new file mode 100644 index 00000000..830e85e7 --- /dev/null +++ b/test/test_formula.py @@ -0,0 +1,27 @@ +from TCutility import formula + + +def test_single_molecule(): + assert formula.molecule('F-', 'html') == 'F' + + +def test_single_molecule2(): + assert formula.molecule('CH3Cl', 'html') == 'CH3Cl' + + +def test_single_molecule3(): + assert formula.molecule('C6H12O6', 'html') == 'C6H12O6' + + +def test_reaction(): + assert formula.molecule('F- + CH3Cl', 'html') == 'F + CH3Cl' + + +def test_reaction2(): + assert formula.molecule('F- + CH3Cl -> CH3F + Cl-', 'html') == 'F + CH3Cl -> CH3F + Cl' + + +if __name__ == '__main__': + import pytest + + pytest.main()