Skip to content

Commit

Permalink
Merge pull request #1503 from mcveanlab/alpha5
Browse files Browse the repository at this point in the history
Batch of updates for alpha 5.
  • Loading branch information
jeromekelleher authored Mar 2, 2021
2 parents 9d0104a + ddfde08 commit 2516ee2
Show file tree
Hide file tree
Showing 5 changed files with 106 additions and 44 deletions.
11 changes: 10 additions & 1 deletion CHANGELOG.rst
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
********************
1.0.0a5 - 2021-XX-XX
1.0.0b0 - 2021-XX-XX
********************

** This changelog is incomplete and needs to be reviewed and updated **
Expand Down Expand Up @@ -42,6 +42,15 @@
- Deprecate module attributes that were moved to tskit.
(:user:`benjeffery`, :issue:`991`, :pr:`1158`)

********************
1.0.0a5 - 2021-03-02
********************

Fifth alpha release of 1.0 for early testing and evaluation.

- Fixes bug in rate map for mutations (:issue:`1470`)
- Add draft of new Demography API.

********************
1.0.0a4 - 2021-02-01
********************
Expand Down
12 changes: 11 additions & 1 deletion msprime/ancestry.py
Original file line number Diff line number Diff line change
Expand Up @@ -217,6 +217,7 @@ def _demography_factory(
migration_matrix=migration_matrix,
demographic_events=demographic_events,
Ne=Ne,
ignore_sample_size=True,
)
return demography.validate()

Expand Down Expand Up @@ -992,13 +993,13 @@ def _parse_sim_ancestry(
def sim_ancestry(
samples=None,
*,
demography=None,
sequence_length=None,
discrete_genome=None,
recombination_rate=None,
gene_conversion_rate=None,
gene_conversion_tract_length=None,
population_size=None,
demography=None,
ploidy=None,
model=None,
initial_state=None,
Expand Down Expand Up @@ -1028,6 +1029,15 @@ def sim_ancestry(
is usually associated with :math:`k` sample *nodes* (or genomes) when
``ploidy`` = :math:`k`. See :ref:`sec_ancestry_samples` for further details.
Either ``samples`` or ``initial_state`` must be specified.
:param demography: The demographic model to simulate, describing the
extant and ancestral populations, their population sizes and growth
rates, their migration rates, and demographic events affecting the
populations over time. See the :ref:`sec_demography` section for
details on how to specify demographic models and
:ref:`sec_ancestry_samples` for details on how to specify the
populations that samples are drawn from. If not specified (or None) we
default to a single population with constant size 1
(see also the ``population_size`` parameter).
:param int ploidy: The number of monoploid genomes per sample individual
(Default=2). See :ref:`sec_ancestry_ploidy` for usage examples.
:param float sequence_length: The length of the genome sequence to simulate.
Expand Down
2 changes: 1 addition & 1 deletion msprime/core.py
Original file line number Diff line number Diff line change
Expand Up @@ -165,7 +165,7 @@ def html_table(
.tskit-table tbody td {text-align: right;padding: 0.5em 0.5em;}
.tskit-table tbody th {padding: 0.5em 0.5em;}
</style>"""
f"<h4>{caption}</h4>"
f"<b>{caption}</b>"
'<table border="1" class="tskit-table">'
"<thead>"
"<tr>" + header + "</tr>"
Expand Down
95 changes: 62 additions & 33 deletions msprime/demography.py
Original file line number Diff line number Diff line change
Expand Up @@ -499,19 +499,20 @@ def add_census(self, time: float) -> CensusEvent:
return self.add_event(CensusEvent(time))

def _populations_table(self):
col_titles = [
"id",
"name",
"description",
"initial_size",
"growth_rate",
"sampling_time",
"extra_metadata",
cols = [
("id", ""),
("name", ""),
("description", ""),
("initial_size", ".1f"),
("growth_rate", ".2f"),
("sampling_time", ".2g"),
("extra_metadata", ""),
]
data = [
[f"{getattr(pop, attr)}" for attr in col_titles] for pop in self.populations
[f"{getattr(pop, attr):{fmt}}" for attr, fmt in cols]
for pop in self.populations
]
return col_titles, data
return [title for title, _ in cols], data

def _populations_text(self):
col_titles, data = self._populations_table()
Expand All @@ -534,11 +535,14 @@ def _populations_html(self):
def _migration_rate_info(self, source, dest, rate):
extra = None
if source != dest:
source_name = self.populations[source].name
dest_name = self.populations[dest].name
extra = (
"Backwards in time migration rate from population "
f"{self.populations[source].name} to {self.populations[dest].name} "
f"= {rate} per generation. "
"Equivalant to **IMPLEMENT ME** forwards in time"
f"{source_name} to {dest_name} = {rate} per generation. "
"Forwards in time, this is the expected number of migrants "
f"moving from {dest_name} to {source_name} "
f"per generation, divided by the size of {source_name}."
)
return core.TableEntry(f"{rate:.4g}", extra)

Expand Down Expand Up @@ -602,11 +606,11 @@ def _events_html(self, events, title="Events"):
def _repr_html_(self):
resolved = self.validate()
return (
"<p>"
'<div style="margin-left:20px">'
+ resolved._populations_html()
+ resolved._migration_matrix_html()
+ resolved._events_html(self.events)
+ "</p>"
+ "</div>"
)

def __str__(self):
Expand Down Expand Up @@ -1040,7 +1044,7 @@ def from_species_tree(
time_units="gen",
generation_time=None,
growth_rate=None,
):
) -> Demography:
"""
Parse a species tree in `Newick
<https://en.wikipedia.org/wiki/Newick_format>`_ format and return the
Expand Down Expand Up @@ -1112,7 +1116,7 @@ def from_species_tree(
)

@staticmethod
def from_starbeast(tree, generation_time, time_units="myr"):
def from_starbeast(tree, generation_time, time_units="myr") -> Demography:
"""
Parse a species tree produced by the program `TreeAnnotator
<https://www.beast2.org/treeannotator>`_
Expand Down Expand Up @@ -1168,7 +1172,7 @@ def _from_old_style_map_populations(
migration_matrix: List[List[float]],
demographic_events: List[DemographicEvent],
population_map: [List[Dict[int, str]]],
):
) -> Demography:
direct_model = Demography._from_old_style_simple(
population_configurations, migration_matrix, demographic_events
)
Expand Down Expand Up @@ -1384,7 +1388,7 @@ def _from_old_style_simple(
population_configurations=None,
migration_matrix=None,
demographic_events=None,
):
) -> Demography:
"""
Creates a Demography object from the pre 1.0 style input parameters,
reproducing the old semantics with respect to default values.
Expand All @@ -1406,11 +1410,25 @@ def from_old_style(
migration_matrix=None,
demographic_events=None,
Ne=1,
ignore_sample_size=False,
population_map: Union[[List[Dict[int, Union[str, int]]]], None] = None,
):
) -> Demography:
"""
Creates a Demography object from the pre 1.0 style input parameters,
reproducing the old semantics with respect to default values.
No sample information is stored in the new-style :class:`.Demography`
objects, and therefore if the ``sample_size`` attribute of any
of the input :class:`.PopulationConfiguration` objects is set a
ValueError will be raised by default. However, if the
``ignore_sample_size`` parameter is set to True, this check will
not be performed and the sample sizes specified in the old-style
:class:`.PopulationConfiguration` objects will be ignored.
Please see the :ref:`sec_ancestry_samples` section for details on
how to specify sample locations in :func:`.sim_ancestry`.
.. todo:: Document the remaining parameters.
"""
if population_configurations is None:
pop_configs = [PopulationConfiguration(initial_size=Ne)]
Expand All @@ -1419,6 +1437,18 @@ def from_old_style(
for pop_config in pop_configs:
if pop_config.initial_size is None:
pop_config.initial_size = Ne

if pop_config.sample_size is not None and not ignore_sample_size:
raise ValueError(
"You have specified a `sample_size` in a "
"PopulationConfiguration object that is to be converted "
"into a new-style Demography object, "
"which does not contain any information about samples. "
"Please use the ``samples`` argument to sim_ancestry "
"instead, which provides flexible options for sampling "
"from different populations"
)

if population_map is None:
return Demography._from_old_style_simple(
pop_configs, migration_matrix, demographic_events
Expand All @@ -1432,7 +1462,7 @@ def from_old_style(
)

@staticmethod
def isolated_model(initial_size, *, growth_rate=None):
def isolated_model(initial_size, *, growth_rate=None) -> Demography:
"""
Returns a :class:`.Demography` object representing a collection of
isolated populations with specified initial population sizes and
Expand Down Expand Up @@ -1480,7 +1510,7 @@ def isolated_model(initial_size, *, growth_rate=None):
return Demography(populations=populations)

@staticmethod
def island_model(initial_size, migration_rate, *, growth_rate=None):
def island_model(initial_size, migration_rate, *, growth_rate=None) -> Demography:
"""
Returns a :class:`.Demography` object representing a collection of
populations with specified initial population sizes and growth
Expand Down Expand Up @@ -1510,7 +1540,7 @@ def island_model(initial_size, migration_rate, *, growth_rate=None):
@staticmethod
def stepping_stone_model(
initial_size, migration_rate, *, growth_rate=None, boundaries=False
):
) -> Demography:
"""
Returns a :class:`.Demography` object representing a collection of
populations with specified initial population sizes and growth
Expand Down Expand Up @@ -2590,6 +2620,7 @@ def __init__(
migration_matrix=migration_matrix,
demographic_events=demographic_events,
Ne=Ne,
ignore_sample_size=True,
)
self.demography = demography.validate()
self.num_populations = demography.num_populations
Expand Down Expand Up @@ -2708,21 +2739,19 @@ def _repr_html_(self):
for epoch in self.epochs:
if epoch.index > 0:
assert len(epoch.events) > 0
title = f"Events @ generation {epoch.start_time}"
title = f"Events @ generation {epoch.start_time:.3g}"
out += self.demography._events_html(epoch.events, title)
out += "</div>"
out += "</div></details>"
else:
assert len(epoch.events) == 0
out += '<div class="msprime-epoch">'
out += f"<h3>{epoch._title_text()}</h3>"
title = epoch._title_text()
out += f'<details open="true"><summary>{title}</summary>'
# Indent the content div slightly
out += '<div style="margin-left:20px">'
out += self._populations_html(epoch)
out += "</div>"
return f"""<div>
<style scoped="">
.msprime-epoch:nth-child(odd) {{background: #f5f5f5;}}
</style>
{out}
</div>"""
out += "</details>"
return f"<div>{out}</div>"

def print_history(self, output=sys.stdout):
"""
Expand Down
30 changes: 22 additions & 8 deletions tests/test_demography.py
Original file line number Diff line number Diff line change
Expand Up @@ -1211,7 +1211,7 @@ def test_census(self):

class DebugOutputBase:
def test_zero_samples_old_style(self):
population_configurations = [msprime.PopulationConfiguration(0)]
population_configurations = [msprime.PopulationConfiguration()]
self.verify(msprime.Demography.from_old_style(population_configurations))

def test_one_population(self):
Expand Down Expand Up @@ -1256,7 +1256,7 @@ class TestDemographyHtml(DebugOutputBase):
def verify(self, demography):
html = demography._repr_html_()
root = xml.etree.ElementTree.fromstring(html)
assert root.tag == "p"
assert root.tag == "div"
children = list(root)
assert len(children) == 3
for child in children:
Expand All @@ -1281,9 +1281,14 @@ def verify(self, demography):
html = debugger._repr_html_()
root = xml.etree.ElementTree.fromstring(html)
assert root.tag == "div"
children = list(root)
assert len(children) - 1 == len(debugger.epochs)
# TODO add more tests when the output format is finalised.
root_children = list(root)
assert len(root_children) == len(debugger.epochs)
for details, epoch in zip(root_children, debugger.epochs):
assert details.tag == "details"
children = list(details)
assert children[0].tag == "summary"
assert children[1].tag == "div"
assert children[0].text == epoch._title_text()


class TestDemographyText(DebugOutputBase):
Expand Down Expand Up @@ -1323,7 +1328,7 @@ def test_one_population(self):
║ ┌────────────────────────────────────────────────────────────────────────────────────────┐
║ │ id │name │description │initial_size │ growth_rate │ sampling_time│extra_metadata │
║ ├────────────────────────────────────────────────────────────────────────────────────────┤
║ │ 0 │pop_0 │ │10.0 │ 0.0 │ 0│{} │
║ │ 0 │pop_0 │ │10.0 │ 0.00 │ 0│{} │
║ └────────────────────────────────────────────────────────────────────────────────────────┘
╟ Migration Matrix
║ ┌───────────────┐
Expand Down Expand Up @@ -1351,8 +1356,8 @@ def test_two_populations(self):
║ ┌────────────────────────────────────────────────────────────────────────────────────────┐
║ │ id │name │description │initial_size │ growth_rate │ sampling_time│extra_metadata │
║ ├────────────────────────────────────────────────────────────────────────────────────────┤
║ │ 0 │pop_0 │ │10.0 │ 1.0 │ 0│{} │
║ │ 1 │pop_1 │ │20.0 │ 2.0 │ 0│{} │
║ │ 0 │pop_0 │ │10.0 │ 1.00 │ 0│{} │
║ │ 1 │pop_1 │ │20.0 │ 2.00 │ 0│{} │
║ └────────────────────────────────────────────────────────────────────────────────────────┘
╟ Migration Matrix
║ ┌───────────────────────┐
Expand Down Expand Up @@ -4413,6 +4418,15 @@ def test_pop_configs_defaults(self):
np.testing.assert_array_equal(demog.migration_matrix, np.zeros((n, n)))
assert list(demog.events) == []

def test_ignore_sample_size(self):
pop_configs = [msprime.PopulationConfiguration(sample_size=1)]
with pytest.raises(ValueError, match="You have specified a `sample_size`"):
msprime.Demography.from_old_style(population_configurations=pop_configs)
demog = msprime.Demography.from_old_style(
population_configurations=pop_configs, ignore_sample_size=True
)
assert demog.num_populations == 1

def test_migration_matrix(self):
for n in range(1, 5):
pop_configs = [msprime.PopulationConfiguration() for _ in range(n)]
Expand Down

0 comments on commit 2516ee2

Please sign in to comment.