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

Time [s]

Elapsed time in seconds

Voltage [V]

Cell voltage

Current [A]

Applied current

Step count

Step index (zero-based)

Cycle count

Cycle index (zero-based)

Step from cycler

Raw step number from the cycler

Cycle from 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}")
print(f"Steps created: {bundle.steps_created}")

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".

Next steps#

With data uploaded you can run parameter fitting and analysis workflows in Pipelines, or run forward simulations in Simulations.