Skip to content

Commit

Permalink
Merge branch 'master' into floatmult_rb1
Browse files Browse the repository at this point in the history
  • Loading branch information
loriab authored Jan 13, 2025
2 parents 22604f8 + cddc531 commit d71b027
Show file tree
Hide file tree
Showing 7 changed files with 152 additions and 13 deletions.
2 changes: 2 additions & 0 deletions .github/workflows/Lint.yml
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@ jobs:
python-version: "3.7"
- name: Install black
run: pip install "black>=22.1.0,<23.0a0"
- name: Print code formatting with black (hints here if next step errors)
run: black --diff .
- name: Check code formatting with black
run: black --check .

Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ This project also contains a generator, validator, and translator for [Molecule

## ✨ Getting Started

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

```sh
python -m pip install qcelemental
Expand Down
8 changes: 7 additions & 1 deletion docs/changelog.rst
Original file line number Diff line number Diff line change
Expand Up @@ -29,9 +29,13 @@ Breaking Changes

New Features
++++++++++++
- UNMERGED (:pr:`350`, :pr:`318`, :issue:`317`) Make behavior consistent between molecular_charge/
- (:pr:`350`, :pr:`318`, :issue:`317`) Make behavior consistent between molecular_charge/
fragment_charges and molecular_multiplicity/fragment_multiplicities by allowing floating point
numbers for multiplicities. @awvwgk
- (:pr:`360`) ``Molecule`` learned new functions ``element_composition`` and ``molecular_weight``.
The first gives a dictionary of element symbols and counts, while the second gives the weight in amu.
Both can access the whole molecule or per-fragment like the existing ``nelectrons`` and
``nuclear_repulsion_energy``. All four can now select all atoms or exclude ghosts (default).

Enhancements
++++++++++++
Expand Down Expand Up @@ -59,6 +63,8 @@ Misc.
+++++
- (:pr:`344`, :issue:`282`) Add a citation file since QCElemental doesn't have a paper. @lilyminium
- (:pr:`342`, :issue:`333`) Update some docs settings and requirements for newer tools.
- (:pr:`353`) copied in pkg_resources.safe_version code as follow-up to Eric switch to packaging as both nwchem and gamess were now working.
the try_harder_safe_version might be even bettter


0.28.0 / 2024-06-21
Expand Down
87 changes: 82 additions & 5 deletions qcelemental/models/molecule.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
"""
Molecule Object Model
"""

import collections
import hashlib
import json
import warnings
Expand Down Expand Up @@ -877,6 +877,10 @@ def get_molecular_formula(self, order: str = "alphabetical", chgmult: bool = Fal
>>> two_pentanol_radcat.get_molecular_formula(chgmult=True)
2^C5H12O+
Notes
-----
This includes all atoms in the molecule, including ghost atoms. See :py:meth:`element_composition` to exclude.
"""

from ..molutil import molecular_formula_from_symbols
Expand Down Expand Up @@ -1153,21 +1157,26 @@ def _inertial_tensor(geom, *, weight):
tensor[2][1] = tensor[1][2] = -1.0 * np.sum(weight * geom[:, 1] * geom[:, 2])
return tensor

def nuclear_repulsion_energy(self, ifr: int = None) -> float:
def nuclear_repulsion_energy(self, ifr: int = None, real_only: bool = True) -> float:
r"""Nuclear repulsion energy.
Parameters
----------
ifr
If not `None`, only compute for the `ifr`-th (0-indexed) fragment.
real_only
Only include real atoms in the sum.
Returns
-------
nre : float
Nuclear repulsion energy in entire molecule or in fragment.
"""
Zeff = [z * int(real) for z, real in zip(cast(Iterable[int], self.atomic_numbers), self.real)]
if real_only:
Zeff = [z * int(real) for z, real in zip(cast(Iterable[int], self.atomic_numbers), self.real)]
else:
Zeff = self.atomic_numbers
atoms = list(range(self.geometry.shape[0]))

if ifr is not None:
Expand All @@ -1180,21 +1189,26 @@ def nuclear_repulsion_energy(self, ifr: int = None) -> float:
nre += Zeff[at1] * Zeff[at2] / dist
return nre

def nelectrons(self, ifr: int = None) -> int:
def nelectrons(self, ifr: int = None, real_only: bool = True) -> int:
r"""Number of electrons.
Parameters
----------
ifr
If not `None`, only compute for the `ifr`-th (0-indexed) fragment.
real_only
Only include real atoms in the sum.
Returns
-------
nelec : int
Number of electrons in entire molecule or in fragment.
"""
Zeff = [z * int(real) for z, real in zip(cast(Iterable[int], self.atomic_numbers), self.real)]
if real_only:
Zeff = [z * int(real) for z, real in zip(cast(Iterable[int], self.atomic_numbers), self.real)]
else:
Zeff = self.atomic_numbers

if ifr is None:
nel = sum(Zeff) - self.molecular_charge
Expand All @@ -1204,6 +1218,69 @@ def nelectrons(self, ifr: int = None) -> int:

return int(nel)

def molecular_weight(self, ifr: int = None, real_only: bool = True) -> float:
r"""Molecular weight in uamu.
Parameters
----------
ifr
If not `None`, only compute for the `ifr`-th (0-indexed) fragment.
real_only
Only include real atoms in the sum.
Returns
-------
mw : float
Molecular weight in entire molecule or in fragment.
"""
if real_only:
masses = [mas * int(real) for mas, real in zip(cast(Iterable[float], self.masses), self.real)]
else:
masses = self.masses

if ifr is None:
mw = sum(masses)

else:
mw = sum([mas for iat, mas in enumerate(masses) if iat in self.fragments[ifr]])

return mw

def element_composition(self, ifr: int = None, real_only: bool = True) -> Dict[str, int]:
r"""Atomic count map.
Parameters
----------
ifr
If not `None`, only compute for the `ifr`-th (0-indexed) fragment.
real_only
Only include real atoms.
Returns
-------
composition : Dict[str, int]
Atomic count map.
Notes
-----
This excludes ghost atoms by default whereas get_molecular_formula always includes them.
"""
if real_only:
symbols = [sym * int(real) for sym, real in zip(cast(Iterable[str], self.symbols), self.real)]
else:
symbols = self.symbols

if ifr is None:
count = collections.Counter(sym.title() for sym in symbols)

else:
count = collections.Counter(sym.title() for iat, sym in enumerate(symbols) if iat in self.fragments[ifr])

count.pop("", None)
return dict(count)

def align(
self,
ref_mol: "Molecule",
Expand Down
14 changes: 11 additions & 3 deletions qcelemental/tests/test_importing.py
Original file line number Diff line number Diff line change
Expand Up @@ -154,6 +154,14 @@ def test_parse_version():
assert str(v) == "5.3.1"


def test_safe_version():
v = qcel.util.safe_version("5.3.1")
assert v == "5.3.1"
@pytest.mark.parametrize(
"inp,out",
[
("5.3.1", "5.3.1"),
("30 SEP 2023 (R2)", "30.SEP.2023.-R2-"),
("7.0.0+N/A", "7.0.0-N-A"),
],
)
def test_safe_version(inp, out):
v = qcel.util.safe_version(inp)
assert v == out
36 changes: 36 additions & 0 deletions qcelemental/tests/test_molecule.py
Original file line number Diff line number Diff line change
Expand Up @@ -954,3 +954,39 @@ def test_frag_multiplicity_types_errors(mult_in, validate, error):
qcel.models.Molecule(**mol_args)

assert error in str(e.value)


_one_helium_mass = 4.00260325413


@pytest.mark.parametrize(
"mol_string,args,formula,formula_dict,molecular_weight,nelec,nre",
[
("He 0 0 0", {}, "He", {"He": 1}, _one_helium_mass, 2, 0.0),
("He 0 0 0\n--\nHe 0 0 5", {}, "He2", {"He": 2}, 2 * _one_helium_mass, 4, 0.4233417684),
("He 0 0 0\n--\n@He 0 0 5", {}, "He2", {"He": 1}, _one_helium_mass, 2, 0.0),
("He 0 0 0\n--\n@He 0 0 5", {"ifr": 0}, "He2", {"He": 1}, _one_helium_mass, 2, 0.0),
("He 0 0 0\n--\n@He 0 0 5", {"ifr": 1}, "He2", {}, 0.0, 0, 0.0),
("He 0 0 0\n--\n@He 0 0 5", {"real_only": False}, "He2", {"He": 2}, 2 * _one_helium_mass, 4, 0.4233417684),
("He 0 0 0\n--\n@He 0 0 5", {"real_only": False, "ifr": 0}, "He2", {"He": 1}, _one_helium_mass, 2, 0.0),
("He 0 0 0\n--\n@He 0 0 5", {"real_only": False, "ifr": 1}, "He2", {"He": 1}, _one_helium_mass, 2, 0.0),
("4He 0 0 0", {}, "He", {"He": 1}, _one_helium_mass, 2, 0.0),
("5He4 0 0 0", {}, "He", {"He": 1}, 5.012057, 2, 0.0), # suffix-4 is label
("[email protected] 0 0 0", {}, "He", {"He": 1}, 3.14, 2, 0.0),
],
)
def test_molecular_weight(mol_string, args, formula, formula_dict, molecular_weight, nelec, nre):
mol = Molecule.from_data(mol_string)

assert mol.molecular_weight(**args) == molecular_weight, f"molecular_weight: ret != {molecular_weight}"
assert mol.nelectrons(**args) == nelec, f"nelectrons: ret != {nelec}"
assert abs(mol.nuclear_repulsion_energy(**args) - nre) < 1.0e-5, f"nre: ret != {nre}"
assert mol.element_composition(**args) == formula_dict, f"element_composition: ret != {formula_dict}"
assert mol.get_molecular_formula() == formula, f"get_molecular_formula: ret != {formula}"

# after py38
# assert (ret := mol.molecular_weight(**args)) == molecular_weight, f"molecular_weight: {ret} != {molecular_weight}"
# assert (ret := mol.nelectrons(**args)) == nelec, f"nelectrons: {ret} != {nelec}"
# assert (abs(ret := mol.nuclear_repulsion_energy(**args)) - nre) < 1.0e-5, f"nre: {ret} != {nre}"
# assert (ret := mol.element_composition(**args)) == formula_dict, f"element_composition: {ret} != {formula_dict}"
# assert (ret := mol.get_molecular_formula()) == formula, f"get_molecular_formula: {ret} != {formula}"
16 changes: 13 additions & 3 deletions qcelemental/util/importing.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import os
import re
import shutil
import sys
from typing import TYPE_CHECKING, List, Union
Expand Down Expand Up @@ -132,14 +133,23 @@ def which(

def safe_version(version) -> str:
"""
Package resources is a very slow load
Convert an arbitrary string to a standard version string by pkg_resources definition.
"""
return str(parse_version(version))
# from https://github.com/pypa/setuptools/blob/main/pkg_resources/__init__.py
# original function deprecated and never one-to-one replaced
from packaging.version import InvalidVersion, Version

try:
# normalize the version
return str(Version(version))
except InvalidVersion:
version = version.replace(" ", ".")
return re.sub("[^A-Za-z0-9.]+", "-", version)


def parse_version(version) -> "Version":
"""
Package resources is a very slow load
Legitimate version
"""
from packaging.version import parse

Expand Down

0 comments on commit d71b027

Please sign in to comment.