diff --git a/doped/chemical_potentials.py b/doped/chemical_potentials.py
index 88789b6a..6fb2f778 100644
--- a/doped/chemical_potentials.py
+++ b/doped/chemical_potentials.py
@@ -1509,6 +1509,117 @@ def cplap_input(self, dependent_variable=None, filename="input.dat"):
print(int(v), k, end=" ")
print(f"{i['formation_energy']} # num_atoms, element, formation_energy")
+ def to_LaTeX_table(self, columns=1, sort_by_energy=False, prune_polymorphs=True):
+ """
+ A very simple function to print out the competing phase formation
+ energies in a LaTeX table format. Needs the mhchem package to work and
+ does *not* use the booktabs package -- change hline to toprule, midrule
+ and bottomrule if you want to use booktabs style.
+
+ Args:
+ columns (int):
+ Number of columns to print out the table in; either 1 or 2
+ sort_by_energy (bool):
+ If True, sorts the table by formation energy (highest to lowest).
+ Default sorting by formula.
+ prune_polymorphs (bool):
+ Whether to only print out the lowest energy polymorphs for each composition.
+ Default is True.
+
+ Returns:
+ str: LaTeX table string
+ """
+ if columns not in [1, 2]:
+ raise ValueError("columns must be either 1 or 2")
+ # done in the pyscfermi report style
+ data = copy.deepcopy(self.data)
+
+ if any("kpoints" not in item for item in data):
+ raise (
+ ValueError(
+ "kpoints need to be present in data; run CompetingPhasesAnalyzer.from_vaspruns "
+ "instead of from_csv"
+ )
+ )
+
+ if prune_polymorphs: # only keep the lowest energy polymorphs
+ formation_energy_df = _calculate_formation_energies(data, self.elemental_energies)
+ indices = formation_energy_df.groupby("formula")["energy_per_atom"].idxmin()
+ pruned_df = formation_energy_df.loc[indices]
+ data = pruned_df.to_dict(orient="records")
+
+ if sort_by_energy:
+ data = sorted(data, key=lambda x: x["formation_energy"], reverse=True)
+ # moves the bulk composition to the top of the list
+ _move_dict_to_start(data, "formula", self.bulk_composition.reduced_formula)
+
+ string = "\\begin{table}[h]\n\\centering\n"
+ string += (
+ "\\caption{Formation energies ($\\Delta E_f$) per formula unit of \\ce{"
+ + self.bulk_composition.reduced_formula
+ + "} and all competing phases, with k-meshes used in calculations."
+ + ("}\n" if not prune_polymorphs else " Only the lowest energy polymorphs are included}\n")
+ )
+ string += "\\label{tab:competing_phase_formation_energies}\n"
+ if columns == 1:
+ string += "\\begin{tabular}{ccc}\n"
+ string += "\\hline\n"
+ string += "Formula & k-mesh & $\\Delta E_f$ (eV) \\\\ \\hline \n"
+ for i in data:
+ kpoints = i["kpoints"].split("x")
+ fe = i["formation_energy"]
+ string += (
+ "\\ce{"
+ + i["formula"]
+ + "} & "
+ + f"{kpoints[0]}$\\times${kpoints[1]}$\\times${kpoints[2]}"
+ " & " + f"{fe:.3f} \\\\ \n"
+ )
+
+ elif columns == 2:
+ string += "\\begin{tabular}{ccc|ccc}\n"
+ string += "\\hline\n"
+ string += (
+ "Formula & k-mesh & $\\Delta E_f$ (eV) & Formula & k-mesh & $\\Delta E_f$ (eV)\\\\ "
+ "\\hline \n"
+ )
+
+ mid = len(data) // 2
+ first_half = data[:mid]
+ last_half = data[mid:]
+
+ for i, j in zip(first_half, last_half):
+ kpoints = i["kpoints"].split("x")
+ fe = i["formation_energy"]
+ kpoints2 = j["kpoints"].split("x")
+ fe2 = j["formation_energy"]
+ string += (
+ "\\ce{"
+ + i["formula"]
+ + "} & "
+ + f"{kpoints[0]}$\\times${kpoints[1]}$\\times${kpoints[2]}"
+ " & "
+ + f"{fe:.3f} & "
+ + "\\ce{"
+ + j["formula"]
+ + "} & "
+ + f"{kpoints2[0]}$\\times${kpoints2[1]}$\\times${kpoints2[2]}"
+ " & " + f"{fe2:.3f} \\\\ \n"
+ )
+
+ string += "\\hline\n"
+ string += "\\end{tabular}\n"
+ string += "\\end{table}"
+
+ return string
+
+
+def _move_dict_to_start(data, key, value):
+ for index, item in enumerate(data):
+ if key in item and item[key] == value:
+ data.insert(0, data.pop(index))
+ return
+
def combine_extrinsic(first, second, extrinsic_species):
# TODO: Can we just integrate this to `CompetingPhaseAnalyzer`, so you just pass in a list of
diff --git a/examples/dope_chemical_potentials.ipynb b/examples/dope_chemical_potentials.ipynb
index 47bad80b..72af34fd 100644
--- a/examples/dope_chemical_potentials.ipynb
+++ b/examples/dope_chemical_potentials.ipynb
@@ -377,7 +377,9 @@
"\n",
"You may want to change the default `ENCUT` (350 eV) or k-point densities that the convergence tests span (5 - 60 kpoints/Å3 for semiconductors & insulators and 40 - 120 kpoints/Å3 for metals in steps of 5 kpoints/Å3). Note that `ISMEAR = -5` is used for metals by default (better kpoint convergence for energies but should not be used during metal geometry relaxations) and k-point convergence testing is not required for molecules (Γ-point sampling is sufficient).\n",
"\n",
- "Note that `doped` generates \"molecule in a box\" structures for the gaseous elemental phases H2, O2, N2, F2 and Cl2. The molecule is placed in a 30 Å x 30 Å x 30 Å box, and relaxed with Γ-point-only k-point sampling.\n",
+ "Note that `doped` generates \"molecule in a box\" structures for the gaseous elemental phases\n",
+ "H2, O2, N2, F2 and Cl2. The molecule is placed in\n",
+ " a slightly-symmetry-broken (to avoid metastable electronic solutions) 30 Å cuboid box, and relaxed with Γ-point-only k-point sampling.\n",
"\n",
"The kpoints convergence calculations are set up with:"
]
@@ -525,7 +527,11 @@
"id": "7e57cb66",
"metadata": {},
"source": [
- "Remember that the final `ENCUT` used for the energy calculations should be the same as for your host material & defects, and that you may still need to account for Pulay stress by increasing `ENCUT` for the geometry relaxations (usual rule of thumb being 1.3*converged `ENCUT`) or re-relaxing each structure until the volume change is minimal (roughly <0.3%)."
+ "Remember that the final `ENCUT` used for the energy calculations should be the same as for your host\n",
+ "material & defects, and that you may still need to account for Pulay stress by increasing `ENCUT` for\n",
+ "the geometry relaxations (a typical rule of thumb being 1.3*converged `ENCUT`) or re-relaxing each\n",
+ "structure until the volume change is minimal (roughly <0.3%). This is not the case for the\n",
+ "molecule-in-a-box competing phases however, due to the large simulation box size and fixed volume."
]
},
{
@@ -730,16 +736,16 @@
},
{
"cell_type": "code",
- "execution_count": 5,
+ "execution_count": 1,
"id": "e273f8fa",
"metadata": {
- "ExecuteTime": {
- "end_time": "2023-08-21T11:54:14.575272Z",
- "start_time": "2023-08-21T11:54:14.548811Z"
- },
"collapsed": false,
"jupyter": {
"outputs_hidden": false
+ },
+ "ExecuteTime": {
+ "end_time": "2023-08-23T21:18:44.960159Z",
+ "start_time": "2023-08-23T21:18:39.250023Z"
}
},
"outputs": [],
@@ -750,12 +756,12 @@
},
{
"cell_type": "code",
- "execution_count": 6,
+ "execution_count": 2,
"id": "dd65cb61-bd10-41c8-bf6d-15398841f226",
"metadata": {
"ExecuteTime": {
- "end_time": "2023-08-21T11:54:16.335532Z",
- "start_time": "2023-08-21T11:54:15.916656Z"
+ "end_time": "2023-08-23T21:18:45.128309Z",
+ "start_time": "2023-08-23T21:18:44.961171Z"
}
},
"outputs": [
@@ -789,16 +795,16 @@
},
{
"cell_type": "code",
- "execution_count": 23,
+ "execution_count": 3,
"id": "a3d1b363",
"metadata": {
- "ExecuteTime": {
- "end_time": "2023-08-21T11:51:09.266661Z",
- "start_time": "2023-08-21T11:51:09.052205Z"
- },
"collapsed": false,
"jupyter": {
"outputs_hidden": false
+ },
+ "ExecuteTime": {
+ "end_time": "2023-08-23T21:18:45.377891Z",
+ "start_time": "2023-08-23T21:18:45.129641Z"
}
},
"outputs": [
@@ -844,6 +850,73 @@
"cpa.from_vaspruns(all_paths)"
]
},
+ {
+ "cell_type": "markdown",
+ "source": [
+ "### Print Formation Energies as a `LaTeX` Table\n",
+ "You may want to print out the parsed calculation results for your competing phases as a LaTeX table, to\n",
+ "be included in supplementary information of journal articles or in theses, to aid open-science and reproducibility.\n",
+ "The formation energies are automatically saved to a `csv` file, which can be converted to LaTeX (see\n",
+ "https://www.tablesgenerator.com/latex_tables) or\n",
+ "Word formats, but you can also print out a nicely-formatted LaTeX table using the `to_LaTeX_table()`\n",
+ "method as shown belo –\n",
+ "either as a one- or two-column table; ordered alphabetially or in descending order of energies."
+ ],
+ "metadata": {
+ "collapsed": false
+ }
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 4,
+ "outputs": [
+ {
+ "name": "stdout",
+ "output_type": "stream",
+ "text": [
+ "\\begin{table}[h]\n",
+ "\\centering\n",
+ "\\caption{Formation energies ($\\Delta E_f$) per formula unit of \\ce{ZrO2} and all competing phases, with k-meshes used in calculations. Only the lowest energy polymorphs are included}\n",
+ "\\label{tab:competing_phase_formation_energies}\n",
+ "\\begin{tabular}{ccc}\n",
+ "\\hline\n",
+ "Formula & k-mesh & $\\Delta E_f$ (eV) \\\\ \\hline \n",
+ "\\ce{ZrO2} & 3$\\times$3$\\times$3 & -10.975 \\\\ \n",
+ "\\ce{O2} & 2$\\times$2$\\times$2 & 0.000 \\\\ \n",
+ "\\ce{Zr} & 9$\\times$9$\\times$5 & 0.000 \\\\ \n",
+ "\\ce{Zr2O} & 5$\\times$5$\\times$2 & -5.729 \\\\ \n",
+ "\\ce{Zr3O} & 5$\\times$5$\\times$5 & -5.987 \\\\ \n",
+ "\\hline\n",
+ "\\end{tabular}\n",
+ "\\end{table}\n"
+ ]
+ }
+ ],
+ "source": [
+ "table = cpa.to_LaTeX_table()\n",
+ "print(table)"
+ ],
+ "metadata": {
+ "collapsed": false,
+ "ExecuteTime": {
+ "end_time": "2023-08-23T21:18:45.385574Z",
+ "start_time": "2023-08-23T21:18:45.382991Z"
+ }
+ }
+ },
+ {
+ "cell_type": "markdown",
+ "source": [
+ "```{note}\n",
+ "The table does not use `booktabs` by default but you can change the horizontal lines (`hline`) to\n",
+ "toprule, midrule and bottomrule to get the desired style effect. The output also relies on using the\n",
+ "`\\ce{}` command from the `mhchem` LaTeX package to get the correct formatting for the chemical formulae.\n",
+ "```"
+ ],
+ "metadata": {
+ "collapsed": false
+ }
+ },
{
"attachments": {},
"cell_type": "markdown",
diff --git a/tests/test_chemical_potentials.py b/tests/test_chemical_potentials.py
index c8f91f87..bcbd0dc7 100644
--- a/tests/test_chemical_potentials.py
+++ b/tests/test_chemical_potentials.py
@@ -217,7 +217,46 @@ def test_cplap_input(self):
assert (
"1 Zr 2 O -10.951109995000005\n" not in contents
) # shouldn't be included as is a higher energy polymorph of the bulk phase
- assert not any("2 Zr 1 O" in i for i in contents) # non-bordering phase
+ assert all("2 Zr 1 O" not in i for i in contents) # non-bordering phase
+
+ def test_latex_table(self):
+ cpa = chemical_potentials.CompetingPhasesAnalyzer(self.stable_system)
+ path = self.path / "ZrO2"
+ cpa.from_vaspruns(path=path, folder="relax", csv_fname=self.csv_path)
+
+ with self.assertRaises(ValueError):
+ cpa.to_LaTeX_table(columns="M")
+
+ string = cpa.to_LaTeX_table(columns=1)
+ assert (
+ string[28:209]
+ == "\\caption{Formation energies ($\\Delta E_f$) per formula unit of \\ce{ZrO2} and all "
+ "competing phases, with k-meshes used in calculations. Only the lowest energy polymorphs "
+ "are included"
+ )
+ assert len(string) == 586
+ assert string.split("hline")[1] == "\nFormula & k-mesh & $\\Delta E_f$ (eV) \\\\ \\"
+ assert string.split("hline")[2][2:45] == "\\ce{ZrO2} & 3$\\times$3$\\times$3 & -10.975 \\"
+
+ string = cpa.to_LaTeX_table(columns=2)
+ assert (
+ string[28:209]
+ == "\\caption{Formation energies ($\\Delta E_f$) per formula unit of \\ce{ZrO2} and all "
+ "competing phases, with k-meshes used in calculations. Only the lowest energy polymorphs "
+ "are included"
+ )
+ assert (
+ string.split("hline")[1]
+ == "\nFormula & k-mesh & $\\Delta E_f$ (eV) & Formula & k-mesh & $\\Delta E_f$ (eV)\\\\ \\"
+ )
+
+ assert string.split("hline")[2][2:45] == "\\ce{ZrO2} & 3$\\times$3$\\times$3 & -10.975 &"
+ assert len(string) == 579
+
+ cpa_csv = chemical_potentials.CompetingPhasesAnalyzer(self.stable_system)
+ cpa_csv.from_csv(self.csv_path)
+ with self.assertRaises(ValueError):
+ cpa_csv.to_LaTeX_table(columns=2)
class BoxedMoleculesTestCase(unittest.TestCase):