rootstock 0.5.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.
- rootstock/__init__.py +34 -0
- rootstock/calculator.py +194 -0
- rootstock/cli.py +426 -0
- rootstock/clusters.py +41 -0
- rootstock/environment.py +238 -0
- rootstock/pep723.py +172 -0
- rootstock/protocol.py +309 -0
- rootstock/server.py +287 -0
- rootstock/worker.py +273 -0
- rootstock-0.5.0.dist-info/METADATA +210 -0
- rootstock-0.5.0.dist-info/RECORD +14 -0
- rootstock-0.5.0.dist-info/WHEEL +4 -0
- rootstock-0.5.0.dist-info/entry_points.txt +2 -0
- rootstock-0.5.0.dist-info/licenses/LICENSE.md +7 -0
rootstock/worker.py
ADDED
|
@@ -0,0 +1,273 @@
|
|
|
1
|
+
#!/usr/bin/env python
|
|
2
|
+
"""
|
|
3
|
+
Rootstock worker process.
|
|
4
|
+
|
|
5
|
+
This runs in an isolated subprocess and:
|
|
6
|
+
1. Loads an MLIP (e.g., MACE)
|
|
7
|
+
2. Connects to the server via Unix socket
|
|
8
|
+
3. Receives positions, calculates forces, sends results back
|
|
9
|
+
4. Persists across multiple calculations (no startup overhead per calculation)
|
|
10
|
+
|
|
11
|
+
The worker is spawned via a generated wrapper script that calls run_worker().
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
import json
|
|
15
|
+
from collections.abc import Callable
|
|
16
|
+
from typing import TYPE_CHECKING
|
|
17
|
+
|
|
18
|
+
import numpy as np
|
|
19
|
+
|
|
20
|
+
from .protocol import (
|
|
21
|
+
IPIProtocol,
|
|
22
|
+
SocketClosed,
|
|
23
|
+
connect_unix_socket,
|
|
24
|
+
create_unix_socket_path,
|
|
25
|
+
)
|
|
26
|
+
|
|
27
|
+
if TYPE_CHECKING:
|
|
28
|
+
from ase.calculators.calculator import Calculator
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
class MLIPWorker:
|
|
32
|
+
"""
|
|
33
|
+
Worker that runs an MLIP calculator and communicates via i-PI protocol.
|
|
34
|
+
|
|
35
|
+
The worker acts as an i-PI client:
|
|
36
|
+
1. Connect to server
|
|
37
|
+
2. Report READY status
|
|
38
|
+
3. Receive positions via POSDATA
|
|
39
|
+
4. Calculate energy/forces
|
|
40
|
+
5. Report HAVEDATA status
|
|
41
|
+
6. Send results via FORCEREADY
|
|
42
|
+
7. Loop back to step 2
|
|
43
|
+
"""
|
|
44
|
+
|
|
45
|
+
def __init__(
|
|
46
|
+
self,
|
|
47
|
+
socket_name: str,
|
|
48
|
+
calculator: "Calculator",
|
|
49
|
+
log=None,
|
|
50
|
+
):
|
|
51
|
+
"""
|
|
52
|
+
Initialize the worker.
|
|
53
|
+
|
|
54
|
+
Args:
|
|
55
|
+
socket_name: Name of Unix socket to connect to
|
|
56
|
+
calculator: Pre-loaded ASE calculator
|
|
57
|
+
log: Optional file object for logging
|
|
58
|
+
"""
|
|
59
|
+
self.socket_name = socket_name
|
|
60
|
+
self.socket_path = create_unix_socket_path(socket_name)
|
|
61
|
+
self.log = log
|
|
62
|
+
|
|
63
|
+
self._calculator = calculator
|
|
64
|
+
self._socket = None
|
|
65
|
+
self._protocol = None
|
|
66
|
+
self._atoms = None # Cache ASE Atoms object
|
|
67
|
+
|
|
68
|
+
# Atomic species info from INIT message
|
|
69
|
+
self._atomic_numbers: list[int] | None = None
|
|
70
|
+
self._pbc: list[bool] | None = None
|
|
71
|
+
|
|
72
|
+
def _log(self, msg):
|
|
73
|
+
if self.log:
|
|
74
|
+
print(f"[Worker] {msg}", file=self.log, flush=True)
|
|
75
|
+
|
|
76
|
+
def _connect(self):
|
|
77
|
+
"""Connect to the server."""
|
|
78
|
+
self._log(f"Connecting to {self.socket_path}")
|
|
79
|
+
self._socket = connect_unix_socket(self.socket_path)
|
|
80
|
+
self._protocol = IPIProtocol(self._socket, log=self.log)
|
|
81
|
+
self._log("Connected")
|
|
82
|
+
|
|
83
|
+
def _create_atoms(self, positions: np.ndarray, cell: np.ndarray):
|
|
84
|
+
"""
|
|
85
|
+
Create or update ASE Atoms object.
|
|
86
|
+
|
|
87
|
+
On first call, creates a new Atoms object.
|
|
88
|
+
On subsequent calls, updates positions and cell in place.
|
|
89
|
+
"""
|
|
90
|
+
from ase import Atoms
|
|
91
|
+
|
|
92
|
+
if self._atoms is None or len(self._atoms) != len(positions):
|
|
93
|
+
# Need to create new Atoms object
|
|
94
|
+
if self._atomic_numbers is None:
|
|
95
|
+
raise RuntimeError(
|
|
96
|
+
"No atomic numbers received. Server must send INIT with species data."
|
|
97
|
+
)
|
|
98
|
+
|
|
99
|
+
self._atoms = Atoms(
|
|
100
|
+
numbers=self._atomic_numbers,
|
|
101
|
+
positions=positions,
|
|
102
|
+
cell=cell,
|
|
103
|
+
pbc=self._pbc if self._pbc is not None else [True, True, True],
|
|
104
|
+
)
|
|
105
|
+
self._atoms.calc = self._calculator
|
|
106
|
+
else:
|
|
107
|
+
# Update existing object (faster - reuses neighbor lists etc.)
|
|
108
|
+
self._atoms.positions = positions
|
|
109
|
+
self._atoms.cell = cell
|
|
110
|
+
|
|
111
|
+
return self._atoms
|
|
112
|
+
|
|
113
|
+
def _calculate(
|
|
114
|
+
self, positions: np.ndarray, cell: np.ndarray
|
|
115
|
+
) -> tuple[float, np.ndarray, np.ndarray]:
|
|
116
|
+
"""
|
|
117
|
+
Run MLIP calculation.
|
|
118
|
+
|
|
119
|
+
Returns:
|
|
120
|
+
energy: Potential energy in eV
|
|
121
|
+
forces: Nx3 forces in eV/Angstrom
|
|
122
|
+
virial: 3x3 virial tensor in eV
|
|
123
|
+
"""
|
|
124
|
+
atoms = self._create_atoms(positions, cell)
|
|
125
|
+
|
|
126
|
+
energy = atoms.get_potential_energy()
|
|
127
|
+
forces = atoms.get_forces()
|
|
128
|
+
|
|
129
|
+
# Calculate virial from stress
|
|
130
|
+
# stress is in eV/ų, virial = -stress * volume
|
|
131
|
+
try:
|
|
132
|
+
stress = atoms.get_stress(voigt=False) # 3x3 tensor
|
|
133
|
+
volume = atoms.get_volume()
|
|
134
|
+
virial = -stress * volume
|
|
135
|
+
except Exception:
|
|
136
|
+
# Some calculators don't support stress
|
|
137
|
+
virial = np.zeros((3, 3))
|
|
138
|
+
|
|
139
|
+
return energy, forces, virial
|
|
140
|
+
|
|
141
|
+
def run(self):
|
|
142
|
+
"""
|
|
143
|
+
Main loop - receive positions, calculate, send results.
|
|
144
|
+
|
|
145
|
+
This implements the i-PI client state machine:
|
|
146
|
+
- NEEDINIT -> receive INIT -> READY
|
|
147
|
+
- READY -> receive POSDATA -> calculate -> HAVEDATA
|
|
148
|
+
- HAVEDATA -> receive GETFORCE -> send FORCEREADY -> NEEDINIT
|
|
149
|
+
"""
|
|
150
|
+
self._connect()
|
|
151
|
+
|
|
152
|
+
state = "NEEDINIT"
|
|
153
|
+
energy = None
|
|
154
|
+
forces = None
|
|
155
|
+
virial = None
|
|
156
|
+
|
|
157
|
+
self._log("Entering main loop")
|
|
158
|
+
|
|
159
|
+
try:
|
|
160
|
+
while True:
|
|
161
|
+
# Wait for message from server
|
|
162
|
+
try:
|
|
163
|
+
msg = self._protocol.recvmsg()
|
|
164
|
+
except SocketClosed:
|
|
165
|
+
self._log("Server closed connection")
|
|
166
|
+
break
|
|
167
|
+
|
|
168
|
+
if msg == "EXIT":
|
|
169
|
+
self._log("Received EXIT, shutting down")
|
|
170
|
+
break
|
|
171
|
+
|
|
172
|
+
elif msg == "STATUS":
|
|
173
|
+
# Report current state
|
|
174
|
+
if state == "NEEDINIT":
|
|
175
|
+
self._protocol.sendmsg("NEEDINIT")
|
|
176
|
+
elif state == "READY":
|
|
177
|
+
self._protocol.sendmsg("READY")
|
|
178
|
+
elif state == "HAVEDATA":
|
|
179
|
+
self._protocol.sendmsg("HAVEDATA")
|
|
180
|
+
|
|
181
|
+
elif msg == "INIT":
|
|
182
|
+
# Receive initialization with atomic species info
|
|
183
|
+
bead_index, init_bytes = self._protocol.recv_init()
|
|
184
|
+
|
|
185
|
+
# Parse JSON from init_bytes
|
|
186
|
+
if init_bytes and init_bytes != b"\x00":
|
|
187
|
+
try:
|
|
188
|
+
init_data = json.loads(init_bytes.decode("utf-8"))
|
|
189
|
+
self._atomic_numbers = init_data.get("numbers")
|
|
190
|
+
self._pbc = init_data.get("pbc", [True, True, True])
|
|
191
|
+
self._log(
|
|
192
|
+
f"Received INIT (bead={bead_index}, "
|
|
193
|
+
f"atoms={len(self._atomic_numbers) if self._atomic_numbers else 0})"
|
|
194
|
+
)
|
|
195
|
+
except (json.JSONDecodeError, UnicodeDecodeError) as e:
|
|
196
|
+
self._log(f"Warning: Failed to parse INIT data: {e}")
|
|
197
|
+
else:
|
|
198
|
+
self._log(f"Received INIT (bead={bead_index}, no species data)")
|
|
199
|
+
|
|
200
|
+
state = "READY"
|
|
201
|
+
|
|
202
|
+
elif msg == "POSDATA":
|
|
203
|
+
# Receive atomic positions
|
|
204
|
+
if state not in ("READY", "NEEDINIT"):
|
|
205
|
+
self._log(f"Warning: POSDATA in state {state}")
|
|
206
|
+
|
|
207
|
+
cell, positions = self._protocol.recv_posdata()
|
|
208
|
+
self._log(f"Received POSDATA: {len(positions)} atoms")
|
|
209
|
+
|
|
210
|
+
# Calculate energy and forces
|
|
211
|
+
energy, forces, virial = self._calculate(positions, cell)
|
|
212
|
+
self._log(f"Calculated: E={energy:.6f} eV")
|
|
213
|
+
|
|
214
|
+
state = "HAVEDATA"
|
|
215
|
+
|
|
216
|
+
elif msg == "GETFORCE":
|
|
217
|
+
# Send results
|
|
218
|
+
if state != "HAVEDATA":
|
|
219
|
+
raise RuntimeError(f"GETFORCE in state {state}")
|
|
220
|
+
|
|
221
|
+
self._protocol.send_forceready(energy, forces, virial)
|
|
222
|
+
self._log("Sent FORCEREADY")
|
|
223
|
+
|
|
224
|
+
state = "NEEDINIT"
|
|
225
|
+
|
|
226
|
+
else:
|
|
227
|
+
self._log(f"Unknown message: {msg}")
|
|
228
|
+
|
|
229
|
+
finally:
|
|
230
|
+
if self._socket:
|
|
231
|
+
self._socket.close()
|
|
232
|
+
self._log("Worker shutdown complete")
|
|
233
|
+
|
|
234
|
+
|
|
235
|
+
def run_worker(
|
|
236
|
+
setup_fn: Callable[[str, str], "Calculator"],
|
|
237
|
+
model: str,
|
|
238
|
+
device: str,
|
|
239
|
+
socket_path: str,
|
|
240
|
+
log=None,
|
|
241
|
+
):
|
|
242
|
+
"""
|
|
243
|
+
Run worker with a provided setup function.
|
|
244
|
+
|
|
245
|
+
This is the entry point used by generated wrapper scripts.
|
|
246
|
+
The setup function is called once to create the calculator, which
|
|
247
|
+
is then reused for all subsequent calculations.
|
|
248
|
+
|
|
249
|
+
Args:
|
|
250
|
+
setup_fn: Function that takes (model, device) and returns an ASE calculator
|
|
251
|
+
model: Model identifier to pass to setup_fn
|
|
252
|
+
device: Device string to pass to setup_fn
|
|
253
|
+
socket_path: Full Unix socket path to connect to
|
|
254
|
+
log: Optional logging file object
|
|
255
|
+
"""
|
|
256
|
+
if log:
|
|
257
|
+
print(f"[Worker] Calling setup({model!r}, {device!r})", file=log, flush=True)
|
|
258
|
+
|
|
259
|
+
# Load calculator via the setup function
|
|
260
|
+
calculator = setup_fn(model, device)
|
|
261
|
+
|
|
262
|
+
if log:
|
|
263
|
+
print(f"[Worker] Calculator loaded: {type(calculator).__name__}", file=log, flush=True)
|
|
264
|
+
|
|
265
|
+
# Extract socket name from path (e.g., /tmp/ipi_rootstock_abc -> rootstock_abc)
|
|
266
|
+
socket_name = socket_path.replace("/tmp/ipi_", "")
|
|
267
|
+
|
|
268
|
+
worker = MLIPWorker(
|
|
269
|
+
socket_name=socket_name,
|
|
270
|
+
calculator=calculator,
|
|
271
|
+
log=log,
|
|
272
|
+
)
|
|
273
|
+
worker.run()
|
|
@@ -0,0 +1,210 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: rootstock
|
|
3
|
+
Version: 0.5.0
|
|
4
|
+
Summary: MLIP calculators with isolated Python environments
|
|
5
|
+
License-File: LICENSE.md
|
|
6
|
+
Requires-Python: >=3.10
|
|
7
|
+
Requires-Dist: ase>=3.22
|
|
8
|
+
Requires-Dist: numpy>=1.24
|
|
9
|
+
Requires-Dist: packaging>=21.0
|
|
10
|
+
Requires-Dist: tomli>=2.0; python_version < '3.11'
|
|
11
|
+
Provides-Extra: dev
|
|
12
|
+
Requires-Dist: pytest>=7.0; extra == 'dev'
|
|
13
|
+
Requires-Dist: ruff>=0.1; extra == 'dev'
|
|
14
|
+
Provides-Extra: mace
|
|
15
|
+
Requires-Dist: mace-torch>=0.3; extra == 'mace'
|
|
16
|
+
Requires-Dist: torch>=2.0; extra == 'mace'
|
|
17
|
+
Provides-Extra: modal
|
|
18
|
+
Requires-Dist: modal>=0.56; extra == 'modal'
|
|
19
|
+
Description-Content-Type: text/markdown
|
|
20
|
+
|
|
21
|
+
# Rootstock
|
|
22
|
+
|
|
23
|
+
Run MLIP (Machine Learning Interatomic Potential) calculators in isolated pre-built Python environments, communicating via the i-PI protocol over Unix sockets.
|
|
24
|
+
|
|
25
|
+
## Quick Start
|
|
26
|
+
|
|
27
|
+
```python
|
|
28
|
+
from ase.build import bulk
|
|
29
|
+
from rootstock import RootstockCalculator
|
|
30
|
+
|
|
31
|
+
atoms = bulk("Cu", "fcc", a=3.6) * (5, 5, 5)
|
|
32
|
+
|
|
33
|
+
# Using a known cluster
|
|
34
|
+
with RootstockCalculator(
|
|
35
|
+
cluster="modal", # or "della"
|
|
36
|
+
model="mace-medium", # or "chgnet", "mace-small", etc.
|
|
37
|
+
device="cuda",
|
|
38
|
+
) as calc:
|
|
39
|
+
atoms.calc = calc
|
|
40
|
+
print(atoms.get_potential_energy())
|
|
41
|
+
print(atoms.get_forces())
|
|
42
|
+
|
|
43
|
+
# Or with an explicit root path
|
|
44
|
+
with RootstockCalculator(
|
|
45
|
+
root="/scratch/gpfs/SHARED/rootstock",
|
|
46
|
+
model="mace-medium",
|
|
47
|
+
device="cuda",
|
|
48
|
+
) as calc:
|
|
49
|
+
atoms.calc = calc
|
|
50
|
+
print(atoms.get_potential_energy())
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
**Note:** Environments must be pre-built before use. See [Administrator Setup](#administrator-setup).
|
|
54
|
+
|
|
55
|
+
## Installation
|
|
56
|
+
|
|
57
|
+
```bash
|
|
58
|
+
pip install rootstock
|
|
59
|
+
# or
|
|
60
|
+
uv pip install rootstock
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
## Model String Format
|
|
64
|
+
|
|
65
|
+
The `model` parameter encodes both the environment and model-specific argument:
|
|
66
|
+
|
|
67
|
+
| `model=` | Environment | Model Arg |
|
|
68
|
+
|---------------------|----------------|---------------------|
|
|
69
|
+
| `"mace-medium"` | mace_env | `"medium"` |
|
|
70
|
+
| `"mace-small"` | mace_env | `"small"` |
|
|
71
|
+
| `"mace-large"` | mace_env | `"large"` |
|
|
72
|
+
| `"chgnet"` | chgnet_env | `""` (default) |
|
|
73
|
+
| `"mace-/path/to/weights.pt"` | mace_env | `"/path/to/weights.pt"` |
|
|
74
|
+
|
|
75
|
+
## Known Clusters
|
|
76
|
+
|
|
77
|
+
| Cluster | Root Path |
|
|
78
|
+
|---------|-----------|
|
|
79
|
+
| `modal` | `/vol/rootstock` |
|
|
80
|
+
| `della` | `/scratch/gpfs/SHARED/rootstock` |
|
|
81
|
+
|
|
82
|
+
For other clusters, use `root="/path/to/rootstock"` directly.
|
|
83
|
+
|
|
84
|
+
## Administrator Setup
|
|
85
|
+
|
|
86
|
+
Environments must be pre-built before users can run calculations.
|
|
87
|
+
|
|
88
|
+
### 1. Create Directory Structure
|
|
89
|
+
|
|
90
|
+
```bash
|
|
91
|
+
mkdir -p /scratch/gpfs/SHARED/rootstock/{environments,envs,cache}
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
### 2. Create Environment Source Files
|
|
95
|
+
|
|
96
|
+
```bash
|
|
97
|
+
# mace_env.py
|
|
98
|
+
cat > /scratch/gpfs/SHARED/rootstock/environments/mace_env.py << 'EOF'
|
|
99
|
+
# /// script
|
|
100
|
+
# requires-python = ">=3.10"
|
|
101
|
+
# dependencies = ["mace-torch>=0.3.0", "ase>=3.22", "torch>=2.0"]
|
|
102
|
+
# ///
|
|
103
|
+
"""MACE environment for Rootstock."""
|
|
104
|
+
|
|
105
|
+
def setup(model: str, device: str = "cuda"):
|
|
106
|
+
from mace.calculators import mace_mp
|
|
107
|
+
return mace_mp(model=model, device=device, default_dtype="float32")
|
|
108
|
+
EOF
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
### 3. Build Environments
|
|
112
|
+
|
|
113
|
+
```bash
|
|
114
|
+
# Build MACE environment with model pre-download
|
|
115
|
+
rootstock build mace_env --root /scratch/gpfs/SHARED/rootstock --models small,medium,large
|
|
116
|
+
|
|
117
|
+
# Build CHGNet environment
|
|
118
|
+
rootstock build chgnet_env --root /scratch/gpfs/SHARED/rootstock
|
|
119
|
+
|
|
120
|
+
# Verify
|
|
121
|
+
rootstock status --root /scratch/gpfs/SHARED/rootstock
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
## Architecture
|
|
125
|
+
|
|
126
|
+
```
|
|
127
|
+
Main Process Worker Process (subprocess)
|
|
128
|
+
+-------------------------+ +-----------------------------+
|
|
129
|
+
| RootstockCalculator | | Pre-built venv Python |
|
|
130
|
+
| (ASE-compatible) | | (mace_env/bin/python) |
|
|
131
|
+
| | | |
|
|
132
|
+
| server.py (i-PI server) |<-------->| worker.py (i-PI client) |
|
|
133
|
+
| - sends positions | Unix | - receives positions |
|
|
134
|
+
| - receives forces | socket | - calculates forces |
|
|
135
|
+
+-------------------------+ +-----------------------------+
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
The worker process uses a pre-built virtual environment, providing:
|
|
139
|
+
- **Fast startup**: No dependency installation at runtime
|
|
140
|
+
- **Filesystem compatibility**: Works on NFS, Lustre, GPFS, Modal volumes
|
|
141
|
+
- **Reproducibility**: Same environment every time
|
|
142
|
+
|
|
143
|
+
## Directory Structure
|
|
144
|
+
|
|
145
|
+
```
|
|
146
|
+
{root}/
|
|
147
|
+
├── environments/ # Environment SOURCE files (*.py with PEP 723)
|
|
148
|
+
│ ├── mace_env.py
|
|
149
|
+
│ └── chgnet_env.py
|
|
150
|
+
├── envs/ # Pre-built virtual environments
|
|
151
|
+
│ ├── mace_env/
|
|
152
|
+
│ │ ├── bin/python
|
|
153
|
+
│ │ ├── lib/python3.11/site-packages/
|
|
154
|
+
│ │ └── env_source.py # Copy of environment source
|
|
155
|
+
│ └── chgnet_env/
|
|
156
|
+
└── cache/ # XDG_CACHE_HOME for model weights
|
|
157
|
+
├── mace/ # MACE models
|
|
158
|
+
└── huggingface/ # HuggingFace models
|
|
159
|
+
```
|
|
160
|
+
|
|
161
|
+
## CLI Commands
|
|
162
|
+
|
|
163
|
+
```bash
|
|
164
|
+
# Build a pre-built environment
|
|
165
|
+
rootstock build <env_name> --root <path> [--models m1,m2] [--force]
|
|
166
|
+
|
|
167
|
+
# Show status
|
|
168
|
+
rootstock status --root <path>
|
|
169
|
+
|
|
170
|
+
# Register an environment source file
|
|
171
|
+
rootstock register <env_file> --root <path>
|
|
172
|
+
|
|
173
|
+
# List environments
|
|
174
|
+
rootstock list --root <path>
|
|
175
|
+
```
|
|
176
|
+
|
|
177
|
+
## Running on Modal
|
|
178
|
+
|
|
179
|
+
```bash
|
|
180
|
+
# Initialize volume and build environments (takes ~10-15 min)
|
|
181
|
+
modal run modal_app.py::init_rootstock_volume
|
|
182
|
+
|
|
183
|
+
# Test pre-built environments
|
|
184
|
+
modal run modal_app.py::test_prebuilt
|
|
185
|
+
|
|
186
|
+
# Show status
|
|
187
|
+
modal run modal_app.py::inspect_status
|
|
188
|
+
|
|
189
|
+
# Run benchmarks
|
|
190
|
+
modal run modal_app.py::benchmark_v4
|
|
191
|
+
```
|
|
192
|
+
|
|
193
|
+
## Performance
|
|
194
|
+
|
|
195
|
+
IPC overhead is <5% for systems with 1000+ atoms compared to direct in-process execution.
|
|
196
|
+
|
|
197
|
+
| System Size | Atoms | Typical Overhead |
|
|
198
|
+
|-------------|-------|------------------|
|
|
199
|
+
| Small | 64 | ~10-15% |
|
|
200
|
+
| Medium | 256 | ~5-8% |
|
|
201
|
+
| Large | 1000 | <5% |
|
|
202
|
+
|
|
203
|
+
## Local Development
|
|
204
|
+
|
|
205
|
+
```bash
|
|
206
|
+
uv venv && source .venv/bin/activate
|
|
207
|
+
uv pip install -e ".[dev]"
|
|
208
|
+
ruff check rootstock/
|
|
209
|
+
ruff format rootstock/
|
|
210
|
+
```
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
rootstock/__init__.py,sha256=Qu570ne-AeWn6IT4Us00iU43A0yoRlyYDygYvKHduVQ,956
|
|
2
|
+
rootstock/calculator.py,sha256=0KpW3coJ0ealEIZkxq9i4kZV5AxqCX1xZqDI1HkUPA4,6257
|
|
3
|
+
rootstock/cli.py,sha256=6YtUXs38Zc5BC0AjEaAygtmct6JAsfaGTX7Zn2K_imY,13815
|
|
4
|
+
rootstock/clusters.py,sha256=JQdPgBl_eyyP3nWxg_JOvD0Kh7YTVVXsNq038ZusVJg,1207
|
|
5
|
+
rootstock/environment.py,sha256=e7AAKd7cECwnX-KHCdfelHYVqm3xa1mIvc2MGaTJwBs,6583
|
|
6
|
+
rootstock/pep723.py,sha256=hgTcP7Tp8_z_DCrmX4VPAitijvujmbGmz2ng1VgNYks,4616
|
|
7
|
+
rootstock/protocol.py,sha256=CgfYNc0aKOkAQ-D8tvUrVO5q1mg0Xwcl2F02ZvYOJCI,10169
|
|
8
|
+
rootstock/server.py,sha256=H7kn1FHGGyrrPZtgRKdfagbAF-SxI89H3u85GSToYmY,9210
|
|
9
|
+
rootstock/worker.py,sha256=ty13OPDcKd_Zy95MAnTwa063Glz_RfUTBDja9Lxy4SM,8961
|
|
10
|
+
rootstock-0.5.0.dist-info/METADATA,sha256=AD3xqByFrIWK-j8LdWfAxJXFKvbsdgro7xX1x9PPp-Y,5993
|
|
11
|
+
rootstock-0.5.0.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
|
|
12
|
+
rootstock-0.5.0.dist-info/entry_points.txt,sha256=rPiVll-qj1wq7wZQTwSl2aF22GLnnpDz37hkPLvqlh0,49
|
|
13
|
+
rootstock-0.5.0.dist-info/licenses/LICENSE.md,sha256=ORJAYeKSWpOYZ89KWT8ETWFb2u6MvKK3AhrMReDMWrA,1072
|
|
14
|
+
rootstock-0.5.0.dist-info/RECORD,,
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
Copyright 2026 The University of Chicago
|
|
2
|
+
|
|
3
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the “Software”), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
|
|
4
|
+
|
|
5
|
+
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
|
|
6
|
+
|
|
7
|
+
THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|