atomforge-py 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.
@@ -0,0 +1,70 @@
1
+ Metadata-Version: 2.4
2
+ Name: atomforge-py
3
+ Version: 0.1.0
4
+ Summary: Python interface for AtomForge: load, edit, and view atomic structures
5
+ Author: AtomForge Contributors
6
+ Author-email: AtomForge Contributors <albert.hzbn@gmail.com>
7
+ License-Expression: MIT
8
+ Project-URL: Homepage, https://github.com/albert-hzbn/AtomForge
9
+ Project-URL: Repository, https://github.com/albert-hzbn/AtomForge
10
+ Project-URL: Bug Tracker, https://github.com/albert-hzbn/AtomForge/issues
11
+ Keywords: atomforge,atoms,crystal,materials,structure,viewer
12
+ Classifier: Development Status :: 3 - Alpha
13
+ Classifier: Intended Audience :: Science/Research
14
+ Classifier: Programming Language :: Python :: 3
15
+ Classifier: Programming Language :: Python :: 3.8
16
+ Classifier: Programming Language :: Python :: 3.9
17
+ Classifier: Programming Language :: Python :: 3.10
18
+ Classifier: Programming Language :: Python :: 3.11
19
+ Classifier: Programming Language :: Python :: 3.12
20
+ Classifier: Topic :: Scientific/Engineering :: Chemistry
21
+ Classifier: Topic :: Scientific/Engineering :: Physics
22
+ Requires-Python: >=3.8
23
+ Description-Content-Type: text/x-rst
24
+ Provides-Extra: ase
25
+ Requires-Dist: ase>=3.22; extra == "ase"
26
+ Dynamic: author
27
+ Dynamic: requires-python
28
+
29
+ atomforge — Python interface for AtomForge
30
+ ==========================================
31
+
32
+ Load, programmatically edit, and launch the AtomForge GUI to view atomic
33
+ structures directly from Python.
34
+
35
+ Quick-start
36
+ -----------
37
+
38
+ .. code-block:: python
39
+
40
+ import atomforge as af
41
+
42
+ # Load any format (XYZ/extXYZ natively; CIF/VASP/PDB via ase extra)
43
+ s = af.load("crystal.cif")
44
+
45
+ # Build from scratch
46
+ s2 = af.Structure()
47
+ s2.set_cell(2.87, 2.87, 2.87) # BCC iron unit cell
48
+ s2.add_atom("Fe", 0.0, 0.0, 0.0)
49
+ s2.add_atom("Fe", 1.435, 1.435, 1.435)
50
+
51
+ # Supercell + open in the AtomForge GUI (non-blocking)
52
+ s2.repeat(4, 4, 4).view()
53
+
54
+ # Edit and save
55
+ s2.translate(1, 0, 0).save("shifted.xyz")
56
+
57
+ Formats
58
+ -------
59
+ XYZ and extended-XYZ are handled natively with no dependencies.
60
+ CIF, VASP/POSCAR, PDB, LAMMPS, mol2, and all other ASE-supported formats
61
+ are available via the ``ase`` extra::
62
+
63
+ pip install "atomforge[ase]"
64
+
65
+ Viewing
66
+ -------
67
+ ``structure.view()`` serialises the structure to a temporary XYZ file and
68
+ launches the AtomForge executable as a detached subprocess. Set the
69
+ ``ATOMFORGE_PATH`` environment variable to the full path of ``AtomForge.exe``
70
+ if it is not on PATH.
@@ -0,0 +1,21 @@
1
+ """
2
+ atomforge — Python interface for AtomForge structures.
3
+
4
+ Quick-start
5
+ -----------
6
+ >>> import atomforge as af
7
+ >>> s = af.load("my_crystal.cif")
8
+ >>> s.repeat(2, 2, 1).view() # open in AtomForge GUI
9
+
10
+ >>> s2 = af.Structure()
11
+ >>> s2.add_atom("Fe", 0, 0, 0)
12
+ >>> s2.add_atom("C", 1.8, 0, 0)
13
+ >>> s2.save("dimer.xyz")
14
+ """
15
+
16
+ from ._structure import Atom, Structure
17
+ from ._io import load, save
18
+ from ._viewer import view
19
+
20
+ __all__ = ["Atom", "Structure", "load", "save", "view"]
21
+ __version__ = "0.1.0"
@@ -0,0 +1,195 @@
1
+ """File I/O: native XYZ/extxyz, and optional ASE fallback for CIF/VASP/PDB/etc."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import os
6
+ from pathlib import Path
7
+ from typing import TYPE_CHECKING, Tuple
8
+
9
+ if TYPE_CHECKING:
10
+ from ._structure import Structure
11
+
12
+ # ── CPK / Jmol colour table (RGB 0-1) ──────────────────────────────────────────
13
+
14
+ _COLORS: dict[str, Tuple[float, float, float]] = {
15
+ "H": (1.000, 1.000, 1.000), "He": (0.851, 1.000, 1.000),
16
+ "Li": (0.800, 0.502, 1.000), "Be": (0.761, 1.000, 0.000),
17
+ "B": (1.000, 0.710, 0.710), "C": (0.565, 0.565, 0.565),
18
+ "N": (0.188, 0.314, 0.973), "O": (1.000, 0.051, 0.051),
19
+ "F": (0.565, 0.878, 0.314), "Ne": (0.702, 0.890, 0.961),
20
+ "Na": (0.671, 0.361, 0.949), "Mg": (0.541, 1.000, 0.000),
21
+ "Al": (0.749, 0.651, 0.651), "Si": (0.941, 0.784, 0.627),
22
+ "P": (1.000, 0.502, 0.000), "S": (1.000, 1.000, 0.188),
23
+ "Cl": (0.122, 0.941, 0.122), "Ar": (0.502, 0.820, 0.890),
24
+ "K": (0.561, 0.251, 0.831), "Ca": (0.239, 1.000, 0.000),
25
+ "Sc": (0.902, 0.902, 0.902), "Ti": (0.749, 0.761, 0.780),
26
+ "V": (0.651, 0.651, 0.671), "Cr": (0.541, 0.600, 0.780),
27
+ "Mn": (0.612, 0.478, 0.780), "Fe": (0.878, 0.400, 0.200),
28
+ "Co": (0.941, 0.565, 0.627), "Ni": (0.314, 0.816, 0.314),
29
+ "Cu": (0.784, 0.502, 0.200), "Zn": (0.490, 0.502, 0.690),
30
+ "Ga": (0.761, 0.561, 0.561), "Ge": (0.400, 0.561, 0.561),
31
+ "As": (0.741, 0.502, 0.890), "Se": (1.000, 0.631, 0.000),
32
+ "Br": (0.651, 0.161, 0.161), "Kr": (0.361, 0.722, 0.820),
33
+ "Rb": (0.439, 0.180, 0.690), "Sr": (0.000, 1.000, 0.000),
34
+ "Y": (0.580, 1.000, 1.000), "Zr": (0.580, 0.878, 0.878),
35
+ "Nb": (0.451, 0.761, 0.788), "Mo": (0.329, 0.710, 0.710),
36
+ "Tc": (0.231, 0.620, 0.620), "Ru": (0.141, 0.561, 0.561),
37
+ "Rh": (0.039, 0.490, 0.549), "Pd": (0.000, 0.412, 0.522),
38
+ "Ag": (0.753, 0.753, 0.753), "Cd": (1.000, 0.851, 0.561),
39
+ "In": (0.651, 0.459, 0.451), "Sn": (0.400, 0.502, 0.502),
40
+ "Sb": (0.620, 0.388, 0.710), "Te": (0.831, 0.478, 0.000),
41
+ "I": (0.580, 0.000, 0.580), "Xe": (0.259, 0.620, 0.690),
42
+ "Cs": (0.341, 0.090, 0.561), "Ba": (0.000, 0.788, 0.000),
43
+ "La": (0.439, 0.831, 1.000), "Ce": (1.000, 1.000, 0.780),
44
+ "Pr": (0.851, 1.000, 0.780), "Nd": (0.780, 1.000, 0.780),
45
+ "Pm": (0.639, 1.000, 0.780), "Sm": (0.561, 1.000, 0.780),
46
+ "Eu": (0.380, 1.000, 0.780), "Gd": (0.271, 1.000, 0.780),
47
+ "Tb": (0.188, 1.000, 0.780), "Dy": (0.122, 1.000, 0.780),
48
+ "Ho": (0.000, 1.000, 0.612), "Er": (0.000, 0.902, 0.459),
49
+ "Tm": (0.000, 0.831, 0.322), "Yb": (0.000, 0.749, 0.220),
50
+ "Lu": (0.000, 0.671, 0.141), "Hf": (0.302, 0.761, 1.000),
51
+ "Ta": (0.302, 0.651, 1.000), "W": (0.129, 0.580, 0.839),
52
+ "Re": (0.149, 0.490, 0.671), "Os": (0.149, 0.400, 0.588),
53
+ "Ir": (0.090, 0.329, 0.529), "Pt": (0.816, 0.816, 0.878),
54
+ "Au": (1.000, 0.820, 0.137), "Hg": (0.722, 0.722, 0.816),
55
+ "Tl": (0.651, 0.329, 0.302), "Pb": (0.341, 0.349, 0.380),
56
+ "Bi": (0.620, 0.310, 0.710), "Po": (0.671, 0.361, 0.000),
57
+ "At": (0.459, 0.310, 0.271), "Rn": (0.259, 0.510, 0.588),
58
+ "Fr": (0.259, 0.000, 0.400), "Ra": (0.000, 0.490, 0.000),
59
+ "Ac": (0.439, 0.671, 0.980), "Th": (0.000, 0.729, 1.000),
60
+ "Pa": (0.000, 0.631, 1.000), "U": (0.000, 0.561, 1.000),
61
+ "Np": (0.000, 0.502, 1.000), "Pu": (0.000, 0.420, 1.000),
62
+ }
63
+
64
+
65
+ def _default_color(symbol: str) -> Tuple[float, float, float]:
66
+ return _COLORS.get(symbol, (0.8, 0.8, 0.8))
67
+
68
+
69
+ # ── XYZ / extended-XYZ ─────────────────────────────────────────────────────────
70
+
71
+ def _load_xyz(path: str) -> "Structure":
72
+ from ._structure import Atom, Structure
73
+ s = Structure()
74
+ with open(path, encoding="utf-8") as fh:
75
+ lines = fh.readlines()
76
+ if not lines:
77
+ return s
78
+ n = int(lines[0].strip())
79
+ comment = lines[1] if len(lines) > 1 else ""
80
+
81
+ # Parse extended-XYZ lattice from comment line
82
+ if 'Lattice=' in comment or 'lattice=' in comment:
83
+ import re
84
+ m = re.search(r'[Ll]attice="([^"]+)"', comment)
85
+ if m:
86
+ vals = list(map(float, m.group(1).split()))
87
+ if len(vals) == 9:
88
+ s.cell = [vals[0:3], vals[3:6], vals[6:9]]
89
+
90
+ atom_lines = lines[2:2 + n]
91
+ properties_order = ["species", "pos", "pos", "pos"]
92
+ if 'Properties=' in comment or 'properties=' in comment:
93
+ import re
94
+ m = re.search(r'[Pp]roperties=(\S+)', comment)
95
+ if m:
96
+ properties_order = m.group(1).split(":")
97
+
98
+ for line in atom_lines:
99
+ parts = line.split()
100
+ if not parts:
101
+ continue
102
+ sym = parts[0]
103
+ x, y, z = float(parts[1]), float(parts[2]), float(parts[3])
104
+ cr, cg, cb = _default_color(sym)
105
+ s.atoms.append(Atom(sym, x, y, z, cr, cg, cb))
106
+ return s
107
+
108
+
109
+ def _save_xyz(s: "Structure", path: str) -> None:
110
+ lines = [str(len(s.atoms)) + "\n"]
111
+ if s.cell:
112
+ a, b, c = s.cell
113
+ lat = " ".join(f"{v:.6f}" for row in [a, b, c] for v in row)
114
+ lines.append(f'Lattice="{lat}" Properties=species:S:1:pos:R:3\n')
115
+ else:
116
+ lines.append("AtomForge structure\n")
117
+ for atom in s.atoms:
118
+ lines.append(f"{atom.symbol} {atom.x:.6f} {atom.y:.6f} {atom.z:.6f}\n")
119
+ with open(path, "w", encoding="utf-8") as fh:
120
+ fh.writelines(lines)
121
+
122
+
123
+ # ── ASE fallback ────────────────────────────────────────────────────────────────
124
+
125
+ def _load_ase(path: str) -> "Structure":
126
+ try:
127
+ import ase.io
128
+ import numpy as np
129
+ except ImportError:
130
+ raise ImportError(
131
+ "Install the 'ase' package to load this file format:\n"
132
+ " pip install ase"
133
+ )
134
+ from ._structure import Atom, Structure
135
+ atoms_ase = ase.io.read(path)
136
+ s = Structure()
137
+ if atoms_ase.cell is not None and any(atoms_ase.pbc):
138
+ cell = np.array(atoms_ase.cell)
139
+ s.cell = [list(cell[0]), list(cell[1]), list(cell[2])]
140
+ for sym, (x, y, z) in zip(atoms_ase.get_chemical_symbols(),
141
+ atoms_ase.get_positions()):
142
+ cr, cg, cb = _default_color(sym)
143
+ s.atoms.append(Atom(sym, float(x), float(y), float(z), cr, cg, cb))
144
+ return s
145
+
146
+
147
+ def _save_ase(s: "Structure", path: str) -> None:
148
+ try:
149
+ import ase
150
+ import ase.io
151
+ import numpy as np
152
+ except ImportError:
153
+ raise ImportError(
154
+ "Install the 'ase' package to save this file format:\n"
155
+ " pip install ase"
156
+ )
157
+ syms = [a.symbol for a in s.atoms]
158
+ pos = np.array([[a.x, a.y, a.z] for a in s.atoms])
159
+ cell = np.array(s.cell) if s.cell else None
160
+ pbc = s.cell is not None
161
+ ase_atoms = ase.Atoms(symbols=syms, positions=pos, cell=cell, pbc=pbc)
162
+ ase.io.write(path, ase_atoms)
163
+
164
+
165
+ # ── Format dispatch ─────────────────────────────────────────────────────────────
166
+
167
+ _XYZ_EXTS = {".xyz", ".extxyz"}
168
+ _ASE_EXTS = {".cif", ".vasp", ".poscar", ".pdb", ".mol", ".mol2", ".lmp",
169
+ ".lammps", ".dump", ".gen", ".sdf", ".cfg"}
170
+
171
+
172
+ def load(path: str) -> "Structure":
173
+ """Load a structure from file. XYZ/extxyz is handled natively; other formats require ASE."""
174
+ ext = Path(path).suffix.lower()
175
+ if ext in _XYZ_EXTS:
176
+ return _load_xyz(path)
177
+ if ext in _ASE_EXTS or not ext:
178
+ return _load_ase(path)
179
+ # Try XYZ first, fall back to ASE
180
+ try:
181
+ return _load_xyz(path)
182
+ except Exception:
183
+ return _load_ase(path)
184
+
185
+
186
+ def save(s: "Structure", path: str) -> None:
187
+ """Save a structure to file. XYZ/extxyz is handled natively; other formats require ASE."""
188
+ ext = Path(path).suffix.lower()
189
+ if ext in _XYZ_EXTS:
190
+ _save_xyz(s, path)
191
+ elif ext in _ASE_EXTS:
192
+ _save_ase(s, path)
193
+ else:
194
+ # Default to XYZ
195
+ _save_xyz(s, path)
@@ -0,0 +1,163 @@
1
+ """Core data types: Atom and Structure."""
2
+
3
+ import copy
4
+ import math
5
+ from dataclasses import dataclass, field
6
+ from typing import List, Optional
7
+
8
+
9
+ @dataclass
10
+ class Atom:
11
+ """A single atom with Cartesian coordinates (Angstrom) and display colour."""
12
+
13
+ symbol: str
14
+ x: float
15
+ y: float
16
+ z: float
17
+ r: float = field(default=1.0, repr=False)
18
+ g: float = field(default=1.0, repr=False)
19
+ b: float = field(default=1.0, repr=False)
20
+
21
+ def __repr__(self) -> str:
22
+ return f"Atom({self.symbol!r}, x={self.x:.4f}, y={self.y:.4f}, z={self.z:.4f})"
23
+
24
+ def copy(self) -> "Atom":
25
+ return copy.copy(self)
26
+
27
+
28
+ class Structure:
29
+ """
30
+ A collection of atoms with an optional periodic unit cell.
31
+
32
+ Attributes
33
+ ----------
34
+ atoms : list[Atom]
35
+ All atoms in Cartesian coordinates (Angstrom).
36
+ cell : list[list[float]] | None
37
+ 3×3 lattice vectors ``[[a1,a2,a3], [b1,b2,b3], [c1,c2,c3]]``
38
+ in Angstrom, or ``None`` for a non-periodic (cluster) structure.
39
+ """
40
+
41
+ def __init__(self) -> None:
42
+ self.atoms: List[Atom] = []
43
+ self.cell: Optional[List[List[float]]] = None
44
+
45
+ # ── Dunder helpers ──────────────────────────────────────────────────────────
46
+
47
+ def __len__(self) -> int:
48
+ return len(self.atoms)
49
+
50
+ def __repr__(self) -> str:
51
+ periodicity = "periodic" if self.cell else "non-periodic"
52
+ species: dict = {}
53
+ for a in self.atoms:
54
+ species[a.symbol] = species.get(a.symbol, 0) + 1
55
+ comp = " ".join(f"{s}{n}" for s, n in sorted(species.items()))
56
+ return f"Structure({len(self.atoms)} atoms, {comp}, {periodicity})"
57
+
58
+ def copy(self) -> "Structure":
59
+ s = Structure()
60
+ s.atoms = [a.copy() for a in self.atoms]
61
+ s.cell = [list(row) for row in self.cell] if self.cell else None
62
+ return s
63
+
64
+ # ── Atom editing ────────────────────────────────────────────────────────────
65
+
66
+ def add_atom(self, symbol: str, x: float, y: float, z: float,
67
+ r: float = -1.0, g: float = -1.0, b: float = -1.0) -> Atom:
68
+ """Append an atom and return it. Default colour is the element's CPK colour."""
69
+ from ._io import _default_color
70
+ cr, cg, cb = _default_color(symbol)
71
+ atom = Atom(symbol, float(x), float(y), float(z),
72
+ r=cr if r < 0 else r,
73
+ g=cg if g < 0 else g,
74
+ b=cb if b < 0 else b)
75
+ self.atoms.append(atom)
76
+ return atom
77
+
78
+ def remove_atom(self, index: int) -> Atom:
79
+ """Remove and return the atom at *index*."""
80
+ return self.atoms.pop(index)
81
+
82
+ # ── Bulk transforms ─────────────────────────────────────────────────────────
83
+
84
+ def translate(self, dx: float, dy: float, dz: float) -> "Structure":
85
+ """Shift all atoms in-place by (dx, dy, dz) Angstrom. Returns self."""
86
+ for a in self.atoms:
87
+ a.x += dx
88
+ a.y += dy
89
+ a.z += dz
90
+ return self
91
+
92
+ def scale(self, factor: float) -> "Structure":
93
+ """Scale all Cartesian coordinates and cell vectors by *factor*. Returns self."""
94
+ for a in self.atoms:
95
+ a.x *= factor
96
+ a.y *= factor
97
+ a.z *= factor
98
+ if self.cell:
99
+ self.cell = [[v * factor for v in row] for row in self.cell]
100
+ return self
101
+
102
+ def repeat(self, nx: int, ny: int = 1, nz: int = 1) -> "Structure":
103
+ """Return a new supercell replicated *nx × ny × nz* times. Requires a unit cell."""
104
+ if not self.cell:
105
+ raise ValueError("repeat() requires a unit cell (set structure.cell first)")
106
+ a, b, c = self.cell
107
+ out = Structure()
108
+ out.cell = [
109
+ [a[i] * nx for i in range(3)],
110
+ [b[i] * ny for i in range(3)],
111
+ [c[i] * nz for i in range(3)],
112
+ ]
113
+ for ix in range(nx):
114
+ for iy in range(ny):
115
+ for iz in range(nz):
116
+ for atom in self.atoms:
117
+ new_atom = atom.copy()
118
+ new_atom.x += a[0]*ix + b[0]*iy + c[0]*iz
119
+ new_atom.y += a[1]*ix + b[1]*iy + c[1]*iz
120
+ new_atom.z += a[2]*ix + b[2]*iy + c[2]*iz
121
+ out.atoms.append(new_atom)
122
+ return out
123
+
124
+ def filter_species(self, *symbols: str) -> "Structure":
125
+ """Return a new Structure containing only atoms of the given element symbols."""
126
+ keep = set(symbols)
127
+ out = self.copy()
128
+ out.atoms = [a for a in out.atoms if a.symbol in keep]
129
+ return out
130
+
131
+ def set_cell(self, a: float, b: float, c: float,
132
+ alpha: float = 90.0, beta: float = 90.0,
133
+ gamma: float = 90.0) -> "Structure":
134
+ """
135
+ Set the unit cell from lattice parameters (lengths in Å, angles in degrees).
136
+ Uses the standard convention: **a** along x, **b** in the xy-plane.
137
+ Returns self.
138
+ """
139
+ rad = math.pi / 180.0
140
+ ca = math.cos(alpha * rad)
141
+ cb = math.cos(beta * rad)
142
+ cg = math.cos(gamma * rad)
143
+ sg = math.sin(gamma * rad)
144
+ ax = a
145
+ bx = b * cg
146
+ by = b * sg
147
+ cx = c * cb
148
+ cy = c * (ca - cb * cg) / sg if sg > 1e-10 else 0.0
149
+ cz = math.sqrt(max(0.0, c * c - cx * cx - cy * cy))
150
+ self.cell = [[ax, 0.0, 0.0], [bx, by, 0.0], [cx, cy, cz]]
151
+ return self
152
+
153
+ # ── Viewing / I/O ───────────────────────────────────────────────────────────
154
+
155
+ def view(self) -> None:
156
+ """Open this structure in the AtomForge GUI (non-blocking)."""
157
+ from ._viewer import view
158
+ view(self)
159
+
160
+ def save(self, path: str) -> None:
161
+ """Save to file; format is inferred from the file extension."""
162
+ from ._io import save
163
+ save(self, path)
@@ -0,0 +1,97 @@
1
+ """Launch the AtomForge GUI to view a Structure."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import os
6
+ import subprocess
7
+ import sys
8
+ import tempfile
9
+ from pathlib import Path
10
+ from typing import TYPE_CHECKING, Optional
11
+
12
+ if TYPE_CHECKING:
13
+ from ._structure import Structure
14
+
15
+
16
+ def _find_atomforge() -> Optional[str]:
17
+ """Return the path to the AtomForge executable, or None if not found."""
18
+ # 1. Explicit env var
19
+ env = os.environ.get("ATOMFORGE_PATH")
20
+ if env and Path(env).is_file():
21
+ return env
22
+
23
+ # 2. Relative to this package file (handles unpacked release: python/ lives next to AtomForge.exe)
24
+ pkg_dir = Path(__file__).resolve().parent
25
+ for candidate in [
26
+ pkg_dir / "AtomForge.exe",
27
+ pkg_dir / "AtomForge",
28
+ pkg_dir.parent / "AtomForge.exe",
29
+ pkg_dir.parent / "AtomForge",
30
+ pkg_dir.parent.parent / "AtomForge.exe",
31
+ pkg_dir.parent.parent / "AtomForge",
32
+ pkg_dir.parent.parent / "build" / "Release" / "AtomForge.exe",
33
+ pkg_dir.parent.parent / "build" / "AtomForge.exe",
34
+ pkg_dir.parent.parent / "build" / "AtomForge",
35
+ ]:
36
+ if candidate.is_file():
37
+ return str(candidate)
38
+
39
+ # 3. System PATH
40
+ import shutil
41
+ found = shutil.which("AtomForge") or shutil.which("atomforge")
42
+ if found:
43
+ return found
44
+
45
+ # 4. Common Windows install paths
46
+ if sys.platform == "win32":
47
+ program_files = os.environ.get("ProgramFiles", r"C:\Program Files")
48
+ for root in [program_files, os.environ.get("ProgramFiles(x86)", "")]:
49
+ candidate = Path(root) / "AtomForge" / "AtomForge.exe"
50
+ if candidate.is_file():
51
+ return str(candidate)
52
+
53
+ return None
54
+
55
+
56
+ def view(s: "Structure") -> None:
57
+ """
58
+ Open *s* in the AtomForge GUI (non-blocking).
59
+
60
+ The structure is serialised to a temporary extXYZ file and AtomForge is
61
+ launched as a detached subprocess. The temp file is cleaned up after
62
+ AtomForge exits (on POSIX) or left to the OS temp-file sweeper (Windows),
63
+ because AtomForge must be able to read the file after this function returns.
64
+ """
65
+ exe = _find_atomforge()
66
+ if exe is None:
67
+ raise FileNotFoundError(
68
+ "AtomForge executable not found. Set the ATOMFORGE_PATH environment "
69
+ "variable to the full path of AtomForge.exe, or ensure it is on PATH."
70
+ )
71
+
72
+ # Write to a named temp file that survives past this function
73
+ suffix = ".xyz"
74
+ tmp = tempfile.NamedTemporaryFile(
75
+ prefix="atomforge_view_", suffix=suffix, delete=False
76
+ )
77
+ tmp_path = tmp.name
78
+ tmp.close()
79
+
80
+ from ._io import _save_xyz
81
+ _save_xyz(s, tmp_path)
82
+
83
+ if sys.platform == "win32":
84
+ # DETACHED_PROCESS: new console, parent death does not kill child
85
+ DETACHED_PROCESS = 0x00000008
86
+ CREATE_NEW_PROCESS_GROUP = 0x00000200
87
+ subprocess.Popen(
88
+ [exe, tmp_path],
89
+ creationflags=DETACHED_PROCESS | CREATE_NEW_PROCESS_GROUP,
90
+ close_fds=True,
91
+ )
92
+ else:
93
+ subprocess.Popen(
94
+ [exe, tmp_path],
95
+ start_new_session=True,
96
+ close_fds=True,
97
+ )
@@ -0,0 +1,70 @@
1
+ Metadata-Version: 2.4
2
+ Name: atomforge-py
3
+ Version: 0.1.0
4
+ Summary: Python interface for AtomForge: load, edit, and view atomic structures
5
+ Author: AtomForge Contributors
6
+ Author-email: AtomForge Contributors <albert.hzbn@gmail.com>
7
+ License-Expression: MIT
8
+ Project-URL: Homepage, https://github.com/albert-hzbn/AtomForge
9
+ Project-URL: Repository, https://github.com/albert-hzbn/AtomForge
10
+ Project-URL: Bug Tracker, https://github.com/albert-hzbn/AtomForge/issues
11
+ Keywords: atomforge,atoms,crystal,materials,structure,viewer
12
+ Classifier: Development Status :: 3 - Alpha
13
+ Classifier: Intended Audience :: Science/Research
14
+ Classifier: Programming Language :: Python :: 3
15
+ Classifier: Programming Language :: Python :: 3.8
16
+ Classifier: Programming Language :: Python :: 3.9
17
+ Classifier: Programming Language :: Python :: 3.10
18
+ Classifier: Programming Language :: Python :: 3.11
19
+ Classifier: Programming Language :: Python :: 3.12
20
+ Classifier: Topic :: Scientific/Engineering :: Chemistry
21
+ Classifier: Topic :: Scientific/Engineering :: Physics
22
+ Requires-Python: >=3.8
23
+ Description-Content-Type: text/x-rst
24
+ Provides-Extra: ase
25
+ Requires-Dist: ase>=3.22; extra == "ase"
26
+ Dynamic: author
27
+ Dynamic: requires-python
28
+
29
+ atomforge — Python interface for AtomForge
30
+ ==========================================
31
+
32
+ Load, programmatically edit, and launch the AtomForge GUI to view atomic
33
+ structures directly from Python.
34
+
35
+ Quick-start
36
+ -----------
37
+
38
+ .. code-block:: python
39
+
40
+ import atomforge as af
41
+
42
+ # Load any format (XYZ/extXYZ natively; CIF/VASP/PDB via ase extra)
43
+ s = af.load("crystal.cif")
44
+
45
+ # Build from scratch
46
+ s2 = af.Structure()
47
+ s2.set_cell(2.87, 2.87, 2.87) # BCC iron unit cell
48
+ s2.add_atom("Fe", 0.0, 0.0, 0.0)
49
+ s2.add_atom("Fe", 1.435, 1.435, 1.435)
50
+
51
+ # Supercell + open in the AtomForge GUI (non-blocking)
52
+ s2.repeat(4, 4, 4).view()
53
+
54
+ # Edit and save
55
+ s2.translate(1, 0, 0).save("shifted.xyz")
56
+
57
+ Formats
58
+ -------
59
+ XYZ and extended-XYZ are handled natively with no dependencies.
60
+ CIF, VASP/POSCAR, PDB, LAMMPS, mol2, and all other ASE-supported formats
61
+ are available via the ``ase`` extra::
62
+
63
+ pip install "atomforge[ase]"
64
+
65
+ Viewing
66
+ -------
67
+ ``structure.view()`` serialises the structure to a temporary XYZ file and
68
+ launches the AtomForge executable as a detached subprocess. Set the
69
+ ``ATOMFORGE_PATH`` environment variable to the full path of ``AtomForge.exe``
70
+ if it is not on PATH.
@@ -0,0 +1,11 @@
1
+ pyproject.toml
2
+ setup.py
3
+ atomforge/__init__.py
4
+ atomforge/_io.py
5
+ atomforge/_structure.py
6
+ atomforge/_viewer.py
7
+ atomforge_py.egg-info/PKG-INFO
8
+ atomforge_py.egg-info/SOURCES.txt
9
+ atomforge_py.egg-info/dependency_links.txt
10
+ atomforge_py.egg-info/requires.txt
11
+ atomforge_py.egg-info/top_level.txt
@@ -0,0 +1,3 @@
1
+
2
+ [ase]
3
+ ase>=3.22
@@ -0,0 +1 @@
1
+ atomforge
@@ -0,0 +1,76 @@
1
+ [build-system]
2
+ requires = ["setuptools>=61", "wheel"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "atomforge-py"
7
+ version = "0.1.0"
8
+ description = "Python interface for AtomForge: load, edit, and view atomic structures"
9
+ authors = [{ name = "AtomForge Contributors", email = "albert.hzbn@gmail.com" }]
10
+ readme = { text = """
11
+ atomforge — Python interface for AtomForge
12
+ ==========================================
13
+
14
+ Load, programmatically edit, and launch the AtomForge GUI to view atomic
15
+ structures directly from Python.
16
+
17
+ Quick-start
18
+ -----------
19
+
20
+ .. code-block:: python
21
+
22
+ import atomforge as af
23
+
24
+ # Load any format (XYZ/extXYZ natively; CIF/VASP/PDB via ase extra)
25
+ s = af.load("crystal.cif")
26
+
27
+ # Build from scratch
28
+ s2 = af.Structure()
29
+ s2.set_cell(2.87, 2.87, 2.87) # BCC iron unit cell
30
+ s2.add_atom("Fe", 0.0, 0.0, 0.0)
31
+ s2.add_atom("Fe", 1.435, 1.435, 1.435)
32
+
33
+ # Supercell + open in the AtomForge GUI (non-blocking)
34
+ s2.repeat(4, 4, 4).view()
35
+
36
+ # Edit and save
37
+ s2.translate(1, 0, 0).save("shifted.xyz")
38
+
39
+ Formats
40
+ -------
41
+ XYZ and extended-XYZ are handled natively with no dependencies.
42
+ CIF, VASP/POSCAR, PDB, LAMMPS, mol2, and all other ASE-supported formats
43
+ are available via the ``ase`` extra::
44
+
45
+ pip install "atomforge[ase]"
46
+
47
+ Viewing
48
+ -------
49
+ ``structure.view()`` serialises the structure to a temporary XYZ file and
50
+ launches the AtomForge executable as a detached subprocess. Set the
51
+ ``ATOMFORGE_PATH`` environment variable to the full path of ``AtomForge.exe``
52
+ if it is not on PATH.
53
+ """, content-type = "text/x-rst" }
54
+ requires-python = ">=3.8"
55
+ license = "MIT"
56
+ keywords = ["atomforge", "atoms", "crystal", "materials", "structure", "viewer"]
57
+ classifiers = [
58
+ "Development Status :: 3 - Alpha",
59
+ "Intended Audience :: Science/Research",
60
+ "Programming Language :: Python :: 3",
61
+ "Programming Language :: Python :: 3.8",
62
+ "Programming Language :: Python :: 3.9",
63
+ "Programming Language :: Python :: 3.10",
64
+ "Programming Language :: Python :: 3.11",
65
+ "Programming Language :: Python :: 3.12",
66
+ "Topic :: Scientific/Engineering :: Chemistry",
67
+ "Topic :: Scientific/Engineering :: Physics",
68
+ ]
69
+
70
+ [project.optional-dependencies]
71
+ ase = ["ase>=3.22"]
72
+
73
+ [project.urls]
74
+ Homepage = "https://github.com/albert-hzbn/AtomForge"
75
+ Repository = "https://github.com/albert-hzbn/AtomForge"
76
+ "Bug Tracker" = "https://github.com/albert-hzbn/AtomForge/issues"
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,19 @@
1
+ from setuptools import setup, find_packages
2
+
3
+ setup(
4
+ name="atomforge-py",
5
+ version="0.1.0",
6
+ description="Python interface for AtomForge: load, edit, and view atomic structures",
7
+ author="AtomForge Contributors",
8
+ python_requires=">=3.8",
9
+ packages=find_packages(),
10
+ install_requires=[],
11
+ extras_require={
12
+ "ase": ["ase>=3.22"],
13
+ },
14
+ classifiers=[
15
+ "Programming Language :: Python :: 3",
16
+ "Topic :: Scientific/Engineering :: Chemistry",
17
+ "Topic :: Scientific/Engineering :: Physics",
18
+ ],
19
+ )