Skip to content

PCR sensitivity through time: ten related models

This example ports the model matrix from pcr-sensitivity-vs-time. The binary observations y measure PCR sensitivity over ordered times t. Five structural assumptions for the time curve are each paired with two link-scale conventions, producing ten complete Stan programs from one shared likelihood.

Two likelihood conventions

The function value logit or log is passed as data to the model builder. Julia gives every function its own type, so @deffun can dispatch on ::typeof(logit) and ::typeof(log):

  • logit interprets theta as a log-odds value and forwards to bernoulli_logit_lpmf and bernoulli_logit_rng;

  • log interprets theta as a log probability and uses bernoulli_lpmf(y, exp(theta)) and bernoulli_rng(exp(theta)).

The log-link parameter must therefore remain non-positive. The heterogeneous and random-walk families achieve that by sampling a positive ordered xi and returning -xi. Under the logit convention, xi is ordered but unbounded. Negation makes the resulting theta sequence descending in both cases.

The five structural families

Family labelModel for the time curve
heteroOne ordered latent value per time point with a common normal(0, 3) density. Apart from the ordering constraint, no temporal smoothing is imposed.
rw1A first-order Gaussian random walk. sigma controls adjacent changes, so the monotone sequence varies smoothly rather than independently.
rw2A second-order Gaussian random walk. Its innovation is measured around 2xi[t-1] - xi[t-2], directly penalizing changes in slope.
regressionA two-parameter line alpha + beta * t with non-positive slope. This replaces one latent value per time point with a global trend.
regression_mixTwo independently parameterized regression submodels plus a beta(2, 2) mixing weight. The preserved source applies link_f to both curves, mixes the results, and replaces the shared custom likelihood with bernoulli(theta); the caveat below explains why this historical definition needs review.

The generated-code labels are the Cartesian product of these rows and the logit/log columns. For example, rw2.log means the second-order random walk with the log-probability likelihood, while regression.logit means the linear curve with the log-odds likelihood.

Reuse in the StanBlocks implementation

base_model owns the observed-data likelihood but leaves theta unimplemented. Each structural @slic fragment returns a candidate theta. Base.merge inserts that fragment into the shared base, and mapping over the named tuples bases and link_fs creates the nested posteriors family without duplicating a model body.

The source also exercises several less common StanBlocks features:

  • mock_data gives the tracer representative element types and shapes: y is an integer array and t a real vector. It is not embedded as the eventual observed dataset.

  • args... defines a variadic pointwise-log-likelihood helper. jbroadcasted applies its scalar method to vectors while forwarding whichever function token and parameter vector follow y.

  • @lpxf marks methods usable by sampling syntax. @lhs additionally states the constrained left-hand-side type: ordered[n] for the logit model and positive_ordered[n] for the log model. Those signatures communicate both the parameter transform and the symbolic length n.

  • Scalar and sized _rng overloads let the same link-specific definition support a single posterior prediction or an n-element generated array.

  • Untyped arguments are deliberately generic. StanBlocks resolves them through its Stan type hierarchy, while the more specific function-token and sized methods keep dispatch unambiguous here.

Historical source caveat

The executable block is preserved for comparison with the original project, including two regression-family lines that should be audited before using these models for inference. It defines upper_alpha(logit) as negative_infinity(), which gives the logit intercept an empty upper range, and regression_mix applies the named link functions directly while combining component curves. The page demonstrates faithful source generation; it does not certify those two historical statistical definitions as a current analysis.

Full Julia source and generated Stan code

The build evaluates the exact displayed Julia source and calls stan_code for every nested member. Choose a family/link label to inspect one complete generated program, or open the side-by-side modal to compare it with the shared Julia definition.

julia
using StanBlocks

import StanBlocks.stan: logit

@deffun begin 
    "Needed for cross validation"
    my_bernoulli_lpmfs(y::int[n], args...) = jbroadcasted(my_bernoulli_lpmfs, y, args...)
    "Needed for cross validation"
    my_bernoulli_lpmfs(y::int, args...) = my_bernoulli_lpmf(y, args...)
    "Needed to compute the joint likelihood"
    @lpxf my_bernoulli_lpmf(y, ::typeof(logit), theta) = bernoulli_logit_lpmf(y, theta)
    "Needed for posterior predictions"
    my_bernoulli_rng(::typeof(logit), theta::real)::int = bernoulli_logit_rng(theta)
    my_bernoulli_rng(int[n], ::typeof(logit), theta::vector[n])::int[n] = bernoulli_logit_rng(theta)
    "Needed to compute the joint likelihood"
    @lpxf my_bernoulli_lpmf(y, ::typeof(log), theta) = bernoulli_lpmf(y, exp(theta))
    "Needed for posterior predictions"
    my_bernoulli_rng(::typeof(log), theta::real)::int = bernoulli_rng(exp(theta))
    my_bernoulli_rng(int[n], ::typeof(log), theta::vector[n])::int[n] = bernoulli_rng(exp(theta))

    "The `Hetero` prior density after constraining - common to both link functions"
    hetero_lpdf(x::vector[n]) = normal_lpdf(x, 0, 3)
    "The `Hetero` prior density after constraining - this function definition gets used to infer the type and shape of the parameter for the logit link"
    @lhs @lpxf hetero_lpdf(x::ordered[n], ::typeof(logit), n) = hetero_lpdf(x)
    "The `Hetero` prior density after constraining - this function definition gets used to infer the type and shape of the parameter for the log link"
    @lhs @lpxf hetero_lpdf(x::positive_ordered[n], ::typeof(log), n) = hetero_lpdf(x)
    "The `RW(1)` prior density after constraining - common to both link functions"
    rw1_lpdf(x::vector[n], sigma) = normal_lpdf(x[1], 0, sigma) + normal_lpdf(x[2:n], x[1:n-1], sigma)
    "The `RW(1)` prior density after constraining - this function definition gets used to infer the type and shape of the parameter for the logit link"
    @lhs @lpxf rw1_lpdf(x::ordered[n], ::typeof(logit), sigma, n) = rw1_lpdf(x, sigma)
    "The `RW(1)` prior density after constraining - this function definition gets used to infer the type and shape of the parameter for the log link"
    @lhs @lpxf rw1_lpdf(x::positive_ordered[n], ::typeof(log), sigma, n) = rw1_lpdf(x, sigma)
    "The `RW(2)` prior density after constraining - common to both link functions"
    rw2_lpdf(x::vector[n], sigma) = rw1_lpdf(x[1:2], sigma) + normal_lpdf(x[3:n], 2x[2:n-1] - x[1:n-2], sigma)
    "The `RW(2)` prior density after constraining - this function definition gets used to infer the type and shape of the parameter for the logit link"
    @lhs @lpxf rw2_lpdf(x::ordered[n], ::typeof(logit), sigma, n) = rw2_lpdf(x, sigma)
    "The `RW(2)` prior density after constraining - this function definition gets used to infer the type and shape of the parameter for the log link"
    @lhs @lpxf rw2_lpdf(x::positive_ordered[n], ::typeof(log), sigma, n) = rw2_lpdf(x, sigma)
    "The upper bound of the alpha parameter (for the logit link function)"
    upper_alpha(::typeof(logit)) = negative_infinity()
    "The upper bound of the alpha parameter (for the log link function)"
    upper_alpha(::typeof(log)) = 0
end
# The main things that StanBlocks.jl has to know is that `y` is an array of `int`s, and `t` is a vector.
mock_data = (;y=[1], t=[1.])

# The base model intentionally leaves theta to the structural fragment merged below.
base_model = @slic mock_data begin 
    n = dims(y)[1]
    "The exact implementation of the likelihood depends on the passed link function `link_f`"
    y ~ my_bernoulli(link_f, theta)
end
# The submodel for the `Hetero` models
centered_hetero = @slic begin 
    "The type of the `xi` parameter depends on the passed link function `link_f`"
    xi ~ hetero(link_f, n)
    "The negation is needed to ensure that `theta` is in descending order"
    return -xi
end
# The submodel for the `RW(1)` models
centered_rw1 = @slic begin 
    sigma ~ std_normal(;lower=0)
    "The type of the `xi` parameter depends on the passed link function `link_f`"
    xi ~ rw1(link_f, sigma, n)
    "The negation is needed to ensure that `theta` is in descending order"
    return -xi
end
# The submodel for the `RW(2)` models
centered_rw2 = @slic begin 
    sigma ~ normal(0, .5; lower=0)
    "The type of the `xi` parameter depends on the passed link function `link_f`"
    xi ~ rw2(link_f, sigma, n)
    "The negation is needed to ensure that `theta` is in descending order"
    return -xi
end
# The submodel for the `regression` models - reused in the `regression_mix` models
regression = @slic begin 
    "The upper bound of alpha `alpha_upper` depends on the link function `link_f`"
    alpha_upper = upper_alpha(link_f)
    # This could be `alpha ~ normal(0, .5; upper=upper_alpha(link_f))`, once https://github.com/nsiccha/StanBlocks.jl/issues/35 is fixed
    alpha ~ normal(0, .5; upper=alpha_upper)
    beta ~ normal(0, .5; upper=0.)
    return alpha + beta * t
end
# The submodel for the `regression_mix` models - reusing the `regression` submodel
regression_mix = @slic begin 
    c1 ~ regression(;link_f, t) 
    c2 ~ regression(;link_f, t)
    lambda ~ beta(2, 2)
    return lambda * link_f(c1) + (1-lambda) * link_f(c2)
end

bases = (;
    hetero=Base.merge(base_model, quote 
        theta ~ centered_hetero(;link_f, n)
    end),
    rw1=Base.merge(base_model, quote 
        theta ~ centered_rw1(;link_f, n)
    end),
    rw2=Base.merge(base_model, quote 
        theta ~ centered_rw2(;link_f, n)
    end),
    regression=Base.merge(base_model, quote 
        theta ~ regression(;link_f, t)
    end),
    regression_mix=Base.merge(base_model, quote 
        theta ~ regression_mix(;link_f, t)
        y ~ bernoulli(theta)
    end)
)
link_fs = (;logit, log)  

# `posteriors` will be a (nested) named tuple - accessing e.g. the `Hetero (log)` model works via `posteriors.hetero.log`
posteriors = map(bases) do base 
    map(link_fs) do link_f 
        base(;link_f)
    end
end

hetero / logit

stan
functions {
// The `Hetero` prior density after constraining - this function definition gets used to infer the type and shape of the parameter for the logit link
real hetero_logit_lpdf(
    vector x,
    int n
) {
    return hetero_lpdf(x);
}
// The `Hetero` prior density after constraining - common to both link functions
real hetero_lpdf(
    vector x
) {
    return normal_lpdf(x | 0, 3);
}
// Needed to compute the joint likelihood
real my_bernoulli_logit_lpmf(
    array[] int y,
    vector theta
) {
    return bernoulli_logit_lpmf(y | theta);
}
// Needed for cross validation
vector my_bernoulli_logit_lpmfs(
    array[] int y,
    vector args1
) {
    return jbroadcasted_my_bernoulli_lpmfs_logit(y, args1);
}
vector jbroadcasted_my_bernoulli_lpmfs_logit(
    array[] int x1,
    vector x3
) {
    int n = dims(x1)[1];
    vector[n] rv;
    for(i in 1:n) {
        rv[i] = my_bernoulli_logit_lpmfs(broadcasted_getindex(x1, i), broadcasted_getindex(x3, i));
    }
    return rv;
}
// Needed for cross validation
real my_bernoulli_logit_lpmfs(
    int y,
    real args1
) {
    return my_bernoulli_logit_lpmf(y | args1);
}
// Needed to compute the joint likelihood
real my_bernoulli_logit_lpmf(
    int y,
    real theta
) {
    return bernoulli_logit_lpmf(y | theta);
}
int broadcasted_getindex(array[] int x, int i) {
    return x[i];
}
real broadcasted_getindex(vector x, int i) {
    return x[i];
}
array[] int my_bernoulli_int_logit_rng(
    int anontok__1,
    vector theta
) {
    int n = anontok__1;
    if (dims(theta)[1] != n) reject("my_bernoulli_rng: dim mismatch — `theta` dim 1 (= ", dims(theta)[1], ") does not match `n` (= ", n, "), inferred from `anontok__1` dim 1. `n` sizes: `anontok__1` dim 1 (= ", anontok__1, "), `theta` dim 1 (= ", dims(theta)[1], ").");
    return bernoulli_logit_rng(theta);
}
}
data {
    int y_n;
    array[y_n] int y;
}
transformed data {
    int n = y_n;
}
parameters {
    ordered[n] theta_xi;
}
transformed parameters {
    // The negation is needed to ensure that `theta` is in descending order
    vector[n] theta = (-theta_xi);
}
model {
    theta_xi ~ hetero_logit(n);
    // The exact implementation of the likelihood depends on the passed link function `link_f`
    y ~ my_bernoulli_logit(theta);
}
generated quantities {
    // The exact implementation of the likelihood depends on the passed link function `link_f`
    vector[y_n] y_likelihood = my_bernoulli_logit_lpmfs(y, theta);
    // The exact implementation of the likelihood depends on the passed link function `link_f`
    array[n] int y_gen = my_bernoulli_int_logit_rng(y_n, theta);
}

hetero / log

stan
functions {
// The `Hetero` prior density after constraining - this function definition gets used to infer the type and shape of the parameter for the log link
real hetero_log_lpdf(
    vector x,
    int n
) {
    return hetero_lpdf(x);
}
// The `Hetero` prior density after constraining - common to both link functions
real hetero_lpdf(
    vector x
) {
    return normal_lpdf(x | 0, 3);
}
// Needed to compute the joint likelihood
real my_bernoulli_log_lpmf(
    array[] int y,
    vector theta
) {
    return bernoulli_lpmf(y | exp(theta));
}
// Needed for cross validation
vector my_bernoulli_log_lpmfs(
    array[] int y,
    vector args1
) {
    return jbroadcasted_my_bernoulli_lpmfs_log(y, args1);
}
vector jbroadcasted_my_bernoulli_lpmfs_log(
    array[] int x1,
    vector x3
) {
    int n = dims(x1)[1];
    vector[n] rv;
    for(i in 1:n) {
        rv[i] = my_bernoulli_log_lpmfs(broadcasted_getindex(x1, i), broadcasted_getindex(x3, i));
    }
    return rv;
}
// Needed for cross validation
real my_bernoulli_log_lpmfs(
    int y,
    real args1
) {
    return my_bernoulli_log_lpmf(y | args1);
}
// Needed to compute the joint likelihood
real my_bernoulli_log_lpmf(
    int y,
    real theta
) {
    return bernoulli_lpmf(y | exp(theta));
}
int broadcasted_getindex(array[] int x, int i) {
    return x[i];
}
real broadcasted_getindex(vector x, int i) {
    return x[i];
}
array[] int my_bernoulli_int_log_rng(
    int anontok__1,
    vector theta
) {
    int n = anontok__1;
    if (dims(theta)[1] != n) reject("my_bernoulli_rng: dim mismatch — `theta` dim 1 (= ", dims(theta)[1], ") does not match `n` (= ", n, "), inferred from `anontok__1` dim 1. `n` sizes: `anontok__1` dim 1 (= ", anontok__1, "), `theta` dim 1 (= ", dims(theta)[1], ").");
    return bernoulli_rng(exp(theta));
}
}
data {
    int y_n;
    array[y_n] int y;
}
transformed data {
    int n = y_n;
}
parameters {
    positive_ordered[n] theta_xi;
}
transformed parameters {
    // The negation is needed to ensure that `theta` is in descending order
    vector[n] theta = (-theta_xi);
}
model {
    theta_xi ~ hetero_log(n);
    // The exact implementation of the likelihood depends on the passed link function `link_f`
    y ~ my_bernoulli_log(theta);
}
generated quantities {
    // The exact implementation of the likelihood depends on the passed link function `link_f`
    vector[y_n] y_likelihood = my_bernoulli_log_lpmfs(y, theta);
    // The exact implementation of the likelihood depends on the passed link function `link_f`
    array[n] int y_gen = my_bernoulli_int_log_rng(y_n, theta);
}

rw1 / logit

stan
functions {
// The `RW(1)` prior density after constraining - this function definition gets used to infer the type and shape of the parameter for the logit link
real rw1_logit_lpdf(
    vector x,
    real sigma,
    int n
) {
    return rw1_lpdf(x | sigma);
}
// The `RW(1)` prior density after constraining - common to both link functions
real rw1_lpdf(
    vector x,
    real sigma
) {
    int n = dims(x)[1];
    return (normal_lpdf(x[1] | 0, sigma) + normal_lpdf(x[2:n] | x[1:(n - 1)], sigma));
}
// Needed to compute the joint likelihood
real my_bernoulli_logit_lpmf(
    array[] int y,
    vector theta
) {
    return bernoulli_logit_lpmf(y | theta);
}
// Needed for cross validation
vector my_bernoulli_logit_lpmfs(
    array[] int y,
    vector args1
) {
    return jbroadcasted_my_bernoulli_lpmfs_logit(y, args1);
}
vector jbroadcasted_my_bernoulli_lpmfs_logit(
    array[] int x1,
    vector x3
) {
    int n = dims(x1)[1];
    vector[n] rv;
    for(i in 1:n) {
        rv[i] = my_bernoulli_logit_lpmfs(broadcasted_getindex(x1, i), broadcasted_getindex(x3, i));
    }
    return rv;
}
// Needed for cross validation
real my_bernoulli_logit_lpmfs(
    int y,
    real args1
) {
    return my_bernoulli_logit_lpmf(y | args1);
}
// Needed to compute the joint likelihood
real my_bernoulli_logit_lpmf(
    int y,
    real theta
) {
    return bernoulli_logit_lpmf(y | theta);
}
int broadcasted_getindex(array[] int x, int i) {
    return x[i];
}
real broadcasted_getindex(vector x, int i) {
    return x[i];
}
array[] int my_bernoulli_int_logit_rng(
    int anontok__1,
    vector theta
) {
    int n = anontok__1;
    if (dims(theta)[1] != n) reject("my_bernoulli_rng: dim mismatch — `theta` dim 1 (= ", dims(theta)[1], ") does not match `n` (= ", n, "), inferred from `anontok__1` dim 1. `n` sizes: `anontok__1` dim 1 (= ", anontok__1, "), `theta` dim 1 (= ", dims(theta)[1], ").");
    return bernoulli_logit_rng(theta);
}
}
data {
    int y_n;
    array[y_n] int y;
}
transformed data {
    int n = y_n;
}
parameters {
    real<lower=0> theta_sigma;
    // The type of the `xi` parameter depends on the passed link function `link_f`
    ordered[n] theta_xi;
}
transformed parameters {
    // The negation is needed to ensure that `theta` is in descending order
    vector[n] theta = (-theta_xi);
}
model {
    theta_sigma ~ std_normal();
    // The type of the `xi` parameter depends on the passed link function `link_f`
    theta_xi ~ rw1_logit(theta_sigma, n);
    // The exact implementation of the likelihood depends on the passed link function `link_f`
    y ~ my_bernoulli_logit(theta);
}
generated quantities {
    // The exact implementation of the likelihood depends on the passed link function `link_f`
    vector[y_n] y_likelihood = my_bernoulli_logit_lpmfs(y, theta);
    // The exact implementation of the likelihood depends on the passed link function `link_f`
    array[n] int y_gen = my_bernoulli_int_logit_rng(y_n, theta);
}

rw1 / log

stan
functions {
// The `RW(1)` prior density after constraining - this function definition gets used to infer the type and shape of the parameter for the log link
real rw1_log_lpdf(
    vector x,
    real sigma,
    int n
) {
    return rw1_lpdf(x | sigma);
}
// The `RW(1)` prior density after constraining - common to both link functions
real rw1_lpdf(
    vector x,
    real sigma
) {
    int n = dims(x)[1];
    return (normal_lpdf(x[1] | 0, sigma) + normal_lpdf(x[2:n] | x[1:(n - 1)], sigma));
}
// Needed to compute the joint likelihood
real my_bernoulli_log_lpmf(
    array[] int y,
    vector theta
) {
    return bernoulli_lpmf(y | exp(theta));
}
// Needed for cross validation
vector my_bernoulli_log_lpmfs(
    array[] int y,
    vector args1
) {
    return jbroadcasted_my_bernoulli_lpmfs_log(y, args1);
}
vector jbroadcasted_my_bernoulli_lpmfs_log(
    array[] int x1,
    vector x3
) {
    int n = dims(x1)[1];
    vector[n] rv;
    for(i in 1:n) {
        rv[i] = my_bernoulli_log_lpmfs(broadcasted_getindex(x1, i), broadcasted_getindex(x3, i));
    }
    return rv;
}
// Needed for cross validation
real my_bernoulli_log_lpmfs(
    int y,
    real args1
) {
    return my_bernoulli_log_lpmf(y | args1);
}
// Needed to compute the joint likelihood
real my_bernoulli_log_lpmf(
    int y,
    real theta
) {
    return bernoulli_lpmf(y | exp(theta));
}
int broadcasted_getindex(array[] int x, int i) {
    return x[i];
}
real broadcasted_getindex(vector x, int i) {
    return x[i];
}
array[] int my_bernoulli_int_log_rng(
    int anontok__1,
    vector theta
) {
    int n = anontok__1;
    if (dims(theta)[1] != n) reject("my_bernoulli_rng: dim mismatch — `theta` dim 1 (= ", dims(theta)[1], ") does not match `n` (= ", n, "), inferred from `anontok__1` dim 1. `n` sizes: `anontok__1` dim 1 (= ", anontok__1, "), `theta` dim 1 (= ", dims(theta)[1], ").");
    return bernoulli_rng(exp(theta));
}
}
data {
    int y_n;
    array[y_n] int y;
}
transformed data {
    int n = y_n;
}
parameters {
    real<lower=0> theta_sigma;
    // The type of the `xi` parameter depends on the passed link function `link_f`
    positive_ordered[n] theta_xi;
}
transformed parameters {
    // The negation is needed to ensure that `theta` is in descending order
    vector[n] theta = (-theta_xi);
}
model {
    theta_sigma ~ std_normal();
    // The type of the `xi` parameter depends on the passed link function `link_f`
    theta_xi ~ rw1_log(theta_sigma, n);
    // The exact implementation of the likelihood depends on the passed link function `link_f`
    y ~ my_bernoulli_log(theta);
}
generated quantities {
    // The exact implementation of the likelihood depends on the passed link function `link_f`
    vector[y_n] y_likelihood = my_bernoulli_log_lpmfs(y, theta);
    // The exact implementation of the likelihood depends on the passed link function `link_f`
    array[n] int y_gen = my_bernoulli_int_log_rng(y_n, theta);
}

rw2 / logit

stan
functions {
// The `RW(2)` prior density after constraining - this function definition gets used to infer the type and shape of the parameter for the logit link
real rw2_logit_lpdf(
    vector x,
    real sigma,
    int n
) {
    return rw2_lpdf(x | sigma);
}
// The `RW(2)` prior density after constraining - common to both link functions
real rw2_lpdf(
    vector x,
    real sigma
) {
    int n = dims(x)[1];
    return (rw1_lpdf(x[1:2] | sigma) + normal_lpdf(x[3:n] | ((2 * x[2:(n - 1)]) - x[1:(n - 2)]), sigma));
}
// The `RW(1)` prior density after constraining - common to both link functions
real rw1_lpdf(
    vector x,
    real sigma
) {
    int n = dims(x)[1];
    return (normal_lpdf(x[1] | 0, sigma) + normal_lpdf(x[2:n] | x[1:(n - 1)], sigma));
}
// Needed to compute the joint likelihood
real my_bernoulli_logit_lpmf(
    array[] int y,
    vector theta
) {
    return bernoulli_logit_lpmf(y | theta);
}
// Needed for cross validation
vector my_bernoulli_logit_lpmfs(
    array[] int y,
    vector args1
) {
    return jbroadcasted_my_bernoulli_lpmfs_logit(y, args1);
}
vector jbroadcasted_my_bernoulli_lpmfs_logit(
    array[] int x1,
    vector x3
) {
    int n = dims(x1)[1];
    vector[n] rv;
    for(i in 1:n) {
        rv[i] = my_bernoulli_logit_lpmfs(broadcasted_getindex(x1, i), broadcasted_getindex(x3, i));
    }
    return rv;
}
// Needed for cross validation
real my_bernoulli_logit_lpmfs(
    int y,
    real args1
) {
    return my_bernoulli_logit_lpmf(y | args1);
}
// Needed to compute the joint likelihood
real my_bernoulli_logit_lpmf(
    int y,
    real theta
) {
    return bernoulli_logit_lpmf(y | theta);
}
int broadcasted_getindex(array[] int x, int i) {
    return x[i];
}
real broadcasted_getindex(vector x, int i) {
    return x[i];
}
array[] int my_bernoulli_int_logit_rng(
    int anontok__1,
    vector theta
) {
    int n = anontok__1;
    if (dims(theta)[1] != n) reject("my_bernoulli_rng: dim mismatch — `theta` dim 1 (= ", dims(theta)[1], ") does not match `n` (= ", n, "), inferred from `anontok__1` dim 1. `n` sizes: `anontok__1` dim 1 (= ", anontok__1, "), `theta` dim 1 (= ", dims(theta)[1], ").");
    return bernoulli_logit_rng(theta);
}
}
data {
    int y_n;
    array[y_n] int y;
}
transformed data {
    int n = y_n;
}
parameters {
    real<lower=0> theta_sigma;
    // The type of the `xi` parameter depends on the passed link function `link_f`
    ordered[n] theta_xi;
}
transformed parameters {
    // The negation is needed to ensure that `theta` is in descending order
    vector[n] theta = (-theta_xi);
}
model {
    theta_sigma ~ normal(0, 0.5);
    // The type of the `xi` parameter depends on the passed link function `link_f`
    theta_xi ~ rw2_logit(theta_sigma, n);
    // The exact implementation of the likelihood depends on the passed link function `link_f`
    y ~ my_bernoulli_logit(theta);
}
generated quantities {
    // The exact implementation of the likelihood depends on the passed link function `link_f`
    vector[y_n] y_likelihood = my_bernoulli_logit_lpmfs(y, theta);
    // The exact implementation of the likelihood depends on the passed link function `link_f`
    array[n] int y_gen = my_bernoulli_int_logit_rng(y_n, theta);
}

rw2 / log

stan
functions {
// The `RW(2)` prior density after constraining - this function definition gets used to infer the type and shape of the parameter for the log link
real rw2_log_lpdf(
    vector x,
    real sigma,
    int n
) {
    return rw2_lpdf(x | sigma);
}
// The `RW(2)` prior density after constraining - common to both link functions
real rw2_lpdf(
    vector x,
    real sigma
) {
    int n = dims(x)[1];
    return (rw1_lpdf(x[1:2] | sigma) + normal_lpdf(x[3:n] | ((2 * x[2:(n - 1)]) - x[1:(n - 2)]), sigma));
}
// The `RW(1)` prior density after constraining - common to both link functions
real rw1_lpdf(
    vector x,
    real sigma
) {
    int n = dims(x)[1];
    return (normal_lpdf(x[1] | 0, sigma) + normal_lpdf(x[2:n] | x[1:(n - 1)], sigma));
}
// Needed to compute the joint likelihood
real my_bernoulli_log_lpmf(
    array[] int y,
    vector theta
) {
    return bernoulli_lpmf(y | exp(theta));
}
// Needed for cross validation
vector my_bernoulli_log_lpmfs(
    array[] int y,
    vector args1
) {
    return jbroadcasted_my_bernoulli_lpmfs_log(y, args1);
}
vector jbroadcasted_my_bernoulli_lpmfs_log(
    array[] int x1,
    vector x3
) {
    int n = dims(x1)[1];
    vector[n] rv;
    for(i in 1:n) {
        rv[i] = my_bernoulli_log_lpmfs(broadcasted_getindex(x1, i), broadcasted_getindex(x3, i));
    }
    return rv;
}
// Needed for cross validation
real my_bernoulli_log_lpmfs(
    int y,
    real args1
) {
    return my_bernoulli_log_lpmf(y | args1);
}
// Needed to compute the joint likelihood
real my_bernoulli_log_lpmf(
    int y,
    real theta
) {
    return bernoulli_lpmf(y | exp(theta));
}
int broadcasted_getindex(array[] int x, int i) {
    return x[i];
}
real broadcasted_getindex(vector x, int i) {
    return x[i];
}
array[] int my_bernoulli_int_log_rng(
    int anontok__1,
    vector theta
) {
    int n = anontok__1;
    if (dims(theta)[1] != n) reject("my_bernoulli_rng: dim mismatch — `theta` dim 1 (= ", dims(theta)[1], ") does not match `n` (= ", n, "), inferred from `anontok__1` dim 1. `n` sizes: `anontok__1` dim 1 (= ", anontok__1, "), `theta` dim 1 (= ", dims(theta)[1], ").");
    return bernoulli_rng(exp(theta));
}
}
data {
    int y_n;
    array[y_n] int y;
}
transformed data {
    int n = y_n;
}
parameters {
    real<lower=0> theta_sigma;
    // The type of the `xi` parameter depends on the passed link function `link_f`
    positive_ordered[n] theta_xi;
}
transformed parameters {
    // The negation is needed to ensure that `theta` is in descending order
    vector[n] theta = (-theta_xi);
}
model {
    theta_sigma ~ normal(0, 0.5);
    // The type of the `xi` parameter depends on the passed link function `link_f`
    theta_xi ~ rw2_log(theta_sigma, n);
    // The exact implementation of the likelihood depends on the passed link function `link_f`
    y ~ my_bernoulli_log(theta);
}
generated quantities {
    // The exact implementation of the likelihood depends on the passed link function `link_f`
    vector[y_n] y_likelihood = my_bernoulli_log_lpmfs(y, theta);
    // The exact implementation of the likelihood depends on the passed link function `link_f`
    array[n] int y_gen = my_bernoulli_int_log_rng(y_n, theta);
}

regression / logit

stan
functions {
// The upper bound of the alpha parameter (for the logit link function)
real upper_alpha_logit(
    
) {
    return negative_infinity();
}
// Needed to compute the joint likelihood
real my_bernoulli_logit_lpmf(
    array[] int y,
    vector theta
) {
    return bernoulli_logit_lpmf(y | theta);
}
// Needed for cross validation
vector my_bernoulli_logit_lpmfs(
    array[] int y,
    vector args1
) {
    return jbroadcasted_my_bernoulli_lpmfs_logit(y, args1);
}
vector jbroadcasted_my_bernoulli_lpmfs_logit(
    array[] int x1,
    vector x3
) {
    int n = dims(x1)[1];
    vector[n] rv;
    for(i in 1:n) {
        rv[i] = my_bernoulli_logit_lpmfs(broadcasted_getindex(x1, i), broadcasted_getindex(x3, i));
    }
    return rv;
}
// Needed for cross validation
real my_bernoulli_logit_lpmfs(
    int y,
    real args1
) {
    return my_bernoulli_logit_lpmf(y | args1);
}
// Needed to compute the joint likelihood
real my_bernoulli_logit_lpmf(
    int y,
    real theta
) {
    return bernoulli_logit_lpmf(y | theta);
}
int broadcasted_getindex(array[] int x, int i) {
    return x[i];
}
real broadcasted_getindex(vector x, int i) {
    return x[i];
}
array[] int my_bernoulli_int_logit_rng(
    int anontok__1,
    vector theta
) {
    int n = anontok__1;
    if (dims(theta)[1] != n) reject("my_bernoulli_rng: dim mismatch — `theta` dim 1 (= ", dims(theta)[1], ") does not match `n` (= ", n, "), inferred from `anontok__1` dim 1. `n` sizes: `anontok__1` dim 1 (= ", anontok__1, "), `theta` dim 1 (= ", dims(theta)[1], ").");
    return bernoulli_logit_rng(theta);
}
}
data {
    int y_n;
    int t_n;
    vector[t_n] t;
    array[y_n] int y;
}
transformed data {
    int n = y_n;
    real theta_alpha_upper = upper_alpha_logit();
}
parameters {
    real<upper=theta_alpha_upper> theta_alpha;
    real<upper=0.0> theta_beta;
}
transformed parameters {
    vector[t_n] theta = (theta_alpha + (theta_beta * t));
}
model {
    theta_alpha ~ normal(0, 0.5);
    theta_beta ~ normal(0, 0.5);
    // The exact implementation of the likelihood depends on the passed link function `link_f`
    y ~ my_bernoulli_logit(theta);
}
generated quantities {
    // The exact implementation of the likelihood depends on the passed link function `link_f`
    vector[y_n] y_likelihood = my_bernoulli_logit_lpmfs(y, theta);
    // The exact implementation of the likelihood depends on the passed link function `link_f`
    array[t_n] int y_gen = my_bernoulli_int_logit_rng(y_n, theta);
}

regression / log

stan
functions {
// The upper bound of the alpha parameter (for the log link function)
int upper_alpha_log(
    
) {
    return 0;
}
// Needed to compute the joint likelihood
real my_bernoulli_log_lpmf(
    array[] int y,
    vector theta
) {
    return bernoulli_lpmf(y | exp(theta));
}
// Needed for cross validation
vector my_bernoulli_log_lpmfs(
    array[] int y,
    vector args1
) {
    return jbroadcasted_my_bernoulli_lpmfs_log(y, args1);
}
vector jbroadcasted_my_bernoulli_lpmfs_log(
    array[] int x1,
    vector x3
) {
    int n = dims(x1)[1];
    vector[n] rv;
    for(i in 1:n) {
        rv[i] = my_bernoulli_log_lpmfs(broadcasted_getindex(x1, i), broadcasted_getindex(x3, i));
    }
    return rv;
}
// Needed for cross validation
real my_bernoulli_log_lpmfs(
    int y,
    real args1
) {
    return my_bernoulli_log_lpmf(y | args1);
}
// Needed to compute the joint likelihood
real my_bernoulli_log_lpmf(
    int y,
    real theta
) {
    return bernoulli_lpmf(y | exp(theta));
}
int broadcasted_getindex(array[] int x, int i) {
    return x[i];
}
real broadcasted_getindex(vector x, int i) {
    return x[i];
}
array[] int my_bernoulli_int_log_rng(
    int anontok__1,
    vector theta
) {
    int n = anontok__1;
    if (dims(theta)[1] != n) reject("my_bernoulli_rng: dim mismatch — `theta` dim 1 (= ", dims(theta)[1], ") does not match `n` (= ", n, "), inferred from `anontok__1` dim 1. `n` sizes: `anontok__1` dim 1 (= ", anontok__1, "), `theta` dim 1 (= ", dims(theta)[1], ").");
    return bernoulli_rng(exp(theta));
}
}
data {
    int y_n;
    int t_n;
    vector[t_n] t;
    array[y_n] int y;
}
transformed data {
    int n = y_n;
    int theta_alpha_upper = upper_alpha_log();
}
parameters {
    real<upper=theta_alpha_upper> theta_alpha;
    real<upper=0.0> theta_beta;
}
transformed parameters {
    vector[t_n] theta = (theta_alpha + (theta_beta * t));
}
model {
    theta_alpha ~ normal(0, 0.5);
    theta_beta ~ normal(0, 0.5);
    // The exact implementation of the likelihood depends on the passed link function `link_f`
    y ~ my_bernoulli_log(theta);
}
generated quantities {
    // The exact implementation of the likelihood depends on the passed link function `link_f`
    vector[y_n] y_likelihood = my_bernoulli_log_lpmfs(y, theta);
    // The exact implementation of the likelihood depends on the passed link function `link_f`
    array[t_n] int y_gen = my_bernoulli_int_log_rng(y_n, theta);
}

regression_mix / logit

stan
functions {
// The upper bound of the alpha parameter (for the logit link function)
real upper_alpha_logit(
    
) {
    return negative_infinity();
}
vector bernoulli_lpmfs(
    array[] int obs,
    vector args1
) {
    return jbroadcasted_bernoulli_lpmfs(obs, args1);
}
vector jbroadcasted_bernoulli_lpmfs(
    array[] int x1,
    vector x2
) {
    int n = dims(x1)[1];
    vector[n] rv;
    for(i in 1:n) {
        rv[i] = bernoulli_lpmfs(broadcasted_getindex(x1, i), broadcasted_getindex(x2, i));
    }
    return rv;
}
real bernoulli_lpmfs(int args1, real args2) {
    return bernoulli_lpmf(args1 | args2);
}
int broadcasted_getindex(array[] int x, int i) {
    return x[i];
}
real broadcasted_getindex(vector x, int i) {
    return x[i];
}
array[] int bernoulli_int_rng(
    int anontok__1,
    vector p
) {
    int n = anontok__1;
    return bernoulli_rng(p);
}
// Needed to compute the joint likelihood
real my_bernoulli_logit_lpmf(
    array[] int y,
    vector theta
) {
    return bernoulli_logit_lpmf(y | theta);
}
// Needed for cross validation
vector my_bernoulli_logit_lpmfs(
    array[] int y,
    vector args1
) {
    return jbroadcasted_my_bernoulli_lpmfs_logit(y, args1);
}
vector jbroadcasted_my_bernoulli_lpmfs_logit(
    array[] int x1,
    vector x3
) {
    int n = dims(x1)[1];
    vector[n] rv;
    for(i in 1:n) {
        rv[i] = my_bernoulli_logit_lpmfs(broadcasted_getindex(x1, i), broadcasted_getindex(x3, i));
    }
    return rv;
}
// Needed for cross validation
real my_bernoulli_logit_lpmfs(
    int y,
    real args1
) {
    return my_bernoulli_logit_lpmf(y | args1);
}
// Needed to compute the joint likelihood
real my_bernoulli_logit_lpmf(
    int y,
    real theta
) {
    return bernoulli_logit_lpmf(y | theta);
}
array[] int my_bernoulli_int_logit_rng(
    int anontok__1,
    vector theta
) {
    int n = anontok__1;
    if (dims(theta)[1] != n) reject("my_bernoulli_rng: dim mismatch — `theta` dim 1 (= ", dims(theta)[1], ") does not match `n` (= ", n, "), inferred from `anontok__1` dim 1. `n` sizes: `anontok__1` dim 1 (= ", anontok__1, "), `theta` dim 1 (= ", dims(theta)[1], ").");
    return bernoulli_logit_rng(theta);
}
}
data {
    int t_n;
    vector[t_n] t;
    int y_n;
    array[y_n] int y;
}
transformed data {
    real theta_c1_alpha_upper = upper_alpha_logit();
    real theta_c2_alpha_upper = upper_alpha_logit();
    int n = y_n;
}
parameters {
    real<upper=theta_c1_alpha_upper> theta_c1_alpha;
    real<upper=0.0> theta_c1_beta;
    real<upper=theta_c2_alpha_upper> theta_c2_alpha;
    real<upper=0.0> theta_c2_beta;
    real<lower=0, upper=1> theta_lambda;
}
transformed parameters {
    vector[t_n] theta_c1 = (theta_c1_alpha + (theta_c1_beta * t));
    vector[t_n] theta_c2 = (theta_c2_alpha + (theta_c2_beta * t));
    vector[t_n] theta = ((theta_lambda * logit(theta_c1)) + ((1 - theta_lambda) * logit(theta_c2)));
}
model {
    theta_c1_alpha ~ normal(0, 0.5);
    theta_c1_beta ~ normal(0, 0.5);
    theta_c2_alpha ~ normal(0, 0.5);
    theta_c2_beta ~ normal(0, 0.5);
    theta_lambda ~ beta(2, 2);
    y ~ bernoulli(theta);
    // The exact implementation of the likelihood depends on the passed link function `link_f`
    y ~ my_bernoulli_logit(theta);
}
generated quantities {
    vector[y_n] y_likelihood = bernoulli_lpmfs(y, theta);
    array[y_n] int y_gen = bernoulli_int_rng(y_n, theta);
    // The exact implementation of the likelihood depends on the passed link function `link_f`
    vector[y_n] y_likelihood = my_bernoulli_logit_lpmfs(y, theta);
    // The exact implementation of the likelihood depends on the passed link function `link_f`
    array[t_n] int y_gen = my_bernoulli_int_logit_rng(y_n, theta);
}

regression_mix / log

stan
functions {
// The upper bound of the alpha parameter (for the log link function)
int upper_alpha_log(
    
) {
    return 0;
}
vector bernoulli_lpmfs(
    array[] int obs,
    vector args1
) {
    return jbroadcasted_bernoulli_lpmfs(obs, args1);
}
vector jbroadcasted_bernoulli_lpmfs(
    array[] int x1,
    vector x2
) {
    int n = dims(x1)[1];
    vector[n] rv;
    for(i in 1:n) {
        rv[i] = bernoulli_lpmfs(broadcasted_getindex(x1, i), broadcasted_getindex(x2, i));
    }
    return rv;
}
real bernoulli_lpmfs(int args1, real args2) {
    return bernoulli_lpmf(args1 | args2);
}
int broadcasted_getindex(array[] int x, int i) {
    return x[i];
}
real broadcasted_getindex(vector x, int i) {
    return x[i];
}
array[] int bernoulli_int_rng(
    int anontok__1,
    vector p
) {
    int n = anontok__1;
    return bernoulli_rng(p);
}
// Needed to compute the joint likelihood
real my_bernoulli_log_lpmf(
    array[] int y,
    vector theta
) {
    return bernoulli_lpmf(y | exp(theta));
}
// Needed for cross validation
vector my_bernoulli_log_lpmfs(
    array[] int y,
    vector args1
) {
    return jbroadcasted_my_bernoulli_lpmfs_log(y, args1);
}
vector jbroadcasted_my_bernoulli_lpmfs_log(
    array[] int x1,
    vector x3
) {
    int n = dims(x1)[1];
    vector[n] rv;
    for(i in 1:n) {
        rv[i] = my_bernoulli_log_lpmfs(broadcasted_getindex(x1, i), broadcasted_getindex(x3, i));
    }
    return rv;
}
// Needed for cross validation
real my_bernoulli_log_lpmfs(
    int y,
    real args1
) {
    return my_bernoulli_log_lpmf(y | args1);
}
// Needed to compute the joint likelihood
real my_bernoulli_log_lpmf(
    int y,
    real theta
) {
    return bernoulli_lpmf(y | exp(theta));
}
array[] int my_bernoulli_int_log_rng(
    int anontok__1,
    vector theta
) {
    int n = anontok__1;
    if (dims(theta)[1] != n) reject("my_bernoulli_rng: dim mismatch — `theta` dim 1 (= ", dims(theta)[1], ") does not match `n` (= ", n, "), inferred from `anontok__1` dim 1. `n` sizes: `anontok__1` dim 1 (= ", anontok__1, "), `theta` dim 1 (= ", dims(theta)[1], ").");
    return bernoulli_rng(exp(theta));
}
}
data {
    int t_n;
    vector[t_n] t;
    int y_n;
    array[y_n] int y;
}
transformed data {
    int theta_c1_alpha_upper = upper_alpha_log();
    int theta_c2_alpha_upper = upper_alpha_log();
    int n = y_n;
}
parameters {
    real<upper=theta_c1_alpha_upper> theta_c1_alpha;
    real<upper=0.0> theta_c1_beta;
    real<upper=theta_c2_alpha_upper> theta_c2_alpha;
    real<upper=0.0> theta_c2_beta;
    real<lower=0, upper=1> theta_lambda;
}
transformed parameters {
    vector[t_n] theta_c1 = (theta_c1_alpha + (theta_c1_beta * t));
    vector[t_n] theta_c2 = (theta_c2_alpha + (theta_c2_beta * t));
    vector[t_n] theta = ((theta_lambda * log(theta_c1)) + ((1 - theta_lambda) * log(theta_c2)));
}
model {
    theta_c1_alpha ~ normal(0, 0.5);
    theta_c1_beta ~ normal(0, 0.5);
    theta_c2_alpha ~ normal(0, 0.5);
    theta_c2_beta ~ normal(0, 0.5);
    theta_lambda ~ beta(2, 2);
    y ~ bernoulli(theta);
    // The exact implementation of the likelihood depends on the passed link function `link_f`
    y ~ my_bernoulli_log(theta);
}
generated quantities {
    vector[y_n] y_likelihood = bernoulli_lpmfs(y, theta);
    array[y_n] int y_gen = bernoulli_int_rng(y_n, theta);
    // The exact implementation of the likelihood depends on the passed link function `link_f`
    vector[y_n] y_likelihood = my_bernoulli_log_lpmfs(y, theta);
    // The exact implementation of the likelihood depends on the passed link function `link_f`
    array[t_n] int y_gen = my_bernoulli_int_log_rng(y_n, theta);
}

Where the parameter type and size come from

Sampling syntax needs more than a log-density: StanBlocks must also know the left-hand side's Stan type, constraints, and dimensions. A distribution method can provide that contract symbolically. In this example,

julia
@lhs @lpxf hetero_lpdf(x::ordered[n], ::typeof(logit), n) = hetero_lpdf(x)

says that xi ~ hetero(logit, n) introduces an ordered[n] parameter, while the corresponding typeof(log) method introduces a positive_ordered[n] parameter. The ordinary hetero_lpdf(x::vector[n]) method holds the density calculation shared by both constrained interfaces.

That mechanism is still useful for reusable custom distributions: the caller does not repeat a constraint that is intrinsic to the distribution. It is no longer the only way to communicate intent, however. Current StanBlocks also supports explicit typed left-hand sides such as

julia
theta::vector[n] ~ distribution(foo, bar)

when the declaration belongs at the model site. The preserved PCR source uses @lhs because the same distribution name intentionally selects a different transform for each function token. This is a design choice in the example, not an outstanding type-annotation stub.

You are viewing the dev branch. This branch may include code written with Claude Code with less human supervision. Only human-approved code is merged into main.