queuedAccepted and waiting for backend capacity.
Python Client 2.0 · API v2
Submit BPX models, follow a run from queue to completion, inspect logs, and load only the result fields your analysis needs.
https://api.dandeliion.com01
You need a DandeLiion API token and a valid BPX 0.5 file. Keep the token in an environment variable so it never appears in source code or notebooks.
python -m pip install "dandeliion-client>=2,<3"
import os
import dandeliion.client as dandeliion
simulator = dandeliion.Simulator(
"https://api.dandeliion.com",
os.environ["DANDELIION_API_KEY"],
)
solution = dandeliion.solve(
simulator=simulator,
params="cell.bpx.json",
is_blocking=True,
)
print(solution.status)
print(f"Final voltage: {solution['Voltage [V]'][-1]:.3f} V")
succeeded
Final voltage: 3.680 V
02
Client 2.0 supports Python 3.10 through 3.13. A virtual environment keeps its packages separate from the rest of your system.
python -m venv .venv
source .venv/bin/activate
python -m pip install --upgrade pip
python -m pip install "dandeliion-client>=2,<3"
Install the PyBaMM extra if you want PyBaMM to validate experiment instructions and construct drive-cycle steps. It is not required for normal DandeLiion experiments.
python -m pip install "dandeliion-client[pybamm]>=2,<3"
Confirm the installed client before submitting a run:
import dandeliion.client as dandeliion
print(dandeliion.__version__)
# 2.0.0
03
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.
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.
The defaults make three bounded attempts and back off polling from one to ten seconds. Override them only when your network or expected result size requires it.
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,
)
04
Pass a BPX filename or pathlib.Path, a valid BPX dictionary,
or an in-memory BPX object. The client validates BPX 0.5 input
before submission.
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 cell or pack voltage between the BPX voltage limits. Initial SOC takes precedence when both are set. |
Time series input |
Current or power values indexed by time for a drive cycle. |
Lumped thermal model |
Enables lumped thermal behaviour; required material properties come from the BPX cell section. |
Heat transfer coefficient [W.m-2.K-1] |
Non-negative coefficient required by the lumped thermal model. |
Module |
Battery-pack geometry and model parameters. |
Safe mode |
Uses tighter convergence conditions at a potential performance cost. |
extra_resources |
Use the Fargate backend for runs expected to exceed 15 minutes or 3 GB of memory. |
Cell section.Electrolyte section.DandeLiion: Initial SOC or
DandeLiion: Initial voltage [V] in
User-defined, or pass the unprefixed key through
extra_params.
05
Use dandeliion.Experiment without PyBaMM, or
pybamm.Experiment when the optional dependency is installed.
experiment = dandeliion.Experiment(
[
(
"Discharge at 1C until 3.8 V",
"Hold at 3.8 V for 10 minutes (5 second period)",
"Rest for 300 seconds",
"Charge at 2000 mA for 1 hour or until 4.0 V",
)
],
period="10 s",
)
Supply a two-column time series and reference it with a
Time series experiment instruction.
import numpy as np
import pandas as pd
data = pd.read_csv("US06.csv", comment="#", header=None).to_numpy()
drive_cycle = np.column_stack([data[:, 0], -data[:, 1]])
experiment = dandeliion.Experiment(
[("Discharge at 1C until 3.8 V",), ("Time series",) * 2],
period="10 s",
)
extra_params["Time series input"] = {
"Time [s]": drive_cycle[:, 0].tolist(),
"Current [A]": drive_cycle[:, 1].tolist(),
}
A PyBaMM current or power step embeds the time series, so it does not need
to be repeated in extra_params.
import pybamm
experiment = pybamm.Experiment(
[
("Discharge at 1C until 3.8 V",),
(pybamm.step.current(drive_cycle),) * 2,
],
period="10 s",
)
06
dandeliion.solve() converts validated BPX input and the
experiment to the API request. Blocking mode waits for a terminal state;
non-blocking mode returns immediately.
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)
solution.join()
print("Status:", solution.status)
Status: running
Status: succeeded
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.
solution = dandeliion.solve(
simulator=simulator,
params=params,
experiment=experiment,
is_blocking=False,
idempotency_key="analysis-batch-0001",
)
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 = solution.token_validation
if validation is not None:
print(validation.status)
print(validation.expires_at)
print(validation.uses_remaining)
07
Reading solution.status refreshes non-terminal runs.
solution.join(timeout=...) waits with bounded polling and
raises DandeliionTimeoutError if the local wait expires.
queuedAccepted and waiting for backend capacity.
runningExecuting on Lambda or Fargate.
cancel_requestedCancellation is being applied.
succeededCompleted with a result available.
failedStopped with an error; inspect the log.
cancelledCancellation reached a terminal state.
timed_outExceeded the backend runtime limit.
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.
print(solution.log)
Your simulation is now running on AWS Lambda.
[2026-07-26 00:07:54.529] [info] Starting dandeliion-models v0.3.6...
[2026-07-26 00:07:54.553] [info] BPX Model: DFN
[2026-07-26 00:07:54.731] [info] Thermal model: Isothermal
[2026-07-26 00:07:54.731] [info] Number of instructions to parse: 11
[2026-07-26 00:07:54.743] [info] Starting the simulation...
...
[2026-07-26 00:08:00.245] [info] Simulation completed successfully
[2026-07-26 00:08:00.479] [info] Total time: 5.950 s
status = solution.cancel()
print(status) # "cancel_requested" or "cancelled"
Cancellation is idempotent. A running Lambda invocation may continue at
the provider after the public run becomes cancelled, but its
later output is hidden and discarded.
08
Available field names come from result metadata. Accessing one field
lazily streams that field—and Time [s] when needed—rather
than downloading the complete result into memory.
for key in sorted(solution.keys()):
print(key)
Charge [A.h]
Current [A]
Electrolyte concentration [mol.m-3]
Electrolyte potential [V]
Electrolyte x-coordinate [m]
Temperature [K]
Time [s]
Voltage [V]
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 exchange current density [A.m-2]
X-averaged positive electrode potential [V]
X-averaged positive electrode surface concentration [mol.m-3]
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]}")
Final time [s]: 6596.0361328125
Final voltage [V]: 3.6802161457188816
Final temperature [K]: 298.15
One-dimensional fields aligned with Time [s] are callable.
The client performs linear interpolation with constant extrapolation.
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}")
0 4.080929637658874
1 4.07556477797119
2 4.070638964445616
3 4.065947609011761
4 4.061984287623227
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()
plt.plot(
solution["Electrolyte x-coordinate [m]"] * 1e6,
solution["Electrolyte concentration [mol.m-3]"][-1],
)
plt.xlabel(r"x [$\mu$m]")
plt.title("Electrolyte concentration at the end of the experiment")
plt.show()
09
A successful solution bundle contains metadata, collected logs, and a complete result streamed directly to disk. The client writes it atomically without materialising the complete result in memory.
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.
An active run is saved with result: null. Reconnection
requires an explicit destination and token so a modified file cannot
redirect credentials to an attacker-controlled server.
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()
10
Token rejections have their own exception type. Other API failures expose structured context that can be recorded safely for support.
try:
solution = dandeliion.solve(
simulator=simulator,
params=params,
is_blocking=False,
idempotency_key="analysis-batch-0001",
)
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
https://api.dandeliion.com/v1 to https://api.dandeliion.com.succeeded instead of success, plus cancellation and timeout states.join() use bounded REST polling.12