Estimating Controller Usage
Before submitting thousands of jobs to your BYOC controller, use the SDK to inspect free capacity and estimate how many jobs will fit. This page covers the two functions that replace guesswork with hard numbers.
Inspect Your Controllers
:func:atomiverse.list_compute_targets returns a list of every controller
registered to your account, including its current free capacity:
import atomiverse
controllers = atomiverse.list_compute_targets()
for c in controllers:
print(f"{c.name}:")
print(f" online: {c.online}")
print(f" free cores: {c.free_cores} / {c.max_cores}")
print(f" free memory: {c.free_memory_mb} MB / {c.max_memory_mb} MB")
print(f" running jobs: {c.running_jobs}")
Each :class:atomiverse.ComputeTarget carries:
| Field | Type | Description |
|---|---|---|
name | str | Controller name you chose during setup |
online | bool | Whether the controller is currently connected |
max_cores | int | Total cores provisioned |
max_memory_mb | int | Total memory provisioned (MB) |
free_cores | int | Cores not consumed by pending/running jobs |
free_memory_mb | int | Memory not consumed by pending/running jobs (MB) |
running_jobs | int | Number of currently running jobs |
Offline controllers are still listed. Their online field is False and
reported free capacity equals the configured maximum (no usage data is
available until the controller reconnects).
Estimate Capacity for a Set of Jobs
Once you know which controller you want to use, call
:func:atomiverse.estimate_capacity with your job definitions:
from atomiverse import SinglePointEnergy, Optimization
from atomiverse.levels import GFN2_XTB, B97_3C
# Define the jobs you plan to submit
spe = SinglePointEnergy(
atoms=small_molecule,
level_of_theory=GFN2_XTB,
)
opt = Optimization(
atoms=larger_molecule,
level_of_theory=B97_3C,
)
estimate = atomiverse.estimate_capacity(
jobs=[spe, opt],
compute_target="my-cluster",
)
print(f"Controller: {estimate.controller.name}")
print(f"Free: {estimate.controller.free_cores} cores, "
f"{estimate.controller.free_memory_mb / 1024:.1f} GB")
print()
for i, e in enumerate(estimate.estimates):
print(f"Job {i}:")
print(f" cores per job: {e['n_cores']}")
print(f" memory per job: {e['memory_mb']} MB")
print(f" max concurrent: {e['max_concurrent']}")
:class:atomiverse.CapacityEstimate contains:
| Field | Type | Description |
|---|---|---|
controller | ComputeTarget | Snapshot of the target controller at query time |
estimates | list[dict] | One entry per submitted job with n_cores, memory_mb, max_concurrent |
Each estimate entry tells you:
n_cores— cores Atomiverse allocates per instance of this job typememory_mb— container memory per instancemax_concurrent— maximum number of parallel instances that fit in the current free capacity
The estimate is a point-in-time snapshot. The actual concurrency changes as
jobs complete and new ones are submitted. Call estimate_capacity right before
a large batch submission to get the most accurate reading.
How max_concurrent is computed
Atomiverse estimates per-job resources from the job type, molecule size, method, and basis set. It then divides the controller's free cores and free memory by each job's requirements:
max_concurrent = min(
free_cores // cores_per_job,
free_memory // memory_per_job,
)
The max_concurrent value assumes you submit only one job type. If you
submit a mix of job types, each will consume its own share of the shared
capacity pool, and the combined consumption must stay within the free limits.
Putting It Together: Batch Submission
The introspection API lets you make informed decisions about submission strategy. Here is a typical batch-submission pattern:
import atomiverse
from atomiverse import SinglePointEnergy, CapacityWaitTimeoutError
from atomiverse.levels import GFN2_XTB
# 1. Check which controllers are online and their free capacity
controllers = atomiverse.list_compute_targets()
online = [c for c in controllers if c.online]
if not online:
print("No controllers are online — start one first.")
exit(1)
target = online[0]
print(f"Using {target.name} ({target.free_cores} free cores)")
# 2. Estimate how many of our job type can run in parallel
estimate = atomiverse.estimate_capacity(
jobs=[spe_job],
compute_target=target.name,
)
max_concurrent = estimate.estimates[0]["max_concurrent"]
print(f"Can run up to {max_concurrent} jobs concurrently")
# 3. Submit in waves that respect capacity
total_jobs = 500
batch_size = max(1, min(max_concurrent, 50))
for wave_start in range(0, total_jobs, batch_size):
wave = min(batch_size, total_jobs - wave_start)
print(f"Submitting wave of {wave} jobs...")
for _ in range(wave):
try:
spe_job.submit(
compute_target=target.name,
wait_for_capacity=True,
max_wait=3600, # wait up to 1h for a slot
wait=False, # return immediately after submission
)
except CapacityWaitTimeoutError as exc:
print(f"Timed out waiting for capacity: {exc.detail}")
break
print("Batch submission complete")
The pattern above:
- Checks which controllers are online before submitting
- Estimates per-job resource needs and max concurrency
- Submits in waves sized below the controller's capacity ceiling
- Uses
wait_for_capacityto let the SDK retry when the controller is momentarily full
Understanding Resource Estimation
When you call estimate_capacity, Atomiverse automatically estimates CPU cores
and memory for each job. The estimation considers:
| Factor | Effect |
|---|---|
| Atom count | Larger molecules → more cores and memory |
| Method family | GFN-FF (lightest) → GFN2-xTB → AIMNet → composite DFT → wB97M-V (heaviest) |
| Basis set | def2-SVP (small) → def2-TZVPPD (medium) → def2-QZVPD (large) |
| Solvation | Implicit solvation promotes the job to the next resource tier |
| Job type | SPE ×1 base walltime, optimization ×100, vibrations ×60, ensemble ×200 |
This is the same estimation logic the platform uses when it allocates resources for managed compute. You do not need to guess cores or memory — the SDK does it for you.
Estimated cost
BYOC compute is billed at €0.003 / core-hour. To get a rough cost estimate, multiply cores by the expected walltime:
estimated_cost = cores × (walltime_s / 3600) × €0.003
The actual walltime varies with molecule size and convergence behaviour. The cost is metered per-second by the billing loop, so you only pay for the wall-clock time the job actually consumed.
For managed compute cost estimates, refer to the Pricing page.
Example: Choosing the Right Submission Size
You have a benchmark set of 10,000 molecules and one controller with 8 cores / 16 GB. You want to run GFN2-xTB single-point energies.
import atomiverse
estimate = atomiverse.estimate_capacity(
jobs=[spe_water, spe_ethanol, spe_aspirin],
compute_target="workstation",
)
for i, e in enumerate(estimate.estimates):
print(f"Job {i}: {e['n_cores']} cores, "
f"{e['memory_mb']} MB, "
f"max {e['max_concurrent']} concurrent")
A typical output:
Job 0: 2 cores, 2048 MB, max 4 concurrent (water, 3 atoms)
Job 1: 2 cores, 2048 MB, max 4 concurrent (ethanol, 9 atoms)
Job 2: 4 cores, 4096 MB, max 2 concurrent (aspirin, 21 atoms)
You now know:
- Small molecules can run 4 at a time → submit in batches of 4
- Aspirin needs more resources → only 2 at a time
- You could mix: 2 small + 1 large concurrently
Without the estimate you would have to guess the batch size and risk wasted capacity or controller saturation.
Related
- Controller Setup — install and start the controller
- Bring Your Own Compute — BYOC overview and when it makes sense
- Pricing — managed and BYOC rates
- Python SDK — Job Management — monitor submitted jobs