# Changelog — ionworks-api All notable changes to this package are documented here. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and this package follows [Semantic Versioning](https://semver.org/). For platform-wide release notes (Studio, pipeline, SDK, and more), see [docs.ionworks.com/changelog](https://docs.ionworks.com/changelog). ## [0.40.0] - 2026-11-04 ### Added - `parameterized_model.get()` and `simulation.get()` take `summary=True` to leave out parameter values and simulation data (#2694) - A design parameter can be a `ParameterTable` (`{x, y, interpolator}`) for a function-valued parameter, in `client.simulation.protocol()` and in `protocol_batch()` conditions (#2689) - `client.parameterized_model.get_parameter_values()` takes `names` and `match` to return only some parameters (#2689) - Parameterized-model create, update and version writes accept `summary=True` to return metadata without parameter values or model configuration (#2688) - `client.pipeline.list_elements()` lists a pipeline's elements with their config and result (`summary=True` returns metadata only), `get_element_job_config()` returns one element's full config, and `client.pipeline.get()` returns `project_id` (#2688) - `client.simulation.get_steps()` downloads a completed run's step summary without its time series (#2688) - `client.parameterized_model.get_parameter_values(resolve=False)` returns calculated parameters as calculations, to copy into another model (#2688) - `client.parameterized_model.create_version()` saves a new version of a model with some parameters changed, and `list_versions()` lists a model's versions (#2688) ### Fixed - A cell specification's `url` opens its page under Cells, and a cell instance's opens its page under its specification, instead of the old data-section routes (#2677) ## [0.39.0] - 2026-11-02 ### Breaking changes - Project records carry your own `my_role` and `my_permissions` instead of `user_project_roles`, every member's role with all its permissions; `client.organization.members()` lists who is in which project (#2546) ### Added - `client.simulation.protocol()` and `protocol_batch()` take `simulation_settings`, numerics for those runs only: `solver`, `var_pts`, `submesh_types` and `spatial_methods` (#2580) - Per-run `simulation_settings` also take `discretisation_kwargs` (#2598) - `client.protocol_group` manages protocol groups (`ProtocolGroup`), and protocols take `protocol_group_id` on create, update and list; `None` on update removes a protocol from its group (#2551) - `client.metric_suite` trains Gaussian-process surrogates of a metric suite's KPIs and predicts from them: `create_surrogate`, `wait_for_surrogate`, `predict` and related methods (#2512) - Records fetched by id carry `url`, their page in the web app, and `api_url`, their API endpoint (#2587) - Sites carry an IANA `timezone`, defaulting to `America/New_York` (#2639) - `client.cell_measurement.create()` adds the cycle-cumulative capacity and energy columns (`Cycle discharge capacity [A.h]` and the like) to a time series that has the per-step columns but not these, before uploading it; columns you supply are kept (#2636) ### Changed - Requires `polars>=2.1.0`, up from `polars>=1.43.0` (#2657) ## [0.38.0] - 2026-10-23 ### Added - `client.model_builder` manages recipes that turn a cell specification into a model's parameters: check a cell spec against one, preview a mapping, `dry_run` it, and build or save a parameterized model (#2111) - `client.model.create_from_zoo(name, cell_spec_id=...)` uploads a PyBaMM model-zoo model and creates its parameterized model in one call (#2485) - `client.parameterized_model.create(None, data)` creates a parameterized model with no cell spec, in a project; `parameters` also accepts a `pybamm.ParameterValues` or a parameter-set name (#2485) - `client.simulation.list()` takes `protocol_id`, `state` (`queued`, `running`, `completed` or `failed`), `summary`, `limit` and `offset`; `protocol_id` combines with `parameterized_model_id` or `study_id`, and with `limit` or `offset` it returns a `PaginatedList` carrying `.total` (#2425) - Simulation records carry `protocol_id` and `protocol_name` (#2425) - `SimulationResponse.already_existed` is True when an identical run, finished or still running, was returned instead of starting a new one; `SimulationResponse.protocol_id` is the saved protocol the run used (#2425) - `client.parameterized_model.list_by_project(summary=True)` lists models without their parameter values (#2425) ### Changed - Models belong to a project: `client.model.list()` lists the client's project unless given `project_id=`, and `create` and `upload_custom` default to the client's project (#2332) - `client.search.query(project_id=...)` also restricts parameterized models, protocols and cell data to that project, keeping rows with no project; before, it narrowed only studies, optimizations and pipelines (#2425) - `client.protocol.convert(..., target="biologic_bttest")` needs `nominal_capacity_ah` when the protocol uses C-rate steps or cutoffs, as `neware` does: BioLogic C-rates are now written as absolute currents (#2542) ### Fixed - Large uploads no longer fail when throughput to storage is low: multipart uploads send 16 MiB parts two at a time, and a part that times out is retried (#2468) ## [0.37.1] - 2026-10-20 ### Changed - `client.pipeline.create` refuses a pipeline whose `DirectEntry` reads a pipeline (`pipeline_id`) or cell specification (`cell_spec_id`) from another project, with a 403 naming the element. Previously such a reference was accepted. Create the pipeline in the source's project, or enter the values directly. - Requires `pandas>=2.2.2` and `pyarrow>=16.0.0`. Earlier releases do not import alongside numpy 2. ## [0.37.0] - 2026-10-09 ### Breaking changes - Cell instances no longer have a free-text `batch` field. Manufacturing lots are now their own records, and an instance points at one through `batch_id`. `client.cell_instance.create()`, `create_or_get()` and `update()` raise a `ValueError` naming the replacement if `data` contains `batch`. To migrate, create or look up the lot with `client.cell_batch.create_or_get(cell_spec_id, {"name": ...})` and pass `batch_id=.id`. - Simulation configs are checked for unknown keys. `client.simulation.protocol()` and `protocol_batch()` raise a `ValueError` naming any misspelt or misplaced key, where it was previously ignored and the simulation ran without it. ### Added - `client.cell_batch` manages cell batches (manufacturing lots of one cell specification): `list()`, `get()`, `create()`, `create_or_get()`, `update()` and `delete()`. Deleting a batch unlinks its cells and never deletes them. The new model `CellBatch` is exported from `ionworks`. - `client.cell_instance.list(..., batch_id=...)` returns only the cells in one batch. Instances carry `batch_id` and a read-only `batch_name`. - Measurement tags. `CellMeasurement.tags` holds a measurement's tags; `client.cell_measurement.list_tags(project_id)` lists the tags a project uses with their counts; `add_tags()` and `remove_tags()` change tags on up to 1000 measurements at once; and `list()` and the new `list_by_project()` filter by `tags_all` / `tags_any`. The new model `MeasurementTagUsage` is exported from `ionworks`. - `client.simulation.protocol()` and `protocol_batch()` accept `protocol_id` to run a saved protocol, in place of `protocol_experiment`. `project_id` then defaults to the protocol's own project. - `client.simulation.protocol_batch()` accepts `conditions`, an explicit list of runs each with its own `experiment_parameters` and `design_parameters`, as an alternative to `design_parameters_doe`. - `client.simulation.get_statuses(simulation_ids)` returns the status of many simulations in one request. - `client.cell_measurement.time_series(..., row_range=(start, stop))` downloads only the rows in that window. - `client.simulation.delete(simulation_id)` and `client.parameterized_model.delete(cell_spec_id, parameterized_model_id)`. ### Changed - A pipeline data fit's result carries its best cost, so `fit.cost` is set where `SimplePipeline` users previously read `result["cost"]`. ### Deprecated - Every `client.simple_pipeline` method emits a `DeprecationWarning` naming its `client.pipeline` equivalent. SimplePipeline will be removed no earlier than November 2026, and past runs cannot be fetched after that. ## [0.36.0] - 2026-09-30 ### Added - Organization membership management on `client.organization`: `members()`, `get_member(email)`, `invite(email, role="Member")`, `set_role(user_id, role)`, `deactivate(user_id)`, `reactivate(user_id)`, `resend_invite(user_id)` and `remove(user_id)`. The new models `OrganizationMember`, `ProjectMembership` and `InviteResult` are exported from `ionworks`. - Project membership management on `client.project`: `roles()`, `add_member(project_id, user_id, role=...)`, `invite_member(project_id, email, role=...)`, `update_member(project_id, user_id, role)` and `remove_member(project_id, user_id)`. `role` is a project role name (case-insensitive) or id. The new model `ProjectRoleInfo` is exported from `ionworks`. - `client.analysis.create(..., plots=[...])` saves plot presets, which the web app offers as views of the analysis table. `Analysis.plots` returns them, and `update()` accepts `plots`. ### Changed - `ProtocolSimulationBatchRequest.max_backward_jumps` documents the server default of 1000 when it is left unset. ## [0.35.0] - 2026-09-29 ### Deprecated - `client.job.create()` now emits a `DeprecationWarning`. The server no longer accepts direct job submissions and answers with a 410 error. Start work through `client.pipeline`, `client.simple_pipeline`, `client.simulation` or `client.ecm`. `get`, `list` and `cancel` are unaffected. ### Changed - Raised the `pybamm` lower bound to `>=26.9.0.0`, and the `boto3` lower bound in the `large-uploads` extra to `>=1.43.98`. ## [0.34.0] - 2026-09-24 ### Added - `client.analysis.find_one(measurement_id, analysis_id=..., analysis_type=..., name=...)` returns the single analysis on a measurement matching every field given. It raises when no analysis or more than one matches, listing the candidates so the right selector can be copied. ### Changed - `client.simulation.get_result()` downloads the time series and steps as parquet rather than one JSON body. It returns the same `SimulationResult` shape as before. A 404 can now also mean an older result that has not yet been converted; retry after a moment. ### Fixed - Files at or above 32 MiB now prefer S3 multipart uploads when `ionworks-api[large-uploads]` is installed. If a signed upload receives a storage-edge timeout, the client makes one multipart attempt from the same file position. ## [0.33.0] - 2026-09-22 ### Changed - App URLs built by `client.urls` no longer carry the `/dashboard` prefix: a project route is now `/projects/{project_id}/...`. The old URLs redirect, but anything comparing against a hardcoded `/dashboard/...` string needs updating. ### Fixed - An `extra_variables` name the model does not carry is refused when the simulation is submitted, rather than failing later during the run. ## [0.32.0] - 2026-09-14 ### Changed - `client.pipeline.wait_for_completion()` and `client.simple_pipeline.wait_for_completion()` now raise `IonworksError` on a cancelled pipeline as well as a failed one when `raise_on_failure=True` (the default). The status is carried into the message (`Pipeline canceled`). Pass `raise_on_failure=False` to get the record back instead, as before. - `PipelineSubmissionResponse` and `SimplePipelineSubmissionResponse` carry server-derived `is_terminal` and `is_failed` flags, so the client no longer decides which statuses are terminal. A status this SDK has never heard of (added server-side later) is classified correctly without an SDK upgrade. Both fields are required, so this version needs a backend that publishes them. ### Fixed - `client.pipeline.wait_for_completion()` stopped polling only on `completed` or `failed`, so a cancelled pipeline kept polling until `timeout` elapsed and then raised `TimeoutError`. It now returns (or raises, see above) as soon as the pipeline is cancelled. ## [0.31.0] - 2026-09-05 ### Added - `client.material_property_dataset.to_prior()` centres a fit prior on a scalar already stored in a property dataset, so a measured value feeds a fit without being retyped as a literal: ```python prior = client.material_property_dataset.to_prior( dataset.id, column="particle radius", parameter_name="Negative particle radius [m]", rel_std=0.1, ) fit = iws.DataFit(objectives=..., priors={prior.name: prior}) ``` A scalar carries no spread, so exactly one of `std` or `rel_std` is required. `distribution="lognormal"` suits a strictly positive parameter spanning orders of magnitude, such as a diffusivity or conductivity. - `ParsedProtocol.nominal_capacity_ah` reports the cell rating declared in a protocol file's header, when the file carries one. ## [0.30.0] - 2026-08-31 ### Added - Pipeline results decode into the typed result objects from `ionworks-schema`, so a fitted parameter or a figure is reachable directly off the result rather than by fetching job metadata and parameter traces by hand: ```python result = client.pipeline.result(pipeline_id) fit = result.element("fit") fit.parameter_values["Positive particle diffusivity [m2.s-1]"] fit.plot_fit_results() fit.plot_trace() ``` `PipelineResponse.element(name)` and `.results` expose the typed objects, and `SimplePipeline` gains `result_object`. This is additive: the raw `.result` and `.element_results` payloads are unchanged. - `result.element(name).series` returns the full-resolution per-objective model-vs-data channels, `{objective: {channel: [values]}}`, for data-fit and validation elements. Use `.overlay` for the decimated data behind figures and `.series` when you want the raw numbers to save or analyse. - Plotting from a result object needs matplotlib, which is not installed by default: `pip install 'ionworks-schema[plot]'`. ### Changed - Requires pybamm 26.8.0.0 or newer. ## [0.29.0] - 2026-08-25 ### Added - `client.cell_measurement.extend(measurement_id, time_series)` appends a delta time series to an unfinished measurement — one whose `end_time` is not yet set — so a running test can be uploaded incrementally instead of re-sending the whole series. Pass only the new rows: their `Time [s]` must start after the last existing sample and the column set must match the existing series exactly. Steps and cycles are recomputed server-side from the stitched result, so never pass steps. The call waits for that processing by default; pass `wait_for_processing=False` to return as soon as the upload is confirmed. - Uploads larger than 512 MiB now use S3 multipart, making multi-gigabyte raw files and time series upload reliably. This needs an optional dependency: install it with `pip install 'ionworks-api[large-uploads]'`. Without the extra, files up to 1 GiB still upload via the existing single request; above that the extra is required. - `client.raw_data.create()` accepts a `timeout`, as a `(connect, read)` pair or a single value for both. On the multipart path the timeout applies per part rather than to the whole transfer. - Measurements carry a `protocol_id`, referencing the protocol a run executed. Set it through `cell_measurement.create()` or `update()` like any other field. The protocol must belong to the measurement's own project. It is preferred over the free-form `protocol` dict, which stays available for imported data with no executable protocol behind it; setting both records what the run was meant to execute plus the conditions it actually ran under. - `client.pipeline` gained `update()`, `cancel()` and `delete()`, matching `client.simple_pipeline`. Previously a pipeline submitted through `client.pipeline` could not be renamed, cancelled or removed from Python at all, even though the API supported all three. - `client.pipeline.list()` accepts the full filter set the endpoint supports — `id`, `name`, `description`, `status`, `created_by_email`, `created_at`, `updated_at`, the `_gt`/`_lt` variants, `order_by`, `order` and `offset` — rather than only `project_id` and `limit`. ### Changed - `job.get_plot_data()` accepts up to 80000 points per trace, raised from 10000, so a long run can be fetched at full resolution in one call. The default is unchanged at 2000. - Both `client.pipeline.list()` and `client.simple_pipeline.list()` now return `PaginatedList`, so the two clients agree. It behaves as a list (iteration, indexing, `len()`, truthiness) and carries `.items`, `.count` and `.total`, so loops and the three attributes keep working unchanged. - **Behaviour change:** `client.simple_pipeline.list()` previously returned a Pydantic model, so pydantic-only methods on the result — `model_dump()`, `model_dump_json()` — no longer exist. Use `.items` and serialise those. The underlying `SimplePipelineListResponse` class is also gone; it was never exported from `ionworks`, so only code importing it from the submodule directly is affected. `client.pipeline.list()` gains `.total`, which it previously discarded. ## [0.28.0] - 2026-08-18 ### Added - `client.channel.update()` accepts a `service_scheduled_until` datetime, recording when a booked outage is expected to end (must be after `service_scheduled_at`). `client.channel.list_incidents()` reads back a channel's out-of-service history, newest first, including whether the current outage is overdue to return. - `client.protocol.convert()` accepts `verify=True` to ask the server to round-trip the emitted file through its parser and reject the conversion if the protocol no longer means the same thing (a dropped loop count, goto, or safety bound). Reliable today for `biologic_bttest` and `novonix`; the other writers still report benign artifacts as differences. - New validator `validate_charge_discharge_column_direction` detects a cycler export with the charge/discharge capacity (or energy) column labels inverted, which is otherwise invisible to every cumulative-reset and magnitude check. - `client.protocol.convert()` accepts `"arbin_sdx"` as a `target`, writing the Arbin MITS Pro 8+ `.sdx` dialect alongside the existing MITS Pro ≤7 `.sdu` writer (`"arbin"`, unchanged and not deprecated). ### Fixed - `client.cell_specification.list(name_exact=...)` (and the internal lookup `create_or_get` falls back to) is now project-scoped, matching spec names being unique per project rather than per organization — an org-wide exact match could previously return a different project's spec or misreport a locally-unique name as ambiguous. - `validate_charge_discharge_column_direction`'s same-sign-current check no longer discards a step's vote when a single boundary sample inherited from the preceding step pollutes its min/max. ## [0.27.0] - 2026-08-11 ### Breaking changes - `client.material.list()` is now project-scoped: it returns only the materials in one project, and raises `ValueError` when no `project_id` is given and the client has no default. Previously it listed materials across the whole organization, and `project_id` only scoped each material's `property_count`. Pass `project_id=` explicitly, or set a client-wide default (via `IONWORKS_PROJECT_ID`), and expect a narrower result set than before. ### Added - Write methods on `client.material_property_dataset`: `create()` uploads a DataFrame (serialised to parquet, inferring `columns` from a trailing `name [unit]` header when not given), `replace_file()` swaps the stored file, `update()` patches metadata, and `delete()` removes a dataset. `get_download_url(kind="parquet"|"original")` returns a signed URL. `replace_file()` does not infer columns, so it cannot erase stored units: when `columns` is omitted it checks that the replacement's layout matches the stored specs and raises `ValueError` on a mismatch rather than mislabelling the data. - Server-side filtering, ordering, and pagination on `client.material.list()`: `name`, `manufacturer`, `product_id`, `created_at` / `updated_at` (plus `_gt` / `_lt` range bounds), `order_by`, and `order`. Text filters 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 exact equality and cannot be combined with `name`. - An `ionworks` console script for managing the Agentic Toolkit: `ionworks skills install` (aliased as `update`) downloads the toolkit and installs it for your agents, removing a previous install so retired skills do not linger, and `ionworks skills list` shows what is installed. Both take `--global` to target the user-level directory instead of the current project, and `--agent` to name target agents explicitly instead of auto-detecting. ## [0.26.0] - 2026-08-10 ### Added - Material reverse lookup on `cell_spec.list(...)`: `material_id` returns the specs referencing a material through any component slot, and accepts a list to match any of several materials. Per-slot `anode_material_id`, `cathode_material_id`, `electrolyte_material_id`, `separator_material_id`, and `case_material_id` restrict the search to one slot, and `exclude_cell_spec_id` drops a spec from the results. A reverse lookup is project-scoped, so it needs a `project_id` (falling back to the client's, or `IONWORKS_PROJECT_ID`). Filtering happens in the database, so `total` counts every matching spec rather than the rows in the page. - `cell_spec.related_specs(cell_spec_id, ...)` returns the other specs that share a component material with a given spec. By default any of its slot materials matches in any slot; pass `slots=["anode", "cathode"]` to compare each named slot only against the same slot on the source spec. `exclude_self` (default True) and `include_components` control self-exclusion and whether nested component and material data comes back. - `client.planned_measurements.auto_schedule_proposal(...)` proposes the earliest channel reservations for a list of requested tests, in priority order. The search is unbounded in time, so each request gets the earliest start that actually fits; an optional `start_after` defers the whole batch when an operator cannot begin immediately. The result is transient — nothing is reserved until it is applied. - `client.planned_measurements.apply_auto_schedule_proposal(...)` commits the assignments you selected from a reviewed proposal. The API re-checks each one against its proposal-time version and current channel availability, and on a stale or conflicting proposal rolls back the whole batch and raises `IonworksError` with `error_code == "SCHEDULE_STALE"` — generate a fresh proposal and retry. - `AutoScheduleProposal` and `AutoScheduleAssignment` models, exported from the top-level `ionworks` namespace. `AutoScheduleAssignment.is_scheduled` tells you whether an assignment is complete and safe to apply; incomplete ones carry an `unscheduled_reason` explaining that no channel in the project can take the test. ## [0.25.0] - 2026-08-06 ### Added - `client.search.query(...)` runs one query across every entity type in the organization — projects, models, cell specifications and instances, measurements, parameterized models, templates, materials, studies, optimizations, and pipelines — returning ranked `SearchResult` records in a `SearchResponse`. Name fields match on case-insensitive substring and free-form text on prefix full-text search, with substring matches ranked first. `per_type` keeps one entity type from crowding out the rest, and `entity_types` / `project_id` narrow the sweep; search spans the whole organization by default rather than the client's `project_id`. - `client.cycler` tracks hardware service: `list_service_events`, `start_service`, and `complete_service`, plus the `CyclerServiceEvent` model. Starting service opens an event and an incident on every channel so the whole instrument reads as down; channels already out of service keep their own more specific incident and stay out when the service completes. - `client.protocol` now covers the whole protocol lifecycle rather than validation and conversion alone: - **Saved protocols** — `list`, `get`, `find_by_name`, `create`, `create_or_get`, `update`, `delete`, plus `human_readable` and `source_protocol` renderings. Protocols are project-scoped and content-addressed, so saving one that already exists returns the existing record instead of a duplicate. - **`parse_file`** — turn a vendor cycler protocol file (Maccor, Arbin, Neware, Novonix, BioLogic, …) into UCP, reporting any drive cycles or subroutines the file referenced but did not carry. - New models: `Protocol` and `ParsedProtocol`, exported from the package root (`from ionworks import Protocol, ParsedProtocol`). - `protocol.list` gained `name` / `name_exact` / `created_by_email` / `created_after` / `created_before` / `updated_after` / `updated_before` / `order_by` / `order`, matching the other list clients. Filtering, ordering and pagination are applied by the database, so `total` counts every matching protocol rather than the rows in the page. - `protocol.find_by_name` now matches server-side. It previously scanned an unpaginated listing, so it returned `None` for any protocol past the server's row cap. It also raises `ValueError` when several protocols in the project share the name — protocols are deduplicated on their body, not their name, so returning an arbitrary match could silently simulate the wrong one. ### Changed - Protocol responses no longer carry `access_level`. Every protocol reachable through the API is project-scoped, so the field could only ever read `"project"`. ### Fixed - `simulation.protocol` now sends the resolved `project_id` to the batch endpoint, so a run that builds a quick model succeeds instead of being rejected for having no project to own the parameterized model. The `project_id` is resolved as before — from the config, the client, or `IONWORKS_PROJECT_ID` — so no call site has to change. ## [0.24.1] - 2026-08-05 ### Fixed - `jobs.get_posterior_samples` documented a narrower contract than it has. Every sampler populates the sample keys, not only Bayesian ones; it is fits driven by a conventional optimizer (`CMAES`, `ScipyMinimize`), along with optimization and validation jobs, that return an empty dict. The docstring now also states that `samples` values are nested `(starts, iterations)` for a multistart fit and a flat per-iteration list for a single start — so index the iteration axis last (`[..., burnin:]`) to handle both — that `sample_costs` is shaped like one parameter's chain, and that `sample_burnin` is `None` for samplers with no burn-in concept (`GridSearch`, `PointEstimateSampler`). Behaviour is unchanged. ## [0.24.0] - 2026-08-03 ### Added - `protocols.convert` accepts `nominal_capacity_ah`, the rated cell capacity in amp-hours. It is required for the `neware` target when the protocol uses C-rate steps or cutoffs, because Neware sets current in absolute mA and has no C-rate mode. Other targets express C-rate natively and ignore it. ### Changed - Raised the `requests` lower bound to `>=2.34.2`. ### Fixed - `jobs.get_plot_data` now calls `GET /pipelines/datafits/{job_id}/plot_data`. The previous unprefixed path returned a 404, so fetching model-vs-data plot traces for a data-fit job failed. ## [0.23.0] - 2026-07-29 ### Added - `ProtocolSimulationBatchRequest` accepts a `project_id`. Parsed protocols are created as project-scoped experiment templates, so the backend now requires the project. When omitted, it falls back to the `project_id` configured on the `Ionworks` client (or the `IONWORKS_PROJECT_ID` environment variable). ## [0.22.0] - 2026-07-28 ### Breaking changes - `channel.create` no longer accepts `out_of_commission` and rejects it with `BAD_REQUEST`: a channel is always commissioned in service. Use `channel.update` to take one out of service afterwards, which records the outage in the channel's incident history. ### Added - Channel outage history. Passing `out_of_commission` to `channel.update` now records an incident, and may be accompanied by `incident_category` (one of `hardware_failure`, `maintenance`, `calibration`, `decommissioned`, `other`; defaults to `other`) and `incident_notes` when taking a channel out of service, or `resolution_notes` when returning it. Re-sending the value a channel already has is a no-op rather than a new outage. - `MeasurementProcessingError`, raised when the server cannot process an uploaded `time_series` measurement. Its `failures` attribute maps each affected measurement id to the reason, so a batch can report several at once. - `cell_measurement.wait_for_processing`, plus a `wait_for_processing` argument (default `True`) on the upload methods, so an upload no longer returns a measurement that looks created but holds no usable steps. ### Changed - `channel.update` now also raises `CONFLICT` when a channel's service state was changed concurrently; re-read the channel and retry. ## [0.21.0] - 2026-07-24 ### Added - `simulation_settings` support (a persistent bag of pybamm mesh and solver settings) on the simulation, study, and parameterized-model create/update paths. ### Changed - Aligned `SimplePipeline.create` with `Pipeline` and wrapped datafit `plot_data`. ## [0.20.0] - 2026-07-22 ### Added - New ``planned_measurement`` client for requesting, scheduling, and cancelling future lab measurements, with a matching skill (#1292). A planned measurement requires a named protocol (``protocol_id``) and a cell specification. - ``channel.watch`` to follow the live test running on a channel, and an org-wide ``channel.list_all`` plus channel/measurement detail helpers. - ``cell_specification_name`` on ``LabMeasurementSummary`` so lab measurement listings can show the cell spec without a second lookup. - Source tracking for material-property datasets and analyses: created records now carry their originating source (#1276). ### Fixed - List endpoints now honour the filters the SDK already sends, so server-side narrowing is applied instead of being silently dropped (#1337). ## [0.19.0] - 2026-07-20 ### Added - New ``lab`` client for answering equipment / occupancy questions: lab status, utilization, free and stale channels, and which measurement is on a channel (#1262, #1274). - ``JobClient.get_posterior_samples`` to retrieve MCMC posterior sample chains from Bayesian sampler datafit job metadata (#1227). - ``validate_measurement_timing`` warning-severity checks for measurement ``start_time`` / ``end_time`` (future timestamps, end-before-start, and wall-clock vs data duration mismatch) (#1246). ### Changed - Raised the ``pydantic`` dependency floor from ``>=2.12.5`` to ``>=2.13.4`` (#1284). ### Fixed - Measurement artifact cache TTL is now evaluated per file, not per directory, so a fresh write no longer resurrects an expired sibling artifact (#1265). ## [0.18.0] - 2026-07-16 ### Added - New ``site``, ``cycler``, and ``channel`` clients for managing test equipment: create, list, retrieve, update, and delete sites, cyclers, and their channels. Cell measurements can now be linked to a channel via a ``channel_id`` field (#1163). ## [0.17.0] - 2026-07-14 ### Changed - Raw-data uploads now use a three-step signed-URL flow (``initiate-upload`` → direct ``PUT`` to storage → ``confirm-upload``) instead of multipart form uploads; the file body no longer passes through the API server, so large raw cycler files upload more reliably. The content type is derived from the filename (#1204). ## [0.16.0] - 2026-07-14 ### Added - New ``analysis`` client for creating, listing, and retrieving measurement-scoped analyses, with results returned as dataframes (#1171). - Organization- and project-scoped raw data storage: a new ``raw_data`` client for uploading, listing, and downloading raw cycler files, plus supporting additions on ``cell_measurement`` (#1179). - Submitted pipelines now accept ``file:`` and ``folder:`` references to raw data, validated client-side before submission (#1040). ## [0.15.1] - 2026-07-13 ### Changed - Internal: model enums now subclass ``StrEnum`` instead of ``(str, Enum)``; runtime string values are unchanged (#1174). ## [0.15.0] - 2026-07-10 ### Added - ``client.organization.usage()`` returns the organization's usage and limits for the current calendar-month period (resetting on the 1st). Simulation exposes ``.usage`` / ``.limit`` and compute adds a per-job-type ``.usage_by_type`` breakdown; all values are in hours and a ``None`` limit means that type is unconstrained (#1117). - EIS measurement upload validation: checks the ``Z_Im`` sign convention and impedance magnitude consistency before upload (#1133). ## [0.14.0] - 2026-07-07 ### Breaking changes - ECM ``FitResults.capacity_Ah`` and ``ValidationResults.capacity_Ah`` are now ``list[float]`` (one entry per measurement segment) instead of a single ``float``. Callers reading ``capacity_Ah`` must index the list (validation results are a single-element list) (#1081). ### Added - ECM fits accept a per-measurement ``capacity`` (all-or-none across measurements) and a ``regularization`` smoothness prior on the R0/RC parameter curves; ``submit_fit_from_file`` gains a ``regularization`` argument (#1081). ### Fixed - Corrected the ``cell_spec.create`` example in the cell-specifications user guide to match the API model (#835). ## [0.13.0] - 2026-06-30 ### Added - Pipeline configs now accept ``element_type: "array_data_fit"`` for fits that run independently at each value of an independent variable (e.g. diffusivity vs. stoichiometry, parameter vs. temperature). Configurable with the same fields as ``data_fit``; see the new ``examples/pipeline/array_datafit.py`` example. - ``client.resolve_measurement(cell_specification, cell_instance, measurement)`` — resolve a measurement from human-readable names, walking the spec -> instance -> measurement hierarchy (each level filtered server-side by exact name). Returns the ``CellMeasurement``; raises ``IonworksError`` (404) when a level has no match, or (409) when a name is ambiguous within its parent. ## [0.12.0] - 2026-06-10 ### Breaking changes - ``OptimizationClient`` is decoupled from the jobs API (#861). Optimizations are now first-class resources under ``/optimizations`` rather than ``/optimize``. ``get``/``wait_for_completion`` return the flat optimization resource (with a top-level ``status``) instead of a ``{"optimization": ..., "job": ...}`` dict, ``list`` returns ``{"optimizations": [...], "total": n}`` (no ``jobs`` key), and the terminal states are renamed ``completed``/``cancelled`` → ``succeeded``/``canceled``. Migration: read ``result["status"]`` and ``result["error"]`` directly, and update any status checks to the new state names. ### Added - SDK support for background-job ECM parameterization (#562). - ``JobClient.get_parameter_trace`` accessor for retrieving the parameter trace of a job (#815). - Project-scoped parameterized model listing on the parameterized model client (#806). ### Fixed - Corrected CC-discharge step mislabeling and an unsigned mixed-mode current sign error in data processing (#848). ## [0.11.0] - 2026-06-05 ### Added - New ``SimulationResult`` class (exported from the package root) — a typed result returned by ``SimulationClient.get_result`` exposing ``time_series`` and ``steps`` as DataFrames in the active backend (polars by default, pandas when configured) (#741). ### Fixed - Minor ``Navigator`` documentation fix (#770). ## [0.10.3] - 2026-06-01 ### Added - New ``Navigator`` class (exported from the package root) — a cached, in-memory view over the cell spec → instance → measurement hierarchy that memoises list/fetch calls and returns name-sorted listings for deterministic iteration (#667). ### Changed - Switched the ``polars`` dependency from ``polars-lts-cpu`` to the standard ``polars`` distribution (#768). ## [0.10.2] - 2026-05-29 ### Changed - Relaxed the ``numpy`` dependency bound to allow ``numpy>=2`` (#754). ### Removed - Dropped unused runtime dependencies ``python-dotenv``, ``supabase``, and ``iwutil`` (#752). ## [0.10.1] - 2026-05-28 ### Added - ``SimplePipeline`` submission now accepts runtime options (e.g. solver settings) that are forwarded to the pipeline worker, giving callers finer-grained control over execution without changing the pipeline configuration (#708). ## [0.10.0] - 2026-05-27 ### Added - `MaterialClient` (`client.material`) for listing and retrieving materials. - `MaterialPropertyDatasetClient` (`client.material_property_dataset`) for listing, retrieving, and downloading tabular physical property datasets (e.g. conductivity, diffusivity, transference number vs. concentration). - New public models: `Material`, `MaterialPropertyDataset`, `ColumnSpec`. - Measurement validators now cross-check the ``capacity`` and ``energy`` columns against the integrals of current and power, catching unit and sign-convention errors that previously slipped through (#687). - `client.whoami()` returns the user profile that the configured API key resolves to — the recommended way to debug credential issues and confirm which organization the SDK is authorized as (#626). ### Fixed - Measurement validators now accept parquet files alongside the existing supported formats (#678). - Sign-convention vote is weighted by ``|I| * dt`` instead of row count, so short high-current segments are not drowned out by long rest periods (#682). - The SDK no longer implicitly loads a ``.env`` file on import. Set ``IONWORKS_API_KEY`` in your environment (or pass it explicitly to ``Ionworks(...)``) before constructing the client (#630). - JSON serialization now handles ``datetime`` and pandas ``Timestamp`` values in request payloads so pandas-derived metadata can be passed directly without manual conversion (#720). ## [0.9.0] - 2026-05-14 ### Added - `SimplePipelineClient` and `ionworks.simple_pipeline` module: a lightweight client for running the new SimplePipeline workflow, exposed on the top-level `Ionworks` client (#585). ## [0.8.0] - 2026-05-13 ### Added - `client.ecm` sub-client for ECM (Equivalent Circuit Model) parameterization. Supports three input modes — `fit_from_example` (synchronous demo), `fit_from_file` (uploads a local cycler file), and `fit_from_measurements` (fits measurements already stored in the platform). Authenticated fits run as background jobs; use `client.ecm.wait_for_completion(job)` to block until ready. - New public models exported from `ionworks`: `EcmFitJob`, `FitResults`, `RcPairFit`, `SaveToProjectResponse`. - Multi-measurement fits with per-segment SoC seeds: `fit_from_measurements` accepts a `measurements: list` where each entry can carry its own `start_step`, `end_step`, and `initial_soc`. - Capacity co-fitting from an OCV(SoC) curve: `ecm_options` now accepts `ocv_soc_curve: {soc, ocv}` and optional `bounds_capacity: {lo, hi}`; the SDK validates curve monotonicity, SoC range, length parity, and bounds inversion locally before the request hits the wire. - Knot-resolution controls on `ecm_options`: `num_knots`, `num_knots_r0`, `knot_schedule`, `clamp_boundary_knots`, `clamp_max_ratio`. - `Ionworks.post_multipart` helper on the HTTP client for endpoints that accept optional multipart file uploads alongside query params. - `JobClient.get_metadata(job_id)` — fetch the parsed JSON contents of a job's metadata blob (large fields stripped from `job.result` and persisted to storage, e.g. `validation_results` and `validation_plot_config`). API-key clients couldn't previously reach these fields because the path through Supabase signed URLs requires a Supabase JWT (#515). - `PipelineClient.get_element_metadata(pipeline_id, element_name, elements=None)` — convenience wrapper that resolves an element by name to its job and returns the metadata blob. Pass a pre-fetched `elements` list to avoid re-fetching when pulling metadata for several elements of the same pipeline (#515). ## [0.7.0] - 2026-05-11 ### Added - `/protocols/convert` endpoint and client helper to convert a UCP protocol into vendor-specific cycler protocol files (#578). - Auto-detect `IONWORKS_PROJECT_ID` from the environment so callers don't have to pass it explicitly (#536). ### Changed - Validation errors now carry a structured `ValidationIssue` list with stable `CheckName` enum, `severity`, `message`, and `payload`. Callers should branch on `e.has_check(CheckName.X)` instead of substring-matching error messages (#544). ## [0.6.0] - 2026-05-04 ### Added - `skip_checks` parameter on validation entry points so callers with least-privileged API keys can opt out of project/permission checks while keeping schema validation (#534). - Helpers on the client for uploading and managing custom PyBaMM models (#566). ### Changed - UCP input is now backed by a machine-checkable JSON Schema, with parser parity tests ensuring the Python parser and schema agree (#494).