Defining a bijector

This page describes the minimum expected interface to implement a bijector.

In general, there are two pieces of information needed to define a bijector:

  1. The transformation itself, i.e., the map $b: \mathbb{R}^d \to \mathbb{R}^d$.

  2. The log-absolute determinant of the Jacobian of that transformation. For a transformation $b: \mathbb{R}^d \to \mathbb{R}^d$, the Jacobian at point $x \in \mathbb{R}^d$ is defined as:

    \[J_{b}(x) = \begin{bmatrix} \partial y_1/\partial x_1 & \partial y_1/\partial x_2 & \cdots & \partial y_1/\partial x_d \\ \partial y_2/\partial x_1 & \partial y_2/\partial x_2 & \cdots & \partial y_2/\partial x_d \\ \vdots & \vdots & \ddots & \vdots \\ \partial y_d/\partial x_1 & \partial y_d/\partial x_2 & \cdots & \partial y_d/\partial x_d \end{bmatrix}\]

    where $y = b(x)$.

The transform itself

The most efficient way to implement a bijector is to provide an implementation of:

ChangesOfVariables.with_logabsdet_jacobianFunction
with_logabsdet_jacobian(t::Transform, x)

Semantically, this must return a tuple of (y, logabsdetjac), where y = transform(t, x) and logabsdetjac = logabsdetjac(t, x). However, you can implement this function to exploit shared computation between the two quantities.

source

If you define with_logabsdet_jacobian(b, x), then you will automatically get default implementations of both transform(b, x) and logabsdetjac(b, x), which respectively return the first and second value of that tuple. So, in fact, you can implement a bijector by defining only with_logabsdet_jacobian.

If you prefer, you can implement transform and logabsdetjac separately, as described below. Having manual implementations of these may also be useful if you expect either to be used heavily without the other.

Transformation

Bijectors.transformFunction
transform(b, x)

Transform x using b.

If with_logabsdet_jacobian is already implemented for b, the default implementation of transform will call first(with_logabsdet_jacobian(b, x)).

source

If transform(b, x) is defined, then you will automatically get a default implementation of b(x) which calls that.

Log-absolute determinant of the Jacobian

Inverse

Often you will want to define an inverse bijector as well. To do so, you will have to implement:

If b is a bijector, then inverse(b) should return the inverse bijector $b^{-1}$.

If your bijector subtypes Bijectors.Bijector, then you will get a default implementation of inverse which constructs Bijectors.Inverse(b). This may be easier than creating a second type for the inverse bijector. Note that you will also need to implement the methods for with_logabsdet_jacobian (and/or transform and logabsdetjac) for the inverse bijector type.

If your bijector is not invertible, you can specify this here:

Distributions

If your bijector is intended for use with a distribution, i.e., it transforms random variables drawn from that distribution to Euclidean space, then you should also implement:

Bijectors.bijectorFunction
bijector(d::Distribution)

Returns the constrained-to-unconstrained bijector for distribution d.

source

which should return your bijector.

On top of that, you should also implement a method for Bijectors.output_size(b, dist::Distribution):

Bijectors.output_sizeFunction
output_size(f, sz)

Returns the size of f(x) when given an input x of size sz.

source
output_size(f, dist::Distribution)

Returns the output size of f given an input drawn from the distribution dist.

By default this just calls output_size(f, size(dist)), but this can be overloaded for specific distributions. This is useful when Base.size(dist) is not defined, e.g. for ProductNamedTupleDistribution and in particular is used by DynamicPPL when generating new random values for transformed distributions.

source

Closed-form

If your bijector does not have a closed-form expression (e.g. if it uses an iterative procedure), then this should be set to false:

Bijectors.isclosedformFunction
isclosedform(b::Transform)::bool
isclosedform(b⁻¹::Inverse{<:Transform})::bool

Returns true or false depending on whether or not evaluation of b has a closed-form implementation.

Most transformations have closed-form evaluations, but there are cases where this is not the case. For example the inverse evaluation of PlanarLayer requires an iterative procedure to evaluate.

source

The default is true so you only need to set this if your bijector is not closed-form.