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}' + 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()
' + s += '