pyatomsk 0.3.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.
- pyatomsk-0.3.0/LICENSE +21 -0
- pyatomsk-0.3.0/PKG-INFO +9 -0
- pyatomsk-0.3.0/README.md +149 -0
- pyatomsk-0.3.0/pyatomsk/__init__.py +58 -0
- pyatomsk-0.3.0/pyatomsk/atomsk.py +93 -0
- pyatomsk-0.3.0/pyatomsk/commands.py +51 -0
- pyatomsk-0.3.0/pyatomsk/dislocations.py +117 -0
- pyatomsk-0.3.0/pyatomsk/structures.py +130 -0
- pyatomsk-0.3.0/pyatomsk/view.py +58 -0
- pyatomsk-0.3.0/pyatomsk.egg-info/PKG-INFO +9 -0
- pyatomsk-0.3.0/pyatomsk.egg-info/SOURCES.txt +14 -0
- pyatomsk-0.3.0/pyatomsk.egg-info/dependency_links.txt +1 -0
- pyatomsk-0.3.0/pyatomsk.egg-info/requires.txt +1 -0
- pyatomsk-0.3.0/pyatomsk.egg-info/top_level.txt +1 -0
- pyatomsk-0.3.0/setup.cfg +4 -0
- pyatomsk-0.3.0/setup.py +12 -0
pyatomsk-0.3.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Rodolfo Herrera Hernandez
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
pyatomsk-0.3.0/PKG-INFO
ADDED
pyatomsk-0.3.0/README.md
ADDED
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
# Python wrapper for Atomsk
|
|
2
|
+
|
|
3
|
+
## Install
|
|
4
|
+
|
|
5
|
+
```bash
|
|
6
|
+
pip install pyatomsk
|
|
7
|
+
```
|
|
8
|
+
|
|
9
|
+
## Quick start
|
|
10
|
+
|
|
11
|
+
```python
|
|
12
|
+
from pathlib import Path
|
|
13
|
+
from pyatomsk import AtomicStructure, CubicLattices, DislocationBuilder, DislocationLoop, PluginHub, view
|
|
14
|
+
|
|
15
|
+
REGISTRY = 'https://raw.githubusercontent.com/VoltLabs-Research/Volt/main/server/static/plugin-registry'
|
|
16
|
+
OUT = Path('output/fcc-dxa')
|
|
17
|
+
|
|
18
|
+
hub = PluginHub(url=REGISTRY, default_publisher='voltlabs')
|
|
19
|
+
ptm = hub.get('polyhedral-template-matching')
|
|
20
|
+
dxa = hub.get('opendxa')
|
|
21
|
+
|
|
22
|
+
# 1. Build an FCC Al slab with a prismatic dislocation loop.
|
|
23
|
+
structure = AtomicStructure(
|
|
24
|
+
lattice=CubicLattices.FCC,
|
|
25
|
+
lattice_params=[4.06],
|
|
26
|
+
species=['Al'],
|
|
27
|
+
orient=['[110]', '[1-12]', '[-111]'],
|
|
28
|
+
duplicate=[60, 40, 20],
|
|
29
|
+
)
|
|
30
|
+
loop = DislocationLoop(x='0.501*box', y='0.501*box', z='0.501*box',
|
|
31
|
+
normal='Z', radius=30, burgers=[2.860954, 0, 0], poisson=0.33)
|
|
32
|
+
builder = DislocationBuilder(atomic_structure=structure, output_file='Al_loop.lmp', dislocations=[loop])
|
|
33
|
+
lmp = builder.run() # downloads Atomsk if needed, returns the output Path
|
|
34
|
+
|
|
35
|
+
# 2. Run the analysis plugins locally.
|
|
36
|
+
ptm_run = ptm(lmp, output_dir=OUT, crystal_structure='fcc', rmsd=0.1)
|
|
37
|
+
dxa_run = dxa(ptm_run, output_dir=OUT, reference_topology='fcc', export_as='json')
|
|
38
|
+
|
|
39
|
+
# 3. Inspect results as a DataFrame, then view them in VOLT.
|
|
40
|
+
print(dxa_run['dislocations.json'].df())
|
|
41
|
+
view(ptm_run['atoms.msgpack'], output_path=OUT / 'ptm_atoms.glb')
|
|
42
|
+
view(dxa_run['dislocations.json'])
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
## Building structures
|
|
47
|
+
|
|
48
|
+
`AtomicStructure` maps directly onto `atomsk --create`. You can inspect the command
|
|
49
|
+
before running it:
|
|
50
|
+
|
|
51
|
+
```python
|
|
52
|
+
from pyatomsk import AtomicStructure, CubicLattices
|
|
53
|
+
|
|
54
|
+
s = AtomicStructure(
|
|
55
|
+
lattice=CubicLattices.FCC,
|
|
56
|
+
lattice_params=[4.05], # a (and c for tetragonal/hexagonal)
|
|
57
|
+
species=['Al'],
|
|
58
|
+
duplicate=[10, 10, 10],
|
|
59
|
+
export_filename='al.xsf',
|
|
60
|
+
formats=['xsf'],
|
|
61
|
+
)
|
|
62
|
+
s.to_command() # 'atomsk --create fcc 4.05 Al -duplicate 10 10 10 al.xsf xsf'
|
|
63
|
+
s.run() # executes it, returns Path('al.xsf')
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
Lattice enums: `CubicLattices` (FCC, BCC, SC, diamond, …), `TetragonalLattices`
|
|
67
|
+
(BCT, FCT, ST, L1_0) and `HexagonalLattices` (HCP, graphite, …).
|
|
68
|
+
|
|
69
|
+
For non-standard cells, `CustomAtomicStructure` writes an XSF seed from an explicit
|
|
70
|
+
`cell` + fractional `basis` and feeds it to Atomsk.
|
|
71
|
+
|
|
72
|
+
## Adding dislocations
|
|
73
|
+
|
|
74
|
+
`DislocationBuilder` prepends a structure command and appends one or more
|
|
75
|
+
`-dislocation` fragments:
|
|
76
|
+
|
|
77
|
+
```python
|
|
78
|
+
from pyatomsk import AtomicStructure, TetragonalLattices, Dislocation, DislocationCharacter, DislocationBuilder
|
|
79
|
+
|
|
80
|
+
structure = AtomicStructure(lattice=TetragonalLattices.BCT, lattice_params=[2.86, 2.95],
|
|
81
|
+
species=['Fe'], duplicate=[32, 32, 24])
|
|
82
|
+
edge = Dislocation(character=DislocationCharacter.EDGE_ADD, p1='0.501*box', p2='0.501*box',
|
|
83
|
+
line='z', plane='y', burgers=[2.86], poisson=0.29)
|
|
84
|
+
|
|
85
|
+
builder = DislocationBuilder(atomic_structure=structure, output_file='bct.lmp', dislocations=[edge])
|
|
86
|
+
builder.run()
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
Use `DislocationLoop` for prismatic loops (`-dislocation loop …`). Both are pure
|
|
90
|
+
command fragments; only `DislocationBuilder` (and the structures) are runnable.
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
## Running VOLT plugins
|
|
94
|
+
|
|
95
|
+
Plugins are fetched from a VOLT plugin registry and executed as local subprocesses by
|
|
96
|
+
voltsdk. A `PluginRun` exposes its outputs as artifacts you
|
|
97
|
+
can look up by name (fuzzy: `.json` ⇄ `.msgpack`, prefixes stripped):
|
|
98
|
+
|
|
99
|
+
```python
|
|
100
|
+
run = ptm(lmp, output_dir='out', crystal_structure='fcc', rmsd=0.1)
|
|
101
|
+
run['annotated.dump'] # PluginArtifact (os.PathLike)
|
|
102
|
+
run['clusters.table'].df() # pandas DataFrame
|
|
103
|
+
run['atoms.msgpack'].json() # parsed payload
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
OpenDXA auto-wires from a previous run: `dxa(ptm_run, reference_topology='fcc')` pulls the
|
|
107
|
+
annotated dump and cluster tables automatically. After a `pattern-structure-matching` run,
|
|
108
|
+
`dxa(psm_run)` also infers `reference_topology` and `lattice_dir` from the run's lattices.
|
|
109
|
+
|
|
110
|
+
## Viewing in VOLT
|
|
111
|
+
|
|
112
|
+
`view()` converts an artifact (or a path / list of them) to GLB, the exporter is detected
|
|
113
|
+
from the payload, and opens it in the VOLT canvas via a local server. GLB files pass
|
|
114
|
+
through untouched.
|
|
115
|
+
|
|
116
|
+
```python
|
|
117
|
+
view(dxa_run['dislocations.json']) # opens in VOLT
|
|
118
|
+
view(ptm_run['atoms.msgpack'], output_path='atoms.glb') # also keep the GLB
|
|
119
|
+
view(dxa_run['defect_mesh.json'], exporter='MeshExporter') # force a specific layer
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
`view()` returns the viewer URL. Equivalent low-level voltsdk calls are re-exported too:
|
|
123
|
+
`PluginArtifact.glb()`, `PluginArtifact.open_in_volt()`, and `open_in_volt(path)`.
|
|
124
|
+
|
|
125
|
+
## Configuration
|
|
126
|
+
|
|
127
|
+
| Variable | Purpose |
|
|
128
|
+
|---|---|
|
|
129
|
+
| `ATOMSK_BIN` | Path to an existing Atomsk executable (skips auto-download). |
|
|
130
|
+
| `XDG_CACHE_HOME` | Cache root; Atomsk under `<cache>/pyatomsk`, plugins under `<cache>/volt`. |
|
|
131
|
+
| `VOLT_PLUGIN_REGISTRY` | Override the plugin registry URL. |
|
|
132
|
+
| `VOLT_CACHE_DIR` | Override the voltsdk (plugin) cache directory. |
|
|
133
|
+
| `VOLT_APP_URL` | VOLT app URL used by the viewer. |
|
|
134
|
+
|
|
135
|
+
## API reference
|
|
136
|
+
|
|
137
|
+
| Symbol | Description |
|
|
138
|
+
|---|---|
|
|
139
|
+
| `AtomicStructure`, `CustomAtomicStructure` | Build a crystal via `atomsk --create` / an XSF seed. |
|
|
140
|
+
| `CubicLattices`, `TetragonalLattices`, `HexagonalLattices` | Lattice type enums. |
|
|
141
|
+
| `Dislocation`, `DislocationLoop` | `-dislocation` command fragments. |
|
|
142
|
+
| `DislocationBuilder` | Combine a structure with one or more dislocations. |
|
|
143
|
+
| `AtomskCommand` | Base class: `.argv()`, `.to_command()`, `.run() -> Path`. |
|
|
144
|
+
| `view(source, *, output_path=None, …)` | Open an artifact/GLB/list in the VOLT canvas. |
|
|
145
|
+
| `PluginHub`, `Plugin`, `PluginRun`, `PluginArtifact`, `Lattice`, `SpatialAssembler`, `open_in_volt` | Re-exported from voltsdk. |
|
|
146
|
+
|
|
147
|
+
## License
|
|
148
|
+
|
|
149
|
+
See the repository for license details.
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
"""Build Atomsk structures/dislocations, run plugins locally, and view results in VOLT.
|
|
2
|
+
|
|
3
|
+
Plugin compute runs on your machine via :mod:`voltsdk`; visualization is delegated to the
|
|
4
|
+
VOLT canvas (``view`` / ``open_in_volt``). pyatomsk's own surface is just the Atomsk command
|
|
5
|
+
builders plus a thin viewer helper.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from voltsdk import (
|
|
9
|
+
Lattice,
|
|
10
|
+
Plugin,
|
|
11
|
+
PluginArtifact,
|
|
12
|
+
PluginError,
|
|
13
|
+
PluginHub,
|
|
14
|
+
PluginRun,
|
|
15
|
+
SpatialAssembler,
|
|
16
|
+
open_in_volt,
|
|
17
|
+
)
|
|
18
|
+
|
|
19
|
+
from pyatomsk.commands import AtomskCommand
|
|
20
|
+
from pyatomsk.dislocations import (
|
|
21
|
+
Dislocation,
|
|
22
|
+
DislocationBuilder,
|
|
23
|
+
DislocationCharacter,
|
|
24
|
+
DislocationLoop,
|
|
25
|
+
)
|
|
26
|
+
from pyatomsk.structures import (
|
|
27
|
+
AtomicStructure,
|
|
28
|
+
CubicLattices,
|
|
29
|
+
CustomAtomicStructure,
|
|
30
|
+
HexagonalLattices,
|
|
31
|
+
TetragonalLattices,
|
|
32
|
+
)
|
|
33
|
+
from pyatomsk.view import view
|
|
34
|
+
|
|
35
|
+
__all__ = [
|
|
36
|
+
# voltsdk re-exports (local compute, VOLT viewing)
|
|
37
|
+
'PluginHub',
|
|
38
|
+
'Plugin',
|
|
39
|
+
'PluginArtifact',
|
|
40
|
+
'PluginRun',
|
|
41
|
+
'PluginError',
|
|
42
|
+
'Lattice',
|
|
43
|
+
'SpatialAssembler',
|
|
44
|
+
'open_in_volt',
|
|
45
|
+
# Atomsk command builders
|
|
46
|
+
'AtomskCommand',
|
|
47
|
+
'AtomicStructure',
|
|
48
|
+
'CustomAtomicStructure',
|
|
49
|
+
'CubicLattices',
|
|
50
|
+
'TetragonalLattices',
|
|
51
|
+
'HexagonalLattices',
|
|
52
|
+
'Dislocation',
|
|
53
|
+
'DislocationCharacter',
|
|
54
|
+
'DislocationLoop',
|
|
55
|
+
'DislocationBuilder',
|
|
56
|
+
# thin viewer helper
|
|
57
|
+
'view',
|
|
58
|
+
]
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
"""Resolve the Atomsk executable, downloading the official binary if needed."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import os
|
|
6
|
+
import platform
|
|
7
|
+
import shutil
|
|
8
|
+
import tarfile
|
|
9
|
+
import urllib.request
|
|
10
|
+
import zipfile
|
|
11
|
+
from pathlib import Path
|
|
12
|
+
|
|
13
|
+
ATOMSK_VERSION = 'b0.13.1'
|
|
14
|
+
_BASE_URL = 'https://atomsk.univ-lille.fr/code'
|
|
15
|
+
|
|
16
|
+
# (system, machine) -> (archive filename, executable name)
|
|
17
|
+
_BINARIES = {
|
|
18
|
+
('linux', 'x86_64'): (f'atomsk_{ATOMSK_VERSION}_Linux-amd64.tar.gz', 'atomsk'),
|
|
19
|
+
('linux', 'amd64'): (f'atomsk_{ATOMSK_VERSION}_Linux-amd64.tar.gz', 'atomsk'),
|
|
20
|
+
('linux', 'i686'): (f'atomsk_{ATOMSK_VERSION}_Linux-i686.tar.gz', 'atomsk'),
|
|
21
|
+
('linux', 'i386'): (f'atomsk_{ATOMSK_VERSION}_Linux-i686.tar.gz', 'atomsk'),
|
|
22
|
+
('windows', 'amd64'): (f'atomsk_{ATOMSK_VERSION}_Windows.zip', 'atomsk.exe'),
|
|
23
|
+
('windows', 'x86_64'): (f'atomsk_{ATOMSK_VERSION}_Windows.zip', 'atomsk.exe'),
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def _cache_dir() -> Path:
|
|
28
|
+
base = os.environ.get('XDG_CACHE_HOME') or str(Path.home() / '.cache')
|
|
29
|
+
return Path(base) / 'pyatomsk'
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def _extract(archive: Path, target: Path) -> None:
|
|
33
|
+
if archive.name.endswith('.tar.gz'):
|
|
34
|
+
with tarfile.open(archive, 'r:gz') as tar:
|
|
35
|
+
tar.extractall(target)
|
|
36
|
+
elif archive.suffix == '.zip':
|
|
37
|
+
with zipfile.ZipFile(archive) as zf:
|
|
38
|
+
zf.extractall(target)
|
|
39
|
+
else:
|
|
40
|
+
raise ValueError(f'Unsupported Atomsk archive: {archive.name}')
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def ensure_atomsk(*, version: str = ATOMSK_VERSION, force: bool = False) -> Path:
|
|
44
|
+
"""Return the Atomsk executable for this OS, downloading and caching it if needed.
|
|
45
|
+
|
|
46
|
+
Resolution order: ``$ATOMSK_BIN`` -> ``atomsk`` on PATH -> cached download ->
|
|
47
|
+
download the official binary. Only Linux and Windows on x86 have official static
|
|
48
|
+
binaries; elsewhere (macOS, ARM) install Atomsk yourself (e.g.
|
|
49
|
+
``conda install -c conda-forge atomsk``) and point ``ATOMSK_BIN`` at it or add it
|
|
50
|
+
to PATH.
|
|
51
|
+
"""
|
|
52
|
+
override = os.environ.get('ATOMSK_BIN')
|
|
53
|
+
if override:
|
|
54
|
+
return Path(override).expanduser()
|
|
55
|
+
|
|
56
|
+
if not force:
|
|
57
|
+
on_path = shutil.which('atomsk')
|
|
58
|
+
if on_path:
|
|
59
|
+
return Path(on_path)
|
|
60
|
+
|
|
61
|
+
system, machine = platform.system().lower(), platform.machine().lower()
|
|
62
|
+
entry = _BINARIES.get((system, machine))
|
|
63
|
+
if entry is None:
|
|
64
|
+
raise RuntimeError(
|
|
65
|
+
f'No prebuilt Atomsk binary for {system}-{machine}. Install it '
|
|
66
|
+
'(e.g. `conda install -c conda-forge atomsk`), add it to PATH, or set '
|
|
67
|
+
'ATOMSK_BIN to its location. See https://atomsk.univ-lille.fr/dl.php'
|
|
68
|
+
)
|
|
69
|
+
|
|
70
|
+
filename, exe = entry
|
|
71
|
+
install_dir = _cache_dir() / 'atomsk' / version / f'{system}-{machine}'
|
|
72
|
+
if install_dir.is_dir() and not force:
|
|
73
|
+
cached = next(install_dir.rglob(exe), None)
|
|
74
|
+
if cached is not None:
|
|
75
|
+
return cached
|
|
76
|
+
|
|
77
|
+
downloads = _cache_dir() / 'downloads'
|
|
78
|
+
downloads.mkdir(parents=True, exist_ok=True)
|
|
79
|
+
archive = downloads / filename
|
|
80
|
+
if not archive.is_file() or force:
|
|
81
|
+
with urllib.request.urlopen(f'{_BASE_URL}/{filename}') as response, archive.open('wb') as out:
|
|
82
|
+
shutil.copyfileobj(response, out)
|
|
83
|
+
|
|
84
|
+
if install_dir.exists():
|
|
85
|
+
shutil.rmtree(install_dir)
|
|
86
|
+
install_dir.mkdir(parents=True, exist_ok=True)
|
|
87
|
+
_extract(archive, install_dir)
|
|
88
|
+
|
|
89
|
+
binary = next(install_dir.rglob(exe), None)
|
|
90
|
+
if binary is None:
|
|
91
|
+
raise RuntimeError(f'{exe!r} not found inside the downloaded Atomsk archive {filename!r}.')
|
|
92
|
+
binary.chmod(binary.stat().st_mode | 0o111)
|
|
93
|
+
return binary
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import shlex
|
|
2
|
+
import subprocess
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
from typing import Any
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
def _prepare_output_path(path: str | Path) -> Path:
|
|
8
|
+
target = Path(path).expanduser()
|
|
9
|
+
target.parent.mkdir(parents=True, exist_ok=True)
|
|
10
|
+
if target.exists():
|
|
11
|
+
if target.is_dir():
|
|
12
|
+
raise IsADirectoryError(f'Expected a file output path, got directory: {target}')
|
|
13
|
+
target.unlink()
|
|
14
|
+
return target
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class AtomskCommand:
|
|
18
|
+
"""Base for objects that generate (and run) an ``atomsk`` command."""
|
|
19
|
+
|
|
20
|
+
def argv(self) -> list[str]:
|
|
21
|
+
raise NotImplementedError
|
|
22
|
+
|
|
23
|
+
def to_command(self) -> str:
|
|
24
|
+
return shlex.join(self.argv())
|
|
25
|
+
|
|
26
|
+
def prepare_run(self) -> None:
|
|
27
|
+
"""Hook for filesystem prep (seed files, output paths). Default: no-op."""
|
|
28
|
+
|
|
29
|
+
def output_path(self) -> Path | None:
|
|
30
|
+
"""Path the command writes, or ``None`` if it only prints to stdout."""
|
|
31
|
+
return None
|
|
32
|
+
|
|
33
|
+
def run(
|
|
34
|
+
self,
|
|
35
|
+
*,
|
|
36
|
+
atomsk_output: bool = False,
|
|
37
|
+
check: bool = True,
|
|
38
|
+
text: bool = True,
|
|
39
|
+
**kwargs: Any,
|
|
40
|
+
) -> Path | None:
|
|
41
|
+
from pyatomsk.atomsk import ensure_atomsk
|
|
42
|
+
|
|
43
|
+
run_kwargs = dict(kwargs)
|
|
44
|
+
if not atomsk_output:
|
|
45
|
+
run_kwargs.setdefault('stdout', subprocess.PIPE)
|
|
46
|
+
run_kwargs.setdefault('stderr', subprocess.PIPE)
|
|
47
|
+
self.prepare_run()
|
|
48
|
+
argv = self.argv()
|
|
49
|
+
argv[0] = str(ensure_atomsk())
|
|
50
|
+
subprocess.run(argv, check=check, text=text, **run_kwargs)
|
|
51
|
+
return self.output_path()
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
import shlex
|
|
2
|
+
from dataclasses import dataclass, field
|
|
3
|
+
from enum import Enum
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
from typing import Sequence, Union
|
|
6
|
+
|
|
7
|
+
from pyatomsk.commands import AtomskCommand, _prepare_output_path
|
|
8
|
+
|
|
9
|
+
Coord = Union[int, float, str]
|
|
10
|
+
Burgers = Union[float, Sequence[float]]
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class DislocationCharacter(Enum):
|
|
14
|
+
EDGE = 'edge'
|
|
15
|
+
EDGE_ADD = 'edge_add'
|
|
16
|
+
EDGE_RM = 'edge_rm'
|
|
17
|
+
SCREW = 'screw'
|
|
18
|
+
MIXED = 'mixed'
|
|
19
|
+
LOOP = 'loop'
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def _burgers_args(burgers: Burgers) -> list[Coord]:
|
|
23
|
+
if isinstance(burgers, Sequence) and not isinstance(burgers, (str, bytes)):
|
|
24
|
+
return list(burgers)
|
|
25
|
+
return [burgers]
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class DislocationSpec:
|
|
29
|
+
"""An ``-dislocation ...`` argv fragment (not a standalone command)."""
|
|
30
|
+
|
|
31
|
+
def argv(self) -> list[str]:
|
|
32
|
+
raise NotImplementedError
|
|
33
|
+
|
|
34
|
+
def to_command(self) -> str:
|
|
35
|
+
return shlex.join(self.argv())
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
@dataclass
|
|
39
|
+
class Dislocation(DislocationSpec):
|
|
40
|
+
character: DislocationCharacter
|
|
41
|
+
p1: Coord
|
|
42
|
+
p2: Coord
|
|
43
|
+
line: str
|
|
44
|
+
plane: str
|
|
45
|
+
burgers: Burgers
|
|
46
|
+
poisson: float | None = None
|
|
47
|
+
|
|
48
|
+
def argv(self) -> list[str]:
|
|
49
|
+
args = [
|
|
50
|
+
'-dislocation',
|
|
51
|
+
self.p1,
|
|
52
|
+
self.p2,
|
|
53
|
+
self.character.value,
|
|
54
|
+
self.line,
|
|
55
|
+
self.plane,
|
|
56
|
+
*_burgers_args(self.burgers),
|
|
57
|
+
]
|
|
58
|
+
if self.poisson is not None:
|
|
59
|
+
args.append(self.poisson)
|
|
60
|
+
return [str(arg) for arg in args]
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
@dataclass
|
|
64
|
+
class DislocationLoop(DislocationSpec):
|
|
65
|
+
x: Coord
|
|
66
|
+
y: Coord
|
|
67
|
+
z: Coord
|
|
68
|
+
normal: str
|
|
69
|
+
radius: float
|
|
70
|
+
burgers: Sequence[float]
|
|
71
|
+
poisson: float
|
|
72
|
+
|
|
73
|
+
def argv(self) -> list[str]:
|
|
74
|
+
args = [
|
|
75
|
+
'-dislocation',
|
|
76
|
+
'loop',
|
|
77
|
+
self.x,
|
|
78
|
+
self.y,
|
|
79
|
+
self.z,
|
|
80
|
+
self.normal,
|
|
81
|
+
self.radius,
|
|
82
|
+
*self.burgers,
|
|
83
|
+
self.poisson,
|
|
84
|
+
]
|
|
85
|
+
return [str(arg) for arg in args]
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
@dataclass
|
|
89
|
+
class DislocationBuilder(AtomskCommand):
|
|
90
|
+
atomic_structure: AtomskCommand
|
|
91
|
+
output_file: str | None = None
|
|
92
|
+
formats: Sequence[str] = ()
|
|
93
|
+
dislocations: list[DislocationSpec] = field(default_factory=list)
|
|
94
|
+
options: list[str] = field(default_factory=list)
|
|
95
|
+
|
|
96
|
+
def argv(self) -> list[str]:
|
|
97
|
+
command = list(self.atomic_structure.argv(include_export=self.output_file is None))
|
|
98
|
+
|
|
99
|
+
for dislocation in self.dislocations:
|
|
100
|
+
command.extend(dislocation.argv())
|
|
101
|
+
|
|
102
|
+
if self.options:
|
|
103
|
+
command.extend(self.options)
|
|
104
|
+
|
|
105
|
+
if self.output_file:
|
|
106
|
+
command.append(self.output_file)
|
|
107
|
+
command.extend(self.formats)
|
|
108
|
+
|
|
109
|
+
return command
|
|
110
|
+
|
|
111
|
+
def output_path(self) -> Path | None:
|
|
112
|
+
return Path(self.output_file) if self.output_file else None
|
|
113
|
+
|
|
114
|
+
def prepare_run(self) -> None:
|
|
115
|
+
self.atomic_structure.prepare_run()
|
|
116
|
+
if self.output_file:
|
|
117
|
+
_prepare_output_path(self.output_file)
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
import shlex
|
|
2
|
+
from dataclasses import dataclass
|
|
3
|
+
from enum import Enum
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
from typing import Sequence, Union
|
|
6
|
+
|
|
7
|
+
from pyatomsk.commands import AtomskCommand, _prepare_output_path
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class CubicLattices(Enum):
|
|
11
|
+
SC = 'sc'
|
|
12
|
+
BCC = 'bcc'
|
|
13
|
+
CsCl = 'CsCl'
|
|
14
|
+
FCC = 'fcc'
|
|
15
|
+
L12 = 'L12'
|
|
16
|
+
FLUORITE = 'fluorite'
|
|
17
|
+
DIAMOND = 'diamond'
|
|
18
|
+
ZINCBLENDE = 'zb'
|
|
19
|
+
ROCKSALT = 'rocksalt'
|
|
20
|
+
PEROVSKITE = 'per'
|
|
21
|
+
A15 = 'a15'
|
|
22
|
+
C15 = 'c15'
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class TetragonalLattices(Enum):
|
|
26
|
+
ST = 'st'
|
|
27
|
+
BCT = 'bct'
|
|
28
|
+
FCT = 'fct'
|
|
29
|
+
L10 = 'L1_0'
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
class HexagonalLattices(Enum):
|
|
33
|
+
HCP = 'hcp'
|
|
34
|
+
WURTZITE = 'wz'
|
|
35
|
+
GRAPHITE = 'graphite'
|
|
36
|
+
BN = 'BN'
|
|
37
|
+
B12 = 'B12'
|
|
38
|
+
C14 = 'C14'
|
|
39
|
+
C36 = 'C36'
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
Lattices = Union[CubicLattices, TetragonalLattices, HexagonalLattices]
|
|
43
|
+
MillerIndex = Union[str, Sequence[int]]
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def _miller(index: MillerIndex) -> str:
|
|
47
|
+
if isinstance(index, str):
|
|
48
|
+
return index
|
|
49
|
+
return '[' + ''.join(map(str, index)) + ']'
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
@dataclass
|
|
53
|
+
class AtomicStructure(AtomskCommand):
|
|
54
|
+
lattice: Lattices
|
|
55
|
+
lattice_params: Sequence[float]
|
|
56
|
+
species: Sequence[str]
|
|
57
|
+
orient: Sequence[MillerIndex] | None = None
|
|
58
|
+
duplicate: Sequence[int] | None = None
|
|
59
|
+
export_filename: str | None = None
|
|
60
|
+
formats: Sequence[str] = ()
|
|
61
|
+
|
|
62
|
+
def argv(self, *, include_export: bool = True) -> list[str]:
|
|
63
|
+
args = ['atomsk', '--create', self.lattice.value]
|
|
64
|
+
args.extend(map(str, self.lattice_params))
|
|
65
|
+
args.extend(self.species)
|
|
66
|
+
|
|
67
|
+
if self.orient is not None:
|
|
68
|
+
args.append('orient')
|
|
69
|
+
args.extend(_miller(index) for index in self.orient)
|
|
70
|
+
|
|
71
|
+
if self.duplicate is not None:
|
|
72
|
+
args.append('-duplicate')
|
|
73
|
+
args.extend(map(str, self.duplicate))
|
|
74
|
+
|
|
75
|
+
if include_export and self.export_filename:
|
|
76
|
+
args.append(self.export_filename)
|
|
77
|
+
args.extend(self.formats)
|
|
78
|
+
|
|
79
|
+
return args
|
|
80
|
+
|
|
81
|
+
def to_command(self, *, include_export: bool = True) -> str:
|
|
82
|
+
return shlex.join(self.argv(include_export=include_export))
|
|
83
|
+
|
|
84
|
+
def output_path(self) -> Path | None:
|
|
85
|
+
return Path(self.export_filename) if self.export_filename else None
|
|
86
|
+
|
|
87
|
+
def prepare_run(self) -> None:
|
|
88
|
+
if self.export_filename:
|
|
89
|
+
_prepare_output_path(self.export_filename)
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
@dataclass
|
|
93
|
+
class CustomAtomicStructure(AtomskCommand):
|
|
94
|
+
cell: Sequence[Sequence[float]]
|
|
95
|
+
basis: Sequence[tuple[str, Sequence[float]]]
|
|
96
|
+
duplicate: Sequence[int] | None = None
|
|
97
|
+
seed_filename: str = 'cementite_unitcell.xsf'
|
|
98
|
+
|
|
99
|
+
def _seed_text(self) -> str:
|
|
100
|
+
lines = [
|
|
101
|
+
'CRYSTAL',
|
|
102
|
+
'PRIMVEC',
|
|
103
|
+
*[f'{v[0]} {v[1]} {v[2]}' for v in self.cell],
|
|
104
|
+
'PRIMCOORD',
|
|
105
|
+
f'{len(self.basis)} 1',
|
|
106
|
+
]
|
|
107
|
+
for species, frac in self.basis:
|
|
108
|
+
x = frac[0] * self.cell[0][0] + frac[1] * self.cell[1][0] + frac[2] * self.cell[2][0]
|
|
109
|
+
y = frac[0] * self.cell[0][1] + frac[1] * self.cell[1][1] + frac[2] * self.cell[2][1]
|
|
110
|
+
z = frac[0] * self.cell[0][2] + frac[1] * self.cell[1][2] + frac[2] * self.cell[2][2]
|
|
111
|
+
lines.append(f'{species} {x} {y} {z}')
|
|
112
|
+
return '\n'.join(lines) + '\n'
|
|
113
|
+
|
|
114
|
+
def write_seed(self) -> Path:
|
|
115
|
+
path = Path(self.seed_filename)
|
|
116
|
+
path.write_text(self._seed_text())
|
|
117
|
+
return path
|
|
118
|
+
|
|
119
|
+
def argv(self, *, include_export: bool = True) -> list[str]:
|
|
120
|
+
args = ['atomsk', str(Path(self.seed_filename))]
|
|
121
|
+
if self.duplicate is not None:
|
|
122
|
+
args.append('-duplicate')
|
|
123
|
+
args.extend(map(str, self.duplicate))
|
|
124
|
+
return args
|
|
125
|
+
|
|
126
|
+
def to_command(self, *, include_export: bool = True) -> str:
|
|
127
|
+
return shlex.join(self.argv(include_export=include_export))
|
|
128
|
+
|
|
129
|
+
def prepare_run(self) -> None:
|
|
130
|
+
self.write_seed()
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import os
|
|
4
|
+
from collections.abc import Sequence
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
from typing import Any, Union
|
|
7
|
+
|
|
8
|
+
from voltsdk import PluginArtifact, SpatialAssembler, open_in_volt
|
|
9
|
+
|
|
10
|
+
Pathish = Union[str, os.PathLike]
|
|
11
|
+
Source = Union[Pathish, PluginArtifact]
|
|
12
|
+
|
|
13
|
+
_GLB_SUFFIXES = {'.glb', '.gltf'}
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def _is_glb(path: Path) -> bool:
|
|
17
|
+
return path.suffix.lower() in _GLB_SUFFIXES or path.name.lower().endswith('.glb.zst')
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def _as_glb(source: Source, *, output_path: Pathish | None = None, **kwargs: Any) -> Path | str:
|
|
21
|
+
# voltsdk artifacts already know how to convert themselves to GLB.
|
|
22
|
+
if isinstance(source, PluginArtifact):
|
|
23
|
+
return source.glb(output_path=output_path, **kwargs)
|
|
24
|
+
|
|
25
|
+
path = Path(os.fspath(source))
|
|
26
|
+
if _is_glb(path):
|
|
27
|
+
return path
|
|
28
|
+
|
|
29
|
+
target = Path(output_path).expanduser() if output_path else path.with_suffix('.glb')
|
|
30
|
+
return SpatialAssembler().glb(path, output_path=target, **kwargs)
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def view(
|
|
34
|
+
source: Source | Sequence[Source],
|
|
35
|
+
*,
|
|
36
|
+
output_path: Pathish | None = None,
|
|
37
|
+
title: str | None = None,
|
|
38
|
+
volt_url: str | None = None,
|
|
39
|
+
open_browser: bool = True,
|
|
40
|
+
**kwargs: Any,
|
|
41
|
+
) -> str:
|
|
42
|
+
"""Open an analysis artifact, a GLB, or a path (or a list of them) in the VOLT canvas.
|
|
43
|
+
|
|
44
|
+
Analysis files (``.json`` / ``.msgpack``) are converted to GLB via voltsdk's
|
|
45
|
+
``SpatialAssembler`` (the exporter is auto-detected from the payload); GLBs are
|
|
46
|
+
passed through untouched. Compute stays local — ``voltsdk.open_in_volt`` serves the
|
|
47
|
+
asset from a local http server. Returns the viewer URL.
|
|
48
|
+
|
|
49
|
+
``output_path`` only applies to a single source; a list uses each asset's default
|
|
50
|
+
``.glb`` path so multiple layers don't collide. Extra keyword arguments (e.g.
|
|
51
|
+
``exporter='DislocationExporter'``) are forwarded to the GLB conversion.
|
|
52
|
+
"""
|
|
53
|
+
if isinstance(source, (str, os.PathLike, PluginArtifact)):
|
|
54
|
+
glb = _as_glb(source, output_path=output_path, **kwargs)
|
|
55
|
+
return open_in_volt(glb, title=title, volt_url=volt_url, open_browser=open_browser)
|
|
56
|
+
|
|
57
|
+
frames = [_as_glb(item, **kwargs) for item in source]
|
|
58
|
+
return open_in_volt(frames, title=title, volt_url=volt_url, open_browser=open_browser)
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
LICENSE
|
|
2
|
+
README.md
|
|
3
|
+
setup.py
|
|
4
|
+
pyatomsk/__init__.py
|
|
5
|
+
pyatomsk/atomsk.py
|
|
6
|
+
pyatomsk/commands.py
|
|
7
|
+
pyatomsk/dislocations.py
|
|
8
|
+
pyatomsk/structures.py
|
|
9
|
+
pyatomsk/view.py
|
|
10
|
+
pyatomsk.egg-info/PKG-INFO
|
|
11
|
+
pyatomsk.egg-info/SOURCES.txt
|
|
12
|
+
pyatomsk.egg-info/dependency_links.txt
|
|
13
|
+
pyatomsk.egg-info/requires.txt
|
|
14
|
+
pyatomsk.egg-info/top_level.txt
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
voltsdk>=3.0.0
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
pyatomsk
|
pyatomsk-0.3.0/setup.cfg
ADDED