Python Client 2.0 · API v2

API Guide

Submit BPX models, follow a run from queue to completion, inspect logs, and load only the result fields your analysis needs.

Client Version
2.0.0
Python Version
3.10–3.13
API URL
https://api.dandeliion.com

01

Quickstart

You need a DandeLiion API token and a valid BPX v0.5 file. Keep the token in an environment variable so it never appears in source code or notebooks.

Terminal
python -m pip install "dandeliion-client>=2,<3"
Python
import os

import dandeliion.client as dandeliion

simulator = dandeliion.Simulator(
    "https://api.dandeliion.com",
    os.environ["DANDELIION_API_KEY"],
)
params = "cell.bpx.json"

solution = dandeliion.solve(
    simulator=simulator,
    params=params,
    is_blocking=True,
)

print(solution.status)
if solution.status == "succeeded":
    print(f"Final voltage: {solution['Voltage [V]'][-1]:.3f} V")
else:
    print(solution.log)
Example output
succeeded
Final voltage: 2.000 V

02

Installation

Client 2.0 supports Python 3.10 through 3.13; Python 3.14 is excluded by the published package. Check python --version and select a supported interpreter before creating the environment (for example, python3.13). A virtual environment keeps its packages separate from the rest of your system.

Terminal
python -m venv .venv
source .venv/bin/activate
python -m pip install --upgrade pip
python -m pip install "dandeliion-client>=2,<3"

On Windows PowerShell, activate with .venv\Scripts\Activate.ps1 instead of source .venv/bin/activate. In a notebook, select this environment’s kernel. The base install includes NumPy and the BPX validator.

Optional plotting dependency

For the plotting examples in section 8, install matplotlib in the same environment:

Terminal
python -m pip install matplotlib

Optional PyBaMM validation

Install the PyBaMM extra if you want PyBaMM to validate experiment instructions and construct drive-cycle steps. String experiments and raw time-series input also work without it. Installing the extra makes dandeliion.Experiment an alias for pybamm.Experiment. PyBaMM validates syntax; it does not guarantee support for every PyBaMM option in DandeLiion’s models. Matplotlib remains a separate install.

Terminal
python -m pip install "dandeliion-client[pybamm]>=2,<3"

Confirm the installed client before submitting a run:

Python
import dandeliion.client as dandeliion

print(dandeliion.__version__)
# 2.0.0

03

Authentication

Create or obtain a token through the DandeLiion Token Portal. API v2 tokens contain exactly 64 lowercase hexadecimal characters. Treat the token like a password: do not commit, log, print, or place it directly in a notebook.

Python
import os

import dandeliion.client as dandeliion

simulator = dandeliion.Simulator(
    api_url="https://api.dandeliion.com",
    api_key=os.environ["DANDELIION_API_KEY"],
)

The service root, /api/v2, or the complete /api/v2/runs collection URL are accepted. Client 2.0 rejects legacy /v1 URLs and requires HTTPS except for localhost.

Transport settings

All five settings below are optional. The values shown are the defaults, so the two-argument Simulator above is sufficient. Set them only when you want to change network waits, polling, or retries. All durations are in seconds.

Python
simulator = dandeliion.Simulator(
    "https://api.dandeliion.com",
    os.environ["DANDELIION_API_KEY"],
    request_timeout=(3.05, 30),
    result_timeout=(3.05, 300),
    poll_interval=1,
    max_poll_interval=10,
    max_attempts=3,
)
Setting and defaultMeaning
request_timeout=(3.05, 30)Connection timeout, then read-inactivity timeout, for submissions, status, logs, and cancellation.
result_timeout=(3.05, 300)Connection timeout, then read-inactivity timeout, for selected fields and full result downloads. A download may take longer overall while bytes keep arriving.
poll_interval=1Initial delay between status requests during blocking solve() or join(). A status change resets the delay.
max_poll_interval=10Maximum delay between unchanged status polls. The client multiplies the delay by 1.5 until it reaches this limit. Must be at least poll_interval.
max_attempts=3Total attempts per retryable HTTP request, including the first: at most two retries. Transport failures and selected temporary HTTP errors can be retried; validation errors and authorization_unknown are not. This does not limit the number of status polls.

These settings do not change the remote simulation deadline. Use solution.join(timeout=60) to limit a local wait; individual HTTP requests still use their own timeouts. That wait does not cancel the run. Reading solution.status manually does not add a polling delay.

04

Simulation parameters

Pass a BPX filename or pathlib.Path, a valid BPX dictionary, or an in-memory BPX object returned by bpx.parse_bpx_file(). The client uses BPX v0.5 to validate input before submission; this is the validator version, not a requirement to rewrite the file’s header. Extra parameters override matching DandeLiion: entries in the BPX Parameterisation → User-defined section. Do not add that prefix yourself inside extra_params. The BPX validator does not check every custom model option; a model rejection after acceptance still uses one token use.

Python
params = "cell.bpx.json"

extra_params = {
    "Mesh": {"x_n": 16, "x_s": 16, "x_p": 16, "r_n": 16, "r_p": 16},
    "Initial SOC": 1.0,
    "extra_resources": False,
}
Parameter Purpose
Mesh Mesh points for x_n, x_s, x_p, r_n, and r_p. Each defaults to 16 and must be at least 4.
Initial SOC Initial state of charge from 0 to 1. The default is 1.
Initial voltage [V] Initial single-cell voltage between the BPX cell voltage limits; Module uses this per series cell. Remove Initial SOC when setting this option, because SOC takes precedence.
Time series input Time in seconds plus exactly one of Current [A] or Power [W]. Use equal-length, finite arrays with at least two samples and strictly increasing times; start at zero. Negative values discharge, positive values charge.
Lumped thermal model Enables lumped thermal behaviour; required material properties come from the BPX cell section. In models v0.4.0, omit this key for an isothermal run: even False selects the lumped model.
Heat transfer coefficient [W.m-2.K-1] Non-negative coefficient for the lumped thermal model. It defaults to zero (no heat exchange); set it explicitly when modelling cooling.
Module Battery-pack geometry and model parameters.
Safe mode Selects the solver’s safe mode, with additional convergence checks and a possible performance cost. In models v0.4.0, the presence of this key enables it even if set to False; omit the key to use the default fast mode.
extra_resources Optional boolean, default False (Lambda). Set True for Fargate when the run may exceed Lambda’s 15-minute or approximately 3 GB limits. Fargate has a longer startup delay and a configured maximum runtime of 12 hours.

Initial conditions

  • Set initial temperature in the BPX Cell section.
  • Set initial electrolyte concentration in the Electrolyte section.
  • Set DandeLiion: Initial SOC or DandeLiion: Initial voltage [V] in User-defined, or pass the unprefixed key through extra_params.

05

Experiments and drive cycles

Use dandeliion.Experiment without PyBaMM, or pybamm.Experiment when the optional dependency is installed. Adapt the example voltage limits to your BPX cell. Explicit experiment targets can exceed the BPX cutoffs, so choose suitable targets yourself.

Python
experiment = dandeliion.Experiment(
    [
        (
            "Discharge at 1C until 3.2 V",
            "Hold at 3.2 V for 1 minute (5 second period)",
            "Rest for 30 seconds",
            "Charge at 2000 mA for 1 minute or until 3.6 V",
        )
    ],
    period="10 s",
)

Drive cycles without PyBaMM

Choose this example in an environment without PyBaMM. Use a two-column CSV file containing time in seconds and current in amps. This example assumes positive current in the CSV means discharge, so it negates the current column for DandeLiion’s negative-discharge convention. It starts at 60% SOC and repeats the profile twice. If your data already uses negative discharge, omit the negation.

Python
import numpy as np

data = np.loadtxt("drive_cycle.csv", delimiter=",", comments="#")
drive_cycle = np.column_stack([data[:, 0], -data[:, 1]])

experiment = dandeliion.Experiment([("Time series",) * 2], period="10 s")
extra_params = {
    "Initial SOC": 0.6,
    "Time series input": {
        "Time [s]": drive_cycle[:, 0].tolist(),
        "Current [A]": drive_cycle[:, 1].tolist(),
    },
}

Drive cycles with PyBaMM

Choose this alternative when the PyBaMM extra is installed. It embeds the time series, so start with fresh extra_params and omit Time series input. Client 2.0 passes step arrays through unchanged: use DandeLiion’s negative-discharge sign here too, even though PyBaMM’s own solver normally uses positive discharge. Negate power profiles in the same way if their source uses positive discharge.

Python
import numpy as np
import pybamm

data = np.loadtxt("drive_cycle.csv", delimiter=",", comments="#")
drive_cycle = np.column_stack([data[:, 0], -data[:, 1]])

experiment = pybamm.Experiment(
    [(pybamm.step.current(drive_cycle),) * 2],
    period="10 s",
)
extra_params = {"Initial SOC": 0.6}

06

Run a simulation

dandeliion.solve() converts validated BPX input and the experiment to the API request. Blocking mode waits for a terminal state; non-blocking mode returns after the API accepts the submission. Set is_blocking=True to wait within solve(), or use False and call join() later as below. Choose one of the experiment setups in section 5; each example replaces experiment and the drive-cycle examples also replace extra_params.

Python
solution = dandeliion.solve(
    simulator=simulator,
    params=params,
    experiment=experiment,
    extra_params=extra_params,
    is_blocking=False,
)

print("Run:", solution.run_id)
print("Status:", solution.status)  # May already be running or terminal.
solution.join()
print("Status:", solution.status)
if solution.status != "succeeded":
    print(solution.log)
Example output
Run: <run UUID>
Status: queued
Status: succeeded

Safe retries and token uses

Every accepted, distinct simulation consumes exactly one Token Portal use. The client automatically generates an idempotency key and reuses it for bounded transport retries. Supply a stable key when your application may repeat the same logical submission later.

Python
# Choose a new key for each distinct simulation. Keep this key for its retries.
submission_key = "analysis-batch-0001"
solution = dandeliion.solve(
    simulator=simulator,
    params=params,
    experiment=experiment,
    extra_params=extra_params,
    is_blocking=False,
    idempotency_key=submission_key,
)

Replaying the same token, canonical input, and key returns the original run without consuming another use. Reusing the key for different input raises DandeliionAPIException with code == "idempotency_conflict". Polling, logs, result downloads, and cancellation do not consume additional uses.

Validation metadata

This is a snapshot from the accepted submission, not a fresh token check. valid should be True, status should be "valid", and uses_remaining is the shared balance after that submission. expires_at is timezone-aware when present. Replaying an old run returns its original snapshot; polling cannot tell you the token’s current balance. Use the Token Portal for that. Reading this property consumes no use and never returns the token itself.

Python
validation = solution.token_validation
if validation is not None:
    print("Valid:", validation.valid)
    print("Status:", validation.status)
    print("Expires at:", validation.expires_at)
    print("Uses remaining after submission:", validation.uses_remaining)

07

Status, logs, and cancellation

Reading solution.status refreshes non-terminal runs. solution.join(timeout=...) waits with bounded polling and raises DandeliionTimeoutError if the local wait expires. Blocking solve() and join() also return normally for failed, cancelled, and timed-out runs; check solution.status before reading results. join() returns None.

queued

Accepted and waiting for backend capacity.

running

Executing on Lambda or Fargate.

cancel_requested

Cancellation is being applied.

succeeded

A usable result is available; it may be complete or partial.

failed

Stopped with an error; inspect the log.

cancelled

Cancellation reached a terminal state.

timed_out

Exceeded the backend runtime limit.

Incremental logs

solution.log retrieves new bounded log chunks and returns all text collected by that solution object so far. It can be called while the run is active. Logs can arrive in batches and may initially be empty, even just after a fast run becomes terminal; read again shortly afterwards. The local object keeps all fetched log text in memory. This loop prints only newly received characters and makes a final delayed read.

Python
import time

printed = 0
terminal_states = {"succeeded", "failed", "cancelled", "timed_out"}
while True:
    text = solution.log
    print(text[printed:], end="", flush=True)
    printed = len(text)
    if solution.status in terminal_states:
        break
    time.sleep(1)

# Log visibility can lag the final status; repeat later if needed.
time.sleep(2)
text = solution.log
print(text[printed:], end="", flush=True)
Show representative runtime log
Your simulation is now running on AWS Lambda.
[2026-09-15 13:22:12.401] [info] Starting dandeliion-models v0.4.0...
[2026-09-15 13:22:12.406] [info] BPX Model: DFN
[2026-09-15 13:22:12.411] [info] Thermal model: Isothermal
[2026-09-15 13:22:12.411] [info] Number of instructions to parse: 4
...
[2026-09-15 13:22:12.486] [info] Simulation completed successfully
[2026-09-15 13:22:12.486] [info] Simulation time: 74.470 ms
[2026-09-15 13:22:12.486] [info] Writing the output to output.h5...
[2026-09-15 13:22:12.632] [info] Output file writing time: 146.524 ms
[2026-09-15 13:22:12.632] [info] Total time: 231.178 ms

Cancellation

Python
# Use while the run is queued or running; it may finish before cancellation.
status = solution.cancel()
print(status)  # Usually "cancel_requested" or "cancelled"; may already be terminal.

Cancellation is idempotent. A running Lambda invocation may continue at the provider after the public run becomes cancelled, but its later output is unavailable as a successful result. Cancellation does not refund the accepted submission’s use. A run that finishes before the request arrives keeps its terminal status; cancel a running example before calling join() if you want to exercise cancellation.

08

Work with results

Wait for succeeded, then inspect field names from result metadata. Accessing one field requests that field and Time [s] when needed. The client materialises and caches each selected array in local memory; a spatial field can still be large. solution.dump() streams the complete result to disk. API JSON remains full precision even when the server stores HDF5; a null result size is normal for these runs.

Python
for key in sorted(solution.keys()):
    print(key)

Check whether the result is complete

succeeded means usable output is available, including saved samples from a solver that stopped early. API metadata reports artifacts.completion_status as complete, partial, or null when no result is published. Client 2.0 has no public completion-status property, so query the run metadata:

Python
import requests

response = requests.get(
    f"https://api.dandeliion.com/api/v2/runs/{solution.run_id}",
    headers={"Authorization": f"Token {os.environ['DANDELIION_API_KEY']}"},
    timeout=(3.05, 30),
)
response.raise_for_status()
metadata = response.json()
print("Completion:", metadata["artifacts"]["completion_status"])
print("Artifacts expire:", metadata["artifacts"]["expires_at"])

Partial results retain every saved sample and support the same plots and exports. Check their final time and runtime log before interpreting them as a whole experiment. A complete result may also end at a normal voltage cutoff. The saved bundle’s result.Header retains Completion status and Termination reason for offline checks.

Show fields from the example run
Charge [A.h]
Current [A]
Electrolyte concentration [mol.m-3]
Electrolyte potential [V]
Electrolyte x-coordinate [m]
Negative electrode concentration [mol.m-3]
Negative electrode exchange current density [A.m-2]
Negative electrode potential [V]
Negative electrode surface concentration [mol.m-3]
Positive electrode concentration [mol.m-3]
Positive electrode exchange current density [A.m-2]
Positive electrode potential [V]
Positive electrode surface concentration [mol.m-3]
Power [W]
Temperature [K]
Time [s]
Voltage [V]
X-averaged negative electrode concentration [mol.m-3]
X-averaged negative electrode exchange current density [A.m-2]
X-averaged negative electrode potential [V]
X-averaged negative electrode surface concentration [mol.m-3]
X-averaged positive electrode concentration [mol.m-3]
X-averaged positive electrode exchange current density [A.m-2]
X-averaged positive electrode potential [V]
X-averaged positive electrode surface concentration [mol.m-3]
X-coordinate in the negative electrode [m]
X-coordinate in the positive electrode [m]

Inspect values

The example output and plots show a two-cycle simulation starting at 60% SOC. Your results will depend on your BPX parameters and drive cycle.

Python
print(f"Final time [s]: {solution['Time [s]'][-1]}")
print(f"Final voltage [V]: {solution['Voltage [V]'][-1]}")
print(f"Final temperature [K]: {solution['Temperature [K]'][-1]}")
Example output
Final time [s]: 1200.0
Final voltage [V]: 3.2767115813933563
Final temperature [K]: 298.15

Interpolate timeline data

One-dimensional fields aligned with Time [s] are callable. The client performs linear interpolation with constant extrapolation: a query before or after the saved range returns the first or last value. Stay within the saved time range when analysing a partial run; extrapolation is not extra simulated data. Spatial arrays are indexed by time and are not callable.

Python
import numpy as np

t_eval = np.arange(0, 5, 1)
for time, voltage in zip(t_eval, solution["Voltage [V]"](t=t_eval)):
    print(f"{time}\t{voltage}")
Example output
0    3.2855181791739145
1    3.2855089881842403
2    3.285500892843016
3    3.2854935618152186
4    3.285486847318352

Plot scalar fields

Install matplotlib as shown in section 2. Current and power are negative during discharge.

Python
import matplotlib.pyplot as plt

fig, axes = plt.subplots(2, 1, figsize=(10, 8))
axes[0].plot(solution["Time [s]"], solution["Current [A]"])
axes[0].set(title="Current [A]", xlabel="time [s]")
axes[1].plot(solution["Time [s]"], solution["Voltage [V]"])
axes[1].set(title="Voltage [V]", xlabel="time [s]")
plt.tight_layout()
plt.show()
Current and voltage traces over two drive cycles, from 0 to 1,200 seconds
Current and voltage for a two-cycle simulation at 60% initial SOC.

Plot a spatial field

Python
import matplotlib.pyplot as plt

plt.figure()
plt.plot(
    solution["Electrolyte x-coordinate [m]"] * 1e6,
    solution["Electrolyte concentration [mol.m-3]"][-1],
)
plt.xlabel(r"x [$\mu$m]")
plt.ylabel(r"Concentration [mol m$^{-3}$]")
plt.title("Electrolyte concentration at the final saved time")
plt.tight_layout()
plt.show()
Example electrolyte concentration across the cell at the end of the experiment
Electrolyte concentration across the cell at the final time.

09

Save and restore solutions

Use solution.dump() to save and Simulator.restore() to load a client v2 bundle. A succeeded run’s bundle contains metadata, logs available at save time, and the full published result streamed directly to disk, including Header and all fields even if you accessed only one. This can be a complete or partial simulation. The client writes atomically without loading the whole result in memory; the destination directory must already exist.

Python
solution.dump("solution.json")
restored_solution = dandeliion.Simulator.restore("solution.json")

print(restored_solution["Voltage [V]"][-1])

Restored result fields are parsed lazily. A completed bundle works offline and contains no API token, origin, or reusable server URL. Re-saving an offline restored bundle works too. This client bundle differs from the raw Header/Solution JSON returned by the REST result endpoint; restore() requires a client v2 bundle. To inspect the saved model header:

Python
import ijson

with open("solution.json", "rb") as saved:
    header = next(ijson.items(saved, "result.Header"))
print(header.get("Completion status"))
print(header.get("Termination reason"))

Reconnect an incomplete bundle

Call dump() immediately after non-blocking submission, before join(), to save an active run with result: null. A fast run may already have finished, in which case the saved bundle includes its result. To reconnect an active bundle, supply the same token that owns the run and an explicit API URL. A bundle from a partial succeeded run already has a result and works offline; it does not resume the solver.

Python
solution.dump("incomplete-solution.json")

restored_solution = dandeliion.Simulator.restore(
    "incomplete-solution.json",
    api_url="https://api.dandeliion.com",
    api_key=os.environ["DANDELIION_API_KEY"],
)
restored_solution.join()
print(restored_solution.status)
if restored_solution.status == "succeeded":
    restored_solution.dump("completed-solution.json")
Keep your own copy. The current artifact retention is 14 days. Check artifacts.expires_at in run metadata and save results before expiry for long-term access. A local bundle with results stays usable offline after server artifacts expire.

10

Errors and troubleshooting

Token rejections have their own exception type. Other API failures expose structured context that can be recorded safely for support. BPX and experiment validation can also raise local ValueError/TypeError (including BPX validation errors) before any API submission or token use. A solver failure after acceptance is reported through the run status and log; it is not automatically raised by blocking solve().

Python
try:
    solution = dandeliion.solve(
        simulator=simulator,
        params=params,
        experiment=experiment,
        extra_params=extra_params,
        is_blocking=False,
        idempotency_key=submission_key,  # Reuse only for this same request.
    )
except dandeliion.DandeliionTokenValidationError as exc:
    print(exc.validation.status)
    print(exc.validation.expires_at)
    print(exc.validation.uses_remaining)
except dandeliion.DandeliionAPIException as exc:
    print(exc.status_code)
    print(exc.code)
    print(exc.request_id)
    print(exc.authorization_request_id)
    print(exc.retry_after)
    print(exc.idempotency_key)
Symptom Action
Token invalid, expired, inactive, or exhausted Inspect DandeliionTokenValidationError.validation and manage the token in the Token Portal.
idempotency_conflict Do not reuse a logical submission key for different parameters. Generate a new key for a distinct simulation.
authorization_unknown Do not resubmit automatically. Give authorization_request_id to support so the possibly consumed use can be reconciled.
HTTP 429 Wait for retry_after. Rate limiting does not consume a portal use.
DandeliionTimeoutError The local wait expired; the remote run may still be active. Check solution.status or cancel explicitly.
Missing result field Inspect solution.keys(); requesting an unknown direct Solution field raises KeyError.
Offline incomplete restore Restore again with both an explicit API URL and API key.

11

Migrate from client 1.x

  1. Change https://api.dandeliion.com/v1 to https://api.dandeliion.com.
  2. Use Python 3.10–3.13; Python 3.9 is no longer supported.
  3. Expect succeeded instead of success, plus cancellation and timeout states.
  4. Remove WebSocket assumptions; status and join() use bounded REST polling.
  5. If a submission loses its network response, retry with the same idempotency_key and identical BPX, experiment, and extra parameters. This retrieves the original run if it was accepted, without charging another use. The key is available on solution.idempotency_key or a submission exception’s idempotency_key. For authorization_unknown, stop and contact support instead.
  6. Client 2.0 can load only bundles written by client 2.0. Keep your original v1 files and use client 1.x in a separate environment to read them; they cannot be attached to an API v2 run. If you need a new v2 simulation, submit its BPX and experiment again (one use), then save it with solution.dump(). There is no automatic v1-to-v2 conversion.

12

Resources

The separately hosted client reference still describes client 1.2.0 and /v1. Use this guide and the versioned 2.0.0 source docstrings below for API v2.