Module asymmetree.genome.GenomeSimulation

Simulation of genomes.

Expand source code
# -*- coding: utf-8 -*-

"""Simulation of genomes."""

import os

from tralda.datastructures.Tree import Tree

import asymmetree.treeevolve as te
import asymmetree.seqevolve as se
from asymmetree.file_io.SeqFileIO import write_alignment, write_fasta
from asymmetree.tools.Sampling import Sampler


__author__ = 'David Schaller'


class GenomeSimulator:
    
    def __init__(self, species_tree, outdir=None):
        """
        Parameters
        ----------
        species_tree : Tree
            The species tree along which gene trees are simulated.
        outdir : str, optional
            The path to a directory into which the results of the simulation
            (serialized trees, one fasta file per genome, one 'true' alignement
            per gene family). The default is None, in which case nothing is
            saved to file.
        """
        
        if not isinstance(species_tree, Tree):
            raise TypeError("species tree must be of type 'Tree'")
        
        self.S = species_tree
        self.outdir = outdir
        
        if self.outdir:
            self._check_outdir()
            self.S.serialize(self._path('species_tree.json'), mode='json')
            
        self.true_gene_trees = []
        self.pruned_gene_trees = []
        
    
    def _check_outdir(self):
        
        if not os.path.exists(self.outdir):
            os.makedirs(self.outdir)
            
        elif os.path.exists(self.outdir) and not os.path.isdir(self.outdir):
            raise FileExistsError("'{}' is not a directory".format(self.outdir))
            
        for directory in ('true_gene_trees', 'fasta_files', 'alignments'):
            path = os.path.join(self.outdir, directory)
            if not os.path.exists(path):
                os.makedirs(path)
            
    
    def _path(self, *args):
        
        return os.path.join(self.outdir, *args)
            
    
    def simulate_gene_trees(self, n,
                            **kwargs):
        """Simulate gene trees.
        
        Simulates the 'true gene trees' which still contain the loss events as
        well as the 'pruned gene trees' in which the loss branches are
        removed.
        
        Parameters
        ----------
        n : int
            Number of gene trees to be simulated.
        kwargs : optional
            See function 'gene_trees' in 'treeevolve' subpackage or 
            documentation.
        """
        
        self.number_of_families = n
        
        self.true_gene_trees = te.gene_trees(self.S, n=n, **kwargs)
        
        self.pruned_gene_trees = [te.prune_losses(tree)
                                  for tree in self.true_gene_trees]
        
        # sequences should be emptied here if methods were called before
        if hasattr(self, 'sequence_dicts'):
            self.sequence_dicts.clear()
        
        if self.outdir:
            for i in range(n):
                filename = self._path('true_gene_trees',
                                      'gene_tree{}.json'.format(i))
                self.true_gene_trees[i].serialize(filename, mode='json')
        
        return self.true_gene_trees, self.pruned_gene_trees
                
    
    def simulate_sequences(self, subst_model,
                           root_genome=None,
                           length_distr=('constant', 200),
                           min_length=10,
                           max_length=None,
                           write_fastas=True,
                           write_alignments=True,
                           **kwargs):
        """Simulate sequences along the (pruned) gene trees.
        
        Parameters
        ----------
        subst_model : SubstModel
            Substitution model.
        root_genome : list, optional
            List of sequences for the roots of the gene trees, must contain the
            same number of str sequences as trees that were simulated. 
            The sequences must be compatible with the specified substitution 
            model (model_type='n'/'a'). The default is None, in which case new
            sequences are generated at random.
        length_distr : tuple, optional
            Distribution of the length of the root sequences if root genome is
            not supplied. See documentation for available options.
        min_length : int, optional
            Minimal length at which the distribution of lengths is truncated;
            must be less than the mean of this distribution. The default is 10.
        max_length : int, optional
            Maximal length at which the distribution of lengths is truncated;
            must be less than the mean of this distribution. The default is
            None, in which case the distribution is not truncated.
        write_fastas : bool, optional
            If True and an output directory was speci ed, write the sequences
            (one file per species) into the directory 'fasta_files' in the
            output directory. The default is True.
        write_alignments : bool, optional
            If True and an output directory was speci ed, write the true 
            alignments (one file per gene tree) into the directory 'alignments'
            in the output directory. The default is True.
        kwargs : optional
            See class 'Evolver' in the 'seqevolve' subpackage or documentation.
        """
        
        self.subst_model = subst_model
        
        if hasattr(self, 'sequence_dicts'):
            self.sequence_dicts.clear()
        else:
            self.sequence_dicts = []
        
        if root_genome:
            if len(root_genome) != len(self.number_of_families):
                raise ValueError('no. of sequences in root genome does not'\
                                 'match no of gene families')
        else:
            if isinstance(length_distr, Sampler):
                self.sampler = length_distr
            else:
                self.sampler = Sampler(length_distr,
                                       minimum=min_length,
                                       maximum=max_length,
                                       discrete=True)
        
        evolver = se.Evolver(subst_model, **kwargs)
        
        for i in range(self.number_of_families):
            
            PGT = self.pruned_gene_trees[i]
            
            if root_genome:
                evolver.evolve_along_tree(PGT, start_seq=root_genome[i].upper())
                
            else:
                evolver.evolve_along_tree(PGT, start_length=self.sampler())
                
            self.sequence_dicts.append(evolver.sequences)
            
            # write one alignment file per gene family
            if self.outdir and write_alignments:
                self._write_alignment(i)
        
        # write one fasta file per species
        if self.outdir and write_fastas:
            self._write_fastas(include_inner=False)
            
            
    def _write_alignment(self, family_id):
        
        if not self.outdir:
            raise RuntimeError('no output directory specified for alignments')        
        
        alg_builder = se.AlignmentBuilder(self.pruned_gene_trees[family_id],
                                          self.sequence_dicts[family_id],
                                          self.subst_model.alphabet,
                                          include_inner=False)
        
        alignment = []
        for node, sequence in alg_builder.build().items():
            label = self._compose_label(node, family_id)
            alignment.append( (label, sequence) )
            
        basename = 'alignment{}.phylip'.format(family_id)
        filename = self._path('alignments', basename)
        write_alignment(filename, alignment, alignment_format='phylip')
    
                
    def _write_fastas(self, include_inner=False):
        
        if not self.outdir:
            raise RuntimeError('no output directory specified for fasta files')
        
        # list of all species labels
        if include_inner:
            species = [v.label for v in self.S.preorder()]
        else:
            species = [v.label for v in self.S.preorder() if not v.children]
        
        # labeled sequences sorted by species
        sorted_seqs = {s: [] for s in species}
        
        for i in range(self.number_of_families):
            for node, evoseq in self.sequence_dicts[i].items():
                
                # skip inner nodes
                if not include_inner and node.children:
                    continue
                
                label = self._compose_label(node, i)
                sequence = self.subst_model.to_sequence(evoseq)
                sorted_seqs[node.reconc].append( (label, sequence) )
                
        for spec, sequences in sorted_seqs.items():
            basename = '{}.f{}a'.format(spec, self.subst_model.model_type)
            filename = self._path('fasta_files', basename)
            write_fasta(filename, sequences)
        
    
    def _compose_label(self, node, family_id):
        
        return 'fam{}gene{}spec{}'.format(family_id, node.label, node.reconc)

Classes

class GenomeSimulator (species_tree, outdir=None)

Parameters

species_tree : Tree
The species tree along which gene trees are simulated.
outdir : str, optional
The path to a directory into which the results of the simulation (serialized trees, one fasta file per genome, one 'true' alignement per gene family). The default is None, in which case nothing is saved to file.
Expand source code
class GenomeSimulator:
    
    def __init__(self, species_tree, outdir=None):
        """
        Parameters
        ----------
        species_tree : Tree
            The species tree along which gene trees are simulated.
        outdir : str, optional
            The path to a directory into which the results of the simulation
            (serialized trees, one fasta file per genome, one 'true' alignement
            per gene family). The default is None, in which case nothing is
            saved to file.
        """
        
        if not isinstance(species_tree, Tree):
            raise TypeError("species tree must be of type 'Tree'")
        
        self.S = species_tree
        self.outdir = outdir
        
        if self.outdir:
            self._check_outdir()
            self.S.serialize(self._path('species_tree.json'), mode='json')
            
        self.true_gene_trees = []
        self.pruned_gene_trees = []
        
    
    def _check_outdir(self):
        
        if not os.path.exists(self.outdir):
            os.makedirs(self.outdir)
            
        elif os.path.exists(self.outdir) and not os.path.isdir(self.outdir):
            raise FileExistsError("'{}' is not a directory".format(self.outdir))
            
        for directory in ('true_gene_trees', 'fasta_files', 'alignments'):
            path = os.path.join(self.outdir, directory)
            if not os.path.exists(path):
                os.makedirs(path)
            
    
    def _path(self, *args):
        
        return os.path.join(self.outdir, *args)
            
    
    def simulate_gene_trees(self, n,
                            **kwargs):
        """Simulate gene trees.
        
        Simulates the 'true gene trees' which still contain the loss events as
        well as the 'pruned gene trees' in which the loss branches are
        removed.
        
        Parameters
        ----------
        n : int
            Number of gene trees to be simulated.
        kwargs : optional
            See function 'gene_trees' in 'treeevolve' subpackage or 
            documentation.
        """
        
        self.number_of_families = n
        
        self.true_gene_trees = te.gene_trees(self.S, n=n, **kwargs)
        
        self.pruned_gene_trees = [te.prune_losses(tree)
                                  for tree in self.true_gene_trees]
        
        # sequences should be emptied here if methods were called before
        if hasattr(self, 'sequence_dicts'):
            self.sequence_dicts.clear()
        
        if self.outdir:
            for i in range(n):
                filename = self._path('true_gene_trees',
                                      'gene_tree{}.json'.format(i))
                self.true_gene_trees[i].serialize(filename, mode='json')
        
        return self.true_gene_trees, self.pruned_gene_trees
                
    
    def simulate_sequences(self, subst_model,
                           root_genome=None,
                           length_distr=('constant', 200),
                           min_length=10,
                           max_length=None,
                           write_fastas=True,
                           write_alignments=True,
                           **kwargs):
        """Simulate sequences along the (pruned) gene trees.
        
        Parameters
        ----------
        subst_model : SubstModel
            Substitution model.
        root_genome : list, optional
            List of sequences for the roots of the gene trees, must contain the
            same number of str sequences as trees that were simulated. 
            The sequences must be compatible with the specified substitution 
            model (model_type='n'/'a'). The default is None, in which case new
            sequences are generated at random.
        length_distr : tuple, optional
            Distribution of the length of the root sequences if root genome is
            not supplied. See documentation for available options.
        min_length : int, optional
            Minimal length at which the distribution of lengths is truncated;
            must be less than the mean of this distribution. The default is 10.
        max_length : int, optional
            Maximal length at which the distribution of lengths is truncated;
            must be less than the mean of this distribution. The default is
            None, in which case the distribution is not truncated.
        write_fastas : bool, optional
            If True and an output directory was speci ed, write the sequences
            (one file per species) into the directory 'fasta_files' in the
            output directory. The default is True.
        write_alignments : bool, optional
            If True and an output directory was speci ed, write the true 
            alignments (one file per gene tree) into the directory 'alignments'
            in the output directory. The default is True.
        kwargs : optional
            See class 'Evolver' in the 'seqevolve' subpackage or documentation.
        """
        
        self.subst_model = subst_model
        
        if hasattr(self, 'sequence_dicts'):
            self.sequence_dicts.clear()
        else:
            self.sequence_dicts = []
        
        if root_genome:
            if len(root_genome) != len(self.number_of_families):
                raise ValueError('no. of sequences in root genome does not'\
                                 'match no of gene families')
        else:
            if isinstance(length_distr, Sampler):
                self.sampler = length_distr
            else:
                self.sampler = Sampler(length_distr,
                                       minimum=min_length,
                                       maximum=max_length,
                                       discrete=True)
        
        evolver = se.Evolver(subst_model, **kwargs)
        
        for i in range(self.number_of_families):
            
            PGT = self.pruned_gene_trees[i]
            
            if root_genome:
                evolver.evolve_along_tree(PGT, start_seq=root_genome[i].upper())
                
            else:
                evolver.evolve_along_tree(PGT, start_length=self.sampler())
                
            self.sequence_dicts.append(evolver.sequences)
            
            # write one alignment file per gene family
            if self.outdir and write_alignments:
                self._write_alignment(i)
        
        # write one fasta file per species
        if self.outdir and write_fastas:
            self._write_fastas(include_inner=False)
            
            
    def _write_alignment(self, family_id):
        
        if not self.outdir:
            raise RuntimeError('no output directory specified for alignments')        
        
        alg_builder = se.AlignmentBuilder(self.pruned_gene_trees[family_id],
                                          self.sequence_dicts[family_id],
                                          self.subst_model.alphabet,
                                          include_inner=False)
        
        alignment = []
        for node, sequence in alg_builder.build().items():
            label = self._compose_label(node, family_id)
            alignment.append( (label, sequence) )
            
        basename = 'alignment{}.phylip'.format(family_id)
        filename = self._path('alignments', basename)
        write_alignment(filename, alignment, alignment_format='phylip')
    
                
    def _write_fastas(self, include_inner=False):
        
        if not self.outdir:
            raise RuntimeError('no output directory specified for fasta files')
        
        # list of all species labels
        if include_inner:
            species = [v.label for v in self.S.preorder()]
        else:
            species = [v.label for v in self.S.preorder() if not v.children]
        
        # labeled sequences sorted by species
        sorted_seqs = {s: [] for s in species}
        
        for i in range(self.number_of_families):
            for node, evoseq in self.sequence_dicts[i].items():
                
                # skip inner nodes
                if not include_inner and node.children:
                    continue
                
                label = self._compose_label(node, i)
                sequence = self.subst_model.to_sequence(evoseq)
                sorted_seqs[node.reconc].append( (label, sequence) )
                
        for spec, sequences in sorted_seqs.items():
            basename = '{}.f{}a'.format(spec, self.subst_model.model_type)
            filename = self._path('fasta_files', basename)
            write_fasta(filename, sequences)
        
    
    def _compose_label(self, node, family_id):
        
        return 'fam{}gene{}spec{}'.format(family_id, node.label, node.reconc)

Methods

def simulate_gene_trees(self, n, **kwargs)

Simulate gene trees.

Simulates the 'true gene trees' which still contain the loss events as well as the 'pruned gene trees' in which the loss branches are removed.

Parameters

n : int
Number of gene trees to be simulated.
kwargs : optional
See function 'gene_trees' in 'treeevolve' subpackage or documentation.
Expand source code
def simulate_gene_trees(self, n,
                        **kwargs):
    """Simulate gene trees.
    
    Simulates the 'true gene trees' which still contain the loss events as
    well as the 'pruned gene trees' in which the loss branches are
    removed.
    
    Parameters
    ----------
    n : int
        Number of gene trees to be simulated.
    kwargs : optional
        See function 'gene_trees' in 'treeevolve' subpackage or 
        documentation.
    """
    
    self.number_of_families = n
    
    self.true_gene_trees = te.gene_trees(self.S, n=n, **kwargs)
    
    self.pruned_gene_trees = [te.prune_losses(tree)
                              for tree in self.true_gene_trees]
    
    # sequences should be emptied here if methods were called before
    if hasattr(self, 'sequence_dicts'):
        self.sequence_dicts.clear()
    
    if self.outdir:
        for i in range(n):
            filename = self._path('true_gene_trees',
                                  'gene_tree{}.json'.format(i))
            self.true_gene_trees[i].serialize(filename, mode='json')
    
    return self.true_gene_trees, self.pruned_gene_trees
def simulate_sequences(self, subst_model, root_genome=None, length_distr=('constant', 200), min_length=10, max_length=None, write_fastas=True, write_alignments=True, **kwargs)

Simulate sequences along the (pruned) gene trees.

Parameters

subst_model : SubstModel
Substitution model.
root_genome : list, optional
List of sequences for the roots of the gene trees, must contain the same number of str sequences as trees that were simulated. The sequences must be compatible with the specified substitution model (model_type='n'/'a'). The default is None, in which case new sequences are generated at random.
length_distr : tuple, optional
Distribution of the length of the root sequences if root genome is not supplied. See documentation for available options.
min_length : int, optional
Minimal length at which the distribution of lengths is truncated; must be less than the mean of this distribution. The default is 10.
max_length : int, optional
Maximal length at which the distribution of lengths is truncated; must be less than the mean of this distribution. The default is None, in which case the distribution is not truncated.
write_fastas : bool, optional
If True and an output directory was speci ed, write the sequences (one file per species) into the directory 'fasta_files' in the output directory. The default is True.
write_alignments : bool, optional
If True and an output directory was speci ed, write the true alignments (one file per gene tree) into the directory 'alignments' in the output directory. The default is True.
kwargs : optional
See class 'Evolver' in the 'seqevolve' subpackage or documentation.
Expand source code
def simulate_sequences(self, subst_model,
                       root_genome=None,
                       length_distr=('constant', 200),
                       min_length=10,
                       max_length=None,
                       write_fastas=True,
                       write_alignments=True,
                       **kwargs):
    """Simulate sequences along the (pruned) gene trees.
    
    Parameters
    ----------
    subst_model : SubstModel
        Substitution model.
    root_genome : list, optional
        List of sequences for the roots of the gene trees, must contain the
        same number of str sequences as trees that were simulated. 
        The sequences must be compatible with the specified substitution 
        model (model_type='n'/'a'). The default is None, in which case new
        sequences are generated at random.
    length_distr : tuple, optional
        Distribution of the length of the root sequences if root genome is
        not supplied. See documentation for available options.
    min_length : int, optional
        Minimal length at which the distribution of lengths is truncated;
        must be less than the mean of this distribution. The default is 10.
    max_length : int, optional
        Maximal length at which the distribution of lengths is truncated;
        must be less than the mean of this distribution. The default is
        None, in which case the distribution is not truncated.
    write_fastas : bool, optional
        If True and an output directory was speci ed, write the sequences
        (one file per species) into the directory 'fasta_files' in the
        output directory. The default is True.
    write_alignments : bool, optional
        If True and an output directory was speci ed, write the true 
        alignments (one file per gene tree) into the directory 'alignments'
        in the output directory. The default is True.
    kwargs : optional
        See class 'Evolver' in the 'seqevolve' subpackage or documentation.
    """
    
    self.subst_model = subst_model
    
    if hasattr(self, 'sequence_dicts'):
        self.sequence_dicts.clear()
    else:
        self.sequence_dicts = []
    
    if root_genome:
        if len(root_genome) != len(self.number_of_families):
            raise ValueError('no. of sequences in root genome does not'\
                             'match no of gene families')
    else:
        if isinstance(length_distr, Sampler):
            self.sampler = length_distr
        else:
            self.sampler = Sampler(length_distr,
                                   minimum=min_length,
                                   maximum=max_length,
                                   discrete=True)
    
    evolver = se.Evolver(subst_model, **kwargs)
    
    for i in range(self.number_of_families):
        
        PGT = self.pruned_gene_trees[i]
        
        if root_genome:
            evolver.evolve_along_tree(PGT, start_seq=root_genome[i].upper())
            
        else:
            evolver.evolve_along_tree(PGT, start_length=self.sampler())
            
        self.sequence_dicts.append(evolver.sequences)
        
        # write one alignment file per gene family
        if self.outdir and write_alignments:
            self._write_alignment(i)
    
    # write one fasta file per species
    if self.outdir and write_fastas:
        self._write_fastas(include_inner=False)