Probabilistic programming in BAli-Phy
Use model expressions or Haskell programs to describe Bayesian models and run inference with BAli-Phy. You can combine existing evolutionary models or define new models with random trees, changing dependencies, and unknown numbers of parameters.
BAli-Phy implements a universal probabilistic programming language: a model defines a distribution over program execution histories. Random choices can determine which branches of a program are taken and which further random variables are created. MCMC samples these histories conditional on the observed data.
- Modular models: combine smaller model components in new analyses.
- Changing structure: let random choices determine dependencies, such as which node is a parent in a random tree.
- Variable dimensions: infer an unknown number of components without writing reversible-jump proposals yourself.
- Random data structures: work with trees, lists, and user-defined types as well as numeric parameters.
For the conceptual explanation, see Programs and inference: execution histories, conditioning on data, and how BAli-Phy updates a model efficiently.
From model expressions to programs
Command-line model expressions provide a compact way to construct models using BAli-Phy’s probabilistic programming capabilities. For example, this command combines the HKY85 substitution model with gamma-distributed variation in rates across sites:
bali-phy sequences.fasta -S 'HKY85 +> ASRV.Gamma'
The command estimates an alignment and tree from the sequences in
sequences.fasta. You can also evaluate a model expression and display its value:
bali-phy print HKY85 -A DNA
Here, the alphabet is supplied because HKY85 depends on it. See the command reference and user's guide for more about using model expressions.
An unknown number of rate categories
Model expressions can also describe models whose number of parameters is inferred from
the data. For example, ASRV.Free estimates the rates and weights of rate
categories. Giving its category count n a prior distribution lets inference
explore different numbers of categories:
bali-phy sequences.fasta -S 'HKY85 +> ASRV.Free(n=1 + ~Geometric(0.5))' --log-format=json
The ~ means “sample from”: ~Geometric(0.5) samples a nonnegative
integer, and adding one makes the category count positive. During MCMC, the number of
categories can change along with their rates and weights.
Use JSON logging for this model: unlike TSV, it can record the changing number of category-specific values.
Writing a program directly
BAli-Phy translates model expressions into Haskell, which it uses to represent probabilistic models and perform inference. Writing a Haskell program directly gives you more control over how a model is constructed, what data it observes, and what results it records. The examples below illustrate this approach.
Example programs
Download a program and its data into the same directory, then run the commands there.
These are short trial runs, not sufficient to establish convergence. For program options,
use bali-phy run PROGRAM.hs --help.
Linear regression
This model describes observations around a line f(x) = b*x + a.
The slope b, intercept a, and residual standard deviation
sigma have prior distributions. The program reads the x
and y columns of xy.csv.
model xs ys = do
b <- prior $ normal 0 1
a <- prior $ normal 0 1
sigma <- prior $ exponential 1
let f x = b * x + a
observe ys $ independent [ normal (f x) sigma | x <- xs ]
return ["b" %=% b, "a" %=% a, "sigma" %=% sigma]
prior $ normal 0 1samples a parameter and contributes its prior density.observe ys $ independent [...]contributes the likelihood of the data.- Each sampled slope and intercept defines a line. Observations vary around that
line according to
normal (f x) sigma. - The returned named quantities are passed to the program's logging setup, which records them during MCMC.
Complete program: LinearRegression.hs
module LinearRegression where
import BAliPhy.Run
import MCMC (runMCMC)
import Options.Applicative
import Probability
import Data.Frame
model xs ys = do
b <- prior $ normal 0 1
a <- prior $ normal 0 1
sigma <- prior $ exponential 1
let f x = b * x + a
observe ys $ independent [ normal (f x) sigma | x <- xs ]
return ["b" %=% b, "a" %=% a, "sigma" %=% sigma]
main = do
options <- execParser $ modelRunParser "LinearRegression" 200000
runInfo <- initializeModelRun (runMode options)
xy_data <- readTable "xy.csv"
let xs = xy_data $$ "x" :: [Double]
ys = xy_data $$ "y" :: [Double]
mcmcState <- makeLoggedMCMCState runInfo (logFormats options) $ model xs ys
case runInfo of
TestRun -> printInitialModel (logFormats options) mcmcState
MCMCRun directory -> do
reportModelRun (iterations options) (logFormats options) directory
runMCMC (iterations options) mcmcState
Download LinearRegression.hs and
xy.csv. Keep the name xy.csv,
which is used by the program.
bali-phy run LinearRegression.hs --iterations=1000 --name=Regression
statreport Regression-1/C1.log.json
Inspect the posterior summaries for a, b, and
sigma, together with their sampling diagnostics.
Tree and alignment inference
This program samples a tree and alignment for DNA sequences. It combines a tree prior, an indel process, and a TN93 substitution model.
branch_length_dist topology branch = gamma (1/2) (2/fromIntegral n) where n = numBranches topology
model seq_data = do
let taxa = getTaxa seq_data
tip_seq_lengths = getSequenceLengths seq_data
-- Tree
scale <- prior $ gamma (1/2) 2
tree <- prior $ uniformLabelledTree'' taxa branch_length_dist
-- Indel model
indel_rate <- prior $ logLaplace (-4) 0.707
mean_length <- (1 +) <$> sample (exponential 10)
let imodel = rs07 indel_rate mean_length tree
-- Substitution model
freqs <- prior $ symmetricDirichletOn (letterSet dna) 1
kappa1 <- prior $ logNormal 0 1
kappa2 <- prior $ logNormal 0 1
let tn93_model = tn93' dna kappa1 kappa2 freqs
-- Alignment
alignment <- prior $ phyloAlignment tree imodel scale tip_seq_lengths
-- Observation
observe seq_data $ phyloCTMC tree alignment tn93_model scale
return
[ "tree" %=% writeNewick tree
, "log(indel_rate)" %=% log indel_rate
, "mean_length" %=% mean_length
, "kappa1" %=% kappa1
, "kappa2" %=% kappa2
, "frequencies" %=% freqs
, "scale" %=% scale
, "|T|" %=% treeLength tree
, "scale*|T|" %=% treeLength tree * scale
, "|A|" %=% alignmentLength alignment
]
- The tree prior supplies topology and branch lengths;
scalemultiplies those lengths for the sequence model. - The indel model and tree define the distribution of the alignment through
phyloAlignment. phyloCTMCconnects the tree, alignment, and substitution model to the observed sequences.
Complete program: InferTreeAlignment.hs
module Model where
import BAliPhy.Run
import MCMC (runMCMC)
import Options.Applicative
import Probability
import Bio.Alignment
import Bio.Alphabet
import Bio.Sequence
import Tree
import Tree.Newick
import SModel
import IModel
branch_length_dist topology branch = gamma (1/2) (2/fromIntegral n) where n = numBranches topology
model seq_data = do
let taxa = getTaxa seq_data
tip_seq_lengths = getSequenceLengths seq_data
-- Tree
scale <- prior $ gamma (1/2) 2
tree <- prior $ uniformLabelledTree'' taxa branch_length_dist
-- Indel model
indel_rate <- prior $ logLaplace (-4) 0.707
mean_length <- (1 +) <$> sample (exponential 10)
let imodel = rs07 indel_rate mean_length tree
-- Substitution model
freqs <- prior $ symmetricDirichletOn (letterSet dna) 1
kappa1 <- prior $ logNormal 0 1
kappa2 <- prior $ logNormal 0 1
let tn93_model = tn93' dna kappa1 kappa2 freqs
-- Alignment
alignment <- prior $ phyloAlignment tree imodel scale tip_seq_lengths
-- Observation
observe seq_data $ phyloCTMC tree alignment tn93_model scale
return
[ "tree" %=% writeNewick tree
, "log(indel_rate)" %=% log indel_rate
, "mean_length" %=% mean_length
, "kappa1" %=% kappa1
, "kappa2" %=% kappa2
, "frequencies" %=% freqs
, "scale" %=% scale
, "|T|" %=% treeLength tree
, "scale*|T|" %=% treeLength tree * scale
, "|A|" %=% alignmentLength alignment
]
main = do
(options, filename) <- execParser $
modelRunParserWith "Model" 200000 $
strArgument (metavar "SEQUENCES" <> help "Unaligned DNA sequences")
runInfo <- initializeModelRun (runMode options)
seq_data <- mkUnalignedCharacterData dna <$> loadSequences filename
mcmcState <- makeLoggedMCMCState runInfo (logFormats options) $ model seq_data
case runInfo of
TestRun -> printInitialModel (logFormats options) mcmcState
MCMCRun directory -> do
reportModelRun (iterations options) (logFormats options) directory
runMCMC (iterations options) mcmcState
Download InferTreeAlignment.hs and 5d-muscle.fasta.
bali-phy run InferTreeAlignment.hs 5d-muscle.fasta --iterations=1000 --name=TreeAlignment
statreport --ignore=tree TreeAlignment-1/C1.log.json
The JSON log includes sampled trees as text. Exclude that field when summarizing numeric parameters such as tree length, alignment length, and indel rate.
A custom codon model
The function gtr_m7_model combines GTR nucleotide exchangeabilities
with a beta distribution of dN/dS (ω) across sites. M7 describes variation in
selective constraint: ω lies between 0 and 1.
gtr_m7_model codons = do
let nucs = getNucleotides codons
-- GTR model parameters
sym <- sample $ symmetricDirichletOn (Set.fromList $ letter_pair_names nucs) 1
pi <- sample $ symmetricDirichletOn (letterSet nucs) 1
let posSelModel w = gtr' sym pi nucs +> x3 codons +> dNdS w
-- Independent uniform priors on the beta mean and normalized variance.
mu <- sample $ uniform 0 1
v <- sample $ uniform 0 1
let m7Model = posSelModel +> m7 mu v 4
let loggers =
[ "gtr:sym" %=% sym
, "gtr:pi" %=% pi
, "m7:mu" %=% mu
, "m7:v" %=% v
]
return (m7Model, loggers)
The function defines the substitution model and its priors, returning both the model and quantities to log. The complete program uses this model to infer a tree and alignment, with branch-specific indel rates.
Complete program: M7.hs
module Model where
import BAliPhy.Run
import Bio.Alignment
import Bio.Alphabet
import qualified Data.Set as Set
import IModel
import MCMC
import Options.Applicative
import Probability
import SModel
import SModel.Parsimony
import Tree
import Tree.Newick
gtr_m7_model codons = do
let nucs = getNucleotides codons
-- GTR model parameters
sym <- sample $ symmetricDirichletOn (Set.fromList $ letter_pair_names nucs) 1
pi <- sample $ symmetricDirichletOn (letterSet nucs) 1
let posSelModel w = gtr' sym pi nucs +> x3 codons +> dNdS w
-- Independent uniform priors on the beta mean and normalized variance.
mu <- sample $ uniform 0 1
v <- sample $ uniform 0 1
let m7Model = posSelModel +> m7 mu v 4
let loggers =
[ "gtr:sym" %=% sym
, "gtr:pi" %=% pi
, "m7:mu" %=% mu
, "m7:v" %=% v
]
return (m7Model, loggers)
model sequenceData = do
let taxa = getTaxa sequenceData
tree <- sample $ uniformLabelledTree taxa (gamma 0.5 (1 / fromIntegral (length taxa)))
let tlength = treeLength tree
sigma <- sample $ logLaplace (-3) 1
indelRates <- fmap (** sigma) <$> sample (iidMap (getUEdgesSet tree) (logNormal 0 1))
let indelTree = addBranchRates indelRates tree
scale <- sample $ gamma 0.5 2
addMove 2 (scaleGroupsSlice [scale] (branchLengths tree))
addMove 1 (scaleGroupsMH [scale] (branchLengths tree))
let codons = mkCodons dna (geneticCode "standard")
(m7_model, log_m7_model) <- gtr_m7_model codons
rate <- sample $ logLaplace (-4) 0.707
meanLength <- sample $ shifted_exponential 10 1
let imodel = IModel.rs07 rate meanLength tree
let sequenceLengths = getSequenceLengths sequenceData
(alignment, propertiesA) <- sampleWithProps (phyloAlignment indelTree imodel scale sequenceLengths)
properties <- observe sequenceData (phyloCTMC tree alignment m7_model scale)
let alignment_length = alignmentLength alignment
let num_indels = totalNumIndels alignment
let total_length_indels = totalLengthIndels alignment
let prior_A = ln (probability propertiesA)
let ancStates = prop_anc_cat_states properties
let ancAlignment = toFasta $ ancestralAlignment tree alignment (getSMap m7_model) codons ancStates
let substs = parsimony tree (unitCostMatrix codons) (sequenceData, alignment)
let loggers =
[ "indelRates:sigma" %=% sigma
, "S1" %>% log_m7_model
, "rs07:rate" %=% rate
, "rs07:mean_length" %=% meanLength
, "scale" %=% scale
, "scale*|T|" %=% (scale * tlength)
, "|A|" %=% alignment_length
, "#indels" %=% num_indels
, "|indels|" %=% total_length_indels
, "#substs" %=% substs
, "prior_A" %=% prior_A
]
return loggers
main = do
(options, filename) <-
execParser $
modelRunParserWith "Model" 200000 $
strArgument (metavar "SEQUENCES" <> help "Unaligned coding sequences")
runInfo <- initializeModelRun (runMode options)
sequenceData <-
mkUnalignedCharacterData (mkCodons dna standard_code)
<$> loadSequences filename
mcmcState <- makeLoggedMCMCState runInfo (logFormats options) $ model sequenceData
case runInfo of
TestRun -> printInitialModel (logFormats options) mcmcState
MCMCRun directory -> do
reportModelRun (iterations options) (logFormats options) directory
runMCMC (iterations options) mcmcState
Download M7.hs and bglobin.fasta.
bali-phy run M7.hs bglobin.fasta --iterations=100 --name=CodonModel
statreport CodonModel-1/C1.log.json
Inspect the beta-distribution parameters and other logged quantities. With this short run, some credible intervals may be unavailable. Directory suffixes increase on subsequent runs; use the directory reported by the program.
Further reading
Programs and inference explains traces, changing model
structure, and incremental computation. The user's guide
covers built-in evolutionary models and ordinary analyses, and the
command reference describes run and
print.