Metadata-Version: 2.4
Name: reactionstudio
Version: 0.1.0
Summary: Python client API for ReactionStudio
Author-email: Open Numerics <info@opennumerics.com>, Hannes Vandecasteele <hannesv@opennumerics.com>
License-Expression: Apache-2.0
Project-URL: Homepage, https://reactionstudio.ai
Project-URL: Source, https://github.com/OpenNumerics/reactionstudio-python
Keywords: chemistry,computational-chemistry,conformers,transition-state,neb,xtb,API
Classifier: Development Status :: 3 - Alpha
Classifier: Intended Audience :: Science/Research
Classifier: Programming Language :: Python :: 3
Classifier: Topic :: Scientific/Engineering :: Chemistry
Requires-Python: >=3.12
Description-Content-Type: text/markdown
License-File: LICENSE
Requires-Dist: httpx>=0.27
Provides-Extra: ase
Requires-Dist: ase>=3.22; extra == "ase"
Provides-Extra: rdkit
Requires-Dist: rdkit>=2023.3; extra == "rdkit"
Provides-Extra: all
Requires-Dist: reactionstudio[ase,rdkit]; extra == "all"
Provides-Extra: test
Requires-Dist: pytest>=8; extra == "test"
Requires-Dist: respx>=0.21; extra == "test"
Dynamic: license-file

# reactionstudio-python

Python client for [ReactionStudio](https://reactionstudio.io) — run conformer
searches, geometry optimisations and transition-path calculations from code.

```bash
pip install reactionstudio
```

## Getting a key

Sign in to ReactionStudio, go to the compute platform, open **Account → API keys**, and generate one. The
key is shown once. Put it in your environment:

```bash
export REACTIONSTUDIO_API_KEY=rs_live_...
```

A key can submit jobs and read their status and results. It cannot buy tokens,
create further keys, or read your billing history — those need a browser
session, so a leaked key can't escalate.

## Quick start

```python
import reactionstudio as rs

client = rs.Client()

conformers = client.generate_conformers( "CCO", force_field="gfn2-xtb" ).wait()

print(f"{len(conformers)} conformers")
print(f"lowest energy: {conformers.lowest_energy.energy:.2f} kJ/mol")
```

`wait()` blocks until the job finishes, polling with backoff, and raises if the
computation fails.

## The three experiments

### Conformer generation

Takes a SMILES string, no chemistry packages required.

```python
conformers = client.generate_conformers(
    "CC(=O)Oc1ccccc1C(=O)O",
    force_field="gfn2-xtb",
    n_initial_conformers=1000,   # embedded before pruning
).wait()

for c in conformers:
    print( c.energy, c.cluster_size )

best = conformers.molecule(0)    # reusable as input elsewhere
```

### Geometry optimisation

```python
mol = rs.Molecule.from_file( "ethanol.xyz" ) # Will infer bonds from atom distances.

result = client.stabilize_conformation( mol, force_field="gfn2-xtb" ).wait()

print(result.converged, result.final_energy)
print(result.energy_change)      # how far downhill it went, kJ/mol
```

### Transition paths

```python
path = client.transition_path(
    rs.Molecule.from_file("reactant.xyz"),
    rs.Molecule.from_file("product.xyz"), # should contain  atoms in the same order
    force_field="gfn2-xtb",
    n_images=20,
).wait()

print(f"barrier: {path.barrier:.1f} kJ/mol")
print(f"reaction energy: {path.reaction_energy:.1f} kJ/mol")

open("path.xyz", "w").write(path.to_xyz())      # multi-frame trajectory
ts = path.transition_state                      # a Molecule
```

Both structures must have their atoms in the same order — interpolation is
per-atom, so the client checks and refuses a mismatch rather than letting you
pay for a meaningless path.

## Molecules

```python
rs.Molecule.from_file("thing.xyz")     # .xyz, .mol, .sdf natively; .pdb via ase
rs.Molecule.from_xyz(text)
rs.Molecule.from_molfile(text)         # keeps the bond block
rs.Molecule.from_smiles("CCO")         # needs [rdkit]; does not use the API
rs.Molecule.from_ase(atoms)            # needs [ase]
rs.Molecule.from_rdkit(mol)            # needs [rdkit]
rs.Molecule.new([6, 8], [[0, 0, 0], [1.2, 0, 0]])
```

`new()` accepts element symbols instead of atomic numbers, and numpy arrays
anywhere coordinates are expected. Supplying bonds lets the backend skip its
own bond inference, which matters for `.xyz` input where connectivity is
otherwise guessed from interatomic distances.

## Chemistry interop

The core client has one dependency, `httpx`. Conversions are optional extras:

```bash
pip install 'reactionstudio[ase]'      # ASE
pip install 'reactionstudio[rdkit]'    # RDKit
pip install 'reactionstudio[all]'
```

```python
atoms = result.to_ase()                # Atoms
images = path.to_ase()                 # list[Atoms], write with ase.io.write
frames = conformers.to_ase()           # list[Atoms], one per conformer
```

## Long jobs

`wait()` is the common case. For anything long, drive the loop yourself:

```python
job = client.transition_path(reactant, product, force_field="gfn2-xtb")
print("submitted", job.run_id)

try:
    path = job.wait(timeout=3600, on_progress=lambda s: print(s.status, s.progress))
except rs.RunTimeoutError:
    pass    # the run continues server-side
```

Or hand it off to a thread and carry on:

```python
future = job.wait_async()        # concurrent.futures.Future
...
result = future.result()         # blocks only when you actually need it
```

Submitting a batch? Wait on them together — the backend runs them
concurrently, so waiting one at a time just wastes wall-clock:

```python
jobs = [client.generate_conformers(s, force_field="gfn2-xtb") for s in smiles]
results = rs.wait_all(jobs)                          # submission order
results = rs.wait_all(jobs, raise_on_failure=False)  # exceptions in place of results
```

A `run_id` is all you need to pick a job back up in another process. `wait()`
returns immediately for a run that has already finished:

```python
job = client.job("6f2a...")
result = job.wait()
```

## Downloads

Jobs produce files alongside their structured results:

```python
job.download("out/")                          # server picks the filename
job.download("conformers.zip")                # or specify it
job.download("in.mol", artifact_type="input_molecule")
```

`artifact_type` defaults to the experiment's main output — the optimised
`.mol`, the conformer `.zip`, or the transition-path `.xyz`. Inputs are kept
too, under `input_molecule`, `reactant_input` and `product_input`.

## Tokens

Each run costs tokens, and submitting without enough raises before any compute
starts:

```python
print(client.token_balance())

try:
    job = client.transition_path(r, p, force_field="gfn2-xtb")
except rs.InsufficientTokensError as exc:
    print(f"needs {exc.required_tokens}, have {exc.current_balance}")
```

## Force fields

```python
for ff in client.force_fields(available_only=True):
    print(ff.id, ff.supports_transition_path)
```

`force_field` is required on every submission and has no default — picking one
silently would be a good way to run a batch at the wrong level of theory. Not
every method supports transition paths.

## Errors

All of these subclass `rs.ReactionStudioError`.

| Exception | Meaning |
| --- | --- |
| `AuthenticationError` | key missing, revoked, or expired |
| `InsufficientTokensError` | not enough tokens; carries the numbers |
| `NotFoundError` | no such run, or it isn't yours |
| `RunFailedError` | the computation failed; carries the backend's message |
| `RunTimeoutError` | `wait()` gave up; the run continues |
| `APIError` | unexpected HTTP response |
| `MissingDependencyError` | an extra is needed, e.g. `[rdkit]` |

Reads are retried on transient failures. **Submissions are never retried** —
the API has no idempotency key, so a retry could create a second run and spend
a second lot of tokens.

## Conventions

Energies are kJ/mol, coordinates Angström. Argument names match the HTTP API,
so anything in the API docs maps onto a keyword here, and numeric defaults
match the web app's, so a script and the UI give the same answer for the same
input.

## Development

```bash
pip install -e '.[test,all]'
pytest
```

## Licence

Apache 2.0
