Release notes for sbmlsim 0.9.0
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,SimulatorandScanResultreplaceScanSim,SimulatorSerialandXResult(#256).ScanSim,SimulatorSerial(sbmlsim.simulator.simulation_serial),XResult(sbmlsim.result.xresult),sbmlsim.simulation.rangeandDimension(dimension, index=, changes=)are removed. The migration:SimulatorSerial(model, **settings)isSimulator(**settings)with the same default tolerances, and the model is an argument of every call.run_simulation(simulation)andrun_scan(scan)aresimulator.run(model, simulation_or_scan), which returns aScanResult,simulate(simulation)issimulator.simulate(model, simulation), aTimecourseResult, andsimulator.load(path)loads a model and returns theRoadrunnerSBMLModel.set_timecourse_selections(selections)ismodel.set_selections(selections)of theRoadrunnerSBMLModel.ScanSim(simulation=sim, dimensions=[Dimension("dim", changes={"n": values})])isScan(sim, [Dimension("dim", values={"n": values})]), andindex=islabels=.xres.xdsisres.ds, anddim_mean,dim_std,dim_minanddim_maxareres.summary(dims, statistics=[...])with the statisticsmean,sd,cv,minandmaxand optional quantiles.to_tsv,to_dataframe,to_mean_dataframe,from_timecoursesandis_timecourseare removed;res.dsis thexarray.Datasetfor everything else.ExperimentRunner(..., simulator=Simulator(...))andSimulationExperiment.run(Simulator(...))take the new simulator, and theresultsof an experiment areScanResults.sbmlsim.utils.process_contextissbmlsim.parallel.process_context.
- A scan of 256 points or more runs on every CPU by default (#256).
Simulator(n_workers=None)runs a scan ofsbmlsim.parallel.POOL_THRESHOLDpoints or more in a pool of processes, so a script which runs such a scan, or setsn_workersto more than 1, must run it behindif __name__ == "__main__":, because the workers import the script again.Simulator(n_workers=1)runs serially. SimulationExperiment.save_resultswrites 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,SensitivityAnalysiswithLocalSensitivityAnalysis,SamplingSensitivityAnalysis,SobolSensitivityAnalysis,FASTSensitivityAnalysisandMorrisSensitivityAnalysis,SensitivityParameter,SensitivityOutput,AnalysisGroup, their pool and pickle cache,plot_S1_ST_indicesandS1_ST_barplotare removed. The migration:- An analysis is a design of
sbmlsim.simulation.sampling(local,sobol,fastormorris) as a dimension of aScan, a run withSimulator.run(model, scan, observables)and the indices ofsbmlsim.sensitivity.local,sobol,fastormorrison its result, seedocs/sensitivity.md. - The outputs of an analysis are observables, e.g.
Formula("px_max", "max(PX)"). - The difference scans of
ModelSensitivityare thelocaldesign, and its distribution scans arerandomdraws of relative distributions such asLogNormal(cv=0.1), with a seed and draws which stay positive.
- An analysis is a design of
Simulation experiments, data and figures
Data.get_datareturns anxarray.DataArrayinstead of a pintQuantity(#279). The array has the dimensions of its source, their coordinates andattrs["units"], andsbmlsim.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 inoveroracross, or is selected by one label. Select a point withData(..., sel={"dim": label})orPlot.add_data(..., sel=...), or draw one line per point withover="dim".sbmlsim.plot.padding.first_curveis replaced byline_values. initialize()checks the data of the tasks before anything is simulated (#279). The index of every taskDataofdata(), 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 aPKobservable without its parameter, an unknown simulation or model of a task, and aselwhose 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; readData.indexinstead, 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
ExperimentRunnerno 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, andrun_experiments(..., raise_on_failure=True)raises oneExperimentRunErrorafter the report is written.ExperimentResult.failedandrunner.failedtell whether something failed.- A test which relies on the exception of a run must use one of these.
run_experimentsreturns the results instead ofNone,SimulationExperiment.run(on_error=...)is new, andcreate_mpl_figures,save_mpl_figuresandsave_interactive_figurestake new arguments.
- A runner releases the results of an experiment once its outputs are written (#277, fixes #273). Reading
experiment.resultsafterExperimentRunner.run_experimentsraises aRuntimeError; passkeep_results=Trueto keep them.SimulationExperiment.runkeeps them by default. - The 17 figure settings are no longer attributes of
Figure(#284, fixes #260). Setting or readingfig_dpi,panel_width,legend_fontsize,legend_positionor another of them onFigure, on a figure or in the body of a subclass raises anAttributeErrorwith the migration:Figure.X = vbefore a run isfigure_settings={"X": v}ofExperimentRunner.run_experiments,run_experimentsorSimulationExperiment.run.Figure.X = vin the module of a model isfigure_settings = MappingProxyType({"X": v})on its base experiment, which every subclass inherits.fig.legend_position = "outside"isFigure(..., settings={"legend_position": "outside"})orfig.settings = {**fig.settings, "legend_position": "outside"}.- A default is read as
FigureSettings().X. Figure.widthandFigure.heightareNoneunless they are set, and the experiment JSON writesnull;figure.size(figure.resolved_settings(experiment))gives the size.
Units and datasets
- The unit registry no longer defines
IUandIU_per_ml(#284, fixes #263). An international unit is specific to a substance: a model defines its own unitIU, whichmodel.Qandexperiment.Qresolve, 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")orureg.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, sosbmlsim.Q(1, "IU/ml")works for the rest of the process, depending on the order of loading. DataSet.from_dfchecks 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_sdand_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 aValueErrorwhich 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 asml/min/(1.73*m^2)is written with a defined unit, e.g.BSA_1_73 = 1.73*m^2.sbmlsim.Qis a callable object, no longer the classureg.Quantity(#284).Q(...),Q.from_list,isinstance(x, Q)and pickling work;issubclass(t, Q),Q | None(aTypeErrorat import),Qas a type annotation and subclasses ofQdo not. Usesbmlsim.units.Quantityfor types.sbmlsim.Qtakes the units of pint only, and itsUndefinedUnitErrornamesmodel.Qandexperiment.Qfor the unit ids of a model.- The JSON of an
OptimizationResulthasunits(#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/minfor a parameter in the unit idper_min. - Smaller changes of units (#284):
copy.deepcopy(UnitsInformation)shares the registry,ParameterMappingraises aValueErrorwhen 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, so1/minandmin^-1agree.
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
timesat its times which are changes. The migration:res.sel(time=t)at a change gives both rows andres.isel(time=k)one of them.res.ds.interp,res.ds.sel(..., method="nearest")andres.ds.reindexraiseInvalidIndexErroron such a result;ScanResult.interpolateputs a result on other times.SensitivityResult.sel(time=t)and the sensitivity plots at a change take the state after it.- A
PKanalysis andScanResult.to_timecourseshand 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' = 1and a changeX = 0at 2 the value att=1is 1.0 instead of 0.676. meanover a jump is the exact integral (an intravenous case changes from 4.054 to 4.028), andmaxandminsee 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.
maxandminin the formula of aDatareduce along the time of each simulation (#257), not over all simulations of a scan, soY/max(Y)normalizes every simulation to its own maximum.meanandatare 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 asafter, 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 overDimension(id, *, values=None, simulations=None, models=None, at=None, labels=None). A dimension scans coupledvaluesof targets,simulationsormodels, and several dimensions span their product in C order. A value replaces its target wherever the simulation sets it, andat=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)hasload,compile,simulateandrun(model, scan, observables=None, *, time=None, keep=None, on_error="raise", progress=None). A point of a scan is the compiledPlanwith 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 onmodel.r, do not reach the workers.time=interpolates every timecourse onto a common grid in the worker.on_error="raise"raises aScanErrorfor the first failing point in the order of the scan with its labels and values,on_error="flag"givesNaNand a variablestatus.ScanResultwraps anxarray.Datasetwith the dimensions of the scan first and the time last,(*dims, time)on a grid or(*dims, _point)with the native time points padded withNaN. The labels and the changed values are coordinates,attrs["units"]has the unit of every variable and coordinate, and it hassummary,quantity,sel,isel,interpolateand netCDF through h5netcdf (to_netcdf,from_netcdf).sbmlsim.parallelowns every pool of the package:process_context,resolve_workers, the keptpool(n),start_pool,stop,shutdownandworker_cache. The workers ignore SIGINT, and Ctrl-C stops the pool of the run.RoadrunnerSBMLModel.set_selectionsandhas_selectionset and check the selections of a model.
- A parallel fit survives a dying worker (#256). The fit runs on
parallel.start_pooland keeps at mostn_coresrepeats in flight; a pool which breaks because a worker dies is started again (at mostMAX_POOL_RESTARTS = 3times), 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 reductionsmax(x),min(x),mean(x)(the time weighted mean, the trapezoidal integral divided by the time span) andat(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 declaredunitis 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_obsandpk.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.keepnames 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 thepkpdutils.NCAResultof aPKobservable andScanResult.to_timecourses(key)a timecourse aspkpdutils.Timecourses.
- The sampler (#258).
sbmlsim.simulation.samplingreturns the designs of a scan as dimensions:- The distributions
Uniform,LogUniform,Normal,LogNormal,Truncated,EmpiricalandFixedtake numbers in the unit of the target or quantities. A distribution without a location, e.g.Uniform(relative=0.2)orLogNormal(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,randomandlhs(with a Spearman rank correlation by a Gaussian copula or the reordering of Iman and Conover),sobol,fastandmorris(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) andpopulation(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=Nonedraws 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.targetsgives the target of every fitted parameter.
- The distributions
- The uncertainty analysis (#258) is a design of draws, a run and
ScanResult.summary;sbmlsim.sensitivity.uncertainty.plot_bandsdraws the median and a quantile band of a timecourse per label andplot_distributiona value per simulation as a histogram or a box per label. - Sensitivity indices on the result of a scan (#272).
sensitivity.localgives the central differencesrawandnormalizedat the reference,sobolgivesS1,STandS2with their intervals,fastgivesS1andST, andmorrisgivesmu,mu_star,sigmaandmu_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.
SensitivityResultholds<observable>.<index>over(parameter, *dims, [time])with units, the options of the analysis and netCDF, and hasindex(name),to_dataframeandclassify(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_indicesandplot_morrisreturn their figures and save them only when given a path.
- Observables in simulation experiments (#279).
SimulationExperiment.observables()returns theFormula,PKandCustomobservables by their id, aDataof a task reads one by its id or a PK parameter asData("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 overrow, 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=...)andPlot.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=...)withy=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.pointsholds what both serializers share, andSimulationExperiment.scan_dimensionandmodel_unitsgive 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.initializedecides the kind of every mapping from the dimensions of its observable aftersel(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
initializewarns about. - The report draws such mappings, the
DV/PREDtable, 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-observableandscan-task;x-observableis 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).
ExperimentResulthaserror,failed_figures,figuresanddatasets, 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 andexperiment.Q(value, unit, model=None)the ones of the models of an experiment fromsimulations()on, every whole identifier of a unit string, somg_per_min/kgworks. AFitParameter.unitmay 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.AFTERis the comparison of PEtab, which a PEtab problem read without thesbmlsimextension uses. The export reports the gapmeasurement-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 thanTIME_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
simulateintegrates past the end of a segment and interpolates its last row linearly, and the next change was applied to that state; a change ofkaat 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.interpolateand 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.roottolruns 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.99999999999999next to4.0after atime_shiftstopped a simulation withRuntimeError: 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;Scanlogs one warning per dimension and target which names the values, their times andDimension(at=...). - Figure settings no longer leak between experiments (#284, fixes #260).
Figure.legend_fontsize = 10in 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 attributeSimulationExperiment.figure_settings, inherited by subclasses) and the figure (Figure(settings=...)); an unknown key raises aValueError. A figure without an explicit size gets it from its panels when it is rendered, and the interactive pages have 72 pixels per inch whateverfig_dpiis. - 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. Qknows the unit ids of a model (#284, fixes #268).Q(0, "mg_per_min")raisedUndefinedUnitErrorwithout a hint, and aFitParameterwithunit="per_min"stopped the initialization of the fit; seemodel.Qandexperiment.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
DataSetowns a copy of itsuinfo(#283, fixes #267), sounit_conversionon 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_dfignores 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
NORMALIZEDtype are labelled as normalized instead of carrying the unit of the data, and a measurement atinfno 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
h5netcdfandh5py, its backend, are dependencies for the netCDF files of aScanResult(#256);h5pywas in thescimlextra.pkpdutils>=1.3.0is a dependency again, imported only when aPKobservable is compiled or evaluated (#257).- pip cannot install the
scimlextra on python 3.14, as in 0.8.5: every release ofpydantic-numpy(a dependency ofpetab-scimlthroughmkstd) which supports python 3.14 requirespydantic~=2.12.5, whilepymetadataand sbmlsim requirepydantic>=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.pycomputes a formula of the mass concentration, the PK analysis of pkpdutils and a custom function of midazolam over three doses (#257).examples/experiment_scans.pyruns 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.pyruns the local, Sobol, FAST and Morris analyses of a simple chain on the scan core with observables and a dimension of conditions (#272), andexamples/model_sensitivity.pyandexamples/demo/demo.pyuse 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
PKobservable and a figure of cmax and AUC over the dose (#281). examples/repressilator/repressilator_scans.pynormalizes every simulation to its own maximum (#257), and the HCTZ fitting readsData.indexinstead 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) anddocs/sensitivity.md(#272) are rewritten for the scan core. docs/data.mddescribes labelled arrays,seland the data of observables,docs/experiments.mdobservables, scans, released results and failing experiments, anddocs/plotting.mdcurves over points, bands and the figure settings (#279, #281, #283, #284).docs/fitting.mddescribes the kinds of fit mappings and the data at the time of a change,docs/petab.mdthe new gaps,docs/units.mdthe international unit and the unit ids of a model, anddocs/simulation.mdthe 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.pointsandmodel.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 withuv sync --locked, which fails whenpyproject.tomland the lock disagree, so a change of a dependency comes with itsuv lock. Theruffcheck 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). Theplain install, the tox environments and the release tools are not locked on purpose. - The job
plain installrunsscripts/plain_install.pyin 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 todevelopnever 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 markedslowand continuous integration skips them withpytest --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).