Repository: duartegroup/autodE (python)

Issue:
Q-Chem Memory Allocation
**Describe the bug**
Memory allocation is not set for Q-Chem jobs, so defaults to (I believe) 1 GB per process. For memory-intensive jobs, e.g. TS Hessian calculations, Q-Chem may crash due to insufficient memory.

**To Reproduce**
The following input script threw a memory error in the Q-Chem output file for the Hessian job when attempting to find a TS

> `import autode as ade`
> 
> `ade.Config.n_cores = 16`
> `ade.Config.lcode = 'XTB'`
> `ade.Config.hcode = 'QChem'`
> 
> `kwds = ade.Config.QChem.keywords`
> `kwds.opt.max_opt_cycles = 200`
> `kwds.opt_ts.max_opt_cycles = 200`
> 
> `if __name__ == "__main__":`
> 
> `    sm = ade.Reactant(name='sm',smiles='CC(O[O])COOC')`
> `    p1 = ade.Product(name='p1', smiles='CC1COOO1')`
> `    p2 = ade.Product(name='p2', smiles='[O]C')`
> 
> `    reaction = ade.Reaction(sm, p1, p2, name='rxn1_step1')`
> `    reaction.calculate_reaction_profile()`

**Expected behavior**
Memory is allocated in the same way as Gaussian, so a `mem_total` rem variable with the number of MB _per process_ should be included by default in the %rem block.

**Environment**
 - Operating System: Linux 
 - Python version: 3.10
 - autodE version: 1.3.5


Pull request description:
Fix QChem not using max_core
## Resolves #273 

Hopefully this is correct!

***

## Checklist

* [x] The changes include an associated explanation of how/why
* [ ] Test pass
* [x] Documentation has been updated
* [x] Changelog has been updated


Patch:
diff --git a/autode/conformers/conformers.py b/autode/conformers/conformers.py
index 7b668e6..9992c08 100644
--- a/autode/conformers/conformers.py
+++ b/autode/conformers/conformers.py
@@ -1,6 +1,7 @@
 import numpy as np
 from typing import Optional, Union
 from rdkit import Chem
+
 from autode.values import Distance, Energy
 from autode.atoms import Atom, Atoms
 from autode.config import Config
@@ -8,6 +9,7 @@ from autode.mol_graphs import make_graph, is_isomorphic
 from autode.geom import calc_heavy_atom_rmsd
 from autode.log import logger
 from autode.utils import ProcessPool
+from autode.exceptions import NoConformers
 
 
 def _calc_conformer(conformer, calc_type, method, keywords, n_cores=1):
@@ -61,9 +63,7 @@ class Conformers(list):
         """
 
         if remove_no_energy:
-            for idx in reversed(range(len(self))):  # Enumerate backwards
-                if self[idx].energy is None:
-                    del self[idx]
+            self.remove_no_energy()
 
         self.prune_on_energy(e_tol=e_tol, n_sigma=n_sigma)
         self.prune_on_rmsd(rmsd_tol=rmsd_tol)
@@ -234,6 +234,21 @@ class Conformers(list):
         logger.info(f"Pruned on connectivity {n_prev_confs} -> {len(self)}")
         return None
 
+    def remove_no_energy(self) -> None:
+        """Remove all conformers from this list that do not have an energy"""
+        n_conformers_before_remove = len(self)
+
+        for idx in reversed(range(len(self))):  # Enumerate backwards
+            if self[idx].energy is None:
+                del self[idx]
+
+        n_conformers = len(self)
+        if n_conformers == 0 and n_conformers != n_conformers_before_remove:
+            raise NoConformers(
+                f"Removed all the conformers "
+                f"{n_conformers_before_remove} -> 0"
+            )
+
     def _parallel_calc(self, calc_type, method, keywords):
         """
         Run a set of calculations (single point energy evaluations or geometry
diff --git a/autode/wrappers/QChem.py b/autode/wrappers/QChem.py
index fc10075..bdb6f9c 100644
--- a/autode/wrappers/QChem.py
+++ b/autode/wrappers/QChem.py
@@ -517,6 +517,9 @@ class QChem(autode.wrappers.methods.ExternalMethodOEGH):
                 self.write("symmetry False")
                 self.write("sym_ignore True")
 
+            total_memory_in_mb = int(Config.max_core.to("MB") * calc.n_cores)
+            self.write(f"mem_total {total_memory_in_mb}")
+
             self.write("$end\n")
 
             return None
diff --git a/doc/changelog.rst b/doc/changelog.rst
index 99f5547..98254ee 100644
--- a/doc/changelog.rst
+++ b/doc/changelog.rst
@@ -28,6 +28,7 @@ Bug Fixes
 *********
 - Fixes pickling issue with :code:`autode.config.Config` on Windows and in multiprocessing spawn for Linux/macOS
 - Fixes RFO Hessian update bug
+- Fixes QChem not using :code:`Config.max_core`
 
 
 1.3.5


Test patch:
diff --git a/tests/test_conformers.py b/tests/test_conformers.py
index dd901aa..75f944f 100644
--- a/tests/test_conformers.py
+++ b/tests/test_conformers.py
@@ -2,6 +2,7 @@ from autode.atoms import Atom, Atoms
 from autode.species import Molecule, NCIComplex
 from autode.conformers import Conformer, Conformers
 from autode.conformers.conformers import _calc_conformer
+from autode.exceptions import NoConformers
 from autode.wrappers.ORCA import ORCA
 from autode.wrappers.XTB import XTB
 from autode.config import Config
@@ -177,11 +178,14 @@ def test_confs_energy_pruning3():
 
 def test_confs_no_energy_pruning():
     # Check that if energies are unassigned then conformers are removed
+    conf0 = Conformer(atoms=[Atom("H")])
+    conf1 = conf0.copy()
+    conf1.energy = -0.5
 
-    confs = Conformers([Conformer(atoms=[Atom("H")])])
+    confs = Conformers([conf0, conf1])
     confs.prune(remove_no_energy=True)
 
-    assert len(confs) == 0
+    assert len(confs) == 1
 
 
 def test_confs_rmsd_pruning1():
@@ -382,3 +386,21 @@ def test_conformer_coordinate_setting_with_different_atomic_attr():
     # But cannot set atoms with a different label (e.g. atomic symbol)
     with pytest.raises(ValueError):
         conf.atoms = Atoms([Atom("H")])
+
+
+def test_pruning_conformers_without_energy_raises():
+
+    conformers = Conformers(
+        [Conformer(atoms=[Atom("H")]), Conformer(atoms=[Atom("H")])]
+    )
+
+    assert sum(c.energy is None for c in conformers) == 2
+
+    with pytest.raises(NoConformers):
+        conformers.prune(remove_no_energy=True)
+
+
+def test_pruning_no_energy_with_no_conformers_is_possible():
+
+    conformers = Conformers()
+    conformers.remove_no_energy()
diff --git a/tests/test_wrappers/test_qchem.py b/tests/test_wrappers/test_qchem.py
index 176c7b1..5340bd5 100644
--- a/tests/test_wrappers/test_qchem.py
+++ b/tests/test_wrappers/test_qchem.py
@@ -4,12 +4,13 @@ import numpy as np
 from autode.point_charges import PointCharge
 from autode.wrappers.QChem import QChem
 from autode.calculations import Calculation
+from autode.values import Allocation
 from autode.atoms import Atom
 from autode.config import Config
 from autode.wrappers.keywords import cpcm, pbe0, def2svp
 from autode.species.molecule import Molecule
 from autode.wrappers.keywords import SinglePointKeywords, OptKeywords
-from autode.utils import work_in_tmp_dir
+from autode.utils import work_in_tmp_dir, temporary_config
 from autode.exceptions import CalculationException
 from ..testutils import work_in_zipped_dir
 
@@ -27,6 +28,7 @@ def _blank_calc(name="test"):
         molecule=Molecule(atoms=[Atom("H")], mult=2),
         method=method,
         keywords=SinglePointKeywords(),
+        n_cores=Config.n_cores,
     )
 
     return calc
@@ -172,6 +174,7 @@ def test_simple_input_generation():
         "$rem\n"
         "method pbe0\n"
         "basis def2-SVP\n"
+        "mem_total 4000\n"
         "$end\n"
     )
 
@@ -576,3 +579,18 @@ def test_coordinate_extract_from_single_atom_calculation():
     calc.molecule = Molecule(atoms=[Atom("H", x=0, y=0, z=0)])
 
     assert np.allclose(QChem().coordinates_from(calc), np.zeros(3))
+
+
+@work_in_tmp_dir()
+def test_total_memory_is_printed_in_input_file():
+
+    with temporary_config():
+        Config.n_cores = 3
+        Config.max_core = Allocation(900, "MB")
+        calc = _blank_calc()
+        calc.input.keywords = method.keywords.sp
+        calc.generate_input()
+
+    input_lines = open(calc.input.filename, "r").readlines()
+
+    assert sum("mem_total 2700" in line for line in input_lines) == 1