import itertools
import arviz as az
import cmdstanpy
import pandas as pd
import numpy as np
from matplotlib import pyplot as plt
Nested numerical solving problems
Scientific knowledge often takes the form of specific relationships expressed by systems of equations. For example:
- A system of ordinary differential equations connects some state variables \(x\) with some other variables \(\theta\) with equations with the form \(\frac{dx}{dt}= f(x,\theta, t)\).
- An algebraic equation systems says that some variables \(v\) are related so that \(f(x, \theta) = 0\)
- A differential algebraic equation system says that some state variables’ rates of change have the form \(\frac{dx}{dt}= f(x, \theta, t)\) and that they satisfy some algebraic constraints \(f(x, \theta)=0\).
If there is an analytic solution to the equation system, we can just include the solution in our statistical model like any other form of structural knowledge: easy! However, often we want to solve equations that are hard or impossible to solve analytically, but can be solved approximately using numerical methods.
This is tricky in the context of Hamiltonian Monte Carlo for two reasons:
- Computation: HMC requires many evaluations of the log probability density function and its gradients.
At every evaluation, the sampler needs to solve the embedded equation system and find the gradients of the solution with respect to all model parameters.
- Extra source of error: how good of an approximation is good enough?
Reading:
- Timonen et al. (2022)
- Stan user guide sections: algebraic equation systems, ODE systems, DAE systems.
Background
Differential Equations
Are equations that relate functions to their derivatives. Some examples of these functions are quantities such as (1) The volume of the liquid in a bioreactor over time \[ \frac{dV}{dt} = F_{in} - F_{out}. \] (2) The temperature of a steel rod with heat source at one end \[ \frac{dT}{dt} = \alpha \frac{d^{2}T}{dx^{2}}. \] (3) The concentration of a substrate in a bioreactor over time (example below). As mentioned previously, the solution to many of these sorts of differential equations does not have an algebraic solution, such as \(T(t) = f(x, t)\).
Ordinary Differential Equations (ODEs)
Arguably, the most simple type of differential equation is what is known as an ordinary differential equation. This means that there is only one independent parameter, typically this will be either time or a dimension. We will only consider ODEs for the remainder of the course.
To gain some intuition about what is going on we will investigate the height of an initially reactor over time with a constant flow rate into the reactor.
\[ \frac{dV}{dt} = F_{in} - F_{out} \] \[ \frac{dh}{dt} = \frac{F_{in} - F_{out}}{Area} \] \[ = \frac{0.1 - 0.02 * h}{1} (\frac{m^3}{min}) \]
This ODE is an example which can be manually integrated and has an analytic solution. We shall investigate how the height changes over time and what the steady state height is.
The first question is an example of an initial-value problem
. Where we know the initial height (h=0), and we can solve the integral
\[ dh = \int_{t=0}^{t} 0.1 - 0.02*h dt. \] By using integrating factors (Don’t worry about this) we can solve for height \[ h = \frac{0.1}{0.02} + Ce^{-0.02t}. \] Finally, we can solve for the height by substituting what we know: at \(t = 0\), \(h = 0\). Therefore, we arrive at the final equation \[ h = \frac{0.1}{0.02}(1 - e^{-0.02t}). \]
We can answer the question about what its final height would be by solving for the steady-state
\[ \frac{dh}{dt} = 0. \]
Where after rearranging we find the final height equal to 5m, within the dimensions of the reactor
\[ h = \frac{0.1}{0.02} (m). \]
This is just a primer on differential equations. Solving differential equations using numerical solvers usually involves solving the initial-value problem
, but rather than solving it analytically, a (hopefully stable) solver will increment the time and state values dependent on the rate equations. This is also an example of a single differential equation, however, we will investigate systems of equations as well.
Example
We have some tubes containing a substrate \(S\) and some biomass \(C\) that we think approximately follow the Monod equation for microbial growth:
\[\begin{align*} \frac{dX}{dt} &= \frac{\mu_{max}\cdot S(t)}{K_{S} + S(t)}\cdot X(t) \\ \frac{dS}{dt} &= -\gamma \cdot \frac{\mu_{max}\cdot S(t)}{K_{s} + S(t)} \cdot X(t) \end{align*}\]
We measured \(X\) and \(S\) at different timepoints in some experiments and we want to try and find out \(\mu_{max}\), \(K_{S}\) and \(\gamma\) for the different strains in the tubes.
You can read more about the Monod equation in Allen and Waclaw (2019).
What we know
\(\mu_{max}, K_S, \gamma, S, X\) are non-negative.
\(S(0)\) and \(X(0)\) vary a little by tube.
\(\mu_{max}, K_S, \gamma\) vary by strain.
Measurement noise is roughly proportional to measured quantity.
Statistical model
We use two regression models to describe the measurements:
\[\begin{align*} y_X &\sim LN(\ln{\hat{X}}, \sigma_{X}) \\ y_S &\sim LN(\ln{\hat{S}}, \sigma_{S}) \end{align*}\]
To capture the variation in parameters by tube and strain we add a hierarchical regression model:
\[\begin{align*} \ln{\mu_{max}} &\sim N(a_{\mu_{max}}, \tau_{\mu_max}) \\ \ln{\gamma} &\sim N(a_{gamma}, \tau_{\gamma}) \\ \ln{\mu_{K_S}} &\sim N(a_{K_S}, \tau_{K_S}) \end{align*}\]
To get a true abundance given some parameters we put an ode in the model:
\[ \hat{X}(t), \hat{S}(t) = \text{solve-monod-equation}(t, X_0, S_0, \mu_max, \gamma, K_S) \]
imports
Specify true parameters
In order to avoid doing too much annoying handling of strings we assume that all the parts of the problem have meaningful 1-indexed integer labels: for example, species 1 is biomass.
This code specifies the dimensions of our problem.
= 4
N_strain = 16
N_tube = 20
N_timepoint = 15
duration = [i+1 for i in range(N_strain)]
strains = [i+1 for i in range(N_tube)]
tubes = [1, 2]
species = [4, 7, 12, 15, 17]
measurement_timepoint_ixs = pd.Series(
timepoints 0.01, duration, N_timepoint),
np.linspace(="time",
name=range(1, N_timepoint+1)
index
)= 12345
SEED = np.random.default_rng(seed=SEED) rng
This code defines some true values for the parameters - we will use these to generate fake data.
= {
true_param_values "a_mu_max": -1.7,
"a_ks": -1.3,
"a_gamma": -0.6,
"t_mu_max": 0.2,
"t_ks": 0.3,
"t_gamma": 0.13,
"species_zero": [
[-2.1, 0.05)),
np.exp(np.random.normal(0.2, 0.05))
np.exp(np.random.normal(for _ in range(N_tube)
]
],"sigma_y": [0.08, 0.1],
"ln_mu_max_z": np.random.normal(0, 1, size=N_strain).tolist(),
"ln_ks_z": np.random.normal(0, 1, size=N_strain).tolist(),
"ln_gamma_z": np.random.normal(0, 1, size=N_strain).tolist(),
}for var in ["mu_max", "ks", "gamma"]:
= np.exp(
true_param_values[var] f"a_{var}"]
true_param_values[+ true_param_values[f"t_{var}"] * np.array(true_param_values[f"ln_{var}_z"])
).tolist()
A bit of data transformation
This code does some handy transformations on the data using pandas, giving us a table of information about the measurements.
= pd.Series(
tube_to_strain
[% N_strain) + 1 for i in range(N_tube) # % operator finds remainder
(i =tubes, name="strain"
], index
)= (
measurements
pd.DataFrame(
itertools.product(tubes, measurement_timepoint_ixs, species),=["tube", "timepoint", "species"],
columns=range(1, len(tubes) * len(measurement_timepoint_ixs) * len(species) + 1)
index
)="tube")
.join(tube_to_strain, on="timepoint")
.join(timepoints, on )
Generating a Stan input dictionary
This code puts the data in the correct format for cmdstanpy.
= {
stan_input_structure "N_measurement": len(measurements),
"N_timepoint": N_timepoint,
"N_tube": N_tube,
"N_strain": N_strain,
"tube": measurements["tube"].values.tolist(),
"measurement_timepoint": measurements["timepoint"].values.tolist(),
"measured_species": measurements["species"].values.tolist(),
"strain": tube_to_strain.values.tolist(),
"timepoint_time": timepoints.values.tolist(),
}
This code defines some prior distributions for the model’s parameters
= {
priors # parameters that can be negative:
"prior_a_mu_max": [-1.8, 0.2],
"prior_a_ks": [-1.3, 0.1],
"prior_a_gamma": [-0.5, 0.1],
# parameters that are non-negative:
"prior_t_mu_max": [-1.4, 0.1],
"prior_t_ks": [-1.2, 0.1],
"prior_t_gamma": [-2, 0.1],
"prior_species_zero": [[[-2.1, 0.1], [0.2, 0.1]]] * N_tube,
"prior_sigma_y": [[-2.3, 0.15], [-2.3, 0.15]],
}
The next bit of code lets us configure Stan’s interface to the Sundials ODE solver.
= {
ode_solver_configuration "abs_tol": 1e-7,
"rel_tol": 1e-7,
"max_num_steps": int(1e7)
}
Now we can put all the inputs together
= stan_input_structure | priors | ode_solver_configuration stan_input_common
Load the model
This code loads the Stan program at monod.stan
as a CmdStanModel
object and compiles it using cmdstan’s compiler.
= cmdstanpy.CmdStanModel(stan_file="../src/stan/monod.stan")
model print(model.code())
functions {
real get_mu_at_t(real mu_max, real ks, real S_at_t) {
return (mu_max * S_at_t) / (ks + S_at_t);
}
vector ddt(real t, vector species, real mu_max, real ks, real gamma) {
real mu_at_t = get_mu_at_t(mu_max, ks, species[2]);
vector[2] out;
out[1] = mu_at_t * species[1];
out[2] = -gamma * mu_at_t * species[1];
return out;
}
}
data {
int<lower=1> N_measurement;
int<lower=1> N_timepoint;
int<lower=1> N_tube;
int<lower=1> N_strain;
array[N_measurement] int<lower=1, upper=N_tube> tube;
array[N_measurement] int<lower=1, upper=N_timepoint> measurement_timepoint;
array[N_measurement] int<lower=1, upper=2> measured_species;
vector<lower=0>[N_measurement] y;
array[N_tube] int<lower=1, upper=N_strain> strain;
array[N_timepoint] real<lower=0> timepoint_time;
array[N_tube, 2] vector[2] prior_species_zero;
array[2] vector[2] prior_sigma_y;
vector[2] prior_a_mu_max;
vector[2] prior_a_ks;
vector[2] prior_a_gamma;
vector[2] prior_t_mu_max;
vector[2] prior_t_gamma;
vector[2] prior_t_ks;
real<lower=0> abs_tol;
real<lower=0> rel_tol;
int<lower=1> max_num_steps;
int<lower=0, upper=1> likelihood;
}
parameters {
vector[N_strain] ln_mu_max_z;
vector[N_strain] ln_ks_z;
vector[N_strain] ln_gamma_z;
real a_mu_max;
real a_ks;
real a_gamma;
real<lower=0> t_mu_max;
real<lower=0> t_ks;
real<lower=0> t_gamma;
array[N_tube] vector<lower=0>[2] species_zero;
vector<lower=0>[2] sigma_y;
}
transformed parameters {
vector[N_strain] mu_max = exp(a_mu_max + ln_mu_max_z * t_mu_max);
vector[N_strain] ks = exp(a_ks + ln_ks_z * t_ks);
vector[N_strain] gamma = exp(a_gamma + ln_gamma_z * t_gamma);
array[N_tube, N_timepoint] vector[2] abundance;
for (tube_t in 1 : N_tube) {
abundance[tube_t] = ode_bdf_tol(ddt, species_zero[tube_t], 0,
timepoint_time,
abs_tol, rel_tol, max_num_steps,
mu_max[strain[tube_t]],
ks[strain[tube_t]], gamma[strain[tube_t]]);
}
}
model {
// priors
ln_mu_max_z ~ std_normal();
ln_ks_z ~ std_normal();
ln_gamma_z ~ std_normal();
a_mu_max ~ normal(prior_a_mu_max[1], prior_a_mu_max[2]);
a_ks ~ normal(prior_a_ks[1], prior_a_ks[2]);
a_gamma ~ normal(prior_a_gamma[1], prior_a_gamma[2]);
t_mu_max ~ lognormal(prior_t_mu_max[1], prior_t_mu_max[2]);
t_ks ~ lognormal(prior_t_ks[1], prior_t_ks[2]);
t_gamma ~ lognormal(prior_t_gamma[1], prior_t_gamma[2]);
for (s in 1 : 2) {
sigma_y[s] ~ lognormal(prior_sigma_y[s, 1], prior_sigma_y[s, 2]);
for (t in 1 : N_tube){
species_zero[t, s] ~ lognormal(prior_species_zero[t, s, 1],
prior_species_zero[t, s, 2]);
}
}
// likelihood
if (likelihood) {
for (m in 1 : N_measurement) {
real yhat = abundance[tube[m], measurement_timepoint[m], measured_species[m]];
y[m] ~ lognormal(log(yhat), sigma_y[measured_species[m]]);
}
}
}
generated quantities {
vector[N_measurement] yrep;
vector[N_measurement] llik;
for (m in 1 : N_measurement){
real yhat = abundance[tube[m], measurement_timepoint[m], measured_species[m]];
yrep[m] = lognormal_rng(log(yhat), sigma_y[measured_species[m]]);
llik[m] = lognormal_lpdf(y[m] | log(yhat), sigma_y[measured_species[m]]);
}
}
Sample in fixed param mode to generate fake data
= stan_input_common | {
stan_input_true "y": np.ones(len(measurements)).tolist(), # dummy values as we don't need measurements yet
"likelihood": 0 # we don't need to evaluate the likelihood
}= {
coords "strain": strains,
"tube": tubes,
"species": species,
"timepoint": timepoints.index.values,
"measurement": measurements.index.values
}= {
dims "abundance": ["tube", "timepoint", "species"],
"mu_max": ["strain"],
"ks": ["strain"],
"gamma": ["strain"],
"species_zero": ["tube", "species"],
"y": ["measurement"],
"yrep": ["measurement"],
"llik": ["measurement"]
}
= model.sample(
mcmc_true =stan_input_true,
data=1,
iter_sampling=True,
fixed_param=1,
chains=1,
refresh=true_param_values,
inits=SEED,
seed
)= az.from_cmdstanpy(
idata_true
mcmc_true,=dims,
dims=coords,
coords={"y": "yrep"},
posterior_predictive="llik"
log_likelihood )
17:36:12 - cmdstanpy - INFO - CmdStan start processing
17:36:12 - cmdstanpy - INFO - CmdStan done processing.
Look at results
def plot_sim(true_abundance, fake_measurements, species_to_ax):
= plt.subplots(1, 2, figsize=[9, 3])
f, axes
1]].set_title("Species 1")
axes[species_to_ax[2]].set_title("Species 2")
axes[species_to_ax[for ax in axes:
"Time")
ax.set_xlabel("Abundance")
ax.set_ylabel(for (tube_i, species_i), df_i in true_abundance.groupby(["tube", "species"]):
= axes[species_to_ax[species_i]]
ax = df_i.merge(
fm "time", axis=1),
fake_measurements.drop(=["tube", "species", "timepoint"]
on
)
ax.plot("time")["abundance"], color="black", linewidth=0.5
df_i.set_index(
)
ax.scatter("time"],
fm["simulated_measurement"],
fm[="r",
color="x",
marker="simulated measurement"
label
)return f, axes
= {1: 0, 2: 1}
species_to_ax = (
true_abundance "abundance"]
idata_true.posterior[
.to_dataframe()"chain", "draw"])
.droplevel([="timepoint")
.join(timepoints, on
.reset_index()
)= measurements.join(
fake_measurements "yrep"]
idata_true.posterior_predictive[
.to_series()"chain", "draw"])
.droplevel(["simulated_measurement")
.rename(
).copy()= plot_sim(true_abundance, fake_measurements, species_to_ax)
f, axes
"img/monod_simulated_data.png") f.savefig(
Sample in prior mode
= stan_input_common | {
stan_input_prior "y": fake_measurements["simulated_measurement"],
"likelihood": 0
}= model.sample(
mcmc_prior =stan_input_prior,
data=100,
iter_warmup=100,
iter_sampling=1,
chains=1,
refresh=True,
save_warmup=true_param_values,
inits=SEED,
seed
)= az.from_cmdstanpy(
idata_prior
mcmc_prior,=dims,
dims=coords,
coords={"y": "yrep"},
posterior_predictive="llik"
log_likelihood
) idata_prior
17:36:12 - cmdstanpy - INFO - CmdStan start processing
17:36:43 - cmdstanpy - INFO - CmdStan done processing.
17:36:43 - cmdstanpy - WARNING - Non-fatal error during sampling:
Exception: ode_bdf_tol: ode parameters and data is inf, but must be finite! (in 'monod.stan', line 56, column 4 to line 60, column 79)
Exception: ode_bdf_tol: ode parameters and data is inf, but must be finite! (in 'monod.stan', line 56, column 4 to line 60, column 79)
Exception: CVode(cvodes_mem, t_final, nv_state_, &t_init, CV_NORMAL) failed with error flag -4:
Exception: lognormal_rng: Location parameter is nan, but must be finite! (in 'monod.stan', line 94, column 4 to column 69)
Exception: lognormal_rng: Location parameter is nan, but must be finite! (in 'monod.stan', line 94, column 4 to column 69)
Exception: lognormal_rng: Location parameter is nan, but must be finite! (in 'monod.stan', line 94, column 4 to column 69)
Exception: lognormal_rng: Location parameter is nan, but must be finite! (in 'monod.stan', line 94, column 4 to column 69)
Exception: lognormal_rng: Location parameter is nan, but must be finite! (in 'monod.stan', line 94, column 4 to column 69)
Consider re-running with show_console=True if the above output is unclear!
-
<xarray.Dataset> Size: 564kB Dimensions: (chain: 1, draw: 100, ln_mu_max_z_dim_0: 4, ln_ks_z_dim_0: 4, ln_gamma_z_dim_0: 4, tube: 16, species: 2, sigma_y_dim_0: 2, strain: 4, timepoint: 20) Coordinates: * chain (chain) int64 8B 0 * draw (draw) int64 800B 0 1 2 3 4 5 6 ... 93 94 95 96 97 98 99 * ln_mu_max_z_dim_0 (ln_mu_max_z_dim_0) int64 32B 0 1 2 3 * ln_ks_z_dim_0 (ln_ks_z_dim_0) int64 32B 0 1 2 3 * ln_gamma_z_dim_0 (ln_gamma_z_dim_0) int64 32B 0 1 2 3 * tube (tube) int64 128B 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 * species (species) int64 16B 1 2 * sigma_y_dim_0 (sigma_y_dim_0) int64 16B 0 1 * strain (strain) int64 32B 1 2 3 4 * timepoint (timepoint) int64 160B 1 2 3 4 5 6 ... 15 16 17 18 19 20 Data variables: (12/15) ln_mu_max_z (chain, draw, ln_mu_max_z_dim_0) float64 3kB 0.5658 ..... ln_ks_z (chain, draw, ln_ks_z_dim_0) float64 3kB -0.5826 ... 1... ln_gamma_z (chain, draw, ln_gamma_z_dim_0) float64 3kB 0.9569 ...... a_mu_max (chain, draw) float64 800B -1.905 -2.169 ... -2.02 -1.509 a_ks (chain, draw) float64 800B -1.299 -1.285 ... -1.214 -1.48 a_gamma (chain, draw) float64 800B -0.5303 -0.4837 ... -0.5252 ... ... species_zero (chain, draw, tube, species) float64 26kB 0.1091 ... 1.43 sigma_y (chain, draw, sigma_y_dim_0) float64 2kB 0.1355 ... 0.... mu_max (chain, draw, strain) float64 3kB 0.1683 0.2053 ... 0.124 ks (chain, draw, strain) float64 3kB 0.2306 ... 0.3453 gamma (chain, draw, strain) float64 3kB 0.6622 ... 0.5915 abundance (chain, draw, tube, timepoint, species) float64 512kB ... Attributes: created_at: 2024-05-03T15:36:43.588429 arviz_version: 0.17.1 inference_library: cmdstanpy inference_library_version: 1.2.1
-
<xarray.Dataset> Size: 130kB Dimensions: (chain: 1, draw: 100, measurement: 160) Coordinates: * chain (chain) int64 8B 0 * draw (draw) int64 800B 0 1 2 3 4 5 6 7 8 ... 92 93 94 95 96 97 98 99 * measurement (measurement) int64 1kB 1 2 3 4 5 6 ... 155 156 157 158 159 160 Data variables: yrep (chain, draw, measurement) float64 128kB 0.137 1.119 ... 1.276 Attributes: created_at: 2024-05-03T15:36:43.594040 arviz_version: 0.17.1 inference_library: cmdstanpy inference_library_version: 1.2.1
-
<xarray.Dataset> Size: 130kB Dimensions: (chain: 1, draw: 100, measurement: 160) Coordinates: * chain (chain) int64 8B 0 * draw (draw) int64 800B 0 1 2 3 4 5 6 7 8 ... 92 93 94 95 96 97 98 99 * measurement (measurement) int64 1kB 1 2 3 4 5 6 ... 155 156 157 158 159 160 Data variables: llik (chain, draw, measurement) float64 128kB -0.06903 ... -66.62 Attributes: created_at: 2024-05-03T15:36:43.594943 arviz_version: 0.17.1 inference_library: cmdstanpy inference_library_version: 1.2.1
-
<xarray.Dataset> Size: 11kB Dimensions: (chain: 1, draw: 200) Coordinates: * chain (chain) int64 8B 0 * draw (draw) int64 2kB 0 1 2 3 4 5 6 ... 194 195 196 197 198 199 Data variables: lp (chain, draw) float64 2kB -52.14 -52.14 ... -56.19 -55.06 acceptance_rate (chain, draw) float64 2kB 0.9173 0.0 0.0 ... 0.9884 0.7772 step_size (chain, draw) float64 2kB 0.03125 12.38 ... 0.06378 0.06378 tree_depth (chain, draw) int64 2kB 7 0 0 5 8 8 7 6 ... 6 6 6 4 6 6 6 6 n_steps (chain, draw) int64 2kB 127 1 1 31 255 ... 23 63 63 63 63 diverging (chain, draw) bool 200B False True True ... False False energy (chain, draw) float64 2kB 72.65 76.3 74.35 ... 76.18 79.57 Attributes: created_at: 2024-05-03T15:36:43.592490 arviz_version: 0.17.1 inference_library: cmdstanpy inference_library_version: 1.2.1
We can find the prior intervals for the true abundance and plot them in the graph.
= idata_prior.posterior["abundance"]
prior_abundances
= 20
n_sample = rng.choice(prior_abundances.coords["chain"].values, n_sample)
chains = rng.choice(prior_abundances.coords["draw"].values, n_sample)
draws = plot_sim(true_abundance, fake_measurements, species_to_ax)
f, axes
for ax, species_i in zip(axes, species):
for tube_j in tubes:
for chain, draw in zip(chains, draws):
= prior_abundances.sel(chain=chain, draw=draw, tube=tube_j, species=species_i)
timeseries
ax.plot(
timepoints.values,
timeseries.values,=0.5, color="skyblue", zorder=-1
alpha
)"img/monod_priors.png") f.savefig(
Sample in posterior mode
= stan_input_common | {
stan_input_posterior "y": fake_measurements["simulated_measurement"],
"likelihood": 1
}= model.sample(
mcmc_posterior =stan_input_posterior,
data=300,
iter_warmup=300,
iter_sampling=4,
chains=1,
refresh=true_param_values,
inits=SEED,
seed
)= az.from_cmdstanpy(
idata_posterior
mcmc_posterior,=dims,
dims=coords,
coords={"y": "yrep"},
posterior_predictive="llik"
log_likelihood
) idata_posterior
17:36:44 - cmdstanpy - INFO - CmdStan start processing
17:42:33 - cmdstanpy - INFO - CmdStan done processing.
17:42:33 - cmdstanpy - WARNING - Non-fatal error during sampling:
Exception: ode_bdf_tol: ode parameters and data is inf, but must be finite! (in 'monod.stan', line 56, column 4 to line 60, column 79)
Exception: ode_bdf_tol: ode parameters and data is inf, but must be finite! (in 'monod.stan', line 56, column 4 to line 60, column 79)
Exception: ode_bdf_tol: initial state[2] is inf, but must be finite! (in 'monod.stan', line 56, column 4 to line 60, column 79)
Exception: ode_bdf_tol: ode parameters and data is inf, but must be finite! (in 'monod.stan', line 56, column 4 to line 60, column 79)
Exception: ode_bdf_tol: ode parameters and data is inf, but must be finite! (in 'monod.stan', line 56, column 4 to line 60, column 79)
Exception: lognormal_lpdf: Location parameter is nan, but must be finite! (in 'monod.stan', line 85, column 6 to column 64)
Consider re-running with show_console=True if the above output is unclear!
-
<xarray.Dataset> Size: 7MB Dimensions: (chain: 4, draw: 300, ln_mu_max_z_dim_0: 4, ln_ks_z_dim_0: 4, ln_gamma_z_dim_0: 4, tube: 16, species: 2, sigma_y_dim_0: 2, strain: 4, timepoint: 20) Coordinates: * chain (chain) int64 32B 0 1 2 3 * draw (draw) int64 2kB 0 1 2 3 4 5 ... 294 295 296 297 298 299 * ln_mu_max_z_dim_0 (ln_mu_max_z_dim_0) int64 32B 0 1 2 3 * ln_ks_z_dim_0 (ln_ks_z_dim_0) int64 32B 0 1 2 3 * ln_gamma_z_dim_0 (ln_gamma_z_dim_0) int64 32B 0 1 2 3 * tube (tube) int64 128B 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 * species (species) int64 16B 1 2 * sigma_y_dim_0 (sigma_y_dim_0) int64 16B 0 1 * strain (strain) int64 32B 1 2 3 4 * timepoint (timepoint) int64 160B 1 2 3 4 5 6 ... 15 16 17 18 19 20 Data variables: (12/15) ln_mu_max_z (chain, draw, ln_mu_max_z_dim_0) float64 38kB 1.314 ..... ln_ks_z (chain, draw, ln_ks_z_dim_0) float64 38kB 0.351 ... 1.379 ln_gamma_z (chain, draw, ln_gamma_z_dim_0) float64 38kB -1.141 ..... a_mu_max (chain, draw) float64 10kB -1.69 -1.732 ... -1.587 -1.892 a_ks (chain, draw) float64 10kB -1.208 -1.304 ... -1.175 -1.36 a_gamma (chain, draw) float64 10kB -0.4732 -0.5094 ... -0.6334 ... ... species_zero (chain, draw, tube, species) float64 307kB 0.1244 ... ... sigma_y (chain, draw, sigma_y_dim_0) float64 19kB 0.08281 ... ... mu_max (chain, draw, strain) float64 38kB 0.2543 ... 0.2909 ks (chain, draw, strain) float64 38kB 0.3284 ... 0.3793 gamma (chain, draw, strain) float64 38kB 0.5383 ... 0.5225 abundance (chain, draw, tube, timepoint, species) float64 6MB 0.... Attributes: created_at: 2024-05-03T15:42:34.027913 arviz_version: 0.17.1 inference_library: cmdstanpy inference_library_version: 1.2.1
-
<xarray.Dataset> Size: 2MB Dimensions: (chain: 4, draw: 300, measurement: 160) Coordinates: * chain (chain) int64 32B 0 1 2 3 * draw (draw) int64 2kB 0 1 2 3 4 5 6 ... 293 294 295 296 297 298 299 * measurement (measurement) int64 1kB 1 2 3 4 5 6 ... 155 156 157 158 159 160 Data variables: yrep (chain, draw, measurement) float64 2MB 0.1999 1.06 ... 0.3905 Attributes: created_at: 2024-05-03T15:42:34.033631 arviz_version: 0.17.1 inference_library: cmdstanpy inference_library_version: 1.2.1
-
<xarray.Dataset> Size: 2MB Dimensions: (chain: 4, draw: 300, measurement: 160) Coordinates: * chain (chain) int64 32B 0 1 2 3 * draw (draw) int64 2kB 0 1 2 3 4 5 6 ... 293 294 295 296 297 298 299 * measurement (measurement) int64 1kB 1 2 3 4 5 6 ... 155 156 157 158 159 160 Data variables: llik (chain, draw, measurement) float64 2MB 3.014 0.3467 ... 2.117 Attributes: created_at: 2024-05-03T15:42:34.034683 arviz_version: 0.17.1 inference_library: cmdstanpy inference_library_version: 1.2.1
-
<xarray.Dataset> Size: 61kB Dimensions: (chain: 4, draw: 300) Coordinates: * chain (chain) int64 32B 0 1 2 3 * draw (draw) int64 2kB 0 1 2 3 4 5 6 ... 294 295 296 297 298 299 Data variables: lp (chain, draw) float64 10kB 106.6 92.14 90.0 ... 104.4 101.2 acceptance_rate (chain, draw) float64 10kB 0.9698 0.878 ... 0.9114 0.715 step_size (chain, draw) float64 10kB 0.03762 0.03762 ... 0.03294 tree_depth (chain, draw) int64 10kB 7 7 7 7 7 7 7 7 ... 7 7 7 7 7 7 7 n_steps (chain, draw) int64 10kB 127 127 127 127 ... 127 127 127 diverging (chain, draw) bool 1kB False False False ... False False energy (chain, draw) float64 10kB -80.62 -57.62 ... -72.64 -71.56 Attributes: created_at: 2024-05-03T15:42:34.032008 arviz_version: 0.17.1 inference_library: cmdstanpy inference_library_version: 1.2.1
Diagnostics: is the posterior ok?
First check the sample_stats
group to see if there were any divergent transitions and if the lp
parameter converged.
az.summary(idata_posterior.sample_stats)
/Users/tedgro/repos/biosustain/cmfa/.venv/lib/python3.12/site-packages/arviz/stats/diagnostics.py:592: RuntimeWarning: invalid value encountered in scalar divide
(between_chain_variance / within_chain_variance + num_samples - 1) / (num_samples)
mean | sd | hdi_3% | hdi_97% | mcse_mean | mcse_sd | ess_bulk | ess_tail | r_hat | |
---|---|---|---|---|---|---|---|---|---|
lp | 105.403 | 5.301 | 94.828 | 114.466 | 0.246 | 0.175 | 462.0 | 792.0 | 1.010000e+00 |
acceptance_rate | 0.935 | 0.082 | 0.776 | 1.000 | 0.002 | 0.002 | 1344.0 | 1181.0 | 1.010000e+00 |
step_size | 0.036 | 0.004 | 0.033 | 0.042 | 0.002 | 0.001 | 4.0 | 4.0 | 5.859337e+15 |
tree_depth | 6.920 | 0.271 | 6.000 | 7.000 | 0.058 | 0.041 | 22.0 | 22.0 | 1.140000e+00 |
n_steps | 125.027 | 17.521 | 127.000 | 127.000 | 0.606 | 0.429 | 657.0 | 1097.0 | 1.070000e+00 |
diverging | 0.000 | 0.000 | 0.000 | 0.000 | 0.000 | 0.000 | 1200.0 | 1200.0 | NaN |
energy | -79.386 | 7.178 | -92.356 | -66.194 | 0.332 | 0.236 | 463.0 | 814.0 | 1.010000e+00 |
Next check the parameter-by-parameter summary
az.summary(idata_posterior)
mean | sd | hdi_3% | hdi_97% | mcse_mean | mcse_sd | ess_bulk | ess_tail | r_hat | |
---|---|---|---|---|---|---|---|---|---|
ln_mu_max_z[0] | 0.600 | 0.502 | -0.350 | 1.500 | 0.023 | 0.016 | 487.0 | 839.0 | 1.0 |
ln_mu_max_z[1] | -0.429 | 0.510 | -1.376 | 0.470 | 0.023 | 0.016 | 495.0 | 701.0 | 1.0 |
ln_mu_max_z[2] | 0.027 | 0.491 | -0.833 | 1.006 | 0.021 | 0.015 | 530.0 | 666.0 | 1.0 |
ln_mu_max_z[3] | 1.175 | 0.518 | 0.208 | 2.108 | 0.024 | 0.017 | 483.0 | 506.0 | 1.0 |
ln_ks_z[0] | -0.106 | 0.968 | -2.030 | 1.577 | 0.022 | 0.030 | 1910.0 | 778.0 | 1.0 |
... | ... | ... | ... | ... | ... | ... | ... | ... | ... |
abundance[16, 18, 2] | 0.244 | 0.026 | 0.193 | 0.286 | 0.001 | 0.000 | 1713.0 | 993.0 | 1.0 |
abundance[16, 19, 1] | 2.013 | 0.100 | 1.817 | 2.194 | 0.002 | 0.002 | 1882.0 | 882.0 | 1.0 |
abundance[16, 19, 2] | 0.153 | 0.026 | 0.108 | 0.199 | 0.001 | 0.000 | 1700.0 | 1074.0 | 1.0 |
abundance[16, 20, 1] | 2.135 | 0.110 | 1.925 | 2.336 | 0.003 | 0.002 | 1697.0 | 1010.0 | 1.0 |
abundance[16, 20, 2] | 0.085 | 0.023 | 0.042 | 0.124 | 0.001 | 0.000 | 1778.0 | 1062.0 | 1.0 |
704 rows × 9 columns
Show posterior intervals
= idata_posterior.posterior["abundance"]
prior_abundances
= 20
n_sample = rng.choice(prior_abundances.coords["chain"].values, n_sample)
chains = rng.choice(prior_abundances.coords["draw"].values, n_sample)
draws = plot_sim(true_abundance, fake_measurements, species_to_ax)
f, axes
for ax, species_i in zip(axes, species):
for tube_j in tubes:
for chain, draw in zip(chains, draws):
= prior_abundances.sel(chain=chain, draw=draw, tube=tube_j, species=species_i)
timeseries
ax.plot(
timepoints.values,
timeseries.values,=0.5, color="skyblue", zorder=-1
alpha
)"img/monod_posteriors.png") f.savefig(
look at the posterior
The next few cells use arviz’s plot_posterior
function to plot the marginal posterior distributions for some of the model’s parameters:
= plt.subplots(1, 4, figsize=[10, 4])
f, axes = az.plot_posterior(
axes
idata_posterior,="hist",
kind=20,
bins=["gamma"],
var_names=axes,
ax=None,
point_estimate="hide"
hdi_prob
)for ax, true_value in zip(axes, true_param_values["gamma"]):
="red") ax.axvline(true_value, color
= plt.subplots(1, 4, figsize=[10, 4])
f, axes = az.plot_posterior(
axes
idata_posterior,="hist",
kind=20,
bins=["mu_max"],
var_names=axes,
ax=None,
point_estimate="hide"
hdi_prob
)for ax, true_value in zip(axes, true_param_values["mu_max"]):
="red") ax.axvline(true_value, color
= plt.subplots(1, 4, figsize=[10, 4])
f, axes = az.plot_posterior(
axes
idata_posterior,="hist",
kind=20,
bins=["ks"],
var_names=axes,
ax=None,
point_estimate="hide"
hdi_prob
)for ax, true_value in zip(axes, true_param_values["ks"]):
="red") ax.axvline(true_value, color