From ionworks
Upload battery measurement data: time series, files, and properties. Use when the user wants to upload cycling data, parquet files, images, CSV data, or measurement properties to the Ionworks API. Triggers: "upload", "import data", "upload measurement", "upload time series", "upload file", "upload parquet", "create measurement".
How this skill is triggered — by the user, by Claude, or both
Slash command
/ionworks:upload-dataThe summary Claude sees in its skill listing — used to decide when to auto-load this skill
Guide for uploading measurement data to the Ionworks
Guide for uploading measurement data to the Ionworks platform. Three measurement types: time_series, file, and properties.
client.schema("data")The most common upload: cycling data with voltage, current, and time columns. Uses a signed-URL flow internally (the client handles this automatically).
import polars as pl
from ionworks import Ionworks
client = Ionworks()
# Prepare a DataFrame with required columns
df = pl.DataFrame({
"Time [s]": [0.0, 1.0, 2.0, 3.0],
"Current [A]": [1.0, 1.0, -1.0, -1.0],
"Voltage [V]": [3.8, 3.9, 3.7, 3.6],
"Step count": [0, 0, 1, 1],
})
# Upload
result = client.cell_measurement.create(
"instance-id",
{
"measurement": {
"name": "discharge_1C",
"notes": "1C discharge at 25°C",
"protocol": {"name": "1C discharge"},
"start_time": "2025-03-15T10:00:00Z",
"test_setup": {"temperature": "25°C"},
},
"time_series": df, # Polars or Pandas DataFrame, or dict
"steps": None, # auto-calculated if None
},
)
# result.id, result.name, result.steps_created
Time [s] — elapsed time (must start at 0, monotonically increasing)Current [A] — current (positive = discharge by convention)Voltage [V] — voltageStep count — monotonically increasing step indexCycle count — cycle indexCharge capacity [A.h], Discharge capacity [A.h]Charge energy [W.h], Discharge energy [W.h]Power [W], Temperature [degC]Validate first, upload second. Before calling
create / create_or_get, run the validators as a dry
run on the same frames and metadata. This catches schema,
column-type, and strict-check failures without producing
half-created records or partial uploads, and gives a clean
report you can act on.
from ionworks.validators import validate_measurement_data
validate_measurement_data(
ts_df,
strict=True,
steps_df=steps_df,
rated_capacity=5.0, # from cell_spec.ratings.capacity.value
voltage_window=(2.5, 4.2), # from cell_spec.ratings.voltage_*
)
# raises MeasurementValidationError on failure;
# returns silently when the upload would succeed
See the validate-data skill for the full validation
workflow (schema check via the discovery API, header
audit, strict measurement checks). Only proceed to
create once dry-run validation passes.
The client validates data before upload:
Recommended: pass validate_strict=True by default. Strict
validation catches subtle data issues before they contaminate
downstream simulations and fits.
result = client.cell_measurement.create(
"instance-id",
measurement_detail,
validate_strict=True,
)
If a specific strict check is a known false positive for the
dataset (e.g. a long rest period legitimately exceeds the 5 h
time-gap threshold), relax only that check via skip_checks
rather than disabling strict mode entirely — keep every other
guardrail on. This is the least-privilege approach to validation.
result = client.cell_measurement.create(
"instance-id",
measurement_detail,
validate_strict=True,
skip_checks={"time_gaps"}, # everything else still enforced
)
Valid names (ionworks.validators.STRICT_CHECK_NAMES):
minimum_points_per_stepcycle_constant_within_steptime_gapsvoltage_continuityconsecutive_same_direction_full_stepsstep_capacity_within_ratedDo not disable strict mode wholesale unless you have a documented reason that applies to multiple checks at once.
Idempotent upload — returns existing measurement if name already exists under the instance:
result = client.cell_measurement.create_or_get(
"instance-id",
measurement_detail,
)
# Returns existing measurement (200) or creates new (201)
Upload files (images, CSVs, arbitrary files) as a file-type measurement:
result = client.cell_measurement.create_file(
"instance-id",
name="SEM images",
filepaths=["/path/to/image1.png", "/path/to/image2.jpg"],
notes="SEM images at 5000x magnification",
protocol={"name": "SEM imaging"},
start_time="2025-03-15T14:00:00Z",
test_setup={"magnification": "5000x"},
)
# result.id, result.name
# Optional: validate image files have correct magic bytes
result = client.cell_measurement.create_file(
"instance-id",
name="validated images",
filepaths=["/path/to/image.png"],
validate_images=True,
)
Supported image formats (when validate_images=True):
jpg, jpeg, png, gif, webp, tiff, bmp.
Files are uploaded in parallel via signed URLs.
Upload structured key-value properties (no binary data):
meas = client.cell_measurement.create_properties(
"instance-id",
name="impedance_25C",
properties={
"R0": {"value": 0.015, "unit": "Ohm"},
"thickness": {"value": 0.52, "unit": "mm"},
"weight": {"value": 48.5, "unit": "g"},
},
notes="Room temperature impedance",
protocol={"name": "EIS"},
start_time="2025-03-15T16:00:00Z",
test_setup={"temperature": "25°C"},
)
Properties use the Quantity format:
{"value": <number>, "unit": "<string>"}.
Upload multiple measurements for a cell instance:
import polars as pl
from pathlib import Path
client = Ionworks()
# Create-or-get the hierarchy
spec = client.cell_spec.create_or_get({"name": "LGM50"})
inst = client.cell_instance.create_or_get(spec.id, {"name": "SN-001"})
# Upload each measurement file
for parquet_path in Path("data/").glob("*.parquet"):
df = pl.read_parquet(parquet_path)
client.cell_measurement.create_or_get(
inst.id,
{
"measurement": {"name": parquet_path.stem},
"time_series": df,
},
validate_strict=True,
)
from ionworks import IssueCode, IonworksError, MeasurementValidationError
try:
result = client.cell_measurement.create(
"instance-id", measurement_detail
)
except MeasurementValidationError as e:
# e.errors is a list of ValidationIssue records. Branch on
# issue.code (an IssueCode enum) — never on substrings of
# issue.message, since wording can change between releases.
for issue in e.errors:
print(f"[{issue.severity}] {issue.code}: {issue.message}")
# Structured details (step indices, columns, observed values, ...)
# are available in issue.payload.
if e.has_code(IssueCode.CURRENT_SIGN_REVERSED):
# Common, auto-fixable: positive current was charge, not discharge.
# Use ionworksdata.transform.set_positive_current_for_discharge(df)
# before re-uploading.
...
except IonworksError as e:
if e.error_code == "CONFLICT":
print("Measurement with this name already exists")
else:
print(f"Upload failed: {e.message}")
Guides creation, editing, and verification of skills for AI coding agents using test-driven development with subagent scenarios. Use when authoring or debugging skills.
npx claudepluginhub ionworks/ionworks-skills --plugin ionworks