Source code for stdpopsim.species

"""
Infrastructure for defining basic information about species and
organising the species catalog.
"""

import logging
import warnings

import attr

import stdpopsim
import stdpopsim.utils

logger = logging.getLogger(__name__)

registered_species = {}


def _missing_from_catalog(type_of_thing, thing_id, available_things):
    """
    Return a user-friendly message if the user requests an identifier not in the
    catalog

    :param str type_of_thing: The kind of requested object (e.g. species,
         genetic map, etc.)
    :param str thing_id: the string identifier of the requested object.
    :param list available_things: a list of string identifiers that are
         available.
    """
    avail_str = ", ".join(available_things)
    return f"{type_of_thing} '{thing_id}' not in catalog ({avail_str})"


def register_species(species):
    """
    Registers the specified ``species``.

    :param species: The species to be registered.
    :type: :class:`Species`
    """
    if species.id in registered_species:
        raise ValueError(f"{species.id} already registered.")
    logger.debug(f"Registering species '{species.id}'")
    registered_species[species.id] = species


[docs]def get_species(id): """ Returns a :class:`Species` object for the specified ``id``. :param str id: The string identifier for the requested species. E.g. "HomSap". A complete list of species, and their IDs, can be found in the :ref:`sec_catalog`. :return: An object containing the species definition. :rtype: :class:`Species` """ if id not in registered_species: # TODO we should probably have a custom exception here and standardise # on using these for all the catalog search functions. raise ValueError(_missing_from_catalog("Species", id, registered_species)) return registered_species[id]
# Convenience methods for getting all the species/genetic maps/models # we have defined in the catalog. def all_species(): """ Returns an iterator over all species in the catalog sorted by ID. """ for species in sorted(registered_species.keys()): yield registered_species[species] def all_genetic_maps(): for species in all_species(): for genetic_map in species.genetic_maps: yield genetic_map def all_demographic_models(): for species in all_species(): for model in species.demographic_models: yield model def all_dfes(): for species in all_species(): for dfe in species.dfes: yield dfe def all_annotations(): for species in all_species(): for an in species.annotations: yield an
[docs]@attr.s() class Species: """ Class representing a species in the catalog. :ivar ~.id: The unique identifier for this species. The species ID is the three first letters of the genus name followed by the first three letters of the species name, and does not contain any spaces or punctuation. The usual scheme is to use the first three letters of the genus and species (similar to the approach used in the UCSC genome browser), e.g., "HomSap" is the ID for Homo Sapiens. :vartype ~.id: str :ivar name: The full name of this species in binominal nomenclature as it would be used in written text, e.g., "Homo sapiens". :vartype name: str :ivar common_name: The name of this species as it would most often be used informally in written text, e.g., "human", or "Orang-utan". Where no common name for the species exist, use the most common abbreviation, e.g., "E. Coli". :vartype common_name: str :ivar genome: The :class:`.Genome` instance describing the details of this species' genome. :vartype genome: stdpopsim.Genome :ivar generation_time: The current best estimate for the generation time of this species in years. Note that individual demographic models in the catalog may or may not use this estimate: each model uses the generation time that was used in the original publication(s). :vartype generation_time: float :ivar ploidy: The ploidy of the organism. :vartype ploidy: int :ivar population_size: The current best estimate for the population size of this species. Note that individual demographic models in the catalog may or may not use this estimate: each model uses the population sizes defined in the original publication(s). :vartype population_size: float :ivar citations: A list of :class:`.Citation` objects providing the source for the generation time and population size estimates. :vartype citations: list :ivar demographic_models: This list of :class:`DemographicModel` instances in the catalog for this species. :vartype demographic_models: list :ivar dfes: This list of :class:`DFE` instances in the catalog for this species. :vartype dfes: list :ivar ensembl_id: The ensembl id for the species which is used by maintenance scripts to query ensembl's database. :vartype ensembl_id: str """ id = attr.ib(type=str, kw_only=True) name = attr.ib(type=str, kw_only=True) common_name = attr.ib(type=str, kw_only=True) genome = attr.ib(type=int, kw_only=True) generation_time = attr.ib(default=0, kw_only=True) ploidy = attr.ib(default=2, type=int, kw_only=True) population_size = attr.ib(default=0, kw_only=True) demographic_models = attr.ib(factory=list, kw_only=True) dfes = attr.ib(factory=list, kw_only=True) ensembl_id = attr.ib(type=str, kw_only=True) citations = attr.ib(factory=list, kw_only=True) # A list of genetic maps. This is undocumented as the parameter is not # intended to be used when the Species is initialised. # Use add_genetic_map() instead. genetic_maps = attr.ib(factory=list, kw_only=True) annotations = attr.ib(factory=list, kw_only=True)
[docs] def get_contig( self, chromosome=None, genetic_map=None, length_multiplier=None, length=None, mutation_rate=None, recombination_rate=None, use_species_gene_conversion=False, inclusion_mask=None, exclusion_mask=None, left=None, right=None, ): """ Returns a :class:`.Contig` instance describing a section of genome that is to be simulated based on empirical information for a given species and chromosome. *Coordinates:* If a particular chunk of a given chromosome is obtained (by specifying a `chromosome` ID and `left` and `right` coordinates), the resulting genomes will have coordinates matching the original (full) genome, and portions of the genome outside of `[left, right)` will be masked (and so contain missing data). So, coordinates of `genetic_map` and masks should be in coordinates of the original genome (so, not shifted to be relative to `left`). If it is desired to have output coordinates relative to `left`, use the :meth:`tskit.TreeSequence.trim` method on the result (for instance, `ts_shifted = ts.trim()`). :param str chromosome: The ID of the chromosome to simulate. A complete list of chromosome IDs for each species can be found in the "Genome" subsection for the species in the :ref:`sec_catalog`. If the chromosome is not given, we specify a "generic" contig with given ``length``. :param str genetic_map: If specified, obtain recombination rate information from the genetic map with the specified ID. If None, simulate using a default uniform recombination rate on a region with the length of the specified chromosome. The default rates are species- and chromosome- specific, and can be found in the :ref:`sec_catalog`. (Default: None) :param float length_multiplier: Deprecated, use `left` and `right` instead. If specified, simulate a region of length `length_multiplier` times the length of the specified chromosome with the same chromosome-specific mutation and recombination rates. This option cannot be used in conjunction with the ``genetic_map`` argument. :param float mutation_rate: The per-base mutation rate. If none is given, the mutation rate defaults to the rate specified by species chromosomes. :param float recombination_rate: The per-base recombination rate. If none is given, the recombination rate defaults to the rate specified by species chromosomes. Ignored when ``genetic_map`` argument is specified. :param bool use_species_gene_conversion: If set to True the parameters for gene conversion of the species chromosome are used if available. For "generic" contigs the gene conversion fraction and length are given by the mean values across all chromosomes. :param inclusion_mask: If specified, simulated genomes are subset to only inlude regions given by the mask. The mask can be specified by the path and file name of a bed file or as a list or array of intervals given by the left and right end points of the intervals. :param exclusion_mask: If specified, simulated genomes are subset to exclude regions given by the mask. The mask can be specified by the path and file name of a bed file or as a list or array of intervals given by the left and right end points of the intervals. :param float length: Used with a "generic" contig, specifies the length of genome sequence for this contig. For a generic contig, mutation and recombination rates are equal to the genome-wide average across all autosomal chromosomes. :param float left: The left coordinate (inclusive) of the region to keep on the chromosome. Defaults to 0. Remaining regions will have missing data when simulated. :param float right: The right coordinate (exclusive) of the region to keep on the chromosome. Defaults to the length of the chromosome. Remaining regions will have missing data when simulated. :rtype: :class:`.Contig` :return: A :class:`.Contig` describing the section of the genome. """ return stdpopsim.Contig.species_contig( species=self, chromosome=chromosome, genetic_map=genetic_map, length_multiplier=length_multiplier, length=length, mutation_rate=mutation_rate, recombination_rate=recombination_rate, use_species_gene_conversion=use_species_gene_conversion, inclusion_mask=inclusion_mask, exclusion_mask=exclusion_mask, left=left, right=right, )
def _warn_browning(self, model_id): if model_id == "AmericanAdmixture_4B11": warnings.warn( "In stdpopsim <= 0.2.1, the AmericanAdmixture_4B18 model " "was named AmericanAdmixture_4B11; but since it comes " "from a 2018 paper, this is corrected. The model name " "AmericanAdmixture_4B11 will work for now but is deprecated." ) model_id = "AmericanAdmixture_4B18" return model_id
[docs] def get_demographic_model(self, id): """ Returns a demographic model with the specified ``id``. :param str id: The string identifier for the demographic model. A complete list of IDs for each species can be found in the "Demographic Models" subsection for the species in the :ref:`sec_catalog`. :rtype: :class:`DemographicModel` :return: A :class:`DemographicModel` that defines the requested model. """ # TODO: remove this after a release or two. See #841. id = self._warn_browning(id) for model in self.demographic_models: if model.id == id: return model available_models = [dm.id for dm in self.demographic_models] raise ValueError( _missing_from_catalog( "DemographicModel", f"{self.id}/{id}", available_models ) )
def add_demographic_model(self, model): if model.id in [m.id for m in self.demographic_models]: raise ValueError( f"DemographicModel '{self.id}/{model.id}' already in catalog." ) self.demographic_models.append(model)
[docs] def get_dfe(self, id): """ Returns a DFE with the specified ``id``. :param str id: The string identifier for the DFE. A complete list of IDs for each species can be found in the # TODO add that section to the species catalog "DFE" subsection for the species in the :ref:`sec_catalog`. :rtype: :class:`DFE` :return: A :class:`DFE` that defines the requested model. """ for dfe in self.dfes: if dfe.id == id: return dfe available_dfes = [d.id for d in self.dfes] raise ValueError( _missing_from_catalog("DFE", f"{self.id}/{id}", available_dfes) )
def add_dfe(self, dfe): if dfe.id in [d.id for d in self.dfes]: raise ValueError(f"DFE '{self.id}/{dfe.id}' already in catalog.") self.dfes.append(dfe) def add_genetic_map(self, genetic_map): if genetic_map.id in [gm.id for gm in self.genetic_maps]: raise ValueError( f"Genetic map '{self.id}/{genetic_map.id}' already in catalog." ) genetic_map.species = self self.genetic_maps.append(genetic_map) def get_genetic_map(self, id): # NOTE: Undocumenting this method as the GeneticMap API isn't part of the # supported public interface. # # Returns a genetic map (AKA. recombination map) with the specified ``id``. # :param str id: The string identifier for the genetic map. # A complete list of IDs for each species can be found in the # "Genetic Maps" subsection for the species in the :ref:`sec_catalog`. # :rtype: :class:`GeneticMap` # :return: A :class:`GeneticMap` with recombination rate across the genome. for gm in self.genetic_maps: if gm.id == id: return gm available_maps = [gm.id for gm in self.genetic_maps] raise ValueError( _missing_from_catalog("GeneticMap", f"{self.id}/{id}", available_maps) ) def add_annotations(self, annotations): if annotations.id in [an.id for an in self.annotations]: raise ValueError( f"Annotations '{self.id}/{annotations.id}' already in catalog." ) annotations.species = self self.annotations.append(annotations)
[docs] def get_annotations(self, id): """ Returns a set of annotations with the specified ``id``. :param str id: The string identifier for the set of annotations A complete list of IDs for each species can be found in the "Annotations" subsection for the species in the :ref:`sec_catalog`. :rtype: :class:`Annotation` :return: A :class:`Annotation` that holds genome annotation information from Ensembl """ for an in self.annotations: if an.id == id: return an available_anno = [anno.id for anno in self.annotations] raise ValueError( _missing_from_catalog("Annotations", f"{self.id}/{id}", available_anno) )