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

# Materials & property datasets

> Manage electrode, electrolyte, and other materials and attach measured property datasets like OCP, diffusivity, and conductivity curves

## What are materials?

A **Material** is a reusable record describing a physical material used in a
[cell specification](/core-concepts/cells) — for example, an NMC811 cathode
powder, a graphite anode, or an LP57 electrolyte.

Materials and their wrapping **cell components** (anode, cathode, electrolyte,
separator, case) are **always scoped to a project**. Every material and
component belongs to exactly one project, and cell specifications only ever
reference materials and components in their own project. Two projects in the
same organization each get their own copy of the "same" material — same name,
manufacturer, and product ID — so property datasets and cell references stay
cleanly separated per project. Reusing a material across projects means
creating a matching material in each project.

<Note>
  Listing and creating materials require a `project_id`, but reading,
  updating, or deleting a single material by ID is authorized at the
  organization level — any project member with access to the material's
  organization can look up or modify a material by ID, including one that
  belongs to a different project within that organization.
</Note>

<Note>
  Any older materials that predated per-project scoping have been split into
  per-project copies, and the shared "System" material library (Graphite,
  NMC, LFP, …) is no longer read from at spec creation time — the library
  definitions are cloned into your own project instead. You will not see
  cross-project material sharing anywhere in the app.
</Note>

Each material can have any number of **property datasets** attached to it.
A property dataset is a tabular measurement (CSV or parquet) of one or more
physical properties as a function of one or more independent variables — for
example, electrolyte conductivity vs. concentration, or anode OCP vs.
stoichiometry. Property datasets belong to the same project as their parent
material.

<Note>
  Property datasets are stored as data — they are separate from
  [parameter interpolants](/build/interpolants), which embed lookup tables
  directly into a parameterized model. Use property datasets to organize and
  share raw measurements, then turn them into interpolants when you are ready
  to use them in a simulation.
</Note>

## When to use materials

Use materials when you want to:

* Keep a single source of truth for properties of a material used across
  multiple cells (e.g. the same electrolyte in several cell builds).
* Store raw measurements (OCP, diffusivity, conductivity, transference
  number, …) alongside the material they were measured on.
* Compare multiple datasets for the same property — for example, OCP curves
  measured at different temperatures or by different labs.
* Track provenance: who uploaded a dataset, when, and from which raw file.

## Managing materials in the UI

Each project has a **Materials** section in the left navigation. From there
you can:

* **Create a material** — give it a name, and optionally a manufacturer and
  product ID.
* **Open a material** — view its property datasets and metadata.
* **Edit or delete** a material from the row actions menu.

Materials created from the cell specification editor inherit the project of
the cell spec, so you rarely need to pick a project explicitly — the material
is created in the same project as the cell using it.

### Uploading a property dataset

From a material's detail page, click **Upload property dataset** and:

1. **Pick a file** — CSV or parquet. CSVs may include or omit a header row.
   When you select a file, the dataset name is prefilled from the file name;
   edit it if you want something different.
2. **Review the dataset name** — prefilled from the file name in step 1; change
   it if you want something different (e.g. `Conductivity at 25 °C`).
3. **Declare columns** — every column detected in the file is listed in order.
   For each one, provide:
   * **Name** — the display name stored in the processed dataset (e.g. `c_e`).
     Every listed column must have a name before you can submit — drop the
     row for any column you don't want to import, or name it and ignore it
     later. Trailing empty columns (a common Excel "trailing comma" artifact)
     are trimmed automatically.
   * **Unit** — the physical unit (e.g. `mol/L`, `S/m`). Leave blank for
     dimensionless quantities.
   * **Source column** — the column in the uploaded file the values come from.
     For headerless CSVs this is a position; for files with a header you can
     pick by name.
4. **Submit.** The file is parsed, every value is coerced to a floating-point
   number (non-numeric cells become NaN), and both the processed parquet and
   the original raw file are stored.

After upload, the dataset appears in the material's property list with the
number of rows and any NaN counts per column, so you can spot parsing issues
quickly.

### Plotting a dataset

Click a dataset to open the **plot dialog**. You can:

* Pick the x and y columns from the dataset. The legend shows each y column
  with its unit (e.g. `kappa (S/m)`) so dual-axis plots are easy to read.
* Zoom and pan; the plot dynamically downsamples and re-fetches points for the
  visible range so large datasets stay responsive.
* Download the processed parquet or the original raw file from the actions
  menu.

### Editing a dataset

The **Edit** action on a dataset lets you:

* Rename the dataset.
* Re-declare column names and units. When columns change, the stored parquet
  is rebuilt from the preserved original file using the new specs — you do
  not need to re-upload.
* Replace the data file entirely while keeping the same dataset ID and
  metadata. Other records that reference the dataset stay linked.

Each data-changing edit bumps the dataset's `data_version`, so downstream
consumers can detect when a cached result is stale.

## Tracking dataset provenance

Every property dataset can record **where it came from** so you can trace a
curve on a plot back to the pipeline, fit, or analysis that produced it —
or to the paper, lab notebook, or vendor sheet it was digitized from.

Provenance is captured with four optional fields:

| Field                       | Description                                                                   |
| --------------------------- | ----------------------------------------------------------------------------- |
| `source_pipeline_id`        | The pipeline run whose output produced this dataset.                          |
| `source_simple_pipeline_id` | The simple pipeline that produced this dataset.                               |
| `source_analysis_id`        | The [analysis](/data/analyses) this dataset was derived from.                 |
| `source_label`              | Free-text note (e.g. `"Smith et al. 2023, Fig. 3"` or `"Manually uploaded"`). |

**At most one** of the three `source_*_id` fields may be set on a given
dataset — a dataset has a single upstream Ionworks record, or none. The
referenced row must exist, or the request is rejected; the check does not
also verify that the row is visible to you (e.g. that it belongs to a
project or organization you can access), so a source ID for a record you
can't otherwise see is set but only reads back as unresolved when someone
tries to follow the link. `source_label` is independent and can be set on
its own — use it when the source is not a linkable Ionworks record.

The Source column on a material's dataset grid renders `source_label` when
set and otherwise the kind of source; when the source resolves to a
pipeline or analysis you can see in Studio, the label is a link straight to
the source's detail page.

### Set source when uploading or editing in the UI

The **Upload property dataset** and **Edit** dialogs include a **Source**
section:

1. Pick a **source kind** — Pipeline, Simple pipeline, Analysis, or leave
   blank for "no linkable source".
2. Paste the corresponding ID. Switching the kind clears any previously
   entered ID so you don't end up with more than one source set.
3. Optionally add a **source label** — a short free-text note that shows up
   in the Source column and on the dataset detail view.

### Set source via the REST API

Pass any subset of the source fields when creating or updating a dataset.
This example records that a dataset was produced by a pipeline run and adds
a free-text label:

```bash theme={null}
curl -X POST "$IONWORKS_URL/material_property_datasets" \
  -H "Authorization: Bearer $IONWORKS_API_KEY" \
  -F "file=@conductivity.csv" \
  -F "material_id=$MATERIAL_ID" \
  -F "project_id=$PROJECT_ID" \
  -F "name=Conductivity at 25 °C" \
  -F 'columns=[{"name":"c_e","unit":"mol.L-1","source_column_index":0},{"name":"kappa","unit":"S.m-1","source_column_index":1}]' \
  -F "source_pipeline_id=$PIPELINE_ID" \
  -F "source_label=Fitted from 25 °C EIS sweep"
```

To change or clear provenance on an existing dataset, `PATCH` the fields
you want to update; send `null` to clear a source ID or label:

```bash theme={null}
curl -X PATCH "$IONWORKS_URL/material_property_datasets/$DATASET_ID" \
  -H "Authorization: Bearer $IONWORKS_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"source_pipeline_id": null, "source_analysis_id": "'$ANALYSIS_ID'"}'
```

The `MaterialPropertyDataset` records returned by the Python client and the
REST API expose the same four fields, so you can read a dataset's
provenance from anywhere it appears:

```python theme={null}
dataset = client.material_property_dataset.get("mpd_def456")
print(dataset.source_pipeline_id, dataset.source_analysis_id, dataset.source_label)
```

## Python client

The [Python API client](/api-client) exposes materials and their property
datasets as two sub-clients:

* `client.material` — list and retrieve materials.
* `client.material_property_dataset` — list, retrieve, and download property
  datasets, and create, edit, or delete them.

`client.material` is read-only; to create or modify a material itself, use the
UI or the [REST API](#rest-api). Property datasets can be managed from the
client — see [Creating and editing property
datasets](#creating-and-editing-property-datasets).

### Listing and retrieving materials

```python theme={null}
from ionworks import Ionworks

client = Ionworks()

# List materials in a specific project
materials = client.material.list(project_id="proj_xyz789", limit=50)
for m in materials:
    print(m.id, m.name, m.manufacturer, m.product_id, m.project_id)

# Retrieve a single material by ID
material = client.material.get("mat_abc123")
```

`project_id` is required when listing materials — every material is scoped
to a project, and the list endpoint returns the materials owned by that
project. Each material record also carries its `project_id`. When the client
has a default project configured, `project_id` falls back to it; without
either, `list()` raises a `ValueError`.

Filtering, ordering, and pagination all happen server-side, so you don't need
to fetch a page and match locally:

```python theme={null}
# Exact match on name
nmc = next(iter(client.material.list(name_exact="NMC811")), None)

# Case-insensitive partial match, newest first
graphites = client.material.list(
    name="ilike.%graphite%",
    order_by="created_at",
    order="desc",
)
```

Text filters (`name`, `manufacturer`, `product_id`) take a bare value for an
exact match, or an operator prefix such as `"ilike.%graphite%"` for a partial
one. `name_exact` is shorthand for an exact match and cannot be combined with
`name`. Time filters `created_at` and `updated_at` accept the same operator
form (e.g. `"gte.2026-01-01"`) and have `_gt` / `_lt` variants for range
bounds. Sort with `order_by` (`name`, `manufacturer`, `created_at`, or
`updated_at`) and `order` (`asc` or `desc`).

### Finding which cells use a material

To go the other way — from a material to the [cell specifications](/core-concepts/cells)
that use it — filter `client.cell_spec.list` by material. The match happens in
the database across all five component slots, so don't fetch every spec and
inspect its components yourself.

```python theme={null}
# Every spec in the project that uses this material, in any slot
specs = client.cell_spec.list(material_id="mat_abc123", project_id="proj_xyz789")

# Several materials at once — matches a spec using any of them
specs = client.cell_spec.list(
    material_id=["mat_abc123", "mat_def456"], project_id="proj_xyz789"
)

# Restrict the search to one slot
specs = client.cell_spec.list(cathode_material_id="mat_abc123", project_id="proj_xyz789")
```

A per-slot parameter is available for each slot — `anode_material_id`,
`cathode_material_id`, `electrolyte_material_id`, `separator_material_id`, and
`case_material_id`. Pass `exclude_cell_spec_id` to drop one spec from the
results.

A material reverse lookup is project-scoped, so it needs a `project_id` — it
falls back to the one set on the client or `IONWORKS_PROJECT_ID`, and raises if
neither is set. Because the filtering happens in the database, `total` counts
every matching spec rather than the rows on the current page, so page through
the results when there may be more than one page.

### Finding cells related by material

Given one spec, `related_specs` finds the others that share a component material
with it — useful for "what else did we build with this cathode?"

```python theme={null}
related = client.cell_spec.related_specs("spec_abc123")

# Compare slot-for-slot instead: same anode, or same cathode
related = client.cell_spec.related_specs("spec_abc123", slots=["anode", "cathode"])
```

By default any of the source spec's slot materials matches in any slot. Passing
`slots` compares each named slot only against the same slot on the source spec.
The source spec is excluded from the results unless you pass
`exclude_self=False`, and `include_components=True` returns each spec with its
nested component and material data.

### Listing property datasets for a material

```python theme={null}
datasets = client.material_property_dataset.list(
    material_id="mat_abc123",
    project_id="proj_xyz789",  # optional: only datasets in this project
    limit=100,
)
for d in datasets:
    print(d.id, d.name, d.data_version, [c.name for c in d.columns])
```

Each `MaterialPropertyDataset` exposes its `columns` (a list of `ColumnSpec`
records with `name`, `unit`, and `source_column_index`), the `data_version`
that bumps on every edit, and per-column `nan_counts`.

### Downloading dataset values

`get_data()` downloads the full dataset and returns it as a DataFrame in the
configured [DataFrame backend](/api-client#dataframe-backend):

```python theme={null}
dataset = client.material_property_dataset.get("mpd_def456")
print(dataset.name, dataset.data_version)

# Map column name to physical unit, e.g. {"c_e": "mol/L", "kappa": "S/m"}
units = {col.name: col.unit for col in dataset.columns}

# Pull all rows as a DataFrame
df = client.material_property_dataset.get_data("mpd_def456")
df.head()
```

### Creating and editing property datasets

`create()` uploads tabular data — a polars or pandas DataFrame, or a
column-name-to-values dict — as a new dataset. Values are stored as floats,
with any non-parseable cell recorded as NaN:

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

df = pl.DataFrame({"c_e [mol/L]": [0.1, 0.5, 1.0], "kappa [S/m]": [0.3, 0.9, 1.2]})

dataset = client.material_property_dataset.create(
    material_id="mat_abc123",
    name="Conductivity vs concentration",
    data=df,
)
```

Each column's display name and unit come from its `columns` specs. When
`columns` is omitted they are inferred from a trailing `[unit]` in the column
name, as above; pass explicit specs to control the units directly.

To edit an existing dataset:

```python theme={null}
# Swap the stored file, keeping the existing column specs
client.material_property_dataset.replace_file("mpd_def456", data=new_df)

# Patch metadata only
client.material_property_dataset.update("mpd_def456", name="Conductivity (revised)")

# Remove it
client.material_property_dataset.delete("mpd_def456")
```

<Note>
  `replace_file()` never infers columns, so it cannot silently erase stored
  units. When you omit `columns` it checks that the replacement's column layout
  matches the stored specs and raises a `ValueError` on a mismatch rather than
  mislabelling the data. Pass `columns` explicitly to change the specs along
  with the file.
</Note>

`get_download_url(dataset_id, kind="parquet")` returns a short-lived signed URL
for the processed parquet; pass `kind="original"` for the file as uploaded.

## REST API

Material property datasets are managed under
`/material_property_datasets`. Materials themselves are managed under
`/materials`.

### Upload a dataset

`POST /material_property_datasets` accepts a multipart form:

| Field                       | Description                                                                                |
| --------------------------- | ------------------------------------------------------------------------------------------ |
| `file`                      | The CSV or parquet file to upload.                                                         |
| `material_id`               | ID of the parent material.                                                                 |
| `project_id`                | ID of the project this dataset is scoped to.                                               |
| `name`                      | Human-readable dataset name.                                                               |
| `columns`                   | JSON array of column specs (see below).                                                    |
| `no_header`                 | `true` if the CSV has no header row. Defaults to `false`.                                  |
| `source_pipeline_id`        | Optional. ID of the pipeline run this dataset was produced by.                             |
| `source_simple_pipeline_id` | Optional. ID of the simple pipeline this dataset was produced by.                          |
| `source_analysis_id`        | Optional. ID of the [analysis](/data/analyses) this dataset was derived from.              |
| `source_label`              | Optional. Free-text provenance note (e.g. a paper reference or manual-upload description). |

At most **one** of the three `source_*_id` fields may be set on a given
dataset. Set `source_label` alone when the provenance is not a linkable
Ionworks record — for example, "manually digitized from Smith et al. 2023".
See [Tracking dataset provenance](#tracking-dataset-provenance) for the full
model.

Each column spec is an object:

```json theme={null}
{
  "name": "c_e",
  "unit": "mol.L-1",
  "source_column_index": 0
}
```

`source_column_index` is the 0-based position of the column in the uploaded
file. It is required even when the file has a header row — names are matched
by position, then renamed to the `name` you provide.

Example upload with `curl`:

```bash theme={null}
curl -X POST "$IONWORKS_URL/material_property_datasets" \
  -H "Authorization: Bearer $IONWORKS_API_KEY" \
  -F "file=@conductivity.csv" \
  -F "material_id=$MATERIAL_ID" \
  -F "project_id=$PROJECT_ID" \
  -F "name=Conductivity at 25 °C" \
  -F 'columns=[
    {"name": "c_e", "unit": "mol.L-1", "source_column_index": 0},
    {"name": "kappa", "unit": "S.m-1", "source_column_index": 1}
  ]'
```

The response is the new dataset record, including its `id`, `storage_path`,
`data_version`, and per-column `nan_counts`.

### List datasets for a material

```bash theme={null}
curl "$IONWORKS_URL/material_property_datasets?material_id=$MATERIAL_ID&project_id=$PROJECT_ID&limit=100&offset=0" \
  -H "Authorization: Bearer $IONWORKS_API_KEY"
```

Returns a paginated list of dataset records.

### Fetch dataset values as JSON

```bash theme={null}
curl "$IONWORKS_URL/material_property_datasets/$DATASET_ID/data?max_points=500&x_col=c_e&x_min=0&x_max=2" \
  -H "Authorization: Bearer $IONWORKS_API_KEY"
```

Returns the dataset as a column-major JSON object:

```json theme={null}
{
  "c_e": [0.5, 1.0, 1.5, 2.0],
  "kappa": [0.42, 0.71, 0.89, 0.95]
}
```

`max_points` downsamples uniformly so large datasets remain responsive to plot.
`x_col`, `x_min`, and `x_max` restrict the response to a range of one column
— useful for zooming charts.

### Download the underlying file

Use `GET /material_property_datasets/{id}/file` to redirect to a short-lived
signed URL for the file, or `GET /material_property_datasets/{id}/download-url`
to receive the URL as JSON (handy when you want to open it from the browser).
Pass `?kind=parquet` (default) to download the processed parquet, or
`?kind=original` to download the raw file you uploaded.

### Update metadata, replace the file, or delete

| Endpoint                                      | Description                                                                                                                     |
| --------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------- |
| `PATCH /material_property_datasets/{id}`      | Rename the dataset and/or re-declare its columns. When columns change, the parquet is rebuilt from the preserved original file. |
| `PATCH /material_property_datasets/{id}/file` | Replace the data file. Optionally update `name`, `columns`, and `no_header` in the same request.                                |
| `DELETE /material_property_datasets/{id}`     | Delete the dataset and its stored files.                                                                                        |

## Related

* [Cells](/core-concepts/cells) — materials are referenced from the anode,
  cathode, electrolyte, and separator components of a cell specification.
* [Parameter interpolants](/build/interpolants) — turn measured property data
  into lookup-table parameters inside a parameterized model.
* [Electrolyte transport from a dataset](/pipelines/direct-entries#building-electrolyte-transport-from-a-material-dataset)
  — build concentration-dependent electrolyte transport parameters from a
  property dataset and drop them into a pipeline.
* [Data overview](/data/overview) — how experimental data is organized in
  Ionworks Studio.
