Metadata-Version: 2.4
Name: amplabs-playground
Version: 1.3.1
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
Keywords: battery,simulation,parameterization,spme,ecm,amplabs,batterylab
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 (join the beta at <https://amplabs.ai>).

## 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 2 h of simulated time per run. Each series has at most 4,000 points.
- With no `thermal_mode`, an anonymous run uses `"convective"`, the playground's
  default (a signed-in run uses `"adiabatic"`).
- 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 (join the beta at amplabs.ai).

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 CSV files. Open or preview that HTML file; in an assistant, return a clickable file link and the CSV download.
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 CSV. 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"])
```

### 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; its fixed-duration default is not a replacement for 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.

The following recipe runs the default Samsung 30T discharge, saves a CSV and chart, and prints the measured wait.
Change the settings in the final call to match the task.
Only public settings and simulation outputs are used.

```python
# playground-task-recipe
import csv
import time
from pathlib import Path
from amplabs import PlaygroundClient
from amplabs.results import plot


def run_playground_task(*, scenario="discharge", cell="Samsung_IN21700_30T",
                        rate=1.0, temperature=25, thermal="convective", htc=0.3,
                        base_url="https://api.hub.amplabs.ai", output="simulation"):
    capacities = {"Samsung_IN21700_30T": 3.0, "LG_M50T": 5.0, "NMC_Graphite": 110.0}
    if scenario not in {"discharge", "charge", "udds"}:
        raise ValueError("Choose discharge, charge or udds")
    if scenario == "udds":
        if cell != "Samsung_IN21700_30T":
            raise ValueError("Measured UDDS is available for Samsung 30T only")
        action, inputs = "run-drive-cycle", {"temperature": temperature}
    else:
        current = capacities[cell] * rate
        inputs = {"current": -current if scenario == "discharge" else current,
                  "init_soc": 100 if scenario == "discharge" else 0,
                  "min_v": 2.5, "max_v": 4.2, "temp_degC": temperature,
                  "cutoff_mode": "voltage" if scenario == "discharge" else "soc",
                  "thermal_mode": thermal}
        if cell != "Samsung_IN21700_30T":
            inputs["cell"] = cell
        if scenario == "charge":
            inputs["target_soc"] = 100
        if thermal == "convective":
            inputs["htc"] = htc
        action = "run"
    started = time.perf_counter()
    with PlaygroundClient(base_url=base_url) as client:
        available = {a["name"] for a in client.module("standard-simulation")["actions"]
                     if a.get("compute") == "live"}
        if action not in available:
            raise RuntimeError(f"The selected API does not offer live {action}")
        answer = client.call("standard-simulation", action, inputs).answer
    elapsed = time.perf_counter() - started
    result = answer["results"]
    if scenario == "udds":
        measured = answer["measured"]
        series = {"time": result["simulated_time"], "voltage": result["simulated_voltage"]}
        # Keep the independently sampled measured trace intact in a separate CSV.
        with Path(f"{output}-measured.csv").open("w", newline="") as f:
            writer = csv.writer(f)
            writer.writerow(["time_s", "measured_voltage_v"])
            writer.writerows(zip(measured["time"], measured["voltage"], strict=True))
    else:
        series = {key: result[key] for key in ("time", "voltage", "current", "temperature")
                  if isinstance(result.get(key), list) and len(result[key]) == len(result["time"])}
    with Path(f"{output}.csv").open("w", newline="") as f:
        writer = csv.writer(f)
        writer.writerow(list(series))
        writer.writerows(zip(*series.values(), strict=True))
    fig = plot(series, title=f"{cell} · {scenario} · {temperature} °C", show=False)
    if scenario == "udds":
        fig.axes[0].lines[0].set_label("Simulated")
        fig.axes[0].plot([t / 3600 for t in measured["time"]], measured["voltage"], label="Measured")
        fig.axes[0].legend()
    fig.savefig(f"{output}.png", dpi=160, bbox_inches="tight")
    print(f"Results produced in {elapsed:.1f} s")
    return answer, fig


answer, figure = run_playground_task()
# In a notebook, display(figure). In a desktop script, use matplotlib.pyplot.show().
```

- **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 CSV 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
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), `init_soc` (%), `temperature` (°C), `v_min`, `v_max` (V), `thermal_mode`. |
| `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`, `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")
```

## CLI

```bash
amplabs whoami                   # account and credits
amplabs simulate --cell LG_M50T --rate 1C --duration 1h
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 --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. |
| `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.
