voltsdk 2.2.2__tar.gz → 2.2.4__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.
- {voltsdk-2.2.2 → voltsdk-2.2.4}/PKG-INFO +69 -8
- {voltsdk-2.2.2 → voltsdk-2.2.4}/pyproject.toml +1 -1
- voltsdk-2.2.4/tests/test_registry.py +67 -0
- voltsdk-2.2.4/tests/test_spatial.py +76 -0
- {voltsdk-2.2.2 → voltsdk-2.2.4}/voltsdk/README.md +68 -7
- {voltsdk-2.2.2 → voltsdk-2.2.4}/voltsdk/__init__.py +13 -1
- {voltsdk-2.2.2 → voltsdk-2.2.4}/voltsdk/plugins/errors.py +1 -1
- {voltsdk-2.2.2 → voltsdk-2.2.4}/voltsdk/plugins/plugin.py +97 -1
- {voltsdk-2.2.2 → voltsdk-2.2.4}/voltsdk/plugins/registry.py +2 -22
- {voltsdk-2.2.2 → voltsdk-2.2.4}/voltsdk/resources/analyses.py +17 -0
- {voltsdk-2.2.2 → voltsdk-2.2.4}/voltsdk/resources/exposures.py +22 -0
- {voltsdk-2.2.2 → voltsdk-2.2.4}/voltsdk/resources/frames.py +17 -0
- {voltsdk-2.2.2 → voltsdk-2.2.4}/voltsdk/resources/trajectories.py +18 -0
- voltsdk-2.2.4/voltsdk/spatial.py +1178 -0
- voltsdk-2.2.4/voltsdk/viewer.py +520 -0
- voltsdk-2.2.4/voltsdk/viewer_server.py +46 -0
- {voltsdk-2.2.2 → voltsdk-2.2.4}/voltsdk.egg-info/PKG-INFO +69 -8
- {voltsdk-2.2.2 → voltsdk-2.2.4}/voltsdk.egg-info/SOURCES.txt +5 -0
- {voltsdk-2.2.2 → voltsdk-2.2.4}/setup.cfg +0 -0
- {voltsdk-2.2.2 → voltsdk-2.2.4}/setup.py +0 -0
- {voltsdk-2.2.2 → voltsdk-2.2.4}/voltsdk/client.py +0 -0
- {voltsdk-2.2.2 → voltsdk-2.2.4}/voltsdk/exceptions.py +0 -0
- {voltsdk-2.2.2 → voltsdk-2.2.4}/voltsdk/http.py +0 -0
- {voltsdk-2.2.2 → voltsdk-2.2.4}/voltsdk/integrations/__init__.py +0 -0
- {voltsdk-2.2.2 → voltsdk-2.2.4}/voltsdk/integrations/glb.py +0 -0
- {voltsdk-2.2.2 → voltsdk-2.2.4}/voltsdk/integrations/ovito.py +0 -0
- {voltsdk-2.2.2 → voltsdk-2.2.4}/voltsdk/integrations/plotting.py +0 -0
- {voltsdk-2.2.2 → voltsdk-2.2.4}/voltsdk/io/__init__.py +0 -0
- {voltsdk-2.2.2 → voltsdk-2.2.4}/voltsdk/io/downloads.py +0 -0
- {voltsdk-2.2.2 → voltsdk-2.2.4}/voltsdk/io/msgpack.py +0 -0
- {voltsdk-2.2.2 → voltsdk-2.2.4}/voltsdk/native/__init__.py +0 -0
- {voltsdk-2.2.2 → voltsdk-2.2.4}/voltsdk/plugins/__init__.py +0 -0
- {voltsdk-2.2.2 → voltsdk-2.2.4}/voltsdk/plugins/hub.py +0 -0
- {voltsdk-2.2.2 → voltsdk-2.2.4}/voltsdk/resources/__init__.py +0 -0
- {voltsdk-2.2.2 → voltsdk-2.2.4}/voltsdk/resources/base.py +0 -0
- {voltsdk-2.2.2 → voltsdk-2.2.4}/voltsdk/resources/listings.py +0 -0
- {voltsdk-2.2.2 → voltsdk-2.2.4}/voltsdk/resources/simulation_cells.py +0 -0
- {voltsdk-2.2.2 → voltsdk-2.2.4}/voltsdk/resources/teams.py +0 -0
- {voltsdk-2.2.2 → voltsdk-2.2.4}/voltsdk.egg-info/dependency_links.txt +0 -0
- {voltsdk-2.2.2 → voltsdk-2.2.4}/voltsdk.egg-info/not-zip-safe +0 -0
- {voltsdk-2.2.2 → voltsdk-2.2.4}/voltsdk.egg-info/requires.txt +0 -0
- {voltsdk-2.2.2 → voltsdk-2.2.4}/voltsdk.egg-info/top_level.txt +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: voltsdk
|
|
3
|
-
Version: 2.2.
|
|
3
|
+
Version: 2.2.4
|
|
4
4
|
Summary: Python SDK for the Volt scientific computing platform
|
|
5
5
|
License-Expression: MIT
|
|
6
6
|
Requires-Python: >=3.10
|
|
@@ -65,6 +65,68 @@ client = VoltClient(
|
|
|
65
65
|
)
|
|
66
66
|
```
|
|
67
67
|
|
|
68
|
+
## Open In Volt
|
|
69
|
+
|
|
70
|
+
Use the Volt web canvas as the viewer for local GLBs, GLB sequences, or
|
|
71
|
+
existing Volt resources:
|
|
72
|
+
|
|
73
|
+
```python
|
|
74
|
+
from voltsdk import open_in_volt
|
|
75
|
+
|
|
76
|
+
open_in_volt('out/frame.glb')
|
|
77
|
+
open_in_volt({
|
|
78
|
+
0: 'out/frame-0000.glb',
|
|
79
|
+
5000: 'out/frame-5000.glb',
|
|
80
|
+
10000: 'out/frame-10000.glb',
|
|
81
|
+
})
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
Resource helpers open the exact canvas route:
|
|
85
|
+
|
|
86
|
+
```python
|
|
87
|
+
trajectory.open_in_volt(timestep=5000)
|
|
88
|
+
frame.open_in_volt()
|
|
89
|
+
analysis.open_in_volt(timestep=5000)
|
|
90
|
+
exposure.open_in_volt(5000)
|
|
91
|
+
run.open_in_volt('dislocations')
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
For plugin runs, `open_in_volt(...)` auto-assembles supported `.json` and
|
|
95
|
+
`.msgpack` exporter payloads into a sibling `.glb` when needed.
|
|
96
|
+
|
|
97
|
+
For local files, VoltSDK serves them directly to `/canvas/glb` without creating
|
|
98
|
+
fake analyses. In a Volt notebook it uses the Jupyter proxy; on a local machine
|
|
99
|
+
it starts a tiny background file server. The browser still needs an authenticated
|
|
100
|
+
Volt session. By default the viewer opens against `https://app.voltcloud.dev`;
|
|
101
|
+
override it with `volt_url=...` or `VOLT_APP_URL` if needed.
|
|
102
|
+
|
|
103
|
+
## SpatialAssembler
|
|
104
|
+
|
|
105
|
+
Volt exporter payloads can also be rendered locally from Python:
|
|
106
|
+
|
|
107
|
+
```python
|
|
108
|
+
from voltsdk import SpatialAssembler, open_in_volt
|
|
109
|
+
|
|
110
|
+
assembler = SpatialAssembler()
|
|
111
|
+
glb_path = assembler.dislocations_glb(
|
|
112
|
+
'output/ptm-dxa_dislocations.json',
|
|
113
|
+
output_path='output/ptm-dxa_dislocations.glb',
|
|
114
|
+
)
|
|
115
|
+
|
|
116
|
+
open_in_volt(glb_path)
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
The Python API accepts either the full artifact file (`.json` or `.msgpack`) or
|
|
120
|
+
the raw nested payload from `export.AtomisticExporter`, `export.MeshExporter`,
|
|
121
|
+
or `export.DislocationExporter`.
|
|
122
|
+
|
|
123
|
+
You can also let a `PluginRun` do the conversion directly:
|
|
124
|
+
|
|
125
|
+
```python
|
|
126
|
+
viewer_url = dxa_run.open_in_volt('dislocations', open_browser=False)
|
|
127
|
+
mesh_glb = dxa_run.glb('defect_mesh')
|
|
128
|
+
```
|
|
129
|
+
|
|
68
130
|
## Plugin hub
|
|
69
131
|
|
|
70
132
|
VoltSDK ships a Hugging Face-style plugin hub. Plugins live in a static
|
|
@@ -80,10 +142,10 @@ ptm = hub.get("voltlabs@polyhedral-template-matching")
|
|
|
80
142
|
result = ptm.run(
|
|
81
143
|
"frame.dump",
|
|
82
144
|
output_base="out/frame",
|
|
83
|
-
|
|
145
|
+
crystal_structure="FCC",
|
|
84
146
|
rmsd=0.1,
|
|
85
147
|
)
|
|
86
|
-
print(result.
|
|
148
|
+
print(result.path("annotated.dump"))
|
|
87
149
|
```
|
|
88
150
|
|
|
89
151
|
The same hub is exposed on an authenticated client via `client.plugins`.
|
|
@@ -129,13 +191,12 @@ The hub expects a static index plus per-platform bundles:
|
|
|
129
191
|
"publisher": "voltlabs",
|
|
130
192
|
"latest": "1.0.0",
|
|
131
193
|
"versions": {
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
194
|
+
"1.0.0": {
|
|
195
|
+
"linux-x86_64": {
|
|
196
|
+
"url": "opendxa/1.0.0/linux-x86_64.tar.zst"
|
|
197
|
+
}
|
|
136
198
|
}
|
|
137
199
|
}
|
|
138
|
-
}
|
|
139
200
|
}
|
|
140
201
|
}
|
|
141
202
|
}
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
import tarfile
|
|
5
|
+
import tempfile
|
|
6
|
+
import unittest
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
import sys
|
|
9
|
+
|
|
10
|
+
sys.path.insert(0, str(Path(__file__).resolve().parents[1]))
|
|
11
|
+
|
|
12
|
+
from voltsdk.plugins.registry import PluginRegistry
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def _write_bundle(path: Path) -> None:
|
|
16
|
+
with tempfile.TemporaryDirectory() as bundle_tmp:
|
|
17
|
+
root = Path(bundle_tmp)
|
|
18
|
+
bin_dir = root / 'bin'
|
|
19
|
+
bin_dir.mkdir(parents=True, exist_ok=True)
|
|
20
|
+
executable = bin_dir / 'demo-plugin'
|
|
21
|
+
executable.write_text('#!/bin/sh\nexit 0\n', encoding='utf-8')
|
|
22
|
+
with tarfile.open(path, 'w:gz') as archive:
|
|
23
|
+
archive.add(bin_dir, arcname='bin')
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class PluginRegistryTests(unittest.TestCase):
|
|
27
|
+
def test_install_ignores_sha256_metadata(self) -> None:
|
|
28
|
+
with tempfile.TemporaryDirectory() as tmp:
|
|
29
|
+
root = Path(tmp)
|
|
30
|
+
bundle_path = root / 'demo-plugin-1.0.0-linux-x86_64.tgz'
|
|
31
|
+
_write_bundle(bundle_path)
|
|
32
|
+
|
|
33
|
+
index = {
|
|
34
|
+
'plugins': {
|
|
35
|
+
'voltlabs': {
|
|
36
|
+
'demo-plugin': {
|
|
37
|
+
'publisher': 'voltlabs',
|
|
38
|
+
'latest': '1.0.0',
|
|
39
|
+
'versions': {
|
|
40
|
+
'1.0.0': {
|
|
41
|
+
'linux-x86_64': {
|
|
42
|
+
'url': bundle_path.name,
|
|
43
|
+
'sha256': 'definitely-wrong',
|
|
44
|
+
},
|
|
45
|
+
},
|
|
46
|
+
},
|
|
47
|
+
},
|
|
48
|
+
},
|
|
49
|
+
},
|
|
50
|
+
}
|
|
51
|
+
index_path = root / 'index.json'
|
|
52
|
+
index_path.write_text(json.dumps(index), encoding='utf-8')
|
|
53
|
+
|
|
54
|
+
registry = PluginRegistry(
|
|
55
|
+
url=index_path.as_uri(),
|
|
56
|
+
cache_dir=root / 'cache',
|
|
57
|
+
platform_tag='linux-x86_64',
|
|
58
|
+
)
|
|
59
|
+
|
|
60
|
+
installed = registry.install('voltlabs@demo-plugin')
|
|
61
|
+
|
|
62
|
+
self.assertTrue(installed.is_dir())
|
|
63
|
+
self.assertTrue((installed / 'bin' / 'demo-plugin').is_file())
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
if __name__ == '__main__':
|
|
67
|
+
unittest.main()
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
import tempfile
|
|
5
|
+
import unittest
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
import sys
|
|
8
|
+
|
|
9
|
+
sys.path.insert(0, str(Path(__file__).resolve().parents[1]))
|
|
10
|
+
|
|
11
|
+
from voltsdk.plugins.plugin import PluginRun
|
|
12
|
+
from voltsdk.spatial import SpatialAssembler
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def _sample_dislocation_payload() -> dict:
|
|
16
|
+
return {
|
|
17
|
+
'export': {
|
|
18
|
+
'DislocationExporter': {
|
|
19
|
+
'segments': [
|
|
20
|
+
{
|
|
21
|
+
'points': [
|
|
22
|
+
[0.0, 0.0, 0.0],
|
|
23
|
+
[1.0, 0.0, 0.0],
|
|
24
|
+
[1.0, 1.0, 0.0],
|
|
25
|
+
],
|
|
26
|
+
'burgers': {
|
|
27
|
+
'vector': [0.5, 0.5, 0.5],
|
|
28
|
+
},
|
|
29
|
+
},
|
|
30
|
+
],
|
|
31
|
+
},
|
|
32
|
+
},
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
class SpatialAssemblerTests(unittest.TestCase):
|
|
37
|
+
def test_glb_infers_dislocation_export_from_json(self) -> None:
|
|
38
|
+
with tempfile.TemporaryDirectory() as tmp:
|
|
39
|
+
root = Path(tmp)
|
|
40
|
+
payload_path = root / 'dislocations.json'
|
|
41
|
+
output_path = root / 'dislocations.glb'
|
|
42
|
+
payload_path.write_text(json.dumps(_sample_dislocation_payload()), encoding='utf-8')
|
|
43
|
+
|
|
44
|
+
result = SpatialAssembler().glb(payload_path, output_path=output_path)
|
|
45
|
+
|
|
46
|
+
self.assertEqual(result, output_path)
|
|
47
|
+
self.assertTrue(output_path.is_file())
|
|
48
|
+
self.assertEqual(output_path.read_bytes()[:4], b'glTF')
|
|
49
|
+
|
|
50
|
+
def test_plugin_run_glb_writes_sibling_glb(self) -> None:
|
|
51
|
+
with tempfile.TemporaryDirectory() as tmp:
|
|
52
|
+
root = Path(tmp)
|
|
53
|
+
payload_path = root / 'output_dislocations.json'
|
|
54
|
+
payload_path.write_text(json.dumps(_sample_dislocation_payload()), encoding='utf-8')
|
|
55
|
+
|
|
56
|
+
run = PluginRun(
|
|
57
|
+
command=[],
|
|
58
|
+
returncode=0,
|
|
59
|
+
stdout='',
|
|
60
|
+
stderr='',
|
|
61
|
+
output_base=root / 'output',
|
|
62
|
+
artifacts={
|
|
63
|
+
'dislocations': payload_path,
|
|
64
|
+
'dislocations.json': payload_path,
|
|
65
|
+
},
|
|
66
|
+
)
|
|
67
|
+
|
|
68
|
+
result = run.glb('dislocations')
|
|
69
|
+
|
|
70
|
+
self.assertEqual(result, root / 'output_dislocations.glb')
|
|
71
|
+
self.assertTrue(result.is_file())
|
|
72
|
+
self.assertEqual(result.read_bytes()[:4], b'glTF')
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
if __name__ == '__main__':
|
|
76
|
+
unittest.main()
|
|
@@ -41,6 +41,68 @@ client = VoltClient(
|
|
|
41
41
|
)
|
|
42
42
|
```
|
|
43
43
|
|
|
44
|
+
## Open In Volt
|
|
45
|
+
|
|
46
|
+
Use the Volt web canvas as the viewer for local GLBs, GLB sequences, or
|
|
47
|
+
existing Volt resources:
|
|
48
|
+
|
|
49
|
+
```python
|
|
50
|
+
from voltsdk import open_in_volt
|
|
51
|
+
|
|
52
|
+
open_in_volt('out/frame.glb')
|
|
53
|
+
open_in_volt({
|
|
54
|
+
0: 'out/frame-0000.glb',
|
|
55
|
+
5000: 'out/frame-5000.glb',
|
|
56
|
+
10000: 'out/frame-10000.glb',
|
|
57
|
+
})
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
Resource helpers open the exact canvas route:
|
|
61
|
+
|
|
62
|
+
```python
|
|
63
|
+
trajectory.open_in_volt(timestep=5000)
|
|
64
|
+
frame.open_in_volt()
|
|
65
|
+
analysis.open_in_volt(timestep=5000)
|
|
66
|
+
exposure.open_in_volt(5000)
|
|
67
|
+
run.open_in_volt('dislocations')
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
For plugin runs, `open_in_volt(...)` auto-assembles supported `.json` and
|
|
71
|
+
`.msgpack` exporter payloads into a sibling `.glb` when needed.
|
|
72
|
+
|
|
73
|
+
For local files, VoltSDK serves them directly to `/canvas/glb` without creating
|
|
74
|
+
fake analyses. In a Volt notebook it uses the Jupyter proxy; on a local machine
|
|
75
|
+
it starts a tiny background file server. The browser still needs an authenticated
|
|
76
|
+
Volt session. By default the viewer opens against `https://app.voltcloud.dev`;
|
|
77
|
+
override it with `volt_url=...` or `VOLT_APP_URL` if needed.
|
|
78
|
+
|
|
79
|
+
## SpatialAssembler
|
|
80
|
+
|
|
81
|
+
Volt exporter payloads can also be rendered locally from Python:
|
|
82
|
+
|
|
83
|
+
```python
|
|
84
|
+
from voltsdk import SpatialAssembler, open_in_volt
|
|
85
|
+
|
|
86
|
+
assembler = SpatialAssembler()
|
|
87
|
+
glb_path = assembler.dislocations_glb(
|
|
88
|
+
'output/ptm-dxa_dislocations.json',
|
|
89
|
+
output_path='output/ptm-dxa_dislocations.glb',
|
|
90
|
+
)
|
|
91
|
+
|
|
92
|
+
open_in_volt(glb_path)
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
The Python API accepts either the full artifact file (`.json` or `.msgpack`) or
|
|
96
|
+
the raw nested payload from `export.AtomisticExporter`, `export.MeshExporter`,
|
|
97
|
+
or `export.DislocationExporter`.
|
|
98
|
+
|
|
99
|
+
You can also let a `PluginRun` do the conversion directly:
|
|
100
|
+
|
|
101
|
+
```python
|
|
102
|
+
viewer_url = dxa_run.open_in_volt('dislocations', open_browser=False)
|
|
103
|
+
mesh_glb = dxa_run.glb('defect_mesh')
|
|
104
|
+
```
|
|
105
|
+
|
|
44
106
|
## Plugin hub
|
|
45
107
|
|
|
46
108
|
VoltSDK ships a Hugging Face-style plugin hub. Plugins live in a static
|
|
@@ -56,10 +118,10 @@ ptm = hub.get("voltlabs@polyhedral-template-matching")
|
|
|
56
118
|
result = ptm.run(
|
|
57
119
|
"frame.dump",
|
|
58
120
|
output_base="out/frame",
|
|
59
|
-
|
|
121
|
+
crystal_structure="FCC",
|
|
60
122
|
rmsd=0.1,
|
|
61
123
|
)
|
|
62
|
-
print(result.
|
|
124
|
+
print(result.path("annotated.dump"))
|
|
63
125
|
```
|
|
64
126
|
|
|
65
127
|
The same hub is exposed on an authenticated client via `client.plugins`.
|
|
@@ -105,13 +167,12 @@ The hub expects a static index plus per-platform bundles:
|
|
|
105
167
|
"publisher": "voltlabs",
|
|
106
168
|
"latest": "1.0.0",
|
|
107
169
|
"versions": {
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
170
|
+
"1.0.0": {
|
|
171
|
+
"linux-x86_64": {
|
|
172
|
+
"url": "opendxa/1.0.0/linux-x86_64.tar.zst"
|
|
173
|
+
}
|
|
112
174
|
}
|
|
113
175
|
}
|
|
114
|
-
}
|
|
115
176
|
}
|
|
116
177
|
}
|
|
117
178
|
}
|
|
@@ -35,7 +35,7 @@ from .plugins import (
|
|
|
35
35
|
)
|
|
36
36
|
from .native import root as native_root
|
|
37
37
|
|
|
38
|
-
__version__ = "2.2.
|
|
38
|
+
__version__ = "2.2.4"
|
|
39
39
|
|
|
40
40
|
__all__ = [
|
|
41
41
|
"VoltClient",
|
|
@@ -47,6 +47,8 @@ __all__ = [
|
|
|
47
47
|
"VoltPermissionError",
|
|
48
48
|
"VoltTimeoutError",
|
|
49
49
|
"msgpack_as_df",
|
|
50
|
+
"open_in_volt",
|
|
51
|
+
"SpatialAssembler",
|
|
50
52
|
"view_glb",
|
|
51
53
|
"Plugin",
|
|
52
54
|
"PluginError",
|
|
@@ -69,6 +71,16 @@ def __getattr__(name: str):
|
|
|
69
71
|
|
|
70
72
|
globals()[name] = msgpack_as_df
|
|
71
73
|
return msgpack_as_df
|
|
74
|
+
if name == "open_in_volt":
|
|
75
|
+
from .viewer import open_in_volt
|
|
76
|
+
|
|
77
|
+
globals()[name] = open_in_volt
|
|
78
|
+
return open_in_volt
|
|
79
|
+
if name == "SpatialAssembler":
|
|
80
|
+
from .spatial import SpatialAssembler
|
|
81
|
+
|
|
82
|
+
globals()[name] = SpatialAssembler
|
|
83
|
+
return SpatialAssembler
|
|
72
84
|
if name == "view_glb":
|
|
73
85
|
from .integrations.glb import view_glb
|
|
74
86
|
|
|
@@ -34,6 +34,57 @@ class PluginRun:
|
|
|
34
34
|
return artifact
|
|
35
35
|
return None
|
|
36
36
|
|
|
37
|
+
def path(self, name: str) -> Path:
|
|
38
|
+
artifact = self.artifact(name)
|
|
39
|
+
if artifact is not None:
|
|
40
|
+
return artifact
|
|
41
|
+
available = ', '.join(sorted(self.artifacts)) or '<none>'
|
|
42
|
+
raise PluginError(f'Artifact {name!r} not found. Available artifacts: {available}.')
|
|
43
|
+
|
|
44
|
+
def df(self, name: str, key: str | None = None):
|
|
45
|
+
path = self.path(name)
|
|
46
|
+
suffix = path.suffix.lower()
|
|
47
|
+
if suffix == '.msgpack':
|
|
48
|
+
from ..io.msgpack import msgpack_as_df
|
|
49
|
+
|
|
50
|
+
return msgpack_as_df(str(path), key)
|
|
51
|
+
if suffix == '.json':
|
|
52
|
+
return _json_df(path, key)
|
|
53
|
+
raise PluginError(f'Artifact {path.name!r} is not a supported dataframe source.')
|
|
54
|
+
|
|
55
|
+
def glb(
|
|
56
|
+
self,
|
|
57
|
+
name: str,
|
|
58
|
+
*,
|
|
59
|
+
output_path: str | os.PathLike[str] | None = None,
|
|
60
|
+
exporter: str | None = None,
|
|
61
|
+
**options: Any,
|
|
62
|
+
) -> Path:
|
|
63
|
+
from ..spatial import SpatialAssembler
|
|
64
|
+
|
|
65
|
+
path = self.path(name)
|
|
66
|
+
if _is_glb_artifact(path):
|
|
67
|
+
return path
|
|
68
|
+
|
|
69
|
+
target = Path(output_path).expanduser().resolve() if output_path else _default_glb_output_path(path)
|
|
70
|
+
result = SpatialAssembler().glb(path, output_path=target, exporter=exporter, **options)
|
|
71
|
+
if not isinstance(result, Path):
|
|
72
|
+
raise PluginError('SpatialAssembler did not return a file path.')
|
|
73
|
+
return result
|
|
74
|
+
|
|
75
|
+
def open_in_volt(
|
|
76
|
+
self,
|
|
77
|
+
name: str,
|
|
78
|
+
*,
|
|
79
|
+
volt_url: str | None = None,
|
|
80
|
+
open_browser: bool = True,
|
|
81
|
+
) -> str:
|
|
82
|
+
from ..viewer import open_in_volt
|
|
83
|
+
|
|
84
|
+
path = self.path(name)
|
|
85
|
+
source = path if _is_glb_artifact(path) else self.glb(name)
|
|
86
|
+
return open_in_volt(source, volt_url=volt_url, open_browser=open_browser)
|
|
87
|
+
|
|
37
88
|
|
|
38
89
|
class Plugin:
|
|
39
90
|
"""A single plugin instance backed by a downloaded bundle.
|
|
@@ -75,7 +126,7 @@ class Plugin:
|
|
|
75
126
|
) -> PluginRun:
|
|
76
127
|
config = _prepare_config({**self.options, **options})
|
|
77
128
|
input_path = Path(input_file).expanduser().resolve()
|
|
78
|
-
output = Path(output_base).expanduser() if output_base else input_path.with_suffix("")
|
|
129
|
+
output = Path(output_base).expanduser().resolve() if output_base else input_path.with_suffix("")
|
|
79
130
|
output.parent.mkdir(parents=True, exist_ok=True)
|
|
80
131
|
|
|
81
132
|
command, completed = self._run_subprocess(
|
|
@@ -271,6 +322,51 @@ def _canonical_artifact_name(prefix: str, filename: str) -> str:
|
|
|
271
322
|
return filename
|
|
272
323
|
|
|
273
324
|
|
|
325
|
+
def _is_glb_artifact(path: Path) -> bool:
|
|
326
|
+
suffix = path.suffix.lower()
|
|
327
|
+
return suffix in {'.glb', '.gltf'} or path.name.lower().endswith('.glb.zst')
|
|
328
|
+
|
|
329
|
+
|
|
330
|
+
def _default_glb_output_path(path: Path) -> Path:
|
|
331
|
+
suffix = path.suffix.lower()
|
|
332
|
+
if suffix:
|
|
333
|
+
return path.with_suffix('.glb')
|
|
334
|
+
return path.with_name(f'{path.name}.glb')
|
|
335
|
+
|
|
336
|
+
|
|
337
|
+
def _json_df(path: Path, key: str | None):
|
|
338
|
+
import pandas as pd
|
|
339
|
+
|
|
340
|
+
from ..io.msgpack import get_nested_value
|
|
341
|
+
|
|
342
|
+
with path.open('r', encoding='utf-8') as fh:
|
|
343
|
+
data = get_nested_value(json.load(fh), key)
|
|
344
|
+
return _data_as_df(data, pd)
|
|
345
|
+
|
|
346
|
+
|
|
347
|
+
def _data_as_df(data: Any, pd):
|
|
348
|
+
if data is None:
|
|
349
|
+
return pd.DataFrame()
|
|
350
|
+
if isinstance(data, list):
|
|
351
|
+
return pd.DataFrame(data)
|
|
352
|
+
if _is_columnar_dict(data):
|
|
353
|
+
return pd.DataFrame(data)
|
|
354
|
+
if isinstance(data, dict):
|
|
355
|
+
return pd.DataFrame([data])
|
|
356
|
+
return pd.DataFrame([{'value': data}])
|
|
357
|
+
|
|
358
|
+
|
|
359
|
+
def _is_columnar_dict(value: Any) -> bool:
|
|
360
|
+
if not isinstance(value, dict) or not value:
|
|
361
|
+
return False
|
|
362
|
+
lengths: list[int] = []
|
|
363
|
+
for item in value.values():
|
|
364
|
+
if not isinstance(item, list):
|
|
365
|
+
return False
|
|
366
|
+
lengths.append(len(item))
|
|
367
|
+
return len(set(lengths)) == 1
|
|
368
|
+
|
|
369
|
+
|
|
274
370
|
def _env(root: Path) -> dict[str, str]:
|
|
275
371
|
env = os.environ.copy()
|
|
276
372
|
plugin_bin_dir = root / "bin"
|
|
@@ -11,8 +11,7 @@ Index format (``GET <registry>/index.json``)::
|
|
|
11
11
|
"versions": {
|
|
12
12
|
"1.0.0": {
|
|
13
13
|
"linux-x86_64": {
|
|
14
|
-
"url": "opendxa/1.0.0/linux-x86_64.tar.zst"
|
|
15
|
-
"sha256": "..."
|
|
14
|
+
"url": "opendxa/1.0.0/linux-x86_64.tar.zst"
|
|
16
15
|
}
|
|
17
16
|
}
|
|
18
17
|
}
|
|
@@ -32,7 +31,6 @@ Layout inside a bundle (after extraction)::
|
|
|
32
31
|
|
|
33
32
|
from __future__ import annotations
|
|
34
33
|
|
|
35
|
-
import hashlib
|
|
36
34
|
import os
|
|
37
35
|
import platform
|
|
38
36
|
import shutil
|
|
@@ -43,7 +41,7 @@ from dataclasses import dataclass
|
|
|
43
41
|
from pathlib import Path
|
|
44
42
|
from typing import Any
|
|
45
43
|
|
|
46
|
-
from .errors import PluginNotFoundError
|
|
44
|
+
from .errors import PluginNotFoundError
|
|
47
45
|
|
|
48
46
|
DEFAULT_REGISTRY_URL = "https://server.voltcloud.dev/plugin-registry"
|
|
49
47
|
INDEX_PATH = "index.json"
|
|
@@ -56,7 +54,6 @@ class BundleRef:
|
|
|
56
54
|
version: str
|
|
57
55
|
platform: str
|
|
58
56
|
url: str
|
|
59
|
-
sha256: str
|
|
60
57
|
|
|
61
58
|
|
|
62
59
|
class PluginRegistry:
|
|
@@ -121,7 +118,6 @@ class PluginRegistry:
|
|
|
121
118
|
version=version,
|
|
122
119
|
platform=self.platform_tag,
|
|
123
120
|
url=str(platform_entry["url"]),
|
|
124
|
-
sha256=str(platform_entry.get("sha256", "")),
|
|
125
121
|
)
|
|
126
122
|
|
|
127
123
|
# ------------------------------------------------------------------
|
|
@@ -135,14 +131,6 @@ class PluginRegistry:
|
|
|
135
131
|
return target
|
|
136
132
|
|
|
137
133
|
archive = self._fetch(ref.url, f"{ref.key}-{ref.version}-{ref.platform}")
|
|
138
|
-
if ref.sha256:
|
|
139
|
-
actual = _sha256(archive)
|
|
140
|
-
if actual != ref.sha256:
|
|
141
|
-
archive.unlink(missing_ok=True)
|
|
142
|
-
raise PluginVerificationError(
|
|
143
|
-
f"sha256 mismatch for {ref.key}@{ref.version}: expected {ref.sha256}, got {actual}."
|
|
144
|
-
)
|
|
145
|
-
|
|
146
134
|
if target.exists():
|
|
147
135
|
shutil.rmtree(target)
|
|
148
136
|
target.mkdir(parents=True, exist_ok=True)
|
|
@@ -239,14 +227,6 @@ def _is_absolute(url: str) -> bool:
|
|
|
239
227
|
return urllib.parse.urlparse(url).scheme in {"http", "https", "file"}
|
|
240
228
|
|
|
241
229
|
|
|
242
|
-
def _sha256(path: Path) -> str:
|
|
243
|
-
hasher = hashlib.sha256()
|
|
244
|
-
with path.open("rb") as fh:
|
|
245
|
-
for chunk in iter(lambda: fh.read(1 << 20), b""):
|
|
246
|
-
hasher.update(chunk)
|
|
247
|
-
return hasher.hexdigest()
|
|
248
|
-
|
|
249
|
-
|
|
250
230
|
def _extract(archive: Path, target: Path) -> None:
|
|
251
231
|
if archive.suffixes[-2:] == [".tar", ".zst"]:
|
|
252
232
|
import zstandard
|
|
@@ -101,6 +101,23 @@ class Analysis(BaseResource):
|
|
|
101
101
|
return self._client.unzip_recursive(zip_path)
|
|
102
102
|
return zip_path
|
|
103
103
|
|
|
104
|
+
def open_in_volt(
|
|
105
|
+
self,
|
|
106
|
+
*,
|
|
107
|
+
timestep: int | None = None,
|
|
108
|
+
volt_url: str | None = None,
|
|
109
|
+
open_browser: bool = True,
|
|
110
|
+
) -> str:
|
|
111
|
+
from voltsdk.viewer import open_canvas_view
|
|
112
|
+
|
|
113
|
+
return open_canvas_view(
|
|
114
|
+
trajectory_id=self.trajectory_id,
|
|
115
|
+
analysis_id=self.id,
|
|
116
|
+
timestep=timestep,
|
|
117
|
+
volt_url=volt_url,
|
|
118
|
+
open_browser=open_browser,
|
|
119
|
+
)
|
|
120
|
+
|
|
104
121
|
|
|
105
122
|
class AnalysisCollection(BaseCollection['Analysis']):
|
|
106
123
|
"""Paginated collection of analyses.
|
|
@@ -101,6 +101,28 @@ class Exposure(BaseResource):
|
|
|
101
101
|
dest=dest,
|
|
102
102
|
)
|
|
103
103
|
|
|
104
|
+
def open_in_volt(
|
|
105
|
+
self,
|
|
106
|
+
timestep: int,
|
|
107
|
+
*,
|
|
108
|
+
volt_url: str | None = None,
|
|
109
|
+
open_browser: bool = True,
|
|
110
|
+
) -> str:
|
|
111
|
+
from voltsdk.viewer import open_canvas_view
|
|
112
|
+
|
|
113
|
+
trajectory_id = self._get('trajectory', '')
|
|
114
|
+
if not trajectory_id:
|
|
115
|
+
raise ValueError('Exposure does not include a trajectory ID.')
|
|
116
|
+
|
|
117
|
+
return open_canvas_view(
|
|
118
|
+
trajectory_id=trajectory_id,
|
|
119
|
+
analysis_id=self.analysis_id,
|
|
120
|
+
exposure_id=self.id,
|
|
121
|
+
timestep=timestep,
|
|
122
|
+
volt_url=volt_url,
|
|
123
|
+
open_browser=open_browser,
|
|
124
|
+
)
|
|
125
|
+
|
|
104
126
|
|
|
105
127
|
class ExposureCollection(BaseCollection['Exposure']):
|
|
106
128
|
"""Paginated collection of exposures for an analysis."""
|
|
@@ -105,6 +105,23 @@ class Frame:
|
|
|
105
105
|
dest=dest,
|
|
106
106
|
)
|
|
107
107
|
|
|
108
|
+
def open_in_volt(
|
|
109
|
+
self,
|
|
110
|
+
analysis_id: str = 'default',
|
|
111
|
+
*,
|
|
112
|
+
volt_url: str | None = None,
|
|
113
|
+
open_browser: bool = True,
|
|
114
|
+
) -> str:
|
|
115
|
+
from voltsdk.viewer import open_canvas_view
|
|
116
|
+
|
|
117
|
+
return open_canvas_view(
|
|
118
|
+
trajectory_id=self._trajectory_id,
|
|
119
|
+
analysis_id=analysis_id,
|
|
120
|
+
timestep=self.timestep,
|
|
121
|
+
volt_url=volt_url,
|
|
122
|
+
open_browser=open_browser,
|
|
123
|
+
)
|
|
124
|
+
|
|
108
125
|
# ------------------------------------------------------------------
|
|
109
126
|
# OVITO integration
|
|
110
127
|
# ------------------------------------------------------------------
|
|
@@ -95,6 +95,24 @@ class Trajectory(BaseResource):
|
|
|
95
95
|
dest=dest,
|
|
96
96
|
)
|
|
97
97
|
|
|
98
|
+
def open_in_volt(
|
|
99
|
+
self,
|
|
100
|
+
*,
|
|
101
|
+
analysis_id: str | None = None,
|
|
102
|
+
timestep: int | None = None,
|
|
103
|
+
volt_url: str | None = None,
|
|
104
|
+
open_browser: bool = True,
|
|
105
|
+
) -> str:
|
|
106
|
+
from voltsdk.viewer import open_canvas_view
|
|
107
|
+
|
|
108
|
+
return open_canvas_view(
|
|
109
|
+
trajectory_id=self.id,
|
|
110
|
+
analysis_id=analysis_id,
|
|
111
|
+
timestep=timestep,
|
|
112
|
+
volt_url=volt_url,
|
|
113
|
+
open_browser=open_browser,
|
|
114
|
+
)
|
|
115
|
+
|
|
98
116
|
# ------------------------------------------------------------------
|
|
99
117
|
# Plotting
|
|
100
118
|
# ------------------------------------------------------------------
|