Parametric Dissolution Models

The Getting started example interpolates the dissolution data linearly. Dissolution can instead be smoothed with a parametric model fitted to each formulation's data: the fit irons out assay noise, evaluates cleanly between and beyond the sampled times, and summarises every profile in a few interpretable numbers. fit_dissolution does this and returns a DissolutionFit that goes into the pipeline in place of the raw data.

Models

Four standard sigmoidal forms are available. In all of them τ = max(t − tlag, 0), and the intercept int and lag tlag are held constant at zero unless you choose to estimate them.

ModelCurveFitted by default
HillDissolutionint + (finf − int) · τᵇ / (mdtᵇ + τᵇ)finf, mdt, b
WeibullDissolutionint + (finf − int) · (1 − exp(−(τ / mdt)ᵇ))finf, mdt, b
DoubleWeibullDissolutionfrac1-weighted sum of two Weibull termsfinf, frac1, mdt1, b1, mdt2, b2
MakoidBanakarDissolutionint + (fmax − int) · (τ / tmax)ᵇ · exp(b (1 − τ / tmax)) up to tmax, then fmaxfmax, tmax, b

Simulated data

first(diss_df, 8)
8×3 DataFrame
Rowformulationtimediss
StringFloat64Float64
1FormA0.00.0
2FormA0.5211240.0463696
3FormA1.074570.130579
4FormA1.410230.189439
5FormA1.98150.294384
6FormA2.467010.383808
7FormA3.073830.489692
8FormA3.951870.625072

Fitting

Pass the dissolution data and a model form tag. Each formulation is fitted by least squares; the result shows the fitted parameters with the residual sum of squares and convergence per formulation:

diss_fit = fit_dissolution(diss_df, WeibullDissolution())
DissolutionFit{WeibullDissolution}: 4 formulations, constant int, tlag
 Row │ formulation  finf     mdt       b        int  tlag  rss         converged 
─────┼───────────────────────────────────────────────────────────────────────────
   1 │ FormA        1.00001   4.00173  1.49831  0.0   0.0  7.80794e-7       true
   2 │ FormB        1.00001   8.00064  1.49945  0.0   0.0  1.26759e-6       true
   3 │ FormC        1.00001  10.0007   1.49977  0.0   0.0  6.70013e-7       true
   4 │ FormD        1.00001   6.0011   1.49935  0.0   0.0  7.7115e-7        true

The Simulated Data is generated from a Weibull with shape 1.5 and scales 4, 8, 10 and 6 h for FormA–FormD that dissolves completely, so the fit recovers mdt ≈ 4, 8, 10, 6, b ≈ 1.5 and finf ≈ 1 (the data are resampled linearly from the exact curve, hence the third decimal). int and tlag are held constant at zero unless asked otherwise. The parameters come back as a table, or per formulation as a named tuple or as the callable model:

coeftable(diss_fit)
4×6 DataFrame
Rowformulationfinfmdtbinttlag
StringFloat64Float64Float64Float64Float64
1FormA1.000014.001731.498310.00.0
2FormB1.000018.000641.499450.00.0
3FormC1.0000110.00071.499770.00.0
4FormD1.000016.00111.499350.00.0
coef(diss_fit, "FormA")
(finf = 1.000011690591569, mdt = 4.001726175000524, b = 1.4983079707420182, int = 0.0, tlag = 0.0)
weibull_a = dissolution_model(diss_fit, "FormA")
weibull_a([1.0, 4.0, 12.0])
3-element Vector{Float64}:
 0.11769205554770891
 0.6318901317475902
 0.9944013225036943

Constant and starting values

constantcoef names the parameters held constant at their initial value, and param sets that value. The tuple is the full set, so pinning the plateau at complete dissolution keeps the two default constants in the list. Without param, finf would be held at its default start, the observed maximum, which is not 1 for a profile that has not finished:

pinned = fit_dissolution(
    diss_df, WeibullDissolution();
    constantcoef = (:finf, :int, :tlag), param = (; finf = 1.0),
)
coeftable(pinned)
4×6 DataFrame
Rowformulationfinfmdtbinttlag
StringFloat64Float64Float64Float64Float64
1FormA1.04.001641.498350.00.0
2FormB1.08.00051.499490.00.0
3FormC1.010.00061.49980.00.0
4FormD1.06.0011.499380.00.0

Dropping tlag from constantcoef estimates a lag time, here started at half an hour (the simulated profiles have none):

coeftable(fit_dissolution(diss_df, WeibullDissolution(); constantcoef = (:int,), param = (; tlag = 0.5)))
4×6 DataFrame
Rowformulationfinfmdtbinttlag
StringFloat64Float64Float64Float64Float64
1FormA1.000014.001731.498310.01.0367e-9
2FormB1.000018.000641.499450.01.04288e-9
3FormC1.0000110.00071.499770.01.05718e-9
4FormD1.000016.00111.499350.01.0524e-9

Bounds live with the model and starting values are read off each profile; default_parameter_specs shows what a profile starts from:

form_a = diss_df[diss_df.formulation .== "FormA", :]
default_parameter_specs(WeibullDissolution(), form_a.time, form_a.diss)
5-element Vector{InVitroInVivoCorrelation.ParameterSpec{Float64}}:
 ParameterSpec(:finf, 1.0, 0.0, 1.5)
 ParameterSpec(:mdt, 4.009609922731674, 0.0, Inf)
 ParameterSpec(:b, 1.0, 0.1, 10.0)
 ParameterSpec(:int, 0.0, 0.0, 1.0)
 ParameterSpec(:tlag, 0.0, 0.0, Inf)

parameter_specs overrides these by name, so changing one bound is one ParameterSpec; everything else, including the default constants, stays:

coeftable(fit_dissolution(diss_df, WeibullDissolution(); parameter_specs = [ParameterSpec(:finf, 1.0, 0.0, 1.2)]))
4×6 DataFrame
Rowformulationfinfmdtbinttlag
StringFloat64Float64Float64Float64Float64
1FormA1.000014.001731.498310.00.0
2FormB1.000018.000641.499450.00.0
3FormC1.0000110.00071.499770.00.0
4FormD1.000016.00111.499350.00.0

Comparing model forms

rss gives the residual sum of squares per formulation. The Hill curve cannot match the Weibull-generated data, and the numbers say so:

hill_fit = fit_dissolution(diss_df, HillDissolution())
DataFrame(formulation = diss_fit.formulations, Weibull = rss(diss_fit), Hill = rss(hill_fit))
4×3 DataFrame
RowformulationWeibullHill
StringFloat64Float64
1FormA7.80794e-70.017949
2FormB1.26759e-60.0256613
3FormC6.70013e-70.0244105
4FormD7.7115e-70.0242597

Running the pipeline

The fit takes the place of the dissolution DataFrame:

result = run_ivivc(
    pk_df,
    diss_fit;
    fast_formulation = "IV",
    validation_formulation = "FormB",
    times = collect(range(0.0, 48.0, length = 101)),
);

Model parameters

coeftable(result.model)
4×3 DataFrame
Rowparameterconstantestimate
SymbolBoolFloat64
1abs_scalefalse0.988818
2abs_interceptfalse0.00264283
3time_scalefalse0.84087
4time_shiftfalse-0.0878416
result.pe_summary_df
1×7 DataFrame
Rowformulationauc_predictedauc_observedpe_auccmax_predictedcmax_observedpe_cmax
StringFloat64Float64Float64Float64Float64Float64
1FormB49.986750.4560.9301652.851712.878570.933208

Plots

result.dissolution_df holds the fitted curves evaluated on the data's time grid, so the dissolution plot shows the smoothed profiles:

dissolution_profiles(result)
Example block output
concentration_profiles(result)
Example block output
absorption_profiles(result)
Example block output
levy_plot(result)
Example block output
ivivc_correlation(result)
Example block output