Skip to content

Commit cddc531

Browse files
authored
announce py38 and add Mol fns (#360)
* Update README.md * Update README.md * Update test_molecule.py * Update test_molecule.py * Update molecule.py * Update test_molecule.py * Update molecule.py * Update changelog.rst * Update test_molecule.py * Update test_molecule.py * Update Lint.yml * Update test_molecule.py
1 parent 4f19879 commit cddc531

File tree

5 files changed

+125
-6
lines changed

5 files changed

+125
-6
lines changed

.github/workflows/Lint.yml

+2
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,8 @@ jobs:
1717
python-version: "3.7"
1818
- name: Install black
1919
run: pip install "black>=22.1.0,<23.0a0"
20+
- name: Print code formatting with black (hints here if next step errors)
21+
run: black --diff .
2022
- name: Check code formatting with black
2123
run: black --check .
2224

README.md

+1-1
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ This project also contains a generator, validator, and translator for [Molecule
1616

1717
## ✨ Getting Started
1818

19-
- Installation. QCElemental supports Python 3.7+.
19+
- Installation. QCElemental supports Python 3.7+. Starting with v0.50 (aka "next", aka "QCSchema v2 available"), Python 3.8+ will be supported.
2020

2121
```sh
2222
python -m pip install qcelemental

docs/changelog.rst

+4
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,10 @@ Breaking Changes
3131

3232
New Features
3333
++++++++++++
34+
- (:pr:`360`) ``Molecule`` learned new functions ``element_composition`` and ``molecular_weight``.
35+
The first gives a dictionary of element symbols and counts, while the second gives the weight in amu.
36+
Both can access the whole molecule or per-fragment like the existing ``nelectrons`` and
37+
``nuclear_repulsion_energy``. All four can now select all atoms or exclude ghosts (default).
3438

3539
Enhancements
3640
++++++++++++

qcelemental/models/molecule.py

+82-5
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
"""
22
Molecule Object Model
33
"""
4-
4+
import collections
55
import hashlib
66
import json
77
import warnings
@@ -875,6 +875,10 @@ def get_molecular_formula(self, order: str = "alphabetical", chgmult: bool = Fal
875875
>>> two_pentanol_radcat.get_molecular_formula(chgmult=True)
876876
2^C5H12O+
877877
878+
Notes
879+
-----
880+
This includes all atoms in the molecule, including ghost atoms. See :py:meth:`element_composition` to exclude.
881+
878882
"""
879883

880884
from ..molutil import molecular_formula_from_symbols
@@ -1151,21 +1155,26 @@ def _inertial_tensor(geom, *, weight):
11511155
tensor[2][1] = tensor[1][2] = -1.0 * np.sum(weight * geom[:, 1] * geom[:, 2])
11521156
return tensor
11531157

1154-
def nuclear_repulsion_energy(self, ifr: int = None) -> float:
1158+
def nuclear_repulsion_energy(self, ifr: int = None, real_only: bool = True) -> float:
11551159
r"""Nuclear repulsion energy.
11561160
11571161
Parameters
11581162
----------
11591163
ifr
11601164
If not `None`, only compute for the `ifr`-th (0-indexed) fragment.
1165+
real_only
1166+
Only include real atoms in the sum.
11611167
11621168
Returns
11631169
-------
11641170
nre : float
11651171
Nuclear repulsion energy in entire molecule or in fragment.
11661172
11671173
"""
1168-
Zeff = [z * int(real) for z, real in zip(cast(Iterable[int], self.atomic_numbers), self.real)]
1174+
if real_only:
1175+
Zeff = [z * int(real) for z, real in zip(cast(Iterable[int], self.atomic_numbers), self.real)]
1176+
else:
1177+
Zeff = self.atomic_numbers
11691178
atoms = list(range(self.geometry.shape[0]))
11701179

11711180
if ifr is not None:
@@ -1178,21 +1187,26 @@ def nuclear_repulsion_energy(self, ifr: int = None) -> float:
11781187
nre += Zeff[at1] * Zeff[at2] / dist
11791188
return nre
11801189

1181-
def nelectrons(self, ifr: int = None) -> int:
1190+
def nelectrons(self, ifr: int = None, real_only: bool = True) -> int:
11821191
r"""Number of electrons.
11831192
11841193
Parameters
11851194
----------
11861195
ifr
11871196
If not `None`, only compute for the `ifr`-th (0-indexed) fragment.
1197+
real_only
1198+
Only include real atoms in the sum.
11881199
11891200
Returns
11901201
-------
11911202
nelec : int
11921203
Number of electrons in entire molecule or in fragment.
11931204
11941205
"""
1195-
Zeff = [z * int(real) for z, real in zip(cast(Iterable[int], self.atomic_numbers), self.real)]
1206+
if real_only:
1207+
Zeff = [z * int(real) for z, real in zip(cast(Iterable[int], self.atomic_numbers), self.real)]
1208+
else:
1209+
Zeff = self.atomic_numbers
11961210

11971211
if ifr is None:
11981212
nel = sum(Zeff) - self.molecular_charge
@@ -1202,6 +1216,69 @@ def nelectrons(self, ifr: int = None) -> int:
12021216

12031217
return int(nel)
12041218

1219+
def molecular_weight(self, ifr: int = None, real_only: bool = True) -> float:
1220+
r"""Molecular weight in uamu.
1221+
1222+
Parameters
1223+
----------
1224+
ifr
1225+
If not `None`, only compute for the `ifr`-th (0-indexed) fragment.
1226+
real_only
1227+
Only include real atoms in the sum.
1228+
1229+
Returns
1230+
-------
1231+
mw : float
1232+
Molecular weight in entire molecule or in fragment.
1233+
1234+
"""
1235+
if real_only:
1236+
masses = [mas * int(real) for mas, real in zip(cast(Iterable[float], self.masses), self.real)]
1237+
else:
1238+
masses = self.masses
1239+
1240+
if ifr is None:
1241+
mw = sum(masses)
1242+
1243+
else:
1244+
mw = sum([mas for iat, mas in enumerate(masses) if iat in self.fragments[ifr]])
1245+
1246+
return mw
1247+
1248+
def element_composition(self, ifr: int = None, real_only: bool = True) -> Dict[str, int]:
1249+
r"""Atomic count map.
1250+
1251+
Parameters
1252+
----------
1253+
ifr
1254+
If not `None`, only compute for the `ifr`-th (0-indexed) fragment.
1255+
real_only
1256+
Only include real atoms.
1257+
1258+
Returns
1259+
-------
1260+
composition : Dict[str, int]
1261+
Atomic count map.
1262+
1263+
Notes
1264+
-----
1265+
This excludes ghost atoms by default whereas get_molecular_formula always includes them.
1266+
1267+
"""
1268+
if real_only:
1269+
symbols = [sym * int(real) for sym, real in zip(cast(Iterable[str], self.symbols), self.real)]
1270+
else:
1271+
symbols = self.symbols
1272+
1273+
if ifr is None:
1274+
count = collections.Counter(sym.title() for sym in symbols)
1275+
1276+
else:
1277+
count = collections.Counter(sym.title() for iat, sym in enumerate(symbols) if iat in self.fragments[ifr])
1278+
1279+
count.pop("", None)
1280+
return dict(count)
1281+
12051282
def align(
12061283
self,
12071284
ref_mol: "Molecule",

qcelemental/tests/test_molecule.py

+36
Original file line numberDiff line numberDiff line change
@@ -913,3 +913,39 @@ def test_frag_multiplicity_types_errors(mult_in, validate, error):
913913
qcel.models.Molecule(**mol_args)
914914

915915
assert error in str(e.value)
916+
917+
918+
_one_helium_mass = 4.00260325413
919+
920+
921+
@pytest.mark.parametrize(
922+
"mol_string,args,formula,formula_dict,molecular_weight,nelec,nre",
923+
[
924+
("He 0 0 0", {}, "He", {"He": 1}, _one_helium_mass, 2, 0.0),
925+
("He 0 0 0\n--\nHe 0 0 5", {}, "He2", {"He": 2}, 2 * _one_helium_mass, 4, 0.4233417684),
926+
("He 0 0 0\n--\n@He 0 0 5", {}, "He2", {"He": 1}, _one_helium_mass, 2, 0.0),
927+
("He 0 0 0\n--\n@He 0 0 5", {"ifr": 0}, "He2", {"He": 1}, _one_helium_mass, 2, 0.0),
928+
("He 0 0 0\n--\n@He 0 0 5", {"ifr": 1}, "He2", {}, 0.0, 0, 0.0),
929+
("He 0 0 0\n--\n@He 0 0 5", {"real_only": False}, "He2", {"He": 2}, 2 * _one_helium_mass, 4, 0.4233417684),
930+
("He 0 0 0\n--\n@He 0 0 5", {"real_only": False, "ifr": 0}, "He2", {"He": 1}, _one_helium_mass, 2, 0.0),
931+
("He 0 0 0\n--\n@He 0 0 5", {"real_only": False, "ifr": 1}, "He2", {"He": 1}, _one_helium_mass, 2, 0.0),
932+
("4He 0 0 0", {}, "He", {"He": 1}, _one_helium_mass, 2, 0.0),
933+
("5He4 0 0 0", {}, "He", {"He": 1}, 5.012057, 2, 0.0), # suffix-4 is label
934+
("[email protected] 0 0 0", {}, "He", {"He": 1}, 3.14, 2, 0.0),
935+
],
936+
)
937+
def test_molecular_weight(mol_string, args, formula, formula_dict, molecular_weight, nelec, nre):
938+
mol = Molecule.from_data(mol_string)
939+
940+
assert mol.molecular_weight(**args) == molecular_weight, f"molecular_weight: ret != {molecular_weight}"
941+
assert mol.nelectrons(**args) == nelec, f"nelectrons: ret != {nelec}"
942+
assert abs(mol.nuclear_repulsion_energy(**args) - nre) < 1.0e-5, f"nre: ret != {nre}"
943+
assert mol.element_composition(**args) == formula_dict, f"element_composition: ret != {formula_dict}"
944+
assert mol.get_molecular_formula() == formula, f"get_molecular_formula: ret != {formula}"
945+
946+
# after py38
947+
# assert (ret := mol.molecular_weight(**args)) == molecular_weight, f"molecular_weight: {ret} != {molecular_weight}"
948+
# assert (ret := mol.nelectrons(**args)) == nelec, f"nelectrons: {ret} != {nelec}"
949+
# assert (abs(ret := mol.nuclear_repulsion_energy(**args)) - nre) < 1.0e-5, f"nre: {ret} != {nre}"
950+
# assert (ret := mol.element_composition(**args)) == formula_dict, f"element_composition: {ret} != {formula_dict}"
951+
# assert (ret := mol.get_molecular_formula()) == formula, f"get_molecular_formula: {ret} != {formula}"

0 commit comments

Comments
 (0)