Skip to main content

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:

FieldTypeDescription
namestrController name you chose during setup
onlineboolWhether the controller is currently connected
max_coresintTotal cores provisioned
max_memory_mbintTotal memory provisioned (MB)
free_coresintCores not consumed by pending/running jobs
free_memory_mbintMemory not consumed by pending/running jobs (MB)
running_jobsintNumber 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:

FieldTypeDescription
controllerComputeTargetSnapshot of the target controller at query time
estimateslist[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 type
  • memory_mb — container memory per instance
  • max_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,
)
note

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_capacity to 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:

FactorEffect
Atom countLarger molecules → more cores and memory
Method familyGFN-FF (lightest) → GFN2-xTB → AIMNet → composite DFT → wB97M-V (heaviest)
Basis setdef2-SVP (small) → def2-TZVPPD (medium) → def2-QZVPD (large)
SolvationImplicit solvation promotes the job to the next resource tier
Job typeSPE ×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.