matcraft-kit 0.1.0__py3-none-any.whl
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.
- matcraft_kit-0.1.0.dist-info/METADATA +13 -0
- matcraft_kit-0.1.0.dist-info/RECORD +25 -0
- matcraft_kit-0.1.0.dist-info/WHEEL +5 -0
- matcraft_kit-0.1.0.dist-info/entry_points.txt +2 -0
- matcraft_kit-0.1.0.dist-info/top_level.txt +1 -0
- mckit/__init__.py +25 -0
- mckit/cli.py +84 -0
- mckit/core/__init__.py +5 -0
- mckit/core/lattice.py +117 -0
- mckit/core/structure.py +160 -0
- mckit/core/tool.py +48 -0
- mckit/io/__init__.py +10 -0
- mckit/io/reader.py +47 -0
- mckit/io/writer.py +41 -0
- mckit/observe/__init__.py +21 -0
- mckit/observe/fundamental.py +155 -0
- mckit/observe/info.py +687 -0
- mckit/operate/__init__.py +42 -0
- mckit/operate/bulk.py +142 -0
- mckit/operate/defect_creation.py +1052 -0
- mckit/operate/interface.py +1197 -0
- mckit/operate/molecule_creation.py +244 -0
- mckit/operate/perturbation.py +589 -0
- mckit/operate/supercell.py +207 -0
- mckit/operate/surface.py +1285 -0
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: matcraft-kit
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: MatCraft Toolkit
|
|
5
|
+
Requires-Python: >=3.9
|
|
6
|
+
Requires-Dist: numpy>=1.21
|
|
7
|
+
Requires-Dist: ase>=3.22
|
|
8
|
+
Requires-Dist: pymatgen>=2022.0.0
|
|
9
|
+
Requires-Dist: pymatgen-analysis-defects>=2024.10.22
|
|
10
|
+
Requires-Dist: rdkit>=2025.9.6
|
|
11
|
+
Provides-Extra: dev
|
|
12
|
+
Requires-Dist: pytest; extra == "dev"
|
|
13
|
+
Requires-Dist: pytest-cov; extra == "dev"
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
mckit/__init__.py,sha256=Wd87JxoIkVKrJakY5RoT3IvSfDoQ5q4w_8aM5stxYrU,697
|
|
2
|
+
mckit/cli.py,sha256=6LVCspQzcXozltprvvCBMrybF0LGrlrjuRf_g-uS_n4,2627
|
|
3
|
+
mckit/core/__init__.py,sha256=cP0Ru_YhC6ruHa6yjyauA8XR1ATmmT0VuscKsI0wJ30,162
|
|
4
|
+
mckit/core/lattice.py,sha256=BT6G4KFxHZjyYWag68q0yThGrvMnsLUSI2yOPF8Oe8I,3504
|
|
5
|
+
mckit/core/structure.py,sha256=h0mzSgd7oTJXXs8SA0zRgRrKeJd9upAVw9V6D26OoK8,5579
|
|
6
|
+
mckit/core/tool.py,sha256=S9c24YGTNaaW5Q__RougE8Q4YeSuumy9Ugcm3YIpU8Y,1562
|
|
7
|
+
mckit/io/__init__.py,sha256=OEUgXPIT1H1yA33ixVKYJyT7jbqoh6chZkjqKZA0rpg,239
|
|
8
|
+
mckit/io/reader.py,sha256=enWiZk9UOsRyjtvA6rha3umvxKbvbSwpTD7nIdrLVv8,1389
|
|
9
|
+
mckit/io/writer.py,sha256=nEmnEelKyjEvhi4rRIwc4vAQlk1mdcMd8_L5al0InuM,1196
|
|
10
|
+
mckit/observe/__init__.py,sha256=Fl8izOl_oEBB_QzqE9emN3kW2-4dW5vQ7kUirVGuy-o,648
|
|
11
|
+
mckit/observe/fundamental.py,sha256=vMHfB0KNAUDEbp2eQSS2yLZwlSW3e39mvDFUnOqlVT0,5568
|
|
12
|
+
mckit/observe/info.py,sha256=CFIIQooF4xqPLcm-hVNIaGSNW04iUUBz9wfY12WqYtc,24997
|
|
13
|
+
mckit/operate/__init__.py,sha256=xzSRRnqK3LNrZAPWvzfoFV6xOZ8AkWYsgWcPhzoN150,1532
|
|
14
|
+
mckit/operate/bulk.py,sha256=hra4slQUpC-2gwt-A4a3mNDHJXo7jissaGbJu_dxSSM,5122
|
|
15
|
+
mckit/operate/defect_creation.py,sha256=tBwtLX_SxJ3Hg78Z38xJijREHeJARjcKSA1oHrt9g7E,35316
|
|
16
|
+
mckit/operate/interface.py,sha256=XSeY59NOAsLWooOlFm_SnhjyKLsN6AiZXZtTVpIHbiw,43924
|
|
17
|
+
mckit/operate/molecule_creation.py,sha256=q82-y3fJBpe6-6dFH4gTuXMQfQ3xMZoYRlbXN1waeRg,8465
|
|
18
|
+
mckit/operate/perturbation.py,sha256=YABOXSdQkB7_J-xIAvo0NaPIWyPY_8jXJr7dcNEuG3U,20415
|
|
19
|
+
mckit/operate/supercell.py,sha256=yGISGZTeTcSMz2iVzG1ca5GPuRW8u0MMDHwmI0-kbIY,6609
|
|
20
|
+
mckit/operate/surface.py,sha256=hn5zjBwhUQGdg8USeESXoeqObx26TTTKJQita4GXvfA,45151
|
|
21
|
+
matcraft_kit-0.1.0.dist-info/METADATA,sha256=F1BW2iQDu22Zxji_aVjGIpxDPR6WkOOZWyJJFmq6ars,388
|
|
22
|
+
matcraft_kit-0.1.0.dist-info/WHEEL,sha256=aeYiig01lYGDzBgS8HxWXOg3uV61G9ijOsup-k9o1sk,91
|
|
23
|
+
matcraft_kit-0.1.0.dist-info/entry_points.txt,sha256=sCf8fpBCkjx6M31fS9gLV-cPV8_M9QiDdDtn__oDgxw,41
|
|
24
|
+
matcraft_kit-0.1.0.dist-info/top_level.txt,sha256=cNncfWCbWtrmRnK9O4jEneIGm149-ABESk2m9CSgTms,6
|
|
25
|
+
matcraft_kit-0.1.0.dist-info/RECORD,,
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
mckit
|
mckit/__init__.py
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
"""
|
|
2
|
+
matmod - Materials Modelling Toolkit
|
|
3
|
+
|
|
4
|
+
A modular framework for building and analyzing atomic structures.
|
|
5
|
+
Backed by ASE and pymatgen for crystallographic computations.
|
|
6
|
+
|
|
7
|
+
Two main subsystems:
|
|
8
|
+
- operations : tools that BUILD or MODIFY structures (bulk, surface, defect, ...)
|
|
9
|
+
- observations: tools that INSPECT structures (info, checks, properties, ...)
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
from mckit.core.lattice import Lattice
|
|
13
|
+
from mckit.core.structure import Structure
|
|
14
|
+
from mckit.core.tool import Operation, Observation
|
|
15
|
+
from mckit.io import read_structure, write_structure
|
|
16
|
+
|
|
17
|
+
__version__ = "0.2.0"
|
|
18
|
+
__all__ = [
|
|
19
|
+
"Lattice",
|
|
20
|
+
"Structure",
|
|
21
|
+
"Operation",
|
|
22
|
+
"Observation",
|
|
23
|
+
"read_structure",
|
|
24
|
+
"write_structure",
|
|
25
|
+
]
|
mckit/cli.py
ADDED
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
"""mckit CLI — auto-dispatching central entry point.
|
|
2
|
+
|
|
3
|
+
Any module under ``mckit.operate`` or ``mckit.observe`` can hook into the CLI
|
|
4
|
+
by defining a ``register_cli(subparsers)`` function. This file discovers and
|
|
5
|
+
calls them automatically, so adding a new tool never requires touching this file.
|
|
6
|
+
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
import argparse
|
|
12
|
+
import importlib
|
|
13
|
+
import pkgutil
|
|
14
|
+
import sys
|
|
15
|
+
from pathlib import Path
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def _discover_modules():
|
|
19
|
+
"""Yield modules with ``register_cli`` from mckit.operate and mckit.observe."""
|
|
20
|
+
import mckit.operate
|
|
21
|
+
import mckit.observe
|
|
22
|
+
|
|
23
|
+
for pkg in (mckit.operate, mckit.observe):
|
|
24
|
+
pkg_path = str(Path(pkg.__file__).parent)
|
|
25
|
+
for info in pkgutil.iter_modules([pkg_path]):
|
|
26
|
+
if info.name.startswith("_"):
|
|
27
|
+
continue
|
|
28
|
+
mod = importlib.import_module(f"{pkg.__name__}.{info.name}")
|
|
29
|
+
if hasattr(mod, "register_cli"):
|
|
30
|
+
yield mod
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def build_parser() -> argparse.ArgumentParser:
|
|
34
|
+
"""Build the top-level argument parser."""
|
|
35
|
+
parser = argparse.ArgumentParser(
|
|
36
|
+
prog="mckit",
|
|
37
|
+
description="MatCraft Kit",
|
|
38
|
+
)
|
|
39
|
+
top_sub = parser.add_subparsers(dest="category")
|
|
40
|
+
|
|
41
|
+
operate = top_sub.add_parser("operate", help="Build / modify structures")
|
|
42
|
+
observe = top_sub.add_parser("observe", help="Inspect structures")
|
|
43
|
+
# defect = top_sub.add_parser("defect", help="Quick defect workflows")
|
|
44
|
+
|
|
45
|
+
op_sub = operate.add_subparsers(dest="tool")
|
|
46
|
+
ob_sub = observe.add_subparsers(dest="tool")
|
|
47
|
+
# defect_sub = defect.add_subparsers(dest="tool")
|
|
48
|
+
|
|
49
|
+
for mod in _discover_modules():
|
|
50
|
+
package = mod.__name__.split(".")[-2] # 'operate' or 'observe'
|
|
51
|
+
sub = op_sub if package == "operate" else ob_sub
|
|
52
|
+
mod.register_cli(sub)
|
|
53
|
+
|
|
54
|
+
# # Optional top-level shortcuts (currently defect workflows).
|
|
55
|
+
# try:
|
|
56
|
+
# from mmkit.operate import defect_creation as defect_mod
|
|
57
|
+
|
|
58
|
+
# if hasattr(defect_mod, "register_cli_root"):
|
|
59
|
+
# defect_mod.register_cli_root(defect_sub)
|
|
60
|
+
# except Exception:
|
|
61
|
+
# # Keep CLI resilient if optional shortcut wiring fails.
|
|
62
|
+
# pass
|
|
63
|
+
|
|
64
|
+
return parser
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def main():
|
|
68
|
+
"""Console entry point."""
|
|
69
|
+
parser = build_parser()
|
|
70
|
+
args = parser.parse_args()
|
|
71
|
+
if not getattr(args, "category", None):
|
|
72
|
+
parser.print_help()
|
|
73
|
+
elif hasattr(args, "handler"):
|
|
74
|
+
try:
|
|
75
|
+
args.handler(args)
|
|
76
|
+
except (ValueError, FileNotFoundError, RuntimeError) as exc:
|
|
77
|
+
print(f"Error: {exc}", file=sys.stderr)
|
|
78
|
+
sys.exit(1)
|
|
79
|
+
else:
|
|
80
|
+
parser.print_help()
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
if __name__ == "__main__":
|
|
84
|
+
main()
|
mckit/core/__init__.py
ADDED
mckit/core/lattice.py
ADDED
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
"""Lattice (unit cell) — a thin wrapper around ``ase.cell.Cell``.
|
|
2
|
+
|
|
3
|
+
All cell-parameter and reciprocal-lattice computations are delegated to ASE
|
|
4
|
+
so we do not reinvent geometry that already exists upstream.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
from dataclasses import dataclass
|
|
10
|
+
|
|
11
|
+
import numpy as np
|
|
12
|
+
from ase.cell import Cell as ASECell
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
@dataclass
|
|
16
|
+
class Lattice:
|
|
17
|
+
"""Crystal lattice defined by three row-vectors in a 3x3 matrix (Å)."""
|
|
18
|
+
|
|
19
|
+
matrix: np.ndarray
|
|
20
|
+
|
|
21
|
+
def __post_init__(self) -> None:
|
|
22
|
+
arr = self.matrix.array if isinstance(self.matrix, ASECell) else self.matrix
|
|
23
|
+
self.matrix = np.asarray(arr, dtype=np.float64).reshape(3, 3)
|
|
24
|
+
|
|
25
|
+
# ------------------------------------------------------------------
|
|
26
|
+
# Constructors
|
|
27
|
+
# ------------------------------------------------------------------
|
|
28
|
+
@classmethod
|
|
29
|
+
def from_parameters(
|
|
30
|
+
cls, a: float, b: float, c: float,
|
|
31
|
+
alpha: float, beta: float, gamma: float,
|
|
32
|
+
) -> "Lattice":
|
|
33
|
+
return cls(matrix=ASECell.new([a, b, c, alpha, beta, gamma]))
|
|
34
|
+
|
|
35
|
+
@classmethod
|
|
36
|
+
def cubic(cls, a: float) -> "Lattice":
|
|
37
|
+
return cls.from_parameters(a, a, a, 90.0, 90.0, 90.0)
|
|
38
|
+
|
|
39
|
+
@classmethod
|
|
40
|
+
def hexagonal(cls, a: float, c: float) -> "Lattice":
|
|
41
|
+
return cls.from_parameters(a, a, c, 90.0, 90.0, 120.0)
|
|
42
|
+
|
|
43
|
+
@classmethod
|
|
44
|
+
def from_ase_cell(cls, cell) -> "Lattice":
|
|
45
|
+
return cls(matrix=np.asarray(cell, dtype=np.float64))
|
|
46
|
+
|
|
47
|
+
# ------------------------------------------------------------------
|
|
48
|
+
# ASE delegation
|
|
49
|
+
# ------------------------------------------------------------------
|
|
50
|
+
def to_ase_cell(self) -> ASECell:
|
|
51
|
+
return ASECell(self.matrix.copy())
|
|
52
|
+
|
|
53
|
+
@property
|
|
54
|
+
def _cellpar(self) -> np.ndarray:
|
|
55
|
+
return self.to_ase_cell().cellpar()
|
|
56
|
+
|
|
57
|
+
@property
|
|
58
|
+
def a_vec(self) -> np.ndarray:
|
|
59
|
+
return self.matrix[0]
|
|
60
|
+
|
|
61
|
+
@property
|
|
62
|
+
def b_vec(self) -> np.ndarray:
|
|
63
|
+
return self.matrix[1]
|
|
64
|
+
|
|
65
|
+
@property
|
|
66
|
+
def c_vec(self) -> np.ndarray:
|
|
67
|
+
return self.matrix[2]
|
|
68
|
+
|
|
69
|
+
@property
|
|
70
|
+
def a(self) -> float:
|
|
71
|
+
return float(self._cellpar[0])
|
|
72
|
+
|
|
73
|
+
@property
|
|
74
|
+
def b(self) -> float:
|
|
75
|
+
return float(self._cellpar[1])
|
|
76
|
+
|
|
77
|
+
@property
|
|
78
|
+
def c(self) -> float:
|
|
79
|
+
return float(self._cellpar[2])
|
|
80
|
+
|
|
81
|
+
@property
|
|
82
|
+
def alpha(self) -> float:
|
|
83
|
+
return float(self._cellpar[3])
|
|
84
|
+
|
|
85
|
+
@property
|
|
86
|
+
def beta(self) -> float:
|
|
87
|
+
return float(self._cellpar[4])
|
|
88
|
+
|
|
89
|
+
@property
|
|
90
|
+
def gamma(self) -> float:
|
|
91
|
+
return float(self._cellpar[5])
|
|
92
|
+
|
|
93
|
+
@property
|
|
94
|
+
def volume(self) -> float:
|
|
95
|
+
return float(self.to_ase_cell().volume)
|
|
96
|
+
|
|
97
|
+
@property
|
|
98
|
+
def reciprocal(self) -> "Lattice":
|
|
99
|
+
"""Reciprocal lattice (2π convention)."""
|
|
100
|
+
rec = self.to_ase_cell().reciprocal().array * 2 * np.pi
|
|
101
|
+
return Lattice(matrix=rec)
|
|
102
|
+
|
|
103
|
+
# ------------------------------------------------------------------
|
|
104
|
+
# Coordinate transforms
|
|
105
|
+
# ------------------------------------------------------------------
|
|
106
|
+
def fractional_to_cartesian(self, frac_coords) -> np.ndarray:
|
|
107
|
+
return np.asarray(frac_coords, dtype=np.float64) @ self.matrix
|
|
108
|
+
|
|
109
|
+
def cartesian_to_fractional(self, cart_coords) -> np.ndarray:
|
|
110
|
+
return np.asarray(cart_coords, dtype=np.float64) @ np.linalg.inv(self.matrix)
|
|
111
|
+
|
|
112
|
+
def __repr__(self) -> str:
|
|
113
|
+
cp = self._cellpar
|
|
114
|
+
return (
|
|
115
|
+
f"Lattice(a={cp[0]:.4f}, b={cp[1]:.4f}, c={cp[2]:.4f}, "
|
|
116
|
+
f"alpha={cp[3]:.2f}, beta={cp[4]:.2f}, gamma={cp[5]:.2f})"
|
|
117
|
+
)
|
mckit/core/structure.py
ADDED
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
"""Atomic structure — a thin wrapper around ``ase.Atoms``.
|
|
2
|
+
|
|
3
|
+
``Structure`` keeps a single ``ase.Atoms`` instance internally and forwards
|
|
4
|
+
properties to it. Use ``structure.atoms`` to reach the full ASE API, or
|
|
5
|
+
``structure.to_pymatgen()`` for pymatgen.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
from collections import Counter
|
|
11
|
+
from typing import Dict, List, Optional, Sequence, Union
|
|
12
|
+
|
|
13
|
+
import numpy as np
|
|
14
|
+
from ase import Atom, Atoms
|
|
15
|
+
|
|
16
|
+
from mckit.core.lattice import Lattice
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
# 1 amu/ų in g/cm³
|
|
20
|
+
_AMU_PER_A3_TO_G_PER_CM3 = 1.66053906660
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class Structure:
|
|
24
|
+
"""A periodic atomic structure backed by ``ase.Atoms``.
|
|
25
|
+
|
|
26
|
+
Construct either from explicit ``lattice/species/positions`` (fractional
|
|
27
|
+
coords) or directly from an existing ``ase.Atoms`` via the ``atoms`` kwarg.
|
|
28
|
+
"""
|
|
29
|
+
|
|
30
|
+
def __init__(
|
|
31
|
+
self,
|
|
32
|
+
lattice: Optional[Union[Lattice, np.ndarray]] = None,
|
|
33
|
+
species: Optional[Sequence[str]] = None,
|
|
34
|
+
positions: Optional[np.ndarray] = None,
|
|
35
|
+
*,
|
|
36
|
+
atoms: Optional[Atoms] = None,
|
|
37
|
+
) -> None:
|
|
38
|
+
if atoms is not None:
|
|
39
|
+
self._atoms = atoms
|
|
40
|
+
return
|
|
41
|
+
if lattice is None:
|
|
42
|
+
raise ValueError("Provide either `atoms=` or `lattice=`.")
|
|
43
|
+
species = list(species) if species is not None else []
|
|
44
|
+
symbols = [str(s).strip().capitalize() for s in species]
|
|
45
|
+
if positions is None:
|
|
46
|
+
positions = np.empty((0, 3), dtype=np.float64)
|
|
47
|
+
positions = np.asarray(positions, dtype=np.float64).reshape(-1, 3)
|
|
48
|
+
if len(symbols) != positions.shape[0]:
|
|
49
|
+
raise ValueError(
|
|
50
|
+
f"species ({len(symbols)}) and positions ({positions.shape[0]}) "
|
|
51
|
+
"must have the same length."
|
|
52
|
+
)
|
|
53
|
+
cell = lattice.matrix if isinstance(lattice, Lattice) else lattice
|
|
54
|
+
self._atoms = Atoms(
|
|
55
|
+
symbols=symbols, scaled_positions=positions, cell=cell, pbc=True,
|
|
56
|
+
)
|
|
57
|
+
|
|
58
|
+
# ------------------------------------------------------------------
|
|
59
|
+
# Underlying ASE handle (escape hatch)
|
|
60
|
+
# ------------------------------------------------------------------
|
|
61
|
+
@property
|
|
62
|
+
def atoms(self) -> Atoms:
|
|
63
|
+
"""The underlying ``ase.Atoms`` (mutating it mutates the Structure)."""
|
|
64
|
+
return self._atoms
|
|
65
|
+
|
|
66
|
+
# ------------------------------------------------------------------
|
|
67
|
+
# Derived views
|
|
68
|
+
# ------------------------------------------------------------------
|
|
69
|
+
@property
|
|
70
|
+
def lattice(self) -> Lattice:
|
|
71
|
+
return Lattice(matrix=np.asarray(self._atoms.cell.array))
|
|
72
|
+
|
|
73
|
+
@property
|
|
74
|
+
def species(self) -> List[str]:
|
|
75
|
+
return self.symbols
|
|
76
|
+
|
|
77
|
+
@property
|
|
78
|
+
def symbols(self) -> List[str]:
|
|
79
|
+
return list(self._atoms.get_chemical_symbols())
|
|
80
|
+
|
|
81
|
+
@property
|
|
82
|
+
def positions(self) -> np.ndarray:
|
|
83
|
+
"""Fractional coordinates (no wrapping)."""
|
|
84
|
+
return self._atoms.get_scaled_positions(wrap=False)
|
|
85
|
+
|
|
86
|
+
@property
|
|
87
|
+
def cart_positions(self) -> np.ndarray:
|
|
88
|
+
return self._atoms.positions.copy()
|
|
89
|
+
|
|
90
|
+
@property
|
|
91
|
+
def num_atoms(self) -> int:
|
|
92
|
+
return len(self._atoms)
|
|
93
|
+
|
|
94
|
+
def __len__(self) -> int:
|
|
95
|
+
return len(self._atoms)
|
|
96
|
+
|
|
97
|
+
@property
|
|
98
|
+
def volume(self) -> float:
|
|
99
|
+
return float(self._atoms.cell.volume)
|
|
100
|
+
|
|
101
|
+
@property
|
|
102
|
+
def total_mass(self) -> float:
|
|
103
|
+
return float(self._atoms.get_masses().sum())
|
|
104
|
+
|
|
105
|
+
@property
|
|
106
|
+
def density(self) -> float:
|
|
107
|
+
"""Mass density (g/cm³)."""
|
|
108
|
+
return self.total_mass * _AMU_PER_A3_TO_G_PER_CM3 / self.volume
|
|
109
|
+
|
|
110
|
+
@property
|
|
111
|
+
def composition(self) -> Dict[str, int]:
|
|
112
|
+
return dict(Counter(self._atoms.get_chemical_symbols()))
|
|
113
|
+
|
|
114
|
+
# ------------------------------------------------------------------
|
|
115
|
+
# Mutation
|
|
116
|
+
# ------------------------------------------------------------------
|
|
117
|
+
def add_atom(self, element: str, frac_position: Sequence[float]) -> None:
|
|
118
|
+
cart = np.asarray(frac_position, dtype=np.float64) @ self._atoms.cell.array
|
|
119
|
+
self._atoms.append(Atom(str(element).strip().capitalize(), position=cart))
|
|
120
|
+
|
|
121
|
+
def remove_atom(self, index: int) -> None:
|
|
122
|
+
del self._atoms[index]
|
|
123
|
+
|
|
124
|
+
def wrap_to_cell(self) -> None:
|
|
125
|
+
self._atoms.wrap()
|
|
126
|
+
|
|
127
|
+
def copy(self) -> "Structure":
|
|
128
|
+
return Structure(atoms=self._atoms.copy())
|
|
129
|
+
|
|
130
|
+
# ------------------------------------------------------------------
|
|
131
|
+
# Geometry / transforms (delegate to ASE)
|
|
132
|
+
# ------------------------------------------------------------------
|
|
133
|
+
def get_distance(self, i: int, j: int, mic: bool = True) -> float:
|
|
134
|
+
return float(self._atoms.get_distance(i, j, mic=mic))
|
|
135
|
+
|
|
136
|
+
def supercell(self, na: int, nb: int, nc: int) -> "Structure":
|
|
137
|
+
return Structure(atoms=self._atoms.repeat((na, nb, nc)))
|
|
138
|
+
|
|
139
|
+
# ------------------------------------------------------------------
|
|
140
|
+
# Interop
|
|
141
|
+
# ------------------------------------------------------------------
|
|
142
|
+
def to_ase_atoms(self) -> Atoms:
|
|
143
|
+
return self._atoms.copy()
|
|
144
|
+
|
|
145
|
+
@classmethod
|
|
146
|
+
def from_ase_atoms(cls, atoms: Atoms) -> "Structure":
|
|
147
|
+
return cls(atoms=atoms.copy())
|
|
148
|
+
|
|
149
|
+
def to_pymatgen(self):
|
|
150
|
+
from pymatgen.io.ase import AseAtomsAdaptor
|
|
151
|
+
return AseAtomsAdaptor().get_structure(self._atoms)
|
|
152
|
+
|
|
153
|
+
@classmethod
|
|
154
|
+
def from_pymatgen(cls, struct) -> "Structure":
|
|
155
|
+
from pymatgen.io.ase import AseAtomsAdaptor
|
|
156
|
+
return cls(atoms=AseAtomsAdaptor().get_atoms(struct))
|
|
157
|
+
|
|
158
|
+
def __repr__(self) -> str:
|
|
159
|
+
comp_str = " ".join(f"{s}{n}" for s, n in self.composition.items())
|
|
160
|
+
return f"Structure({comp_str}, natoms={self.num_atoms}, V={self.volume:.2f} A^3)"
|
mckit/core/tool.py
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
"""Abstract base classes for matmod tools.
|
|
2
|
+
|
|
3
|
+
Two families:
|
|
4
|
+
|
|
5
|
+
* ``Operation`` — *builds* or *modifies* a ``Structure``. Subclasses define
|
|
6
|
+
``apply(...)`` with whatever signature makes sense (no fixed contract on
|
|
7
|
+
arguments — the only requirement is that it returns a ``Structure``).
|
|
8
|
+
* ``Observation`` — *inspects* a ``Structure`` and returns arbitrary data
|
|
9
|
+
without modifying it. Subclasses implement ``observe(structure, **kwargs)``.
|
|
10
|
+
|
|
11
|
+
Adding a new tool is a one-class affair — see the examples in
|
|
12
|
+
``matmod/operations/bulk.py`` and ``matmod/observations/info.py``.
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
from __future__ import annotations
|
|
16
|
+
|
|
17
|
+
from abc import ABC, abstractmethod
|
|
18
|
+
from typing import Any
|
|
19
|
+
|
|
20
|
+
from mckit.core.structure import Structure
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class Operation(ABC):
|
|
24
|
+
"""Base class for structure-building / modifying tools."""
|
|
25
|
+
|
|
26
|
+
@abstractmethod
|
|
27
|
+
def apply(self, *args, **kwargs) -> Structure:
|
|
28
|
+
"""Run the operation and return the resulting ``Structure``."""
|
|
29
|
+
|
|
30
|
+
def __call__(self, *args, **kwargs) -> Structure:
|
|
31
|
+
return self.apply(*args, **kwargs)
|
|
32
|
+
|
|
33
|
+
def __repr__(self) -> str:
|
|
34
|
+
return f"<{type(self).__name__} Operation>"
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
class Observation(ABC):
|
|
38
|
+
"""Base class for structure-inspection tools."""
|
|
39
|
+
|
|
40
|
+
@abstractmethod
|
|
41
|
+
def observe(self, structure: Structure, **kwargs) -> Any:
|
|
42
|
+
"""Inspect the structure and return arbitrary result data."""
|
|
43
|
+
|
|
44
|
+
def __call__(self, structure: Structure, **kwargs) -> Any:
|
|
45
|
+
return self.observe(structure, **kwargs)
|
|
46
|
+
|
|
47
|
+
def __repr__(self) -> str:
|
|
48
|
+
return f"<{type(self).__name__} Observation>"
|
mckit/io/__init__.py
ADDED
mckit/io/reader.py
ADDED
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
"""Read structures as ``ase.Atoms`` using ASE (and pymatgen for CIF)."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
from typing import Optional
|
|
7
|
+
|
|
8
|
+
from ase import Atoms
|
|
9
|
+
from ase.io import read as ase_read
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def read_structure(path: str, format: Optional[str] = None, **kwargs) -> Atoms:
|
|
13
|
+
"""Read a structure file as ``ase.Atoms``.
|
|
14
|
+
|
|
15
|
+
Supports all formats that ASE supports (extxyz, vasp, cif, xyz, poscar, ...)
|
|
16
|
+
plus CIF via pymatgen for better handling of symmetry.
|
|
17
|
+
|
|
18
|
+
Parameters
|
|
19
|
+
----------
|
|
20
|
+
path : str
|
|
21
|
+
File path.
|
|
22
|
+
format : str, optional
|
|
23
|
+
ASE format hint (e.g. ``"vasp"``, ``"cif"``). Auto-detected if omitted.
|
|
24
|
+
**kwargs
|
|
25
|
+
Extra keyword arguments forwarded to ``ase.io.read``.
|
|
26
|
+
|
|
27
|
+
Returns
|
|
28
|
+
-------
|
|
29
|
+
ase.Atoms
|
|
30
|
+
"""
|
|
31
|
+
p = Path(path)
|
|
32
|
+
if not p.exists():
|
|
33
|
+
raise FileNotFoundError(f"File not found: {path}")
|
|
34
|
+
|
|
35
|
+
# Use pymatgen for CIF (better symmetry handling)
|
|
36
|
+
if p.suffix.lower() == ".cif" and format is None:
|
|
37
|
+
from pymatgen.io.cif import CifParser
|
|
38
|
+
|
|
39
|
+
parser = CifParser(str(p))
|
|
40
|
+
structs = parser.parse_structures(primitive=False)
|
|
41
|
+
if not structs:
|
|
42
|
+
raise ValueError(f"No structures parsed from {path}")
|
|
43
|
+
from pymatgen.io.ase import AseAtomsAdaptor
|
|
44
|
+
|
|
45
|
+
return AseAtomsAdaptor().get_atoms(structs[0])
|
|
46
|
+
|
|
47
|
+
return ase_read(str(p), format=format, **kwargs)
|
mckit/io/writer.py
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
"""Write atomic structures to files via ``ase.io.write``."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
from typing import Optional, Union
|
|
7
|
+
|
|
8
|
+
from ase import Atoms
|
|
9
|
+
from ase.io import write as ase_write
|
|
10
|
+
|
|
11
|
+
from mckit.core.structure import Structure
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def write_structure(
|
|
15
|
+
path: str,
|
|
16
|
+
structure: Union[Structure, Atoms, "PmgStructure"],
|
|
17
|
+
format: Optional[str] = None,
|
|
18
|
+
**kwargs,
|
|
19
|
+
) -> str:
|
|
20
|
+
"""Write a ``Structure`` (or raw ``ase.Atoms`` / pymatgen ``Structure``) to a file.
|
|
21
|
+
|
|
22
|
+
The output format is auto-detected from the extension when ``format`` is
|
|
23
|
+
omitted; missing extensions default to ``.extxyz``.
|
|
24
|
+
"""
|
|
25
|
+
if isinstance(structure, Structure):
|
|
26
|
+
atoms = structure.to_ase_atoms()
|
|
27
|
+
elif isinstance(structure, Atoms):
|
|
28
|
+
atoms = structure
|
|
29
|
+
else:
|
|
30
|
+
# Assume pymatgen Structure (or compatible)
|
|
31
|
+
from pymatgen.io.ase import AseAtomsAdaptor
|
|
32
|
+
atoms = AseAtomsAdaptor().get_atoms(structure)
|
|
33
|
+
p = Path(path)
|
|
34
|
+
if p.suffix == "":
|
|
35
|
+
p = p.with_suffix(".extxyz")
|
|
36
|
+
ase_write(str(p), atoms, format=format, **kwargs)
|
|
37
|
+
return str(p)
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
# Back-compat alias used by ``operations.surface``.
|
|
41
|
+
write_atoms = write_structure
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
"""Structure inspection tools."""
|
|
2
|
+
|
|
3
|
+
__all__ = [
|
|
4
|
+
"BasicInfo",
|
|
5
|
+
"InfoSection",
|
|
6
|
+
"SlabCompositionInfo",
|
|
7
|
+
"StructureInfo",
|
|
8
|
+
"VacuumInfo",
|
|
9
|
+
"FundamentalCheck",
|
|
10
|
+
]
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def __getattr__(name):
|
|
14
|
+
"""Lazy import to avoid loading heavy dependencies at startup."""
|
|
15
|
+
if name in ("BasicInfo", "InfoSection", "SlabCompositionInfo", "StructureInfo", "VacuumInfo"):
|
|
16
|
+
from mckit.observe import info
|
|
17
|
+
return getattr(info, name)
|
|
18
|
+
if name == "FundamentalCheck":
|
|
19
|
+
from mckit.observe.fundamental import FundamentalCheck
|
|
20
|
+
return FundamentalCheck
|
|
21
|
+
raise AttributeError(f"module {__name__!r} has no attribute {name!r}")
|