Migration Guide

This page describes the major changes between GraphPPL v3 and v4. The v4 introduced many changes to the language and the API. The changes are designed to make the language more consistent and easier to use, but also are tested better and provides extra features for the future releases. These changes are, however, not backward compatible with the previous versions of GraphPPL, such as v3. In this guide, we will describe the major changes between the two versions and provide examples to help you migrate your code to the new version.

The @model macro

The @model macro is no longer exported from the library and is only provided for interactive purposes (e.g. plotting). The downstream package must define their own @model macro and use the GraphPPL.model_macro_interior function with a custom backend.

Model definition

Model definition in version 4 is similar to version 3. The main difference is the deletion of the datavar, randomvar and constvar syntax. Previously the random variables and data variables needed to be specified in advance. This is no longer possible but also is unnecessary. GraphPPL is able to infer the type of the variable based on the way in which it is used. This greatly trims down the amount of code to be written in the model definition.

The following example is a simple model definition in version 3:

@model function SSM(n, x0, A, B, Q, P) 
 	 x = randomvar(n) 
 	 y = datavar(Vector{Float64}, n) 
 	 x_prior ~ MvNormal(μ = mean(x0), Σ = cov(x0)) 
 	 x_prev = x_prior 
 	 for i in 1:n 
 		   x[i] ~ MvNormal(μ = A * x_prev, Σ = Q) 
 		   y[i] ~ MvNormal(μ = B * x[i], Σ = P) 
 		   x_prev = x[i] 
 	 end 
 end 

The equivalent model definition in version 4 is as follows:

 @model function SSM(y, prior_x, A, B, Q, P) 
     x_prev ~ prior_x
     for i in eachindex(y)
        x[i] ~ MvNormal(μ = A * x_prev, Σ = Q) 
        y[i] ~ MvNormal(μ = B * x[i], Σ = P) 
        x_prev = x[i]
    end
end

As you can see, variable creation still requires the ~ operator. However, there are a couple of subtle changes compared to the old version of GraphPPL:

  • The randomvar and datavar syntax is no longer needed. GraphPPL is able to infer the type of the variable based on the way in which it is used.
  • The data y is an explicit parameter of the model function.
  • The n parameter is no longer needed. The size of the variable x is inferred from the size of the variable y.
  • We are no longer required to extract the mean and covariance of our prior distribution using the MvNormal(μ = mean(x0), Σ = cov(x0)) pattern. Instead, we can pass a prior and call x_prev ~ prior_x to assign it to an edge in the factor graph.
  • The data y is passed as an argument to the model. This is because of the support of nested models in version 4. In the Nested models we elaborate more on this design choice.

Vectors and arrays

As seen in the example above, we can assign x[i] without explicitly defining x first. GraphPPL is able to infer that x is a vector of random variables, and will grow the internal representation of x accordingly to accomodate i elements. Note that this works recursively, so z[i, j] will define a matrix of random variables. GraphPPL does check that the index [i,j] is compatible with the shape of the variable z.

Note

eachindex(y) works only if y has a static data associated with it. Read the documentation for GraphPPL.create_model for more information.

Factor aliases

In version 4, we can define factor aliases to define different implementations of the same factor. For example, in RxInfer.jl, there are multiple implentations of the Normal distribution. Previously, we would have to explicitly call NormalMeanVariance or NormalMeanPrecision. In version 4, we can define factor aliases to default to certain implementations when specific keyword arguments are used on construction. For example: Normal(μ = 0, σ² = 1) will default to NormalMeanVariance and Normal(μ = 0, τ = 1) will default to NormalMeanPrecision. This allows users to quickly toggle between different implementations of the same factor, while keeping an implementation agnostic model definition.

Note

This feature works only in the combination with the ~ operator, which creates factor nodes. Therefore it cannot be used to instantiate a distribution object in a regular Julia code.

Nested models

The major difference between versions 3 and 4 is the support for nested models. In version 4, models can be nested within each other. This allows for more complex models to be built in a modular way. The following example demonstrates how to nest models in version 4:

@model function kalman_filter_step(y, prev_x, new_x, A, B, Q, P)
    new_x ~ MvNormal(μ = A * prev_x, Σ = Q)
    y ~ MvNormal(μ = B * new_x, Σ = P)
end

@model function state_space_model(y, A, B, Q, P)
    x[1] ~ MvNormal(zeros(2), diagm(ones(2)))
    for i in eachindex(y)
        y[i] ~ kalman_filter_step(prev_x = x[i], new_x = new(x[i + 1]), A=A, B=B, Q=Q, P=P)
    end
end

Note that we reuse the kalman_filter_step model in the state_space_model model. In the argument list of any GraphPPL model, we have to specify the Markov Blanket of the model we are defining. This means that all interfaces with the outside world have to be passed as arguments to the model. For the kalman_filter_step model, we pass the previous state prev_x, the new state new_x, the observation y as well as the parameters A, B, Q and P. This means that, when invoking a submodel in a larger model, we can specify all components of the Markov Blanket. Note that, in the state_space_model model, we do not pass y as an argument to the kalman_filter_step model. GraphPPL will infer that y is missing from the argument list and assign it to whatever is left of the ~ operator. Note that we also use the new(x[i + 1]) syntax to create a new variable in the position of x[i + 1]. Since y is also passed in the argument list of the state_space_model model, we could have written this line with the equivalen statement x[i + 1] ~ kalman_filter_step(prev_x = x[i], y = y[i], A=A, B=B, Q=Q, P=P). However, to respect the generative direction of the model and to make the code more readable, we use the new(x[i + 1]) syntax. Note, however, that the underlaying representation of the models in GraphPPL are still undirected.

Constraint specification

With the introduction of nested models, the specification of variational constraints becomes more difficult. In version 3, variable names were uniquely defined in the model, which made it easy to specify constraints on variables. In version 4, nested models can contain variables with the same name as their parents, even though they are distinct random variables. Therefore, we need to specify constraints on submodel level in the constraints macro. This is done with the for q in _submodel_ syntax.

For example:

@constraints begin
    for q in kalman_filter_step
        q(new_x, prev_x, y) = q(new_x, prev_x)q(y)
    end
end

This specification reuses the same constraint to all instances of the kalman_filter_step submodel. Of course, we'd like to have more flexibility in the constraints we can specify. Therefore, we can also specify constraints on a specific instance of the submodel. For example:

@constraints begin
    for q in (kalman_filter_step, 1)
        q(new_x, prev_x, y) = q(new_x, prev_x)q(y)
        q(new_x) :: Normal
    end
end

This pushes the constraint to the first instance of the kalman_filter_step submodel. With this syntax, we can specify constraints on any instance of a submodel.

The function syntax for constraints is still supported. For example:

@constraints function ssm_constraints(factorize)
    for q in kalman_filter_step
        if factorize
            q(new_x, prev_x, y) = MeanField()
        else
            q(new_x, prev_x, y) = q(new_x, prev_x)q(y)
        end
    end
end

Meta specification

The meta specification follows exactly the same structure as the constraints specification. Nested models in the @meta macro are specified in the same way as in the @constraints macro.

For example:

@meta begin
    for meta in some_submodel
        GCV(x, k, w) -> GCVMetadata(GaussHermiteCubature(20))
    end
    y -> SomeMetaData()
end

Additionally, we can pass arbitrary metadata to the inference backend. For example:

@meta begin
    GCV(x, k, w) -> GCVMetadata(GaussHermiteCubature(20))
    x -> (prod_constraint = SomeProdConstraint(), )
end

By passing a NamedTuple in the @model macro, we can pass arbitrary metadata to the inference backend that we would previously have to specify in the where clause of a node. With the added functionality of the @meta macro, we can pass metadata to the inference backend in a more structured way, and detach metadata definition from model definition.

Dispatch

In the new version of GraphPPL.jl it is no longer possible to use Julia's multiple dispatch within the @model function definition. The workaround is to use nested models instead, e.g. users can pass submodels as an extra argument, therefore change the structure of the model based on the input arguments.

Positional arguments

In the new version of GraphPPL.jl positional arguments within model construction is no longer supported and all arguments must be named explicitly. For example:

@model function beta_bernoulli(y, a, b)
    t ~ Beta(a, b)
    for i in eachindex(y)
        y[i] ~ Bernoulli(t)
    end
end
model = beta_bernoulli(1.0, 2.0) # ambigous and unsupported
model = beta_bernoulli(a = 1.0, b = 2.0) # explicitly defined via keyword arguments

The same recipe applies when nesting models within each other, e.g.

@model function bigger_model(y)
    a ~ Uniform(0.0, 10.0)
    b ~ Uniform(0.0, 10.0)
    y ~ beta_bernoulli(a = a, b = b)
end

Mixture Nodes

The interface of the mixture nodes does no longer require a tuple of inputs. Instead, the inputs can be passed as a vector as well. For example:

@model function mixture_model(y)
    # specify two models
    m_1 ~ Beta(2.0, 7.0)
    m_2 ~ Beta(7.0, 2.0)

    # specify prior on switch param
    s ~ Bernoulli(0.7) 

    # specify mixture prior Distribution
    θ ~ Mixture(switch = s, inputs = [m_1, m_2])
end

Overview of the removed functionality

  • The datavar, randomvar and constvar syntax is removed. GraphPPL is able to infer the type of the variable based on the way in which it is used.
  • Specifying factorization constraints in the where clause of a node is no longer possible. The where syntax can still be used to specify metadata for factor nodes, but factorization constraints can only be specified with the @constraints macro.
  • Dispatch in the @model macro is not supported.
  • Positional arguments in the @model macro are not supporeted.