Skip to main content
May 18, 2026
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.
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 numberCycle 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.
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.
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.
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 ResistanceRest 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.
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.