Build an interval specification from a description of the analysis - #622
Merged
Conversation
pknca_interval_table() takes when the interval runs, how the drug was given, how often, and what was collected, and returns the interval specification that describes it. Given only a start, an end, dosing, and route it chooses the parameters usually reported for that context, the AUC they are built on, and the imputation to calculate them from. Three things follow from the context rather than being asked for. Which AUC a bounded interval uses: a single dose is summarized by AUClast and the extrapolation to infinity, a repeated dose by the AUCint family, which interpolates at both interval boundaries. Whether concentrations are back-extrapolated to the time of a bolus. And which imputation applies, together with the parameters that must be kept out of it. That last is why a specification can have more than one row per interval. For an intravenous bolus, c0 is calculated without imputation while everything else uses it: before the first dose a predose measurement is contamination rather than the concentration at the dose, and c0 returns an observed nonzero concentration at the dose time in preference to back-extrapolating. Fixes three classification errors that building on it exposed, none of which the existing checks caught: * aucint.inf.obs and aucint.inf.pred were single-dose only. The "inf" in the name is the extrapolation used for the tail, not the end of the interval; over a bounded interval they give AUCtau, so they apply to any dosing pattern. Steady state was left with no interval AUC. * The standard errors and degrees of freedom produced alongside a sparse AUC were not flagged sparse in the registry, because pk.nca() only needs the flag to route a calculation. They are now classified through what they depend on. * The AUMC parameters had no basis to be built on, because the basis named only areas, so the mean residence times and steady-state volumes were unreachable. 174 of 207 parameters are reachable across the context grid. The 33 that are not are the AUCall family: whether AUCall differs from AUClast depends on values below the limit of quantification after the last measurable one, which is a property of the data rather than of the analysis, so no context chooses it and it is reached by name. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…-table # Conflicts: # NEWS.md
Bioavailability compares two administrations, and renal clearance needs an amount excreted from one matrix together with a plasma AUC from another. One interval cannot supply what either needs, so neither should be chosen automatically for it. `selection` gains a `secondary` element, declared on f and the three renal clearances and propagated through the dependency walk to their dose-normalized forms. pknca_interval_table() leaves them out of what a context gives while `include` still reaches them by name, so a urine interval now offers the amount excreted, the fraction excreted, and the collected volume, and renal clearance is asked for. Fraction excreted is deliberately not marked: it needs the amount excreted and the dose, both of which come from one profile. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
billdenney
commented
Aug 26, 2026
| interval_starts <- rep(start, length.out = max(length(start), length(end))) | ||
| interval_ends <- rep(end, length.out = max(length(start), length(end))) | ||
| long <- | ||
| do.call( |
Member
Author
There was a problem hiding this comment.
This is overly nested. Simplify it.
| first_in_human = | ||
| list( | ||
| dosing = "single", route = "extravascular", | ||
| include = c("clast.obs", "tlast", "span.ratio", "lambda.z.n.points") |
Member
Author
There was a problem hiding this comment.
Remove lambda.z.n.points from here
| mass_balance = | ||
| list( | ||
| dosing = "single", route = "extravascular", sample_type = "interval", | ||
| include = c("excretion_rate") |
| #' pknca_presets()$bioequivalence | ||
| #' @family Interval specifications | ||
| #' @export | ||
| pknca_presets <- function() { |
Member
Author
There was a problem hiding this comment.
Please move pknca_preset_definitions to just be this function unless there is a clear reason to have two functions.
* Build the long form with rep() rather than stacking a data frame per interval through do.call/lapply/rbind. The parameters are the same for every interval, so there is nothing to iterate over, and the row index interval_wider() groups on becomes a rep() instead of a match() on pasted keys. * Drop lambda.z.n.points from the first-in-human preset. * Drop a c() around a single string. * Fold pknca_preset_definitions() into pknca_presets(). The two existed only because the internal accessor was written first and then wrapped. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Closes #157.
pknca_interval_table()takes when the interval runs, how the drug was given,how often, and what was collected, and returns the interval specification that
describes it:
start_predose_conc0c0on its own rowstart_predose_conc0;c0gets noneceoistart_predose_conc0ceoistart_predose_conc0start_predosestart_cminpknca_presets()gives named argument sets (bioequivalence,first_in_human,mass_balance, …); explicit arguments override a preset.Why a specification can have more than one row
For an IV bolus,
c0is calculated without imputation while everything elseuses it. Before the first dose a predose measurement is contamination rather
than the concentration at the dose, and
pk.calc.c0()returns an observednonzero concentration at the dose time in preference to back-extrapolating. An
end-to-end test asserts
c0from the generated specification equalsc0calculated with no imputation at all.
The imputation pairing rules replace the per-parameter
impute_conflictfieldthe plan originally called for. An experiment across every parameter and every
imputation method (1180 interactions) found that almost none of the conflicts
are context-independent —
c0with a shifted predose is correct at steadystate and wrong for a first dose, and so on. Since this function chooses both
the parameters and the imputation, it can simply not pair them, which handles
the context-dependent cases correctly and needs no new registry field.
Three classification errors this exposed
None were caught by the 50 existing classification checks:
aucint.inf.obs/aucint.inf.predwere single-dose only. The "inf" names theextrapolation used for the tail, not the end of the interval — over a bounded
interval they give AUCtau. Steady state was left with no interval AUC at all.
sparse_auc_seand the other diagnostics produced alongside a sparse AUC werenot flagged sparse in the registry, because
pk.nca()only needs the flag toroute a calculation. They leaked into every dense context. Now classified
through what they depend on.
areas, so the mean residence times and steady-state volumes were unreachable.
Regression tests added for the first two.
Reachability
174 of 207 parameters are reachable across the 96-context grid. The 33 that are
not are the AUCall family, pinned as an exact list: whether AUCall differs from
AUClast depends on values below the limit of quantification after the last
measurable one, which is a property of the data rather than of the analysis, so
no context chooses it.
include = "aucall"reaches it, and a test shows that.The sweep needed a cache to run at all — 96 contexts × 28 dependency-closure
walks timed out. Basis families are cached alongside the classification and
dropped together when the registry changes.
80 new tests; full suite passes.
🤖 Generated with Claude Code