Skip to main content

Single Point Energy

A single point energy calculation evaluates a molecule at its current geometry. By default it returns the energy and the nuclear gradients (forces), but you can turn forces off when you only need the scalar energy.

If you want to plug Atomiverse into an ASE-native workflow, see Cloud Calculator. That wrapper is convenient, but direct SinglePointEnergy(...) jobs remain the preferred interface for one-off submissions, explicit job management, and higher-throughput loops.

Your First Calculation

import atomiverse
from atomiverse import Atoms, JobFailedError, SinglePointEnergy
from atomiverse.levels import GFN2_XTB

atomiverse.configure(api_key="your-api-key")

# Build the molecule from SMILES via the Atomiverse backend
water = Atoms.from_smiles("O")

job = SinglePointEnergy(
atoms=water,
level_of_theory=GFN2_XTB,
)

job.submit()

try:
result = job.require_result()
except JobFailedError as exc:
print(f"Job failed: {exc.failure.error}")
else:
print(f"Energy: {result.energy} Hartree")
if result.forces is not None:
for i, (fx, fy, fz) in enumerate(result.forces):
print(f" atom {i}: ({fx:+.6f}, {fy:+.6f}, {fz:+.6f}) Hartree/Angstrom")

Result

job.require_result() returns a SinglePointEnergyResult:

FieldTypeUnitsShape
energyfloatHartree
forceslist[list[float]] | NoneHartree/Angstrom(n_atoms, 3) when requested

When forces are requested, forces[i] is the Cartesian force acting on the i-th atom of job.atoms, in the same order and the same frame of reference as the input geometry. The sign follows the physics convention: a force is -∂E/∂r, so at a local minimum every force points toward zero.

By default, submit() blocks until the job finishes. On success, the typed result is then available through job.require_result().

To cap how long the SDK waits, pass timeout in seconds:

job.submit(timeout=600)
Using your own geometry

You do not have to use Atoms.from_smiles. Passing a regular ase.Atoms object works too — for example, one loaded from a file with ase.io.read.

Energy-Only Calculations

Set compute_forces=False when gradients are not needed:

job = SinglePointEnergy(
atoms=water,
level_of_theory=GFN2_XTB,
compute_forces=False,
)

job.submit()
result = job.require_result()

print(result.energy) # Hartree
print(result.forces) # None

Energy-only jobs skip the force calculation and can be much cheaper for methods where gradients are expensive.

ASE calculator wrapper

CloudCalculator is available when you want Atomiverse behind ASE's calculator API, but it is not the most efficient route for repeated fresh evaluations because each new calculation submits a remote single-point job.

Choosing a Level of Theory

Swap in any predefined constant or build a custom one:

from atomiverse.levels import AIMNET2

job = SinglePointEnergy(
atoms=water,
level_of_theory=AIMNET2,
)

See Levels of Theory for a full list and instructions for building custom levels.

Charged Molecules and Open Shells

Pass charge and multiplicity when the system is not a neutral singlet:

from atomiverse import Atoms, SinglePointEnergy
from atomiverse.levels import GFN2_XTB

hydroxide = Atoms.from_smiles("[OH-]")

job = SinglePointEnergy(
atoms=hydroxide,
charge=-1,
multiplicity=1,
level_of_theory=GFN2_XTB,
)
  • charge — total charge of the system (default: 0)
  • multiplicity — spin multiplicity, 2S + 1 (default: 1, singlet)

Naming a Job

You can give a job a human-readable name that will appear in the dashboard instead of the auto-generated ID:

job = SinglePointEnergy(
atoms=water,
level_of_theory=GFN2_XTB,
name="water-baseline",
)

job.submit()

The name is optional — if omitted the dashboard shows a short version of the job ID. Names do not have to be unique.

Compute Resources

CPU cores and memory are determined automatically based on the molecule size and level of theory. You do not need to configure any resource settings — each job receives an appropriate allocation out of the box. This applies to both managed and BYOC compute targets.

Non-blocking Submission

If you want to submit and continue doing other things rather than waiting:

job.submit(wait=False)
print(job.job_id) # the server-assigned ID

# do other work …

job.refresh() # pull the latest state from the server
print(job.state) # PENDING, RUNNING, DONE, FAILED, or CANCELLED

Call refresh() whenever you need an up-to-date view of the job. It fetches the current state and, if the job has finished, makes the terminal success or failure payload available.

Cancelling a Job

Cancel a running or pending job with cancel():

job.submit(wait=False)

# … decide you no longer need it
job.cancel()

job.refresh()
print(job.state) # CANCELLED

Cancellation is best-effort — if the job has already finished by the time the request arrives, its state stays DONE or FAILED.

Renaming and Deleting

After submission, you can rename a job for easier search and delete terminal jobs to clean history:

job.rename("water-baseline-v2")
job.delete() # only for DONE / FAILED / CANCELLED jobs

See Job Management for validation rules and error handling.

Checking Job State

Every job exposes a state property that reflects its lifecycle:

StateMeaning
PENDINGQueued, waiting for a worker
RUNNINGCurrently executing
DONEFinished successfully
FAILEDFinished with an error
CANCELLEDCancelled by the user
from atomiverse.state import State

job.submit(wait=False)
job.refresh()

if job.state == State.DONE:
result = job.require_result()
print(result.energy)
if result.forces is not None:
print(result.forces[0])
elif job.state == State.FAILED:
print(job.require_failure().error)

Handling Failures

The recommended success-path API is job.require_result(). It keeps the happy path concise and raises JobFailedError with the typed failure payload attached on exc.failure when the job fails:

from atomiverse import JobFailedError

job.submit()

try:
result = job.require_result()
except JobFailedError as exc:
print(f"Job failed: {exc.failure.error}")
else:
print(f"Energy: {result.energy} Hartree")
if result.forces is not None:
max_force = max(sum(f * f for f in row) ** 0.5 for row in result.forces)
print(f"Max |force|: {max_force:.6f} Hartree/Angstrom")

job.failure and job.require_failure() are still available when you want to inspect the typed failure payload directly. job.error remains as a convenience string accessor.

Saving and Loading Jobs

Serialize a job to disk so you can resume or inspect it later:

# Save after submission
job.submit(wait=False)
job.dump("water_spe.json")

# Later, in a new script or session
from atomiverse import SinglePointEnergy

loaded = SinglePointEnergy.load("water_spe.json")
loaded.refresh()
print(loaded.state)

dump() writes the job definition and server-assigned ID to a JSON file. load() restores it. This is useful for long-running jobs where you want to close your script and check back later.

Waiting for BYOC Capacity

If you are submitting to your own BYOC controller and want the SDK to wait until that controller has room, pass wait_for_capacity=True:

job.submit(
compute_target="my-cluster",
wait_for_capacity=True,
)

This only changes BYOC submissions that are temporarily rejected because the controller is already full. Managed jobs behave exactly as before. Errors such as an offline controller or a job that is larger than the controller's limits still raise immediately.

If you also want to cap how long the SDK waits after the job has been accepted, combine it with timeout:

job.submit(
compute_target="my-cluster",
wait_for_capacity=True,
timeout=1800,
)