PyBNF experimental design (pybnf.design)¶
The pybnf.design package works out which measurement to make next. It
reads the expected Fisher information pybnf.gradient.assemble_fisher_hessian()
already builds for the gntr optimizer, and rests on one fact about it: the
information is a plain sum over the measured points, so the information a
planned measurement would add is that measurement’s own term
(pybnf.gradient.iter_fisher_points()).
It has four parts: candidates, which enumerates the measurements a design may choose from and scores each by handing the Fisher assembly a one-row dataset holding the model’s own prediction at that point; criteria, which reduces an information matrix to the single number two designs are compared on; greedy, which chooses the measurements one at a time; and report, which writes the recommendation together with the confidence intervals it is expected to produce.
The user-facing account – what a design may recommend, the criteria, and the grid controls that let it propose a time you have never measured – is in Experimental design (what to measure next).
Configuration¶
The configuration keys an experimental design reads (#574).
These live here, in the design package, rather than beside one method, because two job types read
the same keys: job_type = design runs a design on its own, and job_type =
profile_likelihood can end by recommending one. Both schemas inherit this class, so the keys
mean the same thing and are documented once (ADR-0006 co-locates a method’s own keys with the
method; a set of keys shared by two methods has to sit somewhere both can see).
- class pybnf.design.config.DesignFields(*, design_points: int = 5, design_criterion: str = 'a', design_target: Any = None, design_observables: Any = None, design_confidence: float = 0.95, design_grid: int = 0, design_t_end: float = 0.0)[source]¶
Optimal experimental design settings, shared by
job_type = designand the design report aprofile_likelihoodrun can write.design_pointsis how many measurements to recommend. The same measurement may be recommended more than once, which means measure it that many times.design_criterionis what makes one design better than another:afor the average variance of the parameters (the default, and the classical c-criterion whendesign_targetnames a single parameter),dfor the volume of the joint confidence region,efor the worst-determined direction.design_targetnames the parameters the design is aimed at. Absent, it aims at all of them. Only the A-criterion can use it; the other two are properties of the whole information matrix.design_observablesrestricts the candidate measurements to a named set of observables, for when only some assays can actually be run. Absent, every observable the fit already measures is a candidate.design_confidenceis the confidence level of the predicted intervals in the report. It has the same meaning asprofile_likelihood_confidence, and aprofile_likelihoodrun that writes a design report uses that key instead so the two halves of its output agree.design_gridanddesign_t_endwiden what the design is allowed to recommend. A design can only propose a time the model is already simulated at, and for a time course PyBNF simulates the times the data was measured at, so by default the only new measurement it can propose is a repeat of an existing one.design_gridadds that many extra simulated times, spread evenly from the first measurement out todesign_t_end(which defaults to the last measurement). Set both to let a design say “measure at a time you have never measured”, which is usually the point of asking.- model_config: ClassVar[ConfigDict] = {'arbitrary_types_allowed': True, 'extra': 'forbid', 'populate_by_name': True, 'validate_by_alias': True, 'validate_by_name': True}¶
Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict].
Candidates¶
Enumerating the measurements a design may choose from, and what each one would tell you (#574).
A candidate measurement is one observable, in one experiment, at one time. Two rules keep the candidate space honest and cheap:
The observable has to be one that experiment already measures. Then its noise model is known, because it is the one the fit is already using. Proposing an observable that has never been measured would mean inventing a precision for an assay nobody has run, and the answer would depend entirely on that invented number.
The time has to be one the simulation already passes through. The sensitivities at every simulated time were computed when the best fit was scored, so every candidate is free: no model is re-solved to enumerate or to score the candidate space.
Scoring one candidate means asking what the information matrix would gain if that point were
measured. PyBNF can already answer that, because the information is a sum over measured points
and iter_fisher_points() yields the points one at a time. So a candidate is
scored by handing the machinery a planned measurement: a one-row dataset at the candidate
time, whose value is the model’s own prediction at the best fit. That is what the expected Fisher
information means – the information you expect from data generated by the fitted model – and it
is why every noise model comes along for free. A noise scale read from a data column takes the
value from the nearest real measurement of that same observable, which assumes the planned
measurement is as precise as the ones already made; a scale that is a fitted parameter, a constant,
or a function of the prediction needs no assumption at all.
- class pybnf.design.candidates.CandidateMeasurement(model: str, experiment: str, observable: str, time: float, independent_variable: str = 'time')[source]¶
One measurement a design may recommend: an observable, in an experiment, at a time.
independent_variableis that experiment’s own name for its first column, so a report can saytime = 4.5for a time course and use the right word for anything else.
- class pybnf.design.candidates.CandidateSet(measurements: list = <factory>, blocks: list = <factory>)[source]¶
Every candidate measurement, with the information matrix each one would add.
blocks[i]is the(n_param, n_param)matrix candidatemeasurements[i]contributes, in sampling space, ready to be added to a baseline information matrix.
- class pybnf.design.candidates.DesignExperiment(model: str, suffix: str, sim_data: Any, exp_data: Any, routing: Any)[source]¶
One scored experiment at the best fit, with everything the design needs to reason about it.
sim_datais the simulated trajectory carrying the forward-sensitivity tensor,exp_datathe measurements already taken, androutingthe free-parameter-to-sensitivity-column map the gradient path built.modelandsuffixname the experiment, so a recommendation can say which one it is about; the suffix is also the scoring key the objective resolves a per-series scale against.- as_gradient_tuple()[source]¶
The
(sim_data, exp_data, routing, data_key)shape every gradient assembler takes.
- property label¶
How this experiment is named in a report.
- pybnf.design.candidates.baseline_information(objective, experiments, free_params)[source]¶
The information the existing data already carries, at the best fit.
Every design is judged as an addition to this, because that is the question being asked: given what has already been measured, what should be measured next.
- pybnf.design.candidates.candidate_information(objective, experiments, free_params, observables=None)[source]¶
Enumerate every candidate measurement and the information each one would add.
Walks each experiment’s measured observables over every time on that experiment’s simulated grid.
observablesoptionally restricts the walk to a named set of observables, for a user who can only run some of the assays.Returns a
CandidateSet. The order is stable – experiments in the order given, observables alphabetically, times in simulated order – so a design run is reproducible and ties break the same way every time.
- pybnf.design.candidates.independent_variable(exp_data)[source]¶
The name of an experiment’s independent variable: its first column, which is how every other consumer of a PyBNF dataset identifies it.
- pybnf.design.candidates.measured_observables(objective, experiment)[source]¶
The observables this experiment actually measures, in a stable order.
A column that is present but entirely blank is not measured, so it is left out: its noise model has never been exercised and there is no nearest real measurement to take a data-column noise scale from.
Criteria¶
Reducing an information matrix to one number, so two designs can be compared (#574).
The expected Fisher information F is a square matrix, one row and column per free parameter,
in sampling space (ADR-0029 – so a log-scaled parameter’s entry is about its order of
magnitude, which is the scale it is fitted on). Its inverse is the covariance matrix the fit
would have, so (F^-1)_kk is the variance of parameter k and sqrt(threshold *
(F^-1)_kk) is the half-width of that parameter’s confidence interval in the quadratic
approximation – the same interval a profile-likelihood run traces, for the same threshold.
A criterion turns that matrix into a single score so designs can be ranked:
'a'– the summed variance of the parameters, or of a named subset. With one parameter named this is the classical c-criterion.'d'– the log determinant, which is the volume of the joint confidence region.'e'– the smallest eigenvalue, which is the worst-determined direction.
Singular information is not a numerical accident to be smoothed away. A parameter the data cannot
constrain at all leaves a direction with no information in it, and the honest reading is an
infinite variance, not a large one. Every function here decides that with one shared rule: an
eigenvalue below SINGULAR_TOL times the largest one counts as zero, and a parameter with
any weight on such a direction has infinite variance.
- pybnf.design.criteria.CRITERIA = ('a', 'd', 'e')¶
The criteria a design run may be scored with.
- pybnf.design.criteria.CRITERION_NAMES = {'a': 'A-optimal (average parameter variance)', 'd': 'D-optimal (confidence region volume)', 'e': 'E-optimal (worst-determined direction)'}¶
The full name of each criterion, for messages and report headers.
- pybnf.design.criteria.NULL_COMPONENT_TOL = 1e-10¶
How much of a parameter’s own axis has to lie in the uninformed directions before its variance is infinite rather than merely large. Squared weights, so this is a very small angle.
- pybnf.design.criteria.SINGULAR_TOL = 1e-10¶
An eigenvalue this far below the largest one carries no information. Relative, because the information matrix has the units of the data and can sit anywhere on the number line.
- pybnf.design.criteria.criterion_score(information, criterion, targets=None)[source]¶
The criterion as something to maximize, so the selection loop never has to ask which way a given criterion runs. The A-criterion’s summed variance is negated; the others are already amounts of information.
- pybnf.design.criteria.criterion_value(information, criterion, targets=None)[source]¶
The criterion read the way a person would state it: a summed variance for
'a', a log determinant for'd', the smallest eigenvalue for'e'.targetsis the list of parameter indices the A-criterion sums over (None-> all of them). Uselower_is_better()to know which direction is an improvement.
- pybnf.design.criteria.interval_half_widths(information, threshold)[source]¶
Each parameter’s confidence-interval half-width in sampling space, at the
Delta chi2threshold a profile-likelihood run would use.This is the quadratic (Wald) approximation to the profile interval: the profile of a parameter near the optimum is the parabola
Delta chi2 = (theta_k - theta*_k)^2 / (F^-1)_kk, which crosses the threshold at+- sqrt(threshold * (F^-1)_kk). For a linear model the approximation is exact. Infinite for a parameter with infinite variance, which reads as an open interval.
- pybnf.design.criteria.is_singular(information)[source]¶
Whether any direction in parameter space carries no information at all.
A singular information matrix means some combination of the parameters is invisible to the data. No amount of care with the criterion changes that; the design has to add a measurement that sees the missing direction.
- pybnf.design.criteria.log_determinant(information)[source]¶
log det F, or-infwhen the information is singular (a confidence region of unbounded volume).
- pybnf.design.criteria.lower_is_better(criterion)[source]¶
Whether a smaller
criterion_value()is the better design.The A-criterion is a variance, so smaller is better; the other two are amounts of information, so larger is.
criterion_score()hides this (it is always maximized) but a report has to say which way its numbers read.
- pybnf.design.criteria.null_space_gain(information, block, targets=None)[source]¶
How much of
blockfalls in the directionsinformationcurrently knows nothing about.While the information is singular every criterion is pinned at its worst value – an infinite variance, a log determinant of
-inf, a smallest eigenvalue of zero – so none of them can tell two candidates apart. This can, and it asks the right question at that moment: of the directions the data does not yet see, how much does this measurement see? The selection loop uses it until the information becomes invertible and then goes back to the requested criterion.targetsrestricts the accounting to the uninformed directions that the target parameters actually lie along, so a c-criterion run is not sent off to fix a direction nobody asked about.
- pybnf.design.criteria.parameter_variances(information)[source]¶
Each parameter’s variance – the diagonal of the inverse information – in sampling space.
Infinite for a parameter that lies (even partly) along a direction the data does not constrain, which is the correct reading rather than a failure: no finite confidence interval exists for it. Computed from the eigendecomposition rather than by inverting, so the uninformed directions can be recognized instead of producing a huge finite number.
- pybnf.design.criteria.smallest_eigenvalue(information)[source]¶
The information in the worst-determined direction, floored at zero (a tiny negative eigenvalue is rounding: every term summed into the matrix is positive semi-definite).
- pybnf.design.criteria.unidentified_parameters(information, param_names)[source]¶
The parameters this information matrix leaves with an infinite variance.
Called on the information of the largest possible design – everything already measured plus every candidate at once – it names the parameters no experiment in the candidate space can pin down. That is a structural statement about the model and the observables, not a shortage of data, so it is reported rather than optimized around.
Selection¶
Choosing the best few measurements out of the whole candidate space (#574).
Picking the best five of two hundred candidate measurements is a subset-selection problem, not a continuous one: there are far too many subsets to try them all. The standard answer, and the one here, is to take the measurements one at a time, each time adding whichever remaining candidate improves the criterion most. It is not guaranteed to find the very best subset, but it is simple, it always terminates, and for the D-criterion it is the classical exchange algorithm’s forward half.
The same candidate may be chosen more than once. That is not a bug to be suppressed: choosing a point twice means measuring it twice, and it is exactly the right recommendation when the precision of one measurement, rather than the shape of the trajectory, is what limits you.
One special case has to be handled explicitly. When the information matrix says nothing at all
about a direction the criterion cares about, the criterion sits at its worst possible value for
every candidate and cannot tell them apart – an infinite variance is an infinite variance whatever
you add to it. The selection loop notices and asks a different question until that stops being
true: of the directions nothing yet sees, which candidate sees the most
(null_space_gain())? Then it goes back to the requested criterion for
the rest of the picks. A design aimed at one parameter is not blocked by some other combination
of parameters being invisible, so this only fires when the target itself is the problem.
- class pybnf.design.greedy.DesignResult(criterion: str, targets: list, target_names: list, measurements: list = <factory>, trace: list = <factory>, baseline: ~typing.Any = None, information: ~typing.Any = None, escaped_singular: int = 0, truncated: bool = False)[source]¶
A finished design: what to measure, and what it is expected to buy.
measurementsare in the order they were chosen, repeats included, so reading them in order shows how much each successive measurement is still worth.baselineis the information the existing data already carries andinformationis what it would become;traceis the criterion after each pick, so a design that stops paying off is visible rather than implied.- property baseline_value¶
The criterion before any of the recommended measurements are made.
- grouped()[source]¶
The recommendation as
(measurement, replicates, first_rank)in the order the measurements were first chosen – the reading a person wants, where choosing one point three times is one row saying “measure it three times”.
- property value¶
The criterion once every recommended measurement has been made.
- pybnf.design.greedy.improvement(result)[source]¶
How much better the criterion got, as a plain ratio, or
Nonewhen it cannot be stated.For the A-criterion this is the factor the summed variance shrank by, so 4.0 means the variance is a quarter of what it was and the confidence interval is half as wide. A baseline that was infinite (a parameter the existing data cannot determine at all) has no ratio, which is itself the headline: the design goes from no answer to an answer.
- pybnf.design.greedy.require_identifiable(baseline, candidates, param_names, targets)[source]¶
Refuse a design whose targets no experiment in the candidate space could ever pin down.
Adding every candidate at once is the most a design over this space can know. A target still left with an infinite variance there is not short of data: no measurement of these observables, at any simulated time, tells the model apart along that direction. That is structural non-identifiability, and the fix is a different model or a different observable, not a different design.
- pybnf.design.greedy.resolve_targets(variables, names, criterion)[source]¶
The parameter indices the A-criterion sums variances over, validated against the fit.
namesempty or absent means every free parameter. Naming exactly one makes this the classical c-criterion: minimize the variance of that one parameter, which is what a profile-likelihood verdict about a single parameter asks for.Targets are refused for the D and E criteria rather than quietly ignored. Both are properties of the whole information matrix – a volume and a worst direction – so restricting them to a subset of parameters means something different from what a reader would assume, and a request that cannot be honoured as written should say so.
- pybnf.design.greedy.select_design(baseline, candidates, n_points, criterion, targets, param_names)[source]¶
Choose
n_pointsmeasurements, one at a time, each the best next addition.baselineis the information the existing data carries,candidatestheCandidateSetto choose from. Returns aDesignResult. Ties break toward the earliest candidate, which is the earliest time of the first observable of the first experiment, so the same inputs always give the same design.
Report¶
Writing a design down: what to measure, and what it is expected to buy (#574).
The report has two halves, because a recommendation nobody can check is not worth much.
The first half is the recommendation itself: the measurements to make, in the order they were
chosen, with a count when the same point is chosen more than once. The second half is the reason,
stated in the units the user has already seen from a profile-likelihood run – each parameter’s
confidence interval as it stands now, and as it would be once the recommended measurements are in
hand. Those predicted intervals come from the same information matrix the design was chosen with,
read through the quadratic approximation to the profile: the interval is
theta* +- sqrt(threshold * variance) in the parameter’s own fitted scale. For a linear model
that is exact, and for anything else it is the local approximation, which is also all the design
itself ever claimed to be.
- pybnf.design.report.format_design_summary(result, variables, u_star, threshold)[source]¶
The same design as a short block of lines for the terminal: what to measure, and what it does to the intervals of the parameters the design was aimed at.
- pybnf.design.report.predicted_intervals(result, variables, u_star, threshold)[source]¶
Every parameter’s interval before and after the design, plus how much it shrinks.
Each row is a dict with the parameter’s name, its value at the best fit, the interval the existing data supports, the interval the design would support, and
width_ratio– the designed half-width over the current one, so 0.5 means the interval halves. The ratio isNonewhen the current interval is open, which is the strongest result there is: the design replaces no answer with an answer.