pypi sbmlsim 0.9.0

latest release: 0.9.1
3 hours ago

Release notes for sbmlsim 0.9.0

sbmlsim

A minor release built on a new scan core: Scan, Simulator and ScanResult replace ScanSim, SimulatorSerial and XResult, run the points of a scan serially or in a pool of processes, and compute observables (formulas, non-compartmental PK analyses and custom functions) from every simulation. The sampler, the uncertainty and sensitivity analyses, the figures of simulation experiments and the fits are built on it: the designs of the sampler are dimensions of a scan, figures draw one curve per point of a scan and bands over draws, and a fit compares values per simulation such as a cmax with data. The release also fixes 14 reported bugs of experiments, data, units, figures, output times and changes; among them, the output of a simulation has the state before and the state after a change at its time.

Breaking changes

Scans, simulators and results

  • Scan, Simulator and ScanResult replace ScanSim, SimulatorSerial and XResult (#256). ScanSim, SimulatorSerial (sbmlsim.simulator.simulation_serial), XResult (sbmlsim.result.xresult), sbmlsim.simulation.range and Dimension(dimension, index=, changes=) are removed. The migration:
    • SimulatorSerial(model, **settings) is Simulator(**settings) with the same default tolerances, and the model is an argument of every call. run_simulation(simulation) and run_scan(scan) are simulator.run(model, simulation_or_scan), which returns a ScanResult, simulate(simulation) is simulator.simulate(model, simulation), a TimecourseResult, and simulator.load(path) loads a model and returns the RoadrunnerSBMLModel.
    • set_timecourse_selections(selections) is model.set_selections(selections) of the RoadrunnerSBMLModel.
    • ScanSim(simulation=sim, dimensions=[Dimension("dim", changes={"n": values})]) is Scan(sim, [Dimension("dim", values={"n": values})]), and index= is labels=.
    • xres.xds is res.ds, and dim_mean, dim_std, dim_min and dim_max are res.summary(dims, statistics=[...]) with the statistics mean, sd, cv, min and max and optional quantiles. to_tsv, to_dataframe, to_mean_dataframe, from_timecourses and is_timecourse are removed; res.ds is the xarray.Dataset for everything else.
    • ExperimentRunner(..., simulator=Simulator(...)) and SimulationExperiment.run(Simulator(...)) take the new simulator, and the results of an experiment are ScanResults.
    • sbmlsim.utils.process_context is sbmlsim.parallel.process_context.
  • A scan of 256 points or more runs on every CPU by default (#256). Simulator(n_workers=None) runs a scan of sbmlsim.parallel.POOL_THRESHOLD points or more in a pool of processes, so a script which runs such a scan, or sets n_workers to more than 1, must run it behind if __name__ == "__main__":, because the workers import the script again. Simulator(n_workers=1) runs serially.
  • SimulationExperiment.save_results writes only the netCDF of every result, <sid>_<task>.nc, and no longer its TSV (#256).

Sensitivity analysis

  • The sensitivity analyses are computed on the result of a scan (#258, #272). ModelSensitivity (sbmlsim.simulation.sensitivity), SensitivitySimulation, SensitivityAnalysis with LocalSensitivityAnalysis, SamplingSensitivityAnalysis, SobolSensitivityAnalysis, FASTSensitivityAnalysis and MorrisSensitivityAnalysis, SensitivityParameter, SensitivityOutput, AnalysisGroup, their pool and pickle cache, plot_S1_ST_indices and S1_ST_barplot are removed. The migration:
    • An analysis is a design of sbmlsim.simulation.sampling (local, sobol, fast or morris) as a dimension of a Scan, a run with Simulator.run(model, scan, observables) and the indices of sbmlsim.sensitivity.local, sobol, fast or morris on its result, see docs/sensitivity.md.
    • The outputs of an analysis are observables, e.g. Formula("px_max", "max(PX)").
    • The difference scans of ModelSensitivity are the local design, and its distribution scans are random draws of relative distributions such as LogNormal(cv=0.1), with a seed and draws which stay positive.

Simulation experiments, data and figures

  • Data.get_data returns an xarray.DataArray instead of a pint Quantity (#279). The array has the dimensions of its source, their coordinates and attrs["units"], and sbmlsim.data.to_quantity(array, ureg) gives the quantity. The values a dimension of a scan sets are read as <dimension>.<target>, the plain name of a model symbol is its timecourse, also when the scan changes it, and the plain name of a changed target which the result does not keep raises.
  • A curve of a scan draws the points it names (#279, #281). In 0.8.5 a curve of a scan drew its first point without a word. SimulationExperiment.initialize() now raises for a dimension of a scan which a curve or a band reads unless it is its axis, is named in over or across, or is selected by one label. Select a point with Data(..., sel={"dim": label}) or Plot.add_data(..., sel=...), or draw one line per point with over="dim". sbmlsim.plot.padding.first_curve is replaced by line_values.
  • initialize() checks the data of the tasks before anything is simulated (#279). The index of every task Data of data(), the fit mappings and the figures is the time, an observable, a coordinate of the scan or a selection of the model; an unknown index, the id of a PK observable without its parameter, an unknown simulation or model of a task, and a sel whose dimension or label does not exist raise.
  • The data ids of an amount and of its concentration differ (#283, fixes #269). [S] is <task>__conc__S, a dot becomes __ (<task>__pk__cmax) and any other character which is not a letter, a digit or an underscore becomes _x<hex>_ (X' is <task>__X_x27_). This changes the keys of _data, get_data().name, the experiment JSON and string references. Code which parses data ids, e.g. y.sid.split("__"), now selects other data without an error; read Data.index instead, as the HCTZ example does.
  • The keys of an experiment must be full SIds, [a-zA-Z_][a-zA-Z0-9_]* (#283, fixes #266). Keys of models, datasets, simulations, observables, tasks, data, figures and fit mappings with -, ., spaces or commas raise, all invalid keys of an experiment in one error.
  • A run of an ExperimentRunner no longer raises for a failing experiment or figure (#283, fixes #270). ExperimentRunner(..., on_error="log") is the new default: the failure is logged with its traceback and recorded, and the other experiments and figures are written. The migration:
    • ExperimentRunner(..., on_error="raise") raises as before, and run_experiments(..., raise_on_failure=True) raises one ExperimentRunError after the report is written. ExperimentResult.failed and runner.failed tell whether something failed.
    • A test which relies on the exception of a run must use one of these.
    • run_experiments returns the results instead of None, SimulationExperiment.run(on_error=...) is new, and create_mpl_figures, save_mpl_figures and save_interactive_figures take new arguments.
  • A runner releases the results of an experiment once its outputs are written (#277, fixes #273). Reading experiment.results after ExperimentRunner.run_experiments raises a RuntimeError; pass keep_results=True to keep them. SimulationExperiment.run keeps them by default.
  • The 17 figure settings are no longer attributes of Figure (#284, fixes #260). Setting or reading fig_dpi, panel_width, legend_fontsize, legend_position or another of them on Figure, on a figure or in the body of a subclass raises an AttributeError with the migration:
    • Figure.X = v before a run is figure_settings={"X": v} of ExperimentRunner.run_experiments, run_experiments or SimulationExperiment.run.
    • Figure.X = v in the module of a model is figure_settings = MappingProxyType({"X": v}) on its base experiment, which every subclass inherits.
    • fig.legend_position = "outside" is Figure(..., settings={"legend_position": "outside"}) or fig.settings = {**fig.settings, "legend_position": "outside"}.
    • A default is read as FigureSettings().X.
    • Figure.width and Figure.height are None unless they are set, and the experiment JSON writes null; figure.size(figure.resolved_settings(experiment)) gives the size.

Units and datasets

  • The unit registry no longer defines IU and IU_per_ml (#284, fixes #263). An international unit is specific to a substance: a model defines its own unit IU, which model.Q and experiment.Q resolve, and code or data in IU define a unit before the data or the experiments are imported, e.g. sbmlsim.units.ureg.define("IU_insulin = 0.0347 * mg") or ureg.define("IU = count"). A unit definition of a model with a factor which is no prefix, e.g. IU = 0.0347 mg, is still defined in the registry of the package when the model is loaded, so sbmlsim.Q(1, "IU/ml") works for the rest of the process, depending on the order of loading.
  • DataSet.from_df checks the units of a column (#283, #284, fixes #275). Rows of one column with different units of one dimensionality are converted to the unit of the first row with a value, including their _sd and _se, so the numbers of such columns change. Units of another dimensionality, offset units (degC against kelvin), undefined or unreadable units, units with a factor and non-numeric values raise a ValueError which names the column and the units, where 0.8.5 logged an error. Define the unit before the import (ureg.define("cpm = count/min")) or correct the data; a unit with a factor such as ml/min/(1.73*m^2) is written with a defined unit, e.g. BSA_1_73 = 1.73*m^2.
  • sbmlsim.Q is a callable object, no longer the class ureg.Quantity (#284). Q(...), Q.from_list, isinstance(x, Q) and pickling work; issubclass(t, Q), Q | None (a TypeError at import), Q as a type annotation and subclasses of Q do not. Use sbmlsim.units.Quantity for types. sbmlsim.Q takes the units of pint only, and its UndefinedUnitError names model.Q and experiment.Q for the unit ids of a model.
  • The JSON of an OptimizationResult has units (#284). Files written before still load; files written by 0.9.0 cannot be read by an older sbmlsim. The parameter sets, results, Fisher information and profiles of a fit carry the resolved unit of pint, e.g. 1/min for a parameter in the unit id per_min.
  • Smaller changes of units (#284): copy.deepcopy(UnitsInformation) shares the registry, ParameterMapping raises a ValueError when a unit id means different units in the models a parameter reaches, and the units of the versions of a target are compared as units, so 1/min and min^-1 agree.

Changed results

  • The output has the time of a change after the start twice (#285): the state before the change and the state after it, in the steps of the integrator at every change and in an output of times at its times which are changes. The migration:
    • res.sel(time=t) at a change gives both rows and res.isel(time=k) one of them.
    • res.ds.interp, res.ds.sel(..., method="nearest") and res.ds.reindex raise InvalidIndexError on such a result; ScanResult.interpolate puts a result on other times.
    • SensitivityResult.sel(time=t) and the sensitivity plots at a change take the state after it.
    • A PK analysis and ScanResult.to_timecourses hand over the timecourse without the states before the changes, as pkpdutils expects.
  • Values of runs change, as corrections (#285):
    • The steps of the integrator end every segment, the last one included, in the state at its end instead of roadrunner's linear interpolation past it, which changes the last row of a simulation and the steps after a change.
    • A time of a grid before a change is interpolated toward the state before it: with X' = 1 and a change X = 0 at 2 the value at t=1 is 1.0 instead of 0.676.
    • mean over a jump is the exact integral (an intravenous case changes from 4.054 to 4.028), and max and min see the state before a change.
    • An event of the model at the time of a change fires after the change and once, and an event at the end of a simulation has its two rows.
    • The steps of the integrator of a model with events have another number of rows, and the last row of a segment of a model with events is its state a few units in the last place before the end, on the scale of the times of the segment, which differs from the state at the end in the last digits.
  • max and min in the formula of a Data reduce along the time of each simulation (#257), not over all simulations of a scan, so Y/max(Y) normalizes every simulation to its own maximum. mean and at are reductions of the observables, since data has no time points.
  • Fits whose data lie at the time of a change after the start change their cost (#285, fixes #262). By default such a data point is compared with the state before the change (FitSettings.data_at_change, see the fixes). Settings stored before read as after, the comparison they were fitted with, and the pinned cost of the HCTZ fit and every prediction of it are unchanged. The fits of pkdb_models named in #262 should be run again: empagliflozin (Heise2013, Heise2013a), aliskiren (Nussberger2002), canagliflozin, dapagliflozin (Kasichayanula2011a, Komoroski2009) and losartan (Ohtawa1993).
  • Output times closer than the resolution of the time are one output time (#285, fixes #261), and a data time of a fit is compared with the merged time.

Features

  • The scan core (#256, #249):
    • Scan(simulation, dimensions) runs a simulation over Dimension(id, *, values=None, simulations=None, models=None, at=None, labels=None). A dimension scans coupled values of targets, simulations or models, and several dimensions span their product in C order. A value replaces its target wherever the simulation sets it, and at= makes it a change at that time. Dimensions and scans do not change after they were created, and a run never changes the objects of the user.
    • Simulator(n_workers=None, **integrator_settings) has load, compile, simulate and run(model, scan, observables=None, *, time=None, keep=None, on_error="raise", progress=None). A point of a scan is the compiled Plan with other values, so nothing is compiled per point, and the points run in chunks, serially or in a kept pool of processes. The result does not depend on the number of workers, and a worker integrates with every integrator setting the model of the parent has; settings of roadrunner outside the integrator, e.g. of the steady state solver or the conserved moiety analysis set on model.r, do not reach the workers.
    • time= interpolates every timecourse onto a common grid in the worker. on_error="raise" raises a ScanError for the first failing point in the order of the scan with its labels and values, on_error="flag" gives NaN and a variable status.
    • ScanResult wraps an xarray.Dataset with the dimensions of the scan first and the time last, (*dims, time) on a grid or (*dims, _point) with the native time points padded with NaN. The labels and the changed values are coordinates, attrs["units"] has the unit of every variable and coordinate, and it has summary, quantity, sel, isel, interpolate and netCDF through h5netcdf (to_netcdf, from_netcdf).
    • sbmlsim.parallel owns every pool of the package: process_context, resolve_workers, the kept pool(n), start_pool, stop, shutdown and worker_cache. The workers ignore SIGINT, and Ctrl-C stops the pool of the run.
    • RoadrunnerSBMLModel.set_selections and has_selection set and check the selections of a model.
  • A parallel fit survives a dying worker (#256). The fit runs on parallel.start_pool and keeps at most n_cores repeats in flight; a pool which breaks because a worker dies is started again (at most MAX_POOL_RESTARTS = 3 times), and the repeats which were running run again one at a time, so only a repeat which breaks the pool alone fails.
  • Observables of a scan (#257). Simulator.run(model, scan, observables, keep=...) evaluates them in the workers on the native solution of every simulation, before any interpolation:
    • Formula(id, formula, unit=None) is the math of PEtab over the selections and other observables, with the reductions max(x), min(x), mean(x) (the time weighted mean, the trapezoidal integral divided by the time span) and at(x, t) over the time of each simulation. A formula of values per simulation is a value per simulation, e.g. ins / at(ins, 0) normalizes to the baseline. A declared unit is the unit of the observable everywhere, and a formula which mixes scales of one dimension, e.g. ng/ml and mg/l, raises when the scan is compiled.
    • PK(id, selection, *, dose=None, route=None, options=None, parameters=None) is the non-compartmental analysis of pkpdutils, a value per simulation <id>.<parameter> for every parameter, e.g. pk.cmax, pk.auc_inf_obs and pk.thalf, and <id>.flags. The doses are the values the plan of every point assigns to the dose target at their times, so a dose of a dimension and a multiple dosing need no further input.
    • Custom(id, function, unit, *, symbols, kind=ObservableKind.SCALAR) is a function of a module, called once per simulation with its time points and the values of its symbols.
    • keep names the observables the result keeps; the others are evaluated where a kept one needs them. A result of values per simulation only has no time dimension.
    • ScanResult.nca(id) gives the pkpdutils.NCAResult of a PK observable and ScanResult.to_timecourses(key) a timecourse as pkpdutils.Timecourses.
  • The sampler (#258). sbmlsim.simulation.sampling returns the designs of a scan as dimensions:
    • The distributions Uniform, LogUniform, Normal, LogNormal, Truncated, Empirical and Fixed take numbers in the unit of the target or quantities. A distribution without a location, e.g. Uniform(relative=0.2) or LogNormal(cv=0.1), is relative to the reference of its target, its value after the pre-initialization (references(model, targets, simulation)); parameters_of(model) lists the parameters an analysis of all parameters varies.
    • The designs local, random and lhs (with a Spearman rank correlation by a Gaussian copula or the reordering of Iman and Conover), sobol, fast and morris (the unit cube of SALib mapped through any distribution), fit_parameters (the normal of the Fisher covariance in the directions the data constrains), profile_parameters (from the profile likelihoods, within the bounds), fit_repeats (the best parameter sets of a fit) and population (covariates mapped to targets by a function of a module).
    • The record of a design (Dimension(design=...)) is in the provenance of the result, also in netCDF, so an analysis needs only the result; seed=None draws a seed and records it. Dimension(coordinates=...) carries arrays which no model sees, e.g. the covariates of a population.
    • The start values of a fit are drawn with the sampler and are the same as before for every seed and sampling type. FisherInformation.targets gives the target of every fitted parameter.
  • The uncertainty analysis (#258) is a design of draws, a run and ScanResult.summary; sbmlsim.sensitivity.uncertainty.plot_bands draws the median and a quantile band of a timecourse per label and plot_distribution a value per simulation as a histogram or a box per label.
  • Sensitivity indices on the result of a scan (#272). sensitivity.local gives the central differences raw and normalized at the reference, sobol gives S1, ST and S2 with their intervals, fast gives S1 and ST, and morris gives mu, mu_star, sigma and mu_star_conf:
    • The indices are computed for every observable, every label of the other dimensions of the scan, e.g. a dose or a condition, and every time point of a timecourse on a grid.
    • SensitivityResult holds <observable>.<index> over (parameter, *dims, [time]) with units, the options of the analysis and netCDF, and has index(name), to_dataframe and classify (the classification of the IPCS).
    • What a scan cannot determine is NaN: a zero reference, a failed point and an element which is constant within a tolerance derived from the relative tolerance of the integrator. The bootstrap of Sobol and FAST is reproducible for every seed.
    • plot_heatmap, plot_indices and plot_morris return their figures and save them only when given a path.
  • Observables in simulation experiments (#279). SimulationExperiment.observables() returns the Formula, PK and Custom observables by their id, a Data of a task reads one by its id or a PK parameter as Data("pk.cmax", task=...), and a task whose data read observables runs with them and keeps them next to its selections in one run. Data(sel=...) selects labels of a scan (a dimension the data has not is skipped) or the rows of a dataset by the values of its columns, a dataset column is an array over row, and function data broadcast by dimension name.
  • Figures over the dimensions of a scan (#281). Matplotlib and plotly draw them the same way:
    • Curve(over=...), Plot.curve(..., over=...) and Plot.add_data(..., over=...) draw one line per point of the named dimensions. The colours are shades of the colour of the curve or viridis, a second dimension sets the line style, the legend names the value and the unit of the changed target, and from eleven points a colour bar replaces the legend entries.
    • A value per simulation over a dimension, e.g. x=Data("dose.PODOSE", task=...) with y=Data("pk.cmax", task=...), draws cmax over the dose.
    • Plot.band(x, y, across, quantiles=(0.05, 0.95), median=True, over=None, ...) reduces a dimension of draws to two quantiles and the median when the figure is drawn.
    • initialize() checks from the definitions that every dimension a curve or a band reads is drawn or selected, and names the dimension and the fix.
    • sbmlsim.plot.points holds what both serializers share, and SimulationExperiment.scan_dimension and model_units give the dimension of a task and the units of its model.
  • Fits of values per simulation and of values over a dimension of a scan (#282). A fit mapping compares a value per simulation with data, FitData(..., xid=None, yid="pk.cmax") against one or several rows of a reference, or values over one dimension of a scan, FitData(..., xid="dose.PODOSE", yid="pk.cmax"), interpolated along the values of the dimension:
    • OptimizationProblem.initialize decides the kind of every mapping from the dimensions of its observable after sel (observation_kinds) and raises for anything else.
    • A scan computes the values and times of its points once and simulates every point with the fitted values applied first, so a value of a dimension wins over a fitted parameter of its target, which initialize warns about.
    • The report draws such mappings, the DV/PRED table, the goodness of fit and the Bland-Altman plots take their points, and the x label carries the target and its unit (OptimizationProblem.x_units).
    • The PEtab export raises for such mappings with the gaps scalar-observable and scan-task; x-observable is reported only for a timecourse whose x is not the time, and the gaps count the exported experiments only.
    • The fits of timecourses of plain simulations run as before; the residuals of the HCTZ fit are identical.
  • Failing experiments and figures are reported (#283). ExperimentResult has error, failed_figures, figures and datasets, the HTML, markdown and LaTeX reports list the failures, and a run ends with a summary.
  • The unit ids of a model are units (#284, fixes #268). model.Q(value, unit) resolves the unit ids of a model and experiment.Q(value, unit, model=None) the ones of the models of an experiment from simulations() on, every whole identifier of a unit string, so mg_per_min/kg works. A FitParameter.unit may be a unit id of the model of its target, and the tables of a report show it as written.
  • FitSettings.data_at_change (#285, fixes #262) decides which state a data point at the time of a change after the start is compared with:
    • DataAtChangeType.BEFORE (the default) compares it with the state before the change, e.g. a pre-dose trough or cumulative urine collected until a reset. The first point of a curve, the data point or points with the smallest time of a mapping which has data at a later time, takes the state after it, where a collection starts.
    • DataAtChangeType.AFTER is the comparison of PEtab, which a PEtab problem read without the sbmlsim extension uses. The export reports the gap measurement-at-change, and the version of the extension is 0.4.0.
  • sbmlsim.simulator.plan.merge_times(times, start, end) (#285, fixes #261) merges output times closer than TIME_RTOL * max(|start|, |end|, end - start) (TIME_RTOL = 1e-9) into the smallest time of a cluster and gives the index of every given time; a simulation and the data times of a fit share it.

Fixes

  • A change sees the state at its time (#285). roadrunner 2.10 has no stop time: its variable step simulate integrates past the end of a segment and interpolates its last row linearly, and the next change was applied to that state; a change of ka at 24 h to its own value moved [C] by -6 % at the change and by -1.4 % after it. sbmlsim asks CVODE for exact output at the end of every segment, which also fires an event of the model roadrunner kept pending.
  • The output at a change has the states before and after it (#285). A grid of time=, ScanResult.interpolate and the figures are exact at a change: a time before a change sees the state before it, the time of a change the state after it, and a figure draws the jump vertically instead of a slanted line into it.
  • Events of the model at changes and at the end (#285). An event at the time of a change fires after the change and once, an event in roadrunner's overshoot past the end of a segment no longer fires early at the change, an event at the end of a simulation has its two rows like an event inside it, and a segment past whose end roadrunner fired an event runs again with exact output.
  • Results no longer depend on the process or on the number of workers (#285). roadrunner 2.10 fixes the root tolerance of CVODE at the first integration of a process and reports the rows of an event at that distance before and after it, so the steps of one simulation differed between a parent and a worker and between test orders. sbmlsim.model.roottol runs a probe before the first model of a process and in every worker, which fixes the root tolerance where sbmlsim integrates first and measures it otherwise, and both rows of an event get the time of the event. In a process in which roadrunner integrated before sbmlsim loaded a model, sbmlsim warns once: there the steps lack a first step of the integrator after an event which is shorter than roadrunner's root tolerance. Load a model with sbmlsim before integrating with roadrunner directly.
  • A simulation with a negative start and an event or a change at 0 no longer fails in CVODE (#285). A segment stops a normal time before its end on the scale of its times, never at a subnormal time, a segment shorter than CVODE can integrate is not integrated, and a dose event at two changes a few units in the last place apart fires once.
  • Output times which differ by about 1e-14 no longer collide (#285, fixes #261). Digitized data times such as 3.99999999999999 next to 4.0 after a time_shift stopped a simulation with RuntimeError: The output times of the segment ... are not distinct in local time.
  • Data at the time of a change is no longer compared with the state after the change (#285, fixes #262), see FitSettings.data_at_change.
  • A scan warns when it replaces a target which the simulation sets to different values (#285, fixes #264). A dimension without at= replaces its target wherever the simulation sets it, e.g. also the stop of an infusion; Scan logs one warning per dimension and target which names the values, their times and Dimension(at=...).
  • Figure settings no longer leak between experiments (#284, fixes #260). Figure.legend_fontsize = 10 in one experiment changed every later figure. FigureSettings (sbmlsim.plot.plotting) is a frozen dataclass of the 17 settings, which cascade, the more specific wins: the defaults, the run, the experiment (the class attribute SimulationExperiment.figure_settings, inherited by subclasses) and the figure (Figure(settings=...)); an unknown key raises a ValueError. A figure without an explicit size gets it from its panels when it is rendered, and the interactive pages have 72 pixels per inch whatever fig_dpi is.
  • IU is no longer a mass of insulin for every substance (#284, fixes #263). Q(1, "IU/ml") was a mass concentration of insulin, so data in IU of any other substance was converted with the factor of insulin without a word.
  • Q knows the unit ids of a model (#284, fixes #268). Q(0, "mg_per_min") raised UndefinedUnitError without a hint, and a FitParameter with unit="per_min" stopped the initialization of the fit; see model.Q and experiment.Q.
  • SimulationExperiment(sid="X") keeps the passed sid (#283, fixes #259).
  • The keys of an experiment are checked as a whole (#283, fixes #266); before, only their prefix was checked.
  • A slice, column or copy of a DataSet owns a copy of its uinfo (#283, fixes #267), so unit_conversion on it no longer changes the parent.
  • An amount and its concentration get different data ids (#283, fixes #269), and every generated id is a valid SId.
  • One failing experiment or figure no longer stops a run (#283, fixes #270). A failing experiment releases its results and closes its figures, files an earlier run left for figures which failed now are removed, and an experiment which overrides run, even with the old signature, is caught as well.
  • DataSet.from_df ignores the unit of a row without a value, sd or se (#283, fixes #275), and no longer modifies the DataFrame of the caller.
  • A run of many experiments no longer holds all their results and figures (#277, fixes #273, #276). An experiment kept its matplotlib figures after saving them, each with the pixel buffer of its last rendering, and the runner kept the results of every experiment until the end of the run.
  • The report of a perfect fit no longer crashes (#282). Its AIC and BIC are -inf, log panels without positive values are drawn linear, and the cost bar starts at zero.
  • The metrics of a fit are keyed by experiment and mapping (#282), so two studies which share a mapping key are no longer pooled. Residuals of the NORMALIZED type are labelled as normalized instead of carrying the unit of the data, and a measurement at inf no longer breaks the x limits.
  • The matplotlib serializer sets the scale of an axis before its bounds, so a log axis with one bound keeps its margin (#279).

Performance

  • A scan runs in parallel (#256). A scan of 1e4 points of the repressilator takes 2.35 s on four workers instead of 7.88 s on one, and a serial scan is as fast as in 0.8.5.
  • A run of simulation experiments holds one experiment at a time (#277). A run of the 105 experiments of acetaminophen of pkdb_models needs 3.8 GB instead of 19.5 GB with the results released and about 1.2 GB with the figures released as well; midazolam at 300 dpi needs 3.1 GB instead of 7.1 GB.
  • The common grid of a scan of simulations which differ only in their changes is built in linear time (#285), instead of quadratic in the number of times, e.g. three outputs of 30000 times in 0.05 s instead of 11 s.
  • A simulation of a model with events costs about 30 to 40 µs more (#285), for the stop before the end of a segment; a model without events is unchanged.

Dependencies

  • h5netcdf and h5py, its backend, are dependencies for the netCDF files of a ScanResult (#256); h5py was in the sciml extra.
  • pkpdutils>=1.3.0 is a dependency again, imported only when a PK observable is compiled or evaluated (#257).
  • pip cannot install the sciml extra on python 3.14, as in 0.8.5: every release of pydantic-numpy (a dependency of petab-sciml through mkstd) which supports python 3.14 requires pydantic~=2.12.5, while pymetadata and sbmlsim require pydantic>=2.13.5. uv installs it, and pip installs it on python 3.13; sbmlsim without the extra installs with pip on both.

Examples

  • examples/observables.py computes a formula of the mass concentration, the PK analysis of pkpdutils and a custom function of midazolam over three doses (#257).
  • examples/experiment_scans.py runs scans and observables in a simulation experiment: the PK parameters of midazolam over three doses as labelled arrays (#279), curves per dose, cmax over the dose and a band over Latin hypercube draws (#281), and a fit of a parameter to a synthetic table of cmax over dose (#282).
  • examples/sensitivity/sensitivity_example.py runs the local, Sobol, FAST and Morris analyses of a simple chain on the scan core with observables and a dimension of conditions (#272), and examples/model_sensitivity.py and examples/demo/demo.py use the sampler (#258).
  • The demo draws the initial values as curves with a colour bar, the glucose dose response draws the hormones as observables over the dose dimension instead of its own matplotlib figure, and HCTZ Patel 1984 has a dose scan with a PK observable and a figure of cmax and AUC over the dose (#281).
  • examples/repressilator/repressilator_scans.py normalizes every simulation to its own maximum (#257), and the HCTZ fitting reads Data.index instead of parsing data ids (#283).

Documentation

  • New pages on observables (docs/observables.md, #257) and on sampling and uncertainty (docs/sampling.md, #258); docs/scans.md (#256) and docs/sensitivity.md (#272) are rewritten for the scan core.
  • docs/data.md describes labelled arrays, sel and the data of observables, docs/experiments.md observables, scans, released results and failing experiments, and docs/plotting.md curves over points, bands and the figure settings (#279, #281, #283, #284).
  • docs/fitting.md describes the kinds of fit mappings and the data at the time of a change, docs/petab.md the new gaps, docs/units.md the international unit and the unit ids of a model, and docs/simulation.md the output at a change and the merged output times (#282, #284, #285).
  • The API reference has pages for sbmlsim.parallel, result.scan, simulator.simulator, simulator.worker, simulation.sampling, sensitivity.indices, sensitivity.result, sensitivity.uncertainty, plot.points and model.roottol (#256, #258, #272, #281, #285).

Development

  • The environment is locked in uv.lock (#278). It pins the environment for linux, macOS and windows; every job of the workflows syncs with uv sync --locked, which fails when pyproject.toml and the lock disagree, so a change of a dependency comes with its uv lock. The ruff check reads its version from the lock, the pre-commit hooks of ruff and ty follow the locked versions, and dependabot moves the lock in one grouped pull request (versioning-strategy: lockfile-only). The plain install, the tox environments and the release tools are not locked on purpose.
  • The job plain install runs scripts/plain_install.py in an install without extras, so a missing runtime dependency fails continuous integration (#256).
  • Continuous integration is reduced (#280). The tests run python 3.14 on linux, macOS and windows, python 3.13 runs locally with tox run-parallel, a run of a pull request is cancelled by the next push while a push to develop never is, uv caches the interpreters as well, the release builds without the cache, and dependabot runs monthly. The long end-to-end runs of examples are marked slow and continuous integration skips them with pytest --skip-slow.
  • The speed of the scan core is measured on demand with pytest -m benchmark -n 0 -s tests/simulator/test_benchmark.py (#256).
  • The design documents and plans of coding agents are removed from the repository and ignored (#280).

Don't miss a new sbmlsim release

NewReleases is sending notifications on new releases.