Skip to content

Commit eb1534d

Browse files
authored
Merge pull request #302 from ReactiveBayes/inline_constraints
Add support to specify constraints macros inline for submodels
2 parents 6c2c094 + bcb9295 commit eb1534d

5 files changed

Lines changed: 135 additions & 3 deletions

File tree

docs/src/plugins/constraint_specification.md

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,42 @@ We can specify constraints over the first `toy_model` submodel using the followi
7878
end
7979
```
8080

81+
## Inline constraints on submodel calls
82+
83+
Constraints can also be specified **inline** at the call site of a submodel using the `where` keyword. This is useful when you want to attach constraints to a specific invocation without modifying the outer model's `@constraints` block. For example:
84+
85+
```@example constraints
86+
@model function outer_toy_model(a, b, c)
87+
a ~ toy_model(y = b, z = c) where {
88+
constraints = @constraints begin
89+
q(x, y, z) = q(x, y)q(z)
90+
q(x) :: Normal
91+
end
92+
}
93+
end
94+
```
95+
96+
The `where { constraints = ... }` syntax accepts any constraint set produced by the `@constraints` macro. Constraint sets defined with the `@constraints function` form can also be passed by reference:
97+
98+
```@example constraints
99+
@constraints function my_constraints()
100+
q(x, y, z) = q(x, y)q(z)
101+
q(x) :: Normal
102+
end
103+
104+
@model function outer_toy_model(a, b, c)
105+
a ~ toy_model(y = b, z = c) where { constraints = my_constraints() }
106+
end
107+
```
108+
109+
Inline constraints apply only to the specific submodel invocation they are attached to and propagate to any submodels nested within it. Their priority relative to other constraint sources, from highest to lowest, is:
110+
111+
1. **External constraints** — passed at model creation via `for q in submodel` or `for q in (submodel, index)` blocks.
112+
2. **Inline constraints** — specified with `where { constraints = ... }` at the call site.
113+
3. **Default constraints** — defined via `GraphPPL.default_constraints`.
114+
115+
This means that if external constraints also target the same submodel, they will override the inline constraints.
116+
81117
## Constraints over vector variables
82118

83119
When a model contains vector (or array) latent variables, we can specify factorization constraints over individual elements using the `begin` and `end` indexing syntax. For example, consider a random walk model where latent states `x` are coupled through sequential dependencies:

src/graph_engine.jl

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -452,9 +452,10 @@ struct Context
452452
tensor_variables::UnorderedDictionary{Symbol, ResizableArray{NodeLabel}}
453453
proxies::UnorderedDictionary{Symbol, ProxyLabel}
454454
returnval::Ref{Any}
455+
options::Any
455456
end
456457

457-
function Context(depth::Int, fform::Function, prefix::String, parent)
458+
function Context(depth::Int, fform::Function, prefix::String, parent, options = nothing)
458459
return Context(
459460
depth,
460461
fform,
@@ -467,13 +468,17 @@ function Context(depth::Int, fform::Function, prefix::String, parent)
467468
UnorderedDictionary{Symbol, ResizableArray{NodeLabel, Vector{NodeLabel}, 1}}(),
468469
UnorderedDictionary{Symbol, ResizableArray{NodeLabel}}(),
469470
UnorderedDictionary{Symbol, ProxyLabel}(),
470-
Ref{Any}()
471+
Ref{Any}(),
472+
options
471473
)
472474
end
473475

474476
Context(parent::Context, model_fform::Function) = Context(
475477
parent.depth + 1, model_fform, (parent.prefix == "" ? parent.prefix : parent.prefix * "_") * getname(model_fform), parent
476478
)
479+
Context(parent::Context, model_fform::Function, options) = Context(
480+
parent.depth + 1, model_fform, (parent.prefix == "" ? parent.prefix : parent.prefix * "_") * getname(model_fform), parent, options
481+
)
477482
Context(fform) = Context(0, fform, "", nothing)
478483
Context() = Context(identity)
479484

@@ -660,6 +665,8 @@ Base.getindex(::NodeCreationOptions{Nothing}, keys...) = error("type `NodeCreati
660665
Base.keys(::NodeCreationOptions{Nothing}) = ()
661666
Base.get(::NodeCreationOptions{Nothing}, key::Symbol, default) = default
662667

668+
context_options(context::Context) = something(context.options, EmptyNodeCreationOptions)
669+
663670
withopts(::NodeCreationOptions{Nothing}, options::NamedTuple) = NodeCreationOptions(options)
664671
withopts(options::NodeCreationOptions, extra::NamedTuple) = NodeCreationOptions((; options.options..., extra...))
665672

src/model_macro.jl

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -771,7 +771,7 @@ function get_make_node_function(model_specification, ms_body, ms_args, ms_name)
771771
__n_interfaces__::GraphPPL.StaticInt{$(length(ms_args))}
772772
)
773773
__interfaces__ = GraphPPL.prepare_interfaces(__model__, $ms_name, __lhs_interface__, __rhs_interfaces__)
774-
__context__ = GraphPPL.Context(__parent_context__, $ms_name)
774+
__context__ = GraphPPL.Context(__parent_context__, $ms_name, __options__)
775775
GraphPPL.copy_markov_blanket_to_child_context(__context__, __interfaces__)
776776
GraphPPL.add_composite_factor_node!(__model__, __parent_context__, __context__, $ms_name)
777777
__returnval__ = GraphPPL.add_terminated_submodel!(

src/plugins/variational_constraints/variational_constraints_engine.jl

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1000,10 +1000,13 @@ function apply_constraints!(model::Model, context::Context, constraint_set::Cons
10001000
apply_constraints!(model, context, rfc)
10011001
end
10021002
for (factor_id, child) in pairs(children(context))
1003+
inline_constraints = get(context_options(child), :constraints, nothing)
10031004
if factor_id keys(specific_submodel_constraints(constraint_set))
10041005
apply_constraints!(model, child, getconstraint(specific_submodel_constraints(constraint_set)[factor_id]), stack)
10051006
elseif fform(factor_id) keys(general_submodel_constraints(constraint_set))
10061007
apply_constraints!(model, child, getconstraint(general_submodel_constraints(constraint_set)[fform(child)]), stack)
1008+
elseif inline_constraints !== nothing
1009+
apply_constraints!(model, child, inline_constraints, stack)
10071010
else
10081011
apply_constraints!(model, child, default_constraints(fform(factor_id)), stack)
10091012
end

test/plugins/variational_constraints/variational_constraints_tests.jl

Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1132,6 +1132,92 @@ end
11321132
@test length(collect(filter(as_node(Normal), model))) == 11
11331133
end
11341134

1135+
@testitem "inline constraints on submodel calls" begin
1136+
using Distributions
1137+
import GraphPPL:
1138+
create_model,
1139+
with_plugins,
1140+
getcontext,
1141+
getextra,
1142+
hasextra,
1143+
context_options,
1144+
VariationalConstraintsPlugin,
1145+
PluginsCollection,
1146+
Constraints
1147+
1148+
include("../../testutils.jl")
1149+
1150+
@model function inline_constraints_inner(y, x)
1151+
theta ~ Normal(0.0, 1.0)
1152+
y ~ Normal(x, theta)
1153+
end
1154+
1155+
@model function inline_constraints_middle(y, x)
1156+
y ~ inline_constraints_inner(x = x) where {
1157+
constraints = @constraints begin
1158+
q(x, y, theta) = MeanField()
1159+
end
1160+
}
1161+
end
1162+
1163+
@model function inline_constraints_outer()
1164+
x ~ Normal(0.0, 1.0)
1165+
y ~ inline_constraints_inner(x = x) where {
1166+
constraints = @constraints begin
1167+
q(x, y, theta) = MeanField()
1168+
end
1169+
}
1170+
end
1171+
1172+
@constraints function make_inline_constraints()
1173+
q(x, y, theta) = MeanField()
1174+
end
1175+
1176+
@model function inline_constraints_function_outer()
1177+
x ~ Normal(0.0, 1.0)
1178+
y ~ inline_constraints_inner(x = x) where { constraints = make_inline_constraints() }
1179+
end
1180+
1181+
@model function nested_inline_constraints_outer()
1182+
x ~ Normal(0.0, 1.0)
1183+
y ~ inline_constraints_middle(x = x)
1184+
end
1185+
1186+
model = create_model(with_plugins(inline_constraints_outer(), PluginsCollection(VariationalConstraintsPlugin())))
1187+
context = getcontext(model)
1188+
inner_context = context[inline_constraints_inner, 1]
1189+
node = inner_context[NormalMeanVariance, 2]
1190+
@test get(context_options(inner_context), :constraints, nothing) isa Constraints
1191+
@test hasextra(model[node], :factorization_constraint_indices)
1192+
@test Tuple.(getextra(model[node], :factorization_constraint_indices)) == ((1,), (2,), (3,))
1193+
1194+
model = create_model(with_plugins(inline_constraints_function_outer(), PluginsCollection(VariationalConstraintsPlugin())))
1195+
context = getcontext(model)
1196+
inner_context = context[inline_constraints_inner, 1]
1197+
node = inner_context[NormalMeanVariance, 2]
1198+
@test get(context_options(inner_context), :constraints, nothing) isa Constraints
1199+
@test hasextra(model[node], :factorization_constraint_indices)
1200+
@test Tuple.(getextra(model[node], :factorization_constraint_indices)) == ((1,), (2,), (3,))
1201+
1202+
model = create_model(with_plugins(nested_inline_constraints_outer(), PluginsCollection(VariationalConstraintsPlugin())))
1203+
context = getcontext(model)
1204+
inner_context = context[inline_constraints_middle, 1][inline_constraints_inner, 1]
1205+
node = inner_context[NormalMeanVariance, 2]
1206+
@test hasextra(model[node], :factorization_constraint_indices)
1207+
@test Tuple.(getextra(model[node], :factorization_constraint_indices)) == ((1,), (2,), (3,))
1208+
1209+
external_constraints = @constraints begin
1210+
for q in inline_constraints_inner
1211+
q(x, y, theta) = q(x, y, theta)
1212+
end
1213+
end
1214+
model = create_model(with_plugins(inline_constraints_outer(), PluginsCollection(VariationalConstraintsPlugin(external_constraints))))
1215+
context = getcontext(model)
1216+
node = context[inline_constraints_inner, 1][NormalMeanVariance, 2]
1217+
@test hasextra(model[node], :factorization_constraint_indices)
1218+
@test Tuple.(getextra(model[node], :factorization_constraint_indices)) == ((1, 2, 3),)
1219+
end
1220+
11351221
@testitem "`@constraints` should save the source code #1" begin
11361222
using GraphPPL
11371223

0 commit comments

Comments
 (0)