Open
Description
Describe the bug
For some inputs, xtb-python produces impossible outputs which are not handled as errors.
To Reproduce
This is a script to demonstrate unhandled xTB errors for simple cubes.
The command line arguments are atomic number and grid spacing (in Angstroms) for a cube of the given element.
Metals tend to be more unstable.
import sys
import numpy as np
from scipy import constants
import xtb
from xtb.interface import Calculator, XTBException
from xtb.libxtb import VERBOSITY_MUTED
from xtb.utils import get_method
ANGSTROM_IN_BOHR = constants.physical_constants['Bohr radius'][0] * 1.0e10
def run_xtb(atomic_numbers, xyz, net_charge, n_unpaired_electrons, max_iter=250, conv=1.0, method="GFN2-xTB"):
'''
Run a single xtb job and return the result.
Throws XTBException for errors or unrealistic results.
Arguments:
atomic_numbers (np.array): integer element labels for all atoms
xyz (np.array): Nx3 array of coordinates for all atoms, in Angstroms
n_unpaired_electrons (int): number of unpaired electrons in the system
max_iter (int): max number of SCF iterations before giving up
conv (float): SCF cutoffs, smaller means tighter convergence
Returns:
E_xtb: total system energy in Hartree
gradient_xtb: dE/dxyz for each atom, in Hartree/Angstrom
dipole_xtb: system dipole in electron-Angstrom (not available with PBC)
qs_xtb: Mulliken atomic charges, in elementary charge units
'''
calc = Calculator(get_method(method), atomic_numbers, xyz / ANGSTROM_IN_BOHR,
charge=net_charge, uhf=n_unpaired_electrons)
calc.set_verbosity(VERBOSITY_MUTED)
calc.set_accuracy(conv) # default = 1.0, smaller = tighter convergence
calc.set_max_iterations(max_iter) # default = 250
result = calc.singlepoint()
if result.check() != 0:
raise XTBException(result.get_error())
E_xtb = result.get_energy()
if np.isnan(E_xtb): # yet another convergence issue
raise XTBException('Energy is NaN')
qs_xtb = result.get_charges()
if abs(net_charge - np.sum(qs_xtb)) > 1e-5:
raise XTBException('Bad net charge: %d vs %f' % (net_charge, np.sum(qs_xtb)))
if np.max(np.abs(qs_xtb)) > 4.0:
raise XTBException('Very implausible partial charge: %f' % np.max(np.abs(qs_xtb)))
dipole_xtb = result.get_dipole() * ANGSTROM_IN_BOHR # from electron-Bohr to electron-Angstrom
gradient_xtb = result.get_gradient() / ANGSTROM_IN_BOHR # from Hartree/Bohr to Hartree/Angstrom
# units: Hartree, Hartree/Angstrom, electron-Angstrom, electrons
return E_xtb, gradient_xtb, dipole_xtb, qs_xtb
def test_metal_cubes(atomic_number, default_scale=3.0, cube_size=3):
print('Cube of atomic number', atomic_number, 'with', cube_size**3, 'atoms, default interatomic r =', default_scale)
for scale in np.arange(0.2, 1.2, 0.01):
scale *= default_scale
xyz = []
for i in range(cube_size):
for j in range(cube_size):
for k in range(cube_size):
xyz.append((i, j, k))
xyz = np.array(xyz, dtype=np.float64) * scale
atomic_numbers = np.array([atomic_number] * len(xyz))
q = np.sum(atomic_numbers) % 2 # if odd n electrons, remove 1
try:
E_xtb, gradient_xtb, dipole_xtb, qs_xtb = run_xtb(atomic_numbers, xyz, q, 0)
# print('r = %.2f' % scale, 'E_xtb =', E_xtb)
except XTBException as ex:
print('r = %.2f' % scale, 'Failed,', ex)
print('xTB version', xtb.__version__)
atomic_number, default_scale = int(sys.argv[1]), float(sys.argv[2])
test_metal_cubes(atomic_number, default_scale)
Example input:
python xtb_huge_error.py 3 2.7
Output:
xTB version 20.1
Cube of atomic number 3 with 27 atoms, default interatomic r = 2.7
r = 0.92 Failed, Very implausible partial charge: 209.337257
r = 0.95 Failed, Very implausible partial charge: 96.060772
r = 0.97 Failed, Very implausible partial charge: 66.404307
r = 1.00 Failed, Very implausible partial charge: 163.766930
r = 1.05 Failed, Very implausible partial charge: 145.753860
r = 1.08 Failed, Bad net charge: 1 vs 1.000011
r = 1.13 Failed, Very implausible partial charge: 29.071110
r = 1.19 Failed, Very implausible partial charge: 257.358086
r = 1.22 Failed, Very implausible partial charge: 273.118857
r = 1.24 Failed, Bad net charge: 1 vs 0.412483
r = 1.27 Failed, Very implausible partial charge: 115.909595
r = 1.30 Failed, Very implausible partial charge: 19332.830109
r = 1.35 Failed, Very implausible partial charge: 5.019581
r = 1.38 Failed, Very implausible partial charge: 33.592007
r = 1.40 Failed, Very implausible partial charge: 33.003970
r = 1.43 Failed, Very implausible partial charge: 66.802335
r = 1.46 Failed, Bad net charge: 1 vs 1.000011
r = 1.49 Failed, Very implausible partial charge: 91.379054
r = 1.51 Failed, Bad net charge: 1 vs 1471240208384.000000
r = 1.76 Failed, Energy is NaN
r = 2.11 Failed, Energy is NaN
Expected behaviour
These error conditions should be handled within xtb or xtb-python, rather than requiring user checks. Probably the most alarming error here is Bad net charge: 1 vs 0.412483
, where a significant fraction of an electron has appeared from nowhere.
Additional context
GFN1-xTB and GFN0-xTB don't have the same problems with bad net charges or implausible partial charges, but they can still give NaN for energy.