Thanks so much. I’m trying to sample some stochastic simulations of compartment models. My idea was simulating via an Euler-Maruyama or Euler-Binomial using PyMC to get MCMC samples or to sample the simulated output to test parameters recoverability. There are several models of increasing complexity, so I made an example with very simple simulations:
import pymc as pm
import numpy as np
import pytensor
import pytensor.tensor as pt
import matplotlib.pyplot as plt
from pymc.pytensorf import collect_default_updates
import arviz as az
# rng = np.random.default_rng()
# rvs = sp.stats.norm(loc=1000, scale=500).rvs
# M = sp.sparse.random(10, 10, density=0.75, random_state=rng, data_rvs=rvs).A
# M[M<0] = 0
N = pm.draw( pm.TruncatedNormal.dist(mu=20e6, sigma=10e6), 1).round(0)
#### Simulate SIR epidemic development
# initial parameters
initial_infected = 1
#epidemic length
n_days = 100 #days
times = np.arange(n_days) #days array index
#defined parameters
beta = 0.5 #infection rate
gamma = 0.1 #recovery rate
# Initialize arrays
susceptible = np.zeros(n_days)
infected = np.zeros(n_days)
recovered = np.zeros(n_days)
# Set initial conditions
susceptible[0] = N - initial_infected
infected[0] = initial_infected
I_begin = infected[0]
S_begin = N - infected[0]
R_begin = I_begin*0
dt = 1
not_take_off = True
while not_take_off:
for t in range(1, n_days):
# Compute new infections and recoveries
# new_infections = beta * susceptible[t - 1] * infected[t - 1] / N
# new_recoveries = gamma * infected[t - 1]
new_infections = np.random.binomial(susceptible[t - 1], 1 - np.exp(-beta*dt * infected[t - 1] / N))
new_recoveries = np.random.binomial(infected[t - 1], 1 - np.exp(-gamma*dt))
# Update compartments
susceptible[t] = susceptible[t - 1] - new_infections
infected[t] = infected[t - 1] + new_infections - new_recoveries
recovered[t] = recovered[t - 1] + new_recoveries
if np.array([infected, recovered]).sum() > 0:
not_take_off = False
S = susceptible
I = infected
R = recovered
plt.plot(range(n_days), S, linestyle=":", label='Susceptible')
plt.plot(range(n_days), I, linestyle="--", label='Infected')
plt.plot(range(n_days), R, label='Recovered')
plt.xlabel('Days')
plt.ylabel('Number of Individuals')
plt.title('SIR Model Simulation')
plt.rc('axes.spines',top=False, right=False)
plt.tight_layout()
plt.legend()
plt.grid(alpha=0.2)
plt.show()
plt.close()
rngI = pytensor.shared(np.random.default_rng())
rngR = pytensor.shared(np.random.default_rng())
def sir_func(S_prev, I_prev, R_prev, beta, gamma, N, dt):
new_I = pm.Binomial.dist(S_prev, 1 - pt.exp(-beta*dt * I_prev / N))
new_R = pm.Binomial.dist(I_prev, 1 - pt.exp(-gamma*dt))
# new_I = beta * S_prev * I_prev / N
# new_R = gamma * I_prev
S = S_prev - new_I
I = I_prev + new_I - new_R
R = R_prev + new_R
return (S, I, R), collect_default_updates([new_I, new_R])
with pm.Model() as mod:
beta = pm.HalfNormal("beta", 1)
gamma = pm.HalfNormal("gamma", 1)
dt = 0.1
outputs, _ = pytensor.scan(
fn=sir_func,
outputs_info=[S_begin, I_begin, R_begin],
non_sequences=[beta, gamma, N, dt],
n_steps=n_days
)
Send = pm.Deterministic("S", outputs[0])
Iend = pm.Deterministic("I", outputs[1])
Rend = pm.Deterministic("R", outputs[2])
y = pm.Poisson("y", mu=Iend, observed=infected)
with mod:
idata = pm.sample()
S = az.extract(idata)["S"].values.mean(axis=1)
I = az.extract(idata)["I"].values.mean(axis=1)
R = az.extract(idata)["R"].values.mean(axis=1)
plt.plot(range(n_days), S, linestyle=":", label='Susceptible')
plt.plot(range(n_days), I, linestyle="--", label='Infected')
plt.plot(range(n_days), R, label='Recovered')
plt.xlabel('Days')
plt.ylabel('Number of Individuals')
plt.title('SIR Model Simulation\n(origin country)')
plt.rc('axes.spines',top=False, right=False)
plt.tight_layout()
plt.legend()
plt.grid(alpha=0.2)
plt.show()
plt.close()
The notebook you linked was of great help to get a relatively functioning model. There’s one issue, however, that I cannot figure out from the notebook. If I try to sample without the “y = pm.Poisson(“y”, mu=Iend, observed=infected)” likelihood the model samples well, though with a bit of a clumsy output. But when I add the aforementioned likelihood, I get the following error:
ValueError: Random variables detected in the logp graph: {binomial_rv{0, (0, 0), int64, False}.out, binomial_rv{0, (0, 0), int64, False}.0, halfnormal_rv{0, (0, 0), floatX, False}.0, binomial_rv{0, (0, 0), int64, False}.out, binomial_rv{0, (0, 0), int64, False}.0, halfnormal_rv{0, (0, 0), floatX, False}.0}.
This can happen when DensityDist logp or Interval transform functions reference nonlocal variables,
or when not all rvs have a corresponding value variable.