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.
@@ -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,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (82.0.1)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ mckit = mckit.cli:main
@@ -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
@@ -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
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
+ )
@@ -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
@@ -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
+ ]
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}")