> ## Documentation Index
> Fetch the complete documentation index at: https://docs.ionworks.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Preparing data

> Read cycler files, BDF, and EIS data into the Ionworks format with the ionworksdata library

Before uploading, raw data from your cycler needs to be converted into the
[Ionworks data format](/data/format). The
[`ionworksdata`](https://data.docs.ionworks.com/) library reads files from
common battery cyclers, auto-detects formats, and normalizes units, timestamps,
and column names.

```bash theme={null}
pip install ionworksdata
```

## Supported cyclers

`ionworksdata` auto-detects the file format when possible and produces a polars
DataFrame with the standard columns.

| Cycler                                                                                    | File types                                                                           |
| ----------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------ |
| Arbin                                                                                     | `.csv`, `.xlsx`, `.res`                                                              |
| BaSyTec                                                                                   | `.csv`                                                                               |
| BioLogic                                                                                  | `.mpt`, `.mpr`, `.txt`                                                               |
| Maccor                                                                                    | `.txt`, `.csv`, `.xls`, `.xlsx`                                                      |
| Neware                                                                                    | `.csv`, `.xls`, `.xlsx` (multi-sheet supported; automatic Latin-1 fallback for CSVs) |
| Novonix                                                                                   | `.csv`                                                                               |
| Repower                                                                                   | `.csv`                                                                               |
| Generic CSV                                                                               | `.csv` (any CSV with recognized column headers or custom mappings)                   |
| Generic Parquet                                                                           | `.parquet` (any parquet file with recognized column headers or custom mappings)      |
| BDF ([Battery Data Format](https://battery-data-alliance.github.io/battery-data-format/)) | `.bdf`, `.bdf.gz`, `.bdf.parquet`, `.csv`                                            |

```python theme={null}
import ionworksdata as iwd

df = iwd.read.time_series("my_test.mpt", "biologic")
```

## One-call read pipeline

The format-specific readers (`read.neware`, `read.biologic`, …) return a *raw*
canonical-subset frame — no step count, capacity, energy, or step summary.
Prefer the high-level entrypoints, which run the full standard pipeline in a
single call and accept the same `reader`, `extra_column_mappings`, and `options`
arguments:

* `read.time_series` — parses the file, derives step and cycle counts, and
  computes capacity and energy. Returns the processed time-series DataFrame.
* `read.time_series_and_steps` — does everything `time_series` does, then adds
  the per-step summary DataFrame and runs a **validation-gated current-sign
  auto-fix**.

```python theme={null}
import ionworksdata as iwd

ts, steps = iwd.read.time_series_and_steps(
    "neware_data.csv",
    reader="neware",
)
```

With the default `options={"validate": True}`, `time_series_and_steps`
validates the frame against the same checks the Ionworks API applies on upload.
If validation reports a reversed or indeterminate current-sign convention, it
flips the sign, recomputes capacity and energy, and re-validates — so the
returned data already matches Ionworks' **positive = discharge** convention.

<Note>
  Every read path normalizes current sign to Ionworks' **positive = discharge**
  convention — `set_positive_current_for_discharge` runs inside the standard
  processing applied by `read.<format>`, `read.time_series`, and
  `read.time_series_and_steps` alike. What `time_series_and_steps` adds on top is
  a *validation-gated* re-check: if the upload validator still reports a reversed
  or indeterminate convention, it flips the sign, recomputes capacity and energy,
  and re-validates. The transform is detect-then-fix — it only flips when it
  detects that positive current corresponds to charge — so re-applying
  `iwd.transform.set_positive_current_for_discharge` to already-normalized data
  leaves the convention unchanged rather than double-flipping it (see
  [current sign convention troubleshooting](#incorrect-current-sign-convention)).
</Note>

If the source file has no step column, `time_series` derives one from
current-sign transitions automatically — no manual step-index pass needed.

## Custom column mappings

If your CSV uses non-standard column names, map them to the standard names
with `extra_column_mappings`. The mapping overrides the reader's built-in
rename table for any raw column you name, so auto-detection is skipped for it.

<Note>
  The *standard* name you map to still drives the reader's normal unit
  handling — mapping a column to `Current [mA]` declares its values are
  milliamps and the reader converts them to `Current [A]`. Pick the target name
  that matches the column's real unit, not the one its header claims (see
  [overriding misleading current-unit headers](#overriding-misleading-current-unit-headers)).
</Note>

```python theme={null}
import ionworksdata as iwd

df = iwd.read.time_series(
    "my_data.csv",
    "csv",
    extra_column_mappings={
        "vCell": "Voltage [V]",
        "iCell": "Current [A]",
        "t": "Time [s]",
        "Tcell": "Temperature [degC]",
    },
)
```

Partial mappings work too — any standard column you don't map is still
auto-detected:

```python theme={null}
df = iwd.read.time_series(
    "my_data.csv",
    "csv",
    extra_column_mappings={"vCell": "Voltage [V]"},
)
```

<Tip>
  Use `extra_column_mappings` when your CSV comes from a custom test setup or
  proprietary cycler. Mapped values are used as-is for standard-unit targets
  (V, A, s, °C) — no rescaling is applied — so ensure your data is already in
  those units before using this parameter. The one exception is a milliamp
  target: mapping to `Current [mA]` triggers the reader's milliamp-to-amp
  conversion (see [overriding misleading current-unit headers](#overriding-misleading-current-unit-headers)).
</Tip>

## Generic parquet files

For parquet files that don't follow the [BDF spec](#battery-data-format-bdf),
use the generic parquet reader. It mirrors the CSV reader's column-detection
strategy — recognizing common aliases for voltage, current, time, and
temperature — but skips text-parsing concerns since parquet is strongly
typed and has unambiguous column names. No separator, encoding, or quote
handling is needed.

Use it when you have cycler data already exported to parquet (for example,
from an internal pipeline or another tool) and want it normalized into the
[Ionworks data format](/data/format).

Any `.parquet` file (except `.bdf.parquet`) is auto-detected; you can also
select the reader explicitly:

```python theme={null}
import ionworksdata as iwd

# Auto-detected by extension
df = iwd.read.time_series("cell.parquet")

# Or call the reader directly
df = iwd.read.parquet("cell.parquet")
```

If the file uses non-standard column names, pass `extra_column_mappings`
just like with the CSV reader:

```python theme={null}
df = iwd.read.parquet(
    "cell.parquet",
    extra_column_mappings={
        "vCell": "Voltage [V]",
        "iCell": "Current [A]",
        "t": "Time [s]",
    },
)
```

<Note>
  BDF parquet files (`.bdf.parquet`) are still routed to the BDF reader —
  the generic parquet reader is only used as a fallback for `.parquet`
  files that aren't BDF.
</Note>

## Battery Data Format (BDF)

`ionworksdata` can read and write files in the
[Battery Data Format (BDF)](https://battery-data-alliance.github.io/battery-data-format/)
defined by the Battery Data Alliance. CSV, gzipped CSV, and parquet variants
are all supported. Files are auto-detected by their header or extension
(`.bdf`, `.bdf.gz`, `.bdf.parquet`).

```python theme={null}
import ionworksdata as iwd

# Read
df = iwd.read.bdf("cell.bdf")
df = iwd.read.time_series("cell.bdf.parquet")  # via the generic entrypoint

# Write
iwd.write.bdf(df, "out.bdf")                  # CSV with preferred labels
iwd.write.bdf(df, "out.bdf.gz")               # gzipped CSV
iwd.write.bdf(df, "out.bdf.parquet")          # parquet
iwd.write.bdf(df, "out.bdf", use_machine_readable_names=True)
```

<Note>
  BDF does not mandate a current sign convention. The reader normalises current
  to the Ionworks convention (**positive = discharge**) on load, so third-party
  BDF files that follow the opposite IEC convention are flipped automatically.
  The writer emits whatever convention is in the input DataFrame — pass the
  data through `transform.set_positive_current_for_discharge` first if you
  need to guarantee discharge-positive output.
</Note>

## EIS and impedance data

| Instrument | File types                                                  |
| ---------- | ----------------------------------------------------------- |
| BioLogic   | `.mpt`, `.mpr`, `.txt` (files containing impedance columns) |
| Gamry      | `.dta` (ZCURVE table)                                       |

Impedance data is read into columns `Frequency [Hz]`, `Z_Re [Ohm]`,
`Z_Im [Ohm]`, `Z_Mod [Ohm]`, and `Z_Phase [deg]`.

```python theme={null}
import ionworksdata as iwd

df = iwd.read.gamry("eis_measurement.dta")
```

See the [data format](/data/format#eis-and-impedance-data) page for the full
column spec and sign convention.

## Reader gotchas

A few cycler exports need a small nudge to read cleanly.

### Multi-sheet Neware Excel (BTSDA)

A Neware BTSDA `.xlsx` export splits into `unit`, `test`, `cycle`, `step`, and
`record` sheets, with the time series on the `record` sheet. The reader picks
`record` automatically when it is present, so no `options` are needed:

```python theme={null}
import ionworksdata as iwd

ts, steps = iwd.read.time_series_and_steps("neware_btsda.xlsx", reader="neware")
```

The `record` sheet often has no step or cycle column — step count is derived
from current-sign transitions automatically.

For workbooks whose time series sheet is named something else (older `.xls`
exports use `Detail_1`, `Detail_1_1`, …), name the sheets explicitly. The
reader concatenates multiple sheets and sorts by timestamp:

```python theme={null}
ts, steps = iwd.read.time_series_and_steps(
    "neware_detail.xls",
    reader="neware",
    options={"sheets": {"type": "pattern", "value": "Detail_"}},
)
```

`sheets` also accepts `{"type": "name", "value": "record"}` for a single named
sheet (or a list of names), and `{"type": "all"}` to read every sheet.

### Overriding misleading current-unit headers

The Neware reader maps amp-headed columns (`Current (A)`, `Current(A)`) to
`Current [A]` and milliamp-headed columns (`Current (mA)`, `Cur(mA)`) to
`Current [mA]`. Trust the *values*, not the header text: after reading,
sanity-check the current range against the cell's expected C-rate. If a header
lies about its unit (for example, an amp-headed column that actually carries
milliamps), override the target mapping with `extra_column_mappings`:

```python theme={null}
df = iwd.read.time_series(
    "mislabeled.csv",
    "neware",
    extra_column_mappings={"Current (A)": "Current [mA]"},
)
```

`extra_column_mappings` is merged on top of the reader's built-in rename table,
overriding the built-in entry for any raw column you map. Mapping to
`Current [mA]` is a deliberate *unit declaration*, not a raw passthrough — the
reader still applies its normal milliamp-to-amp conversion (dividing by 1000 to
produce `Current [A]`), which is exactly what corrects the mislabeled column.

### Vendor and MES re-exports

When a platform like Voltaiq re-exports a cycler file (extra derived columns
plus a timezone-aware `Timestamp`), the native cycler reader can choke on the
timezone with a datetime parse error. Fall back to the generic CSV reader and
map the already-elapsed time column so no timestamp parsing is needed:

```python theme={null}
df = iwd.read.time_series(
    "voltaiq_export.csv",
    "csv",
    extra_column_mappings={
        "Test Time (s)": "Time [s]",
        "Voltage (V)": "Voltage [V]",
        "Current (A)": "Current [A]",
    },
)
```

## Troubleshooting

### Incorrect current sign convention

**Problem:** When uploading measurement data, you receive an error like:

> Current sign convention error: positive current appears to be charge, not
> discharge.

**Solution:** Ionworks expects **positive current = discharge** and **negative
current = charge**. If your cycler uses the opposite convention, convert the
data before uploading:

```python theme={null}
import ionworksdata as iwd

data = iwd.transform.set_positive_current_for_discharge(data)
```

### Ambiguous current sign convention

**Problem:** When uploading measurement data, you receive an error like:

> Current sign convention error: the sign convention is ambiguous.

This happens when all current values have the same sign, so the validator
cannot determine whether positive means charge or discharge.

**Solution:** Use the same transform — it uses voltage-response analysis
(fitting an OCV-R equivalent circuit model under both sign conventions) to
infer charge vs. discharge direction even when all currents share the same
sign:

```python theme={null}
import ionworksdata as iwd

data = iwd.transform.set_positive_current_for_discharge(data)
```

If the automatic approach does not work for your data (for example, flat
voltage profiles), you can manually apply a sign based on the step type column
from your cycler:

```python theme={null}
import polars as pl

# Replace "Step type" and "charge" with your cycler's column name
# and step-type label (e.g. "CC_Charge", "Charge", "C", etc.)
data = data.with_columns(
    pl.when(pl.col("Step type") == "charge")
    .then(-pl.col("Current [A]").abs())
    .otherwise(pl.col("Current [A]").abs())
    .alias("Current [A]")
)
```

### Unsigned (magnitude-only) current

**Problem:** When uploading measurement data, you receive an error like:

> Current sign convention error: the current appears to be unsigned
> (magnitude-only) but contains both charge and discharge steps. Sign it using
> the cycler mode column via
> `ionworksdata.transform.set_positive_current_for_discharge(data)` before
> validation.

This fires when every non-rest current sample is positive yet the data clearly
contains both charge and discharge steps — a fingerprint of a cycler that
records current as a magnitude and encodes direction in a separate mode column
rather than in the sign of `Current [A]`. The issue is distinct from
[ambiguous sign convention](#ambiguous-current-sign-convention) because it has
a concrete, deterministic fix: re-sign the current from the cycler's own
charge/discharge labels.

**Solution:** Run `set_positive_current_for_discharge`. When it detects an
all-positive non-rest current alongside both charge and discharge steps, it
flips the sign of charge-step samples using the cycler mode column instead of
falling back to the voltage-response heuristic:

```python theme={null}
import ionworksdata as iwd

data = iwd.transform.set_positive_current_for_discharge(data)
```

The issue code is `CURRENT_SIGN_UNSIGNED` — branch on it explicitly if you
want to apply the auto-fix without prompting:

```python theme={null}
from ionworks import IssueCode, MeasurementValidationError
import ionworksdata as iwd

try:
    client.cell_measurement.create(cell_instance.id, data)
except MeasurementValidationError as e:
    if e.has_code(IssueCode.CURRENT_SIGN_UNSIGNED):
        data = iwd.transform.set_positive_current_for_discharge(data)
        client.cell_measurement.create(cell_instance.id, data)
    else:
        raise
```

### Swapped charge and discharge cumulative columns

**Problem:** When uploading with `validate_strict=True`, you receive an error
like:

> Column 'Discharge capacity \[A.h]' disagrees with the running integral of
> 'max(I, 0)' over time by up to 96.4% (exceeds 10% tolerance).

…and the reported `Discharge capacity [A.h]` looks like it tracks the charge
half-wave, with the same flipped behaviour on `Charge capacity [A.h]`. This
happens with some half-cell exports and cycler configurations that label the
two cumulative columns inversely.

**Solution:** Use `fix_swapped_charge_discharge_columns` to compare each
column against the trapezoidal integral of current (and power, for energy)
and rename the pair when the swapped assignment is within tolerance and the
as-is assignment is not.

```python theme={null}
import ionworksdata as iwd

# Required prerequisite — without a known sign convention, "Discharge" vs
# "Charge" has no ground-truth meaning to compare against.
data = iwd.transform.set_positive_current_for_discharge(data)

# Inspects "Discharge/Charge capacity [A.h]" and, when present,
# "Discharge/Charge energy [W.h]". Returns `data` unchanged when no swap
# is warranted.
data = iwd.transform.fix_swapped_charge_discharge_columns(data)
```

The transform requires `Time [s]`, `Step count`, and `Current [A]` columns,
and uses `Power [W]` (or `Voltage [V] * Current [A]` when `Power [W]` is
absent) for the energy pair. The default `tolerance` is 10 % relative
error; pass a different value if your data needs a tighter or looser
threshold:

```python theme={null}
data = iwd.transform.fix_swapped_charge_discharge_columns(data, tolerance=0.05)
```

<Note>
  The function refuses to swap labels when positive current does not
  correspond to discharge — without a known sign convention there is no way
  to tell which column is which, and swapping would mask a real sign-
  convention bug. Run `set_positive_current_for_discharge` first.
</Note>

### Time not cumulative

**Problem:** Time resets to 0 for each cycle.

**Solution:** Track a cumulative time offset:

```python theme={null}
cumulative_time_offset = 0.0
for cycle_num, cycle_df in df.group_by("Cycle from cycler"):
    cycle_df = cycle_df.with_columns(
        (pl.col("Time [s]") + cumulative_time_offset).alias("Time [s]")
    )
    cumulative_time_offset = cycle_df["Time [s]"].max()
```

### Step count not cumulative

**Problem:** Step count resets for each cycle.

**Solution:** Track a step count offset across cycles:

```python theme={null}
step_offset = 0
for cycle_num, cycle_df in df.group_by("Cycle count"):
    cycle_df = cycle_df.with_columns(
        (pl.col("Step from cycler") + step_offset).alias("Step count")
    )
    step_offset = cycle_df["Step count"].max() + 1
```

### Missing capacity columns

**Problem:** Capacity calculation fails.

**Solution:** Ensure you have `Time [s]`, `Current [A]`, and `Voltage [V]`
columns before calculating capacity.

### Non-UTF-8 CSV files (e.g. Neware)

**Problem:** Reading a Neware CSV file fails with an encoding error.

**Solution:** The Neware reader automatically falls back to Latin-1 if UTF-8
decoding fails, so no action is needed in most cases:

```python theme={null}
import ionworksdata as iwd

df = iwd.read.time_series("neware_data.csv", "neware")
```

To explicitly control the encoding:

```python theme={null}
df = iwd.read.time_series(
    "neware_data.csv",
    "neware",
    options={"file_encoding": "latin-1"},
)
```

## Next steps

<CardGroup cols={2}>
  <Card title="Data format" icon="file-code" href="/data/format">
    Full reference for recognized columns, units, and sign conventions.
  </Card>

  <Card title="Uploading data" icon="upload" href="/data/uploading">
    Upload prepared data as cell specs, instances, and measurements.
  </Card>

  <Card title="ionworksdata API reference" icon="book" href="https://data.docs.ionworks.com/">
    Complete reference for `read`, `write`, `transform`, `steps`, and `load`.
  </Card>

  <Card title="ionworksdata on GitHub" icon="github" href="https://github.com/ionworks/ionworksdata">
    Report issues or browse the source.
  </Card>
</CardGroup>
