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.
Files changed (30) hide show
  1. matcraft_kit-0.1.0/PKG-INFO +13 -0
  2. matcraft_kit-0.1.0/README.md +164 -0
  3. matcraft_kit-0.1.0/matcraft_kit.egg-info/PKG-INFO +13 -0
  4. matcraft_kit-0.1.0/matcraft_kit.egg-info/SOURCES.txt +28 -0
  5. matcraft_kit-0.1.0/matcraft_kit.egg-info/dependency_links.txt +1 -0
  6. matcraft_kit-0.1.0/matcraft_kit.egg-info/entry_points.txt +2 -0
  7. matcraft_kit-0.1.0/matcraft_kit.egg-info/requires.txt +9 -0
  8. matcraft_kit-0.1.0/matcraft_kit.egg-info/top_level.txt +1 -0
  9. matcraft_kit-0.1.0/mckit/__init__.py +25 -0
  10. matcraft_kit-0.1.0/mckit/cli.py +84 -0
  11. matcraft_kit-0.1.0/mckit/core/__init__.py +5 -0
  12. matcraft_kit-0.1.0/mckit/core/lattice.py +117 -0
  13. matcraft_kit-0.1.0/mckit/core/structure.py +160 -0
  14. matcraft_kit-0.1.0/mckit/core/tool.py +48 -0
  15. matcraft_kit-0.1.0/mckit/io/__init__.py +10 -0
  16. matcraft_kit-0.1.0/mckit/io/reader.py +47 -0
  17. matcraft_kit-0.1.0/mckit/io/writer.py +41 -0
  18. matcraft_kit-0.1.0/mckit/observe/__init__.py +21 -0
  19. matcraft_kit-0.1.0/mckit/observe/fundamental.py +155 -0
  20. matcraft_kit-0.1.0/mckit/observe/info.py +687 -0
  21. matcraft_kit-0.1.0/mckit/operate/__init__.py +42 -0
  22. matcraft_kit-0.1.0/mckit/operate/bulk.py +142 -0
  23. matcraft_kit-0.1.0/mckit/operate/defect_creation.py +1052 -0
  24. matcraft_kit-0.1.0/mckit/operate/interface.py +1197 -0
  25. matcraft_kit-0.1.0/mckit/operate/molecule_creation.py +244 -0
  26. matcraft_kit-0.1.0/mckit/operate/perturbation.py +589 -0
  27. matcraft_kit-0.1.0/mckit/operate/supercell.py +207 -0
  28. matcraft_kit-0.1.0/mckit/operate/surface.py +1285 -0
  29. matcraft_kit-0.1.0/pyproject.toml +24 -0
  30. 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,2 @@
1
+ [console_scripts]
2
+ mckit = mckit.cli:main
@@ -0,0 +1,9 @@
1
+ numpy>=1.21
2
+ ase>=3.22
3
+ pymatgen>=2022.0.0
4
+ pymatgen-analysis-defects>=2024.10.22
5
+ rdkit>=2025.9.6
6
+
7
+ [dev]
8
+ pytest
9
+ pytest-cov
@@ -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,5 @@
1
+ """Core data structures."""
2
+
3
+ from mckit.core.lattice import Lattice
4
+ from mckit.core.structure import Structure
5
+ from mckit.core.tool import Operation, Observation
@@ -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>"
@@ -0,0 +1,10 @@
1
+ """I/O utilities (read/write structures) backed by ASE."""
2
+
3
+ from mckit.io.reader import read_structure
4
+ from mckit.io.writer import write_structure, write_atoms
5
+
6
+ __all__ = [
7
+ "read_structure",
8
+ "write_structure",
9
+ "write_atoms",
10
+ ]