stjames 0.0.39__tar.gz → 0.0.41__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {stjames-0.0.39/stjames.egg-info → stjames-0.0.41}/PKG-INFO +3 -3
- {stjames-0.0.39 → stjames-0.0.41}/README.md +1 -1
- {stjames-0.0.39 → stjames-0.0.41}/pyproject.toml +16 -6
- {stjames-0.0.39 → stjames-0.0.41}/stjames/__init__.py +3 -0
- stjames-0.0.41/stjames/atom.py +66 -0
- stjames-0.0.41/stjames/base.py +42 -0
- {stjames-0.0.39 → stjames-0.0.41}/stjames/basis_set.py +3 -7
- {stjames-0.0.39 → stjames-0.0.41}/stjames/calculation.py +2 -0
- stjames-0.0.41/stjames/constraint.py +36 -0
- stjames-0.0.41/stjames/data/__init__.py +1 -0
- stjames-0.0.41/stjames/data/elements.py +27 -0
- stjames-0.0.41/stjames/data/read_nist_isotopes.py +116 -0
- {stjames-0.0.39 → stjames-0.0.41}/stjames/method.py +34 -1
- stjames-0.0.41/stjames/molecule.py +192 -0
- stjames-0.0.41/stjames/opt_settings.py +21 -0
- stjames-0.0.41/stjames/periodic_cell.py +34 -0
- {stjames-0.0.39 → stjames-0.0.41}/stjames/settings.py +26 -39
- {stjames-0.0.39 → stjames-0.0.41}/stjames/solvent.py +2 -0
- {stjames-0.0.39 → stjames-0.0.41}/stjames/task.py +1 -0
- stjames-0.0.41/stjames/types.py +8 -0
- {stjames-0.0.39 → stjames-0.0.41}/stjames/workflows/__init__.py +6 -0
- stjames-0.0.41/stjames/workflows/admet.py +7 -0
- stjames-0.0.41/stjames/workflows/basic_calculation.py +9 -0
- stjames-0.0.41/stjames/workflows/bde.py +269 -0
- {stjames-0.0.39 → stjames-0.0.41}/stjames/workflows/conformer.py +1 -0
- {stjames-0.0.39 → stjames-0.0.41}/stjames/workflows/descriptors.py +2 -1
- stjames-0.0.41/stjames/workflows/fukui.py +12 -0
- stjames-0.0.41/stjames/workflows/molecular_dynamics.py +60 -0
- stjames-0.0.41/stjames/workflows/multistage_opt.py +261 -0
- {stjames-0.0.39 → stjames-0.0.41}/stjames/workflows/redox_potential.py +9 -8
- {stjames-0.0.39 → stjames-0.0.41}/stjames/workflows/scan.py +6 -6
- stjames-0.0.41/stjames/workflows/spin_states.py +144 -0
- stjames-0.0.41/stjames/workflows/workflow.py +46 -0
- {stjames-0.0.39 → stjames-0.0.41/stjames.egg-info}/PKG-INFO +3 -3
- {stjames-0.0.39 → stjames-0.0.41}/stjames.egg-info/SOURCES.txt +14 -1
- stjames-0.0.41/tests/test_molecule.py +39 -0
- stjames-0.0.39/stjames/base.py +0 -42
- stjames-0.0.39/stjames/constraint.py +0 -16
- stjames-0.0.39/stjames/molecule.py +0 -91
- stjames-0.0.39/stjames/opt_settings.py +0 -16
- stjames-0.0.39/stjames/workflows/fukui.py +0 -13
- stjames-0.0.39/stjames/workflows/workflow.py +0 -16
- {stjames-0.0.39 → stjames-0.0.41}/LICENSE +0 -0
- {stjames-0.0.39 → stjames-0.0.41}/setup.cfg +0 -0
- {stjames-0.0.39 → stjames-0.0.41}/stjames/_deprecated_solvent_settings.py +0 -0
- {stjames-0.0.39 → stjames-0.0.41}/stjames/correction.py +0 -0
- {stjames-0.0.39 → stjames-0.0.41}/stjames/diis_settings.py +0 -0
- {stjames-0.0.39 → stjames-0.0.41}/stjames/grid_settings.py +0 -0
- {stjames-0.0.39 → stjames-0.0.41}/stjames/int_settings.py +0 -0
- {stjames-0.0.39 → stjames-0.0.41}/stjames/message.py +0 -0
- {stjames-0.0.39 → stjames-0.0.41}/stjames/mode.py +0 -0
- {stjames-0.0.39 → stjames-0.0.41}/stjames/py.typed +0 -0
- {stjames-0.0.39 → stjames-0.0.41}/stjames/scf_settings.py +0 -0
- {stjames-0.0.39 → stjames-0.0.41}/stjames/status.py +0 -0
- {stjames-0.0.39 → stjames-0.0.41}/stjames/thermochem_settings.py +0 -0
- {stjames-0.0.39 → stjames-0.0.41}/stjames/workflows/pka.py +0 -0
- {stjames-0.0.39 → stjames-0.0.41}/stjames/workflows/tautomer.py +0 -0
- {stjames-0.0.39 → stjames-0.0.41}/stjames.egg-info/dependency_links.txt +0 -0
- {stjames-0.0.39 → stjames-0.0.41}/stjames.egg-info/requires.txt +0 -0
- {stjames-0.0.39 → stjames-0.0.41}/stjames.egg-info/top_level.txt +0 -0
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
Metadata-Version: 2.1
|
|
2
2
|
Name: stjames
|
|
3
|
-
Version: 0.0.
|
|
3
|
+
Version: 0.0.41
|
|
4
4
|
Summary: standardized JSON atom/molecule encoding scheme
|
|
5
5
|
Author-email: Corin Wagen <corin@rowansci.com>
|
|
6
6
|
Project-URL: Homepage, https://github.com/rowansci/stjames
|
|
7
7
|
Project-URL: Bug Tracker, https://github.com/rowansci/stjames/issues
|
|
8
|
-
Requires-Python: >=3.
|
|
8
|
+
Requires-Python: >=3.11
|
|
9
9
|
Description-Content-Type: text/markdown
|
|
10
10
|
License-File: LICENSE
|
|
11
11
|
Requires-Dist: pydantic>=2.4
|
|
@@ -27,7 +27,7 @@ This is not intended to be run as a standalone library: it's basically just a bi
|
|
|
27
27
|
|
|
28
28
|
## Installation
|
|
29
29
|
|
|
30
|
-
To install, ensure you have Python 3.
|
|
30
|
+
To install, ensure you have Python 3.11 or newer. Then run:
|
|
31
31
|
|
|
32
32
|
```
|
|
33
33
|
pip install stjames
|
|
@@ -14,7 +14,7 @@ This is not intended to be run as a standalone library: it's basically just a bi
|
|
|
14
14
|
|
|
15
15
|
## Installation
|
|
16
16
|
|
|
17
|
-
To install, ensure you have Python 3.
|
|
17
|
+
To install, ensure you have Python 3.11 or newer. Then run:
|
|
18
18
|
|
|
19
19
|
```
|
|
20
20
|
pip install stjames
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "stjames"
|
|
3
|
-
version = "0.0.
|
|
3
|
+
version = "0.0.41"
|
|
4
4
|
description = "standardized JSON atom/molecule encoding scheme"
|
|
5
5
|
readme = "README.md"
|
|
6
|
-
requires-python = ">=3.
|
|
6
|
+
requires-python = ">=3.11"
|
|
7
7
|
authors = [
|
|
8
8
|
{ name = "Corin Wagen", email = "corin@rowansci.com" },
|
|
9
9
|
]
|
|
@@ -33,15 +33,25 @@ line-length = 160
|
|
|
33
33
|
|
|
34
34
|
[tool.ruff.lint]
|
|
35
35
|
select = [
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
36
|
+
"E", # pycodestyle errors
|
|
37
|
+
"F", # pyflakes
|
|
38
|
+
"I", # isort
|
|
39
|
+
"W", # pycodestyle warnings
|
|
40
40
|
]
|
|
41
41
|
ignore = ["E741"]
|
|
42
42
|
|
|
43
43
|
[tool.ruff.lint.per-file-ignores]
|
|
44
44
|
"__init__.py" = ["F401", "F403"]
|
|
45
45
|
|
|
46
|
+
[tool.pytest.ini_options]
|
|
47
|
+
testpaths = ["stjames", "tests"]
|
|
48
|
+
addopts = "--doctest-modules"
|
|
49
|
+
doctest_optionflags = "NORMALIZE_WHITESPACE"
|
|
50
|
+
markers = [
|
|
51
|
+
"smoke: sanity tests to reveal simple failures"
|
|
52
|
+
]
|
|
53
|
+
|
|
46
54
|
[tool.mypy]
|
|
47
55
|
plugins = ["pydantic.mypy"]
|
|
56
|
+
strict = true
|
|
57
|
+
warn_unused_ignores = true
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
# ruff: noqa: I001
|
|
2
2
|
|
|
3
3
|
from .calculation import *
|
|
4
|
+
from .atom import *
|
|
5
|
+
from .periodic_cell import *
|
|
4
6
|
from .molecule import *
|
|
5
7
|
from .workflows import *
|
|
6
8
|
|
|
@@ -21,3 +23,4 @@ from .mode import *
|
|
|
21
23
|
from .status import *
|
|
22
24
|
from .constraint import *
|
|
23
25
|
from .message import *
|
|
26
|
+
from .types import *
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
from typing import Self, Sequence
|
|
2
|
+
|
|
3
|
+
from pydantic import NonNegativeInt
|
|
4
|
+
|
|
5
|
+
from .base import Base
|
|
6
|
+
from .data import ELEMENT_SYMBOL, SYMBOL_ELEMENT
|
|
7
|
+
from .types import Vector3D
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class Atom(Base):
|
|
11
|
+
atomic_number: NonNegativeInt
|
|
12
|
+
position: Vector3D # in Å
|
|
13
|
+
|
|
14
|
+
def __repr__(self) -> str:
|
|
15
|
+
"""
|
|
16
|
+
>>> Atom(atomic_number=2, position=[0, 1, 2])
|
|
17
|
+
Atom(2, [0.00000, 1.00000, 2.00000])
|
|
18
|
+
"""
|
|
19
|
+
x, y, z = self.position
|
|
20
|
+
return f"Atom({self.atomic_number}, [{x:.5f}, {y:.5f}, {z:.5f}])"
|
|
21
|
+
|
|
22
|
+
def __str__(self) -> str:
|
|
23
|
+
"""
|
|
24
|
+
>>> str(Atom(atomic_number=2, position=[0, 1, 2]))
|
|
25
|
+
'He 0.0000000000 1.0000000000 2.0000000000'
|
|
26
|
+
"""
|
|
27
|
+
x, y, z = self.position
|
|
28
|
+
return f"{self.atomic_symbol:2} {x:15.10f} {y:15.10f} {z:15.10f}"
|
|
29
|
+
|
|
30
|
+
@property
|
|
31
|
+
def atomic_symbol(self) -> str:
|
|
32
|
+
"""
|
|
33
|
+
>>> Atom(atomic_number=2, position=[0, 1, 2]).atomic_symbol
|
|
34
|
+
'He'
|
|
35
|
+
"""
|
|
36
|
+
return ELEMENT_SYMBOL[self.atomic_number]
|
|
37
|
+
|
|
38
|
+
def edited(self, atomic_number: int | None = None, position: Sequence[float] | None = None) -> Self:
|
|
39
|
+
"""
|
|
40
|
+
Create a new Atom with the specified changes.
|
|
41
|
+
|
|
42
|
+
>>> a = Atom(atomic_number=2, position=[0, 1, 2])
|
|
43
|
+
>>> a2 = a.edited(3)
|
|
44
|
+
>>> a is a2
|
|
45
|
+
False
|
|
46
|
+
>>> a2
|
|
47
|
+
Atom(3, [0.00000, 1.00000, 2.00000])
|
|
48
|
+
"""
|
|
49
|
+
if atomic_number is None:
|
|
50
|
+
atomic_number = self.atomic_number
|
|
51
|
+
if position is None:
|
|
52
|
+
position = list(self.position)
|
|
53
|
+
|
|
54
|
+
return self.__class__(atomic_number=atomic_number, position=position)
|
|
55
|
+
|
|
56
|
+
@classmethod
|
|
57
|
+
def from_xyz(cls: type[Self], xyz_line: str) -> Self:
|
|
58
|
+
"""
|
|
59
|
+
>>> Atom.from_xyz("H 0 0 0")
|
|
60
|
+
Atom(1, [0.00000, 0.00000, 0.00000])
|
|
61
|
+
"""
|
|
62
|
+
name, *xyz = xyz_line.split()
|
|
63
|
+
symbol = int(name) if name.isdigit() else SYMBOL_ELEMENT[name]
|
|
64
|
+
if not len(xyz) == 3:
|
|
65
|
+
raise ValueError("XYZ file should have 3 coordinates per atom")
|
|
66
|
+
return cls(atomic_number=symbol, position=xyz)
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
from enum import Enum
|
|
2
|
+
from typing import Annotated, Any, Hashable, TypeVar
|
|
3
|
+
|
|
4
|
+
import numpy as np
|
|
5
|
+
import pydantic
|
|
6
|
+
|
|
7
|
+
_T = TypeVar("_T")
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class Base(pydantic.BaseModel):
|
|
11
|
+
@pydantic.field_validator("*", mode="before")
|
|
12
|
+
@classmethod
|
|
13
|
+
def coerce_numpy(cls, val: _T) -> _T | list[Any]:
|
|
14
|
+
if isinstance(val, np.ndarray):
|
|
15
|
+
return val.tolist() # type: ignore [no-any-return, unused-ignore]
|
|
16
|
+
|
|
17
|
+
return val
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class LowercaseStrEnum(str, Enum):
|
|
21
|
+
"""Enum where hyphens, underscores, and case are ignored."""
|
|
22
|
+
|
|
23
|
+
@classmethod
|
|
24
|
+
def _missing_(cls, value: object) -> str | None:
|
|
25
|
+
for member in cls:
|
|
26
|
+
if isinstance(value, str):
|
|
27
|
+
if member.lower().replace("-", "").replace("_", "") == value.lower().replace("-", "").replace("_", ""):
|
|
28
|
+
return member
|
|
29
|
+
return None
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
# cf. https://github.com/pydantic/pydantic-core/pull/820#issuecomment-1670475909
|
|
33
|
+
_H = TypeVar("_H", bound=Hashable)
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def _validate_unique_list(v: list[_H]) -> list[_H]:
|
|
37
|
+
if len(v) != len(set(v)):
|
|
38
|
+
raise ValueError("this list must be unique, and isn't!")
|
|
39
|
+
return v
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
UniqueList = Annotated[list[_H], pydantic.AfterValidator(_validate_unique_list)]
|
|
@@ -1,10 +1,6 @@
|
|
|
1
|
-
import
|
|
2
|
-
from pydantic import PositiveFloat, PositiveInt
|
|
1
|
+
from typing import Optional, Self
|
|
3
2
|
|
|
4
|
-
|
|
5
|
-
from typing import Optional, Self
|
|
6
|
-
except ImportError:
|
|
7
|
-
from typing_extensions import Optional, Self
|
|
3
|
+
from pydantic import PositiveFloat, PositiveInt, model_validator
|
|
8
4
|
|
|
9
5
|
from .base import Base
|
|
10
6
|
|
|
@@ -14,7 +10,7 @@ class BasisSetOverride(Base):
|
|
|
14
10
|
atomic_numbers: Optional[list[PositiveInt]] = None
|
|
15
11
|
atoms: Optional[list[PositiveInt]] = None # 1-indexed
|
|
16
12
|
|
|
17
|
-
@
|
|
13
|
+
@model_validator(mode="after")
|
|
18
14
|
def check_override(self) -> Self:
|
|
19
15
|
# ^ is xor
|
|
20
16
|
assert (self.atomic_numbers is not None) ^ (self.atoms is not None), "Exactly one of ``atomic_numbers`` or ``atoms`` must be specified!"
|
|
@@ -5,6 +5,7 @@ from .message import Message
|
|
|
5
5
|
from .molecule import Molecule
|
|
6
6
|
from .settings import Settings
|
|
7
7
|
from .status import Status
|
|
8
|
+
from .types import UUID
|
|
8
9
|
|
|
9
10
|
|
|
10
11
|
class StJamesVersion(LowercaseStrEnum):
|
|
@@ -29,6 +30,7 @@ class Calculation(Base):
|
|
|
29
30
|
messages: list[Message] = []
|
|
30
31
|
|
|
31
32
|
engine: Optional[str] = "peregrine"
|
|
33
|
+
uuids: list[UUID | None] | None = None
|
|
32
34
|
|
|
33
35
|
# not to be changed by end users, diff. versions will have diff. defaults
|
|
34
36
|
json_format: str = StJamesVersion.V0
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
from pydantic import PositiveFloat, PositiveInt
|
|
2
|
+
|
|
3
|
+
from .base import Base, LowercaseStrEnum
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class ConstraintType(LowercaseStrEnum):
|
|
7
|
+
"""Different sorts of constraints."""
|
|
8
|
+
|
|
9
|
+
BOND = "bond"
|
|
10
|
+
ANGLE = "angle"
|
|
11
|
+
DIHEDRAL = "dihedral"
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class Constraint(Base):
|
|
15
|
+
"""Represents a single (absolute) constraint."""
|
|
16
|
+
|
|
17
|
+
constraint_type: ConstraintType
|
|
18
|
+
atoms: list[PositiveInt] # 1-indexed
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class PairwiseHarmonicConstraint(Base):
|
|
22
|
+
"""
|
|
23
|
+
Represents a harmonic constraint, with a characteristic spring constant.
|
|
24
|
+
"""
|
|
25
|
+
|
|
26
|
+
atoms: tuple[PositiveInt, PositiveInt] # 1-indexed
|
|
27
|
+
spring_constant: PositiveFloat # kcal/mol / Å**2
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
class SphericalHarmonicConstraint(Base):
|
|
31
|
+
"""
|
|
32
|
+
Represents a spherical harmonic constraint to keep a system near the origin.
|
|
33
|
+
"""
|
|
34
|
+
|
|
35
|
+
confining_radius: PositiveFloat
|
|
36
|
+
confining_force_constant: PositiveFloat = 10 # kcal/mol / Å**2
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
from .elements import *
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
"""Read elemental data from files."""
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
from collections import namedtuple
|
|
5
|
+
from importlib import resources
|
|
6
|
+
|
|
7
|
+
data_dir = resources.files("stjames").joinpath("data")
|
|
8
|
+
|
|
9
|
+
with data_dir.joinpath("symbol_element.json").open() as f:
|
|
10
|
+
SYMBOL_ELEMENT: dict[str, int] = json.loads(f.read())
|
|
11
|
+
|
|
12
|
+
ELEMENT_SYMBOL = {v: k for k, v in SYMBOL_ELEMENT.items()}
|
|
13
|
+
|
|
14
|
+
Isotope = namedtuple("Isotope", ["relative_atomic_mass", "isotopic_composition", "standard_atomic_weight"])
|
|
15
|
+
with data_dir.joinpath("nist_isotopes.json").open() as f:
|
|
16
|
+
d = json.loads(f.read())
|
|
17
|
+
|
|
18
|
+
ISOTOPES: dict[int, dict[int, Isotope]] = {
|
|
19
|
+
int(k): {
|
|
20
|
+
int(kk): Isotope(*vv)
|
|
21
|
+
for kk, vv in v.items() # stay open
|
|
22
|
+
}
|
|
23
|
+
for k, v in d.items()
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
with data_dir.joinpath("bragg_radii.json").open() as f:
|
|
27
|
+
BRAGG_RADII: dict[int, float] = json.loads(f.read())
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Read the NIST isotopes data file and write it to a JSON file.
|
|
3
|
+
|
|
4
|
+
NIST Isotopes data from:
|
|
5
|
+
https://physics.nist.gov/cgi-bin/Compositions/stand_alone.pl?ele=&all=all&ascii=ascii2
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import json
|
|
9
|
+
from collections import defaultdict
|
|
10
|
+
from importlib import resources
|
|
11
|
+
from typing import Callable, TypeVar
|
|
12
|
+
|
|
13
|
+
data_dir = resources.files("stjames").joinpath("data")
|
|
14
|
+
|
|
15
|
+
_T = TypeVar("_T")
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def process_line(line: str, fmt: Callable[[str], _T] = str) -> _T: # type: ignore[assignment]
|
|
19
|
+
"""
|
|
20
|
+
Process a line from the NIST data file.
|
|
21
|
+
|
|
22
|
+
:param line: line to process
|
|
23
|
+
:param fmt: function to format the value
|
|
24
|
+
>>> process_line("Atomic Number = 1", int)
|
|
25
|
+
1
|
|
26
|
+
"""
|
|
27
|
+
return fmt(line.split("=")[-1].strip())
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def fmt_float(val: str) -> float:
|
|
31
|
+
"""
|
|
32
|
+
Format a float from the NIST data file.
|
|
33
|
+
|
|
34
|
+
>>> fmt_float(" 1.00784(7)")
|
|
35
|
+
1.00784
|
|
36
|
+
"""
|
|
37
|
+
return float(val.strip().split("(")[0])
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def fmt_maybe_list(val: str) -> float:
|
|
41
|
+
"""
|
|
42
|
+
Format a float or list of floats from the NIST data file.
|
|
43
|
+
|
|
44
|
+
Only the first value is returned.
|
|
45
|
+
|
|
46
|
+
>>> fmt_maybe_list("1.00784(7)")
|
|
47
|
+
1.00784
|
|
48
|
+
>>> fmt_maybe_list(" [1.00784,1.00811]")
|
|
49
|
+
1.00784
|
|
50
|
+
>>> fmt_maybe_list(" [98]")
|
|
51
|
+
98.0
|
|
52
|
+
"""
|
|
53
|
+
val = val.strip()
|
|
54
|
+
if val.startswith("["):
|
|
55
|
+
val = val[1:-1].split(",")[0]
|
|
56
|
+
return fmt_float(val)
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def process_chunk(chunk: str) -> tuple[int, int, tuple[float, float, float]]:
|
|
60
|
+
r"""
|
|
61
|
+
Atomic Number, Mass Number, (Relative Atomic Mass, Isotopic Composition, Standard Atomic Weight)
|
|
62
|
+
|
|
63
|
+
>>> process_chunk('''\
|
|
64
|
+
... Atomic Number = 1
|
|
65
|
+
... Atomic Symbol = H
|
|
66
|
+
... Mass Number = 1
|
|
67
|
+
... Relative Atomic Mass = 1.00784(7)
|
|
68
|
+
... Isotopic Composition = 0.999885(70)
|
|
69
|
+
... Standard Atomic Weight = [1.00784,1.00811]
|
|
70
|
+
... Notes = m
|
|
71
|
+
... ''')
|
|
72
|
+
(1, 1, (1.00784, 0.999885, 1.00784))
|
|
73
|
+
"""
|
|
74
|
+
lines = chunk.splitlines()
|
|
75
|
+
|
|
76
|
+
atomic_number = process_line(lines[0], int)
|
|
77
|
+
_atomic_symbol = process_line(lines[1], str)
|
|
78
|
+
mass_number = process_line(lines[2], int)
|
|
79
|
+
relative_atomic_mass = process_line(lines[3], fmt_float)
|
|
80
|
+
try:
|
|
81
|
+
isotopic_composition = process_line(lines[4], fmt_float)
|
|
82
|
+
except ValueError:
|
|
83
|
+
isotopic_composition = 0
|
|
84
|
+
try:
|
|
85
|
+
standard_atomic_weight = process_line(lines[5], fmt_maybe_list)
|
|
86
|
+
except ValueError:
|
|
87
|
+
standard_atomic_weight = relative_atomic_mass
|
|
88
|
+
|
|
89
|
+
return atomic_number, mass_number, (relative_atomic_mass, isotopic_composition, standard_atomic_weight)
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
def read_nist_isotopes() -> dict[int, dict[int, tuple[float, float, float]]]:
|
|
93
|
+
"""
|
|
94
|
+
Read the NIST data file and write it to a JSON file.
|
|
95
|
+
|
|
96
|
+
{Atomic Number: {Mass Number, (Relative Atomic Mass, Isotopic Composition, Standard Atomic Weight)}}
|
|
97
|
+
"""
|
|
98
|
+
with data_dir.joinpath("nist_isotopes.txt").open() as f:
|
|
99
|
+
next(f), next(f) # Skip the first two lines
|
|
100
|
+
nist_isotopes = f.read()
|
|
101
|
+
|
|
102
|
+
isotopes: dict[int, dict[int, tuple[float, float, float]]] = defaultdict(dict)
|
|
103
|
+
for chunk in nist_isotopes.split("\n\n"):
|
|
104
|
+
atomic_number, mass_number, values = process_chunk(chunk)
|
|
105
|
+
isotopes[atomic_number][mass_number] = values
|
|
106
|
+
|
|
107
|
+
with open("nist_isotopes.json", "w") as f:
|
|
108
|
+
json.dump(isotopes, f)
|
|
109
|
+
|
|
110
|
+
return isotopes
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
if __name__ == "__main__":
|
|
114
|
+
from pprint import pprint
|
|
115
|
+
|
|
116
|
+
pprint(read_nist_isotopes())
|
|
@@ -29,10 +29,43 @@ class Method(LowercaseStrEnum):
|
|
|
29
29
|
|
|
30
30
|
AIMNET2_WB97MD3 = "aimnet2_wb97md3"
|
|
31
31
|
|
|
32
|
+
GFN_FF = "gfn_ff"
|
|
32
33
|
GFN0_XTB = "gfn0_xtb"
|
|
33
34
|
GFN1_XTB = "gfn1_xtb"
|
|
34
35
|
GFN2_XTB = "gfn2_xtb"
|
|
35
|
-
GFN_FF = "gfn_ff"
|
|
36
36
|
|
|
37
37
|
# this was going to be removed, but Jonathon wrote such a nice basis set test... it's off the front end.
|
|
38
38
|
BP86 = "bp86"
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
MLFF = [
|
|
42
|
+
Method.AIMNET2_WB97MD3,
|
|
43
|
+
]
|
|
44
|
+
|
|
45
|
+
XTB_METHODS = [
|
|
46
|
+
Method.GFN_FF,
|
|
47
|
+
Method.GFN0_XTB,
|
|
48
|
+
Method.GFN1_XTB,
|
|
49
|
+
Method.GFN2_XTB,
|
|
50
|
+
]
|
|
51
|
+
|
|
52
|
+
COMPOSITE_METHODS = [
|
|
53
|
+
Method.HF3C,
|
|
54
|
+
Method.B973C,
|
|
55
|
+
Method.R2SCAN3C,
|
|
56
|
+
Method.WB97X3C,
|
|
57
|
+
]
|
|
58
|
+
|
|
59
|
+
PREPACKAGED_METHODS = [
|
|
60
|
+
*MLFF,
|
|
61
|
+
*XTB_METHODS,
|
|
62
|
+
*COMPOSITE_METHODS,
|
|
63
|
+
]
|
|
64
|
+
|
|
65
|
+
METHODS_WITH_CORRECTION = [
|
|
66
|
+
Method.WB97XD3,
|
|
67
|
+
Method.WB97XV,
|
|
68
|
+
Method.WB97MV,
|
|
69
|
+
Method.WB97MD3BJ,
|
|
70
|
+
Method.DSDBLYPD3BJ,
|
|
71
|
+
]
|
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
from pathlib import Path
|
|
2
|
+
from typing import Iterable, Optional, Self
|
|
3
|
+
|
|
4
|
+
import pydantic
|
|
5
|
+
from pydantic import NonNegativeInt, PositiveInt
|
|
6
|
+
|
|
7
|
+
from .atom import Atom
|
|
8
|
+
from .base import Base
|
|
9
|
+
from .periodic_cell import PeriodicCell
|
|
10
|
+
from .types import Matrix3x3, Vector3D, Vector3DPerAtom
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class MoleculeReadError(RuntimeError):
|
|
14
|
+
pass
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class VibrationalMode(Base):
|
|
18
|
+
frequency: float # in cm-1
|
|
19
|
+
reduced_mass: float # amu
|
|
20
|
+
|
|
21
|
+
# todo - check units here?
|
|
22
|
+
force_constant: float
|
|
23
|
+
displacements: Vector3DPerAtom
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class Molecule(Base):
|
|
27
|
+
charge: int
|
|
28
|
+
multiplicity: PositiveInt
|
|
29
|
+
atoms: list[Atom]
|
|
30
|
+
|
|
31
|
+
# for periodic boundary conditions
|
|
32
|
+
cell: Optional[PeriodicCell] = None
|
|
33
|
+
|
|
34
|
+
energy: Optional[float] = None # in Hartree
|
|
35
|
+
scf_iterations: Optional[NonNegativeInt] = None
|
|
36
|
+
scf_completed: Optional[bool] = None
|
|
37
|
+
elapsed: Optional[float] = None # in seconds
|
|
38
|
+
|
|
39
|
+
homo_lumo_gap: Optional[float] = None # in eV
|
|
40
|
+
|
|
41
|
+
gradient: Optional[Vector3DPerAtom] = None # Hartree/Å
|
|
42
|
+
stress: Optional[Matrix3x3] = None # Hartree/Å
|
|
43
|
+
|
|
44
|
+
velocities: Optional[Vector3DPerAtom] = None # Å/fs
|
|
45
|
+
|
|
46
|
+
mulliken_charges: Optional[list[float]] = None
|
|
47
|
+
mulliken_spin_densities: Optional[list[float]] = None
|
|
48
|
+
dipole: Optional[Vector3D] = None # in Debye
|
|
49
|
+
|
|
50
|
+
vibrational_modes: Optional[list[VibrationalMode]] = None
|
|
51
|
+
|
|
52
|
+
zero_point_energy: Optional[float] = None
|
|
53
|
+
thermal_energy_corr: Optional[float] = None
|
|
54
|
+
thermal_enthalpy_corr: Optional[float] = None
|
|
55
|
+
thermal_free_energy_corr: Optional[float] = None
|
|
56
|
+
|
|
57
|
+
def __len__(self) -> int:
|
|
58
|
+
return len(self.atoms)
|
|
59
|
+
|
|
60
|
+
def distance(self, atom1: PositiveInt, atom2: PositiveInt) -> float:
|
|
61
|
+
r"""
|
|
62
|
+
Get the distance between atoms.
|
|
63
|
+
|
|
64
|
+
>>> mol = Molecule.from_xyz("H 0 1 0\nH 0 0 1")
|
|
65
|
+
>>> mol.distance(1, 2)
|
|
66
|
+
1.4142135623730951
|
|
67
|
+
"""
|
|
68
|
+
return sum((q2 - q1) ** 2 for q1, q2 in zip(self.atoms[atom1 - 1].position, self.atoms[atom2 - 1].position)) ** 0.5 # type: ignore [no-any-return,unused-ignore]
|
|
69
|
+
|
|
70
|
+
@property
|
|
71
|
+
def coordinates(self) -> Vector3DPerAtom:
|
|
72
|
+
return [a.position for a in self.atoms]
|
|
73
|
+
|
|
74
|
+
@property
|
|
75
|
+
def atomic_numbers(self) -> list[NonNegativeInt]:
|
|
76
|
+
return [a.atomic_number for a in self.atoms]
|
|
77
|
+
|
|
78
|
+
@property
|
|
79
|
+
def sum_energy_zpe(self) -> Optional[float]:
|
|
80
|
+
if (self.energy is None) or (self.zero_point_energy is None):
|
|
81
|
+
return None
|
|
82
|
+
return self.energy + self.zero_point_energy
|
|
83
|
+
|
|
84
|
+
@property
|
|
85
|
+
def sum_energy_thermal_corr(self) -> Optional[float]:
|
|
86
|
+
if (self.energy is None) or (self.thermal_energy_corr is None):
|
|
87
|
+
return None
|
|
88
|
+
return self.energy + self.thermal_energy_corr
|
|
89
|
+
|
|
90
|
+
@property
|
|
91
|
+
def sum_energy_enthalpy(self) -> Optional[float]:
|
|
92
|
+
if (self.energy is None) or (self.thermal_enthalpy_corr is None):
|
|
93
|
+
return None
|
|
94
|
+
return self.energy + self.thermal_enthalpy_corr
|
|
95
|
+
|
|
96
|
+
@property
|
|
97
|
+
def sum_energy_free_energy(self) -> Optional[float]:
|
|
98
|
+
if (self.energy is None) or (self.thermal_free_energy_corr is None):
|
|
99
|
+
return None
|
|
100
|
+
return self.energy + self.thermal_free_energy_corr
|
|
101
|
+
|
|
102
|
+
@pydantic.model_validator(mode="after")
|
|
103
|
+
def check_electron_sanity(self) -> Self:
|
|
104
|
+
num_electrons = sum(self.atomic_numbers) - self.charge
|
|
105
|
+
num_unpaired_electrons = self.multiplicity - 1
|
|
106
|
+
if (num_electrons - num_unpaired_electrons) % 2 != 0:
|
|
107
|
+
raise ValueError(
|
|
108
|
+
f"The combination of {num_electrons} electrons, charge {self.charge}, and multiplicity {self.multiplicity} is impossible. "
|
|
109
|
+
"Double-check the charge and multiplicity values given and verify that they are correct."
|
|
110
|
+
)
|
|
111
|
+
|
|
112
|
+
return self
|
|
113
|
+
|
|
114
|
+
@classmethod
|
|
115
|
+
def from_file(cls: type[Self], filename: Path | str, format: str | None = None, charge: int = 0, multiplicity: PositiveInt = 1) -> Self:
|
|
116
|
+
r"""
|
|
117
|
+
Read a molecule from a file.
|
|
118
|
+
|
|
119
|
+
>>> import tempfile
|
|
120
|
+
>>> with tempfile.NamedTemporaryFile("w+", suffix=".xyz") as f:
|
|
121
|
+
... _ = f.write("2\nComment\nH 0 0 0\nF 0 0 1")
|
|
122
|
+
... _ = f.seek(0)
|
|
123
|
+
... mol = Molecule.from_file(f.name)
|
|
124
|
+
>>> print(mol.to_xyz())
|
|
125
|
+
2
|
|
126
|
+
<BLANKLINE>
|
|
127
|
+
H 0.0000000000 0.0000000000 0.0000000000
|
|
128
|
+
F 0.0000000000 0.0000000000 1.0000000000
|
|
129
|
+
"""
|
|
130
|
+
filename = Path(filename)
|
|
131
|
+
if not format:
|
|
132
|
+
format = filename.suffix[1:]
|
|
133
|
+
|
|
134
|
+
with open(filename) as f:
|
|
135
|
+
match format:
|
|
136
|
+
case "xyz":
|
|
137
|
+
return cls.from_xyz_lines(f.readlines(), charge=charge, multiplicity=multiplicity)
|
|
138
|
+
case _:
|
|
139
|
+
raise ValueError(f"Unsupported {format=}")
|
|
140
|
+
|
|
141
|
+
@classmethod
|
|
142
|
+
def from_xyz(cls: type[Self], xyz: str, charge: int = 0, multiplicity: PositiveInt = 1) -> Self:
|
|
143
|
+
r"""
|
|
144
|
+
Generate a Molecule from an XYZ string.
|
|
145
|
+
|
|
146
|
+
Note: only supports single molecule inputs.
|
|
147
|
+
|
|
148
|
+
>>> len(Molecule.from_xyz("2\nComment\nH 0 0 0\nH 0 0 1"))
|
|
149
|
+
2
|
|
150
|
+
"""
|
|
151
|
+
return cls.from_xyz_lines(xyz.strip().splitlines(), charge=charge, multiplicity=multiplicity)
|
|
152
|
+
|
|
153
|
+
@classmethod
|
|
154
|
+
def from_xyz_lines(cls: type[Self], lines: Iterable[str], charge: int = 0, multiplicity: PositiveInt = 1) -> Self:
|
|
155
|
+
lines = list(lines)
|
|
156
|
+
if len(lines[0].split()) == 1:
|
|
157
|
+
natoms = lines[0].strip()
|
|
158
|
+
if not natoms.isdigit() or (int(lines[0]) != len(lines) - 2):
|
|
159
|
+
raise MoleculeReadError(f"First line of XYZ file should be the number of atoms, got: {lines[0]} != {len(lines) - 2}")
|
|
160
|
+
lines = lines[2:]
|
|
161
|
+
|
|
162
|
+
try:
|
|
163
|
+
return cls(atoms=[Atom.from_xyz(line) for line in lines], charge=charge, multiplicity=multiplicity)
|
|
164
|
+
except Exception as e:
|
|
165
|
+
raise MoleculeReadError("Error reading molecule from xyz") from e
|
|
166
|
+
|
|
167
|
+
def to_xyz(self, comment: str = "", out_file: Path | str | None = None) -> str:
|
|
168
|
+
r"""
|
|
169
|
+
Generate an XYZ string.
|
|
170
|
+
|
|
171
|
+
>>> mol = Molecule.from_xyz("2\nComment\nH 0 1 2\nF 1 2 3")
|
|
172
|
+
>>> print(mol.to_xyz(comment="HF"))
|
|
173
|
+
2
|
|
174
|
+
HF
|
|
175
|
+
H 0.0000000000 1.0000000000 2.0000000000
|
|
176
|
+
F 1.0000000000 2.0000000000 3.0000000000
|
|
177
|
+
>>> import tempfile
|
|
178
|
+
>>> with tempfile.TemporaryDirectory() as directory:
|
|
179
|
+
... file = Path(directory) / "mol.xyz"
|
|
180
|
+
... out = mol.to_xyz(comment="HF", out_file=file)
|
|
181
|
+
... with file.open() as f:
|
|
182
|
+
... Molecule.from_xyz(f.read()).to_xyz("HF") == out
|
|
183
|
+
True
|
|
184
|
+
"""
|
|
185
|
+
geom = "\n".join(map(str, self.atoms))
|
|
186
|
+
out = f"{len(self)}\n{comment}\n{geom}"
|
|
187
|
+
|
|
188
|
+
if out_file:
|
|
189
|
+
with Path(out_file).open("w") as f:
|
|
190
|
+
f.write(out)
|
|
191
|
+
|
|
192
|
+
return out
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
from typing import Sequence
|
|
2
|
+
|
|
3
|
+
from pydantic import PositiveFloat, PositiveInt
|
|
4
|
+
|
|
5
|
+
from .base import Base
|
|
6
|
+
from .constraint import Constraint
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class OptimizationSettings(Base):
|
|
10
|
+
max_steps: PositiveInt = 250
|
|
11
|
+
transition_state: bool = False
|
|
12
|
+
|
|
13
|
+
# when are we converged? (Hartree and Hartree/Å)
|
|
14
|
+
max_gradient_threshold: PositiveFloat = 7e-4
|
|
15
|
+
rms_gradient_threshold: PositiveFloat = 6e-4
|
|
16
|
+
energy_threshold: PositiveFloat = 1e-6
|
|
17
|
+
|
|
18
|
+
# for periodic systems only
|
|
19
|
+
optimize_cell: bool = False
|
|
20
|
+
|
|
21
|
+
constraints: Sequence[Constraint] = tuple()
|