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:
| Field | Type | Units | Shape |
|---|---|---|---|
energy | float | Hartree | — |
forces | list[list[float]] | None | Hartree/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)
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.
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:
| State | Meaning |
|---|---|
PENDING | Queued, waiting for a worker |
RUNNING | Currently executing |
DONE | Finished successfully |
FAILED | Finished with an error |
CANCELLED | Cancelled 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,
)