Skip to content

Commit

Permalink
Merge pull request #227 from loriab/misc_0_15
Browse files Browse the repository at this point in the history
Miscellaneous before v0.15.0
  • Loading branch information
loriab authored Jun 25, 2020
2 parents 7f65188 + 8d9f72d commit 5ad222a
Show file tree
Hide file tree
Showing 11 changed files with 195 additions and 35 deletions.
12 changes: 10 additions & 2 deletions docs/source/changelog.rst
Original file line number Diff line number Diff line change
Expand Up @@ -14,19 +14,27 @@ Changelog
.. +++++++++
0.15.0 / 2020-06-DD
0.15.0 / 2020-06-25
-------------------

New Features
++++++++++++
- (:pr:`182`) Added experimental protocol for controlling autocorrection attemps. (That is, when a calculation throws a
known error that QCEngine thinks it can tweak the input and rerun.) Currently in trial for NWChem.

Enhancements
++++++++++++
- (:pr:`223`) ``molparse.to_string`` MADNESS dtype developed.
- (:pr:`186`, :pr:`223`) ``molparse.to_string`` Orca and MADNESS dtypes developed.
- (:pr:`226`) Allow ``which_import`` to distinguish between ordinary and namespace packages.
- (:pr:`227`) Add non-default ``strict`` argument to ``periodictable.to_Z``, ``to_symbol``, and ``to_element`` that fails when isotope information is given.
- (:pr:`227`) Allow nonphysical masses to pass validation in ``molparse.from_schema(..., nonphysical=True)``.
Also allowed in forming ``qcel.models.Molecule(..., nonphysical=True)``.

Bug Fixes
+++++++++
- (:pr:`227`) Fixed deception described in issue 225 where ``qcel.models.Molecule(..., symbols=["O18"])`` accepted "O18"
but did not influence the isotope, as user might have expected. That now raises ``NotAnElementError``, and an example
of correctly setting isotope/masses has been added. This error now caught at ``qcel.molparse.from_arrays`` so general.


0.14.0 / 2020-03-06
Expand Down
1 change: 1 addition & 0 deletions docs/source/model_molecule.rst
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,7 @@ the ``from_data`` function or by passing explicit fragments in the
>>> Ne 0.000000 0.000000 0.000000
>>> --
>>> Ne 3.100000 0.000000 0.000000
>>> units au
>>> """)
>>> mol = qcel.models.Molecule(
Expand Down
10 changes: 6 additions & 4 deletions qcelemental/exceptions.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,12 @@
class NotAnElementError(Exception):
"""Error when element or nuclide can't be identified."""

def __init__(self, atom):
self.message = "Atom identifier ({}) uninterpretable as atomic number, element symbol, or nuclide symbol".format(
atom
)
def __init__(self, atom, strict=False):
if strict:
msg = "atomic number or element"
else:
msg = "atomic number, element symbol, or nuclide symbol"
self.message = f"Atom identifier ({atom}) uninterpretable as {msg}"


class DataUnavailableError(Exception):
Expand Down
7 changes: 5 additions & 2 deletions qcelemental/models/molecule.py
Original file line number Diff line number Diff line change
Expand Up @@ -281,7 +281,10 @@ def __init__(self, orient: bool = False, validate: Optional[bool] = None, **kwar
kwargs["schema_version"] = kwargs.pop("schema_version", 2)
# original_keys = set(kwargs.keys()) # revive when ready to revisit sparsity

schema = to_schema(from_schema(kwargs), dtype=kwargs["schema_version"], copy=False, np_out=True)
nonphysical = kwargs.pop("nonphysical", False)
schema = to_schema(
from_schema(kwargs, nonphysical=nonphysical), dtype=kwargs["schema_version"], copy=False, np_out=True
)
schema = _filter_defaults(schema)

kwargs["validated"] = True
Expand Down Expand Up @@ -691,7 +694,7 @@ def to_string( # type: ignore
Suggest psi4 --> psi4frag and psi4 route to to_string
"""
molrec = from_schema(self.dict())
molrec = from_schema(self.dict(), nonphysical=True)
return to_string(
molrec,
dtype=dtype,
Expand Down
1 change: 1 addition & 0 deletions qcelemental/molparse/from_arrays.py
Original file line number Diff line number Diff line change
Expand Up @@ -188,6 +188,7 @@ def from_arrays(
tooclose : float, optional
Interatom distance (native `geom` units) nearer than which atoms not allowed.
nonphysical : bool, optional
Do allow masses outside an element's natural range to pass validation?
speclabel : bool, optional
If `True`, interpret `elbl` as potentially full nucleus spec including
ghosting, isotope, mass, tagging information, e.g., `@13C_mine` or
Expand Down
6 changes: 4 additions & 2 deletions qcelemental/molparse/from_schema.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,13 +7,15 @@
from .from_arrays import from_arrays


def from_schema(molschema, *, verbose: int = 1) -> Dict:
def from_schema(molschema, *, nonphysical: bool = False, verbose: int = 1) -> Dict:
"""Construct molecule dictionary representation from non-Psi4 schema.
Parameters
----------
molschema : dict
Dictionary form of Molecule following known schema.
nonphysical : bool, optional
Do allow masses outside an element's natural range to pass validation?
verbose : int, optional
Amount of printing.
Expand Down Expand Up @@ -82,7 +84,7 @@ def from_schema(molschema, *, verbose: int = 1) -> Dict:
speclabel=False,
# tooclose=tooclose,
# zero_ghost_fragments=zero_ghost_fragments,
# nonphysical=nonphysical,
nonphysical=nonphysical,
# mtol=mtol,
verbose=verbose,
)
Expand Down
2 changes: 1 addition & 1 deletion qcelemental/molparse/nucleus.py
Original file line number Diff line number Diff line change
Expand Up @@ -165,7 +165,7 @@ def reconcile(exact, tests, feature):
def offer_element_symbol(e):
"""Given an element, what can be suggested and asserted about Z, A, mass?"""

_Z = periodictable.to_Z(e)
_Z = periodictable.to_Z(e, strict=True)
offer_atomic_number(_Z)

def offer_atomic_number(z):
Expand Down
62 changes: 43 additions & 19 deletions qcelemental/periodic_table.py
Original file line number Diff line number Diff line change
Expand Up @@ -65,28 +65,37 @@ def __init__(self):
for EE, m, A in zip(self._EE, self.mass, self.A):
self._el2a2mass[EE][A] = float(m)

def _resolve_atom_to_key(self, atom: Union[int, str]) -> str:
def _resolve_atom_to_key(self, atom: Union[int, str], strict: bool = False) -> str:
"""Given `atom` as element name, element symbol, nuclide symbol, atomic number, or atomic number string,
return valid `self._eliso2mass` key, regardless of case. Raises `NotAnElementError` if unidentifiable.
"""
try:
self._eliso2mass[atom.capitalize()] # type: ignore
except (KeyError, AttributeError):

def resolve_eliso(atom):
try:
E = self._z2el[int(atom)]
except (KeyError, ValueError):
self._eliso2mass[atom.capitalize()] # type: ignore
except (KeyError, AttributeError):
try:
E = self._element2el[atom.capitalize()] # type: ignore
except (KeyError, AttributeError):
raise NotAnElementError(atom)
E = self._z2el[int(atom)]
except (KeyError, ValueError):
try:
E = self._element2el[atom.capitalize()] # type: ignore
except (KeyError, AttributeError):
raise NotAnElementError(atom)
else:
return E
else:
return E
else:
return E
else:
assert isinstance(atom, str)
return atom.capitalize()
assert isinstance(atom, str)
return atom.capitalize()

eliso = resolve_eliso(atom)

if strict and eliso not in self.E:
raise NotAnElementError(eliso, strict=strict)

return eliso

def to_mass(self, atom: Union[int, str], *, return_decimal: bool = False) -> Union[float, "Decimal"]:
"""Get atomic mass of `atom`.
Expand Down Expand Up @@ -147,7 +156,7 @@ def to_A(self, atom: Union[int, str]) -> int:
identifier = self._resolve_atom_to_key(atom)
return self._eliso2a[identifier]

def to_Z(self, atom: Union[int, str]) -> int:
def to_Z(self, atom: Union[int, str], strict: bool = False) -> int:
"""Get atomic number of `atom`.
Functions :py:func:`to_Z` and :py:func:`to_atomic_number` are aliases.
Expand All @@ -156,6 +165,8 @@ def to_Z(self, atom: Union[int, str]) -> int:
----------
atom : int or str
Identifier for element or nuclide, e.g., `H`, `D`, `H2`, `He`, `hE4`.
strict
Allow only element identification in `atom`, not nuclide.
Returns
-------
Expand All @@ -166,12 +177,13 @@ def to_Z(self, atom: Union[int, str]) -> int:
------
NotAnElementError
If `atom` cannot be resolved into an element or nuclide.
If `strict=True` and `atom` resolves into nuclide, not element.
"""
identifier = self._resolve_atom_to_key(atom)
identifier = self._resolve_atom_to_key(atom, strict=strict)
return self._el2z[self._eliso2el[identifier]]

def to_E(self, atom: Union[int, str]) -> str:
def to_E(self, atom: Union[int, str], strict: bool = False) -> str:
"""Get element symbol of `atom`.
Functions :py:func:`to_E` and :py:func:`to_symbol` are aliases.
Expand All @@ -180,16 +192,25 @@ def to_E(self, atom: Union[int, str]) -> str:
----------
atom : Union[int, str]
Identifier for element or nuclide, e.g., `H`, `D`, `H2`, `He`, `hE4`.
strict
Allow only element identification in `atom`, not nuclide.
Returns
-------
str
Element symbol, capitalized.
Raises
------
NotAnElementError
If `atom` cannot be resolved into an element or nuclide.
If `strict=True` and `atom` resolves into nuclide, not element.
"""
identifier = self._resolve_atom_to_key(atom)
identifier = self._resolve_atom_to_key(atom, strict=strict)
return self._eliso2el[identifier]

def to_element(self, atom: Union[int, str]) -> str:
def to_element(self, atom: Union[int, str], strict: bool = False) -> str:
"""Get element name of `atom`.
Functions :py:func:`to_element` and :py:func:`to_name` are aliases.
Expand All @@ -198,6 +219,8 @@ def to_element(self, atom: Union[int, str]) -> str:
----------
atom : int or str
Identifier for element or nuclide, e.g., `H`, `D`, `H2`, `He`, `hE4`.
strict
Allow only element identification in `atom`, not nuclide.
Returns
-------
Expand All @@ -208,9 +231,10 @@ def to_element(self, atom: Union[int, str]) -> str:
------
NotAnElementError
If `atom` cannot be resolved into an element or nuclide.
If `strict=True` and `atom` resolves into nuclide, not element.
"""
identifier = self._resolve_atom_to_key(atom)
identifier = self._resolve_atom_to_key(atom, strict=strict)
return self._el2element[self._eliso2el[identifier]]

to_mass_number = to_A
Expand Down
19 changes: 19 additions & 0 deletions qcelemental/tests/test_molecule.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
import pytest

import qcelemental as qcel
from qcelemental.exceptions import NotAnElementError
from qcelemental.models import Molecule
from qcelemental.testing import compare, compare_values

Expand Down Expand Up @@ -715,3 +716,21 @@ def test_sparse_molecule_connectivity():

mol = Molecule(symbols=["He", "He"], geometry=[0, 0, -2, 0, 0, 2])
assert "connectivity" not in mol.dict()


def test_bad_isotope_spec():
with pytest.raises(NotAnElementError) as e:
qcel.models.Molecule(symbols=["He3"], geometry=[0, 0, 0])


def test_good_isotope_spec():
assert compare_values(
[3.01602932], qcel.models.Molecule(symbols=["He"], mass_numbers=[3], geometry=[0, 0, 0]).masses, "nonstd mass"
)


def test_nonphysical_spec():
mol = qcel.models.Molecule(symbols=["He"], masses=[100], geometry=[0, 0, 0], nonphysical=True)
assert compare_values([100.0], mol.masses, "nonphysical mass")

print(mol.to_string(dtype="psi4"))
11 changes: 9 additions & 2 deletions qcelemental/tests/test_molparse_reconcile_nucleus.py
Original file line number Diff line number Diff line change
Expand Up @@ -63,14 +63,21 @@ def test_reconcile_nucleus_assertionerror(inp):
assert co_dominant_shortmass == qcelemental.molparse.reconcile_nucleus(**inp)


@pytest.mark.parametrize("inp", [{"A": 80, "Z": 27}, {"Z": -27, "mass": 200, "nonphysical": True}])
@pytest.mark.parametrize(
"inp",
[{"A": 80, "Z": 27}, {"Z": -27, "mass": 200, "nonphysical": True}, {"A": 100, "E": "He", "nonphysical": True}],
)
def test_reconcile_nucleus_notanelementerror(inp):
with pytest.raises(qcelemental.NotAnElementError):
qcelemental.molparse.reconcile_nucleus(**inp)


def test_reconcile_nucleus_41():
qcelemental.molparse.reconcile_nucleus(Z=27, mass=200, nonphysical=True)
ans = qcelemental.molparse.reconcile_nucleus(Z=27, mass=200, nonphysical=True)
assert ans == (-1, 27, "Co", 200.0, True, "")

ans = qcelemental.molparse.reconcile_nucleus(A=None, Z=None, E="He", mass=100, label=None, nonphysical=True)
assert ans == (-1, 2, "He", 100.0, True, "")


@pytest.mark.parametrize(
Expand Down
Loading

0 comments on commit 5ad222a

Please sign in to comment.