EpiSewer — a composable renewal library
R EpiSewer packs a large model space into one config-driven Stan program: several reproduction-number processes, several observation families, and seeding / shedding / limit-of-detection / digital-PCR / outlier options, each selected at runtime by an integer flag. EpiSewer.jl describes the same model space the opposite way — as composable Turing components, with no monolith.
StanBlocks realizes both the same way, and it is the way the two designs already point at: a small library of pieces assembled at construction time. The runtime flags of the R monolith become plain Julia composition, and the pieces line up one-to-one with EpiSewer.jl's components. This page shows a slice of that library — one renewal core, a swappable Rt process, and a swappable observation family — and transpiles three assembled configurations to three focused Stan programs. The arrays are build fixtures, not real data.
Companion port in BayesianRegressionModels.jl
BRM ports the same EpiSewer renewal core through its formula interface: Wastewater-based Rt inference (EpiSewer / ww-inference-model). That page frames the renewal model as a @brm kernel(...) term; this page shows the pure-StanBlocks @slic / @deffun library the same backend emits.
One renewal core, swappable Rt process and observation family
Two axes vary here, both by composition:
Rt process — a weekly random walk, or a sparse spline basis evaluated with the
csr_matrix_times_vectorprimitive (the R model'sR_model0/1/3 spline processes, in compressed-row form).Observation family — an observation submodel per family:
obs_lodmeasures log-concentration with an LOD-censored normal, andobs_dpcrmeasures digital-PCR positive-partition counts, usingto_intfor the real→integer partition total. Each family pairs the same renewal core with a different measurement node.
Each model is one Base.merge of the core with an Rt fragment; the monolith's if-on-a-flag is gone.
using StanBlocks
@deffun begin
es_conv_at(x::vector[nx], pmf::vector[np], t::int)::real = begin
acc = 0.0
kmax = min(np, t - 1)
for k in 1:kmax
acc = acc + x[t - k] * pmf[k]
end
acc
end
es_conv(x::vector[nx], pmf::vector[np], out_n::int, off::int)::vector[out_n] = begin
out::vector[out_n]
for i in 1:out_n
out[i] = es_conv_at(x, pmf, i + off)
end
out
end
es_rw(x0::real, sd::real, z::vector[m])::vector[m+1] = begin
out::vector[m+1]
out[1] = x0
for i in 1:m
out[i+1] = out[i] + sd * z[i]
end
out
end
es_renewal(log_i0::real, growth::real, log_rt::vector[nt],
gen_int::vector[gmax], uot::int)::vector[nt] = begin
infections::vector[nt]
for t in 1:uot
infections[t] = exp(log_i0 + growth * (t - 1))
end
for t in (uot + 1):nt
infections[t] = exp(log_rt[t]) * es_conv_at(infections, gen_int, t)
end
infections
end
end
# ── Swappable observation-family submodels (§3 Form A) ──
# Concentration measured with LOD-censored normal.
obs_lod = @slic begin
log10_g ~ normal(9.0, 1.0)
sigma ~ normal(0.0, 1.0; lower = 0.0)
model_conc = log(10.0) * log10_g + log(signal + 1.0e-8) - log(mwpd)
obs ~ censored(normal, model_conc, sigma; lower = lod)
return obs
end
# Digital-PCR positive-partition counts: real->int partition total via `to_int`.
obs_dpcr = @slic begin
phi ~ gamma(2.0, 0.1)
n_part = to_int(round(n_partitions)) # real -> int (data-side)
lambda = (1.0e5 * signal) * n_part / mwpd
obs ~ neg_binomial_2(lambda, phi)
return obs
end
es_data = (;
gen_int = [0.15, 0.30, 0.30, 0.15, 0.10],
shed = [0.05, 0.15, 0.25, 0.2, 0.15, 0.1, 0.07, 0.03],
week_of_day = repeat(1:5, inner = 7),
conc = [3.4, 2.0, 4.1, 3.8, 2.0, 3.2, 4.4, 3.9, 2.0, 3.1, 4.0, 3.7,
3.6, 2.0, 4.2, 3.5, 3.9, 2.0, 4.0, 3.7, 3.3, 2.0, 4.1, 3.8,
2.0, 3.2, 4.4, 3.9],
lod = fill(2.0, 28),
partitions_pos = fill(1200, 28),
n_partitions = 20000.0,
spl_w = vcat([[0.5, 0.5] for _ in 1:28]...),
spl_v = vcat([[min(i, 5), min(i, 5) + 1] for i in 1:28]...),
spl_u = collect(1:2:(2*28 + 1)),
n_basis = 6, mwpd = 1.0e6,
)
# ── Renewal cores: one per observation family, each leaving the Rt process
# (`log_rt`) open. The concentration and dPCR families pair the SAME renewal
# core with a different observation submodel. ──
es_base_conc = @slic es_data begin
nt = dims(week_of_day)[1]
ot = dims(conc)[1]
uot = nt - ot
log_i0 ~ normal(-13.0, 1.0)
growth ~ normal(0.0, 0.05)
infections = es_renewal(log_i0, growth, log_rt, gen_int, uot) # log_rt UNBOUND
signal = es_conv(infections, shed, ot, uot)
conc ~ obs_lod(; signal, mwpd, lod)
end
es_base_dpcr = @slic es_data begin
nt = dims(week_of_day)[1]
ot = dims(partitions_pos)[1]
uot = nt - ot
log_i0 ~ normal(-13.0, 1.0)
growth ~ normal(0.0, 0.05)
infections = es_renewal(log_i0, growth, log_rt, gen_int, uot) # log_rt UNBOUND
signal = es_conv(infections, shed, ot, uot)
partitions_pos ~ obs_dpcr(; signal, n_partitions, mwpd)
end
# ── Swappable Rt processes ──
rt_rw = quote
log_r0 ~ normal(0.0, 0.2)
eta_sd ~ normal(0.0, 0.1; lower = 0.0)
n_weeks = maximum(week_of_day)
w :: vector[n_weeks - 1] ~ std_normal()
log_rt = es_rw(log_r0, eta_sd, w)[week_of_day]
end
rt_spline = quote
beta :: vector[n_basis] ~ std_normal()
log_rt = csr_matrix_times_vector(nt, n_basis, spl_w, spl_v, spl_u, beta)
end
# ── The composable library — swap the Rt process (and the observation family) ──
es_models = (;
rw_lod = Base.merge(es_base_conc, rt_rw), # RW Rt, LOD concentration
spline_lod = Base.merge(es_base_conc, rt_spline), # CSR spline Rt, LOD concentration
rw_dpcr = Base.merge(es_base_dpcr, rt_rw), # RW Rt, dPCR counts (to_int)
)rw_lod
functions {
vector es_rw(
real x0,
real sd,
vector z
) {
int m = dims(z)[1];
vector[(m + 1)] out;
out[1] = x0;
for(i in 1:m) {
out[(i + 1)] = (out[i] + (sd * z[i]));
}
return out;
}
vector es_renewal(
real log_i0,
real growth,
vector log_rt,
vector gen_int,
int uot
) {
int nt = dims(log_rt)[1];
vector[nt] infections;
for(t in 1:uot) {
infections[t] = exp((log_i0 + (growth * (t - 1))));
}
for(t in (uot + 1):nt) {
infections[t] = (exp(log_rt[t]) * es_conv_at(infections, gen_int, t));
}
return infections;
}
real es_conv_at(
vector x,
vector pmf,
int t
) {
int np = dims(pmf)[1];
real acc = 0.0;
int kmax = min(np, (t - 1));
for(k in 1:kmax) {
acc = (acc + (x[(t - k)] * pmf[k]));
}
return acc;
}
vector es_conv(
vector x,
vector pmf,
int out_n,
int off
) {
vector[out_n] out;
for(i in 1:out_n) {
out[i] = es_conv_at(x, pmf, (i + off));
}
return out;
}
real lower_clamping_normal_lpdf(
vector y,
vector lo,
vector args1,
real args2
) {
return sum(lower_clamping_normal_lpdfs(y, lo, args1, args2));
}
vector lower_clamping_normal_lpdfs(
vector y,
vector lo,
vector args1,
real args2
) {
int n = dims(y)[1];
return jbroadcasted_lower_clamping_lpdf_normal(y, lo, args1, args2);
}
vector jbroadcasted_lower_clamping_lpdf_normal(
vector x1,
vector x3,
vector x4,
real x5
) {
int n = dims(x1)[1];
vector[n] rv;
for(i in 1:n) {
rv[i] = lower_clamping_normal_lpdf(broadcasted_getindex(x1, i) |
broadcasted_getindex(x3, i),
broadcasted_getindex(x4, i),
x5
);
}
return rv;
}
real lower_clamping_normal_lpdf(
real y,
real lo,
real args1,
real args2
) {
array[1] real rv;
rv[1] = negative_infinity();
if((y == lo)) {
rv[1] = normal_lcdf_stable(lo, args1, args2);
} else {
if((y > lo)) {
rv[1] = normal_lpdf(y | args1, args2);
}
}
return rv[1];
}
real normal_lcdf_stable(
real x,
real loc,
real scale
) {
return (log(erfc(((-(x - loc)) / (scale * sqrt(2.0))))) - log(2.0));
}
real broadcasted_getindex(vector x, int i) {
return x[i];
}
vector lower_clamping_vector_normal_rng(
int anontok__1,
vector lo,
vector args1,
real args2
) {
int n = anontok__1;
return jbroadcasted_lower_clamping_cell_rng_normal_rng(rep_vector(0.0, n), lo, args1, args2);
}
vector jbroadcasted_lower_clamping_cell_rng_normal_rng(
vector x1,
vector x3,
vector x4,
real x5
) {
int n = dims(x1)[1];
vector[n] rv;
for(i in 1:n) {
rv[i] = lower_clamping_cell_normal_rng(
broadcasted_getindex(x1, i),
broadcasted_getindex(x3, i),
broadcasted_getindex(x4, i),
x5
);
}
return rv;
}
real lower_clamping_cell_normal_rng(
real dummy,
real lo,
real args1,
real args2
) {
return lower_clamping_normal_rng(lo, args1, args2);
}
real lower_clamping_normal_rng(
real lo,
real args1,
real args2
) {
vector[1] draw;
draw[1] = normal_rng(args1, args2);
if((draw[1] < lo)) {
draw[1] = lo;
}
return draw[1];
}
}
data {
int week_of_day_n;
int conc_n;
array[week_of_day_n] int week_of_day;
int gen_int_n;
vector[gen_int_n] gen_int;
int shed_n;
vector[shed_n] shed;
real mwpd;
vector[conc_n] conc;
int lod_n;
vector[lod_n] lod;
}
transformed data {
int nt = week_of_day_n;
int ot = conc_n;
int uot = (nt - ot);
int n_weeks = max(week_of_day);
}
parameters {
real log_i0;
real growth;
real log_r0;
real<lower=0.0> eta_sd;
vector[(n_weeks - 1)] w;
real conc_log10_g;
real<lower=0.0> conc_sigma;
}
transformed parameters {
vector[week_of_day_n] log_rt = es_rw(log_r0, eta_sd, w)[week_of_day];
vector[week_of_day_n] infections = es_renewal(log_i0, growth, log_rt, gen_int, uot);
vector[ot] signal = es_conv(infections, shed, ot, uot);
vector[ot] conc_model_conc = (((log(10.0) * conc_log10_g) + log((signal + 1.0e-8))) - log(mwpd));
}
model {
log_i0 ~ normal(-13.0, 1.0);
growth ~ normal(0.0, 0.05);
log_r0 ~ normal(0.0, 0.2);
eta_sd ~ normal(0.0, 0.1);
w ~ std_normal();
conc_log10_g ~ normal(9.0, 1.0);
conc_sigma ~ normal(0.0, 1.0);
conc ~ lower_clamping_normal(lod, conc_model_conc, conc_sigma);
}
generated quantities {
vector[conc_n] conc_likelihood = lower_clamping_normal_lpdfs(conc, lod, conc_model_conc, conc_sigma);
vector[conc_n] conc_gen = lower_clamping_vector_normal_rng(conc_n, lod, conc_model_conc, conc_sigma);
}spline_lod
functions {
vector es_renewal(
real log_i0,
real growth,
vector log_rt,
vector gen_int,
int uot
) {
int nt = dims(log_rt)[1];
vector[nt] infections;
for(t in 1:uot) {
infections[t] = exp((log_i0 + (growth * (t - 1))));
}
for(t in (uot + 1):nt) {
infections[t] = (exp(log_rt[t]) * es_conv_at(infections, gen_int, t));
}
return infections;
}
real es_conv_at(
vector x,
vector pmf,
int t
) {
int np = dims(pmf)[1];
real acc = 0.0;
int kmax = min(np, (t - 1));
for(k in 1:kmax) {
acc = (acc + (x[(t - k)] * pmf[k]));
}
return acc;
}
vector es_conv(
vector x,
vector pmf,
int out_n,
int off
) {
vector[out_n] out;
for(i in 1:out_n) {
out[i] = es_conv_at(x, pmf, (i + off));
}
return out;
}
real lower_clamping_normal_lpdf(
vector y,
vector lo,
vector args1,
real args2
) {
return sum(lower_clamping_normal_lpdfs(y, lo, args1, args2));
}
vector lower_clamping_normal_lpdfs(
vector y,
vector lo,
vector args1,
real args2
) {
int n = dims(y)[1];
return jbroadcasted_lower_clamping_lpdf_normal(y, lo, args1, args2);
}
vector jbroadcasted_lower_clamping_lpdf_normal(
vector x1,
vector x3,
vector x4,
real x5
) {
int n = dims(x1)[1];
vector[n] rv;
for(i in 1:n) {
rv[i] = lower_clamping_normal_lpdf(broadcasted_getindex(x1, i) |
broadcasted_getindex(x3, i),
broadcasted_getindex(x4, i),
x5
);
}
return rv;
}
real lower_clamping_normal_lpdf(
real y,
real lo,
real args1,
real args2
) {
array[1] real rv;
rv[1] = negative_infinity();
if((y == lo)) {
rv[1] = normal_lcdf_stable(lo, args1, args2);
} else {
if((y > lo)) {
rv[1] = normal_lpdf(y | args1, args2);
}
}
return rv[1];
}
real normal_lcdf_stable(
real x,
real loc,
real scale
) {
return (log(erfc(((-(x - loc)) / (scale * sqrt(2.0))))) - log(2.0));
}
real broadcasted_getindex(vector x, int i) {
return x[i];
}
vector lower_clamping_vector_normal_rng(
int anontok__1,
vector lo,
vector args1,
real args2
) {
int n = anontok__1;
return jbroadcasted_lower_clamping_cell_rng_normal_rng(rep_vector(0.0, n), lo, args1, args2);
}
vector jbroadcasted_lower_clamping_cell_rng_normal_rng(
vector x1,
vector x3,
vector x4,
real x5
) {
int n = dims(x1)[1];
vector[n] rv;
for(i in 1:n) {
rv[i] = lower_clamping_cell_normal_rng(
broadcasted_getindex(x1, i),
broadcasted_getindex(x3, i),
broadcasted_getindex(x4, i),
x5
);
}
return rv;
}
real lower_clamping_cell_normal_rng(
real dummy,
real lo,
real args1,
real args2
) {
return lower_clamping_normal_rng(lo, args1, args2);
}
real lower_clamping_normal_rng(
real lo,
real args1,
real args2
) {
vector[1] draw;
draw[1] = normal_rng(args1, args2);
if((draw[1] < lo)) {
draw[1] = lo;
}
return draw[1];
}
}
data {
int week_of_day_n;
int conc_n;
int n_basis;
int spl_w_n;
vector[spl_w_n] spl_w;
int spl_v_n;
array[spl_v_n] int spl_v;
int spl_u_n;
array[spl_u_n] int spl_u;
int gen_int_n;
vector[gen_int_n] gen_int;
int shed_n;
vector[shed_n] shed;
real mwpd;
vector[conc_n] conc;
int lod_n;
vector[lod_n] lod;
}
transformed data {
int nt = week_of_day_n;
int ot = conc_n;
int uot = (nt - ot);
}
parameters {
real log_i0;
real growth;
vector[n_basis] beta;
real conc_log10_g;
real<lower=0.0> conc_sigma;
}
transformed parameters {
vector[nt] log_rt = csr_matrix_times_vector(nt, n_basis, spl_w, spl_v, spl_u, beta);
vector[nt] infections = es_renewal(log_i0, growth, log_rt, gen_int, uot);
vector[ot] signal = es_conv(infections, shed, ot, uot);
vector[ot] conc_model_conc = (((log(10.0) * conc_log10_g) + log((signal + 1.0e-8))) - log(mwpd));
}
model {
log_i0 ~ normal(-13.0, 1.0);
growth ~ normal(0.0, 0.05);
beta ~ std_normal();
conc_log10_g ~ normal(9.0, 1.0);
conc_sigma ~ normal(0.0, 1.0);
conc ~ lower_clamping_normal(lod, conc_model_conc, conc_sigma);
}
generated quantities {
vector[conc_n] conc_likelihood = lower_clamping_normal_lpdfs(conc, lod, conc_model_conc, conc_sigma);
vector[conc_n] conc_gen = lower_clamping_vector_normal_rng(conc_n, lod, conc_model_conc, conc_sigma);
}rw_dpcr
functions {
vector es_rw(
real x0,
real sd,
vector z
) {
int m = dims(z)[1];
vector[(m + 1)] out;
out[1] = x0;
for(i in 1:m) {
out[(i + 1)] = (out[i] + (sd * z[i]));
}
return out;
}
vector es_renewal(
real log_i0,
real growth,
vector log_rt,
vector gen_int,
int uot
) {
int nt = dims(log_rt)[1];
vector[nt] infections;
for(t in 1:uot) {
infections[t] = exp((log_i0 + (growth * (t - 1))));
}
for(t in (uot + 1):nt) {
infections[t] = (exp(log_rt[t]) * es_conv_at(infections, gen_int, t));
}
return infections;
}
real es_conv_at(
vector x,
vector pmf,
int t
) {
int np = dims(pmf)[1];
real acc = 0.0;
int kmax = min(np, (t - 1));
for(k in 1:kmax) {
acc = (acc + (x[(t - k)] * pmf[k]));
}
return acc;
}
vector es_conv(
vector x,
vector pmf,
int out_n,
int off
) {
vector[out_n] out;
for(i in 1:out_n) {
out[i] = es_conv_at(x, pmf, (i + off));
}
return out;
}
vector neg_binomial_2_lpmfs(
array[] int obs,
vector mu,
real phi
) {
return jbroadcasted_neg_binomial_2_lpmfs(obs, mu, phi);
}
vector jbroadcasted_neg_binomial_2_lpmfs(
array[] int x1,
vector x2,
real x3
) {
int n = dims(x1)[1];
vector[n] rv;
for(i in 1:n) {
rv[i] = neg_binomial_2_lpmfs(broadcasted_getindex(x1, i), broadcasted_getindex(x2, i), x3);
}
return rv;
}
real neg_binomial_2_lpmfs(
int args1,
real args2,
real args3
) {
return neg_binomial_2_lpmf(args1 | args2, args3);
}
int broadcasted_getindex(array[] int x, int i) {
return x[i];
}
real broadcasted_getindex(vector x, int i) {
return x[i];
}
array[] int neg_binomial_2_int_rng(
int anontok__1,
vector a,
real b
) {
int n = anontok__1;
if((n == 0)) {
array[n] int rv;
return rv;
} else {
return neg_binomial_2_rng(a, b);
}
}
}
data {
int week_of_day_n;
int partitions_pos_n;
array[week_of_day_n] int week_of_day;
int gen_int_n;
vector[gen_int_n] gen_int;
int shed_n;
vector[shed_n] shed;
real n_partitions;
real mwpd;
array[partitions_pos_n] int partitions_pos;
}
transformed data {
int nt = week_of_day_n;
int ot = partitions_pos_n;
int uot = (nt - ot);
int n_weeks = max(week_of_day);
int partitions_pos_n_part = to_int(round(n_partitions));
}
parameters {
real log_i0;
real growth;
real log_r0;
real<lower=0.0> eta_sd;
vector[(n_weeks - 1)] w;
real<lower=0.0> partitions_pos_phi;
}
transformed parameters {
vector[week_of_day_n] log_rt = es_rw(log_r0, eta_sd, w)[week_of_day];
vector[week_of_day_n] infections = es_renewal(log_i0, growth, log_rt, gen_int, uot);
vector[ot] signal = es_conv(infections, shed, ot, uot);
vector[ot] partitions_pos_lambda = (((100000.0 * signal) * partitions_pos_n_part) / mwpd);
}
model {
log_i0 ~ normal(-13.0, 1.0);
growth ~ normal(0.0, 0.05);
log_r0 ~ normal(0.0, 0.2);
eta_sd ~ normal(0.0, 0.1);
w ~ std_normal();
partitions_pos_phi ~ gamma(2.0, 0.1);
partitions_pos ~ neg_binomial_2(partitions_pos_lambda, partitions_pos_phi);
}
generated quantities {
vector[partitions_pos_n] partitions_pos_likelihood = neg_binomial_2_lpmfs(partitions_pos, partitions_pos_lambda, partitions_pos_phi);
array[partitions_pos_n] int partitions_pos_gen = neg_binomial_2_int_rng(partitions_pos_n, partitions_pos_lambda, partitions_pos_phi);
}What this exercises
to_int— the real→integer partition total in the digital-PCR observation family (to_int(round(n_partitions))), a data-side conversion that lands intransformed datawhere Stan'sdata-qualified-argument rule is satisfied.csr_matrix_times_vector— the sparse compressed-row spline basis for the spline Rt process, exposed directly rather than densified.Observation submodels — the concentration and dPCR measurement families as self-contained, swappable nodes.
Base.mergecomposition — the R monolith's runtime module flags become construction-time assembly, mirroringEpiSewer.jl's component design.
For the full joint renewal model — two coupled observation streams, a multi-subpopulation structure, tuple-returning renewal and per-site effects — see the wastewater renewal model case study.