From 1acbb016e23a85dfe0367c9024119cb847a7b93f Mon Sep 17 00:00:00 2001 From: Xianda Sun Date: Mon, 7 Apr 2025 15:35:14 +0100 Subject: [PATCH 01/15] implement the macros --- src/JuliaBUGS.jl | 2 + src/model_macro.jl | 187 ++++++++++++++++++++++++++++++++++++++++++++ test/model_macro.jl | 36 +++++++++ test/runtests.jl | 1 + 4 files changed, 226 insertions(+) create mode 100644 src/model_macro.jl create mode 100644 test/model_macro.jl diff --git a/src/JuliaBUGS.jl b/src/JuliaBUGS.jl index b6b365017..191111e55 100644 --- a/src/JuliaBUGS.jl +++ b/src/JuliaBUGS.jl @@ -251,6 +251,8 @@ Only defined with `MCMCChains` extension. """ function gen_chains end +include("model_macro.jl") + include("experimental/ProbabilisticGraphicalModels/ProbabilisticGraphicalModels.jl") end diff --git a/src/model_macro.jl b/src/model_macro.jl new file mode 100644 index 000000000..19ab4c26c --- /dev/null +++ b/src/model_macro.jl @@ -0,0 +1,187 @@ +using MacroTools + +const __struct_name_to_field_name = Dict{Symbol,Vector{Symbol}}() + +struct ParameterPlaceholder end + +macro parameters(struct_expr) + if MacroTools.@capture(struct_expr, struct struct_name_ + struct_fields__ + end) + return _generate_struct_definition(struct_name, struct_fields) + else + # Use ArgumentError for invalid macro input + return :(throw( + ArgumentError( + "Expected a struct definition like '@parameters struct MyParams ... end'" + ), + )) + end +end + +function _generate_struct_definition(struct_name, struct_fields) + if !isa(struct_name, Symbol) + return :(throw( + ArgumentError( + "Parametrized types (e.g., `struct MyParams{T}`) are not supported yet" + ), + )) + elseif !all(isa.(struct_fields, Symbol)) + return :(throw( + ArgumentError( + "Field types are determined by JuliaBUGS automatically. Do not specify types in the struct definition.", + ), + )) + end + + struct_name_quoted = QuoteNode(struct_name) + struct_fields_quoted = [QuoteNode(f) for f in struct_fields] + + show_method_expr = quote + function Base.show(io::IO, mime::MIME"text/plain", params::$(esc(struct_name))) + println(io, "$(nameof(typeof(params))):") + fields = fieldnames(typeof(params)) + max_len = isempty(fields) ? 0 : maximum(length ∘ string, fields) + for field in fields + value = getfield(params, field) + field_str = rpad(string(field), max_len) + print(io, " ", field_str, " = ") + if value isa JuliaBUGS.ParameterPlaceholder + printstyled(io, ""; color=:light_black) + else + show(io, mime, value) + end + println(io) + end + end + end + + return quote + __struct_name_to_field_name[$(esc(struct_name_quoted))] = [ + $(esc.(struct_fields_quoted)...) + ] + + Base.@kwdef struct $(esc(struct_name)) + $(map(f -> :($(esc(f)) = ParameterPlaceholder()), struct_fields)...) + end + + $(show_method_expr) + end +end + +macro model(model_function_expr) + return _generate_model_definition(model_function_expr, __source__) +end + +function _generate_model_definition(model_function_expr, __source__) + if MacroTools.@capture( + #! format: off + model_function_expr, + function model_name_(param_splat_, constant_variables__) + body_expr__ + end + #! format: on + ) + block_body_expr = Expr(:block, body_expr...) + body_with_lines = _add_line_number_nodes(block_body_expr) # hack, see _add_line_number_nodes + + bugs_ast_input = body_with_lines + + # refer parser/bugs_macro.jl + Parser.warn_cumulative_density_deviance(bugs_ast_input) + bugs_ast = Parser.bugs_top(bugs_ast_input, __source__) + + vars_and_numdims = extract_variable_names_and_numdims(bugs_ast) + vars_assigned_to = extract_variables_assigned_to(bugs_ast) + stochastic_vars = [vars_assigned_to[2]..., vars_assigned_to[4]...] + deterministic_vars = [vars_assigned_to[1]..., vars_assigned_to[3]...] + all_vars = collect(keys(vars_and_numdims)) + constants = setdiff(all_vars, vcat(stochastic_vars, deterministic_vars)) + + if MacroTools.@capture(param_splat, (; param_fields__)::param_type_) + if !haskey(__struct_name_to_field_name, param_type) + return :(error( + "$param_type is not registered as a parameter struct. Use `@parameters` to define it.", + )) + else + # check if the field names coincide with stochastic_vars + if !all(in(stochastic_vars), __struct_name_to_field_name[param_type]) + return :(error( + "The field names of the struct definition of the parameters in the model function should coincide with the stochastic variables.", + )) + end + end + + if !all(in(constant_variables), constants) + missing_constants = setdiff(constants, constant_variables) + return :(error( + "The following constants used in the model are not included in the function arguments: $($(QuoteNode(missing_constants)))", + )) + end + + return esc( + MacroTools.@q begin + __model_def__ = $(QuoteNode(bugs_ast)) + function $model_name( + __params__::$(param_type), $(constant_variables...) + ) + pairs_vector = Pair{Symbol,Any}[] + $( + map( + field_name -> quote + val = __params__.$(field_name) + if !(val isa JuliaBUGS.ParameterPlaceholder) + push!(pairs_vector, $(QuoteNode(field_name)) => val) + end + end, + __struct_name_to_field_name[param_type], + )... + ) + data = NamedTuple(pairs_vector) + constants_nt = (; $(constant_variables...)) + combined_data = Base.merge(data, constants_nt) + + return compile(__model_def__, combined_data) + end + end + ) + else + return :(throw( + ArgumentError( + "The first argument of the model function must be a destructuring assignment with a type annotation defined using `@parameters`.", + ), + )) + end + else + return :(throw(ArgumentError("Expected a model function definition"))) + end +end + +# this is a hack, the reason I need this is that even the code is the same, if parsed as a function body +# the parser only inserts a LineNumberNode for the first statement, not for each statement in the body +# in contrast, if parsed as a "begin ... end" block, the parser inserts a LineNumberNode for each statement +# `bugs_top` made an assumption that the input is from a macro, so it assumes there is a LineNumberNode preceding each statement +# this function is a hack to ensure that there is a LineNumberNode preceding each statement in the body of the model function +function _add_line_number_nodes(expr) + if !(expr isa Expr) + return expr + end + + if Meta.isexpr(expr, :block) + new_args = [] + + for arg in expr.args + if !(arg isa LineNumberNode) && + (isempty(new_args) || !(new_args[end] isa LineNumberNode)) + push!(new_args, LineNumberNode(0, :none)) # use a dummy LineNumberNode + end + + push!(new_args, arg isa Expr ? _add_line_number_nodes(arg) : arg) + end + + return Expr(:block, new_args...) + else + new_args = map(arg -> _add_line_number_nodes(arg), expr.args) + return Expr(expr.head, new_args...) + end +end diff --git a/test/model_macro.jl b/test/model_macro.jl new file mode 100644 index 000000000..9f08deaa6 --- /dev/null +++ b/test/model_macro.jl @@ -0,0 +1,36 @@ +using JuliaBUGS +using JuliaBUGS: @parameters, @model + +@testset "model macro" begin + @parameters struct Tp + r + b + alpha0 + alpha1 + alpha2 + alpha12 + tau + end + + @macroexpand @model function seeds( + (; r, b, alpha0, alpha1, alpha2, alpha12, tau)::Tp, x1, x2, N, n + ) + for i in 1:N + r[i] ~ dbin(p[i], n[i]) + b[i] ~ dnorm(0.0, tau) + p[i] = logistic( + alpha0 + alpha1 * x1[i] + alpha2 * x2[i] + alpha12 * x1[i] * x2[i] + b[i] + ) + end + alpha0 ~ dnorm(0.0, 1.0E-6) + alpha1 ~ dnorm(0.0, 1.0E-6) + alpha2 ~ dnorm(0.0, 1.0E-6) + alpha12 ~ dnorm(0.0, 1.0E-6) + tau ~ dgamma(0.001, 0.001) + return sigma = 1 / sqrt(tau) + end + + data = JuliaBUGS.BUGSExamples.seeds.data + m = seeds(Tp(), data.x1, data.x2, data.N, data.n) + @test m isa JuliaBUGS.BUGSModel +end diff --git a/test/runtests.jl b/test/runtests.jl index 9061842bf..3f42d8636 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -45,6 +45,7 @@ if test_group == "elementary" || test_group == "all" include("parser/test_parser.jl") include("passes.jl") include("graphs.jl") + include("model_macro.jl") end if test_group == "compilation" || test_group == "all" From 337ead36dc565b269da615088892d91639f1b2b8 Mon Sep 17 00:00:00 2001 From: Xianda Sun Date: Fri, 11 Apr 2025 14:14:41 +0100 Subject: [PATCH 02/15] update the code --- src/model_macro.jl | 235 +++++++++++++++++++++++++------------------- test/model_macro.jl | 10 +- 2 files changed, 143 insertions(+), 102 deletions(-) diff --git a/src/model_macro.jl b/src/model_macro.jl index 19ab4c26c..057830466 100644 --- a/src/model_macro.jl +++ b/src/model_macro.jl @@ -1,6 +1,10 @@ using MacroTools -const __struct_name_to_field_name = Dict{Symbol,Vector{Symbol}}() +# The `@capture` macro from MacroTools is used to pattern-match Julia code. +# When a variable in the pattern is followed by a single underscore (e.g., `var_`), +# it captures a single component of the Julia expression and binds it locally to that +# variable name. If a variable is followed by double underscores (e.g., `vars__`), +# it captures multiple components into an array. struct ParameterPlaceholder end @@ -10,7 +14,6 @@ macro parameters(struct_expr) end) return _generate_struct_definition(struct_name, struct_fields) else - # Use ArgumentError for invalid macro input return :(throw( ArgumentError( "Expected a struct definition like '@parameters struct MyParams ... end'" @@ -26,41 +29,53 @@ function _generate_struct_definition(struct_name, struct_fields) "Parametrized types (e.g., `struct MyParams{T}`) are not supported yet" ), )) - elseif !all(isa.(struct_fields, Symbol)) + end + + if !all(isa.(struct_fields, Symbol)) return :(throw( ArgumentError( - "Field types are determined by JuliaBUGS automatically. Do not specify types in the struct definition.", + "Field types are determined by JuliaBUGS automatically. Specify types for fields is not allowed for now.", ), )) end - struct_name_quoted = QuoteNode(struct_name) - struct_fields_quoted = [QuoteNode(f) for f in struct_fields] - - show_method_expr = quote - function Base.show(io::IO, mime::MIME"text/plain", params::$(esc(struct_name))) - println(io, "$(nameof(typeof(params))):") - fields = fieldnames(typeof(params)) - max_len = isempty(fields) ? 0 : maximum(length ∘ string, fields) - for field in fields - value = getfield(params, field) - field_str = rpad(string(field), max_len) - print(io, " ", field_str, " = ") - if value isa JuliaBUGS.ParameterPlaceholder - printstyled(io, ""; color=:light_black) - else - show(io, mime, value) - end - println(io) + show_method_expr = MacroTools.@q function Base.show( + io::IO, mime::MIME"text/plain", params::$(esc(struct_name)) + ) + # Use IOContext for potentially compact/limited printing of field values + ioc = IOContext(io, :compact => true, :limit => true) + + println(ioc, "$(nameof(typeof(params))):") + fields = fieldnames(typeof(params)) + + # Handle empty structs gracefully + if isempty(fields) + print(ioc, " (no fields)") + return + end + + # Calculate maximum field name length for alignment + max_len = maximum(length ∘ string, fields) + for field in fields + value = getfield(params, field) + field_str = rpad(string(field), max_len) + print(ioc, " ", field_str, " = ") + if value isa JuliaBUGS.ParameterPlaceholder + # Use the IOContext here as well + printstyled(ioc, ""; color=:light_black) + else + # Capture the string representation using the context + # Use the basic `show` for a more compact representation, especially for arrays + str_representation = sprint(show, value; context=ioc) + # Print the captured string with color + printstyled(ioc, str_representation; color=:cyan) end + # Use the IOContext for the newline too + println(ioc) end end return quote - __struct_name_to_field_name[$(esc(struct_name_quoted))] = [ - $(esc.(struct_fields_quoted)...) - ] - Base.@kwdef struct $(esc(struct_name)) $(map(f -> :($(esc(f)) = ParameterPlaceholder()), struct_fields)...) end @@ -74,94 +89,114 @@ macro model(model_function_expr) end function _generate_model_definition(model_function_expr, __source__) - if MacroTools.@capture( + MacroTools.@capture( #! format: off model_function_expr, - function model_name_(param_splat_, constant_variables__) + function model_name_(param_destructure_, constant_variables__) body_expr__ end #! format: on + ) || return :(throw(ArgumentError("Expected a model function definition"))) + + model_def = _add_line_number_nodes(Expr(:block, body_expr...)) # hack, see _add_line_number_nodes + Parser.warn_cumulative_density_deviance(model_def) # refer to parser/bugs_macro.jl + + bugs_ast = Parser.bugs_top(model_def, __source__) + + param_type = nothing + MacroTools.@capture( + param_destructure, (((; param_fields__)::param_type_) | ((; param_fields__))) + ) || return :(throw( + ArgumentError( + "The first argument of the model function must be a destructuring assignment with a type annotation defined using `@parameters`.", + ), + )) + + vars_and_numdims = extract_variable_names_and_numdims(bugs_ast) + vars_assigned_to = extract_variables_assigned_to(bugs_ast) + stochastic_vars = [vars_assigned_to[2]..., vars_assigned_to[4]...] + deterministic_vars = [vars_assigned_to[1]..., vars_assigned_to[3]...] + all_vars = collect(keys(vars_and_numdims)) + constants = setdiff(all_vars, vcat(stochastic_vars, deterministic_vars)) + + # Check if all constants used in the model are included in function arguments + if !all(in(constant_variables), constants) + missing_constants = setdiff(constants, constant_variables) + formatted_vars = join(missing_constants, ", ", " and ") + return MacroTools.@q error( + string( + "The following constants used in the model are not included in the function arguments: ", + $(QuoteNode(formatted_vars)), + ), + ) + end + + # Check if all stochastic variables are included in the parameters struct + missing_stochastic_vars = setdiff(stochastic_vars, param_fields) + if !isempty(missing_stochastic_vars) + formatted_vars = join(missing_stochastic_vars, ", ", " and ") + return MacroTools.@q error( + string( + "The following stochastic variables used in the model are not included in the parameters ", + "in the first argument of the model function: ", + $(QuoteNode(formatted_vars)), + ), + ) + end + + func_expr = MacroTools.@q function ($(esc(model_name)))( + params_struct, $(esc.(constant_variables)...) ) - block_body_expr = Expr(:block, body_expr...) - body_with_lines = _add_line_number_nodes(block_body_expr) # hack, see _add_line_number_nodes - - bugs_ast_input = body_with_lines - - # refer parser/bugs_macro.jl - Parser.warn_cumulative_density_deviance(bugs_ast_input) - bugs_ast = Parser.bugs_top(bugs_ast_input, __source__) - - vars_and_numdims = extract_variable_names_and_numdims(bugs_ast) - vars_assigned_to = extract_variables_assigned_to(bugs_ast) - stochastic_vars = [vars_assigned_to[2]..., vars_assigned_to[4]...] - deterministic_vars = [vars_assigned_to[1]..., vars_assigned_to[3]...] - all_vars = collect(keys(vars_and_numdims)) - constants = setdiff(all_vars, vcat(stochastic_vars, deterministic_vars)) - - if MacroTools.@capture(param_splat, (; param_fields__)::param_type_) - if !haskey(__struct_name_to_field_name, param_type) - return :(error( - "$param_type is not registered as a parameter struct. Use `@parameters` to define it.", - )) - else - # check if the field names coincide with stochastic_vars - if !all(in(stochastic_vars), __struct_name_to_field_name[param_type]) - return :(error( - "The field names of the struct definition of the parameters in the model function should coincide with the stochastic variables.", - )) + (; $(esc.(param_fields)...)) = params_struct + data = _param_struct_to_NT((; + $([esc.(param_fields)..., esc.(constant_variables)...]...) + )) + model_def = $(QuoteNode(bugs_ast)) + return compile(model_def, data) + end + + if param_type === nothing + return func_expr + else + return MacroTools.@q begin + # Create a constructor for the parameter type that uses values from the model's evaluation environment + function $(esc(param_type))(model::BUGSModel) + env = model.evaluation_env + field_names = fieldnames($(esc(param_type))) + kwargs = Dict{Symbol, Any}() + + for field in field_names + if haskey(env, field) + kwargs[field] = env[field] + end end + + return $(esc(param_type))(; kwargs...) end + $func_expr + end + end +end - if !all(in(constant_variables), constants) - missing_constants = setdiff(constants, constant_variables) - return :(error( - "The following constants used in the model are not included in the function arguments: $($(QuoteNode(missing_constants)))", - )) - end +function _param_struct_to_NT(param_struct) + field_names = fieldnames(typeof(param_struct)) + pairs = Pair{Symbol,Any}[] - return esc( - MacroTools.@q begin - __model_def__ = $(QuoteNode(bugs_ast)) - function $model_name( - __params__::$(param_type), $(constant_variables...) - ) - pairs_vector = Pair{Symbol,Any}[] - $( - map( - field_name -> quote - val = __params__.$(field_name) - if !(val isa JuliaBUGS.ParameterPlaceholder) - push!(pairs_vector, $(QuoteNode(field_name)) => val) - end - end, - __struct_name_to_field_name[param_type], - )... - ) - data = NamedTuple(pairs_vector) - constants_nt = (; $(constant_variables...)) - combined_data = Base.merge(data, constants_nt) - - return compile(__model_def__, combined_data) - end - end - ) - else - return :(throw( - ArgumentError( - "The first argument of the model function must be a destructuring assignment with a type annotation defined using `@parameters`.", - ), - )) + for field_name in field_names + value = getfield(param_struct, field_name) + if !(value isa ParameterPlaceholder) + push!(pairs, field_name => value) end - else - return :(throw(ArgumentError("Expected a model function definition"))) end + + return NamedTuple(pairs) end -# this is a hack, the reason I need this is that even the code is the same, if parsed as a function body -# the parser only inserts a LineNumberNode for the first statement, not for each statement in the body -# in contrast, if parsed as a "begin ... end" block, the parser inserts a LineNumberNode for each statement -# `bugs_top` made an assumption that the input is from a macro, so it assumes there is a LineNumberNode preceding each statement -# this function is a hack to ensure that there is a LineNumberNode preceding each statement in the body of the model function +# This function addresses a discrepancy in how Julia's parser handles LineNumberNode insertion. +# When parsing a function body, the parser only adds a LineNumberNode before the first statement. +# In contrast, when parsing a "begin ... end" block, it inserts a LineNumberNode before each statement. +# The `bugs_top` function assumes input comes from a macro and expects a LineNumberNode before each statement. +# As a workaround, this function ensures that a LineNumberNode precedes every statement in the model function's body. function _add_line_number_nodes(expr) if !(expr isa Expr) return expr diff --git a/test/model_macro.jl b/test/model_macro.jl index 9f08deaa6..97c816f90 100644 --- a/test/model_macro.jl +++ b/test/model_macro.jl @@ -12,7 +12,8 @@ using JuliaBUGS: @parameters, @model tau end - @macroexpand @model function seeds( + #! format: off + @model function seeds( (; r, b, alpha0, alpha1, alpha2, alpha12, tau)::Tp, x1, x2, N, n ) for i in 1:N @@ -27,10 +28,15 @@ using JuliaBUGS: @parameters, @model alpha2 ~ dnorm(0.0, 1.0E-6) alpha12 ~ dnorm(0.0, 1.0E-6) tau ~ dgamma(0.001, 0.001) - return sigma = 1 / sqrt(tau) + sigma = 1 / sqrt(tau) end + #! format: on data = JuliaBUGS.BUGSExamples.seeds.data + m = seeds(Tp(), data.x1, data.x2, data.N, data.n) + + Tp(m) + @test m isa JuliaBUGS.BUGSModel end From e7d356ea0178eb0d2f2881d4d52cae6c2f3e5c17 Mon Sep 17 00:00:00 2001 From: Xianda Sun Date: Fri, 11 Apr 2025 14:31:13 +0100 Subject: [PATCH 03/15] improve tests and format --- src/model_macro.jl | 8 ++++---- test/model_macro.jl | 47 ++++++++++++++++++++++++++++++++++++++++++--- 2 files changed, 48 insertions(+), 7 deletions(-) diff --git a/src/model_macro.jl b/src/model_macro.jl index 057830466..c6cc0d304 100644 --- a/src/model_macro.jl +++ b/src/model_macro.jl @@ -51,7 +51,7 @@ function _generate_struct_definition(struct_name, struct_fields) # Handle empty structs gracefully if isempty(fields) print(ioc, " (no fields)") - return + return nothing end # Calculate maximum field name length for alignment @@ -163,14 +163,14 @@ function _generate_model_definition(model_function_expr, __source__) function $(esc(param_type))(model::BUGSModel) env = model.evaluation_env field_names = fieldnames($(esc(param_type))) - kwargs = Dict{Symbol, Any}() - + kwargs = Dict{Symbol,Any}() + for field in field_names if haskey(env, field) kwargs[field] = env[field] end end - + return $(esc(param_type))(; kwargs...) end $func_expr diff --git a/test/model_macro.jl b/test/model_macro.jl index 97c816f90..e572df735 100644 --- a/test/model_macro.jl +++ b/test/model_macro.jl @@ -32,11 +32,52 @@ using JuliaBUGS: @parameters, @model end #! format: on - data = JuliaBUGS.BUGSExamples.seeds.data + @test_throws ErrorException begin + #! format: off + @model function seeds( + (; r, b, alpha0, alpha1, alpha2, alpha12)::Tp, x1, x2, N, n + ) + for i in 1:N + r[i] ~ dbin(p[i], n[i]) + b[i] ~ dnorm(0.0, tau) + p[i] = logistic( + alpha0 + alpha1 * x1[i] + alpha2 * x2[i] + alpha12 * x1[i] * x2[i] + b[i] + ) + end + alpha0 ~ dnorm(0.0, 1.0E-6) + alpha1 ~ dnorm(0.0, 1.0E-6) + alpha2 ~ dnorm(0.0, 1.0E-6) + alpha12 ~ dnorm(0.0, 1.0E-6) + tau ~ dgamma(0.001, 0.001) + sigma = 1 / sqrt(tau) + end + #! format: on + end - m = seeds(Tp(), data.x1, data.x2, data.N, data.n) + @test_throws ErrorException begin + #! format: off + @model function seeds( + (; r, b, alpha0, alpha1, alpha2, alpha12, tau)::Tp, x2, N, n + ) + for i in 1:N + r[i] ~ dbin(p[i], n[i]) + b[i] ~ dnorm(0.0, tau) + p[i] = logistic( + alpha0 + alpha1 * x1[i] + alpha2 * x2[i] + alpha12 * x1[i] * x2[i] + b[i] + ) + end + alpha0 ~ dnorm(0.0, 1.0E-6) + alpha1 ~ dnorm(0.0, 1.0E-6) + alpha2 ~ dnorm(0.0, 1.0E-6) + alpha12 ~ dnorm(0.0, 1.0E-6) + tau ~ dgamma(0.001, 0.001) + sigma = 1 / sqrt(tau) + end + #! format: on + end - Tp(m) + data = JuliaBUGS.BUGSExamples.seeds.data + m = seeds(Tp(), data.x1, data.x2, data.N, data.n) @test m isa JuliaBUGS.BUGSModel end From c35e61834a40777dd5849bfaefa3ed30aa58c8bb Mon Sep 17 00:00:00 2001 From: Xianda Sun Date: Fri, 11 Apr 2025 16:34:30 +0100 Subject: [PATCH 04/15] add some documentation, still need editing --- docs/src/julia_syntax.md | 127 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 127 insertions(+) create mode 100644 docs/src/julia_syntax.md diff --git a/docs/src/julia_syntax.md b/docs/src/julia_syntax.md new file mode 100644 index 000000000..e8052c979 --- /dev/null +++ b/docs/src/julia_syntax.md @@ -0,0 +1,127 @@ +# JuliaBUGS Model Syntax + +## Legacy Syntax + +Previously, JuliaBUGS provided a `@bugs` macro that mirrored the traditional BUGS `compile` interface, accepting model definitions as strings or within a `begin...end` block: + +```julia +# Example using string macro (legacy) +@bugs""" +model { + # Priors for regression coefficients + beta0 ~ dnorm(0, 0.001) + beta1 ~ dnorm(0, 0.001) + # Prior for precision (inverse variance) + tau ~ dgamma(0.001, 0.001) + sigma <- 1 / sqrt(tau) + # Likelihood + for (i in 1:N) { + mu[i] <- beta0 + beta1 * x[i] + y[i] ~ dnorm(mu[i], tau) + } +} +""" + +# Example using block macro (legacy) +@bugs begin + # Priors for regression coefficients + beta0 ~ Normal(0, sqrt(1/0.001)) + beta1 ~ Normal(0, sqrt(1/0.001)) + # Prior for precision (inverse variance) + tau ~ Gamma(0.001, 1/0.001) + sigma = 1 / sqrt(tau) + # Likelihood + for i in 1:N + mu[i] = beta0 + beta1 * x[i] + y[i] ~ Normal(mu[i], sqrt(1/tau)) + end +end +``` + +In both legacy cases, the macro returned a Julia AST representation of the model. The `compile` function then took this AST and user-provided data (as a `NamedTuple`) to create a `BUGSModel` instance. While functional, this approach is less idiomatic in Julia compared to defining models within functions. + +In the future, we will only support the first case (using String to define model), and move the latter to a more Julia syntax. (see below) + +## `@model` and `@parameters` + + +### The `@model` Macro + +The `@model` macro transforms a standard Julia function definition into a factory for creating `BUGSModel` instances that compatible with `AbstractMCMC.sample` function. + +```julia +JuliaBUGS.@model function model_definition((;r, b, alpha0, alpha1, alpha2, alpha12, tau)::MyParams, x1, x2, N, n) + + for i in 1:N + r[i] ~ dbin(p[i], n[i]) + b[i] ~ dnorm(0.0, tau) + p[i] = logistic(alpha0 + alpha1 * x1[i] + alpha2 * x2[i] + alpha12 * x1[i] * x2[i] + b[i]) + end + + alpha0 ~ dnorm(0.0, 1.0E-6) + alpha1 ~ dnorm(0.0, 1.0E-6) + alpha2 ~ dnorm(0.0, 1.0E-6) + alpha12 ~ dnorm(0.0, 1.0E-6) + tau ~ dgamma(0.001, 0.001) + + sigma = 1 / sqrt(tau) +end +``` + +**Function Signature:** + +The `@model` macro expects a specific function signature: + +1. **First Argument (Parameters):** This argument **must** declare the model's stochastic parameters (variables defined using `~`) using destructuring assignment (e.g., `(; param1, param2)`). + * **Explicit Declaration:** This explicit declaration of parameters is a design choice. Although JuliaBUGS compiler can determine these variables. We still ask user to specify them to be explicit. + * **Type Annotation (Optional but Recommended):** You can provide a type annotation for the parameters (e.g., `(; r, b, ...)::MyParams`). If you do, and if `MyParams` is defined using `@parameters` (see below), the macro automatically defines a constructor `MyParams(model::BUGSModel)`. This allows easy extraction of fitted parameter values from a `BUGSModel` object back into your structured type. + * **`NamedTuple` Alternative:** You can use a `NamedTuple` type annotation or no annotation. However, managing parameter placeholders or default values might require more manual effort compared to using `@parameters`. + +2. **Subsequent Arguments (Constants/Data):** These arguments declare fixed data, constants required by the model logic (e.g., `x1, x2, N, n`). These are variables that used on RHS, but not appeared on the LHS, which are required to compile the model and sample from prior + +**Validation:** + +The macro performs validation checks: + +* It ensures that all variables read within the function body but *not* assigned via `~` or `=` (i.e., constants or data) are included as arguments *after* the first parameter argument. +* It verifies that all stochastic parameters (assigned via `~`) are listed in the destructuring assignment of the first argument. + +**Model Generation:** + +The function created by `@model` acts as a model factory. When you call this function with: + +1. A parameter object (an instance of the struct defined via `@parameters` or a compatible `NamedTuple`). +2. The required constants/data. + +It performs the following steps: + +1. Combines the provided constants and any concrete values from the parameter object into the data structure needed by the BUGS engine. +2. Parses the model logic defined within the function body. +3. Calls the internal `JuliaBUGS.compile` function. +4. Returns a ready-to-use `BUGSModel` object, suitable for sampling with MCMC algorithms (e.g., via `AbstractMCMC.jl`). + +### The `@parameters` Macro + +The `@parameters` macro simplifies the creation of mutable structs intended to hold model parameters, designed to work seamlessly with `@model`. + +```julia +# Example defining a parameter struct +JuliaBUGS.@parameters struct MyParams + r + b + alpha0 + alpha1 + alpha2 + alpha12 + tau + # sigma is derived, not a parameter defined with ~ +end +``` + +**Features:** + +* **Keyword-Based Construction:** Uses `Base.@kwdef`, allowing easy instantiation with keyword arguments (e.g., `MyParams(alpha0=0.0, tau=1.0)`). If constructed without arguments (e.g., `MyParams()`), fields are initialized with placeholders. Providing initial values can be useful for setting starting points for sampling. +* **Default Placeholders:** By default, fields are initialized with `JuliaBUGS.ParameterPlaceholder()`. This allows creating parameter struct instances even without initial values, which is necessary during the model compilation phase when only the structure, not the values, might be known. The concrete types and sizes of placeholder parameters are determined during the `compile` step when the model function is called with constants. +* **Convenient Instantiation from Model:** As mentioned, if a `@parameters` struct is used as the type annotation in `@model`, a constructor `MyParams(model::BUGSModel)` is automatically generated, simplifying the extraction of results post-inference. +* **Clear Display:** Includes a custom `Base.show` method that indicates whether fields hold concrete values or placeholders, aiding inspection and debugging. + From 58085d357c452909519b0ccfea6607ef07dfd660 Mon Sep 17 00:00:00 2001 From: Xianda Sun Date: Fri, 11 Apr 2025 16:34:54 +0100 Subject: [PATCH 05/15] use MacroTools.@q instead of quote --- src/model_macro.jl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/model_macro.jl b/src/model_macro.jl index c6cc0d304..88acd58ec 100644 --- a/src/model_macro.jl +++ b/src/model_macro.jl @@ -75,7 +75,7 @@ function _generate_struct_definition(struct_name, struct_fields) end end - return quote + return MacroTools.@q begin Base.@kwdef struct $(esc(struct_name)) $(map(f -> :($(esc(f)) = ParameterPlaceholder()), struct_fields)...) end From 4011c1a228f5521ec09bf05b29dab4ac28fb384f Mon Sep 17 00:00:00 2001 From: Xianda Sun Date: Sun, 13 Apr 2025 08:56:53 +0100 Subject: [PATCH 06/15] improve the macos and doc --- docs/src/julia_syntax.md | 209 +++++++++++++++++++++++++-------------- src/model_macro.jl | 34 ++++++- 2 files changed, 165 insertions(+), 78 deletions(-) diff --git a/docs/src/julia_syntax.md b/docs/src/julia_syntax.md index e8052c979..2096f6735 100644 --- a/docs/src/julia_syntax.md +++ b/docs/src/julia_syntax.md @@ -1,112 +1,104 @@ -# JuliaBUGS Model Syntax +# How to Specify and Create a `BUGSModel` -## Legacy Syntax +It takes a BUGS program and the values of some variables to specify a BUGS model. +Before we move on, it is instructional to explain and distinguish between different kinds of values one can give to the JuliaBUGS compiler. +* constants: values used in loop bounds, variables used to resolve indices + * these values are required to specify the model, the former decides the size of the model (how many variables) and the latter is part of the process of determine the edges +* independent variables (features, predictors, covariances): these are variables required for forward simulation of the model +* observations: values of stochastic variables, these are not necessary to specify the model, but if there are provided, they will be data that the conditioned model is conditioned on. (exception is that, one can use stochastic variables to add to the log density, but this is not strictly generative modeling). +* initialization: these are points for the MCMC to start, some models, for instance poorly specified models or model with wide priors might require carefully picked initialization values to run. -Previously, JuliaBUGS provided a `@bugs` macro that mirrored the traditional BUGS `compile` interface, accepting model definitions as strings or within a `begin...end` block: +## Syntax from previous BUGS softwares and their R packages + +Previously, users ues BUGS language through the software interface. Which on a high level, comprised of following steps: +1. write the model in a text file +2. check the model (parsing) +3. compile the model with the program text and data +4. (optional) initialize the sampling process + +Because the R interface packages rely on the BUGS softwares, their interface, albeit in a pure text-based (R program) interface, closely mimic the software interfaces. + +Until now, JuliaBUGS has inherent this interface. Partially because the interface is intuitive enough, and we want to provide users with previous BUGS experience a familiar interface. To be explicit, JuliaBUGS provided a `@bugs` macro that accepts model definitions as strings or within a `begin...end` block: ```julia -# Example using string macro (legacy) +# Example using string macro @bugs""" model { - # Priors for regression coefficients - beta0 ~ dnorm(0, 0.001) - beta1 ~ dnorm(0, 0.001) - # Prior for precision (inverse variance) - tau ~ dgamma(0.001, 0.001) - sigma <- 1 / sqrt(tau) - # Likelihood - for (i in 1:N) { - mu[i] <- beta0 + beta1 * x[i] - y[i] ~ dnorm(mu[i], tau) - } + for( i in 1 : N ) { + r[i] ~ dbin(p[i],n[i]) + b[i] ~ dnorm(0.0,tau) + logit(p[i]) <- alpha0 + alpha1 * x1[i] + alpha2 * x2[i] + + alpha12 * x1[i] * x2[i] + b[i] + } + alpha0 ~ dnorm(0.0,1.0E-6) + alpha1 ~ dnorm(0.0,1.0E-6) + alpha2 ~ dnorm(0.0,1.0E-6) + alpha12 ~ dnorm(0.0,1.0E-6) + tau ~ dgamma(0.001,0.001) + sigma <- 1 / sqrt(tau) } """ -# Example using block macro (legacy) +# Example using block macro @bugs begin - # Priors for regression coefficients - beta0 ~ Normal(0, sqrt(1/0.001)) - beta1 ~ Normal(0, sqrt(1/0.001)) - # Prior for precision (inverse variance) - tau ~ Gamma(0.001, 1/0.001) - sigma = 1 / sqrt(tau) - # Likelihood for i in 1:N - mu[i] = beta0 + beta1 * x[i] - y[i] ~ Normal(mu[i], sqrt(1/tau)) + r[i] ~ dbin(p[i], n[i]) + b[i] ~ dnorm(0.0, tau) + p[i] = logistic(alpha0 + alpha1 * x1[i] + alpha2 * x2[i] + alpha12 * x1[i] * x2[i] + + b[i]) end + alpha0 ~ dnorm(0.0, 1.0e-6) + alpha1 ~ dnorm(0.0, 1.0e-6) + alpha2 ~ dnorm(0.0, 1.0e-6) + alpha12 ~ dnorm(0.0, 1.0e-6) + tau ~ dgamma(0.001, 0.001) + sigma = 1 / sqrt(tau) end ``` -In both legacy cases, the macro returned a Julia AST representation of the model. The `compile` function then took this AST and user-provided data (as a `NamedTuple`) to create a `BUGSModel` instance. While functional, this approach is less idiomatic in Julia compared to defining models within functions. +In both cases, the macro returned a Julia AST representation of the model. The `compile` function then took this AST and user-provided values (as a `NamedTuple`) to create a `BUGSModel` instance. -In the future, we will only support the first case (using String to define model), and move the latter to a more Julia syntax. (see below) +We still want to preserve this interface for users with previous BUGS experiences. But at the same time, we also want to provide an interface that's more idiomatic in Julia. -## `@model` and `@parameters` +## The interface +We take heavy inspiration from Turing.jl's model macro syntax. +The `model` macro creates a "model creating function", which takes some input and returns a model upon which many operations are defined. +The operations includes `AbstractMCMC.sample` and functions like `condition`. ### The `@model` Macro -The `@model` macro transforms a standard Julia function definition into a factory for creating `BUGSModel` instances that compatible with `AbstractMCMC.sample` function. - ```julia -JuliaBUGS.@model function model_definition((;r, b, alpha0, alpha1, alpha2, alpha12, tau)::MyParams, x1, x2, N, n) - +JuliaBUGS.@model function model_definition((;r, b, alpha0, alpha1, alpha2, alpha12, tau)::Params, x1, x2, N, n) for i in 1:N r[i] ~ dbin(p[i], n[i]) b[i] ~ dnorm(0.0, tau) p[i] = logistic(alpha0 + alpha1 * x1[i] + alpha2 * x2[i] + alpha12 * x1[i] * x2[i] + b[i]) end - alpha0 ~ dnorm(0.0, 1.0E-6) alpha1 ~ dnorm(0.0, 1.0E-6) alpha2 ~ dnorm(0.0, 1.0E-6) alpha12 ~ dnorm(0.0, 1.0E-6) tau ~ dgamma(0.001, 0.001) - sigma = 1 / sqrt(tau) end ``` -**Function Signature:** - -The `@model` macro expects a specific function signature: - -1. **First Argument (Parameters):** This argument **must** declare the model's stochastic parameters (variables defined using `~`) using destructuring assignment (e.g., `(; param1, param2)`). - * **Explicit Declaration:** This explicit declaration of parameters is a design choice. Although JuliaBUGS compiler can determine these variables. We still ask user to specify them to be explicit. - * **Type Annotation (Optional but Recommended):** You can provide a type annotation for the parameters (e.g., `(; r, b, ...)::MyParams`). If you do, and if `MyParams` is defined using `@parameters` (see below), the macro automatically defines a constructor `MyParams(model::BUGSModel)`. This allows easy extraction of fitted parameter values from a `BUGSModel` object back into your structured type. - * **`NamedTuple` Alternative:** You can use a `NamedTuple` type annotation or no annotation. However, managing parameter placeholders or default values might require more manual effort compared to using `@parameters`. - -2. **Subsequent Arguments (Constants/Data):** These arguments declare fixed data, constants required by the model logic (e.g., `x1, x2, N, n`). These are variables that used on RHS, but not appeared on the LHS, which are required to compile the model and sample from prior - -**Validation:** - -The macro performs validation checks: - -* It ensures that all variables read within the function body but *not* assigned via `~` or `=` (i.e., constants or data) are included as arguments *after* the first parameter argument. -* It verifies that all stochastic parameters (assigned via `~`) are listed in the destructuring assignment of the first argument. - -**Model Generation:** +The `model` macro expects a specific function signature: -The function created by `@model` acts as a model factory. When you call this function with: +The first argument **must** declare the model's stochastic parameters (variables defined using `~`) using destructuring assignment (e.g., `(; param1, param2)`). +all stochastic parameters (assigned via `~`) are listed in the destructuring assignment of the first argument. +We encourage users to provide a type annotation for the parameters (e.g., `(; r, b, ...)::Params`). +If you do, and if `Params` is defined using `@parameters` (see below), the macro automatically defines a constructor `MyParams(model::BUGSModel)`. This allows easy extraction of fitted parameter values from a `BUGSModel` object back into your structured type. +It is also possible to use a `NamedTuple` type annotation or no annotation. +However, user would need to create the NamedTuple with `ParameterPlaceholder` or Array (possibly of `missing`s), instead of the automatically generated constructors (see below). -1. A parameter object (an instance of the struct defined via `@parameters` or a compatible `NamedTuple`). -2. The required constants/data. +The rest of the arguments need to give all the constants and independent variables required by the model logic (e.g., `x1, x2, N, n`). These are variables that used on RHS, but not appeared on the LHS, which are required to compile the model and sample from prior. -It performs the following steps: - -1. Combines the provided constants and any concrete values from the parameter object into the data structure needed by the BUGS engine. -2. Parses the model logic defined within the function body. -3. Calls the internal `JuliaBUGS.compile` function. -4. Returns a ready-to-use `BUGSModel` object, suitable for sampling with MCMC algorithms (e.g., via `AbstractMCMC.jl`). - -### The `@parameters` Macro - -The `@parameters` macro simplifies the creation of mutable structs intended to hold model parameters, designed to work seamlessly with `@model`. +Like mentioned before, The `@parameters` macro simplifies the creation of mutable structs intended to hold model parameters, designed to work seamlessly with `@model`. ```julia -# Example defining a parameter struct -JuliaBUGS.@parameters struct MyParams +JuliaBUGS.@parameters struct Params r b alpha0 @@ -114,14 +106,83 @@ JuliaBUGS.@parameters struct MyParams alpha2 alpha12 tau - # sigma is derived, not a parameter defined with ~ end ``` -**Features:** +The macro is simple -- it `Base.@kwdef` to allowing easy instantiation with keyword arguments. particularly, it creates a constructor that takes no argument. +In this case, all the field will be default to `JuliaBUGS.ParameterPlaceholder`. +Alternatively, one can also use Arrays of `missing`s if the variable is not observation. +The concrete types and sizes of placeholder parameters are determined during the `compile` step when the model function is called with constants. +A cosntructor `Params(::BUGSModel)` is created. + +### Example + +```julia +julia> @model function seeds( + (; r, b, alpha0, alpha1, alpha2, alpha12, tau)::Params, x1, x2, N, n + ) + for i in 1:N + r[i] ~ dbin(p[i], n[i]) + b[i] ~ dnorm(0.0, tau) + p[i] = logistic( + alpha0 + alpha1 * x1[i] + alpha2 * x2[i] + alpha12 * x1[i] * x2[i] + b[i] + ) + end + alpha0 ~ dnorm(0.0, 1.0E-6) + alpha1 ~ dnorm(0.0, 1.0E-6) + alpha2 ~ dnorm(0.0, 1.0E-6) + alpha12 ~ dnorm(0.0, 1.0E-6) + tau ~ dgamma(0.001, 0.001) + sigma = 1 / sqrt(tau) + end +seeds (generic function with 1 method) + +julia> (; x1, x2, N, n) = JuliaBUGS.BUGSExamples.seeds.data; # extract data from existing BUGS example + +julia> @parameters struct Params + r + b + alpha0 + alpha1 + alpha2 + alpha12 + tau + end -* **Keyword-Based Construction:** Uses `Base.@kwdef`, allowing easy instantiation with keyword arguments (e.g., `MyParams(alpha0=0.0, tau=1.0)`). If constructed without arguments (e.g., `MyParams()`), fields are initialized with placeholders. Providing initial values can be useful for setting starting points for sampling. -* **Default Placeholders:** By default, fields are initialized with `JuliaBUGS.ParameterPlaceholder()`. This allows creating parameter struct instances even without initial values, which is necessary during the model compilation phase when only the structure, not the values, might be known. The concrete types and sizes of placeholder parameters are determined during the `compile` step when the model function is called with constants. -* **Convenient Instantiation from Model:** As mentioned, if a `@parameters` struct is used as the type annotation in `@model`, a constructor `MyParams(model::BUGSModel)` is automatically generated, simplifying the extraction of results post-inference. -* **Clear Display:** Includes a custom `Base.show` method that indicates whether fields hold concrete values or placeholders, aiding inspection and debugging. +julia> m = seeds(Params(), x1, x2, N, n) +BUGSModel (parameters are in transformed (unconstrained) space, with dimension 47): + Model parameters: + alpha2 + b[21], b[20], b[19], b[18], b[17], b[16], b[15], b[14], b[13], b[12], b[11], b[10], b[9], b[8], b[7], b[6], b[5], b[4], b[3], b[2], b[1] + r[21], r[20], r[19], r[18], r[17], r[16], r[15], r[14], r[13], r[12], r[11], r[10], r[9], r[8], r[7], r[6], r[5], r[4], r[3], r[2], r[1] + tau + alpha12 + alpha1 + alpha0 + + Variable sizes and types: + b: size = (21,), type = Vector{Float64} + p: size = (21,), type = Vector{Float64} + n: size = (21,), type = Vector{Int64} + alpha2: type = Float64 + sigma: type = Float64 + alpha12: type = Float64 + alpha0: type = Float64 + N: type = Int64 + tau: type = Float64 + alpha1: type = Float64 + r: size = (21,), type = Vector{Float64} + x1: size = (21,), type = Vector{Int64} + x2: size = (21,), type = Vector{Int64} + +julia> Params(m) +Params: + r = [0.0, 0.0, 0.0, 0.0, 39.0, 0.0, 0.0, 72.0, 0.0, 0.0 … 0.0, 0.0, 0.0, 0.0, 4.0, 12.0, 0.0, 0.0, 0.0, 0.0] + b = [-Inf, -Inf, -Inf, -Inf, Inf, -Inf, -Inf, Inf, -Inf, -Inf … -Inf, -Inf, -Inf, -Inf, Inf, Inf, -Inf, -Inf, -Inf, -Inf] + alpha0 = -1423.52 + alpha1 = 1981.99 + alpha2 = -545.664 + alpha12 = 1338.25 + tau = 0.0 +``` diff --git a/src/model_macro.jl b/src/model_macro.jl index 88acd58ec..71dd5af27 100644 --- a/src/model_macro.jl +++ b/src/model_macro.jl @@ -81,14 +81,18 @@ function _generate_struct_definition(struct_name, struct_fields) end $(show_method_expr) + + function $(esc(struct_name))(model::BUGSModel) + return getparams($(esc(struct_name)), model) + end end end macro model(model_function_expr) - return _generate_model_definition(model_function_expr, __source__) + return _generate_model_definition(model_function_expr, __source__, __module__) end -function _generate_model_definition(model_function_expr, __source__) +function _generate_model_definition(model_function_expr, __source__, __module__) MacroTools.@capture( #! format: off model_function_expr, @@ -112,6 +116,29 @@ function _generate_model_definition(model_function_expr, __source__) ), )) + illegal_constant_variables = Any[] + constant_variables_symbols = map(constant_variables) do constant_variable + if constant_variable isa Symbol + return constant_variable + elseif MacroTools.@capture( + constant_variable, ((name_ = default_value_) | (name_::type_)) + ) + return name_ + else + push!(illegal_constant_variables, constant_variable) + end + end + if !isempty(illegal_constant_variables) + formatted_vars = join(illegal_constant_variables, ", ", " and ") + return MacroTools.@q error( + string( + "The following arguments are not supported syntax for the model function currently: ", + $(QuoteNode(formatted_vars)), + "Please report this issue at https://github.com/TuringLang/JuliaBUGS.jl/issues", + ), + ) + end + vars_and_numdims = extract_variable_names_and_numdims(bugs_ast) vars_assigned_to = extract_variables_assigned_to(bugs_ast) stochastic_vars = [vars_assigned_to[2]..., vars_assigned_to[4]...] @@ -159,8 +186,7 @@ function _generate_model_definition(model_function_expr, __source__) return func_expr else return MacroTools.@q begin - # Create a constructor for the parameter type that uses values from the model's evaluation environment - function $(esc(param_type))(model::BUGSModel) + function JuliaBUGS.getparams($(esc(param_type)), model::BUGSModel) env = model.evaluation_env field_names = fieldnames($(esc(param_type))) kwargs = Dict{Symbol,Any}() From 03e91c50aebdad3f21d679e2da4ea329f1dfdb5c Mon Sep 17 00:00:00 2001 From: Xianda Sun Date: Mon, 14 Apr 2025 11:49:43 +0100 Subject: [PATCH 07/15] improve the writing --- docs/src/julia_syntax.md | 69 ++++++++++++++++++++-------------------- 1 file changed, 34 insertions(+), 35 deletions(-) diff --git a/docs/src/julia_syntax.md b/docs/src/julia_syntax.md index 2096f6735..c2858149f 100644 --- a/docs/src/julia_syntax.md +++ b/docs/src/julia_syntax.md @@ -1,24 +1,33 @@ # How to Specify and Create a `BUGSModel` -It takes a BUGS program and the values of some variables to specify a BUGS model. -Before we move on, it is instructional to explain and distinguish between different kinds of values one can give to the JuliaBUGS compiler. -* constants: values used in loop bounds, variables used to resolve indices - * these values are required to specify the model, the former decides the size of the model (how many variables) and the latter is part of the process of determine the edges -* independent variables (features, predictors, covariances): these are variables required for forward simulation of the model -* observations: values of stochastic variables, these are not necessary to specify the model, but if there are provided, they will be data that the conditioned model is conditioned on. (exception is that, one can use stochastic variables to add to the log density, but this is not strictly generative modeling). -* initialization: these are points for the MCMC to start, some models, for instance poorly specified models or model with wide priors might require carefully picked initialization values to run. +Creating a `BUGSModel` requires two key components: a BUGS program that defines the model structure and values for specific variables that parameterize the model. + +To understand how to specify a model properly, it is important to distinguish between the different types of values you can provide to the JuliaBUGS compiler: + +* **Constants**: Values used in loop bounds and index resolution + * These are essential for model specification as they determine the model's dimensionality (how many variables are created) and establish the dependency structure between variables + +* **Independent variables** (also called features, predictors, or covariates): Non-stochastic inputs required for forward simulation of the model + * Examples include predictor variables in a regression model or time points in a time series model + +* **Observations**: Values for stochastic variables that you wish to condition on + * These are not necessary to specify the model structure, but when provided, they become the data that your model is conditioned on + * (Note: In some advanced cases, stochastic variables can contribute to the log density without being part of a strictly generative model) + +* **Initialization values**: Starting points for MCMC sampling + * While optional in many cases, some models (particularly those with weakly informative priors or complex structures) require carefully chosen initialization values for effective sampling ## Syntax from previous BUGS softwares and their R packages -Previously, users ues BUGS language through the software interface. Which on a high level, comprised of following steps: -1. write the model in a text file -2. check the model (parsing) -3. compile the model with the program text and data -4. (optional) initialize the sampling process +Traditionally, BUGS models were created through a software interface following these steps: +1. Write the model in a text file +2. Check the model syntax (parsing) +3. Compile the model with program text and data +4. Initialize the sampling process (optional) -Because the R interface packages rely on the BUGS softwares, their interface, albeit in a pure text-based (R program) interface, closely mimic the software interfaces. +R interface packages for BUGS maintained this workflow pattern through text-based interfaces that closely mirrored the original software. -Until now, JuliaBUGS has inherent this interface. Partially because the interface is intuitive enough, and we want to provide users with previous BUGS experience a familiar interface. To be explicit, JuliaBUGS provided a `@bugs` macro that accepts model definitions as strings or within a `begin...end` block: +JuliaBUGS initially adopted this familiar workflow to accommodate users with prior BUGS experience. Specifically, JuliaBUGS provides a `@bugs` macro that accepts model definitions either as strings or within a `begin...end` block: ```julia # Example using string macro @@ -56,15 +65,13 @@ model { end ``` -In both cases, the macro returned a Julia AST representation of the model. The `compile` function then took this AST and user-provided values (as a `NamedTuple`) to create a `BUGSModel` instance. +In both cases, the macro returns a Julia AST representation of the model. The `compile` function then takes this AST and user-provided values (as a `NamedTuple`) to create a `BUGSModel` instance. -We still want to preserve this interface for users with previous BUGS experiences. But at the same time, we also want to provide an interface that's more idiomatic in Julia. +While we maintain this interface for compatibility, we now also offer a more idiomatic Julia approach. -## The interface +## The Interface -We take heavy inspiration from Turing.jl's model macro syntax. -The `model` macro creates a "model creating function", which takes some input and returns a model upon which many operations are defined. -The operations includes `AbstractMCMC.sample` and functions like `condition`. +JuliaBUGS provides a Julian interface inspired by Turing.jl's model macro syntax. The `@model` macro creates a "model creating function" that returns a model object supporting operations like `AbstractMCMC.sample` (which samples MCMC chains) and `condition` (which modifies the model by incorporating observations). ### The `@model` Macro @@ -84,18 +91,14 @@ JuliaBUGS.@model function model_definition((;r, b, alpha0, alpha1, alpha2, alpha end ``` -The `model` macro expects a specific function signature: - -The first argument **must** declare the model's stochastic parameters (variables defined using `~`) using destructuring assignment (e.g., `(; param1, param2)`). -all stochastic parameters (assigned via `~`) are listed in the destructuring assignment of the first argument. -We encourage users to provide a type annotation for the parameters (e.g., `(; r, b, ...)::Params`). -If you do, and if `Params` is defined using `@parameters` (see below), the macro automatically defines a constructor `MyParams(model::BUGSModel)`. This allows easy extraction of fitted parameter values from a `BUGSModel` object back into your structured type. -It is also possible to use a `NamedTuple` type annotation or no annotation. -However, user would need to create the NamedTuple with `ParameterPlaceholder` or Array (possibly of `missing`s), instead of the automatically generated constructors (see below). +The `@model` macro requires a specific function signature: -The rest of the arguments need to give all the constants and independent variables required by the model logic (e.g., `x1, x2, N, n`). These are variables that used on RHS, but not appeared on the LHS, which are required to compile the model and sample from prior. +1. The first argument must declare stochastic parameters (variables defined with `~`) using destructuring assignment with the format `(; param1, param2, ...)`. +2. We recommend providing a type annotation (e.g., `(; r, b, ...)::Params`). If `Params` is defined using `@parameters`, the macro automatically defines a constructor `Params(model::BUGSModel)` for extracting parameter values from the model. +3. Alternatively, you can use a `NamedTuple` instead of a custom type. In this case, no type annotation is needed, but you would need to manually create a `NamedTuple` with `ParameterPlaceholder()` values or arrays of `missing` values for parameters that don't have observations. +4. The remaining arguments must specify all constants and independent variables required by the model (variables used on the RHS but not on the LHS). -Like mentioned before, The `@parameters` macro simplifies the creation of mutable structs intended to hold model parameters, designed to work seamlessly with `@model`. +The `@parameters` macro simplifies creating structs to hold model parameters: ```julia JuliaBUGS.@parameters struct Params @@ -109,11 +112,7 @@ JuliaBUGS.@parameters struct Params end ``` -The macro is simple -- it `Base.@kwdef` to allowing easy instantiation with keyword arguments. particularly, it creates a constructor that takes no argument. -In this case, all the field will be default to `JuliaBUGS.ParameterPlaceholder`. -Alternatively, one can also use Arrays of `missing`s if the variable is not observation. -The concrete types and sizes of placeholder parameters are determined during the `compile` step when the model function is called with constants. -A cosntructor `Params(::BUGSModel)` is created. +This macro applies `Base.@kwdef` to enable keyword initialization and creates a no-argument constructor. By default, fields are initialized to `JuliaBUGS.ParameterPlaceholder`. The concrete types and sizes of parameters are determined during compilation when the model function is called with constants. A constructor `Params(::BUGSModel)` is created for easy extraction of parameter values. ### Example From 331a1f9438f674fdfa88ded2393547cc0fe573ba Mon Sep 17 00:00:00 2001 From: Xianda Sun <5433119+sunxd3@users.noreply.github.com> Date: Tue, 15 Apr 2025 09:15:15 +0100 Subject: [PATCH 08/15] Apply suggestions from code review Co-authored-by: Markus Hauru --- test/model_macro.jl | 2 ++ 1 file changed, 2 insertions(+) diff --git a/test/model_macro.jl b/test/model_macro.jl index e572df735..59de50464 100644 --- a/test/model_macro.jl +++ b/test/model_macro.jl @@ -32,6 +32,7 @@ using JuliaBUGS: @parameters, @model end #! format: on + # Try destructuring the random variables but forgetting to include one (tau). @test_throws ErrorException begin #! format: off @model function seeds( @@ -54,6 +55,7 @@ using JuliaBUGS: @parameters, @model #! format: on end + # Try leaving out one constant variable. @test_throws ErrorException begin #! format: off @model function seeds( From 01d1831abe500f08d2b8179b40e51b452780b1ef Mon Sep 17 00:00:00 2001 From: Xianda Sun Date: Tue, 15 Apr 2025 09:16:44 +0100 Subject: [PATCH 09/15] improve naming per Penny's suggestion --- docs/src/julia_syntax.md | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/docs/src/julia_syntax.md b/docs/src/julia_syntax.md index c2858149f..1fb2ecf98 100644 --- a/docs/src/julia_syntax.md +++ b/docs/src/julia_syntax.md @@ -76,7 +76,7 @@ JuliaBUGS provides a Julian interface inspired by Turing.jl's model macro syntax ### The `@model` Macro ```julia -JuliaBUGS.@model function model_definition((;r, b, alpha0, alpha1, alpha2, alpha12, tau)::Params, x1, x2, N, n) +JuliaBUGS.@model function model_definition((;r, b, alpha0, alpha1, alpha2, alpha12, tau)::SeedsParams, x1, x2, N, n) for i in 1:N r[i] ~ dbin(p[i], n[i]) b[i] ~ dnorm(0.0, tau) @@ -94,14 +94,14 @@ end The `@model` macro requires a specific function signature: 1. The first argument must declare stochastic parameters (variables defined with `~`) using destructuring assignment with the format `(; param1, param2, ...)`. -2. We recommend providing a type annotation (e.g., `(; r, b, ...)::Params`). If `Params` is defined using `@parameters`, the macro automatically defines a constructor `Params(model::BUGSModel)` for extracting parameter values from the model. +2. We recommend providing a type annotation (e.g., `(; r, b, ...)::SeedsParams`). If `SeedsParams` is defined using `@parameters`, the macro automatically defines a constructor `SeedsParams(model::BUGSModel)` for extracting parameter values from the model. 3. Alternatively, you can use a `NamedTuple` instead of a custom type. In this case, no type annotation is needed, but you would need to manually create a `NamedTuple` with `ParameterPlaceholder()` values or arrays of `missing` values for parameters that don't have observations. 4. The remaining arguments must specify all constants and independent variables required by the model (variables used on the RHS but not on the LHS). The `@parameters` macro simplifies creating structs to hold model parameters: ```julia -JuliaBUGS.@parameters struct Params +JuliaBUGS.@parameters struct SeedsParams r b alpha0 @@ -112,13 +112,13 @@ JuliaBUGS.@parameters struct Params end ``` -This macro applies `Base.@kwdef` to enable keyword initialization and creates a no-argument constructor. By default, fields are initialized to `JuliaBUGS.ParameterPlaceholder`. The concrete types and sizes of parameters are determined during compilation when the model function is called with constants. A constructor `Params(::BUGSModel)` is created for easy extraction of parameter values. +This macro applies `Base.@kwdef` to enable keyword initialization and creates a no-argument constructor. By default, fields are initialized to `JuliaBUGS.ParameterPlaceholder`. The concrete types and sizes of parameters are determined during compilation when the model function is called with constants. A constructor `SeedsParams(::BUGSModel)` is created for easy extraction of parameter values. ### Example ```julia julia> @model function seeds( - (; r, b, alpha0, alpha1, alpha2, alpha12, tau)::Params, x1, x2, N, n + (; r, b, alpha0, alpha1, alpha2, alpha12, tau)::SeedsParams, x1, x2, N, n ) for i in 1:N r[i] ~ dbin(p[i], n[i]) @@ -138,7 +138,7 @@ seeds (generic function with 1 method) julia> (; x1, x2, N, n) = JuliaBUGS.BUGSExamples.seeds.data; # extract data from existing BUGS example -julia> @parameters struct Params +julia> @parameters struct SeedsParams r b alpha0 @@ -148,7 +148,7 @@ julia> @parameters struct Params tau end -julia> m = seeds(Params(), x1, x2, N, n) +julia> m = seeds(SeedsParams(), x1, x2, N, n) BUGSModel (parameters are in transformed (unconstrained) space, with dimension 47): Model parameters: @@ -175,8 +175,8 @@ BUGSModel (parameters are in transformed (unconstrained) space, with dimension 4 x1: size = (21,), type = Vector{Int64} x2: size = (21,), type = Vector{Int64} -julia> Params(m) -Params: +julia> SeedsParams(m) +SeedsParams: r = [0.0, 0.0, 0.0, 0.0, 39.0, 0.0, 0.0, 72.0, 0.0, 0.0 … 0.0, 0.0, 0.0, 0.0, 4.0, 12.0, 0.0, 0.0, 0.0, 0.0] b = [-Inf, -Inf, -Inf, -Inf, Inf, -Inf, -Inf, Inf, -Inf, -Inf … -Inf, -Inf, -Inf, -Inf, Inf, Inf, -Inf, -Inf, -Inf, -Inf] alpha0 = -1423.52 From bed9b138288dbf1119607e63f8180a64b7124e43 Mon Sep 17 00:00:00 2001 From: Xianda Sun Date: Wed, 30 Apr 2025 07:54:41 +0100 Subject: [PATCH 10/15] use old compile to implement the new compile --- src/JuliaBUGS.jl | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/src/JuliaBUGS.jl b/src/JuliaBUGS.jl index 79c2e3d1f..d3ea22d42 100644 --- a/src/JuliaBUGS.jl +++ b/src/JuliaBUGS.jl @@ -153,6 +153,16 @@ end Compile the model with model definition and data. Optionally, initializations can be provided. If initializations are not provided, values will be sampled from the prior distributions. """ +function compile( + model_str::String, + data::NamedTuple, + initial_params::NamedTuple=NamedTuple(); + replace_period::Bool=true, + no_enclosure::Bool=false, +) + model_def = _bugs_string_input(model_str, replace_period, no_enclosure) + return compile(model_def, data, initial_params) +end function compile(model_def::Expr, data::NamedTuple, initial_params::NamedTuple=NamedTuple()) data = check_input(data) eval_env = semantic_analysis(model_def, data) From 92d7e9fd37ade445177614f4762be8f10f049bbf Mon Sep 17 00:00:00 2001 From: Xianda Sun Date: Wed, 30 Apr 2025 10:20:15 +0100 Subject: [PATCH 11/15] fix error in 1.10 --- src/JuliaBUGS.jl | 71 +++++++++++++++++++++++++++++++++------------ src/model_macro.jl | 3 +- test/model_macro.jl | 32 ++++++++++++++++++-- 3 files changed, 84 insertions(+), 22 deletions(-) diff --git a/src/JuliaBUGS.jl b/src/JuliaBUGS.jl index d3ea22d42..ff7fad346 100644 --- a/src/JuliaBUGS.jl +++ b/src/JuliaBUGS.jl @@ -43,20 +43,30 @@ include("source_gen.jl") include("BUGSExamples/BUGSExamples.jl") function check_input(input::NamedTuple) + valid_pairs = Pair{Symbol,Any}[] for (k, v) in pairs(input) - if v isa AbstractArray - if !(eltype(v) <: Union{Int,Float64,Missing}) + if v === missing + continue # Skip missing values + elseif v isa AbstractArray + # Allow arrays containing Int, Float64, or Missing + allowed_eltypes = Union{Int,Float64,Missing} + if !(eltype(v) <: allowed_eltypes) error( - "For array input, only Int, Float64, or Missing types are supported. Received: $(typeof(v)).", + "For array input '$k', only elements of type $allowed_eltypes are supported. Received array with eltype: $(eltype(v)).", ) end - elseif v === missing - error("Scalars cannot be missing. Received: $k") - elseif !(v isa Union{Int,Float64}) - error("Scalars must be of type Int or Float64. Received: $k") + push!(valid_pairs, k => v) + elseif v isa Union{Int,Float64} + # Allow scalar Int or Float64 + push!(valid_pairs, k => v) + else + # Error for other scalar types + error( + "Scalar input '$k' must be of type Int or Float64. Received: $(typeof(v))." + ) end end - return input + return NamedTuple(valid_pairs) end function check_input(input::Dict{KT,VT}) where {KT,VT} if isempty(input) @@ -153,16 +163,6 @@ end Compile the model with model definition and data. Optionally, initializations can be provided. If initializations are not provided, values will be sampled from the prior distributions. """ -function compile( - model_str::String, - data::NamedTuple, - initial_params::NamedTuple=NamedTuple(); - replace_period::Bool=true, - no_enclosure::Bool=false, -) - model_def = _bugs_string_input(model_str, replace_period, no_enclosure) - return compile(model_def, data, initial_params) -end function compile(model_def::Expr, data::NamedTuple, initial_params::NamedTuple=NamedTuple()) data = check_input(data) eval_env = semantic_analysis(model_def, data) @@ -188,6 +188,41 @@ function compile(model_def::Expr, data::NamedTuple, initial_params::NamedTuple=N return BUGSModel(g, nonmissing_eval_env, model_def, data, initial_params) end +""" + compile(model_str::String, data::NamedTuple[, initial_params::NamedTuple=NamedTuple()]; kwargs...) + +Compile a BUGS model defined by a string, along with provided data and optional initial parameter values. + +This method first parses the BUGS model string into a Julia expression using `JuliaBUGS.Parser.to_julia_program` +and then calls the primary `compile` method. + +# Arguments +- `model_str::String`: A string containing the BUGS model definition. +- `data::NamedTuple`: A NamedTuple mapping variable names (as Symbols) to their corresponding data values. +- `initial_params::NamedTuple=NamedTuple()`: Optional. A NamedTuple providing initial values for model parameters. If not provided, initial values might be sampled from prior distributions or determined by the inference algorithm. + +# Keyword Arguments +- `replace_period::Bool=true`: If `true`, periods (`.`) in BUGS variable names are replaced with underscores (`_`) during parsing. If `false`, periods are retained, and variable names containing periods are wrapped in `var"..."` (e.g., `var"a.b"`). See [`JuliaBUGS.Parser.to_julia_program`](@ref) for details. +- `no_enclosure::Bool=false`: If `true`, the parser does not require the model code to be enclosed within `model { ... }`. See [`JuliaBUGS.Parser.to_julia_program`](@ref) for details. + +# Returns +- `BUGSModel`: A compiled model object ready for inference. + +# See Also +- [`compile(model_def::Expr, data::NamedTuple, initial_params::NamedTuple)`](@ref): The primary compile method taking a Julia expression. +- [`JuliaBUGS.Parser.to_julia_program`](@ref): The function used internally to parse the model string. +""" +function compile( + model_str::String, + data::NamedTuple, + initial_params::NamedTuple=NamedTuple(); + replace_period::Bool=true, + no_enclosure::Bool=false, +) + model_def = _bugs_string_input(model_str, replace_period, no_enclosure) + return compile(model_def, data, initial_params) +end + """ @register_primitive(expr) diff --git a/src/model_macro.jl b/src/model_macro.jl index 71dd5af27..a6a6d681c 100644 --- a/src/model_macro.jl +++ b/src/model_macro.jl @@ -7,6 +7,7 @@ using MacroTools # it captures multiple components into an array. struct ParameterPlaceholder end +const PARAMETER_PLACEHOLDER = ParameterPlaceholder() macro parameters(struct_expr) if MacroTools.@capture(struct_expr, struct struct_name_ @@ -77,7 +78,7 @@ function _generate_struct_definition(struct_name, struct_fields) return MacroTools.@q begin Base.@kwdef struct $(esc(struct_name)) - $(map(f -> :($(esc(f)) = ParameterPlaceholder()), struct_fields)...) + $(map(f -> :($(esc(f)) = JuliaBUGS.PARAMETER_PLACEHOLDER), struct_fields)...) end $(show_method_expr) diff --git a/test/model_macro.jl b/test/model_macro.jl index 59de50464..4c12c5e42 100644 --- a/test/model_macro.jl +++ b/test/model_macro.jl @@ -36,9 +36,10 @@ using JuliaBUGS: @parameters, @model @test_throws ErrorException begin #! format: off @model function seeds( + # tau is missing (; r, b, alpha0, alpha1, alpha2, alpha12)::Tp, x1, x2, N, n ) - for i in 1:N + for i in 1:N r[i] ~ dbin(p[i], n[i]) b[i] ~ dnorm(0.0, tau) p[i] = logistic( @@ -59,9 +60,10 @@ using JuliaBUGS: @parameters, @model @test_throws ErrorException begin #! format: off @model function seeds( + # x1 is missing (; r, b, alpha0, alpha1, alpha2, alpha12, tau)::Tp, x2, N, n ) - for i in 1:N + for i in 1:N r[i] ~ dbin(p[i], n[i]) b[i] ~ dnorm(0.0, tau) p[i] = logistic( @@ -81,5 +83,29 @@ using JuliaBUGS: @parameters, @model data = JuliaBUGS.BUGSExamples.seeds.data m = seeds(Tp(), data.x1, data.x2, data.N, data.n) - @test m isa JuliaBUGS.BUGSModel + # use NamedTuple to pass parameters + # with missing values + N = data.N + params_nt = ( + r = fill(missing, N), + b = fill(missing, N), + alpha0 = missing, + alpha1 = missing, + alpha2 = missing, + alpha12 = missing, + tau = missing, + ) + m = seeds(params_nt, data.x1, data.x2, data.N, data.n) + + params_nt_with_data = ( + r = data.r, + b = JuliaBUGS.PARAMETER_PLACEHOLDER, + alpha0 = JuliaBUGS.PARAMETER_PLACEHOLDER, + alpha1 = JuliaBUGS.PARAMETER_PLACEHOLDER, + alpha2 = JuliaBUGS.PARAMETER_PLACEHOLDER, + alpha12 = JuliaBUGS.PARAMETER_PLACEHOLDER, + tau = JuliaBUGS.PARAMETER_PLACEHOLDER, + ) + m = seeds(params_nt_with_data, data.x1, data.x2, data.N, data.n) + end From 6655e2249e688b7fb80b9ecc28e75ca67888fab7 Mon Sep 17 00:00:00 2001 From: Xianda Sun <5433119+sunxd3@users.noreply.github.com> Date: Wed, 30 Apr 2025 10:22:50 +0100 Subject: [PATCH 12/15] Apply suggestions from code review Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> --- test/model_macro.jl | 29 ++++++++++++++--------------- 1 file changed, 14 insertions(+), 15 deletions(-) diff --git a/test/model_macro.jl b/test/model_macro.jl index 4c12c5e42..2964b1e3e 100644 --- a/test/model_macro.jl +++ b/test/model_macro.jl @@ -87,25 +87,24 @@ using JuliaBUGS: @parameters, @model # with missing values N = data.N params_nt = ( - r = fill(missing, N), - b = fill(missing, N), - alpha0 = missing, - alpha1 = missing, - alpha2 = missing, - alpha12 = missing, - tau = missing, + r=fill(missing, N), + b=fill(missing, N), + alpha0=missing, + alpha1=missing, + alpha2=missing, + alpha12=missing, + tau=missing, ) m = seeds(params_nt, data.x1, data.x2, data.N, data.n) params_nt_with_data = ( - r = data.r, - b = JuliaBUGS.PARAMETER_PLACEHOLDER, - alpha0 = JuliaBUGS.PARAMETER_PLACEHOLDER, - alpha1 = JuliaBUGS.PARAMETER_PLACEHOLDER, - alpha2 = JuliaBUGS.PARAMETER_PLACEHOLDER, - alpha12 = JuliaBUGS.PARAMETER_PLACEHOLDER, - tau = JuliaBUGS.PARAMETER_PLACEHOLDER, + r=data.r, + b=JuliaBUGS.PARAMETER_PLACEHOLDER, + alpha0=JuliaBUGS.PARAMETER_PLACEHOLDER, + alpha1=JuliaBUGS.PARAMETER_PLACEHOLDER, + alpha2=JuliaBUGS.PARAMETER_PLACEHOLDER, + alpha12=JuliaBUGS.PARAMETER_PLACEHOLDER, + tau=JuliaBUGS.PARAMETER_PLACEHOLDER, ) m = seeds(params_nt_with_data, data.x1, data.x2, data.N, data.n) - end From b5dc5c4b8deb304f660cbbdd289540bf66a30aea Mon Sep 17 00:00:00 2001 From: Xianda Sun Date: Wed, 30 Apr 2025 18:07:52 +0100 Subject: [PATCH 13/15] construct kwargs explicitly to be compat with 1.10 --- src/model_macro.jl | 20 +++++++++++++++----- 1 file changed, 15 insertions(+), 5 deletions(-) diff --git a/src/model_macro.jl b/src/model_macro.jl index a6a6d681c..0c18d9e69 100644 --- a/src/model_macro.jl +++ b/src/model_macro.jl @@ -7,13 +7,14 @@ using MacroTools # it captures multiple components into an array. struct ParameterPlaceholder end -const PARAMETER_PLACEHOLDER = ParameterPlaceholder() macro parameters(struct_expr) if MacroTools.@capture(struct_expr, struct struct_name_ struct_fields__ end) - return _generate_struct_definition(struct_name, struct_fields) + return _generate_struct_definition( + struct_name, struct_fields, __source__, __module__ + ) else return :(throw( ArgumentError( @@ -23,7 +24,7 @@ macro parameters(struct_expr) end end -function _generate_struct_definition(struct_name, struct_fields) +function _generate_struct_definition(struct_name, struct_fields, __source__, __module__) if !isa(struct_name, Symbol) return :(throw( ArgumentError( @@ -76,9 +77,18 @@ function _generate_struct_definition(struct_name, struct_fields) end end + kw_assignments = map(f -> Expr(:kw, esc(f), :(ParameterPlaceholder())), struct_fields) + kwarg_constructor_expr = MacroTools.@q function $(esc(struct_name))(; + $(kw_assignments...) + ) + return $(esc(struct_name))($(map(esc, struct_fields)...)) + end return MacroTools.@q begin - Base.@kwdef struct $(esc(struct_name)) - $(map(f -> :($(esc(f)) = JuliaBUGS.PARAMETER_PLACEHOLDER), struct_fields)...) + begin + struct $(esc(struct_name)) + $(map(esc, struct_fields)...) + end + $(kwarg_constructor_expr) end $(show_method_expr) From 6bd42b2d4252cbb2c22cb1096b2b3f2b4cc9b499 Mon Sep 17 00:00:00 2001 From: Xianda Sun Date: Wed, 30 Apr 2025 18:12:23 +0100 Subject: [PATCH 14/15] fix error --- test/model_macro.jl | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/test/model_macro.jl b/test/model_macro.jl index 2964b1e3e..0de6eccee 100644 --- a/test/model_macro.jl +++ b/test/model_macro.jl @@ -99,12 +99,12 @@ using JuliaBUGS: @parameters, @model params_nt_with_data = ( r=data.r, - b=JuliaBUGS.PARAMETER_PLACEHOLDER, - alpha0=JuliaBUGS.PARAMETER_PLACEHOLDER, - alpha1=JuliaBUGS.PARAMETER_PLACEHOLDER, - alpha2=JuliaBUGS.PARAMETER_PLACEHOLDER, - alpha12=JuliaBUGS.PARAMETER_PLACEHOLDER, - tau=JuliaBUGS.PARAMETER_PLACEHOLDER, + b=JuliaBUGS.ParameterPlaceholder(), + alpha0=JuliaBUGS.ParameterPlaceholder(), + alpha1=JuliaBUGS.ParameterPlaceholder(), + alpha2=JuliaBUGS.ParameterPlaceholder(), + alpha12=JuliaBUGS.ParameterPlaceholder(), + tau=JuliaBUGS.ParameterPlaceholder(), ) m = seeds(params_nt_with_data, data.x1, data.x2, data.N, data.n) end From 2b2a1cd320ffc0f3cac8ff5804aca315aed0c4f6 Mon Sep 17 00:00:00 2001 From: Xianda Sun Date: Wed, 30 Apr 2025 18:40:34 +0100 Subject: [PATCH 15/15] remove the new `compile` definition --- src/JuliaBUGS.jl | 45 ++++++++++----------------------------------- 1 file changed, 10 insertions(+), 35 deletions(-) diff --git a/src/JuliaBUGS.jl b/src/JuliaBUGS.jl index ff7fad346..b9b27f31f 100644 --- a/src/JuliaBUGS.jl +++ b/src/JuliaBUGS.jl @@ -187,41 +187,16 @@ function compile(model_def::Expr, data::NamedTuple, initial_params::NamedTuple=N ) return BUGSModel(g, nonmissing_eval_env, model_def, data, initial_params) end - -""" - compile(model_str::String, data::NamedTuple[, initial_params::NamedTuple=NamedTuple()]; kwargs...) - -Compile a BUGS model defined by a string, along with provided data and optional initial parameter values. - -This method first parses the BUGS model string into a Julia expression using `JuliaBUGS.Parser.to_julia_program` -and then calls the primary `compile` method. - -# Arguments -- `model_str::String`: A string containing the BUGS model definition. -- `data::NamedTuple`: A NamedTuple mapping variable names (as Symbols) to their corresponding data values. -- `initial_params::NamedTuple=NamedTuple()`: Optional. A NamedTuple providing initial values for model parameters. If not provided, initial values might be sampled from prior distributions or determined by the inference algorithm. - -# Keyword Arguments -- `replace_period::Bool=true`: If `true`, periods (`.`) in BUGS variable names are replaced with underscores (`_`) during parsing. If `false`, periods are retained, and variable names containing periods are wrapped in `var"..."` (e.g., `var"a.b"`). See [`JuliaBUGS.Parser.to_julia_program`](@ref) for details. -- `no_enclosure::Bool=false`: If `true`, the parser does not require the model code to be enclosed within `model { ... }`. See [`JuliaBUGS.Parser.to_julia_program`](@ref) for details. - -# Returns -- `BUGSModel`: A compiled model object ready for inference. - -# See Also -- [`compile(model_def::Expr, data::NamedTuple, initial_params::NamedTuple)`](@ref): The primary compile method taking a Julia expression. -- [`JuliaBUGS.Parser.to_julia_program`](@ref): The function used internally to parse the model string. -""" -function compile( - model_str::String, - data::NamedTuple, - initial_params::NamedTuple=NamedTuple(); - replace_period::Bool=true, - no_enclosure::Bool=false, -) - model_def = _bugs_string_input(model_str, replace_period, no_enclosure) - return compile(model_def, data, initial_params) -end +# function compile( +# model_str::String, +# data::NamedTuple, +# initial_params::NamedTuple=NamedTuple(); +# replace_period::Bool=true, +# no_enclosure::Bool=false, +# ) +# model_def = _bugs_string_input(model_str, replace_period, no_enclosure) +# return compile(model_def, data, initial_params) +# end """ @register_primitive(expr)