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

# Changelog

> Latest Ionworks Studio updates: A rebuilt execution engine for parallel data fits, name-based measurement lookup, Arbin subschedule/loop support, and clearer model and duplicate errors.

<Update label="June 29, 2026" description="A rebuilt execution engine for parallel data fits, name-based measurement lookup, Arbin subschedule/loop support, and clearer model and duplicate errors">
  ## A rebuilt execution engine for data fitting

  The pipeline's parallelisation layer has been rebuilt as a dedicated
  execution engine. `DataFit.fit` is now `DataFit.run` on this engine,
  which drives optimizers (ask/tell), point evaluation, multistart
  coordination, and progress reporting through a single set of executor
  protocols (serial, multiprocessing, and — on the backend — Ray over a
  warm actor pool). The old `distributed.py` Joblib configuration zoo
  and its `num_workers` / `parallel` schema fields are retired in favour
  of engine-owned parallelism, and backend datafit jobs now run through
  a thin `datafit.run()` host with classified-failure error mapping and
  per-generation progress streaming.

  ## Name-based measurement lookup

  `client.resolve_measurement(cell_specification, cell_instance,
    measurement)` resolves a `measurement_id` from human-readable names,
  walking the spec → instance → measurement hierarchy with server-side
  filtering at each level. Scripts no longer need to hand-walk three
  list endpoints and repeat name-matching boilerplate just to get an id.
  `parameterized_model.create_or_get` also lands, giving parameterized
  models the re-runnable create-or-resolve behaviour the other cell
  resources already had.

  ## Clearer errors for model failures and duplicates

  Two opaque "An unexpected error occurred" cases — a fit whose model
  setup needs geometry the user didn't supply, and a custom model with
  no discretisation recipe — now surface as a clear `ModelError` naming
  what failed. Separately, attempting to create a resource that already
  exists now returns a `409 CONFLICT` (with the existing id) instead of
  a `500`, both via a catch-all for Postgres unique-constraint
  violations and per-path translation at every insert site, so
  duplicate-creation attempts no longer page on-call.

  <AccordionGroup>
    <Accordion title="Studio">
      **Improvements**

      * "Save as template" for an optimization now builds the template
        server-side via a new `POST /optimization_templates/from-optimization`
        endpoint that copies an optimization's saved config directly into
        the template.

      **Fixes**

      * Completed validation/datafit results no longer sporadically show
        "Failed to load plot data." for one of a pair of plots. A job's
        metadata is rewritten in place as it runs (checkpoints, then the
        final write that adds the validation plot config), but the
        in-process metadata cache assumed metadata was immutable after
        completion; it now self-heals stale entries, and per-job storage
        blobs are written no-cache so a stale early write can't win.
      * `CycleAgeing` experiments passed as a lazy `DataLoader`
        (`experiment="from data"`) now resolve their `db:` measurement
        references server-side, instead of surviving to the
        credential-less fit worker and failing there.
    </Accordion>

    <Accordion title="Pipeline">
      **Improvements**

      * A bare element-wise cost (`SSE`/`MSE`/`RMSE`/`MAE`/`Max`) applied
        to a multi-variable objective now warns when variable lengths
        mismatch — e.g. a model-axis dQ/dV variable against a data-axis
        voltage variable — instead of silently broadcasting to a
        meaningless residual.
      * All 1-D interpolant calculations now accept a `"pchip"`
        interpolator (monotone cubic), the right choice for sparse,
        order-of-magnitude `D(sto)` tables such as half-cell GITT
        per-pulse diffusivity.

      **Fixes**

      * The MSMR half-cell logistic value and derivative are now evaluated
        through a single per-species-stabilized helper whose exponent is
        always ≤ 0, so it can never overflow.
      * The object path (`iwp.Pipeline.from_schema(...).run()`) now rebuilds
        a serialized model dict in objective options the same way the
        config/server path does, so both paths agree.
      * A custom model with serialised geometry/mesh now survives stdlib
        `pickle` / raw multiprocessing multistart — the previous
        anonymous-subclass approach could only be serialised by value
        (cloudpickle/Ray).
    </Accordion>

    <Accordion title="Python API">
      **Improvements**

      * `client.resolve_measurement` and
        `parameterized_model.create_or_get` (both introduced above), plus
        `Model.config` for inspecting a model's stored configuration.
      * Passing a bare pandas/polars DataFrame to a schema `data` field
        (objective `data_input`, `OCPDataInterpolant.data`, the Arrhenius
        calculations' `data`) is now auto-wrapped correctly on
        serialization, instead of failing server-side validation with the
        opaque "Required field 'data' missing".
    </Accordion>

    <Accordion title="Protocol Simulator">
      **Improvements**

      * Arbin protocols now support **subschedules** (mapped to a UCP
        subroutine with the referenced `.subsdx` registered and parsed
        recursively), **counter loops**, and **temperature add-ins**, so
        schedules that previously crashed on upload with "Unsupported step
        type: SubSchedule" now parse and simulate end-to-end.

      **Fixes**

      * BioLogic `.mps` simulation fixes: the loop counter is off by one no
        longer (EC-Lab's `ctrl_repeat` does not count the first pass, so a
        loop now runs `ctrl_repeat + 1` times), plus BCD/EIS support and
        drive-cycle fixes, each reproduced against the reported setting
        files before fixing.
      * User-correctable protocol and configuration errors across the
        Protocol Simulator and pipeline libraries now raise specific
        domain error types (`ProtocolConfigurationError`,
        `UserConfigurationError`) instead of generic `ValueError` /
        `RuntimeError`, so they surface to the user rather than paging
        on-call.
    </Accordion>

    <Accordion title="Data Processing">
      **Improvements**

      * The `process-data` reader gotchas are now documented: selecting the
        `record` sheet for multi-sheet Neware BTSDA `.xlsx` files, and
        current-unit handling — alongside the one-call
        `read.time_series_and_steps` entry point.
    </Accordion>
  </AccordionGroup>
</Update>

<Update label="June 22, 2026" description="GITTModel for diffusion-only fits, ionworks-model download for pybamm-only users, full app-URL helpers, and the start of a strict schema-config contract">
  ## GITTModel — a first-class diffusion-only model

  `GITTModel` and `HalfCellGITTModel` are now first-class models in
  `ionworkspipeline` (with matching `ionworks_schema` siblings). GITT
  fits overwhelmingly use the same lightweight model rather than SPMe:
  x-averaged particle diffusion per electrode, a single lumped ohmic
  resistance, fixed OCPs, and no Butler-Volmer kinetics, electrolyte
  dynamics, or thermal effects. Until now that model lived as
  near-identical local copies in per-engagement repos, which meant the
  API route could not run it. Shipping it as a built-in model unblocks
  server-side GITT fits and backs the new half-cell GITT fit template.

  ## Download ionworks-defined models for plain pybamm

  Users who have only `ionworks-schema` / `ionworks-api` — and not the
  licensed `ionworkspipeline` package — can now obtain ionworks-defined
  models (`ECM`, `LumpedSPMR`, the MSMR models, `GITTModel`) in a form
  they can load and run with plain `pybamm`. A new
  `POST /discovery/ionworks_models/serialize` endpoint builds the model
  server-side (where the license lives) and returns a loadable
  serialisation, exposed through the Python SDK.

  ## App-URL helpers for every routed resource

  The SDK's `client.urls` grew from a single `.measurement()` method to
  a full set of web-app link builders — `project`, `model`,
  `parameterized_model`, `optimization`, `pipeline`, `study`,
  `simulation`, `cell_specification`, `cell_instance`, and more — so
  callers never hand-construct `app.ionworks.com` URLs from IDs. The
  helpers are environment-aware, deriving the right host for whichever
  API environment the client is configured against.

  ## Stricter, clearer pipeline configuration

  Most user-driven errors in `ionworkspipeline` now raise
  `UserConfigurationError` (a `ValueError` subclass) instead of bare
  `ValueError` / `KeyError`, so configuration mistakes — wrong field
  types, out-of-range values, missing keys — are routed to a clear
  **Configuration error** for the user instead of paging on-call as an
  internal error. Alongside this, an in-progress schema-contract
  hardening effort began validating optimizer configs at the boundary:
  passing SciPy-only keys to a native optimizer (e.g.
  `DifferentialEvolution(popsize=5, maxiter=10)`) now hard-errors at
  submission time instead of being silently swallowed.

  <AccordionGroup>
    <Accordion title="Pipeline">
      **Improvements**

      * A single **calculation structure** —
        `dict[str, list[str] | None]` mapping each objective to the
        variables to compute — is now the source of truth for objective-
        and variable-level cost scoping, replacing the per-cost
        `objective_names` list. This lets a weighted `Wasserstein`
        (position-shifting dQ/dV) and a per-variable `SSE` share one
        `MultiCost` over an objective, with each cost explicitly scoped to
        the variables it should consume. `ElectrodeBalancing` gained a
        `dQdU model axis` option that emits dQ/dV on the model's own
        full-window voltage axis so a weighted Wasserstein can align peaks
        in voltage rather than on the data grid.
      * Strict optimizer-option validation: unknown `algorithm_options`
        keys (and method/options mismatches) are rejected at submission
        time, and `AskTellOptimizer` rejects unknown constructor kwargs at
        runtime with an actionable message — the previous behaviour
        silently ignored them. The typed option wrappers (`CMAESOptions`,
        `PSOOptions`, `DEOptions`, `XNESOptions`,
        `BayesianOptimizationOptions`, `SOBEROptions`, `TuRBOOptions`)
        remain the recommended way to pass options.
      * Failure sentinels across the cost and design spaces are unified
        into a single `FAILURE_PENALTY` constant, so the fitting and
        design-optimization paths can no longer drift apart and the
        penalty value is better-conditioned for surrogate-GP fits.

      **Fixes**

      * ECM coulomb-count capacity reference is now computed per segment.
        Multi-measurement fits previously accumulated the cumulative
        integral across concatenated segment boundaries, inflating the
        capacity estimate to roughly the sum of per-segment throughputs
        and triggering spurious capacity-sanity-check warnings.
      * A `geometry` passed through `simulation_kwargs` with symbolic
        bounds (e.g. a `pybamm.Scalar` min and a `pybamm.Parameter` max)
        is now deserialised back into pybamm objects before the simulation
        is built, instead of arriving as raw JSON and choking the solver.
      * `parse_model` now honours serialised `geometry`, `var_pts`,
        `spatial_methods`, and `submesh_types` for custom models, attaching
        them as per-instance defaults instead of letting pybamm rebuild
        geometry from `model.options`.
    </Accordion>

    <Accordion title="Python API">
      **Improvements**

      * `client.model.download` / `serialize` (introduced above) return
        ionworks-defined models in a plain-pybamm-loadable form.
      * `client.urls` (introduced above) now builds links for every routed
        resource, environment-aware.
    </Accordion>
  </AccordionGroup>
</Update>

<Update label="June 15, 2026" description="Global search with ⌘K, surrogate-based Bayesian / TuRBO / SOBER optimizers, ECM parameterization on the worker fleet, inline image previews for file measurements">
  ## Global search across the platform

  A `⌘K` dialog now searches across projects, cell specifications,
  cell instances, cell measurements, pipelines, datafits, simulations,
  optimizations, and parameterized models — with grouped, deep-linked
  results and per-entity-type chip filters. Cell measurements and
  instances are indexed via full-text search (Postgres `tsvector` with
  a backfilled `search_vector` column), so a partial name or id from
  any of these surfaces lands on the right detail page in one keystroke.
  The backend exposes `limit`, `offset`, `entity_types`, and `project_id`
  query parameters so the dialog can chip-filter and page server-side.

  ## Bayesian Optimization, TuRBO, and SOBER for parameter estimation

  Three surrogate-based optimizers ship in `ionworkspipeline` on a shared
  `SurrogateBasedAlgorithm` base (LHS warm-up, GP fitting with outlier
  clamping, constraint-GP handling, and EI-based surrogate convergence):

  * **`BayesianOptimization`** — sequential or batch BO with (log-)Expected
    Improvement and a feasibility-weighted acquisition for constrained
    problems.
  * **`TuRBO`** — trust-region BO with Thompson sampling, SCBO-style
    constraint handling, and anisotropic lengthscale weighting.
  * **`SOBER`** — batch BO via Bayesian quadrature with Tchernychova–Lyons
    kernel recombination, prior-aware importance sampling, and adaptive
    WKDE prior updates.

  Each algorithm has a matching `BayesianOptimizationOptions`,
  `TuRBOOptions`, `SOBEROptions` schema, and a new method-selection guide
  in the docs explains when to reach for them over Nelder–Mead, CMA-ES,
  or differential evolution.

  ## ECM parameterization runs as a background job

  Authenticated ECM fits now run as `ecm_fit` jobs on the Anyscale/Ray
  worker fleet instead of blocking a FastAPI worker for 10–60 s.
  `POST /ecm/fit-from-file` and `POST /ecm/fit-from-measurements` return
  `202 {job_id, status}`; the public demo `POST /ecm/fit` stays
  synchronous so unauthenticated example fits still complete in one
  round-trip. The new `client.ecm` Python SDK sub-client covers all three
  input modes — `fit_from_example` (sync), `fit_from_file` and
  `fit_from_measurements` (returning an `EcmFitJob` with
  `wait_for_completion(...)` polling), plus `save_to_project` and
  `detect_and_read` — and a new `ecm-fitting` agent skill walks an agent
  through picking a mode, running the fit, and persisting it as a
  parameterized model. Solver `ValueError`s from user data now surface
  as `BadRequestError`/`CONFIGURATION_ERROR` instead of paging Sentry.

  ## Inline image previews for file measurements

  File-type cell measurements used to render a "Visualization not yet
  supported" placeholder; the measurement detail page now fetches every
  attached file, renders images inline (filling the column with a
  click-to-expand lightbox, Esc or click-out to dismiss), and offers a
  download link for any file type. The detail-page loader also stopped
  unconditionally requesting `steps_and_cycles` / `time_series` for
  non-time-series measurements — a 400 from those endpoints used to
  reject the whole thunk and show "Measurement not found" for `file`
  and `properties` rows.

  <AccordionGroup>
    <Accordion title="Studio">
      **Improvements**

      * Optimizations are decoupled from the internal jobs system: a new
        `/optimizations` REST surface returns job-free `OptimizationResource`
        DTOs (list, get, config, metadata, runs, statuses, cancel, resubmit,
        delete) and the optimization list page polls a lightweight
        `/statuses?ids=...` projection every 5 s instead of opening one
        WebSocket per row. The parameterized-model selector also fetches by
        id rather than scanning the jobs cache.
      * A project-scoped parameterized model listing lets the
        parameterized-model picker filter to models that belong to the
        current project.
      * `id_text` and `search_vector` columns are hidden in the object-fields
        panels so detail views no longer surface internal indexing helpers.
      * Datafit results now surface "Validation not supported" as an inline
        alert when the result payload sets `validation_not_supported`,
        instead of leaving the validation tab blank or failing the page.
      * Protocol files that pause or end on a non-error stop condition now
        render a user-correctable inline message; previously the route
        returned a generic 500.
      * A new `pipelines/validations/{job_id}/plot_data` /
        `pipelines/datafits/{job_id}/plot_data` consistency pass plus
        generic backend mapping converts any plain Python error raised inside
        the web layer into a structured domain error response.
      * The Universal Cycler Protocol simulator pages now list Arbin and
        Novonix alongside Maccor, Bitrode, and BioLogic as supported cycler
        protocol formats, and the supported-protocols copy is shared between
        both UCP pages so the two stay in sync.
      * The `StandardDataGrid` card now claims an intrinsic width, fixing the
        collapsed-grid layout when the parent container had no explicit
        width.
      * Measurement-file uploads to storage now retry on transient errors
        with capped exponential backoff and a structured 502 once attempts
        are exhausted.
      * Pipeline list and detail fetches enforce last-wins via `requestId`
        guards and pass the thunk's `AbortSignal` through to `axios`, so
        navigating between pipelines during a slow request no longer reverts
        the view to a stale pipeline. Pipeline-elements caching now merges
        scoped entries on refetch instead of wiping the whole entity dict,
        and the pipeline detail subscription's polling callback guards
        against the stale-closure race that fired refetches for the previous
        pipeline.
      * The measurements list resets to page 1 at render time when the
        parent entity changes, removing the flash of stale rows that
        appeared while a new request was in flight.
      * Org-context resolution on hard refresh is now deterministic — the
        bootstrap ordering avoids the race where the active project briefly
        reverted to the first project in the list before the persisted
        selection rehydrated.

      **Fixes**

      * Dark mode now derives selection, focus, and hover colors from the
        Ionworks brand pink so the focused row, selected nav item, and
        hovered button no longer look muddy against the dark background.
      * Validation plot axis titles render correctly under Plotly 3.x — the
        upgrade started requiring the `{ text: ... }` wrapper that earlier
        versions inferred from a bare string.
      * The cell card on the cell detail page no longer shows redundant
        metadata fields that duplicated the breadcrumb.
      * The all-measurements page now loads cell specs on cold/direct
        navigation so the spec-name and spec-anode/cathode columns populate
        on first paint instead of only after navigating in from elsewhere.
    </Accordion>

    <Accordion title="Pipeline">
      **Improvements**

      * `CycleAgeing` defaults `experiment_model_mode="unified"` when an
        experiment is supplied, and `iwp.Simulation`'s `compile` flag now
        defaults to `True` in unified mode. Cycling experiments repeat the
        same handful of steps many times, so a single switching model
        collapses build and solve cost compared to one model per step;
        combined with compilation this is a substantial speedup for cycle-
        ageing fits. Pinned to PyBaMM 26.6.1.0.
      * `CycleAgeing` accepts `experiment="from data"`, which builds the
        cycling experiment from the loaded dataset instead of requiring
        the user to restate it.
      * `CycleAgeing` auto-enables `store_first_last` when every requested
        metric only needs first/last-cycle quantities, skipping the
        intermediate-cycle storage that the metrics would never read.
      * `save_at_cycles` on `CycleAgeing` is now auto-derived from the
        requested metric tree when the caller does not pass one
        explicitly — only the cycles that downstream metrics actually
        consume are saved.
      * `ElectrodeBalancing` emits `dQ/dV` curves alongside the existing
        capacity outputs, so weighted Wasserstein costs can be computed
        directly against the dQ/dV peak structure.
      * The `Time` metric accepts a vector of times and reports the metric
        at each one, replacing the previous one-scalar-at-a-time loop in
        callers that needed a few sample points.
      * A generic `solve_kwargs` passes through every objective into
        `Simulation.solve`, complementing last week's `solver_kwargs` on
        the constructor side.
      * Metric-tree introspection moved onto the `Metric` and `Slicer`
        classes so callers can ask a metric what it stores instead of
        re-deriving the answer from a config dict.
      * `SimplePipeline` is a new `Pipeline` subclass with a client-side
        validator that rejects configs containing more than one expensive
        element (`DataFit`, `ArrayDataFit`, `Validation`), mirroring the
        server-side guardrail.

      **Fixes**

      * `interactive_preprocessing` is rejected at the schema level when
        passed in objective options, matching the runtime ban and giving
        a clearer config-time error.
    </Accordion>

    <Accordion title="Python API">
      **Improvements**

      * `client.electrolyte.transport_from_dataset(dataset_id, forms=, columns=)`
        turns a material property dataset into a `pybamm.ParameterValues`
        of concentration-dependent transport functions ready to hand to
        `iws.direct_entries.DirectEntry(parameters=...)`. Two function
        forms are supported per property: tabulated `pybamm.Interpolant`
        (default) and Landesfeind–Gasteiger isothermal `y = A·exp(B·c)`
        (least-squares on `ln y`), which stays positive and finite below
        the lowest measured concentration so high-rate DFN solves don't
        hit `IDA_ERR_FAIL` when the electrolyte depletes.
      * `Quantity` constructors accept PyBaMM-style unit strings and
        normalize them on input, so values copied from a PyBaMM parameter
        set round-trip into and out of cell schemas without a manual unit
        rewrite.
      * `JobClient.get_parameter_trace(job_id)` returns the parameter-
        evaluation trace from an optimization job as a DataFrame for
        inspection or downstream plotting.
      * `client.ecm` (introduced above) is the new ECM sub-client.

      **Fixes**

      * POST and PATCH calls retry on transient connection drops with the
        same capped-backoff policy already used for GETs, removing the
        spurious `ConnectionResetError` surfacing during long-running
        uploads against a flaky link.
    </Accordion>

    <Accordion title="Data Processing">
      **Fixes**

      * A constant-current discharge segment with the unsigned mixed-mode
        sign convention no longer mislabels the step direction, and the
        matching CC-discharge classifier no longer reverses the sign on
        parquet round-trip.
    </Accordion>

    <Accordion title="Protocol Simulator">
      **Improvements**

      * Arbin protocols with a `SIGN(...)` or step-time piecewise-current
        formula now parse correctly and emit the expected step plan; the
        previous parser failed on the conditional with a generic syntax
        error.
      * `human_readable_protocol(...)` renders drive-cycle steps in the
        same shorthand notation the simulator uses elsewhere, instead of
        expanding them inline.

      **Fixes**

      * Pause and end stop reasons now surface as user-correctable inline
        errors instead of being treated as solver failures.
    </Accordion>

    <Accordion title="Skills">
      **Improvements**

      * The `parameterize` skill gains a current-driven fit template and a
        full-cell OCV template, each with an objective / cost decision tree
        so an agent can route an incoming dataset to the right starting
        configuration without re-deriving the choice every time.
      * A new `electrolyte-transport` SDK skill documents the
        `client.electrolyte.transport_from_dataset(...)` workflow,
        including the interpolant vs. Landesfeind-exponential trade-off
        and the `DirectEntry` integration pattern.
      * A new `ecm-fitting` SDK skill walks through the three ECM input
        modes (example, file, measurements) and the save-to-project flow.
      * The `parameterize` skill explains the component/material model and
        the material → cells reverse lookup so an agent can find which cell
        specs reference a given material before editing it.
    </Accordion>
  </AccordionGroup>
</Update>

<Update label="June 8, 2026" description="Brand-aligned theme refresh, semantic zoom on validation plots, Navigator helper and new data agent skills, tunable solver_kwargs with PyBaMM 26.6 support">
  ## Brand-aligned theme refresh

  Studio is no longer the unmodified Minimals MUI template — it now
  follows the Ionworks brand guide. Pink (`#FF87B7`) drives primary
  CTAs, selection, focus, and active nav/tabs; green-black (`#020E0D`)
  anchors text and neutrals; blue (`#0C68E9`) is reserved for
  hyperlinks so links stay obviously clickable. Corners are sharpened
  to 4px, typography is updated, and JSON viewers, plots, tables, and
  ID chips have been re-skinned to match. All changes are
  theme-level — no component or data-flow changes — so every screen
  lifts at once.

  ## Semantic zoom on pipeline validation plots

  Validation plots now fetch their data on demand and decimate
  server-side based on the visible x-range. Box-zoom into a region
  re-requests denser data for that window; a step-back zoom and a
  full-reset control are stacked in a zoom history. Pipeline and
  datafit jobs gain two new endpoints (`/pipelines/validations/{job_id}/plot_data`
  and `/pipelines/datafits/{job_id}/plot_data`) that return decimated
  traces with stride sampling, cached for one hour per
  `(job_id, organization_id)`.

  ## Tunable `solver_kwargs` with PyBaMM 26.6 support

  `iwp.Simulation` accepts a new `solver_kwargs` dict that merges
  over the tuned `IonworksSolver` defaults — e.g.
  `solver_kwargs={"options": {"compile": True}}` flips IDAKLU
  compilation on for long unified-experiment runs without restating
  the rest of the solver configuration. It threads through every
  simulation-backed objective (`CurrentDriven`, `Pulse`,
  `CalendarAgeing`, `CycleAgeing`). The workspace also upgrades to
  PyBaMM 26.6 (first release with the serialisation kernel), with a
  compatibility sniffer that accepts both the new fully-qualified
  `$type` tags and the legacy short `type` tags so historical and
  freshly-generated payloads both decode.

  ## Navigator helper and new data agent skills

  `ionworks.Navigator` is a new cached walker over the
  `cell_spec → cell_instance → cell_measurement → steps / time_series`
  hierarchy. It memoises every lookup, paginates automatically,
  returns name-sorted listings for stable iteration, and copies
  cached DataFrames so callers can't corrupt the cache. Two new agent
  skills land alongside it: `build-data-report` (multi-section
  markdown + PDF analysis covering rate capability, DCIR, OCV, GITT,
  entropic, aging, and gap analysis) and `validate-data` (a
  pre-upload gate covering header audit, schema, parquet, and strict
  measurement validators).

  <AccordionGroup>
    <Accordion title="Studio">
      **Improvements**

      * Cell-measurements list endpoints have been consolidated and now
        use server-side pagination across project, spec, and instance
        scopes. The frontend resolves spec metadata from the existing
        cells store and pre-fetches spec-level filters (`spec_name`,
        `spec_anode`, `spec_cathode`) separately so the main list query
        stays cheap.

      **Fixes**

      * Deleting a project no longer shows a misleading "The project you
        are trying to access does not exist…" toast. The delete
        confirmation dialog also now requires typing the exact project
        name to enable the **Delete project** button, matching the
        destructive-action pattern used elsewhere.
      * Signed-URL generation for measurement files retries on
        `httpx.ReadTimeout` with 0.5 s / 1.0 s backoff; a new batch
        endpoint replaces the N parallel calls in the file-list path with
        a single POST. File downloads also retry on `JSONDecodeError`
        (raised when the storage gateway returns HTML instead of JSON
        under load) and surface exhausted attempts as a structured 502.
      * Cell-instance measurements grid no longer renders the previous
        spec's rows after navigating between specs; `CellSelector` shows
        a skeleton while loading and no longer bails when an unrelated
        cells fetch is in flight; the ECM measurement selector switched
        from loading every project measurement into memory to true
        server-side pagination.
      * The `list_for_template` drive-cycles endpoint now awaits
        `execute()` — previously it raised `AttributeError` on `.data` at
        runtime — and has been upgraded to standard `limit` / `offset`
        pagination.
      * `RHFAutocomplete` now forwards `slotProps` (`listbox`, `paper`,
        `popper`, …) as a proper `slotProps` object instead of spreading
        them as flat props that MUI silently ignored.
    </Accordion>

    <Accordion title="Pipeline">
      **Improvements**

      * `skip_objective_callbacks` is a new `DataFit` option that skips
        the pre- and post-fit objective callbacks, each of which runs an
        extra full objective simulation purely to capture initial/final
        results that the server does not surface. Backend datafit jobs
        now set it by default for faster cluster fits; local users
        keep the existing callbacks unless they opt in.
      * Configuration errors raised inside `ionworkspipeline` are now
        `UserConfigurationError` (the new name for `ConfigurationError`)
        instead of bare `ValueError`, so the job classifier reliably
        maps them to `CONFIGURATION_ERROR` rather than `SOLVER_ERROR`
        and the **Configuration error** UI path lights up correctly.
    </Accordion>

    <Accordion title="Python API">
      **Improvements**

      * `client.simulation.get_result()` returns a typed `SimulationResult`
        dataclass with `time_series` and `steps` as DataFrames (polars by
        default, pandas when `set_dataframe_backend("pandas")` is active)
        and `metrics` as a plain dict, replacing the previous raw-dict
        response. Callers use attribute access and `.columns` membership
        checks instead of dict-key lookups.
    </Accordion>

    <Accordion title="Data Processing">
      **Improvements**

      * `DataLoader.data` and `DataLoader.steps` no longer emit the
        Polars-migration `FutureWarning` on every access — the polars
        return type is now the established default.
    </Accordion>

    <Accordion title="Skills">
      **Improvements**

      * New `sim-results` SDK skill: fetching time-series and step data
        from a completed simulation, computing SOC via step-boundary
        cumulation, identifying step types by mean current, deriving
        DCIR from pulse steps using an OCV baseline plus onset current,
        and polars-aware plotting conventions.
      * The `parameterize` skill now documents electrode geometry
        (thickness, porosity, particle radius, AM fraction, maximum
        concentration) as a hard **build** requirement for DFN/SPMe — a
        physics-based model cannot be assembled without these structural
        parameters. Geometry lives on the cell spec's components (not on
        a measurement), can come from teardown / metrology / vendor
        datasheet / literature, and FPBM is no longer offered as
        "available" for Tier-2 cells that lack it.
    </Accordion>
  </AccordionGroup>
</Update>

<Update label="June 1, 2026" description="Ionworks solver replaces IDAKLU as the default, structured error details on failed pipelines and optimizations, Wasserstein weighted mode and MSMRFullCell dQ/dU output">
  ## Ionworks solver

  A new `IonworksSolver` is now the default solver inside
  `ionworkspipeline.Simulation`. It is a drop-in replacement for
  `pybamm.IDAKLUSolver` that detects DAE structural properties
  (quadrature, linear-constant / linear-varying, block-constant /
  block-varying) and substitutes exact analytical solutions where the
  structure allows, falling back to IDAKLU for non-analytical models.
  Runtime C codegen via CasADi's `CodeGenerator` plus Numba-JIT
  integration kernels reduce per-step cost on the analytical paths.
  Piecewise interpolation now also accepts `smoothing=0` so a hard step
  function can be modelled directly.

  ## Structured error details on failed pipelines and optimizations

  Pipeline element and optimization failures now carry a structured
  `error_detail` with the exception type and a Sentry deep-link. Two new
  job error codes — `CONFIGURATION_ERROR` and `SOLVER_ERROR` — separate
  user-fixable config mistakes from solver-side failures. Inside the
  pipeline package, `FuzzyDict` / `ParameterStore` raise the new
  `ParameterNotFoundError` (instead of bare `KeyError`) and
  `ConfigurationError` distinguishes invalid configs from runtime
  errors, so the classifier attaches the right code. In Studio, failed
  optimizations now show a **View in Sentry** button (superadmin only)
  that opens the captured event directly; the exception message itself
  is stripped before serialization so user-visible diagnostics stay safe
  to share.

  ## Wasserstein weighted-point-cloud mode and MSMRFullCell dQ/dU output

  `iws.costs.Wasserstein` / `iwp.costs.Wasserstein` gain optional
  `position_variable` and `weight_variable` fields. When both are set,
  the cost computes one Wasserstein-1 distance per objective comparing
  two weighted point clouds — useful for full-cell MSMR fits where
  peak-location error in `Voltage [V]` should be measured by the
  `|dQ/dV|` weights instead of sample-by-sample. `MSMRFullCell` also
  now emits `Differential capacity [Ah/V]` when listed in
  `objective variables`, derived from `Full voltage [V]` /
  `Full capacity [A.h]` and interpolated to the data voltage grid.

  ## Pipelines documentation moved to schema-first Docs section

  The runnable "how do I actually run this" pipelines content has moved
  out of the Guide into a new schema-first **Pipelines** section under
  the Documentation tab, with every code sample rewritten to use
  `ionworks-schema` + `ionworks-api`. The Guide retains the theory —
  equations, parameter tables, intuition — and its existing "Pipelines"
  group is renamed to **Parameterization**. The Japanese tab mirrors
  the new section.

  <AccordionGroup>
    <Accordion title="Studio">
      **Fixes**

      * Project-scoped ECM fitting from existing measurements no longer
        returns 422: the frontend now sends the nested
        `{measurements: [{id, initial_soc?}], ecm_options: {...}}` payload
        the backend has expected since the per-measurement SOC change, and
        shows a per-measurement initial-SOC input next to each selected
        measurement.
    </Accordion>

    <Accordion title="Pipeline">
      **Improvements**

      * `client.simulation.protocol(...)` accepts `design_parameters` again
        as a flat dict — the single-simulation convenience that was
        accidentally removed when the protocol-template flow landed.
        Supplying both `design_parameters` and `design_parameters_doe`, or a
        DOE that expands to more than one simulation, now raises explicitly
        instead of silently billing for the extra runs.
      * BioLogic `.mps` protocols with large embedded drive-cycle tables
        (e.g. \~96k-row Urban Profile traces inlined as YAML block scalars)
        no longer time out at the edge proxy on
        `/protocols/parse-to-template` — the modified YAML is no longer
        re-parsed with the slow PyYAML path.
      * `iws.direct_entries.DirectEntry` accepts a `pybamm.ParameterValues`
        directly. Callable values (concentration- / temperature-dependent
        interpolants) are serialized to symbol-JSON via
        `ParameterValues.to_json()` automatically, and the in-pipeline
        `DirectEntry.from_schema` path deserializes them back into pybamm
        symbols so local and API consumption paths behave the same.

      **Fixes**

      * Child-module loggers under `ionworkspipeline.*` (e.g.
        `ionworkspipeline.data_fits.*`) are now governed by
        `set_logging_level` — the package logger was previously a sibling
        rather than the parent, so submodule records propagated past it.
    </Accordion>

    <Accordion title="Python API">
      **Improvements**

      * `client.simple_pipeline.create(...)` accepts a `PipelineOptions`
        (e.g. `live_progress_updates=False`) — the worker already honoured
        `pipeline_options` in job params, this wires the request model,
        service layer, and SDK client through.

      **Fixes**

      * Outbound JSON payloads no longer raise `TypeError` when a request
        body contains a pandas DataFrame with datetime columns. `pd.Timestamp`
        is serialized via `isoformat()` and `pd.NaT` becomes `null`.
    </Accordion>

    <Accordion title="Protocol Simulator">
      **Fixes**

      * BioLogic `.mps` parser no longer hangs on protocols like
        `lfp_gr_cccv.mps` where `lim*_seq` stores an internal sub-cycle
        pointer ≤ `Ns+1`. Only forward jumps (`seq > Ns+1`) emit gotos;
        backward or equal values are treated as plain step-ends.
    </Accordion>

    <Accordion title="Skills">
      **Improvements**

      * Ionworks skills can now be installed as a Gemini CLI extension via
        `gemini extensions install ~/ionworks-skills`; a `GEMINI.md` context
        file loads all eight skills as passive context on every session.
        The Coding agents docs page is updated to cover Gemini CLI, Cursor,
        and GitHub Copilot install paths alongside Codex and Claude Code.
      * New half-cell MSMR template (`assets/half_cell_msmr_template.py`)
        plus `discover_half_cell.py`, `inspect_half_cell_data.py`, and
        `validate_half_cell_fit.py` helpers in the parameterize skill. The
        reference page mandates the template and lays out a discover →
        inspect → scaffold → dry-run → submit → validate workflow so every
        half-cell OCP fit uses the same canonical priors, multistarts, and
        Xj method.

      **Fixes**

      * The process-data skill now documents the platform's actual current
        sign convention (`positive = discharge`), matches the validator and
        `set_positive_current_for_discharge` transform, and calls out the
        double-flip trap.
    </Accordion>
  </AccordionGroup>
</Update>

<Update label="May 25, 2026" description="Tighter measurement-data validation, broader UCP-to-cycler conversion, Wasserstein cost function">
  ## Stronger measurement-data validation and processing

  A new strict-mode check, `capacity_energy_from_current_power`, compares the
  reported `Discharge/Charge capacity [A.h]` and `…energy [W.h]` columns against a
  per-step trapezoidal integral of `Current [A]` and `Power [W]`, flagging
  row-by-row mismatches (10% default tolerance) so a transient mid-step error that
  later cancels out is still caught. When the reported charge/discharge columns are
  swapped — common in half-cell exports — `fix_swapped_charge_discharge_columns`
  detects and corrects the labels. The current-sign-convention check now weights
  each step's vote by the charge it actually passes (∫|I| dt) instead of its row
  count, so a long near-zero-current voltage hold no longer outvotes genuine
  discharge pulses and raises a false `CURRENT_SIGN_REVERSED`.

  Small-current coin- and half-cell data now processes end-to-end: rest-step
  detection scales its threshold to the trace's peak current (sub-mA currents are
  no longer all classified as rest), the auto-flip retry drops stale
  capacity/energy columns before recomputing, and caller-supplied `Step count`
  labels survive sign correction. A new generic `parquet` reader uses the same
  alias-based column detection as the CSV reader, and `folder:` data references now
  accept `time_series.parquet` / `steps.parquet` (preferred over CSV when both are
  present).

  ## Broader, validated UCP-to-cycler conversion

  The UCP-to-Arbin and UCP-to-Maccor converters handle more end-condition and
  step types (variable / loop / compound ends, temperature ends, Maccor pause
  steps) and now validate up front, raising clear errors on features a target
  cycler genuinely cannot represent instead of emitting files that fail to
  re-parse.

  ## Wasserstein distance cost function

  `iwp.costs.Wasserstein` adds the 1-Wasserstein (earth mover's) distance as a
  data-fitting cost, comparing model output and data as distributions rather than
  point-by-point. Available in both the pipeline and schema packages.

  <AccordionGroup>
    <Accordion title="Studio">
      **Improvements**

      * Protocols list loads dramatically faster: the experiment-templates list
        endpoint now omits heavy JSONB columns by default (\~7.5 KB vs 7.7 MB on 22
        templates), with callers opting into `protocol_config`, `parameters_schema`,
        and similar fields via `?include=`.
      * Failed pipelines can be resubmitted from the pipeline details page, which now
        shows an error banner that deep-links to each failed element; individual
        elements display an inline error alert.
      * Material property dataset plots now have an always-visible vertical legend with
        units, positioned to the right of the plot.

      **Fixes**

      * Filtering measurements by ID on the project data page now narrows the list —
        the `id` query parameter was previously dropped silently, returning every
        measurement.
      * Material dataset upload no longer shifts column indices when a CSV has an
        unnamed leading column, and now requires every column to be named (with a clear
        error) instead of silently skipping unnamed ones.
      * Project membership management, active-organization switching, and
        parameterized-model deletion now go through the backend API instead of direct
        database writes, so they behave consistently under row-level security.
    </Accordion>

    <Accordion title="Python API">
      **Improvements**

      * `client.auth_check()` and `client.whoami()` verify which user and organization a
        configured API key resolves to, backed by a new `GET /auth/health` endpoint —
        useful for debugging wrong-org (403) errors and confirming which key is active.
    </Accordion>

    <Accordion title="Protocol Simulator">
      **Improvements**

      * Maccor protocols with `SubRout` steps survive UCP YAML round-trips: resolved
        subroutine steps are namespaced and written back into the YAML, and
        intra-subroutine `goto` targets resolve at simulation time, fixing the
        `Subroutine '<name>' not found` error in the parse-then-simulate flow.
    </Accordion>

    <Accordion title="Skills">
      **Improvements**

      * New `validate-data` skill guides agents through the measurement-data validation
        and column-fix workflow.
      * A Codex plugin manifest was added so the Ionworks skills package installs in
        Codex alongside Claude Code.
    </Accordion>
  </AccordionGroup>
</Update>

<Update label="May 18, 2026" description="Material properties in Studio, native UCP simulation for design optimization, SimplePipeline workflow, global /search endpoint">
  ## Material properties in Studio

  A new **Materials** section in the project sidebar lets you create
  materials within a project and attach measured property datasets
  (CSV / parquet) to each one. Upload, plot, edit, re-process, replace,
  and delete are all wired up, backed by a new
  `material_property_datasets` table with composite
  `(project_id, organization_id)` foreign keys and a dedicated Supabase
  bucket. The Python SDK gains read-only `client.material` and
  `client.material_property_dataset` sub-clients with `list`, `get`,
  `get_units`, and `get_data` (returns a `polars.DataFrame`); the REST
  surface supports signed-URL downloads, on-the-fly downsampling, and
  x-range filtering. A new docs page covers the UI workflow and REST
  endpoints.

  ## Native UCP simulation for design optimization

  `DesignObjective` gained a `backend="ucp"` option (also selectable via
  the `IONWORKS_SIMULATION_BACKEND` env var) that runs UCP protocols
  natively inside the optimization loop instead of converting them to a
  `pybamm.Experiment`. This preserves UCP features that the conversion
  dropped — dynamic loops, conditionals, gotos, `set_variable`,
  subroutines — and avoids the per-iteration parser overhead. EIS steps
  are now rejected up front in design optimization (with a clear error
  both in the frontend form and at the backend), because the UCP backend
  does not yet support frequency-domain steps; standalone simulations
  remain the path for EIS.

  ## SimplePipeline workflow

  A new lightweight pipeline variant for configs with at most one
  expensive element (one data fit or one validation). The whole config
  runs end-to-end as a single Ray job on the batch queue instead of
  fanning out to child jobs. CRUD endpoints under
  `/simple_pipelines` (POST returns 202; LIST supports filters and
  Supabase operator syntax; PATCH for name/description; cancel and
  delete), plus a `client.simple_pipeline` SDK sub-client with
  `create`, `get`, `list`, `update`, `cancel`, `delete`, and
  `wait_for_completion`. Large validation outputs are written to
  metadata storage instead of the DB record, and distributed evaluation
  inside the single job is wired through a new `_on_setup_complete`
  hook on `DataFit.setup()`.

  ## Global search API

  New `GET /search` endpoint performs prefix full-text + substring
  search across projects, studies, simulations, models, parameterized
  models, optimizations, optimization templates, experiment templates,
  pipelines, cell specifications, and materials within the authenticated
  organization. Backed by Postgres `tsvector` columns + GIN indexes per
  table, with all entity queries fanned out in parallel via
  `asyncio.gather`. There is no frontend search bar in Studio yet — the
  new docs page makes this explicit so you don't go looking for one.

  <AccordionGroup>
    <Accordion title="Studio">
      **Improvements**

      * Optimizations table: bulk **Delete** restored next to the existing
        bulk Cancel action, gated by `optimization:delete`, with a
        confirmation dialog that pluralizes correctly and per-row error
        toasts.
      * Protocol simulator: **Download CSV** button next to "Configure Plot"
        exports the full time-series plus step-level columns (cycle count,
        step number, protocol variables) expanded to match each time point,
        regardless of zoom.
      * Time-series measurement plot: overlay multiple variables sharing the
        same unit on either Y-axis via a `+` button — unit-filtered dropdown,
        distinct color cycle per series, individual `×` to remove, and
        selecting a primary variable with a different unit clears
        incompatible extras.
      * ECM models now expose `Anode potential [V]`, `Cathode potential [V]`,
        and their open-circuit counterparts so BioLogic three-electrode
        EWE / ECE control limits can be reproduced from a simulation. New
        `LFP/Li metal` half-cell chemistry added to the parameter library and
        the cell configuration UI.
      * Hover tooltips standardized to 3 decimal places across Plotly and
        Highcharts via `createBasePlotLayout`; documented as a new
        frontend convention.
      * EIS Nyquist plot renders as markers-only scatter (no connecting
        lines), with a slightly larger marker.
      * Cell-spec cascade delete batches up to 1000 storage paths per
        `bucket.remove()` call and parallelises per-measurement folder
        deletes with `Semaphore(16)`, with `asyncio.gather` for
        measurement-list fetches so one transient DB failure doesn't abort
        cleanup for the others.
      * Supabase storage downloads now retry transient 5xx errors with
        exponential backoff (3 attempts, 0.5 s → 4 s, jittered) — fixes job
        failures where storage3 crashed on non-JSON 502 response bodies.
      * `GET /jobs/{job_id}/metadata` route returns the parsed contents of a
        job's `metadata.json.gz` blob, giving the Python SDK a path to large
        validation payloads (`validation_results`, `validation_plot_config`)
        that the legacy `/pipelines/validations/{job_id}/result` endpoint
        could not reach.
      * Simulation submission unified to a two-step pattern
        (`/protocols/parse-to-template` → `/simulations/with-template/batch`).
        Removed the redundant `/simulations/protocol`,
        `/simulations/protocol/batch`, single
        `/simulations/with-template`, and `/standalone-cycler/simulate`
        endpoints, and dropped the transient `cycler_protocol_results`
        table.

      **Fixes**

      * Simulation dedup: removed `cycler_protocol_record_id` (which changes
        every parse session) from the simulation\_options uniqueness key, so
        the "simulation already exists" path actually triggers and duplicate
        rows stop accumulating.
      * Measurement details: Cycles tab is visible again and the cycle
        filter slider's range is correct after the `Cycle number` →
        `Cycle count` column rename.
      * Measurements of type `properties` or `file` now show an
        informational alert pointing to the details panel or the SDK
        instead of rendering empty time-series tabs.
    </Accordion>

    <Accordion title="Pipeline">
      **Improvements**

      * `ionworks-schema` is now the single validation boundary for parser
        inputs across the pipeline. Every pipeline class with a schema
        counterpart (\~40 classes) gained a `from_schema(schema)` classmethod;
        parsers call `iws.X.model_validate(config)` and construct runtime
        objects via `from_schema`. The auto-generated
        `ConfigMixin.config_schema()` is retired, and the SDK now depends
        on `ionworks-schema` directly so `PipelineClient.create()` accepts
        `iws.Pipeline | dict`.

      * Standardized user-facing optimiser kwargs across all scipy wrappers:
        `max_iterations` replaces `maxiter` / `max_nfev` / `iters` / `niter`,
        and `population_size` replaces `popsize` on
        `ScipyDifferentialEvolution`. Old names continue to work with a
        `DeprecationWarning`. `DataFit.max_iterations` and
        `FunctionTimeout.max_iterations` align with the same name.

      * `ionworks_ucp.SolverError` is now registered in
        `BaseObjective._acceptable_errors`, so a transient UCP solver
        failure during differential evolution lands on the finite-penalty
        path instead of killing the optimization. Protocol and configuration
        errors remain `ValueError` / `RuntimeError` so static bugs still
        surface.
        **Fixes**

      * `pybamm.Experiment` `period` and `temperature` now round-trip
        through `Serialise.serialise_experiment` (fix shipped in
        `pybamm 26.4.3`). `ExperimentStepConfigSchema` and
        `ExperimentConfigSchema` accept the new field set
        (per-step `period`, `temperature`, `tags`, `description`,
        `direction`, `start_time`, `skip_ok`; experiment-level `period`,
        `temperature`, `termination`); `duration` also accepts
        human-readable strings like `"287 seconds"`. The previous
        `_apply_dropped_fields` workaround is removed.

      * SimplePipeline jobs run `DataFit` in-process and now establish their
        own Ray connection (sharing
        `_connect_to_ray_with_retry` with the child-job runner) so
        distributed evaluation actually fires; when the Ray connection
        fails the evaluator hook is skipped and DataFit falls back to its
        in-process path. Legacy element-type labels
        (`"Data Fit"`, `"Direct Entry"`, `"datafit"`) are now canonicalized
        to wire discriminators at ingress.
    </Accordion>

    <Accordion title="Python API">
      **Improvements**

      * `client.simple_pipeline` sub-client for the new SimplePipeline
        workflow.
      * `client.protocol.convert(protocol, target)` returns a
        `ConvertResult` with `primary_bytes`, `text()`, and `save(dir)`
        helpers — exports a UCP YAML protocol to a native vendor file
        (Maccor, Arbin, Neware, BioLogic BT-Test, or Novonix). Maccor returns
        any drive-cycle MWF assets alongside the primary file.
      * `client.job.get_metadata(job_id)` returns the parsed contents of a
        job's `metadata.json.gz`, giving the SDK access to large validation
        payloads the legacy result endpoint could not reach.
    </Accordion>

    <Accordion title="Protocol Simulator">
      **Improvements**

      * BioLogic `.mps` parser: User Profile (drive cycle) steps are now
        extracted from embedded Urban Profile Tables, or from sibling `.txt`
        files supplied via `additional_content` when the `.mps` lacks
        embedded tables. Current sign is flipped on the way in so positive
        represents discharge for UCP/PyBaMM.
      * Arbin parser rewritten to keep the step list flat with raw gotos
        instead of inferring loops from backward-goto patterns —
        `dynamic_experiment` already resolves gotos in a flat namespace and
        guards backward jumps via `max_backward_jumps`. Fixes sibling
        backward gotos to the same target, cross-loop goto resolution, and
        digit-bearing formula labels like `F_EIS_10%_capacity_change`. Pause
        steps emit UCP's first-class auxiliary `Pause` step.
      * More Arbin / Maccor step types recognized:
        Arbin `Internal Resistance` → `Rest` with a `UserWarning`,
        `av_t` / `pv_chan_test_time` / `pv_chan_cv_stage_current` mapped to
        their UCP types, bracketed `MV_UD[n]` normalised, leading-negative
        current expressions classified as `Discharge`. Maccor `User Def
        CYCLE <op> N` translated to a UCP `VariableEnd` against the runtime
        `CYCLE` alias.
      * Per-step overhead trimmed substantially on long protocols. When the
        model lacks `Temperature [degC]` the per-step lookup skips pybamm's
        O(N²) "did you mean…?" `difflib` search entirely (\~41 ms per step);
        when the protocol contains no derivative ends, `set_variable`, or
        variable-driven goto targets, the per-step full-trace evaluation is
        skipped and `_create_minimal_step_df` emits a single-row frame with
        only `Time [s]`.
      * New `ionworks_ucp.SolverError` exception is raised for genuine
        pybamm solver failures; protocol and configuration errors keep
        their original `ValueError` / `RuntimeError` types and just gain
        step context.

      **Fixes**

      * Real Maccor `.MWF` exports that include the \~28-line preamble plus
        a `Type\tMode\tValue\t…` header row now parse — `read_waveform`
        scans for the header sentinel and skips up to and including it
        before handing the rest to `pd.read_csv`. Files containing only
        data rows still parse unchanged.
    </Accordion>

    <Accordion title="Skills">
      **Improvements**

      * New `run-simple-pipelines` skill walks through the SimplePipeline
        client end-to-end.
      * `process-data`: clarified that `protocol` holds test conditions that
        affect the electrochemical outcome (temperature, C-rate, SoC, DoD,
        pressure) while `test_setup` holds physical logistics (cycler model,
        operator, lab, channel) that do not. `test_setup` lives only on
        measurements, not on cell instances.
      * `process-data`: `set_step_count` with a step column is now the
        unambiguous default — it keys off `np.sign(np.diff(...))`, so
        decreasing / repeating step ids from GITT or RPT-with-substeps work
        the same as monotonic ones; `set_cumulative_step_number(method="current sign")`
        is reframed as a fallback for when no step column exists at all.
        Added a caveat for cyclers that emit duplicate `Time [s]` rows at
        step transitions.
      * `process-data`: mandatory header-audit step codifies eight rules
        (walk every file, group by cohort × column-set, classify
        Standard / Auxiliary / Drop, diff reader output, preserve aux
        columns, keep multi-thermocouple channels separate, confirm units /
        sign per cohort, surface missing-temperature as a finding) and a
        required confirmation-report shape, so silent column drops between
        cycler families are caught before any standardized parquet is
        written.
    </Accordion>
  </AccordionGroup>
</Update>

<Update label="May 11, 2026" description="Custom PyBaMM model + Li-S support, ECM capacity co-optimization, default project for the Python SDK, structured validation issues">
  ## Custom PyBaMM models with Lithium-Sulfur chemistry

  The `/models/upload-custom` endpoint now accepts a `chemistry` field (defaulting
  to `lithium_ion`), and Li-S models get a chemistry-aware initial-state shim
  plus tighter IDAKLU tolerances when they run. The `manage-projects` SDK skill
  documents the full upload workflow — `pybamm.Serialise().save_custom_model(filename=...)`
  → multipart upload → `client.model.get(id)` returns `is_custom_model: true` —
  including the `EventType`-not-JSON-serializable gotcha that hits anyone trying
  to `json.dumps` the dict from `serialise_custom_model()` directly.

  ## ECM fit: capacity co-optimization and per-segment initial SoC

  Several interlocking improvements to the project-scoped ECM fit. Supplying an
  `ocv_soc_curve` (and optional `bounds_capacity`) lets you co-optimize cell
  capacity `Q` jointly with the RC `beta` knots in the outer least-squares loop
  instead of pinning `Q` to a single seed; on a 25 °C rate-test trace the fitted
  capacity now lands within 0.9 % of the coulomb-counting truth across all knot
  schedules. `initial_soc` accepts a list (one entry per measurement) so
  multi-measurement fits reset SoC at each segment boundary instead of
  integrating coulombs across the gaps; if you omit it, the new auto-seed
  routine refines each segment's `soc0` by root-finding
  `V[s] = OCV(soc0) − I[s]·R0(soc0)`. `num_knots`, `num_knots_r0`,
  `knot_schedule`, and `clamp_max_ratio` are now first-class parameters on
  `/fit-from-measurements` and `/fit-from-file`. The boundary clamp default also
  loosened from `max_ratio=1.0` to `10.0`, which was collapsing R0 and triggering
  pybamm IDAKLU `CONV_FAIL` on rate-test forward sims.

  ## Default project for the Python SDK

  The `Ionworks` client now resolves a default `project_id` at construction time
  from a `project_id=` argument or the new `IONWORKS_PROJECT_ID` environment
  variable, so callers no longer have to thread `project_id` through every call.
  The previous `PROJECT_ID` env var still works but emits a `DeprecationWarning`.
  All `client.study.*` methods take `project_id` as an optional keyword
  (after the resource ID) defaulting to the client value, and pipelines and
  optimizations auto-inject it into payloads.

  ## Structured measurement-validation issues

  `MeasurementValidationError.errors` is now `list[ValidationIssue]` — a frozen
  dataclass carrying a stable `IssueCode` (StrEnum), `severity`, human-readable
  `message`, and JSON-native `payload`. Downstream code can branch on check
  identity via `e.has_code(IssueCode.CURRENT_SIGN_REVERSED)` instead of grepping
  the message string. `ionworksdata`'s auto-fix path now keys off the new codes;
  `IssueCode` and `ValidationIssue` are re-exported from the top-level
  `ionworks` package.

  <AccordionGroup>
    <Accordion title="Studio">
      **Improvements**

      * `organization_id` is now `NOT NULL` on \~15 tables, all RLS policies have
        been rewritten to read it directly (replacing the `has_permission_via_*`
        function chain), and composite `(project_id, organization_id)` foreign
        keys prevent org drift on project-scoped tables.
      * Simulation boards CRUD moved to a proper backend API at
        `/projects/{project_id}/studies/{study_id}/simulation_boards` with
        `organization_id` resolved server-side, fixing a blank Visualization tab
        on stage where direct Supabase inserts were silently rejected by the
        stricter RLS.
      * Optimization Performance Detail and Performance Summary tabs now surface
        buried `validation_warning` / `validation_not_supported` issues as
        top-level alerts above the tabs, with info "no data" alerts inside the
        tabs for the rare empty-but-valid case.
      * Defensive UX in the Visualization tab: the Data/Visualization toggle
        stays visible even when no board is available, with a warning alert
        prompting the switch back to Data instead of trapping the user on a
        blank page.

      **Fixes**

      * The single simulation result page no longer flashes "Simulation not
        found" before the data loads on a fresh navigation.
    </Accordion>

    <Accordion title="Pipeline">
      **Improvements**

      * `ionworks-schema` gained `Constraint`, `Penalty`, `CMAESOptions`,
        `PSOOptions`, `DEOptions`, `LatinHypercube`, and `Uniform` schema
        classes that pipeline already had, plus `Field(description=...)`
        enrichment on \~40 pilot-touched classes across objectives, data fits,
        parameter estimators, regularizers, and distribution samplers. A new
        Sphinx docs skeleton auto-generates a reference page per submodule and
        cross-links each schema class to the matching `ionworkspipeline` page
        via intersphinx.

      **Fixes**

      * `EmptySolution` `AttributeError` (raised when SUNDIALS gives up at IC
        for a bad parameter combination) now routes to the existing fit-failure
        penalty path instead of crashing the whole datafit. Cloud fits stay
        alive and the offending sample just gets a huge cost.
    </Accordion>

    <Accordion title="Python API">
      **Improvements**

      * New `client.urls.measurement(measurement_id, project_id)` helper
        returns the web app deep link for a measurement, so callers don't have
        to hand-build URLs against `frontend/src/routes/paths.ts`.
    </Accordion>

    <Accordion title="Skills">
      **Improvements**

      * `manage-projects` documents the `/models/upload-custom` multipart
        workflow and the Model / ParameterizedModel disambiguation.
      * `upload-data` and other validation-aware skills updated to reference
        the new `IssueCode` / `ValidationIssue` API and the
        `e.has_code(...)` pattern instead of substring-matching error strings.
    </Accordion>
  </AccordionGroup>
</Update>

<Update label="May 4, 2026" description="Full optimization experiment editor, Arbin reader, Maccor coverage, UCP input schema, baseline migration squash">
  ## Full editor for optimization experiments

  Optimization experiments now use the same protocol editor as the rest of the
  app. The bare text field has been replaced with a CodeMirror-based editor
  featuring YAML syntax highlighting, dark-mode support, frontend pre-validation,
  and the full protocol-builder dialog for picking templates and tuning steps.
  New protocols pre-load a "Constant Current Charge" template so users have a
  working starting point.

  ## Iterative metrics for design optimization

  Design optimization now supports `CyclewiseMetric` and `StepwiseMetric`
  wrappers end-to-end. The optimization form lets you target a metric on a
  specific cycle or step with bounded input fields driven by the parsed
  experiment, mix pipeline variables alongside PyBaMM variables, and
  round-trips wrapped metrics through save/load. The underlying UCP→PyBaMM
  parser also fixes a long-standing bug where repeated blocks were flat-expanded,
  which broke `solution.cycles` indexing for cycle-wise metrics. Two new
  templates ship: a simplified Charge optimization and composite system models.

  ## Arbin cycler support

  `ionworksdata` now reads native Arbin exports — CSV, XLSX, and the binary
  `.res` format. The `.res` reader uses `mdb-export` to extract data from the
  underlying Access database (no Python MDB library required), sorts rows by
  `DateTime` to handle interleaved multi-session recordings, and reads the
  absolute start time from `Global_Table.Start_DateTime`. Auto-detection picks
  the right reader from file headers, and `arbin res` is also exposed as an
  explicit reader name.

  ## Maccor protocol coverage

  The Maccor parser and simulator now handle several real-world protocol
  features that previously failed at parse time or during simulation, including
  per-limit safety gotos, `Chg/Dis Func CCCV` step types, and robust `VAR`
  setvar handling. The Maccor `ionworksdata` reader also gained support for the
  compact short-form column header set used by some firmware versions, which
  previously crashed with `ColumnNotFoundError: "Time [s]"` and silently
  dropped capacity/energy columns.

  ## Machine-checkable UCP input schema

  The discovery endpoint `/discovery/schemas/protocol` now exposes a JSON
  Schema generated from the parser's own enums, matching the actual YAML
  authoring format (single-key dicts like `{Charge: {...}}`) rather than the
  parsed in-memory shape. Parity tests round-trip 17 good and 6 bad fixtures
  through both the schema and the parser so they cannot drift. Submitting the
  legacy `kind:` shape to `POST /protocols/validate` now returns a helpful
  error pointing at the canonical YAML examples, and the `discover-api` and
  `run-simulations` skills show local pre-validation via
  `jsonschema.validate(...)`.

  <AccordionGroup>
    <Accordion title="Studio">
      **Improvements**

      * Organization ID is now denormalized onto every row that sits under an
        organization (including join tables), simplifying RLS checks and
        cross-resource queries.
      * The squashed baseline migration collapses \~100 historical migrations into a
        single idempotent file, with a CI/CD repair step that handles out-of-order
        feature-branch deploys on internal environments.
      * The Ionworks Agent tab has been replaced with a static instructions page
        pointing to the [Ionworks Agentic Toolkit](https://github.com/ionworks/ionworks-skills);
        the in-app pydantic-ai chat agent has been removed in favour of the
        SKILL.md-based workflow.
      * New cell specifications no longer pre-fill a default capacity, so users
        enter the actual cell capacity from the start.

      **Fixes**

      * Storage permissions are now enforced at the organization level so members
        of the same org can read each other's measurement files as expected.
      * `created_by_email` is now consistently populated on cell-spec, cell-instance,
        and cell-measurement responses across both `get` and `list` endpoints.
      * Simulation usage is aggregated at the organization level (matching the
        org-level usage limit that the backend already enforces) and is fetched
        from a dedicated `GET /organizations/current/usage` endpoint.
    </Accordion>

    <Accordion title="Pipeline">
      **Improvements**

      * Cost / objective-function layer restructured into focused modules
        (accumulators, error metrics, normalizers, NaN policies) with several
        correctness fixes around scalarization, normalization defaults, and
        partial-solution handling.
      * `FunctionParameters` are now serialized correctly inside objective and
        data-fit configs, so configs with function-valued parameters round-trip
        through JSON.

      **Fixes**

      * Half-cell ECM now exposes anode and cathode potential variables
        (`Anode potential [V]`, `Cathode potential [V]`) for plotting and metrics.
      * `get_cycle_metrics` keeps the `Cycle count` column name on output instead
        of renaming it to `Cycle number`, matching the input convention used
        throughout the data pipeline.
    </Accordion>

    <Accordion title="Python API">
      **Improvements**

      * `validate_measurement_data`, `CellMeasurement.create`, and
        `CellMeasurement.create_or_get` accept a `skip_checks` parameter so callers
        can relax a single strict validator (e.g. `time_gaps`) instead of disabling
        strict mode wholesale. Unknown check names raise `ValueError`, and the
        canonical set is exposed as `ionworks.validators.STRICT_CHECK_NAMES`.
        `validate_strict=True` is now the recommended default.
    </Accordion>

    <Accordion title="Data Processing">
      **Improvements**

      * New Arbin reader covering CSV, XLSX, and native `.res` exports (see the
        highlight above).
      * Maccor reader now accepts the compact short-form column header set used
        by some cycler firmware versions.
    </Accordion>

    <Accordion title="Documentation">
      **Improvements**

      * New electrolyte parameterization page with a worked Landesfeind fit
        example.
      * Optimization experiment editor, iterative metrics, simplified Charge
        template, and composite system models are documented.
      * Custom variables reference now documents anode/cathode potential
        exposure on the half-cell ECM (English and Japanese).
      * Maccor native `CCCV` step type is documented (English and Japanese).
      * Arbin is listed in the supported cyclers table.
      * SEO metadata (keywords, OG tags, tightened descriptions) added across
        the docs site, including expanded data-fitting guide descriptions.
    </Accordion>

    <Accordion title="Skills">
      **Improvements**

      * Each mirrored package (`ionworksdata`, `ionworks-schema`, `ionworks-api`,
        `iwutil`, `ionworkspipeline`, `skills`) now ships a per-package
        `CHANGELOG.md` in Keep-a-Changelog format, written and validated as part
        of the release workflows.
      * `discover-api` and `run-simulations` skills updated to reference the new
        UCP input JSON Schema and demonstrate local pre-validation.
      * `upload-data` skill updated to recommend `validate_strict=True` and
        document the new `skip_checks` parameter.
    </Accordion>
  </AccordionGroup>
</Update>

<Update label="April 27, 2026" description="Project-scoped ECM fitting, PyBaMM v26.4.0 support, SDK skills plugin, strict upload validators">
  ## Project-scoped ECM fitting

  Equivalent-circuit-model fitting is now available directly inside a project. Pick any
  measurement already attached to one of the project's cells (or upload a fresh CSV), preview the
  trace, run the fit, and save the result back to the project as a new model with one click. The
  new flow replaces the multi-step stepper with stacked cards, adds a model-vs-data plot, an error
  plot, a fitting-progress indicator, an OCV toggle helper, and auto-scrolls to the results when
  the fit completes. The standalone ECM demo continues to work for unauthenticated users.

  ## PyBaMM v26.4.0 support

  The pipeline and protocol simulator have been updated for PyBaMM v26.4.0, which introduces a
  dedicated `Rest` step class in place of the old zero-current step and renames the MSMR
  electrode-capacity parameters from short keys (e.g. `Q_n_1`, `Q_p_1`) to descriptive strings
  (e.g. "Negative electrode host site occupancy capacity (1) \[A.h]"). Existing experiments and
  MSMR parameter sets continue to parse correctly under the new release.

  ## SDK skills as a Claude Code plugin

  The Ionworks Python SDK now ships with a set of installable Claude Code skills covering
  api-discovery, cell-data, data-upload, simulations, pipelines, and projects-and-studies. Each
  skill documents the relevant sub-clients, method signatures, and the create-or-get patterns so
  an AI assistant can drive the SDK against the current API surface. Install via
  `/plugin marketplace add ionworks/ionworks-skills` and `/plugin install ionworks` to get the
  slash commands and SessionStart hook for any Ionworks project.

  ## Strict upload validators in the Python API

  `CellMeasurementClient.create` and `create_or_get` now run optional client-side validators
  before sending data to Studio so processing bugs surface as a local error instead of a confusing
  upload result. A hard time-gap check (>5 h between consecutive rows) runs by default; passing
  `voltage_window` enables a voltage-continuity check (catches chronological misordering from a
  faulty `partition_by`), and passing `rated_capacity` enables a consecutive same-direction full-step
  check plus a soft warning for any step whose capacity exceeds 5× rated.

  <AccordionGroup>
    <Accordion title="Studio">
      **Improvements**

      * Measurement chart controls collapse into a compact header summary; per-axis variable
        selectors now allow adding and removing series dynamically with a unified two-column layout.

      **Fixes**

      * Study visualization no longer collapses to an empty state when switching between studies whose
        experiment-type tabs differ — the filter resets to All when the previous tab isn't present.
      * Users with a stale Supabase session can recover by signing out via the OTP `auth/continue`
        page instead of being stuck on the redirect.
      * The empty cells page now uses the correct organization-level permission check, so users who
        can create cells no longer see a misleading "contact a project admin" message.
      * Pipeline element details view no longer triggers rapid duplicate fetches for `Data Fit` and
        `Validation` configs — the large `job_config` is read directly from storage and cached.
    </Accordion>

    <Accordion title="Pipeline">
      **Improvements**

      * Function compilation is now enabled by default in `Simulation` (IDAKLU solver), speeding up
        repeated runs that share the same model.
      * `submesh_types` and `spatial_methods` now serialise correctly through
        `SimulationObjective.to_config`, fixing JSON round-trip for objectives such as `CycleAgeing`
        that carry mesh generators or spatial methods in `simulation_kwargs`.
    </Accordion>

    <Accordion title="Python API">
      **Fixes**

      * Time-monotonicity validation error message now reports the correct two adjacent values
        (the previous off-by-one showed the values just before the actual violation).
    </Accordion>

    <Accordion title="Data Processing">
      **Fixes**

      * Sign-convention detection no longer misclassifies low-current pulse datasets as all-rest;
        the rest threshold now scales relative to the 95th-percentile current so violations are
        caught on small-amplitude data.
    </Accordion>
  </AccordionGroup>
</Update>

***

<div className="flex justify-between items-center pt-2">
  <span />

  <a href="/changelog/2">Older updates →</a>
</div>
