matcraft-kit 0.1.0__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.
- matcraft_kit-0.1.0/PKG-INFO +13 -0
- matcraft_kit-0.1.0/README.md +164 -0
- matcraft_kit-0.1.0/matcraft_kit.egg-info/PKG-INFO +13 -0
- matcraft_kit-0.1.0/matcraft_kit.egg-info/SOURCES.txt +28 -0
- matcraft_kit-0.1.0/matcraft_kit.egg-info/dependency_links.txt +1 -0
- matcraft_kit-0.1.0/matcraft_kit.egg-info/entry_points.txt +2 -0
- matcraft_kit-0.1.0/matcraft_kit.egg-info/requires.txt +9 -0
- matcraft_kit-0.1.0/matcraft_kit.egg-info/top_level.txt +1 -0
- matcraft_kit-0.1.0/mckit/__init__.py +25 -0
- matcraft_kit-0.1.0/mckit/cli.py +84 -0
- matcraft_kit-0.1.0/mckit/core/__init__.py +5 -0
- matcraft_kit-0.1.0/mckit/core/lattice.py +117 -0
- matcraft_kit-0.1.0/mckit/core/structure.py +160 -0
- matcraft_kit-0.1.0/mckit/core/tool.py +48 -0
- matcraft_kit-0.1.0/mckit/io/__init__.py +10 -0
- matcraft_kit-0.1.0/mckit/io/reader.py +47 -0
- matcraft_kit-0.1.0/mckit/io/writer.py +41 -0
- matcraft_kit-0.1.0/mckit/observe/__init__.py +21 -0
- matcraft_kit-0.1.0/mckit/observe/fundamental.py +155 -0
- matcraft_kit-0.1.0/mckit/observe/info.py +687 -0
- matcraft_kit-0.1.0/mckit/operate/__init__.py +42 -0
- matcraft_kit-0.1.0/mckit/operate/bulk.py +142 -0
- matcraft_kit-0.1.0/mckit/operate/defect_creation.py +1052 -0
- matcraft_kit-0.1.0/mckit/operate/interface.py +1197 -0
- matcraft_kit-0.1.0/mckit/operate/molecule_creation.py +244 -0
- matcraft_kit-0.1.0/mckit/operate/perturbation.py +589 -0
- matcraft_kit-0.1.0/mckit/operate/supercell.py +207 -0
- matcraft_kit-0.1.0/mckit/operate/surface.py +1285 -0
- matcraft_kit-0.1.0/pyproject.toml +24 -0
- matcraft_kit-0.1.0/setup.cfg +4 -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,164 @@
|
|
|
1
|
+
# mckit — MatCraft Toolkit
|
|
2
|
+
|
|
3
|
+
A modular Python framework for building and analyzing atomic structures. Backed by [ASE](https://wiki.fysik.dtu.dk/ase/) and [pymatgen](https://pymatgen.org/) for materials modelling.
|
|
4
|
+
|
|
5
|
+
## Features
|
|
6
|
+
|
|
7
|
+
- **Operations** — modelling materials
|
|
8
|
+
- **Observations** — inspect structures and run sanity checks
|
|
9
|
+
|
|
10
|
+
---
|
|
11
|
+
|
|
12
|
+
## 1. Usage
|
|
13
|
+
|
|
14
|
+
### Installation
|
|
15
|
+
|
|
16
|
+
```bash
|
|
17
|
+
pip install -e .
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
Or install with dev dependencies (pytest, coverage):
|
|
21
|
+
|
|
22
|
+
```bash
|
|
23
|
+
pip install -e ".[dev]"
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
### CLI
|
|
27
|
+
|
|
28
|
+
```bash
|
|
29
|
+
# for observations:
|
|
30
|
+
mckit observe -h
|
|
31
|
+
# for operations:
|
|
32
|
+
mckit operate -h
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
## 2. Development
|
|
37
|
+
|
|
38
|
+
<details>
|
|
39
|
+
<summary>Click to expand development instructions</summary>
|
|
40
|
+
|
|
41
|
+
### Prerequisites
|
|
42
|
+
|
|
43
|
+
- Python ≥ 3.9
|
|
44
|
+
- pip
|
|
45
|
+
|
|
46
|
+
### Setup
|
|
47
|
+
|
|
48
|
+
```bash
|
|
49
|
+
# Clone the repository
|
|
50
|
+
git clone <repo-url>
|
|
51
|
+
cd mat-modelling-kit
|
|
52
|
+
|
|
53
|
+
# Create and activate a virtual environment
|
|
54
|
+
python -m venv .venv
|
|
55
|
+
source .venv/bin/activate # Linux/macOS
|
|
56
|
+
# .venv\Scripts\activate # Windows
|
|
57
|
+
|
|
58
|
+
# Install in editable mode with dev dependencies
|
|
59
|
+
pip install -e ".[dev]"
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
### Project Structure
|
|
63
|
+
|
|
64
|
+
```
|
|
65
|
+
mckit/
|
|
66
|
+
├── __init__.py # Package root — exports public API
|
|
67
|
+
├── core/
|
|
68
|
+
│ ├── lattice.py # Lattice dataclass (3×3 matrix, ASE Cell-backed)
|
|
69
|
+
│ ├── structure.py # Structure dataclass (lattice + species + positions)
|
|
70
|
+
│ └── tool.py # Abstract base classes: Operation, Observation
|
|
71
|
+
├── operate/
|
|
72
|
+
│ ├── bulk.py # BulkBuilder — standard crystal structures
|
|
73
|
+
│ └── surface.py # SurfaceBuilder, TerminationAnalyzer, MoleculeDetector
|
|
74
|
+
├── observe/
|
|
75
|
+
│ ├── info.py # StructureInfo — structural summary
|
|
76
|
+
│ └── fundamental.py # FundamentalCheck — geometric validity checks
|
|
77
|
+
└── io/
|
|
78
|
+
├── reader.py # read_structure() -> ASE Atoms
|
|
79
|
+
└── writer.py # write_structure() — ASE io.write
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
### Architecture
|
|
83
|
+
|
|
84
|
+
The framework follows an **Operation / Observation** pattern:
|
|
85
|
+
|
|
86
|
+
- **`Operation`** (abstract) — tools that **build or modify** structures. Subclasses implement `apply(...)` and return a `Structure`.
|
|
87
|
+
- **`Observation`** (abstract) — tools that **inspect** structures without modifying them. Subclasses implement `observe(structure) → Any`.
|
|
88
|
+
|
|
89
|
+
Both are single-method ABCs defined in `mckit.core.tool`, making it straightforward to add new operations or observations.
|
|
90
|
+
|
|
91
|
+
**Core data flow:**
|
|
92
|
+
|
|
93
|
+
```
|
|
94
|
+
File (CIF, VASP, ...) ──read_structure()──▶ ase.Atoms ──Operation──▶ Structure/ase.Atoms ──write_structure()──▶ File
|
|
95
|
+
│
|
|
96
|
+
└──Observation──▶ dict / CheckResult / ...
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
### Running Tests
|
|
100
|
+
|
|
101
|
+
```bash
|
|
102
|
+
pytest
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
With coverage:
|
|
106
|
+
|
|
107
|
+
```bash
|
|
108
|
+
pytest --cov=mckit --cov-report=term-missing
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
### Adding a New Operation
|
|
112
|
+
|
|
113
|
+
Create a file under `mckit/operate/` and subclass `Operation`:
|
|
114
|
+
|
|
115
|
+
```python
|
|
116
|
+
from mckit.core.structure import Structure
|
|
117
|
+
from mckit.core.tool import Operation
|
|
118
|
+
|
|
119
|
+
class MyBuilder(Operation):
|
|
120
|
+
"""Build or modify a structure."""
|
|
121
|
+
|
|
122
|
+
def apply(self, *, some_param: float, **kwargs) -> Structure:
|
|
123
|
+
# ... your logic here ...
|
|
124
|
+
return new_structure
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
Then re-export it from `mckit/operate/__init__.py`.
|
|
128
|
+
|
|
129
|
+
### Adding a New Observation
|
|
130
|
+
|
|
131
|
+
Create a file under `mckit/observe/` and subclass `Observation`:
|
|
132
|
+
|
|
133
|
+
```python
|
|
134
|
+
from mckit.core.structure import Structure
|
|
135
|
+
from mckit.core.tool import Observation
|
|
136
|
+
|
|
137
|
+
class MyAnalysis(Observation):
|
|
138
|
+
"""Inspect a structure and return results."""
|
|
139
|
+
|
|
140
|
+
def observe(self, structure: Structure, **kwargs):
|
|
141
|
+
# ... your logic here ...
|
|
142
|
+
return result_dict
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
Then re-export it from `mckit/observe/__init__.py`.
|
|
146
|
+
|
|
147
|
+
### Building the Package
|
|
148
|
+
|
|
149
|
+
```bash
|
|
150
|
+
pip install build
|
|
151
|
+
python -m build
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
This produces a wheel and sdist under `dist/`.
|
|
155
|
+
|
|
156
|
+
### Dependencies
|
|
157
|
+
|
|
158
|
+
| Package | Purpose |
|
|
159
|
+
|---------|---------|
|
|
160
|
+
| `numpy ≥ 1.21` | Numerical computing |
|
|
161
|
+
| `ase ≥ 3.22` | Atomic Simulation Environment — crystal builders, I/O, coordinate transforms |
|
|
162
|
+
| `pymatgen ≥ 2022.0.0` | Python Materials Genomics — CIF parsing, symmetry analysis |
|
|
163
|
+
|
|
164
|
+
</details>
|
|
@@ -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,28 @@
|
|
|
1
|
+
README.md
|
|
2
|
+
pyproject.toml
|
|
3
|
+
matcraft_kit.egg-info/PKG-INFO
|
|
4
|
+
matcraft_kit.egg-info/SOURCES.txt
|
|
5
|
+
matcraft_kit.egg-info/dependency_links.txt
|
|
6
|
+
matcraft_kit.egg-info/entry_points.txt
|
|
7
|
+
matcraft_kit.egg-info/requires.txt
|
|
8
|
+
matcraft_kit.egg-info/top_level.txt
|
|
9
|
+
mckit/__init__.py
|
|
10
|
+
mckit/cli.py
|
|
11
|
+
mckit/core/__init__.py
|
|
12
|
+
mckit/core/lattice.py
|
|
13
|
+
mckit/core/structure.py
|
|
14
|
+
mckit/core/tool.py
|
|
15
|
+
mckit/io/__init__.py
|
|
16
|
+
mckit/io/reader.py
|
|
17
|
+
mckit/io/writer.py
|
|
18
|
+
mckit/observe/__init__.py
|
|
19
|
+
mckit/observe/fundamental.py
|
|
20
|
+
mckit/observe/info.py
|
|
21
|
+
mckit/operate/__init__.py
|
|
22
|
+
mckit/operate/bulk.py
|
|
23
|
+
mckit/operate/defect_creation.py
|
|
24
|
+
mckit/operate/interface.py
|
|
25
|
+
mckit/operate/molecule_creation.py
|
|
26
|
+
mckit/operate/perturbation.py
|
|
27
|
+
mckit/operate/supercell.py
|
|
28
|
+
mckit/operate/surface.py
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
mckit
|
|
@@ -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
|
+
]
|
|
@@ -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()
|
|
@@ -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
|
+
)
|
|
@@ -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)"
|
|
@@ -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>"
|