Metadata-Version: 2.4
Name: amplabs-playground
Version: 1.5.0
Summary: AmpLabs Playground and BatteryLab SDK — simulate, parameterize, and validate battery cells
Author: AmpLabs.ai Inc
Project-URL: Homepage, https://hub.amplabs.ai
Project-URL: Documentation, https://hub.amplabs.ai/docs/api
Project-URL: API reference, https://api.hub.amplabs.ai/docs
Project-URL: Battery Data Format (BDF), https://battery-data-alliance.github.io/battery-data-format/
Keywords: battery,simulation,parameterization,spme,ecm,amplabs,batterylab,bdf,battery-data-format
Classifier: Development Status :: 4 - Beta
Classifier: License :: Other/Proprietary License
Classifier: Intended Audience :: Science/Research
Classifier: Intended Audience :: Developers
Classifier: Operating System :: POSIX :: Linux
Classifier: Operating System :: MacOS
Classifier: Operating System :: Microsoft :: Windows
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3 :: Only
Classifier: Programming Language :: Python :: 3.10
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Classifier: Programming Language :: Python :: 3.14
Classifier: Programming Language :: Python :: Implementation :: CPython
Classifier: Topic :: Scientific/Engineering
Classifier: Typing :: Typed
Requires-Python: >=3.10
Description-Content-Type: text/markdown
License-File: LICENSE
Requires-Dist: httpx<1,>=0.28.1
Provides-Extra: cli
Requires-Dist: typer<1,>=0.27.3; extra == "cli"
Requires-Dist: rich<16,>=15.0.0; extra == "cli"
Provides-Extra: bpx
Requires-Dist: bpx<2,>=1.1.1; extra == "bpx"
Provides-Extra: plot
Requires-Dist: matplotlib<4,>=3.8; extra == "plot"
Provides-Extra: dev
Requires-Dist: pytest; extra == "dev"
Requires-Dist: pytest-asyncio; extra == "dev"
Requires-Dist: respx; extra == "dev"
Requires-Dist: ruff; extra == "dev"
Dynamic: license-file

# AmpLabs Playground and BatteryLab SDK

One package gives both the free AmpLabs Playground and the full BatteryLab SDK for AmpLabs account users.

Simulate, parameterize and validate battery cells from Python or the command
line. The SDK calls the AmpLabs BatteryLab API. You do not need the web UI.

You can run simulations with no account (see "Try it with no account"). Fitting,
data upload and custom cells need an AmpLabs account (create one at <https://hub.amplabs.ai/sign-up>).

## Install

```bash
pip install amplabs-playground          # Python SDK
pip install "amplabs-playground[cli]"   # SDK and the `amplabs` command
pip install "amplabs-playground[plot]"  # SDK and matplotlib, for result.plot()
```

The import name is `amplabs` (`from amplabs import AmpLabs`). `amplabs --version`
shows the installed version.

Do not install the old `amplabs` 0.2.x package next to it; both use the import name `amplabs`.

### Supported platforms

The package is published as compiled wheels only:

| Operating system | CPU | Python |
|---|---|---|
| Linux (glibc 2.17 or later, or musl 1.2 or later) | x86_64, aarch64 | CPython 3.10 to 3.14 |
| macOS 11 or later | x86_64 (Intel), arm64 (Apple silicon) | CPython 3.10 to 3.14 |
| Windows | x86-64 | CPython 3.10 to 3.14 |

On a different platform or Python version (for example 32-bit Windows, PyPy,
or Python 3.9), pip finds no wheel, and `pip install amplabs-playground` stops with:

```
ERROR: Could not find a version that satisfies the requirement amplabs-playground (from versions: none)
ERROR: No matching distribution found for amplabs-playground
```

## Try it with no account

```python
from amplabs import AmpLabs

bl = AmpLabs()  # no token: anonymous trial
r = bl.simulate(cell="LG_M50T", current=-5, duration=3600)
print(r.voltage[-1], bl.remaining_today)
```

With no token, `simulate()` runs in the AmpLabs playground:

- The first run on a machine does a one-time setup of about 20 s: a proof of
  work on one CPU core. The SDK keeps the pass it gets in
  `~/.amplabs/anon_pass.json` and renews it before it expires. A new network
  address needs a new setup.
- Built-in cells only: `Samsung_IN21700_30T`, `LG_M50T` and `NMC_Graphite`. At
  most 120 h of simulated time per run. Each series has at most 4,000 points.
- With no `thermal_mode`, a run uses `"convective"` (0.3 W/K), the page's
  default, with or without a token.
- Each machine has 5 runs a minute and 30 runs a day. A network address has
  about 100 runs a day. The day starts at 00:00 UTC. When few runs are left, the
  SDK says so on stderr. At the limit, `simulate()` raises `AnonymousLimitError`
  with `reset_at`.
- Fitting, data upload, custom cells and every other call need an account. They
  raise `AccountRequiredError`. Run `amplabs login` with an AmpLabs account (create one at https://hub.amplabs.ai/sign-up).

The SDK sends `sha256(salt + machine ID)`, never the raw machine ID or a MAC
address. The machine ID is `IOPlatformUUID` on macOS, `/etc/machine-id` on Linux
and `MachineGuid` on Windows. In a container, or when there is no machine ID, the
SDK makes a random ID once and keeps it in `~/.amplabs/anon` (mode 600).

From the command line, with no account:

```bash
pip install "amplabs-playground[cli]"
amplabs simulate --cell LG_M50T --rate 1C --duration 1h
```

### The playground layer

Anonymous `simulate()` is a thin wrapper over `amplabs.playground`, a small layer
that any program can use (the MCP server uses it too). It lists the playground's
modules and runs any live action with a client pass:

```python
from amplabs import PlaygroundClient

with PlaygroundClient() as playground:            # base_url= for another API
    print([m["id"] for m in playground.modules()])
    answer = playground.call("standard-simulation", "run", {"cell": "LG_M50T", "current": -5})
    print(answer.answer["results"]["voltage"][-1], answer.remaining_today, answer.reset_at)
```

`call()` returns a `PlaygroundAnswer`: the server's allow-listed `answer`, and
`remaining_today`, `remaining_minute`, `limit_day` and `reset_at` from the
server. `read(module, action)` reads a stored action's answer (no run is
counted). `PlaygroundClient(notices=False)` prints nothing to stderr.

### Open a simulation dashboard

```bash
python -m amplabs playground --json
```

This command needs only `pip install amplabs-playground`. No login, plotting package or local solver is required.
It runs a fully charged Samsung 30T discharging at 1C and 25 °C with convective cooling (0.3 W/K), stopping at 2.5 V, on the production API.
The JSON receipt names the actual HTML dashboard and Simulation Data (BDF) files (`.bdf.csv`). Open or preview that HTML file; in an agent, return a clickable file link and the BDF download. The receipt’s `bdf` field lists the files; `csv` remains a compatibility alias. Measured UDDS data is a separate attributed BDF file on its original timestamps.
Do not claim the webpage was opened or publicly hosted unless that actually happened.
The file works offline, with interactive voltage/current/temperature charts, settings, metrics, simulated time and measured time to result.
UDDS includes the independent measured trace and its BDF download. No result is uploaded or published.

```bash
python -m amplabs playground --scenario charge --rate 1 --temperature 25 --json
python -m amplabs playground --scenario udds --temperature 25 --json
python -m amplabs playground --cell LG_M50T --rate 0.75 --thermal adiabatic --temperature 44 --json
```

Use `--base-url` only when the copied task explicitly names another AmpLabs API.
The selected API must offer the requested live mode; otherwise the command stops without creating a success page.
Use `--open` to request a local browser, or `--output my-result.html` for an explicit filename (existing files are never overwritten).
Default filenames are unique for each run. The command respects the browser's supported cells, charge limits and measured UDDS temperatures.
No private fitted parameters, identity/pass files or raw response metadata are written into the dashboard.

Python usage:

```python
from amplabs.demo import run
receipt = run(scenario="discharge", rate=1, temperature=25)
print(receipt["dashboard"])
```

### Battery Data Format (BDF)

BDF is the standard data export throughout AmpLabs.
[Battery Data Format](https://battery-data-alliance.github.io/battery-data-format/) gives battery data named channels and explicit units for exchange between tools.
The SDK writes `.bdf.csv` files with test time in seconds, voltage in volts, current in amperes, and temperature when available.
Each file identifies the cell and data source; simulated and measured data are separate downloads.
The CSV encoding keeps BDF files readable in spreadsheets and Python; use `receipt["bdf"]` to find them.

### Reproduce a browser playground task

For a task copied from the marketing playground, use `PlaygroundClient.call()`.
It accepts the same public inputs as the browser, including CCCV and live drive cycles.
`AmpLabs.simulate()` is the simpler constant-current convenience method. It runs CC and CC+Time only, not every browser mode.
Use the API address in the copied task, or the default production API when none is stated.
Read the live action list before running.
If a requested live action is unavailable, report that fact; do not substitute a stored comparison or fabricate a result.

Use the same dashboard command from Python. It preserves the selected settings and exports BDF automatically:

```python
# playground-task-recipe
from amplabs.demo import run

receipt = run(
    cell="Samsung_IN21700_30T", scenario="discharge", rate=1,
    temperature=25, thermal="convective", htc=0.3,
    output="simulation.html",
)
print(receipt["dashboard"])
for filename in receipt["bdf"]:
    print(filename)
```

- **CC discharge:** `scenario="discharge"`, the selected C-rate and temperature, starting fully charged and stopping at 2.5 V.
- **CCCV charge:** `scenario="charge"`, starting empty, charging at the selected rate and holding 4.2 V to the 100% SOC target.
- **Live UDDS:** `scenario="udds"`, Samsung 30T, and one of the measured temperatures: −20, −10, 0, 10, 25 or 40 °C. This protocol fixes current and thermal settings; the browser disables those controls.
- **Cooling for CC/CCCV:** `thermal="convective", htc=0.3` is the browser default. `htc` is heat transfer to air in W/K. Use the selected value, or `thermal="isothermal"`/`"adiabatic"`; inactive HTC is omitted.
- **Cell names:** Samsung 30T means `Samsung_IN21700_30T` (3 Ah); LG M50T means `LG_M50T` (5 Ah). Use the browser's supported modes and limits.

The chart and BDF downloads must come from the live response.
The battery time axis is simulated time; the measured wait includes client setup, network and any retries.
The first anonymous setup can take longer than a warmed run.
This recipe never downloads fitted cell parameters or a local solver.

## See your results

Every `simulate()` result, with a token or without one, is saved to
`~/.amplabs/results/<UTC timestamp>-<id>.json` (mode 600), and the SDK prints
the path to stderr. The newest 200 files are kept. Pass `save=False` to save
nothing, or set `AMPLABS_RESULTS_DIR` to use another folder.

```python
r = bl.simulate(cell="LG_M50T", current=-5, duration=3600)
print(r.summary())   # cell, current, C-rate, delivered Ah, end voltage, duration, max temperature,
                     # and capacity_ah and energy_wh, as on the page's Results card
r.energy, r.power    # every series the run returns is a field; r.series has them all by name
print(r.terminal_plot())   # voltage against time as text, for a terminal with no display
r.plot()             # matplotlib: pip install "amplabs-playground[plot]"
r.saved_path         # the saved JSON file
```

`amplabs simulate` prints a summary table and a terminal plot of voltage
against time. `amplabs simulate --json` prints only the raw result as JSON on
stdout, for scripts and agents; every other line goes to stderr.
`--no-save` saves nothing.

### Saved result files

A later local viewer (`amplabs ui`) reads these files, so the format is fixed
and versioned. One file is one JSON object:

| Key | Value |
|---|---|
| `schema` | `"amplabs.simulation-result/1"`. A new version changes this value. |
| `id` | 8 hex characters, also the end of the file name. |
| `created_at` | UTC time, ISO 8601 (`2026-10-16T18:02:11+00:00`). |
| `kind` | `"simulate"`. |
| `mode` | `"anonymous"` (no token) or `"account"`. |
| `api` | The API address the run used. |
| `sdk_version` | The SDK version that wrote the file. |
| `inputs` | The `simulate()` arguments: `cell`, `current` (A, negative is discharge), `duration` (s, or null for a run to the voltage cutoff), `init_soc` (%), `temperature` (°C), `v_min`, `v_max` (V), `thermal_mode`, and `htc` (W/K) when it was given. |
| `summary` | `cell`, `current_a`, `c_rate` (null for a cell with no known nominal capacity), `temperature_c`, `delivered_ah`, `end_voltage_v`, `duration_s`, `max_temperature_c`, `capacity_ah`, `energy_wh` (the last value of the run's capacity and energy, as on the page's Results card), `points`. A value is null when the run did not give it. |
| `series` | `time` (s), `voltage` (V), `current` (A), `soc` (%), `temperature` (°C): lists of the same length, or empty lists. |
| `raw` | The API's whole answer, as `SimulationResult.raw`. |

`amplabs.results.load(path)` reads a file.

## Sign in

Each call except an anonymous `simulate()` needs a personal access token (PAT).

```bash
amplabs login                    # opens a browser sign-in, then saves a PAT
```

`amplabs login` prints a URL and a code. Approve the sign-in in any browser.
The CLI then makes a PAT and saves it in `~/.amplabs/token` with mode 600.

For a machine with no browser (CI, a server, an agent), use one of these:

```bash
export BATTERYLAB_TOKEN=pat_...  # a PAT from https://hub.amplabs.ai/settings/tokens
amplabs login --machine          # machine client: set AMPLABS_CLIENT_SECRET (and AMPLABS_CLIENT_ID)
```

`AmpLabs()` finds the token in this order:

1. the `token=` argument;
2. the `BATTERYLAB_TOKEN` environment variable;
3. the file `~/.amplabs/token` that `amplabs login` writes.

With no token in any of these, `AmpLabs()` is in anonymous mode
(`bl.anonymous` is `True`; see "Try it with no account").

The SDK and the CLI refuse to read `~/.amplabs/token` when other users can read
it. Fix it with `chmod 600 ~/.amplabs/token`, or run `amplabs login` again.

## Example: quote, then fit

```python
from amplabs import AmpLabs

client = AmpLabs()                                   # token= → BATTERYLAB_TOKEN → ~/.amplabs/token
run = client.new_run(model="spme")
run.load(files={"ocv": "c20.csv", "hppc": "hppc.csv", "validation": "udds.csv"})
run.configure(anode_model="graphite_Chen2020", nominal_capacity_ah=5.0)
price = run.quote()["tokens"]                        # nothing is charged
print(f"Fit costs {price} tokens; balance {client.whoami().get('balance')}")
run.fit()
print(run.validate())                                # error against the drive cycle
run.register(name="my cell v1")                      # save as a reusable profile
```

`run.status()` tells you which steps are done and which step is next.

Simulate a cell profile:

```python
result = client.simulate(cell="LG_M50T", current=-5, duration=3600)
print(f"Final voltage: {result.voltage[-1]:.3f} V")
```

### Constant-current runs: sign, run time and cooling

`simulate()` uses the same conventions as the Standard Simulation page and the API:

- **Sign.** `current` is in amps. A negative current is a discharge. A positive
  current is a charge.
- **Run time.** With no `duration` (the default), the run goes to the voltage
  cutoff: `v_min` for a discharge, `v_max` for a charge. This is the page's CC
  mode. The API sizes the run, up to 120 h. Give `duration` in seconds to stop
  the run at that time or at the cutoff, whichever comes first (CC+Time).
  `estimate_simulation_tokens()` takes the same arguments and quotes the same
  request.
- **Cooling.** `thermal_mode` is `"convective"`, `"isothermal"` or
  `"adiabatic"`. `htc` is the heat transfer coefficient in W/K for
  `"convective"`; the API uses 0.3 W/K when you give none. With no
  `thermal_mode`, the run is `"convective"`, the page's default.
- **Defaults.** `simulate()` with no arguments runs the page's default run: LG
  M50T, a 5 A charge from 0 % SOC at 25 °C, convective, to 4.2 V. With no
  `init_soc`, a discharge starts at 100 % and a charge at 0 %.
- **Refusals and time limits.** When the API refuses a run, the error text is
  the API's own sentence in full, then its reason code in brackets; `e.body`
  holds the whole reply. `timeout=` is the most time the call waits for the
  job; then it raises `JobTimeoutError` (see "When a wait times out").

```python
# 0.5C discharge of the 5 Ah LG M50T, to 2.5 V, cooled as on the page
r = client.simulate(cell="LG_M50T", current=-2.5, thermal_mode="convective", htc=0.3)
```

`amplabs simulate` uses the same conventions:

- `--rate` takes a C-rate (`1C`, `0.5C`) or amps (`5A`, or a plain number such
  as `5`). A negative value (`-5A`, `-1C`) is a discharge and a value with `+`
  (`+5A`) is a charge. A value with no sign is a discharge. You can also write
  `discharge 2C` or `charge 0.5C`; do not use a word and a sign together.
  Write a negative value as `--rate=-5A` or `--rate -5A`.
- `--duration` takes `2h`, `1.5h`, `30m`, `90min`, `3600s` or plain seconds
  (`3600`). Leave it out to run to the voltage cutoff. Any other value stops
  the command with a usage error that lists these formats.
- `--thermal` (also `--thermal-mode`) is `convective` (the default, as on the
  page), `isothermal` or `adiabatic`. `--htc` sets the heat transfer
  coefficient in W/K for `convective`.

### Batch, LUT and Performance Insights: price, stop, resume

These jobs work as on their web pages. Price a job before you run it, run it
without waiting, stop it, and resume it later. A resumed job runs only the work
it did not do, and a stop gives back the tokens for that work.

```python
grid = {"soc_points": [10, 50, 90], "temperatures": [0, 25, 45],
        "direction": "discharge", "c_rates": [1.0], "pulse_duration": 10}
print(client.quote_lut("LG_M50T", "dcir", grid))            # {"tokens": ..., "solves": ...}
job = client.generate_lut("LG_M50T", "dcir", grid, wait=False)
client.stop_job(job["job_id"])
table = client.resume_lut(job["job_id"])                   # waits, returns the table
```

- LUT types: `ocv_soc`, `dcir`, `pulse_discharge_limit`, `pulse_charge_limit`,
  `fast_charge`, `fast_charge_soc` (`client.lut_types()`). A key you leave
  out of the config takes the server's default grid, which can be large.
- `quote_batch()` and `quote_perf_insights()` price a sweep and an evaluation.
  `resume_batch()`, `resume_perf_insights()` and `resume_job()` resume them.
  `wait_for_job(job_id)` follows any job to its result.
- A Performance Insights `sweep` changes one geometry value in percent steps,
  for example `{"parameter": "cathode.geometry.thickness", "minPct": -5,
  "maxPct": 5, "stepPct": 2.5}`. It is not a temperature or C-rate sweep.

### When a wait times out

Every paid call (`simulate()`, `batch_simulate()`, `generate_lut()`,
`perf_insights()`, fits and validations) starts a job on the server and then
waits for it. When the wait passes `timeout=`, the SDK stops waiting but the
job does not stop. It keeps running and is charged as it runs. The SDK raises
`JobTimeoutError` (a subclass of `SimulationError`) with the job id:

```python
from amplabs import JobTimeoutError

try:
    table = client.generate_lut("LG_M50T", "dcir", grid, timeout=600)
except JobTimeoutError as e:
    table = client.wait_for_job(e.job_id, timeout=3600)  # get the result
    # or: client.stop_job(e.job_id)                      # stop it
```

`stop_job()` stops Batch, LUT and Performance Insights jobs. Other jobs run to
their end; the message says so. In the CLI, use `amplabs jobs wait <id>` and
`amplabs jobs stop <id>`.

You get the job id before the wait starts, too, so a Ctrl+C or a crash does not
lose it:

- `AmpLabs(on_submitted=print)` calls your function with each job id.
- `client.last_job_id` holds the id of the last job.
- The id is logged at INFO on the `amplabs` logger
  (`logging.basicConfig(level=logging.INFO)` shows it).
- The CLI prints `Waiting for job <id>` before it waits, and a Ctrl+C prints the
  job id and what to do.

### A lost answer is not charged twice

Each paid call sends a new `Idempotency-Key` header with its request. If the
answer is lost (a Wi-Fi drop, a timeout, a 502 or 504), the SDK sends the same
request with the same key again, up to 3 times. The server answers a repeated
key with the job it already started, and does not charge again. A new call is a
new run with a new key, so two calls with the same settings are two runs.

The SDK sends a request again only when the server's `/health` says
`idempotencyKeys: true`. An older server ignores the key, so there the SDK
raises the error as before and does not send the request again. A request that
never reached the server (the connection was refused) is always sent again.

### CCCV, time step, cooling power, co-simulation and BDF

These are the other settings of the Standard Simulation page (#2568).
`simulate()` and `estimate_simulation_tokens()` take all of them and send the
request the page sends, so the quote prices the run that you start.

- **CCCV.** `cutoff_mode="soc"` runs at `current` to the voltage limit, then
  holds that voltage until `target_soc` (0-100 %). With no `target_soc`, the
  API uses 100 for a charge and 0 for a discharge. `cv_cutoff_current` (A)
  stops the hold earlier; with none, the API uses C/20; `0` means no cutoff. A
  CCCV run takes no `duration`.
- **Time step.** `dt` is the largest solver step in seconds. With none, the
  solver picks its own step.
- **Cooling power.** `q_cool_w` is in W. With none, the API uses 0 W.
- **Co-simulation.** `co_simulate(cell=..., drive_cycle="measured.csv")` is the
  page's Upload mode. It stores the file in your files, runs it on the named
  cell and returns RMSE, MAE, the maximum error, the simulated trace, the job id
  and the file id. `init_soc` is a percent; with none, the server reads it from
  the rest the file starts with.
- **BDF.** `result.to_bdf("run.bdf.csv")` writes the same file as the page's
  "Download BDF", and returns its text.

```python
quote = client.estimate_simulation_tokens(cell="LG_M50T", current=5, init_soc=0,
                                          cutoff_mode="soc", target_soc=100)
r = client.simulate(cell="LG_M50T", current=5, init_soc=0, cutoff_mode="soc", target_soc=100, dt=10)
r.to_bdf("cccv.bdf.csv")

cosim = client.co_simulate(cell="LG_M50T", drive_cycle="measured.csv", temperature=25)
print(cosim.rmse_mv, cosim.job_id)
```

On the command line: `amplabs simulate --cccv --target-soc 80 --cv-cutoff 0.25
--dt 10 --q-cool 1 --bdf run.bdf.csv`, and `amplabs co-simulate measured.csv
--cell LG_M50T`.

## CLI

```bash
amplabs whoami                   # account and credits
amplabs simulate --cell LG_M50T --rate 1C   # to the voltage cutoff
amplabs simulate --rate=-5A --duration 90min --thermal isothermal
amplabs simulate --rate "charge 1C" --soc 0 --cccv --target-soc 80 --bdf run.bdf.csv
amplabs co-simulate measured.csv --cell LG_M50T
amplabs validate --cell LG_M50T udds.csv --temperature 25   # as the web Upload
amplabs validate profile.json udds.csv --temperature 25
amplabs parameterize --cell my_cell.json --hppc hppc.csv --c20 c20.csv --anode-model graphite_Chen2020
amplabs profiles
amplabs lut --tables dcir --temp-min 0 --temp-max 40 --quote   # price only
amplabs insights --profiles default_LG_M50T --detach            # prints the job id
amplabs jobs stop <job id>; amplabs jobs resume <job id>
amplabs --help
```

## Configuration

| Variable | Use |
|---|---|
| `BATTERYLAB_TOKEN` | Your PAT. Used when you do not pass `token=`; `~/.amplabs/token` is used when this variable is not set. |
| `BATTERYLAB_URL` | API address. Default: `https://api.hub.amplabs.ai`. Must be `https://`; plain `http://` is allowed only for `localhost`. |
| `AMPLABS_RESULTS_DIR` | Folder for saved results. Default: `~/.amplabs/results`. |

## Errors

All SDK errors are subclasses of `amplabs.BatteryLabError`.

| Exception | When |
|---|---|
| `AuthError` | The token is missing, wrong or revoked (401). Run `amplabs login`. |
| `AccountRequiredError` | Anonymous mode, and the call needs an account. A subclass of `AuthError`. Run `amplabs login`. |
| `AnonymousLimitError` | Anonymous mode, and today's runs are used. `reset_at` holds the reset time (UTC). A subclass of `RateLimitError`. |
| `InsufficientCreditsError` | Not enough credits (402). `required` and `available` hold the numbers. |
| `NotFoundError` | No such resource (404). |
| `ValidationError` | The request is not valid (422). |
| `RateLimitError` | Too many requests (429). `retry_after` holds the wait in seconds. |
| `ServerError` | The API had a server error (5xx). Try again later. |
| `JobTimeoutError` | The SDK stopped waiting for a job that is still running and is charged. `job_id` holds its id for `wait_for_job()` or `stop_job()`. A subclass of `SimulationError`. |
| `APIConnectionError` | The SDK could not reach the API (network error or timeout). |

`result.plot()` with no matplotlib raises `BatteryLabError` with the install
command `pip install "amplabs-playground[plot]"`.

GET, PUT and DELETE requests are retried automatically on 429, 502, 503, 504
and on connection errors. POST requests are never retried, so a job is never
submitted twice. In anonymous mode, a playground run that the server refuses as
busy (503) or as over the per-minute limit (429) is tried again: the server did
not run or count it.

A script that does not catch an SDK error stops with one line, for example
`amplabs.exceptions.AuthError: Authentication failed: …`, and exit status 1.
Set `AMPLABS_TRACEBACK=1` to see the full traceback. The CLI also prints one
line for an API error.

## Documentation

- AmpLabs Hub: <https://hub.amplabs.ai>
- API documentation: <https://hub.amplabs.ai/docs/api>
- API reference (OpenAPI): <https://api.hub.amplabs.ai/docs>

The package has type stubs, so your IDE shows signatures and docstrings.
`help(AmpLabs)` works too.

## The old `amplabs` 0.x package

The 2024 `amplabs` 0.x package on PyPI is a different tool (Dash plots). It
uses the same import name `amplabs`, so do not install it next to
`amplabs-playground`. If both are installed, `import amplabs` warns. Remove the old one
with `pip uninstall amplabs`, then reinstall: `pip install --force-reinstall amplabs-playground`.

## License

Proprietary. See the `LICENSE` file in the package.
