Skip to content

Commit 0fe821f

Browse files
authored
Merge pull request #11 from sofroniewn/optimal_geometry
Add optimal_geometry method
2 parents 864fbe4 + a678f31 commit 0fe821f

File tree

6 files changed

+170
-4
lines changed

6 files changed

+170
-4
lines changed

dqc/api/properties.py

+46-1
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,8 @@
1313
convert_raman_ints
1414

1515
__all__ = ["hessian_pos", "vibration", "edipole", "equadrupole", "is_orb_min",
16-
"lowest_eival_orb_hessian", "ir_spectrum", "raman_spectrum"]
16+
"lowest_eival_orb_hessian", "ir_spectrum", "raman_spectrum",
17+
"optimal_geometry"]
1718

1819
# This file contains functions to calculate the perturbation properties of systems.
1920

@@ -317,6 +318,28 @@ def is_orb_min(qc: BaseQCCalc, threshold: float = -1e-3) -> bool:
317318
eival = lowest_eival_orb_hessian(qc)
318319
return bool(torch.all(eival > threshold))
319320

321+
def optimal_geometry(qc: BaseQCCalc, length_unit: Optional[str] = None) -> torch.Tensor:
322+
"""
323+
Compute the optimal atomic positions of the system.
324+
325+
Arguments
326+
---------
327+
qc: BaseQCCalc
328+
Quantum Chemistry calculation that has run.
329+
330+
length_unit: str or None
331+
The returned unit. If ``None``, returns in atomic unit.
332+
333+
Returns
334+
-------
335+
torch.Tensor
336+
Tensor with shape ``(natoms, ndim)`` represents the position
337+
of atoms at the optimal geometry.
338+
"""
339+
atompos = _optimal_geometry(qc)
340+
atompos = convert_length(atompos, to_unit=length_unit)
341+
return atompos
342+
320343
@memoize_method
321344
def _hessian_pos(qc: BaseQCCalc) -> torch.Tensor:
322345
# calculate the hessian in atomic unit
@@ -460,6 +483,28 @@ def _equadrupole(qc: BaseQCCalc) -> torch.Tensor:
460483

461484
return quadrupole + ion_quadrupole
462485

486+
@memoize_method
487+
def _optimal_geometry(qc: BaseQCCalc) -> torch.Tensor:
488+
# calculate the optimal geometry
489+
system = qc.get_system()
490+
atompos = system.atompos
491+
492+
# check if the atompos requires grad
493+
_check_differentiability(atompos, "atom positions", "optimal geometry")
494+
495+
# get the energy for a given geometry
496+
def _get_energy(atompos: torch.Tensor) -> torch.Tensor:
497+
new_system = system.make_copy(moldesc=(system.atomzs, atompos))
498+
new_qc = qc.__class__(new_system).run()
499+
ene = new_qc.energy() # calculate the energy
500+
return ene
501+
502+
# get the minimal enery position
503+
minpos = xitorch.optimize.minimize(_get_energy, atompos, method="gd", maxiter=200,
504+
step=1e-2)
505+
506+
return minpos
507+
463508
########### helper functions ###########
464509

465510
def _jac(a: torch.Tensor, b: torch.Tensor, create_graph: Optional[bool] = None,

dqc/system/base_system.py

+9-1
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,14 @@ def requires_grid(self) -> bool:
8080
def getparamnames(self, methodname: str, prefix: str = "") -> List[str]:
8181
pass
8282

83+
@abstractmethod
84+
def make_copy(self, **kwargs) -> BaseSystem:
85+
"""
86+
Returns a copy of the system identical to the orginal except for new
87+
parameters set in the kwargs.
88+
"""
89+
pass
90+
8391
####################### system properties #######################
8492
@abstractproperty
8593
def atompos(self) -> torch.Tensor:
@@ -129,4 +137,4 @@ def efield(self) -> Optional[Tuple[torch.Tensor, ...]]:
129137
Returns the external electric field of the system, or None if there is
130138
no electric field.
131139
"""
132-
pass
140+
pass

dqc/system/mol.py

+31
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
from __future__ import annotations
12
from typing import List, Union, Optional, Tuple, Dict
23
import warnings
34
import torch
@@ -294,6 +295,36 @@ def getparamnames(self, methodname: str, prefix: str = "") -> List[str]:
294295
else:
295296
raise KeyError("Unknown methodname: %s" % methodname)
296297

298+
def make_copy(self, **kwargs) -> Mol:
299+
"""
300+
Returns a copy of the system identical to the orginal except for new
301+
parameters set in the kwargs.
302+
303+
Arguments
304+
---------
305+
**kwargs
306+
Must be the same kwargs as Mol.
307+
"""
308+
# create dictionary of all parameters
309+
parameters = {
310+
'moldesc': (self.atomzs, self.atompos),
311+
'basis': self._basis_inp,
312+
'orthogonalize_basis': self._orthogonalize_basis,
313+
'ao_parameterizer': self._aoparamzer,
314+
'grid': self._grid_inp,
315+
'spin': self._spin,
316+
'charge': self._charge,
317+
'orb_weights': None,
318+
'efield': self._efield,
319+
'vext': self._vext,
320+
'dtype': self._dtype,
321+
'device': self._device
322+
}
323+
# update dictionary with provided kwargs
324+
parameters.update(kwargs)
325+
# create new system
326+
return Mol(**parameters)
327+
297328
################### properties ###################
298329
@property
299330
def atompos(self) -> torch.Tensor:

dqc/system/sol.py

+30-1
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
from __future__ import annotations
12
from typing import Optional, Tuple, Union, List, Dict
23
import torch
34
import numpy as np
@@ -68,6 +69,7 @@ def __init__(self,
6869
self._dtype = dtype
6970
self._device = device
7071
self._grid_inp = grid
72+
self._basis_inp = basis
7173
self._grid: Optional[BaseGrid] = None
7274
charge = 0 # we can't have charged solids for now
7375

@@ -99,7 +101,8 @@ def __init__(self,
99101
self._orb_weights = _orb_weights
100102
self._orb_weights_u = _orb_weights_u
101103
self._orb_weights_d = _orb_weights_d
102-
self._lattice = Lattice(alattice)
104+
self._alattice_inp = alattice
105+
self._lattice = Lattice(self._alattice_inp)
103106
self._lattsum_opt = PBCIntOption.get_default(lattsum_opt)
104107

105108
def densityfit(self, method: Optional[str] = None,
@@ -240,6 +243,32 @@ def requires_grid(self) -> bool:
240243
def getparamnames(self, methodname: str, prefix: str = "") -> List[str]:
241244
pass
242245

246+
def make_copy(self, **kwargs) -> Sol:
247+
"""
248+
Returns a copy of the system identical to the orginal except for new
249+
parameters set in the kwargs.
250+
251+
Arguments
252+
---------
253+
**kwargs
254+
Must be the same kwargs as Sol.
255+
"""
256+
# create dictionary of all parameters
257+
parameters = {
258+
'soldesc': (self.atomzs, self.atompos),
259+
'alattice': self._alattice_inp,
260+
'basis': self._basis_inp,
261+
'grid': self._grid_inp,
262+
'spin': self._spin,
263+
'lattsum_opt': self._lattsum_opt,
264+
'dtype': self._dtype,
265+
'device': self._device
266+
}
267+
# update dictionary with provided kwargs
268+
parameters.update(kwargs)
269+
# create new system
270+
return Sol(**parameters)
271+
243272
################### properties ###################
244273
@property
245274
def atompos(self) -> torch.Tensor:

dqc/test/test_properties.py

+29-1
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
import psutil
66
from dqc.api.properties import hessian_pos, vibration, edipole, equadrupole, \
77
ir_spectrum, raman_spectrum, is_orb_min, \
8-
lowest_eival_orb_hessian
8+
lowest_eival_orb_hessian, optimal_geometry
99
from dqc.system.mol import Mol
1010
from dqc.qccalc.hf import HF
1111
from dqc.xc.base_xc import BaseXC
@@ -441,3 +441,31 @@ def get_jac_ene(atomposs, efield, grad_efield):
441441
# raman spectra intensities
442442
torch.autograd.gradgradcheck(get_jac_ene, (atomposs.detach(), efield, grad_efield.detach()),
443443
atol=3e-4)
444+
445+
def test_optimal_geometry(h2o_qc):
446+
# test if the optimal geometry of h2o similar to pyscf
447+
# from CCCBDB (calculated geometry for H2O)
448+
pyscf_h2o_opt = h2o_qc.get_system().atompos
449+
450+
# create a new h2o with poor initial geometry
451+
h2o_init = torch.tensor([
452+
[0.0, 0.0, 0.214],
453+
[0.0, 1.475, -0.863],
454+
[0.0, -1.475, -0.863],
455+
], dtype=dtype).requires_grad_()
456+
457+
# use bond length to assess optimal geometry as they are rotation invariant
458+
def bond_length(h2o):
459+
# Get the bond lengths of an h20 molecule
460+
return torch.stack([(h2o[0] - h2o[1]).norm(), (h2o[0] - h2o[2]).norm()])
461+
462+
# check starting geometry is not optimal
463+
assert not torch.allclose(bond_length(h2o_init), bond_length(pyscf_h2o_opt), rtol=2e-4)
464+
465+
# optimize geometry
466+
system = h2o_qc.get_system()
467+
new_system = system.make_copy(moldesc=(system.atomzs, system.atompos))
468+
new_qc = h2o_qc.__class__(new_system).run()
469+
h2o_opt = optimal_geometry(new_qc)
470+
471+
assert torch.allclose(bond_length(h2o_opt), bond_length(pyscf_h2o_opt), rtol=2e-4)

dqc/test/test_system.py

+25
Original file line numberDiff line numberDiff line change
@@ -127,6 +127,18 @@ def test_mol_cache():
127127
if os.path.exists(cache_fname):
128128
os.remove(cache_fname)
129129

130+
def test_mol_copy():
131+
# test if copy is computed correctly
132+
moldesc = "H 0 0 0; H 1 0 0"
133+
mol = Mol(moldesc, basis="3-21G")
134+
135+
mol_copy = mol.make_copy()
136+
assert torch.allclose(mol_copy.atompos, mol.atompos)
137+
138+
new_pos = torch.tensor([[0.0, 0.0, 0.01], [1.01, 0.0, 0.0]], dtype=dtype)
139+
mol_copy_2 = mol.make_copy(moldesc=(mol.atomzs, new_pos))
140+
assert torch.allclose(mol_copy_2.atompos, new_pos)
141+
130142
def test_sol_cache():
131143

132144
# test if cache is stored correctly
@@ -168,6 +180,19 @@ def test_sol_cache():
168180
j2c1 = h1.df.j2c
169181
assert torch.allclose(j2c, j2c1)
170182

183+
def test_sol_copy():
184+
# test if copy is computed correctly
185+
soldesc = "H 0 0 0"
186+
a = torch.tensor([[1.0, 0.0, 0.0], [0.0, 1.0, 0.0], [0.0, 0.0, 1.0]], dtype=dtype) * 3
187+
sol = Sol(soldesc, alattice=a, basis="3-21G")
188+
189+
sol_copy = sol.make_copy()
190+
assert torch.allclose(sol_copy.atompos, sol.atompos)
191+
192+
new_pos = torch.tensor([[0.0, 0.0, 0.01]], dtype=dtype)
193+
sol_copy_2 = sol.make_copy(soldesc=(sol.atomzs, new_pos))
194+
assert torch.allclose(sol_copy_2.atompos, new_pos)
195+
171196
##################### pbc #####################
172197
def test_mol_pbc_nuclei_energy():
173198
# test the calculation of ion-ion interaction energy (+ gradients w.r.t. pos)

0 commit comments

Comments
 (0)