BayesianRegressionModels.jlA brms-shaped formula DSL for Julia
Same formula grammar, but your columns do not all have to come from one equal-length data frame. Runs directly in Turing or transpiles to Stan through StanBlocks.
A formula DSL for Bayesian regression. The macro @brm parses brms-style syntax (y ~ 1 + a + (1 | g), log(err) ~ 1 + b, y ~ Normal(loc, err)) into a BRMI intermediate representation, then lowers into a backend-specific executor:
TuringBRMI — direct Turing / DynamicPPL.Model execution for the generic backend capabilities and support contract.
SBBRMI — StanBlocks → Stan source, fit via Pathfinder or full warmup HMC (WarmupHMC.adaptive_warmup_mcmc).
The BRM feature atlas gives every executable example the same build-generated four-way view: BRM authoring, the emitted StanBlocks model, generated Stan, and the selected Turing model.
The Warfarin PK/PD examples render both the faithful public two-stage workflow and a joint one-posterior model where shared latent PK effects feed the PK and PD likelihoods.
The Adaptive centering case studies compare fixed coordinates, post-hoc selection and online adaptation, with explicit gradient costs and common scientific quantities:
Motorcycle HSGP: two Gaussian processes for the mean and changing noise level.
Eight schools: PosteriorDB priors and manual fully centered selection.
Radon: county intercepts and floor slopes, with categorical predictive checks.
Pupil: numeric scale predictor: automatic total coefficients, brms sum-to-zero comparisons and both adaptation losses.
Pupil: hierarchical residual SD: automatic totals in both the mean and residual-scale predictors.
Air pollution: regional intercepts and slopes, exact marginalization and matched scientific quantities.
BRM regression modelintro_model = (@brm begin
y ~ Normal(loc, err)
loc ~ 1 + age + sex + (1 + age | subj)
err ~ Exponential(1)
end)((;
age=[21.0, 38.0, 55.0, 29.0, 47.0, 61.0],
sex=[1, 2, 1, 2, 1, 2],
subj=[1, 1, 2, 2, 3, 3],
y=[0.2, 1.1, -0.4, 0.7, 1.4, 1.0],
))BRMI:
loc ~ 1 + age + sex + ((1 + age) | subj)
err ~ Exponential(1)
y ~ Normal(loc, err)
age: data (eltype=Float64, n=6)
sex: data (eltype=Int64, n=6)
subj: data (eltype=Int64, n=6)SBBRMI with data keys = [:age, :n_subj, :n_terms_loc_subj, :sex, :sex_idx, :sex_n_levels, :subj, :subj_idx, :y]
emitted @slic body:
begin
err ~ exponential(1.0 ./ 1)
X_loc = hcat(rep_vector(1.0, num_elements(age)), age)
pop_loc ~ popefs(; X = X_loc)
cat_loc_sex ~ _sb_cat(; x = sex_idx, n_levels = sex_n_levels)
Z_loc_subj = hcat(rep_vector(1.0, num_elements(age)), age)
r_loc_subj ~ ranef_correlated(; Z = Z_loc_subj, group_idx = subj_idx, n_groups = n_subj, n_terms = n_terms_loc_subj)
loc = pop_loc + cat_loc_sex + r_loc_subj
y ~ normal(loc, err)
endfunctions {
matrix hcat(
vector x,
vector y
) {
int n = dims(x)[1];
if (dims(y)[1] != n) reject("hcat: dim mismatch — `y` dim 1 (= ", dims(y)[1], ") does not match `n` (= ", n, "), inferred from `x` dim 1. `n` sizes: `x` dim 1 (= ", dims(x)[1], "), `y` dim 1 (= ", dims(y)[1], ").");
return append_col(x, y);
}
vector normal_lpdfs(
vector obs,
vector loc,
real scale
) {
return jbroadcasted_normal_lpdfs(obs, loc, scale);
}
vector jbroadcasted_normal_lpdfs(
vector x1,
vector x2,
real x3
) {
int n = dims(x1)[1];
vector[n] rv;
for(i in 1:n) {
rv[i] = normal_lpdfs(broadcasted_getindex(x1, i), broadcasted_getindex(x2, i), x3);
}
return rv;
}
real normal_lpdfs(
real args1,
real args2,
real args3
) {
return normal_lpdf(args1 | args2, args3);
}
real broadcasted_getindex(vector x, int i) {
return x[i];
}
vector normal_vector_rng(
int anontok__1,
vector a,
real b
) {
int n = anontok__1;
if((n == 0)) {
vector[n] rv;
return rv;
} else {
return to_vector(normal_rng(a, b));
}
}
}
data {
int age_n;
vector[age_n] age;
int sex_n_levels;
int sex_idx_n;
array[sex_idx_n] int sex_idx;
int n_terms_loc_subj;
int n_subj;
int subj_idx_n;
array[subj_idx_n] int subj_idx;
int y_n;
vector[y_n] y;
}
transformed data {
matrix[age_n, 2] X_loc = hcat(rep_vector(1.0, num_elements(age)), age);
int pop_loc_n_covariates = 2;
matrix[age_n, 2] Z_loc_subj = hcat(rep_vector(1.0, num_elements(age)), age);
}
parameters {
real<lower=0.0> err;
vector[pop_loc_n_covariates] pop_loc_beta_pop;
vector[(sex_n_levels - 1)] cat_loc_sex_beta;
cholesky_factor_corr[n_terms_loc_subj] r_loc_subj_L;
vector<lower=0.0>[n_terms_loc_subj] r_loc_subj_tau;
vector[(n_terms_loc_subj * n_subj)] r_loc_subj_z_flat;
}
transformed parameters {
vector[age_n] pop_loc = (X_loc * pop_loc_beta_pop);
vector[sex_idx_n] cat_loc_sex = append_row(0.0, cat_loc_sex_beta)[sex_idx];
matrix[n_terms_loc_subj, n_subj] r_loc_subj_z = to_matrix(r_loc_subj_z_flat, n_terms_loc_subj, n_subj);
matrix[n_subj, n_terms_loc_subj] r_loc_subj_b = ((diag_pre_multiply(r_loc_subj_tau, r_loc_subj_L) * r_loc_subj_z)');
vector[subj_idx_n] r_loc_subj = rows_dot_product(Z_loc_subj, r_loc_subj_b[subj_idx, :]);
vector[age_n] loc = (pop_loc + cat_loc_sex + r_loc_subj);
}
model {
err ~ exponential((1.0 ./ 1));
pop_loc_beta_pop ~ std_normal();
cat_loc_sex_beta ~ std_normal();
r_loc_subj_L ~ lkj_corr_cholesky(1.0);
r_loc_subj_tau ~ std_normal();
r_loc_subj_z_flat ~ std_normal();
y ~ normal(loc, err);
}
generated quantities {
vector[y_n] y_likelihood = normal_lpdfs(y, loc, err);
vector[y_n] y_gen = normal_vector_rng(y_n, loc, err);
}#= line 0 =# Turing.@model(function brm_model(y, X_loc, group_matrix_loc_1, group_indices_loc_1, group_levels_loc_1)
err ~ Distributions.Exponential(1)
beta_pop ~ Distributions.product_distribution(Base.fill(Distributions.Normal(), 3))
eta_loc = X_loc * beta_pop
group_effect_1 = Base.zeros(Base.length(y))
group_1_1 ~ DynamicPPL.to_submodel(BRM.turing_default_correlated_group(group_matrix_loc_1, group_indices_loc_1, group_levels_loc_1, 1.0))
group_effect_1 = group_effect_1 + group_1_1.effect
eta_loc = eta_loc + group_effect_1
loc = eta_loc
begin
for i = Base.eachindex(y)
y[i] ~ Distributions.Normal(loc[i], err)
end
end
(; loc = loc, err = err, response = y)
end)BRM is deliberately brms-shaped, so most of what you know transfers. The differences worth knowing up front are one structural gain and a genuinely shorter feature list.
Formula grammar, random-effect syntax, and the two features people usually reach brms for:
| brms | @brm |
|---|---|
y ~ 1 + age + sex | y ~ 1 + age + sex |
(1 + age | subj) | (1 + age | subj) |
(1 + age |p| subj) — correlated across formulas | (1 + age |p| subj) |
(1 | gr(subj, by = diagnosis)) | (1 | gr(subj, by = diagnosis)) |
bf(y1) + bf(y2) | two ~ lines in the same block |
Distributional regression — bf(y ~ x, sigma ~ x) | every distributional parameter is just another ~ line |
Nonlinear terms — bf(y ~ a * exp(-b * x), a ~ 1, b ~ 1, nl = TRUE) | the same, with no nl switch |
Distributional regression needs no special form: give the parameter its own formula, and apply the link yourself where you want one.
Distributional Gaussian regressiondistributional = (@brm begin
y ~ Normal(mu, sigma)
mu ~ 1 + age
log(sigma) ~ 1 + age # explicit link, addressed as `sigma`
end)((; age=[21.0, 38.0, 55.0, 29.0], y=[0.2, 1.1, -0.4, 0.7]))BRMI:
mu ~ 1 + age
log(sigma) ~ 1 + age
y ~ Normal(mu, sigma)
age: data (eltype=Float64, n=4)SBBRMI with data keys = [:age, :y]
emitted @slic body:
begin
X_mu = hcat(rep_vector(1.0, num_elements(age)), age)
pop_mu ~ _popefs_coefs(; X = X_mu)
X_log_sigma = hcat(rep_vector(1.0, num_elements(age)), age)
pop_log_sigma ~ popefs(; X = X_log_sigma)
log_sigma = pop_log_sigma
sigma = exp(log_sigma)
y ~ normal_id_glm(X_mu, 0.0, pop_mu, sigma)
mu = X_mu * pop_mu
endfunctions {
matrix hcat(
vector x,
vector y
) {
int n = dims(x)[1];
if (dims(y)[1] != n) reject("hcat: dim mismatch — `y` dim 1 (= ", dims(y)[1], ") does not match `n` (= ", n, "), inferred from `x` dim 1. `n` sizes: `x` dim 1 (= ", dims(x)[1], "), `y` dim 1 (= ", dims(y)[1], ").");
return append_col(x, y);
}
vector normal_id_glm_lpdfs(
vector y,
matrix X,
real alpha,
vector beta,
vector sigma
) {
int n = dims(y)[1];
if (dims(X)[1] != n) reject("normal_id_glm_lpdfs: dim mismatch — `X` dim 1 (= ", dims(X)[1], ") does not match `n` (= ", n, "), inferred from `y` dim 1. `n` sizes: `y` dim 1 (= ", dims(y)[1], "), `X` dim 1 (= ", dims(X)[1], ").");
vector[n] rv;
for(i in 1:n) {
rv[i] = normal_lpdf(y[i] | (alpha + (X[i, :] * beta)), sigma);
}
return rv;
}
vector normal_id_glm_vector_rng(
int anontok__1,
matrix X,
real alpha,
vector beta,
vector sigma
) {
int m = anontok__1;
if (dims(X)[1] != m) reject("normal_id_glm_rng: dim mismatch — `X` dim 1 (= ", dims(X)[1], ") does not match `m` (= ", m, "), inferred from `anontok__1` dim 1. `m` sizes: `anontok__1` dim 1 (= ", anontok__1, "), `X` dim 1 (= ", dims(X)[1], ").");
if((m == 0)) {
vector[m] rv;
return rv;
} else {
return normal_id_glm_rng(X, alpha, beta, sigma);
}
}
vector normal_id_glm_rng(
matrix X,
real alpha,
vector beta,
vector sigma
) {
int m = dims(X)[1];
if((m == 0)) {
vector[m] rv;
return rv;
} else {
return to_vector(normal_rng((rep_vector(alpha, m) + (X * beta)), sigma));
}
}
}
data {
int age_n;
vector[age_n] age;
int y_n;
vector[y_n] y;
}
transformed data {
matrix[age_n, 2] X_mu = hcat(rep_vector(1.0, num_elements(age)), age);
int pop_mu_n_covariates = 2;
matrix[age_n, 2] X_log_sigma = hcat(rep_vector(1.0, num_elements(age)), age);
int pop_log_sigma_n_covariates = 2;
}
parameters {
vector[pop_mu_n_covariates] pop_mu_beta_pop;
vector[pop_log_sigma_n_covariates] pop_log_sigma_beta_pop;
}
transformed parameters {
vector[pop_mu_n_covariates] pop_mu = pop_mu_beta_pop;
vector[age_n] pop_log_sigma = (X_log_sigma * pop_log_sigma_beta_pop);
vector[age_n] log_sigma = pop_log_sigma;
vector[age_n] sigma = exp(log_sigma);
}
model {
pop_mu_beta_pop ~ std_normal();
pop_log_sigma_beta_pop ~ std_normal();
y ~ normal_id_glm(X_mu, 0.0, pop_mu, sigma);
}
generated quantities {
vector[age_n] y_likelihood = normal_id_glm_lpdfs(y, X_mu, 0.0, pop_mu, sigma);
vector[age_n] y_gen = normal_id_glm_vector_rng(y_n, X_mu, 0.0, pop_mu, sigma);
vector[age_n] mu = (X_mu * pop_mu);
}#= line 0 =# Turing.@model(function brm_model(y, X_mu, X_sigma)
beta_pop ~ Distributions.product_distribution(Base.fill(Distributions.Normal(), 2))
eta_mu = X_mu * beta_pop
mu = eta_mu
beta_pop_sigma ~ Distributions.product_distribution(Base.fill(Distributions.Normal(), 2))
eta_sigma = X_sigma * beta_pop_sigma
sigma = Base.exp.(eta_sigma)
begin
for i = Base.eachindex(y)
y[i] ~ Distributions.Normal(mu[i], sigma[i])
end
end
(; mu = mu, sigma = sigma, response = y)
end)Nonlinear models need no nl = TRUE and no nlf(). A declaration is a named value, so composing declarations into an arbitrary Julia expression is the whole feature:
Nonlinear predictor compositionnonlinear = (@brm begin
a ~ 1 + (1 | g) # ordinary linear predictors …
b ~ 1
y ~ Normal(a * exp(-b * x), sigma) # … composed nonlinearly
sigma ~ Exponential(1)
end)((;
g=[1, 1, 2, 2], x=[0.0, 0.5, 1.0, 1.5],
y=[1.0, 0.8, 0.5, 0.3],
))BRMI:
g: data (eltype=Int64, n=4)
a ~ 1 + (1 | g)
b ~ 1
x: data (eltype=Float64, n=4)
sigma ~ Exponential(1)
y ~ Normal((a * exp((-(b) * x))), sigma)SBBRMI with data keys = [:g, :total_A_a, :total_group_a, :total_location_a, :total_ng_a, :total_nk_a, :total_np_a, :total_precision_a, :x, :y]
configured submodels:
_brm_total_scales_configured_1 = Base.merge(BayesianRegressionModels._brm_total_scales, quote
tau ~ (ValueFamily(brm_vector_prior_faeb6f6956d0662a))(0.0, 1.0; n = 1)
end)
emitted @slic body:
begin
total_scale_a ~ _brm_total_scales_configured_1(; n = total_nk_a)
total_a::matrix[total_ng_a, total_nk_a] ~ brm_total(total_scale_a, total_A_a, total_location_a, total_precision_a)
population_a = brm_total_recover_rng(total_a, total_scale_a, total_A_a, total_location_a, total_precision_a)
deviation_a = brm_total_deviations(total_a, total_A_a * population_a)
total_Z_a = hcat(rep_vector(1.0, num_elements(total_group_a)))
a = rows_dot_product(total_a[total_group_a, :], total_Z_a)
X_b = hcat(rep_vector(1.0, num_elements(y)))
pop_b ~ popefs(; X = X_b)
b = pop_b
sigma ~ exponential(1.0 ./ 1)
y ~ normal(a .* (exp)((-)(b) .* x), sigma)
endfunctions {
// value UDF brm_vector_prior_faeb6f6956d0662a_lpdf
real brm_vector_prior_faeb6f6956d0662a_lpdf(
vector x,
real arg_1,
real arg_2
) {
if((x[1] < 0.0)) {
return negative_infinity();
}
return lognormal_lpdf(x[1] | arg_1, arg_2);
}
real brm_total_lpdf(
matrix total,
vector tau,
matrix A,
vector location,
vector precision
) {
int j = dims(total)[1];
int k = dims(total)[2];
int p = dims(A)[2];
if (dims(tau)[1] != k) reject("brm_total_lpdf: dim mismatch — `tau` dim 1 (= ", dims(tau)[1], ") does not match `k` (= ", k, "), inferred from `total` dim 2. `k` sizes: `total` dim 2 (= ", dims(total)[2], "), `tau` dim 1 (= ", dims(tau)[1], "), `A` dim 1 (= ", dims(A)[1], ").");
if (dims(A)[1] != k) reject("brm_total_lpdf: dim mismatch — `A` dim 1 (= ", dims(A)[1], ") does not match `k` (= ", k, "), inferred from `total` dim 2. `k` sizes: `total` dim 2 (= ", dims(total)[2], "), `tau` dim 1 (= ", dims(tau)[1], "), `A` dim 1 (= ", dims(A)[1], ").");
if (dims(location)[1] != p) reject("brm_total_lpdf: dim mismatch — `location` dim 1 (= ", dims(location)[1], ") does not match `p` (= ", p, "), inferred from `A` dim 2. `p` sizes: `A` dim 2 (= ", dims(A)[2], "), `location` dim 1 (= ", dims(location)[1], "), `precision` dim 1 (= ", dims(precision)[1], ").");
if (dims(precision)[1] != p) reject("brm_total_lpdf: dim mismatch — `precision` dim 1 (= ", dims(precision)[1], ") does not match `p` (= ", p, "), inferred from `A` dim 2. `p` sizes: `A` dim 2 (= ", dims(A)[2], "), `location` dim 1 (= ", dims(location)[1], "), `precision` dim 1 (= ", dims(precision)[1], ").");
matrix[dims(precision)[1], dims(precision)[1]] Q = brm_total_precision(tau, A, precision, j);
vector[dims(total)[2]] average = brm_total_mean(total);
vector[dims(precision)[1]] beta = brm_total_conditional_mean(total, tau, A, location, precision, Q);
vector[dims(total)[2]] residual = (average - (A * beta));
real quadratic = 0.0;
real lp = ((-0.5 * ((j * k) - p) * 1.8378770664093453) - (j * sum(log(tau))));
for(c in 1:k) {
quadratic += ((j * square(residual[c])) / square(tau[c]));
for(g in 1:j) {
quadratic += (square((total[g, c] - average[c])) / square(tau[c]));
}
}
for(a in 1:p) {
if((precision[a] > 0.0)) {
lp += (0.5 * (log(precision[a]) - 1.8378770664093453));
quadratic += (precision[a] * square((beta[a] - location[a])));
}
}
return (lp - (0.5 * (log_determinant(Q) + quadratic)));
}
matrix brm_total_precision(
vector tau,
matrix A,
vector precision,
int n_groups
) {
int k = dims(tau)[1];
int p = dims(A)[2];
if (dims(A)[1] != k) reject("brm_total_precision: dim mismatch — `A` dim 1 (= ", dims(A)[1], ") does not match `k` (= ", k, "), inferred from `tau` dim 1. `k` sizes: `tau` dim 1 (= ", dims(tau)[1], "), `A` dim 1 (= ", dims(A)[1], ").");
if (dims(precision)[1] != p) reject("brm_total_precision: dim mismatch — `precision` dim 1 (= ", dims(precision)[1], ") does not match `p` (= ", p, "), inferred from `A` dim 2. `p` sizes: `A` dim 2 (= ", dims(A)[2], "), `precision` dim 1 (= ", dims(precision)[1], ").");
matrix[dims(precision)[1], dims(precision)[1]] out = diag_matrix(precision);
for(a in 1:p) {
for(b in 1:p) {
for(c in 1:k) {
out[a, b] += ((n_groups * A[c, a] * A[c, b]) / square(tau[c]));
}
}
}
return out;
}
vector brm_total_mean(
matrix total
) {
int j = dims(total)[1];
int k = dims(total)[2];
vector[k] out = rep_vector(0.0, k);
for(c in 1:k) {
out[c] = (sum(total[:, c]) / j);
}
return out;
}
vector brm_total_conditional_mean(
matrix total,
vector tau,
matrix A,
vector location,
vector precision,
matrix Q
) {
int j = dims(total)[1];
int k = dims(total)[2];
int p = dims(A)[2];
if (dims(tau)[1] != k) reject("brm_total_conditional_mean: dim mismatch — `tau` dim 1 (= ", dims(tau)[1], ") does not match `k` (= ", k, "), inferred from `total` dim 2. `k` sizes: `total` dim 2 (= ", dims(total)[2], "), `tau` dim 1 (= ", dims(tau)[1], "), `A` dim 1 (= ", dims(A)[1], ").");
if (dims(A)[1] != k) reject("brm_total_conditional_mean: dim mismatch — `A` dim 1 (= ", dims(A)[1], ") does not match `k` (= ", k, "), inferred from `total` dim 2. `k` sizes: `total` dim 2 (= ", dims(total)[2], "), `tau` dim 1 (= ", dims(tau)[1], "), `A` dim 1 (= ", dims(A)[1], ").");
if (dims(location)[1] != p) reject("brm_total_conditional_mean: dim mismatch — `location` dim 1 (= ", dims(location)[1], ") does not match `p` (= ", p, "), inferred from `A` dim 2. `p` sizes: `A` dim 2 (= ", dims(A)[2], "), `location` dim 1 (= ", dims(location)[1], "), `precision` dim 1 (= ", dims(precision)[1], "), `Q` dim 1 (= ", dims(Q)[1], "), `Q` dim 2 (= ", dims(Q)[2], ").");
if (dims(precision)[1] != p) reject("brm_total_conditional_mean: dim mismatch — `precision` dim 1 (= ", dims(precision)[1], ") does not match `p` (= ", p, "), inferred from `A` dim 2. `p` sizes: `A` dim 2 (= ", dims(A)[2], "), `location` dim 1 (= ", dims(location)[1], "), `precision` dim 1 (= ", dims(precision)[1], "), `Q` dim 1 (= ", dims(Q)[1], "), `Q` dim 2 (= ", dims(Q)[2], ").");
if (dims(Q)[1] != p) reject("brm_total_conditional_mean: dim mismatch — `Q` dim 1 (= ", dims(Q)[1], ") does not match `p` (= ", p, "), inferred from `A` dim 2. `p` sizes: `A` dim 2 (= ", dims(A)[2], "), `location` dim 1 (= ", dims(location)[1], "), `precision` dim 1 (= ", dims(precision)[1], "), `Q` dim 1 (= ", dims(Q)[1], "), `Q` dim 2 (= ", dims(Q)[2], ").");
if (dims(Q)[2] != p) reject("brm_total_conditional_mean: dim mismatch — `Q` dim 2 (= ", dims(Q)[2], ") does not match `p` (= ", p, "), inferred from `A` dim 2. `p` sizes: `A` dim 2 (= ", dims(A)[2], "), `location` dim 1 (= ", dims(location)[1], "), `precision` dim 1 (= ", dims(precision)[1], "), `Q` dim 1 (= ", dims(Q)[1], "), `Q` dim 2 (= ", dims(Q)[2], ").");
vector[dims(total)[2]] average = brm_total_mean(total);
vector[dims(location)[1]] natural = (precision .* location);
for(a in 1:p) {
for(c in 1:k) {
natural[a] += ((j * A[c, a] * average[c]) / square(tau[c]));
}
}
return mdivide_left_spd(Q, natural);
}
vector brm_total_recover_rng(
matrix total,
vector tau,
matrix A,
vector location,
vector precision
) {
int j = dims(total)[1];
int k = dims(total)[2];
int p = dims(A)[2];
if (dims(tau)[1] != k) reject("brm_total_recover_rng: dim mismatch — `tau` dim 1 (= ", dims(tau)[1], ") does not match `k` (= ", k, "), inferred from `total` dim 2. `k` sizes: `total` dim 2 (= ", dims(total)[2], "), `tau` dim 1 (= ", dims(tau)[1], "), `A` dim 1 (= ", dims(A)[1], ").");
if (dims(A)[1] != k) reject("brm_total_recover_rng: dim mismatch — `A` dim 1 (= ", dims(A)[1], ") does not match `k` (= ", k, "), inferred from `total` dim 2. `k` sizes: `total` dim 2 (= ", dims(total)[2], "), `tau` dim 1 (= ", dims(tau)[1], "), `A` dim 1 (= ", dims(A)[1], ").");
if (dims(location)[1] != p) reject("brm_total_recover_rng: dim mismatch — `location` dim 1 (= ", dims(location)[1], ") does not match `p` (= ", p, "), inferred from `A` dim 2. `p` sizes: `A` dim 2 (= ", dims(A)[2], "), `location` dim 1 (= ", dims(location)[1], "), `precision` dim 1 (= ", dims(precision)[1], ").");
if (dims(precision)[1] != p) reject("brm_total_recover_rng: dim mismatch — `precision` dim 1 (= ", dims(precision)[1], ") does not match `p` (= ", p, "), inferred from `A` dim 2. `p` sizes: `A` dim 2 (= ", dims(A)[2], "), `location` dim 1 (= ", dims(location)[1], "), `precision` dim 1 (= ", dims(precision)[1], ").");
matrix[dims(precision)[1], dims(precision)[1]] Q = brm_total_precision(tau, A, precision, j);
vector[dims(precision)[1]] beta = brm_total_conditional_mean(total, tau, A, location, precision, Q);
return multi_normal_rng(beta, inverse_spd(Q));
}
matrix brm_total_deviations(
matrix total,
vector mu
) {
int j = dims(total)[1];
int k = dims(total)[2];
if (dims(mu)[1] != k) reject("brm_total_deviations: dim mismatch — `mu` dim 1 (= ", dims(mu)[1], ") does not match `k` (= ", k, "), inferred from `total` dim 2. `k` sizes: `total` dim 2 (= ", dims(total)[2], "), `mu` dim 1 (= ", dims(mu)[1], ").");
matrix[dims(total)[1], dims(total)[2]] out = total;
for(c in 1:k) {
out[:, c] = (total[:, c] - rep_vector(mu[c], j));
}
return out;
}
matrix hcat(vector x) {
int n = dims(x)[1];
return to_matrix(x, n, 1);
}
vector normal_lpdfs(
vector obs,
vector loc,
real scale
) {
return jbroadcasted_normal_lpdfs(obs, loc, scale);
}
vector jbroadcasted_normal_lpdfs(
vector x1,
vector x2,
real x3
) {
int n = dims(x1)[1];
vector[n] rv;
for(i in 1:n) {
rv[i] = normal_lpdfs(broadcasted_getindex(x1, i), broadcasted_getindex(x2, i), x3);
}
return rv;
}
real normal_lpdfs(
real args1,
real args2,
real args3
) {
return normal_lpdf(args1 | args2, args3);
}
real broadcasted_getindex(vector x, int i) {
return x[i];
}
vector normal_vector_rng(
int anontok__1,
vector a,
real b
) {
int n = anontok__1;
if((n == 0)) {
vector[n] rv;
return rv;
} else {
return to_vector(normal_rng(a, b));
}
}
}
data {
int total_ng_a;
int total_nk_a;
int total_A_a_m;
int total_A_a_n;
matrix[total_A_a_m, total_A_a_n] total_A_a;
int total_location_a_n;
vector[total_location_a_n] total_location_a;
int total_precision_a_n;
vector[total_precision_a_n] total_precision_a;
int total_group_a_n;
array[total_group_a_n] int total_group_a;
int y_n;
vector[y_n] y;
int x_n;
vector[x_n] x;
}
transformed data {
matrix[num_elements(total_group_a), 1] total_Z_a = hcat(rep_vector(1.0, num_elements(total_group_a)));
matrix[num_elements(y), 1] X_b = hcat(rep_vector(1.0, num_elements(y)));
int pop_b_n_covariates = 1;
}
parameters {
vector<lower=0.0>[1] total_scale_a_tau;
matrix[total_ng_a, total_nk_a] total_a;
vector[pop_b_n_covariates] pop_b_beta_pop;
real<lower=0.0> sigma;
}
transformed parameters {
vector<lower=0.0>[1] total_scale_a = total_scale_a_tau;
vector[num_elements(total_group_a)] a = rows_dot_product(total_a[total_group_a, :], total_Z_a);
vector[num_elements(y)] pop_b = (X_b * pop_b_beta_pop);
vector[num_elements(y)] b = pop_b;
}
model {
total_scale_a_tau ~ brm_vector_prior_faeb6f6956d0662a(0.0, 1.0);
total_a ~ brm_total(total_scale_a, total_A_a, total_location_a, total_precision_a);
pop_b_beta_pop ~ std_normal();
sigma ~ exponential((1.0 ./ 1));
y ~ normal((a .* exp(((-b) .* x))), sigma);
}
generated quantities {
vector[total_precision_a_n] population_a = brm_total_recover_rng(total_a, total_scale_a, total_A_a, total_location_a, total_precision_a);
matrix[total_ng_a, total_A_a_m] deviation_a = brm_total_deviations(total_a, (total_A_a * population_a));
vector[y_n] y_likelihood = normal_lpdfs(y, (a .* exp(((-b) .* x))), sigma);
vector[y_n] y_gen = normal_vector_rng(y_n, (a .* exp(((-b) .* x))), sigma);
}#= line 0 =# Turing.@model(function brm_model(y, X_a, group_effects_a_1, X_b, x)
beta_pop ~ Distributions.product_distribution(Base.fill(Distributions.Normal(), 1))
eta_a = X_a * beta_pop
group_effect_1 = Base.zeros(Base.length(y))
group_1_1 ~ DynamicPPL.to_submodel(BRM.turing_group_effect(group_effects_a_1, (nothing,), nothing))
group_effect_1 = group_effect_1 + group_1_1.effect
eta_a = eta_a + group_effect_1
a = eta_a
beta_pop_b ~ Distributions.product_distribution(Base.fill(Distributions.Normal(), 1))
eta_b = X_b * beta_pop_b
b = eta_b
sigma ~ Distributions.Exponential(1)
begin
for i = Base.eachindex(y)
y[i] ~ Distributions.Normal(a[i] * Base.exp(-(b[i]) * x[i]), sigma)
end
end
(; a = a, b = b, sigma = sigma, response = y)
end)That lowers to y ~ normal(a .* exp(-b .* x), sigma) in the generated Stan, and — as with any BRM response — you also get the pointwise log-likelihood y_likelihood and predictive draws y_gen for free.
Your data does not have to be one data frame with equal-length columns. This is the main structural difference. brms takes a single data.frame, so every column shares one row axis. @brm takes any column collection — a NamedTuple is fine — whose columns may live on different row axes, and different linear predictors in one model may be defined on different axes. The multi-axis population PK kernel is a runnable example: its subject columns have one row per person, while its observation columns have one row per sample.
ragged(x, group) attaches a flat secondary frame to the grouping axis, and kernel broadcasts a do-block cell — arbitrary Julia, including a structural or ODE-like time course — over pre-grouped rows, taking the per-group values of ordinary linear predictors as arguments. In brms this class of model is nlf() plus manual data2 bookkeeping, or Stan by hand.
Two smaller gains: VBRMI gives you a pure-Julia LogDensityProblems object with no Stan toolchain in the loop, and brm_descriptor exposes one reflectable description of everything the model emits, so a consumer mounts a fitted model without keeping a parallel registry of parameter names.
Not feature-complete against brms. The gaps a brms user is most likely to hit:
Residual correlation is Gaussian and complete-row only. Use [y1, y2] ~ MvNormalCholesky([mu1, mu2], L_res) with a declared LKJCovarianceFactor; partially missing response vectors and non-Gaussian residual copulas are not implemented.
No fixed / known covariance groups. by= is the only gr option — no cov=, so no phylogenetic or pedigree random effects.
Autocorrelation is first-order only. ar(time; p=1) emits an ordinary AR(1) noise column; dar(time; p=1) emits a direct differenced-AR(1) trajectory. There is no MA, ARMA, compound symmetry, unstructured, CAR or SAR surface.
Fewer families. Likelihoods is the complete list, and it is considerably shorter than brms'.
Formula terms has the full catalogue of what is supported.
Population coefficients use independent standard-normal priors by default. Override selected coefficients with separate effect(...) statements; the coefficient names are exactly those returned by popcoefnames:
Population effect priorspk = (@brm begin
log_ka ~ 1 + weight + (1 | pk | subject)
effect(log_ka, Intercept) ~ Normal(log(1 / 8), 0.8)
effect(log_ka, weight) ~ Normal(0, 0.1)
y ~ Normal(log_ka, 1.0)
end)((;
weight=[55.0, 65.0, 75.0, 85.0],
subject=[1, 1, 2, 2], y=[-2.1, -1.9, -1.8, -1.7],
))BRMI:
weight: data (eltype=Float64, n=4)
subject: data (eltype=Int64, n=4)
log_ka ~ 1 + weight + (1 | pk | subject)
effect(log_ka, Intercept) ~ Normal(log(/(1, 8)), 0.8)
effect(log_ka, weight) ~ Normal(0, 0.1)
y ~ Normal(log_ka, 1.0)SBBRMI with data keys = [:subject, :total_A_log_ka, :total_group_log_ka, :total_location_log_ka, :total_ng_log_ka, :total_nk_log_ka, :total_np_log_ka, :total_precision_log_ka, :weight, :y]
configured submodels:
_brm_total_scales_configured_1 = Base.merge(BayesianRegressionModels._brm_total_scales, quote
tau ~ (ValueFamily(brm_vector_prior_ca4b8a1c1bc116d6))(0.0, 1.0; n = 1)
end)
emitted @slic body:
begin
total_scale_log_ka ~ _brm_total_scales_configured_1(; n = total_nk_log_ka)
total_log_ka::matrix[total_ng_log_ka, total_nk_log_ka] ~ brm_total(total_scale_log_ka, total_A_log_ka, total_location_log_ka, total_precision_log_ka)
population_log_ka = brm_total_recover_rng(total_log_ka, total_scale_log_ka, total_A_log_ka, total_location_log_ka, total_precision_log_ka)
deviation_log_ka = brm_total_deviations(total_log_ka, total_A_log_ka * population_log_ka)
total_Z_log_ka = hcat(rep_vector(1.0, num_elements(total_group_log_ka)))
X_log_ka = hcat(weight)
pop_log_ka ~ _popefs_normal(; X = X_log_ka, beta_loc = [0], beta_scale = [0.1])
log_ka = rows_dot_product(total_log_ka[total_group_log_ka, :], total_Z_log_ka) + pop_log_ka
y ~ normal(log_ka, 1.0)
endfunctions {
// value UDF brm_vector_prior_ca4b8a1c1bc116d6_lpdf
real brm_vector_prior_ca4b8a1c1bc116d6_lpdf(
vector x,
real arg_1,
real arg_2
) {
if((x[1] < 0.0)) {
return negative_infinity();
}
return normal_lpdf(x[1] | arg_1, arg_2);
}
real brm_total_lpdf(
matrix total,
vector tau,
matrix A,
vector location,
vector precision
) {
int j = dims(total)[1];
int k = dims(total)[2];
int p = dims(A)[2];
if (dims(tau)[1] != k) reject("brm_total_lpdf: dim mismatch — `tau` dim 1 (= ", dims(tau)[1], ") does not match `k` (= ", k, "), inferred from `total` dim 2. `k` sizes: `total` dim 2 (= ", dims(total)[2], "), `tau` dim 1 (= ", dims(tau)[1], "), `A` dim 1 (= ", dims(A)[1], ").");
if (dims(A)[1] != k) reject("brm_total_lpdf: dim mismatch — `A` dim 1 (= ", dims(A)[1], ") does not match `k` (= ", k, "), inferred from `total` dim 2. `k` sizes: `total` dim 2 (= ", dims(total)[2], "), `tau` dim 1 (= ", dims(tau)[1], "), `A` dim 1 (= ", dims(A)[1], ").");
if (dims(location)[1] != p) reject("brm_total_lpdf: dim mismatch — `location` dim 1 (= ", dims(location)[1], ") does not match `p` (= ", p, "), inferred from `A` dim 2. `p` sizes: `A` dim 2 (= ", dims(A)[2], "), `location` dim 1 (= ", dims(location)[1], "), `precision` dim 1 (= ", dims(precision)[1], ").");
if (dims(precision)[1] != p) reject("brm_total_lpdf: dim mismatch — `precision` dim 1 (= ", dims(precision)[1], ") does not match `p` (= ", p, "), inferred from `A` dim 2. `p` sizes: `A` dim 2 (= ", dims(A)[2], "), `location` dim 1 (= ", dims(location)[1], "), `precision` dim 1 (= ", dims(precision)[1], ").");
matrix[dims(precision)[1], dims(precision)[1]] Q = brm_total_precision(tau, A, precision, j);
vector[dims(total)[2]] average = brm_total_mean(total);
vector[dims(precision)[1]] beta = brm_total_conditional_mean(total, tau, A, location, precision, Q);
vector[dims(total)[2]] residual = (average - (A * beta));
real quadratic = 0.0;
real lp = ((-0.5 * ((j * k) - p) * 1.8378770664093453) - (j * sum(log(tau))));
for(c in 1:k) {
quadratic += ((j * square(residual[c])) / square(tau[c]));
for(g in 1:j) {
quadratic += (square((total[g, c] - average[c])) / square(tau[c]));
}
}
for(a in 1:p) {
if((precision[a] > 0.0)) {
lp += (0.5 * (log(precision[a]) - 1.8378770664093453));
quadratic += (precision[a] * square((beta[a] - location[a])));
}
}
return (lp - (0.5 * (log_determinant(Q) + quadratic)));
}
matrix brm_total_precision(
vector tau,
matrix A,
vector precision,
int n_groups
) {
int k = dims(tau)[1];
int p = dims(A)[2];
if (dims(A)[1] != k) reject("brm_total_precision: dim mismatch — `A` dim 1 (= ", dims(A)[1], ") does not match `k` (= ", k, "), inferred from `tau` dim 1. `k` sizes: `tau` dim 1 (= ", dims(tau)[1], "), `A` dim 1 (= ", dims(A)[1], ").");
if (dims(precision)[1] != p) reject("brm_total_precision: dim mismatch — `precision` dim 1 (= ", dims(precision)[1], ") does not match `p` (= ", p, "), inferred from `A` dim 2. `p` sizes: `A` dim 2 (= ", dims(A)[2], "), `precision` dim 1 (= ", dims(precision)[1], ").");
matrix[dims(precision)[1], dims(precision)[1]] out = diag_matrix(precision);
for(a in 1:p) {
for(b in 1:p) {
for(c in 1:k) {
out[a, b] += ((n_groups * A[c, a] * A[c, b]) / square(tau[c]));
}
}
}
return out;
}
vector brm_total_mean(
matrix total
) {
int j = dims(total)[1];
int k = dims(total)[2];
vector[k] out = rep_vector(0.0, k);
for(c in 1:k) {
out[c] = (sum(total[:, c]) / j);
}
return out;
}
vector brm_total_conditional_mean(
matrix total,
vector tau,
matrix A,
vector location,
vector precision,
matrix Q
) {
int j = dims(total)[1];
int k = dims(total)[2];
int p = dims(A)[2];
if (dims(tau)[1] != k) reject("brm_total_conditional_mean: dim mismatch — `tau` dim 1 (= ", dims(tau)[1], ") does not match `k` (= ", k, "), inferred from `total` dim 2. `k` sizes: `total` dim 2 (= ", dims(total)[2], "), `tau` dim 1 (= ", dims(tau)[1], "), `A` dim 1 (= ", dims(A)[1], ").");
if (dims(A)[1] != k) reject("brm_total_conditional_mean: dim mismatch — `A` dim 1 (= ", dims(A)[1], ") does not match `k` (= ", k, "), inferred from `total` dim 2. `k` sizes: `total` dim 2 (= ", dims(total)[2], "), `tau` dim 1 (= ", dims(tau)[1], "), `A` dim 1 (= ", dims(A)[1], ").");
if (dims(location)[1] != p) reject("brm_total_conditional_mean: dim mismatch — `location` dim 1 (= ", dims(location)[1], ") does not match `p` (= ", p, "), inferred from `A` dim 2. `p` sizes: `A` dim 2 (= ", dims(A)[2], "), `location` dim 1 (= ", dims(location)[1], "), `precision` dim 1 (= ", dims(precision)[1], "), `Q` dim 1 (= ", dims(Q)[1], "), `Q` dim 2 (= ", dims(Q)[2], ").");
if (dims(precision)[1] != p) reject("brm_total_conditional_mean: dim mismatch — `precision` dim 1 (= ", dims(precision)[1], ") does not match `p` (= ", p, "), inferred from `A` dim 2. `p` sizes: `A` dim 2 (= ", dims(A)[2], "), `location` dim 1 (= ", dims(location)[1], "), `precision` dim 1 (= ", dims(precision)[1], "), `Q` dim 1 (= ", dims(Q)[1], "), `Q` dim 2 (= ", dims(Q)[2], ").");
if (dims(Q)[1] != p) reject("brm_total_conditional_mean: dim mismatch — `Q` dim 1 (= ", dims(Q)[1], ") does not match `p` (= ", p, "), inferred from `A` dim 2. `p` sizes: `A` dim 2 (= ", dims(A)[2], "), `location` dim 1 (= ", dims(location)[1], "), `precision` dim 1 (= ", dims(precision)[1], "), `Q` dim 1 (= ", dims(Q)[1], "), `Q` dim 2 (= ", dims(Q)[2], ").");
if (dims(Q)[2] != p) reject("brm_total_conditional_mean: dim mismatch — `Q` dim 2 (= ", dims(Q)[2], ") does not match `p` (= ", p, "), inferred from `A` dim 2. `p` sizes: `A` dim 2 (= ", dims(A)[2], "), `location` dim 1 (= ", dims(location)[1], "), `precision` dim 1 (= ", dims(precision)[1], "), `Q` dim 1 (= ", dims(Q)[1], "), `Q` dim 2 (= ", dims(Q)[2], ").");
vector[dims(total)[2]] average = brm_total_mean(total);
vector[dims(location)[1]] natural = (precision .* location);
for(a in 1:p) {
for(c in 1:k) {
natural[a] += ((j * A[c, a] * average[c]) / square(tau[c]));
}
}
return mdivide_left_spd(Q, natural);
}
vector brm_total_recover_rng(
matrix total,
vector tau,
matrix A,
vector location,
vector precision
) {
int j = dims(total)[1];
int k = dims(total)[2];
int p = dims(A)[2];
if (dims(tau)[1] != k) reject("brm_total_recover_rng: dim mismatch — `tau` dim 1 (= ", dims(tau)[1], ") does not match `k` (= ", k, "), inferred from `total` dim 2. `k` sizes: `total` dim 2 (= ", dims(total)[2], "), `tau` dim 1 (= ", dims(tau)[1], "), `A` dim 1 (= ", dims(A)[1], ").");
if (dims(A)[1] != k) reject("brm_total_recover_rng: dim mismatch — `A` dim 1 (= ", dims(A)[1], ") does not match `k` (= ", k, "), inferred from `total` dim 2. `k` sizes: `total` dim 2 (= ", dims(total)[2], "), `tau` dim 1 (= ", dims(tau)[1], "), `A` dim 1 (= ", dims(A)[1], ").");
if (dims(location)[1] != p) reject("brm_total_recover_rng: dim mismatch — `location` dim 1 (= ", dims(location)[1], ") does not match `p` (= ", p, "), inferred from `A` dim 2. `p` sizes: `A` dim 2 (= ", dims(A)[2], "), `location` dim 1 (= ", dims(location)[1], "), `precision` dim 1 (= ", dims(precision)[1], ").");
if (dims(precision)[1] != p) reject("brm_total_recover_rng: dim mismatch — `precision` dim 1 (= ", dims(precision)[1], ") does not match `p` (= ", p, "), inferred from `A` dim 2. `p` sizes: `A` dim 2 (= ", dims(A)[2], "), `location` dim 1 (= ", dims(location)[1], "), `precision` dim 1 (= ", dims(precision)[1], ").");
matrix[dims(precision)[1], dims(precision)[1]] Q = brm_total_precision(tau, A, precision, j);
vector[dims(precision)[1]] beta = brm_total_conditional_mean(total, tau, A, location, precision, Q);
return multi_normal_rng(beta, inverse_spd(Q));
}
matrix brm_total_deviations(
matrix total,
vector mu
) {
int j = dims(total)[1];
int k = dims(total)[2];
if (dims(mu)[1] != k) reject("brm_total_deviations: dim mismatch — `mu` dim 1 (= ", dims(mu)[1], ") does not match `k` (= ", k, "), inferred from `total` dim 2. `k` sizes: `total` dim 2 (= ", dims(total)[2], "), `mu` dim 1 (= ", dims(mu)[1], ").");
matrix[dims(total)[1], dims(total)[2]] out = total;
for(c in 1:k) {
out[:, c] = (total[:, c] - rep_vector(mu[c], j));
}
return out;
}
matrix hcat(vector x) {
int n = dims(x)[1];
return to_matrix(x, n, 1);
}
vector normal_lpdfs(
vector obs,
vector loc,
real scale
) {
return jbroadcasted_normal_lpdfs(obs, loc, scale);
}
vector jbroadcasted_normal_lpdfs(
vector x1,
vector x2,
real x3
) {
int n = dims(x1)[1];
vector[n] rv;
for(i in 1:n) {
rv[i] = normal_lpdfs(broadcasted_getindex(x1, i), broadcasted_getindex(x2, i), x3);
}
return rv;
}
real normal_lpdfs(
real args1,
real args2,
real args3
) {
return normal_lpdf(args1 | args2, args3);
}
real broadcasted_getindex(vector x, int i) {
return x[i];
}
vector normal_vector_rng(
int anontok__1,
vector a,
real b
) {
int n = anontok__1;
if((n == 0)) {
vector[n] rv;
return rv;
} else {
return to_vector(normal_rng(a, b));
}
}
}
data {
int total_ng_log_ka;
int total_nk_log_ka;
int total_A_log_ka_m;
int total_A_log_ka_n;
matrix[total_A_log_ka_m, total_A_log_ka_n] total_A_log_ka;
int total_location_log_ka_n;
vector[total_location_log_ka_n] total_location_log_ka;
int total_precision_log_ka_n;
vector[total_precision_log_ka_n] total_precision_log_ka;
int total_group_log_ka_n;
array[total_group_log_ka_n] int total_group_log_ka;
int weight_n;
vector[weight_n] weight;
int y_n;
vector[y_n] y;
}
transformed data {
matrix[num_elements(total_group_log_ka), 1] total_Z_log_ka = hcat(rep_vector(1.0, num_elements(total_group_log_ka)));
matrix[weight_n, 1] X_log_ka = hcat(weight);
int pop_log_ka_n_covariates = 1;
}
parameters {
vector<lower=0.0>[1] total_scale_log_ka_tau;
matrix[total_ng_log_ka, total_nk_log_ka] total_log_ka;
vector[pop_log_ka_n_covariates] pop_log_ka_beta_pop;
}
transformed parameters {
vector<lower=0.0>[1] total_scale_log_ka = total_scale_log_ka_tau;
vector[weight_n] pop_log_ka = (X_log_ka * pop_log_ka_beta_pop);
vector[num_elements(total_group_log_ka)] log_ka = (rows_dot_product(total_log_ka[total_group_log_ka, :], total_Z_log_ka) + pop_log_ka);
}
model {
total_scale_log_ka_tau ~ brm_vector_prior_ca4b8a1c1bc116d6(0.0, 1.0);
total_log_ka ~ brm_total(total_scale_log_ka, total_A_log_ka, total_location_log_ka, total_precision_log_ka);
pop_log_ka_beta_pop ~ normal([0]', [0.1]');
y ~ normal(log_ka, 1.0);
}
generated quantities {
vector[total_precision_log_ka_n] population_log_ka = brm_total_recover_rng(
total_log_ka,
total_scale_log_ka,
total_A_log_ka,
total_location_log_ka,
total_precision_log_ka
);
matrix[total_ng_log_ka, total_A_log_ka_m] deviation_log_ka = brm_total_deviations(total_log_ka, (total_A_log_ka * population_log_ka));
vector[y_n] y_likelihood = normal_lpdfs(y, log_ka, 1.0);
vector[y_n] y_gen = normal_vector_rng(y_n, log_ka, 1.0);
}#= line 0 =# Turing.@model(function brm_model(y, X_log_ka, group_effects_log_ka_1)
beta_pop ~ Distributions.product_distribution([Distributions.Normal(Base.log(1 / 8), 0.8), Distributions.Normal(0, 0.1)])
eta_log_ka = X_log_ka * beta_pop
group_effect_1 = Base.zeros(Base.length(y))
group_1_1 ~ DynamicPPL.to_submodel(BRM.turing_group_effect(group_effects_log_ka_1, (nothing,), nothing))
group_effect_1 = group_effect_1 + group_1_1.effect
eta_log_ka = eta_log_ka + group_effect_1
log_ka = eta_log_ka
begin
for i = Base.eachindex(y)
y[i] ~ Distributions.Normal(log_ka[i], 1.0)
end
end
(; log_ka = log_ka, response = y)
end)Both slots are mandatory, and : is the wildcard for either. effect(:, weight) ~ Normal(0, 0.1) is the default layer for :weight — it reaches that column in every predictor owning it, and a more specific address such as effect(log_ka, weight) overrides it. Two addresses of equal specificity reaching one parameter, and unknown addresses, error. The first shipped lowering supports Normal(location, scale) overrides and retains the existing pop_<predictor>_beta_pop vector parameter, its popcoefnames labels, and descriptor provenance. Inspect the captured formula statements with effect_priors(brmi). This surface belongs to SBBRMI; VBRMI does not implement it.
A categorical predictor — a bare integer/CategoricalArray column, or one wrapped in factor(...) — is not a beta_pop column, so popcoefnames deliberately never lists it: it owns a separate cat_<predictor>_<column>_beta vector holding its K−1 treatment contrasts, with the reference level pinned at 0. Those contrasts also default to std_normal(), and the same effect(...) address changes them — keyed by the column name, not the emitted predictor-qualified cat_<predictor>_<column> parameter name:
Categorical effect priorcategorical_prior = (@brm begin
sigma ~ Exponential(1)
mu ~ 1 + factor(g) + x
effect(mu, g) ~ Normal(0.0, 0.5) # ⇒ cat_mu_g_beta ~ normal(0.0, 0.5);
y ~ Normal(mu, sigma)
end)((;
g=[1, 2, 3, 1, 2, 3], x=[-1.0, -0.5, 0.0, 0.5, 1.0, 1.5],
y=[-2.4, -2.2, -2.0, -1.8, -1.7, -1.5],
))BRMI:
sigma ~ Exponential(1)
g: data (eltype=Int64, n=6)
x: data (eltype=Float64, n=6)
mu ~ 1 + factor(g) + x
effect(mu, g) ~ Normal(0.0, 0.5)
y ~ Normal(mu, sigma)SBBRMI with data keys = [:g, :g_idx, :g_n_levels, :x, :y]
emitted @slic body:
begin
sigma ~ exponential(1.0 ./ 1)
X_mu = hcat(rep_vector(1.0, num_elements(x)), x)
pop_mu ~ popefs(; X = X_mu)
cat_mu_g ~ _sb_cat_normal(; x = g_idx, n_levels = g_n_levels, beta_loc = 0.0, beta_scale = 0.5)
mu = pop_mu + cat_mu_g
y ~ normal(mu, sigma)
endfunctions {
matrix hcat(
vector x,
vector y
) {
int n = dims(x)[1];
if (dims(y)[1] != n) reject("hcat: dim mismatch — `y` dim 1 (= ", dims(y)[1], ") does not match `n` (= ", n, "), inferred from `x` dim 1. `n` sizes: `x` dim 1 (= ", dims(x)[1], "), `y` dim 1 (= ", dims(y)[1], ").");
return append_col(x, y);
}
vector normal_lpdfs(
vector obs,
vector loc,
real scale
) {
return jbroadcasted_normal_lpdfs(obs, loc, scale);
}
vector jbroadcasted_normal_lpdfs(
vector x1,
vector x2,
real x3
) {
int n = dims(x1)[1];
vector[n] rv;
for(i in 1:n) {
rv[i] = normal_lpdfs(broadcasted_getindex(x1, i), broadcasted_getindex(x2, i), x3);
}
return rv;
}
real normal_lpdfs(
real args1,
real args2,
real args3
) {
return normal_lpdf(args1 | args2, args3);
}
real broadcasted_getindex(vector x, int i) {
return x[i];
}
vector normal_vector_rng(
int anontok__1,
vector a,
real b
) {
int n = anontok__1;
if((n == 0)) {
vector[n] rv;
return rv;
} else {
return to_vector(normal_rng(a, b));
}
}
}
data {
int x_n;
vector[x_n] x;
int g_n_levels;
int g_idx_n;
array[g_idx_n] int g_idx;
int y_n;
vector[y_n] y;
}
transformed data {
matrix[x_n, 2] X_mu = hcat(rep_vector(1.0, num_elements(x)), x);
int pop_mu_n_covariates = 2;
}
parameters {
real<lower=0.0> sigma;
vector[pop_mu_n_covariates] pop_mu_beta_pop;
vector[(g_n_levels - 1)] cat_mu_g_beta;
}
transformed parameters {
vector[x_n] pop_mu = (X_mu * pop_mu_beta_pop);
vector[g_idx_n] cat_mu_g = append_row(0.0, cat_mu_g_beta)[g_idx];
vector[x_n] mu = (pop_mu + cat_mu_g);
}
model {
sigma ~ exponential((1.0 ./ 1));
pop_mu_beta_pop ~ std_normal();
cat_mu_g_beta ~ normal(0.0, 0.5);
y ~ normal(mu, sigma);
}
generated quantities {
vector[y_n] y_likelihood = normal_lpdfs(y, mu, sigma);
vector[y_n] y_gen = normal_vector_rng(y_n, mu, sigma);
}#= line 0 =# Turing.@model(function brm_model(y, X_mu)
sigma ~ Distributions.Exponential(1)
beta_pop ~ Distributions.product_distribution([Distributions.Normal(0, 1), Distributions.Normal(0.0, 0.5), Distributions.Normal(0.0, 0.5), Distributions.Normal(0, 1)])
eta_mu = X_mu * beta_pop
mu = eta_mu
begin
for i = Base.eachindex(y)
y[i] ~ Distributions.Normal(mu[i], sigma)
end
end
(; mu = mu, sigma = sigma, response = y)
end)One shared (location, scale) covers every contrast in the block; a treatment contrast has no per-level address (a cell mean does — see below). The :-predictor form effect(:, g) reaches the corresponding block in every predictor owning it, and the statement composes with population overrides on the same predictor (effect(mu, x) ~ Normal(0, 0.25)) — each addresses its own parameter. A non-default reference level emits cat_<predictor>_<column>__ref_<k>_beta, which the plain column name still addresses whenever that is unambiguous; when two factor(g; ref=…) blocks of one column would both claim it, the bare address is refused and each block is addressed by its reference-qualified column name. Models with no such statement keep the same std_normal() contrast prior under the predictor-qualified name.
Treatment contrasts measure each level against a reference, so they need an intercept to measure from. A predictor without one codes its first categorical term by cell means instead — one coefficient per level, no reference level, exactly what 0 + factor(g) means in brms and in R's model.matrix. Each cell mean is addressable on its own, as <column>_lvl_<k> with k the level's position in the fitted level order, so a per-group location can carry a per-group prior:
Cell means with a per-level priorcell_means = (@brm begin
sigma ~ Exponential(1)
mu ~ 0 + factor(site)
effect(mu, site) ~ Normal(0.0, 2.0) # every site
effect(mu, site_lvl_3) ~ Normal(4.0, 0.5) # ... except the third
y ~ Normal(mu, sigma)
end)((;
site=[1, 2, 3, 1, 2, 3],
y=[-0.4, 0.2, 4.1, -0.1, 0.5, 3.8],
))BRMI:
sigma ~ Exponential(1)
site: data (eltype=Int64, n=6)
mu ~ 0 + factor(site)
effect(mu, site) ~ Normal(0.0, 2.0)
effect(mu, site_lvl_3) ~ Normal(4.0, 0.5)
y ~ Normal(mu, sigma)SBBRMI with data keys = [:site, :site_idx, :site_n_levels, :y]
emitted @slic body:
begin
sigma ~ exponential(1.0 ./ 1)
cat_mu_site ~ _sb_cat_cells_normal(; x = site_idx, n_levels = site_n_levels, beta_loc = [0.0, 0.0, 4.0], beta_scale = [2.0, 2.0, 0.5])
mu = cat_mu_site
y ~ normal(mu, sigma)
endfunctions {
vector normal_lpdfs(
vector obs,
vector loc,
real scale
) {
return jbroadcasted_normal_lpdfs(obs, loc, scale);
}
vector jbroadcasted_normal_lpdfs(
vector x1,
vector x2,
real x3
) {
int n = dims(x1)[1];
vector[n] rv;
for(i in 1:n) {
rv[i] = normal_lpdfs(broadcasted_getindex(x1, i), broadcasted_getindex(x2, i), x3);
}
return rv;
}
real normal_lpdfs(
real args1,
real args2,
real args3
) {
return normal_lpdf(args1 | args2, args3);
}
real broadcasted_getindex(vector x, int i) {
return x[i];
}
vector normal_vector_rng(
int anontok__1,
vector a,
real b
) {
int n = anontok__1;
if((n == 0)) {
vector[n] rv;
return rv;
} else {
return to_vector(normal_rng(a, b));
}
}
}
data {
int site_n_levels;
int site_idx_n;
array[site_idx_n] int site_idx;
int y_n;
vector[y_n] y;
}
transformed data {
}
parameters {
real<lower=0.0> sigma;
vector[site_n_levels] cat_mu_site_beta;
}
transformed parameters {
vector[site_idx_n] cat_mu_site = cat_mu_site_beta[site_idx];
vector[site_idx_n] mu = cat_mu_site;
}
model {
sigma ~ exponential((1.0 ./ 1));
cat_mu_site_beta ~ normal([0.0, 0.0, 4.0]', [2.0, 2.0, 0.5]');
y ~ normal(mu, sigma);
}
generated quantities {
vector[y_n] y_likelihood = normal_lpdfs(y, mu, sigma);
vector[y_n] y_gen = normal_vector_rng(y_n, mu, sigma);
}#= line 0 =# Turing.@model(function brm_model(y, X_mu)
sigma ~ Distributions.Exponential(1)
beta_pop ~ Distributions.product_distribution([Distributions.Normal(0.0, 2.0), Distributions.Normal(0.0, 2.0), Distributions.Normal(4.0, 0.5)])
eta_mu = X_mu * beta_pop
mu = eta_mu
begin
for i = Base.eachindex(y)
y[i] ~ Distributions.Normal(mu[i], sigma)
end
end
(; mu = mu, sigma = sigma, response = y)
end)The emitted carrier is the same cat_<predictor>_<column>_beta vector, of length K rather than K−1 and with no pinned zero. The column address sets one prior over all K cell means; a level address is more specific and overrides it for that level, under the same most-specific-wins rule as every other effect(...) statement.
The rule, in full:
BRM has no implicit intercept and 0 is only a marker, so "without an intercept" means the predictor has no 1 term: mu ~ site, mu ~ 0 + site and mu ~ 0 + factor(site) are the same formula.
Only the first categorical term is cell-mean coded. A second one's full indicator set would be collinear with the first's, so later categorical terms — and every & interaction — stay treatment-coded.
factor(site; cmc=false) — brms' switch, "cell-mean coding" — keeps K−1 treatment contrasts in a predictor without an intercept, and the cell means pass to the next categorical term. A ref= alone does not opt out: as in R, a releveled factor without an intercept is still cell-mean coded, in its releveled order.
An ordinal model's estimated thresholds are its location predictor's intercept, so the eta of Ordinal(...) / OrderedLogistic(...) keeps treatment contrasts even though it is written eta ~ 0 + ....
A random intercept (1 | g) is not a population intercept.
The same rule holds inside a random-effect block, decided on that block's own left-hand side: (0 + c | g) gives every level of c its own group-level effect (margins c_dummy_1 … c_dummy_K, addressable by sd(lp, ID, c_dummy_k) on a shared |ID| block), (1 + c | g) keeps the random intercept plus K−1 dummies, and (0 + factor(c; cmc=false) | g) opts out. BRM merges (1 | g) + (0 + c | g) into one block, so the sibling intercept keeps c treatment-coded there; (0 + c || g) is decided on its original left-hand side before it is split into uncorrelated terms.
brm_population_effect_coordinates reports which coding a block has (coding === :cellmeans or :treatment); a cell-mean block has no reference_level, and cells pairs each level with its posterior coordinate. An r2d2(...) decomposition allocates its shares over treatment contrasts and refuses a cell-mean block.
Some terms own parameters no coefficient address can reach — s(x)'s smoothing scale, mo(c)'s Dirichlet increments, me(x, sd)'s latent true covariate, a Gaussian process's length scale and amplitude. They are addressed by naming the term itself in the target slot, under the same head-position grammar:
Term-internal priorsterm_priors_example = (@brm begin
y ~ Normal(mu, 1.)
mu ~ 1 + s(age) + mo(dose) + me(w_obs, 0.3) + hsgp(conc; k=5)
sd(:, s(age)) ~ Exponential(2) # smoothing SD
simplex(mu, mo(dose)) ~ Dirichlet(2) # monotonic increments
latent(:, me(w_obs)) ~ Normal(0, 5) # latent true covariate
length_scale(:, hsgp(conc)) ~ Uniform(0.84, 2) # GP length scale
sd(:, hsgp(conc)) ~ Normal(0, 0.5) # GP marginal amplitude
end)((;
age=collect(20.0:29.0), dose=repeat(1:5; inner=2),
w_obs=collect(1.0:10.0), conc=collect(range(-2, 2; length=10)),
y=collect(range(-1, 1; length=10)),
))BRMI:
mu ~ 1 + s(age) + mo(dose) + me(w_obs, 0.3) + hsgp(conc; k=5)
y ~ Normal(mu, 1.0)
age: data (eltype=Float64, n=10)
dose: data (eltype=Int64, n=10)
w_obs: data (eltype=Float64, n=10)
conc: data (eltype=Float64, n=10)
effect(term_sd, s(age), :) ~ Exponential(2)
effect(term_simplex, mo(dose), mu) ~ Dirichlet(2)
effect(term_latent, me(w_obs), :) ~ Normal(0, 5)
effect(term_length_scale, hsgp(conc), :) ~ Uniform(0.84, 2)
effect(term_sd, hsgp(conc), :) ~ Normal(0, 0.5)SBBRMI with data keys = [:PHI_hsgp_conc, :Xnull_age, :Zpen_age, :age, :conc, :dose, :dose_idx, :omega2_hsgp_conc, :rho_lower_hsgp_conc, :sd_w_obs, :w_obs, :y]
configured submodels:
_sb_s_generic_configured_1 = Base.merge(BayesianRegressionModels._sb_s_generic, quote
sd_pen ~ (ValueFamily(brm_vector_prior_655a23ff68a31dcc))(0.5; n = 1)
end)
_sb_hsgp_configured_1 = Base.merge(BayesianRegressionModels._sb_hsgp, quote
rho_iso ~ uniform(0.84, 2; lower = 0.84, upper = 2.0)
sigma ~ normal(0.0, 0.5; lower = 0.0)
end)
emitted @slic body:
begin
mo_dose ~ _sb_mo(; x = dose_idx, alpha = rep_vector(2.0, 4))
me_w_obs ~ _sb_me(; x_obs = w_obs, sd_x = sd_w_obs, x_true_loc = 0, x_true_scale = 5)
X_mu = hcat(rep_vector(1.0, num_elements(dose)), mo_dose, me_w_obs)
pop_mu ~ popefs(; X = X_mu)
s_age ~ _sb_s_generic_configured_1(; Xnull = Xnull_age, Zpen = Zpen_age)
hsgp_conc ~ _sb_hsgp_configured_1(; PHI = PHI_hsgp_conc, omega2 = omega2_hsgp_conc, rho_lower = rho_lower_hsgp_conc)
mu = pop_mu + s_age + hsgp_conc
y ~ normal(mu, 1.0)
endfunctions {
vector normal_lpdfs(
vector obs,
vector loc,
real scale
) {
return jbroadcasted_normal_lpdfs(obs, loc, scale);
}
vector jbroadcasted_normal_lpdfs(
vector x1,
vector x2,
real x3
) {
int n = dims(x1)[1];
vector[n] rv;
for(i in 1:n) {
rv[i] = normal_lpdfs(broadcasted_getindex(x1, i), broadcasted_getindex(x2, i), x3);
}
return rv;
}
real normal_lpdfs(
real args1,
real args2,
real args3
) {
return normal_lpdf(args1 | args2, args3);
}
real broadcasted_getindex(vector x, int i) {
return x[i];
}
vector normal_vector_rng(
int anontok__1,
vector a,
real b
) {
int n = anontok__1;
if((n == 0)) {
vector[n] rv;
return rv;
} else {
return to_vector(normal_rng(a, b));
}
}
matrix hcat(vector x, vector y, vector z) {
return hcat(hcat(x, y), z);
}
matrix hcat(
matrix x,
vector y
) {
int m = dims(x)[1];
int n = dims(x)[2];
if (dims(y)[1] != m) reject("hcat: dim mismatch — `y` dim 1 (= ", dims(y)[1], ") does not match `m` (= ", m, "), inferred from `x` dim 1. `m` sizes: `x` dim 1 (= ", dims(x)[1], "), `y` dim 1 (= ", dims(y)[1], ").");
return append_col(x, y);
}
matrix hcat(
vector x,
vector y
) {
int n = dims(x)[1];
if (dims(y)[1] != n) reject("hcat: dim mismatch — `y` dim 1 (= ", dims(y)[1], ") does not match `n` (= ", n, "), inferred from `x` dim 1. `n` sizes: `x` dim 1 (= ", dims(x)[1], "), `y` dim 1 (= ", dims(y)[1], ").");
return append_col(x, y);
}
// value UDF brm_vector_prior_655a23ff68a31dcc_lpdf
real brm_vector_prior_655a23ff68a31dcc_lpdf(
vector x,
real arg_1
) {
if((x[1] < 0.0)) {
return negative_infinity();
}
return exponential_lpdf(x[1] | arg_1);
}
vector brm_hsgp_sqrt_spd(
matrix omega2,
real sigma,
vector rho
) {
int m = dims(omega2)[1];
int d = dims(omega2)[2];
if (dims(rho)[1] != d) reject("brm_hsgp_sqrt_spd: dim mismatch — `rho` dim 1 (= ", dims(rho)[1], ") does not match `d` (= ", d, "), inferred from `omega2` dim 2. `d` sizes: `omega2` dim 2 (= ", dims(omega2)[2], "), `rho` dim 1 (= ", dims(rho)[1], ").");
vector[m] rv;
real scale = sigma;
for(axis in 1:d) {
scale *= sqrt((rho[axis] * 2.5066282746310002));
}
for(b in 1:m) {
real exponent = 0.0;
for(axis in 1:d) {
exponent += (rho[axis] * rho[axis] * omega2[b, axis]);
}
rv[b] = (scale * exp((-0.25 * exponent)));
}
return rv;
}
}
data {
int dose_idx_n;
array[dose_idx_n] int dose_idx;
int w_obs_n;
vector[w_obs_n] w_obs;
real sd_w_obs;
int dose_n;
array[dose_n] int dose;
int Zpen_age_n;
int Xnull_age_m;
int Xnull_age_n;
matrix[Xnull_age_m, Xnull_age_n] Xnull_age;
int Zpen_age_m;
matrix[Zpen_age_m, Zpen_age_n] Zpen_age;
int omega2_hsgp_conc_m;
int omega2_hsgp_conc_n;
matrix[omega2_hsgp_conc_m, omega2_hsgp_conc_n] omega2_hsgp_conc;
int PHI_hsgp_conc_m;
int PHI_hsgp_conc_n;
matrix[PHI_hsgp_conc_m, PHI_hsgp_conc_n] PHI_hsgp_conc;
int y_n;
vector[y_n] y;
}
transformed data {
int pop_mu_n_covariates = (2 + 1);
int s_age_n_pen = Zpen_age_n;
int hsgp_conc_n_basis = omega2_hsgp_conc_m;
int hsgp_conc_n_axes = omega2_hsgp_conc_n;
}
parameters {
simplex[4] mo_dose_simplex_incr;
vector[num_elements(w_obs)] me_w_obs_x_true;
vector[pop_mu_n_covariates] pop_mu_beta_pop;
vector[2] s_age_b_fixed;
vector<lower=0.0>[1] s_age_sd_pen;
vector[s_age_n_pen] s_age_b_pen_raw;
real<lower=0.84, upper=2.0> hsgp_conc_rho_iso;
real<lower=0.0> hsgp_conc_sigma;
vector[hsgp_conc_n_basis] hsgp_conc_beta_raw;
}
transformed parameters {
vector[dose_idx_n] mo_dose = cumulative_sum(append_row(0.0, mo_dose_simplex_incr))[dose_idx];
vector[num_elements(w_obs)] me_w_obs = me_w_obs_x_true;
matrix[num_elements(w_obs), (2 + 1)] X_mu = hcat(rep_vector(1.0, num_elements(dose)), mo_dose, me_w_obs);
vector[num_elements(w_obs)] pop_mu = (X_mu * pop_mu_beta_pop);
vector[s_age_n_pen] s_age_b_pen = (s_age_sd_pen[1] * s_age_b_pen_raw);
vector[Xnull_age_m] s_age = ((Xnull_age * s_age_b_fixed) + (Zpen_age * s_age_b_pen));
vector[hsgp_conc_n_axes] hsgp_conc_rho = rep_vector(hsgp_conc_rho_iso, hsgp_conc_n_axes);
vector[omega2_hsgp_conc_m] hsgp_conc_sqrt_spd = brm_hsgp_sqrt_spd(omega2_hsgp_conc, hsgp_conc_sigma, hsgp_conc_rho);
vector[PHI_hsgp_conc_m] hsgp_conc = (PHI_hsgp_conc * (hsgp_conc_sqrt_spd .* hsgp_conc_beta_raw));
vector[num_elements(w_obs)] mu = (pop_mu + s_age + hsgp_conc);
}
model {
mo_dose_simplex_incr ~ dirichlet(rep_vector(2.0, 4));
me_w_obs_x_true ~ normal(0, 5);
w_obs ~ normal(me_w_obs_x_true, sd_w_obs);
pop_mu_beta_pop ~ std_normal();
s_age_sd_pen ~ brm_vector_prior_655a23ff68a31dcc(0.5);
s_age_b_pen_raw ~ std_normal();
hsgp_conc_rho_iso ~ uniform(0.84, 2);
hsgp_conc_sigma ~ normal(0.0, 0.5);
hsgp_conc_beta_raw ~ std_normal();
y ~ normal(mu, 1.0);
}
generated quantities {
vector[w_obs_n] w_obs_likelihood = normal_lpdfs(w_obs, me_w_obs_x_true, sd_w_obs);
vector[w_obs_n] w_obs_gen = normal_vector_rng(w_obs_n, me_w_obs_x_true, sd_w_obs);
vector[y_n] y_likelihood = normal_lpdfs(y, mu, 1.0);
vector[y_n] y_gen = normal_vector_rng(y_n, mu, 1.0);
}#= line 0 =# Turing.@model(function brm_model(y, X_mu, terms_mu_1, age, terms_mu_2, dose, terms_mu_3, w_obs, terms_mu_4, conc)
beta_pop ~ Distributions.product_distribution(Base.fill(Distributions.Normal(), 1))
eta_mu = X_mu * beta_pop
term_mu_1 ~ DynamicPPL.to_submodel(BRM.turing_term_model(terms_mu_1, Base.length(y), NamedTuple{$(QuoteNode((:sd,)))}((Distributions.Exponential(2),)), NamedTuple{$(QuoteNode((:age,)))}((age,))))
eta_mu = eta_mu + term_mu_1.effect
term_mu_2 ~ DynamicPPL.to_submodel(BRM.turing_term_model(terms_mu_2, Base.length(y), NamedTuple{$(QuoteNode((:simplex,)))}((Distributions.Dirichlet(Base.vect(2.0, 2.0, 2.0, 2.0)),)), NamedTuple{$(QuoteNode((:dose,)))}((dose,))))
eta_mu = eta_mu + term_mu_2.effect
term_mu_3 ~ DynamicPPL.to_submodel(BRM.turing_term_model(terms_mu_3, Base.length(y), NamedTuple{$(QuoteNode((:latent,)))}((Distributions.Normal(0, 5),)), NamedTuple{$(QuoteNode((:w_obs,)))}((w_obs,))))
eta_mu = eta_mu + term_mu_3.effect
term_mu_4 ~ DynamicPPL.to_submodel(BayesianRegressionModelsTuringExt._brm_turing_hsgp_ncp_model(terms_mu_4, Base.length(y), NamedTuple{$(QuoteNode((:rho, :sigma)))}((Distributions.Uniform(0.84, 2), Distributions.Normal(0, 0.5))), NamedTuple{$(QuoteNode((:conc,)))}((conc,))))
eta_mu = eta_mu + term_mu_4.effect
mu = eta_mu
begin
for i = Base.eachindex(y)
y[i] ~ Distributions.Normal(mu[i], 1.0)
end
end
(; mu = mu, response = y)
end)The term is spelled the way the formula spells it, minus numeric and keyword arguments — me(w_obs, 0.3) is addressed as me(w_obs). term_priors(brmi) returns the captured statements. See Term-internal priors for the full table, the t2 component slot, why the standardized raw innovations are deliberately not configurable, and the approximation-validity floor an hsgp term puts on its length scale by default.
See Formula terms and Likelihoods for the supported syntax and backend-specific contracts. The Gallery provides live, interactive examples — input formula, the SLIC submodel body, the transpiled Stan source, and the auto-generated posterior-predictive check, all in one card. The API page lists every public binding.