Skip to content

Commit ca21858

Browse files
authored
Merge pull request #128 from lcmd-epfl/bugfix
- Fix existing and potential errors when Mole.atom_charges() are used to determine atom number - Fix xyz_to_mol(ignore=True) bug - Fix number of electrons correction - Refactor: remove singleatom_basis_enumerator() - Remove legacy code from sym.py and refactor
2 parents 54c76e2 + 7387cd0 commit ca21858

10 files changed

Lines changed: 126 additions & 184 deletions

File tree

qstack/compound.py

Lines changed: 33 additions & 51 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
from pyscf import gto, data
88
from qstack import constants
99
from qstack.reorder import get_mrange
10-
from qstack.mathutils.array import stack_padding
10+
from qstack.mathutils.array import stack_padding, loadtxtvar
1111
from qstack.mathutils.rotation_matrix import rotate_euler
1212
from qstack.tools import Cursor
1313

@@ -144,8 +144,9 @@ def xyz_to_mol(inp, basis="def2-svp", charge=None, spin=None, ignore=False, unit
144144
if ignore:
145145
if charge not in (0, None) or spin not in (0, None):
146146
warnings.warn("Spin and charge values are overwritten", RuntimeWarning, stacklevel=2)
147+
atoms = [int(q) if q.isdigit() else data.elements.ELEMENTS_PROTON[q] for q in loadtxtvar(molxyz, dtype='str', usecols=0)]
147148
mol.spin = 0
148-
mol.charge = - sum(mol.atom_charges())%2
149+
mol.charge = -(sum(atoms)%2)
149150
else:
150151
if charge is not None:
151152
mol.charge = charge
@@ -162,8 +163,7 @@ def xyz_to_mol(inp, basis="def2-svp", charge=None, spin=None, ignore=False, unit
162163
mol.spin = 0
163164

164165
mol.build()
165-
species_charges = [data.elements.charge(z) for z in mol.elements]
166-
if mol.basis == 'minao' and ecp is None and (np.array(species_charges) > 36).any():
166+
if mol.basis == 'minao' and ecp is None and (numbers(mol) > 36).any():
167167
msg = f"{mol.basis} basis set requires the use of effective core potentials for atoms with Z>36"
168168
raise RuntimeError(msg)
169169
return mol
@@ -303,64 +303,31 @@ def make_atom(q, basis, ecp=None):
303303
return mol
304304

305305

306-
def singleatom_basis_enumerator(basis):
307-
"""Enumerate the different tensors of atomic orbitals within a 1-atom basis set.
308-
309-
Each tensor is a 2l+1-sized group of orbitals that share a radial function and l value.
310-
311-
Args:
312-
basis (list): Basis set definition in pyscf format.
313-
314-
Returns:
315-
tuple: A tuple containing:
316-
- l_per_bas (list): Angular momentum quantum number l for each basis shell.
317-
- n_per_bas (list): Radial function counter n (starting at 0) for each basis shell.
318-
- ao_starts (list): Starting index in AO array for each basis shell.
319-
"""
320-
ao_starts = []
321-
l_per_bas = []
322-
n_per_bas = []
323-
cursor = Cursor(action='ranger')
324-
cursor_per_l = []
325-
for bas in basis:
326-
# shape of `bas`, l, then another optional constant, then lists [exp, coeff, coeff, coeff]
327-
# that make a matrix between the number of functions (number of coeff per list)
328-
# and the number of primitive gaussians (one per list)
329-
l = bas[0]
330-
while len(cursor_per_l) <= l:
331-
cursor_per_l.append(Cursor(action='ranger'))
332-
n_count = len(bas[-1])-1
333-
l_per_bas += [l] * n_count
334-
n_per_bas.extend(cursor_per_l[l].add(n_count))
335-
msize = 2*l+1
336-
ao_starts.extend(cursor.add(msize*n_count)[::msize])
337-
return l_per_bas, n_per_bas, ao_starts
338-
339-
340306
def basis_flatten(mol, return_both=True, return_shells=False):
341307
"""Flatten a basis set definition for AOs.
342308
343309
Args:
344310
mol (pyscf.gto.Mole): pyscf Mole object.
345311
return_both (bool): Whether to return both AO info and primitive Gaussian info. Defaults to True.
346-
return_shells (bool): Whether to return angular momenta per shell. Defaults to False.
312+
return_shells (bool): Whether to return angular momenta and starting indices per shell
313+
(2l+1-sized group of orbitals that share a radial function and l value). Defaults to False.
347314
348315
Returns:
349316
- numpy.ndarray: 3×mol.nao int array where each column corresponds to an AO and rows are:
350-
- 0: atom index
351-
- 1: angular momentum quantum number l
352-
- 2: magnetic quantum number m
317+
- 0: atom index,
318+
- 1: angular momentum quantum number l,
319+
- 2: magnetic quantum number m.
353320
If return_both is True, also returns:
354321
- numpy.ndarray: 2×mol.nao×max_n float array where index (i,j,k) means:
355-
- i: 0 for exponent, 1 for contraction coefficient of a primitive Gaussian
356-
- j: AO index
357-
- k: radial function index (padded with zeros if necessary)
358-
If return_shell is True, also returns:
359-
- numpy.ndarray: angular momentum quantum number for each shell
360-
322+
- i: 0 for exponent, 1 for contraction coefficient of a primitive Gaussian,
323+
- j: AO index,
324+
- k: radial function index (padded with zeros if necessary).
325+
If return_shells is True, also returns:
326+
- numpy.ndarray: starting AO indices for each shell.
361327
"""
362328
x = []
363-
L = []
329+
ao_starts = []
330+
cursor = Cursor(action='ranger')
364331
y = np.zeros((3, mol.nao), dtype=int)
365332
i = Cursor(action='slicer')
366333
a = mol.bas_exps()
@@ -376,11 +343,26 @@ def basis_flatten(mol, return_both=True, return_shells=False):
376343
x.extend([ac]*msize)
377344
y[:,i(msize*n)] = np.vstack((np.array([[iat, l]]*msize*n).T, [*get_mrange(l)]*n))
378345
if return_shells:
379-
L.extend([l]*n)
346+
ao_starts.extend(cursor.add(msize*n)[::msize])
380347

381348
ret = [y]
382349
if return_both:
383350
ret.append(stack_padding(x).transpose((1,0,2)))
384351
if return_shells:
385-
ret.append(np.array(L))
352+
ret.append(np.array(ao_starts))
386353
return ret[0] if len(ret)==1 else ret
354+
355+
356+
def numbers(mol):
357+
"""Get atom numbers of a molecule.
358+
359+
Use this function to get atomic NUMBERS to index elements.
360+
Use `mol.atom_charges()` to get CHARGES (it returns effective charges when ECP are used).
361+
362+
Args:
363+
mol (pyscf.gto.Mole): pyscf Mole object.
364+
365+
Returns:
366+
numpy.ndarray: Array of atomic numbers.
367+
"""
368+
return np.array([data.elements.charge(q) for q in mol.elements])

qstack/equio.py

Lines changed: 12 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -4,11 +4,10 @@
44
from functools import reduce
55
from types import SimpleNamespace
66
import numpy as np
7-
from pyscf import data
87
import metatensor
98
from qstack.tools import Cursor
109
from qstack.reorder import get_mrange, pyscf2gpr_l1_order
11-
from qstack.compound import singleatom_basis_enumerator
10+
from qstack.compound import basis_flatten, numbers
1211

1312

1413
vector_label_names = SimpleNamespace(
@@ -35,9 +34,14 @@ def _get_llist(mol):
3534
mol (pyscf.gto.Mole): pyscf Mole object.
3635
3736
Returns:
38-
dict: Dictionary with atom numbers as keys and List of angular momentum quantum numbers for each basis function as values.
37+
dict: Dictionary with atom numbers as keys and List of angular momentum quantum numbers for each shell as values.
3938
"""
40-
return {int(q): singleatom_basis_enumerator(mol._basis[data.elements.ELEMENTS[q]])[0] for q in np.unique(mol.atom_charges())}
39+
ao, ao_start = basis_flatten(mol, return_both=False, return_shells=True)
40+
iat_shells, l_shells, _ = ao[:,ao_start]
41+
llist = {}
42+
for q, iat in zip(*np.unique(numbers(mol), return_index=True), strict=True):
43+
llist[q] = l_shells[np.where(iat_shells==iat)].tolist()
44+
return llist
4145

4246

4347
def _get_tsize(tensor):
@@ -78,7 +82,7 @@ def vector_to_tensormap(mol, c):
7882
Returns:
7983
metatensor.TensorMap: Tensor map representation of the vector.
8084
"""
81-
atom_charges = mol.atom_charges()
85+
atom_charges = numbers(mol)
8286

8387
tm_label_vals = []
8488
block_prop_label_vals = {}
@@ -161,7 +165,7 @@ def tensormap_to_vector(mol, tensor):
161165
raise RuntimeError(f'Tensor size mismatch ({nao} instead of {mol.nao})')
162166

163167
c = np.zeros(mol.nao)
164-
atom_charges = mol.atom_charges()
168+
atom_charges = numbers(mol)
165169
llists = _get_llist(mol)
166170
i = 0
167171
for iat, q in enumerate(atom_charges):
@@ -191,7 +195,7 @@ def matrix_to_tensormap(mol, dm):
191195
Returns:
192196
metatensor.TensorMap: Tensor map representation of the matrix.
193197
"""
194-
atom_charges = mol.atom_charges()
198+
atom_charges = numbers(mol)
195199
elements, counts = np.unique(atom_charges, return_counts=True)
196200
counts = dict(zip(elements, counts, strict=True))
197201
element_indices = {q: np.where(atom_charges==q)[0] for q in elements}
@@ -313,7 +317,7 @@ def tensormap_to_matrix(mol, tensor):
313317
raise RuntimeError(f'Tensor size mismatch ({nao2} instead of {mol.nao*mol.nao})')
314318

315319
dm = np.zeros((mol.nao, mol.nao))
316-
atom_charges = mol.atom_charges()
320+
atom_charges = numbers(mol)
317321
llists = _get_llist(mol)
318322
i1 = 0
319323
for iat1, q1 in enumerate(atom_charges):

qstack/fields/decomposition.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -203,7 +203,7 @@ def correct_N(mol, c0, N=None, mode='Lagrange', metric='u'):
203203
numpy ndarray: Corrected decomposition coefficients (1D array).
204204
"""
205205
mode = mode.lower()
206-
q = moments.r2_c(mol, None, moments=[0])
206+
q = moments.r2_c(mol, None, moments=[0])[0]
207207
N0 = c0 @ q
208208

209209
if N is None:

qstack/mathutils/array.py

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
"""Array manipulation utility functions."""
22

3+
from io import StringIO
34
import numpy as np
45
from qstack.tools import slice_generator
56

@@ -94,3 +95,19 @@ def vstack_padding(xs):
9495
slices = (s0, *(np.s_[0:s] for s in x.shape[1:]))
9596
X[slices] = x
9697
return X
98+
99+
100+
def loadtxtvar(var, *kargs, **kwargs):
101+
"""Load a numpy array from a string variable.
102+
103+
Args:
104+
var (str): String variable containing the data.
105+
*kargs: Additional positional arguments for numpy.loadtxt.
106+
**kwargs: Additional keyword arguments for numpy.loadtxt.
107+
108+
Returns:
109+
numpy.ndarray: Loaded array.
110+
"""
111+
with StringIO(var) as f:
112+
x = np.loadtxt(f, *kargs, **kwargs)
113+
return x

qstack/reorder.py

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -98,9 +98,9 @@ def reorder_ao(mol, vector, src='pyscf', dest='gpr'):
9898
NotImplementedError: If the specified convention is not implemented.
9999
ValueError: If vector dimension is not 1 or 2.
100100
"""
101-
def get_idx(L, m, convention):
101+
def get_idx(l_shells, m, convention):
102102
convention = convention.lower()
103-
l_slices = slice_generator(L, inc=lambda l: 2*l+1)
103+
l_slices = slice_generator(l_shells, inc=lambda l: 2*l+1)
104104
if convention == 'gpr':
105105
return np.arange(len(m)), np.ones_like(m)
106106
elif convention == 'pyscf':
@@ -113,9 +113,11 @@ def get_idx(L, m, convention):
113113

114114
from .compound import basis_flatten
115115

116-
(_, _, m), L = basis_flatten(mol, return_both=False, return_shells=True)
117-
idx_src, sign_src = get_idx(L, m, src)
118-
idx_dest, sign_dest = get_idx(L, m, dest)
116+
(_, l, m), shell_start = basis_flatten(mol, return_both=False, return_shells=True)
117+
l_shells = l[shell_start]
118+
119+
idx_src, sign_src = get_idx(l_shells, m, src)
120+
idx_dest, sign_dest = get_idx(l_shells, m, dest)
119121

120122
if vector.ndim == 2:
121123
sign_src = np.einsum('i,j->ij', sign_src, sign_src)

0 commit comments

Comments
 (0)