Cell Measurements#
Measurements store time-series data recorded from testing a cell instance. Each measurement includes metadata (protocol, test setup, notes) and the raw time-series data itself.
Uploading a measurement is a single client call, but behind the scenes the client handles a three-step signed-URL upload process automatically.
Preparing time-series data#
Time-series data is passed as a pandas or polars DataFrame (or a plain
dict of lists). The following columns are required:
Column |
Description |
|---|---|
|
Elapsed time in seconds |
|
Cell voltage |
|
Applied current |
|
Step index (zero-based) |
|
Cycle index (zero-based) |
|
Raw step number from the cycler |
|
Raw cycle number from the cycler |
import pandas as pd
time_series = pd.DataFrame({
"Time [s]": [0, 1, 2, 3, 4, 5],
"Voltage [V]": [3.0, 3.2, 3.5, 3.8, 4.0, 4.2],
"Current [A]": [0.002, 0.002, 0.002, 0.002, 0.002, 0.002],
"Step count": [0, 0, 0, 1, 1, 1],
"Cycle count": [0, 0, 0, 0, 0, 0],
"Step from cycler": [1, 1, 1, 2, 2, 2],
"Cycle from cycler": [0, 0, 0, 0, 0, 0],
})
Uploading a measurement#
bundle = client.cell_measurement.create(
cell_instance.id,
{
"measurement": {
"name": "Formation Cycle 1",
"protocol": {
"name": "CC-CV charge at C/10 to 4.2V",
"ambient_temperature_degc": 25,
},
"test_setup": {
"cycler": "Biologic VMP3",
"operator": "Jane Smith",
},
"notes": "Formation cycle — first charge",
},
"time_series": time_series,
},
)
print(f"Measurement ID: {bundle.measurement.name}")
Steps are derived on the server after the record is created, so steps_created
is always 0 on the response. create() waits for that processing to finish and
raises MeasurementProcessingError if the file could not be read — a returned
result means the upload is usable, an exception means it is not.
Uploading a batch is the one case where that wait is worth moving out of the call. The server processes uploads concurrently, so submitting them all and then waiting on every id together takes about as long as the slowest single upload, where waiting inside each call takes the sum:
results = [
client.cell_measurement.create(cell_instance.id, detail, wait_for_processing=False)
for detail in details
]
client.cell_measurement.wait_for_processing([r.id for r in results])
wait_for_processing() applies one timeout across the whole batch and reports
every failure together — a rejected upload does not stop the rest being waited
on. Read MeasurementProcessingError.failures for the per-measurement reasons.
Use create_or_get() to skip the upload when a measurement with the same name
already exists on the instance:
result = client.cell_measurement.create_or_get(cell_instance.id, {
"measurement": {"name": "Formation Cycle 1", ...},
"time_series": time_series,
})
Listing measurements#
measurements = client.cell_measurement.list(cell_instance.id)
for m in measurements:
print(m.name)
Retrieving a measurement#
# Metadata only (no data)
measurement = client.cell_measurement.get(measurement_id)
Full measurement detail#
detail() fetches metadata, steps, cycles, and time-series
data in parallel. Use the include_* flags to skip data you
don’t need:
# Everything (3 parallel requests + download)
detail = client.cell_measurement.detail(measurement_id)
print(detail.steps.shape)
print(detail.cycles.shape)
print(detail.time_series.shape)
# Metadata + steps/cycles only (no download)
detail = client.cell_measurement.detail(
measurement_id, include_time_series=False
)
Accessing steps, cycles, and time series individually#
Each data type can also be fetched on its own:
steps = client.cell_measurement.steps(measurement_id)
cycles = client.cell_measurement.cycles(measurement_id)
ts = client.cell_measurement.time_series(measurement_id)
# Or fetch steps and cycles together (one call,
# more efficient than two separate calls):
sc = client.cell_measurement.steps_and_cycles(measurement_id)
print(sc.steps.shape, sc.cycles.shape)
Updating a measurement#
updated = client.cell_measurement.update(measurement_id, {
"notes": "Updated notes",
})
Deleting a measurement#
client.cell_measurement.delete(measurement_id)
DataFrame backend#
By default, data is returned as polars DataFrames. To use pandas instead, set the backend at client creation or at any time:
# At client creation
client = Ionworks(dataframe_backend="pandas")
# Or at any time
from ionworks import set_dataframe_backend
set_dataframe_backend("pandas")
You can also set the IONWORKS_DATAFRAME_BACKEND environment
variable to "pandas" or "polars".
Linking to the web app#
To get a URL to a measurement’s detail page in the Ionworks web app,
use client.urls:
# Uses the project_id configured on the client
# (from `IONWORKS_PROJECT_ID` or the `project_id=` constructor arg)
url = client.urls.measurement(measurement_id)
# Or pass an explicit project_id to override the client default
url = client.urls.measurement(measurement_id, project_id="other-project")
# https://app.ionworks.com/dashboard/projects/<project_id>/data/measurements/<measurement_id>
This is a pure local helper — no network call. The web-app host is derived
from the client’s api_url (api.ionworks.com → app.ionworks.com); set
IONWORKS_APP_URL to override, e.g. for local development.
Next steps#
With data uploaded you can run parameter fitting and analysis workflows in Pipelines, or run forward simulations in Simulations.