# Dynamic predictive microbiology models

## Overview

The dynamic modelling helpers in `predmicror` are designed for prediction and fitting when environmental conditions change over time. This is useful, for example, when temperature varies during storage, transport, processing, or abuse scenarios.

The basic workflow is:

1. define an environmental profile with `dynamic_profile()`;
2. simulate microbial behaviour with `predict_dynamic_growth()` or `predict_dynamic_inactivation()`;
3. fit selected dynamic parameters with `fit_dynamic_growth()` or `fit_dynamic_inactivation()`;
4. inspect fitted values, residuals, metrics, and sensitivity coefficients.

## A dynamic temperature profile

A dynamic profile stores time-varying environmental conditions. At present, the most common use case is temperature as a function of time.

    profile <- dynamic_profile(
      time = c(0, 5, 10, 15, 20, 25, 30),
      temperature = c(10, 4, 2, 15, 15, 2, 10)
    )

    profile
    #> Dynamic environmental profile
    #>   points: 7
    #>   time range: [0, 30]
    #>   variables: temperature
    #>   time temperature
    #> 1    0          10
    #> 2    5           4
    #> 3   10           2
    #> 4   15          15
    #> 5   20          15
    #> 6   25           2
    #> 7   30          10

    plot(profile$time, profile$temperature,
      type = "b",
      xlab = "Time",
      ylab = "Temperature"
    )

## Dynamic growth prediction

The Huang dynamic growth model can be combined with a secondary square-root model for the effect of temperature on the maximum specific growth rate.

    growth_pred <- predict_dynamic_growth(
      profile = profile,
      model = "huang",
      secondary = "huang_sqrt",
      start = list(
        logN0 = 2,
        logNmax = 8.8,
        a = 0.0886,
        Tmin = 8.91,
        lag = 2
      ),
      times = seq(0, 30, by = 1)
    )

    head(growth_pred)
    #> dynamic_growth prediction
    #>   model: huang
    #>   secondary: huang_sqrt
    #>   rows: 6
    #>   time response      lnN     logN temperature
    #> 1    0 2.000000 4.605170 2.000000        10.0
    #> 2    1 2.000002 4.605175 2.000002         8.8
    #> 3    2 2.000002 4.605175 2.000002         7.6
    #> 4    3 2.000002 4.605175 2.000002         6.4
    #> 5    4 2.000002 4.605175 2.000002         5.2
    #> 6    5 2.000002 4.605175 2.000002         4.0
    tail(growth_pred)
    #> dynamic_growth prediction
    #>   model: huang
    #>   secondary: huang_sqrt
    #>   rows: 6
    #>    time response      lnN     logN temperature
    #> 26   25 2.352192 5.416123 2.352192         2.0
    #> 27   26 2.352192 5.416123 2.352192         3.6
    #> 28   27 2.352192 5.416123 2.352192         5.2
    #> 29   28 2.352192 5.416123 2.352192         6.8
    #> 30   29 2.352192 5.416123 2.352192         8.4
    #> 31   30 2.353249 5.418557 2.353249        10.0

    plot(growth_pred$time, growth_pred$response,
      type = "l",
      lwd = 2,
      xlab = "Time",
      ylab = "Predicted log10 count"
    )

## Dynamic growth fitting

Dynamic fitting estimates selected parameters by repeatedly solving the dynamic model and minimizing residual error at the observation times. In practice, it is often wise to estimate only a small number of parameters and keep others fixed, especially when the data set is small.

Here we simulate observations from the dynamic growth prediction and estimate the secondary-model parameter `a`.

    obs_growth <- data.frame(
      time = growth_pred$time[seq(1, nrow(growth_pred), by = 3)],
      logN = growth_pred$response[seq(1, nrow(growth_pred), by = 3)]
    )

    growth_fit <- fit_dynamic_growth(
      data = obs_growth,
      profile = profile,
      time = "time",
      response = "logN",
      model = "huang",
      secondary = "huang_sqrt",
      start = list(
        logN0 = 2,
        logNmax = 8.8,
        a = 0.07,
        Tmin = 8.91,
        lag = 2
      ),
      estimate = "a",
      fixed = list(
        logN0 = 2,
        logNmax = 8.8,
        Tmin = 8.91,
        lag = 2
      )
    )

    growth_fit
    #> predmicror dynamic fit
    #>   type: dynamic_growth
    #>   model: huang
    #>   secondary: huang_sqrt
    #>   response: logN (log10)
    #>   SSE: 0.000000000012959
    #>   convergence: 0
    coef(growth_fit)
    #>      logN0    logNmax          a       Tmin        lag
    #> 2.00000000 8.80000000 0.08859978 8.91000000 2.00000000
    fit_metrics(growth_fit)
    #>   model           type response response_scale  n p          SSE         RMSE
    #> 1 huang dynamic_growth     logN          log10 11 1 1.295925e-11 1.085409e-06
    #>            MAE         bias          RSE R2 adj_R2   logLik       AIC       BIC
    #> 1 7.395177e-07 7.395177e-07 1.138387e-06  1      1 135.4608 -268.9215 -268.5236
    #>   converged
    #> 1      TRUE

    plot(obs_growth$time, obs_growth$logN,
      pch = 16,
      xlab = "Time",
      ylab = "log10 count"
    )
    lines(growth_fit$prediction$time, growth_fit$prediction$response, lwd = 2)

The fitted object supports familiar helper methods:

    head(fitted(growth_fit))
    #> [1] 2.000000 2.000002 2.000002 2.000002 2.000002 2.048006
    head(residuals(growth_fit))
    #> [1] 0.000000e+00 9.289902e-12 9.289902e-12 9.289902e-12 9.289902e-12
    #> [6] 2.370187e-07
    head(predmicror_augment(growth_fit))
    #>   time     logN  .fitted       .resid .model          .type
    #> 1    0 2.000000 2.000000 0.000000e+00  huang dynamic_growth
    #> 2    3 2.000002 2.000002 9.289902e-12  huang dynamic_growth
    #> 3    6 2.000002 2.000002 9.289902e-12  huang dynamic_growth
    #> 4    9 2.000002 2.000002 9.289902e-12  huang dynamic_growth
    #> 5   12 2.000002 2.000002 9.289902e-12  huang dynamic_growth
    #> 6   15 2.048006 2.048006 2.370187e-07  huang dynamic_growth

## Dynamic inactivation prediction

Dynamic inactivation can be simulated with the Weibull-Peleg formulation. In the simplest case, the rate is kept constant.

    inact_profile <- dynamic_profile(
      time = c(0, 10),
      temperature = c(60, 60)
    )

    inact_pred <- predict_dynamic_inactivation(
      profile = inact_profile,
      model = "weibull_peleg",
      secondary = "constant",
      start = list(
        logN0 = 7,
        b = 0.2,
        n = 1
      ),
      times = seq(0, 10, by = 1)
    )

    head(inact_pred)
    #> dynamic_inactivation prediction
    #>   model: weibull_peleg
    #>   secondary: constant
    #>   rows: 6
    #>   time response logN log_survival temperature
    #> 1    0      7.0  7.0          0.0          60
    #> 2    1      6.8  6.8         -0.2          60
    #> 3    2      6.6  6.6         -0.4          60
    #> 4    3      6.4  6.4         -0.6          60
    #> 5    4      6.2  6.2         -0.8          60
    #> 6    5      6.0  6.0         -1.0          60
    tail(inact_pred)
    #> dynamic_inactivation prediction
    #>   model: weibull_peleg
    #>   secondary: constant
    #>   rows: 6
    #>    time response logN log_survival temperature
    #> 6     5      6.0  6.0         -1.0          60
    #> 7     6      5.8  5.8         -1.2          60
    #> 8     7      5.6  5.6         -1.4          60
    #> 9     8      5.4  5.4         -1.6          60
    #> 10    9      5.2  5.2         -1.8          60
    #> 11   10      5.0  5.0         -2.0          60

    plot(inact_pred$time, inact_pred$response,
      type = "l",
      lwd = 2,
      xlab = "Time",
      ylab = "Predicted log10 count"
    )

## Dynamic inactivation fitting

The same framework can fit selected inactivation parameters. Here we estimate the Weibull-Peleg rate parameter `b`.

    obs_inact <- data.frame(
      time = c(0, 5, 10),
      logN = c(7, 6, 5)
    )

    inact_fit <- fit_dynamic_inactivation(
      data = obs_inact,
      profile = inact_profile,
      time = "time",
      response = "logN",
      model = "weibull_peleg",
      secondary = "constant",
      start = list(
        logN0 = 7,
        b = 0.1,
        n = 1
      ),
      estimate = "b",
      fixed = list(
        logN0 = 7,
        n = 1
      )
    )

    inact_fit
    #> predmicror dynamic fit
    #>   type: dynamic_inactivation
    #>   model: weibull_peleg
    #>   secondary: constant
    #>   response: logN (log10)
    #>   SSE: 0.000000000000000000000000051711
    #>   convergence: 0
    coef(inact_fit)
    #> logN0     b     n
    #>   7.0   0.2   1.0
    fit_metrics(inact_fit)
    #>           model                 type response response_scale n p          SSE
    #> 1 weibull_peleg dynamic_inactivation     logN          log10 3 1 5.171141e-26
    #>           RMSE          MAE          bias          RSE R2 adj_R2   logLik
    #> 1 1.312903e-13 1.030287e-13 -1.030287e-13 1.607971e-13  1      1 84.72728
    #>         AIC      BIC converged
    #> 1 -167.4546 -168.356      TRUE

    plot(obs_inact$time, obs_inact$logN,
      pch = 16,
      xlab = "Time",
      ylab = "log10 count"
    )
    lines(inact_fit$prediction$time, inact_fit$prediction$response, lwd = 2)

## Sensitivity analysis

Finite-difference sensitivity analysis helps identify which parameters most influence the model output. The scaled sensitivity coefficients are useful because they are expressed on the response scale.

    sens <- dynamic_sensitivity(
      type = "growth",
      profile = profile,
      model = "huang",
      secondary = "huang_sqrt",
      start = list(
        logN0 = 2,
        logNmax = 8.8,
        a = 0.0886,
        Tmin = 8.91,
        lag = 2
      ),
      parameters = c("a", "Tmin"),
      times = seq(0, 30, by = 2)
    )

    head(sens)
    #>   time parameter prediction  sensitivity scaled_sensitivity
    #> 1    0         a   2.000000 0.000000e+00       0.000000e+00
    #> 2    2         a   2.000002 4.249422e-05       3.764988e-06
    #> 3    4         a   2.000002 4.249422e-05       3.764988e-06
    #> 4    6         a   2.000002 4.249422e-05       3.764988e-06
    #> 5    8         a   2.000002 4.249422e-05       3.764988e-06
    #> 6   10         a   2.000002 4.249422e-05       3.764988e-06

    plot(sens$time, sens$scaled_sensitivity,
      type = "n",
      xlab = "Time",
      ylab = "Scaled sensitivity"
    )
    for (p in unique(sens$parameter)) {
      z <- sens[sens$parameter == p, ]
      lines(z$time, z$scaled_sensitivity, lwd = 2)
    }
    legend("topleft", legend = unique(sens$parameter), lty = 1, lwd = 2, bty = "n")

## Practical notes

Dynamic fitting can be computationally more demanding than fitting explicit isothermal models because each objective-function evaluation requires solving the dynamic trajectory. For small data sets, avoid estimating too many parameters at once. Use `fixed` to keep parameters at known or literature-supported values and estimate only the parameters that are informed by the available observations.