diff --git a/CHANGELOG.md b/CHANGELOG.md index 6af8d4dbe..380098220 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,13 @@ # Changelog +## [1.0.4] - 2021-12-01 + +**New features**: + +- Support for Demes 0.2.0, which introduces a change to how pulse + sources and proportions are specified. + ({pr}`1936`, {issue}`1930`, {user}`apragsdale`) + ## [1.0.3] - 2021-11-12 This is a bugfix release recommended for all users. diff --git a/msprime/demography.py b/msprime/demography.py index bfa4005b4..da1294f35 100644 --- a/msprime/demography.py +++ b/msprime/demography.py @@ -1982,12 +1982,14 @@ def get_growth_rate(epoch): # Add pulses in reverse order, so that pulses with the same time # correspond to the correct backwards-time mass migration ordering. for pulse in reversed(events.pop("pulses")): - demography.add_mass_migration( - time=pulse.time, - source=pulse.dest, - dest=pulse.source, - proportion=pulse.proportion, - ) + sequential_props = _proportions_to_sequential(pulse.proportions) + for prop, source in zip(sequential_props, pulse.sources): + demography.add_mass_migration( + time=pulse.time, + source=pulse.dest, + dest=source, + proportion=prop, + ) for merger in events.pop("mergers"): demography.add_admixture( time=merger.time, @@ -2710,9 +2712,9 @@ def epoch_resolve(deme, time): for lm in lineage_movements: if (lm.source, lm.dest, lm.proportion) in pulses: b.add_pulse( - source=resolved[lm.dest].name, + sources=[resolved[lm.dest].name], dest=resolved[lm.source].name, - proportion=lm.proportion, + proportions=[lm.proportion], time=time, ) diff --git a/requirements/CI-complete/requirements.txt b/requirements/CI-complete/requirements.txt index 99fb24e0a..64a2c772e 100644 --- a/requirements/CI-complete/requirements.txt +++ b/requirements/CI-complete/requirements.txt @@ -1,7 +1,7 @@ bintrees==2.2.0 codecov==2.1.12 daiquiri==3.0.1 -demes==0.1.2 +demes==0.2.0 hypothesis==6.23.2 mypy==0.910 newick==1.3.1 diff --git a/requirements/CI-docs/requirements.txt b/requirements/CI-docs/requirements.txt index ab7839cb3..2c6aa4df5 100644 --- a/requirements/CI-docs/requirements.txt +++ b/requirements/CI-docs/requirements.txt @@ -1,6 +1,6 @@ daiquiri==3.0.1 -demes==0.1.2 -demesdraw==0.1.4 +demes==0.2.0 +demesdraw==0.2.0 jupyter-book==0.12.1 matplotlib==3.5.0 newick==1.3.1 diff --git a/requirements/CI-tests-conda/requirements.txt b/requirements/CI-tests-conda/requirements.txt index c97ad5885..125381044 100644 --- a/requirements/CI-tests-conda/requirements.txt +++ b/requirements/CI-tests-conda/requirements.txt @@ -5,4 +5,4 @@ jsonschema<4.0 gsl tskit==0.3.7 stdpopsim==0.1.2 -demes==0.1.2 +demes==0.2.0 diff --git a/requirements/CI-tests-pip/requirements.txt b/requirements/CI-tests-pip/requirements.txt index a688a52d6..406b1c2b2 100644 --- a/requirements/CI-tests-pip/requirements.txt +++ b/requirements/CI-tests-pip/requirements.txt @@ -1,6 +1,6 @@ bintrees==2.2.0 daiquiri==3.0.1 -demes==0.1.2 +demes==0.2.0 hypothesis==6.29.0 newick==1.3.0 numpy==1.21.4 diff --git a/requirements/development.txt b/requirements/development.txt index de254ed4c..869e99431 100644 --- a/requirements/development.txt +++ b/requirements/development.txt @@ -4,7 +4,7 @@ bintrees codecov coverage daiquiri -demes>=0.1.2 +demes>=0.2.0 demesdraw flake8 hypothesis diff --git a/setup.cfg b/setup.cfg index 11d9a37e0..7fdd5a2a9 100644 --- a/setup.cfg +++ b/setup.cfg @@ -49,7 +49,7 @@ install_requires = numpy newick>=1.3.0 tskit>=0.3.5 - demes<0.2 + demes>=0.2 setup_requires = numpy setuptools diff --git a/tests/test_demes.py b/tests/test_demes.py index a88a5f931..a7a602057 100644 --- a/tests/test_demes.py +++ b/tests/test_demes.py @@ -286,14 +286,14 @@ def test_pulses(self): epochs: - start_size: 2000 pulses: - - source: X + - sources: [X] dest: A time: 500 - proportion: 0.1 - - source: A + proportions: [0.1] + - sources: [A] dest: X time: 100 - proportion: 0.2 + proportions: [0.2] """ d = self.from_yaml(yaml) assert d.num_populations == 2 @@ -340,10 +340,10 @@ def test_pulses_ordering(self): - name: D - name: E pulses: - - {source: A, dest: B, time: 100, proportion: 0.1} - - {source: B, dest: C, time: 100, proportion: 0.2} - - {source: C, dest: D, time: 100, proportion: 0.3} - - {source: A, dest: E, time: 200, proportion: 0.4} + - {sources: [A], dest: B, time: 100, proportions: [0.1]} + - {sources: [B], dest: C, time: 100, proportions: [0.2]} + - {sources: [C], dest: D, time: 100, proportions: [0.3]} + - {sources: [A], dest: E, time: 200, proportions: [0.4]} """ d = self.from_yaml(yaml) assert d.num_populations == 5 @@ -367,6 +367,24 @@ def test_pulses_ordering(self): assert d.events[3].dest == "A" assert d.events[3].proportion == 0.4 + def test_multipulse(self): + b = demes.Builder(defaults=dict(epoch=dict(start_size=1))) + b.add_deme("a") + b.add_deme("b") + b.add_deme("c") + b.add_pulse(sources=["a", "b"], proportions=[0.1, 0.2], dest="c", time=1) + g = b.resolve() + d = msprime.Demography.from_demes(g) + assert len(d.events) == 2 + assert isinstance(d.events[0], msprime.MassMigration) + assert isinstance(d.events[1], msprime.MassMigration) + assert d.events[0].source == "c" + assert d.events[0].dest == "a" + assert d.events[0].proportion == 0.1 + assert d.events[1].source == "c" + assert d.events[1].dest == "b" + assert d.events[1].proportion == 0.2 / (1 - 0.1) + def test_merger(self): yaml = """\ time_units: generations @@ -865,9 +883,9 @@ def test_pulse(self): assert len(graph.pulses) == 1 pulse = graph.pulses[0] assert pulse.time == 100 - assert pulse.source == "pop_1" + assert pulse.sources[0] == "pop_1" assert pulse.dest == "pop_0" - assert pulse.proportion == 0.1 + assert pulse.proportions[0] == 0.1 @pytest.mark.filterwarnings( # demes warns about multiple pulses with the same time @@ -884,21 +902,21 @@ def test_pulse_ordering(self): assert len(graph.migrations) == 0 assert len(graph.pulses) == 4 pulses = graph.pulses - assert pulses[0].source == "pop_4" + assert pulses[0].sources[0] == "pop_4" assert pulses[0].dest == "pop_3" - assert pulses[0].proportion == 0.4 + assert pulses[0].proportions[0] == 0.4 assert pulses[0].time == 200 - assert pulses[1].source == "pop_3" + assert pulses[1].sources[0] == "pop_3" assert pulses[1].dest == "pop_2" - assert pulses[1].proportion == 0.3 + assert pulses[1].proportions[0] == 0.3 assert pulses[1].time == 100 - assert pulses[2].source == "pop_2" + assert pulses[2].sources[0] == "pop_2" assert pulses[2].dest == "pop_1" - assert pulses[2].proportion == 0.2 + assert pulses[2].proportions[0] == 0.2 assert pulses[2].time == 100 - assert pulses[3].source == "pop_1" + assert pulses[3].sources[0] == "pop_1" assert pulses[3].dest == "pop_0" - assert pulses[3].proportion == 0.1 + assert pulses[3].proportions[0] == 0.1 assert pulses[3].time == 100 def test_bad_pulse(self): @@ -1018,6 +1036,49 @@ def test_census_event_is_ignored(self): demog.add_census(100) demog.to_demes() + @pytest.mark.filterwarnings( + # demes warns about multiple pulses with the same time + "ignore:Multiple pulses:UserWarning" + ) + def test_multipulse_roundtrip_two_sources(self): + b = demes.Builder(defaults=dict(epoch=dict(start_size=1))) + b.add_deme("a") + b.add_deme("b") + b.add_deme("c") + b.add_pulse(sources=["a", "b"], proportions=[0.1, 0.2], dest="c", time=1) + g = b.resolve() + d = msprime.Demography.from_demes(g) + g2 = d.to_demes() + assert len(g2.pulses) == 2 + assert g2.pulses[0].sources[0] == "b" + assert g2.pulses[0].proportions[0] == 0.2 / (1 - 0.1) + assert g2.pulses[1].sources[0] == "a" + assert g2.pulses[1].proportions[0] == 0.1 + + @pytest.mark.filterwarnings( + # demes warns about multiple pulses with the same time + "ignore:Multiple pulses:UserWarning" + ) + def test_multipulse_roundtrip_three_sources(self): + b = demes.Builder(defaults=dict(epoch=dict(start_size=1))) + b.add_deme("a") + b.add_deme("b") + b.add_deme("c") + b.add_deme("d") + b.add_pulse( + sources=["a", "b", "c"], proportions=[0.1, 0.2, 0.3], dest="d", time=1 + ) + g = b.resolve() + d = msprime.Demography.from_demes(g) + g2 = d.to_demes() + assert len(g2.pulses) == 3 + assert g2.pulses[0].sources[0] == "c" + assert g2.pulses[0].proportions[0] == 0.3 / (1 - 0.1 - 0.2) + assert g2.pulses[1].sources[0] == "b" + assert g2.pulses[1].proportions[0] == 0.2 / (1 - 0.1) + assert g2.pulses[2].sources[0] == "a" + assert g2.pulses[2].proportions[0] == 0.1 + # class TestRoundTrip: # def remove_selfing_and_cloning(self, graph):