From 4848345a61e0a61c70e5c96a6f9071639a1866cd Mon Sep 17 00:00:00 2001 From: Aaron Ragsdale Date: Thu, 21 Jan 2021 15:53:24 -0600 Subject: [PATCH] update to follow spec --- demes/demes.py | 66 ++++++------ examples/browning_america.yml | 4 + examples/gutenkunst_ooa.yml | 3 + examples/jacobs_papuans.yml | 4 + examples/offshoots.yml | 3 + tests/test_demes.py | 187 ++++++++++++++++++---------------- tests/test_load_dump.py | 56 ++++++---- tests/test_moments.py | 88 ++++++++++++---- 8 files changed, 255 insertions(+), 156 deletions(-) diff --git a/demes/demes.py b/demes/demes.py index 18c8bfac..ffd572a5 100644 --- a/demes/demes.py +++ b/demes/demes.py @@ -1366,60 +1366,68 @@ def deme( This must be provided. :param final_size: The final population size of the deme. If ``None``, the deme has a constant ``initial_size`` population size. - :param float selfing_rate: The default selfing rate for this deme. - May be ``None``. - :param float cloning_rate: The default cloning rate for this deme. - May be ``None``. :param epochs: Epochs that define population sizes, selfing rates, and cloning rates, for the deme over various time periods. If not specified, a single epoch will be created for the deme that spans from ``start_time`` to ``end_time``, using the ``initial_size``, ``final_size``, ``selfing_rate`` and ``cloning_rate`` provided. + :param defaults: Default attributes for epochs, including cloning_rate + and selfing_rate. :return: Newly created deme. :rtype: :class:`.Deme` """ + # some basic deme property checks if id in self: raise ValueError(f"deme {id} already exists in this graph") + if epochs is None: + raise ValueError(f"deme {id} must have at least one specified epoch") if initial_size is None and epochs is not None: initial_size = epochs[0].initial_size if initial_size is None: - raise ValueError(f"must set initial_size for {id}") + raise ValueError(f"must set initial_size for deme {id}") if ancestors is None: ancestors = [] if not isinstance(ancestors, list): - raise TypeError("ancestors must be a list of deme IDs") + raise TypeError(f"ancestors must be a list of deme IDs for deme {id}") for ancestor in ancestors: if ancestor not in self: - raise ValueError(f"ancestor deme {ancestor} not in graph") + raise ValueError(f"ancestor deme {ancestor} not in graph for deme {id}") if proportions is None: if len(ancestors) == 1: proportions = [1.0] else: proportions = [] - if epochs is None: - raise ValueError("Demes must have at least one specified epoch") - # set the start time to first epoch's start time, to inf or to - # the ancestor's end time, if not given + # set the start time to first epoch's start time. + # if first epoch does not have a start time, set to inf or to + # the ancestor's end time if start_time is None: - if epochs is not None and epochs[0].start_time is not None: + if epochs[0].start_time is not None: start_time = epochs[0].start_time elif len(ancestors) > 0: if len(ancestors) > 1: raise ValueError( - "with multiple ancestors, start_time must be specified" + f"with multiple ancestors, start_time must be specified for deme {id}" ) start_time = self[ancestors[0]].end_time else: start_time = float("inf") - # set the end time to the last epoch's end time or to zero if not given + # the first epoch's start time is set to deme's start time + if epochs[0].start_time is None: + epochs[0].start_time = start_time + elif epochs[0].start_time != start_time: + raise ValueError( + f"deme and first epoch start times do not align for deme {id}" + ) + + # set the end time to the last epoch's end time if end_time is None: - if epochs[-1].end_time is not None: - end_time = epochs[-1].end_time - else: - end_time = 0 + end_time = epochs[-1].end_time if epochs[-1].end_time is not None and epochs[-1].end_time != end_time: - raise ValueError("deme and final epoch end times do not align") + raise ValueError( + f"deme and final epoch end times do not align for deme {id}" + ) + # check start time is valid wrt ancestor time intervals for ancestor in ancestors: anc = self[ancestor] @@ -1429,6 +1437,7 @@ def deme( f"of existence for ancestor {ancestor} " f"({anc.start_time}, {anc.end_time})" ) + # set default cloning and selfing rates if "selfing_rate" in defaults: selfing_rate = defaults["selfing_rate"] @@ -1443,24 +1452,17 @@ def deme( else: cloning_rate = None - # the last epoch's end time is set to deme's end time - if epochs[-1].end_time is None: - epochs[-1].end_time = end_time - # the first epoch's start time is set to deme's start time - if epochs[0].start_time is None: - epochs[0].start_time = start_time - elif epochs[0].start_time != start_time: - raise ValueError("deme and first epoch start times do not align") - # fill in all attributes of epochs for i in range(len(epochs)): - # set the start and end times based on surrounding demes + # set the start time based on the last epoch's end time if epochs[i].start_time is None: epochs[i].start_time = epochs[i - 1].end_time if epochs[i].end_time is None: - if epochs[i + 1].start_time is None: - raise ValueError("ambiguity about epochs' start/end times") - epochs[i].end_time = epochs[i + 1].start_time + raise ValueError("all epochs must specify the end time") + if i > 0 and epochs[i].start_time != epochs[i - 1].end_time: + raise ValueError( + "epoch start and end times do not align for deme {id}, epochs {i - 1} and {i}" + ) # for each subsequent epoch, fill in start size, final size, # and size function as necessary based on last epoch if epochs[i].initial_size is None: diff --git a/examples/browning_america.yml b/examples/browning_america.yml index 0c5d06b2..8912d448 100644 --- a/examples/browning_america.yml +++ b/examples/browning_america.yml @@ -26,18 +26,21 @@ demes: ancestors: [AMH] epochs: - initial_size: 14474 + end_time: 0 - id: EUR description: European population ancestors: [OOA] epochs: - initial_size: 1000 final_size: 34039 + end_time: 0 - id: EAS description: East Asian population ancestors: [OOA] epochs: - initial_size: 510 final_size: 45852 + end_time: 0 - id: ADMIX description: Admixed America ancestors: [AFR, EUR, EAS] @@ -46,6 +49,7 @@ demes: - start_time: 12 initial_size: 30000 final_size: 54664 + end_time: 0 migrations: symmetric: - demes: [AFR, OOA] diff --git a/examples/gutenkunst_ooa.yml b/examples/gutenkunst_ooa.yml index 553d103e..dd4eecae 100644 --- a/examples/gutenkunst_ooa.yml +++ b/examples/gutenkunst_ooa.yml @@ -31,18 +31,21 @@ demes: ancestors: [AMH] epochs: - initial_size: 12300 + end_time: 0 - id: CEU description: Utah Residents (CEPH) with Northern and Western European Ancestry ancestors: [OOA] epochs: - initial_size: 1000 final_size: 29725 + end_time: 0 - id: CHB description: Han Chinese in Beijing, China ancestors: [OOA] epochs: - initial_size: 510 final_size: 54090 + end_time: 0 migrations: symmetric: - demes: [YRI, OOA] diff --git a/examples/jacobs_papuans.yml b/examples/jacobs_papuans.yml index 3c1a3560..4341f673 100644 --- a/examples/jacobs_papuans.yml +++ b/examples/jacobs_papuans.yml @@ -43,16 +43,19 @@ demes: epochs: - start_time: 12500.0 initial_size: 13249.0 + end_time: 0 - id: Den1 ancestors: [DenA] epochs: - start_time: 9750.0 initial_size: 13249.0 + end_time: 0 - id: Nea1 ancestors: [NeaA] epochs: - start_time: 3375.0 initial_size: 13249.0 + end_time: 0 - id: Ghost ancestors: [YRI] start_time: 2218.0 @@ -84,6 +87,7 @@ demes: epochs: - start_time: 1293.0 initial_size: 6962.0 + end_time: 0 migrations: symmetric: - demes: [YRI, Ghost] diff --git a/examples/offshoots.yml b/examples/offshoots.yml index 85654b16..d34768b2 100644 --- a/examples/offshoots.yml +++ b/examples/offshoots.yml @@ -7,18 +7,21 @@ demes: description: Main population epochs: - initial_size: 1000 + end_time: 0 - id: offshoot1 description: More recent offshoot population ancestors: [ancestral] epochs: - start_time: 500 initial_size: 100 + end_time: 0 - id: offshoot2 description: More ancient offshoot population ancestors: [ancestral] epochs: - start_time: 1000 initial_size: 200 + end_time: 0 migrations: asymmetric: - source: ancestral diff --git a/tests/test_demes.py b/tests/test_demes.py index 1b84e271..acd20aac 100644 --- a/tests/test_demes.py +++ b/tests/test_demes.py @@ -1318,7 +1318,7 @@ def test_in_generations(self): def test_bad_migration_time(self): dg = demes.Graph(description="test bad migration", time_units="generations") - dg.deme("deme1", epochs=[Epoch(initial_size=1000)]) + dg.deme("deme1", epochs=[Epoch(initial_size=1000, end_time=0)]) dg.deme("deme2", epochs=[Epoch(end_time=100, initial_size=1000)]) with self.assertRaises(ValueError): dg.migration( @@ -1327,8 +1327,8 @@ def test_bad_migration_time(self): def test_overlapping_migrations(self): dg = demes.Graph(description="test", time_units="generations") - dg.deme("A", epochs=[Epoch(initial_size=1)]) - dg.deme("B", epochs=[Epoch(initial_size=1)]) + dg.deme("A", epochs=[Epoch(initial_size=1, end_time=0)]) + dg.deme("B", epochs=[Epoch(initial_size=1, end_time=0)]) dg.migration(source="A", dest="B", rate=0.01) with self.assertRaises(ValueError): dg.migration(source="A", dest="B", start_time=10, rate=0.02) @@ -1346,27 +1346,34 @@ def test_overlapping_migrations(self): def test_bad_pulse_time(self): dg = demes.Graph(description="test bad pulse time", time_units="generations") - dg.deme("deme1", epochs=[Epoch(initial_size=1000)]) + dg.deme("deme1", epochs=[Epoch(initial_size=1000, end_time=0)]) dg.deme("deme2", epochs=[Epoch(end_time=100, initial_size=1000)]) with self.assertRaises(ValueError): dg.pulse(source="deme1", dest="deme2", proportion=0.1, time=10) def test_bad_deme(self): dg = demes.Graph(description="a", time_units="generations") - dg.deme("b", epochs=[Epoch(initial_size=1)]) + dg.deme("b", epochs=[Epoch(initial_size=1, end_time=0)]) with self.assertRaises(ValueError): - # no initial_size + # no epochs given dg.deme("a") + with self.assertRaises(ValueError): + # no initial_size + dg.deme("a", epochs=[Epoch(end_time=1)]) with self.assertRaises(TypeError): # ancestors must be a list - dg.deme("a", ancestors="b", epochs=[Epoch(initial_size=1, start_time=10)]) + dg.deme( + "a", + ancestors="b", + epochs=[Epoch(initial_size=1, start_time=10, end_time=0)], + ) with self.assertRaises(ValueError): # ancestor c doesn't exist dg.deme( "a", ancestors=["b", "c"], proportions=[0.5, 0.5], - epochs=[Epoch(initial_size=1)], + epochs=[Epoch(initial_size=1, end_time=0)], ) with self.assertRaises(ValueError): # end_time and final epoch end_time are different @@ -1396,9 +1403,9 @@ def test_bad_deme(self): def test_duplicate_deme(self): dg = demes.Graph(description="a", time_units="generations") - dg.deme("a", epochs=[Epoch(initial_size=1)]) + dg.deme("a", epochs=[Epoch(initial_size=1, end_time=0)]) with self.assertRaises(ValueError): - dg.deme("a", epochs=[Epoch(initial_size=1)]) + dg.deme("a", epochs=[Epoch(initial_size=1, end_time=0)]) def test_ancestor_not_in_graph(self): dg = demes.Graph(description="a", time_units="generations") @@ -1419,16 +1426,20 @@ def test_duplicate_ancestors(self): def test_bad_start_time_wrt_ancestors(self): dg = demes.Graph(description="a", time_units="generations") dg.deme("a", epochs=[Epoch(initial_size=100, start_time=100, end_time=50)]) - dg.deme("b", epochs=[Epoch(initial_size=100)]) + dg.deme("b", epochs=[Epoch(initial_size=100, end_time=0)]) with self.assertRaises(ValueError): # start_time too old dg.deme( - "c", ancestors=["a"], epochs=[Epoch(initial_size=100, start_time=200)] + "c", + ancestors=["a"], + epochs=[Epoch(initial_size=100, start_time=200, end_time=0)], ) with self.assertRaises(ValueError): # start_time too young dg.deme( - "c", ancestors=["a"], epochs=[Epoch(initial_size=100, start_time=20)] + "c", + ancestors=["a"], + epochs=[Epoch(initial_size=100, start_time=20, end_time=0)], ) with self.assertRaises(ValueError): # start_time too old @@ -1436,7 +1447,7 @@ def test_bad_start_time_wrt_ancestors(self): "c", ancestors=["a", "b"], proportions=[0.5, 0.5], - epochs=[Epoch(start_time=200, initial_size=100)], + epochs=[Epoch(start_time=200, initial_size=100, end_time=0)], ) with self.assertRaises(ValueError): # start_time too young @@ -1444,7 +1455,7 @@ def test_bad_start_time_wrt_ancestors(self): "c", ancestors=["a", "b"], proportions=[0.5, 0.5], - epochs=[Epoch(initial_size=100, start_time=20)], + epochs=[Epoch(initial_size=100, start_time=20, end_time=0)], ) with self.assertRaises(ValueError): # start_time not provided @@ -1452,7 +1463,7 @@ def test_bad_start_time_wrt_ancestors(self): "c", ancestors=["a", "b"], proportions=[0.5, 0.5], - epochs=[Epoch(initial_size=100)], + epochs=[Epoch(initial_size=100, end_time=0)], ) def test_proportions_default(self): @@ -1464,7 +1475,7 @@ def test_proportions_default(self): dg.deme( "c", ancestors=["a", "b"], - epochs=[Epoch(initial_size=100, start_time=100)], + epochs=[Epoch(initial_size=100, start_time=100, end_time=0)], ) with self.assertRaises(ValueError): # proportions wrong length @@ -1472,7 +1483,7 @@ def test_proportions_default(self): "c", ancestors=["a", "b"], proportions=[1], - epochs=[Epoch(initial_size=100, start_time=100)], + epochs=[Epoch(initial_size=100, start_time=100, end_time=0)], ) with self.assertRaises(ValueError): # proportions wrong length @@ -1480,9 +1491,9 @@ def test_proportions_default(self): "c", ancestors=["a", "b"], proportions=[1 / 3, 1 / 3, 1 / 3], - epochs=[Epoch(initial_size=100, start_time=100)], + epochs=[Epoch(initial_size=100, start_time=100, end_time=0)], ) - dg.deme("c", ancestors=["b"], epochs=[Epoch(initial_size=100)]) + dg.deme("c", ancestors=["b"], epochs=[Epoch(initial_size=100, end_time=0)]) self.assertEqual(len(dg["c"].proportions), 1) self.assertEqual(dg["c"].proportions[0], 1.0) @@ -1503,7 +1514,7 @@ def test_bad_migration(self): dg.symmetric_migration(demes=["a"], rate=0.1) with self.assertRaises(ValueError): dg.migration(source="a", dest="b", rate=0.1) - dg.deme("a", epochs=[Epoch(initial_size=100)]) + dg.deme("a", epochs=[Epoch(initial_size=100, end_time=0)]) with self.assertRaises(ValueError): dg.migration(source="a", dest="b", rate=0.1) with self.assertRaises(ValueError): @@ -1511,7 +1522,7 @@ def test_bad_migration(self): def test_bad_pulse(self): dg = demes.Graph(description="a", time_units="generations") - dg.deme("a", epochs=[Epoch(initial_size=100)]) + dg.deme("a", epochs=[Epoch(initial_size=100, end_time=0)]) with self.assertRaises(ValueError): dg.pulse(source="a", dest="b", proportion=0.1, time=10) with self.assertRaises(ValueError): @@ -1520,7 +1531,7 @@ def test_bad_pulse(self): def test_pulse_same_time(self): g1 = Graph(description="test", time_units="generations") for j in range(4): - g1.deme(f"d{j}", epochs=[Epoch(initial_size=1000)]) + g1.deme(f"d{j}", epochs=[Epoch(initial_size=1000, end_time=0)]) T = 100 # time of pulses @@ -1576,7 +1587,7 @@ def test_isclose(self): time_units="generations", ) g2 = copy.deepcopy(g1) - g1.deme("d1", epochs=[Epoch(initial_size=1000)]) + g1.deme("d1", epochs=[Epoch(initial_size=1000, end_time=0)]) self.assertTrue(g1.isclose(g1)) self.assertTrue(g1.isclose(demes.loads(demes.dumps(g1)))) @@ -1585,7 +1596,7 @@ def test_isclose(self): description="some other description", time_units="generations", ) - g3.deme("d1", epochs=[Epoch(initial_size=1000)]) + g3.deme("d1", epochs=[Epoch(initial_size=1000, end_time=0)]) self.assertTrue(g1.isclose(g3)) # Don't care about doi for equality. @@ -1594,16 +1605,16 @@ def test_isclose(self): time_units="generations", doi=["https://example.com/foo.bar"], ) - g3.deme("d1", epochs=[Epoch(initial_size=1000)]) + g3.deme("d1", epochs=[Epoch(initial_size=1000, end_time=0)]) self.assertTrue(g1.isclose(g3)) # The order in which demes are added shouldn't matter. g3 = copy.deepcopy(g2) g4 = copy.deepcopy(g2) - g3.deme("d1", epochs=[Epoch(initial_size=1000)]) - g3.deme("d2", epochs=[Epoch(initial_size=1000)]) - g4.deme("d2", epochs=[Epoch(initial_size=1000)]) - g4.deme("d1", epochs=[Epoch(initial_size=1000)]) + g3.deme("d1", epochs=[Epoch(initial_size=1000, end_time=0)]) + g3.deme("d2", epochs=[Epoch(initial_size=1000, end_time=0)]) + g4.deme("d2", epochs=[Epoch(initial_size=1000, end_time=0)]) + g4.deme("d1", epochs=[Epoch(initial_size=1000, end_time=0)]) self.assertTrue(g3.isclose(g4)) # The order in which migrations are added shouldn't matter. @@ -1626,78 +1637,80 @@ def test_isclose(self): self.assertFalse(g1 == g2) g3 = copy.deepcopy(g2) - g3.deme("dX", epochs=[Epoch(initial_size=1000)]) + g3.deme("dX", epochs=[Epoch(initial_size=1000, end_time=0)]) self.assertFalse(g1.isclose(g3)) g3 = copy.deepcopy(g2) - g3.deme("d1", epochs=[Epoch(initial_size=1001)]) + g3.deme("d1", epochs=[Epoch(initial_size=1001, end_time=0)]) self.assertFalse(g1.isclose(g3)) g3 = copy.deepcopy(g2) - g3.deme("d1", epochs=[Epoch(initial_size=1000)]) - g3.deme("d2", epochs=[Epoch(initial_size=1000)]) + g3.deme("d1", epochs=[Epoch(initial_size=1000, end_time=0)]) + g3.deme("d2", epochs=[Epoch(initial_size=1000, end_time=0)]) self.assertFalse(g1.isclose(g3)) g3 = copy.deepcopy(g1) g4 = copy.deepcopy(g1) - g3.deme("d2", epochs=[Epoch(initial_size=1000, start_time=50)]) + g3.deme("d2", epochs=[Epoch(initial_size=1000, start_time=50, end_time=0)]) g4.deme( - "d2", ancestors=["d1"], epochs=[Epoch(initial_size=1000, start_time=50)] + "d2", + ancestors=["d1"], + epochs=[Epoch(initial_size=1000, start_time=50, end_time=0)], ) self.assertFalse(g3.isclose(g4)) g3 = copy.deepcopy(g2) - g3.deme("d1", epochs=[Epoch(initial_size=1000)]) - g3.deme("d2", epochs=[Epoch(initial_size=1000)]) + g3.deme("d1", epochs=[Epoch(initial_size=1000, end_time=0)]) + g3.deme("d2", epochs=[Epoch(initial_size=1000, end_time=0)]) g4 = copy.deepcopy(g2) - g4.deme("d1", epochs=[Epoch(initial_size=1000)]) - g4.deme("d2", epochs=[Epoch(initial_size=1000)]) + g4.deme("d1", epochs=[Epoch(initial_size=1000, end_time=0)]) + g4.deme("d2", epochs=[Epoch(initial_size=1000, end_time=0)]) g4.migration(source="d2", dest="d1", rate=1e-5) self.assertFalse(g3.isclose(g4)) g3 = copy.deepcopy(g2) - g3.deme("d1", epochs=[Epoch(initial_size=1000)]) - g3.deme("d2", epochs=[Epoch(initial_size=1000)]) + g3.deme("d1", epochs=[Epoch(initial_size=1000, end_time=0)]) + g3.deme("d2", epochs=[Epoch(initial_size=1000, end_time=0)]) g3.migration(source="d1", dest="d2", rate=1e-5) g4 = copy.deepcopy(g2) - g4.deme("d1", epochs=[Epoch(initial_size=1000)]) - g4.deme("d2", epochs=[Epoch(initial_size=1000)]) + g4.deme("d1", epochs=[Epoch(initial_size=1000, end_time=0)]) + g4.deme("d2", epochs=[Epoch(initial_size=1000, end_time=0)]) g4.migration(source="d2", dest="d1", rate=1e-5) self.assertFalse(g3.isclose(g4)) g3 = copy.deepcopy(g2) - g3.deme("d1", epochs=[Epoch(initial_size=1000)]) - g3.deme("d2", epochs=[Epoch(initial_size=1000)]) + g3.deme("d1", epochs=[Epoch(initial_size=1000, end_time=0)]) + g3.deme("d2", epochs=[Epoch(initial_size=1000, end_time=0)]) g3.migration(source="d2", dest="d1", rate=1e-5) g4 = copy.deepcopy(g2) - g4.deme("d1", epochs=[Epoch(initial_size=1000)]) - g4.deme("d2", epochs=[Epoch(initial_size=1000)]) + g4.deme("d1", epochs=[Epoch(initial_size=1000, end_time=0)]) + g4.deme("d2", epochs=[Epoch(initial_size=1000, end_time=0)]) g4.symmetric_migration(demes=["d2", "d1"], rate=1e-5) self.assertFalse(g3.isclose(g4)) g3 = copy.deepcopy(g2) - g3.deme("d1", epochs=[Epoch(initial_size=1000)]) - g3.deme("d2", epochs=[Epoch(initial_size=1000)]) + g3.deme("d1", epochs=[Epoch(initial_size=1000, end_time=0)]) + g3.deme("d2", epochs=[Epoch(initial_size=1000, end_time=0)]) g4 = copy.deepcopy(g2) - g4.deme("d1", epochs=[Epoch(initial_size=1000)]) - g4.deme("d2", epochs=[Epoch(initial_size=1000)]) + g4.deme("d1", epochs=[Epoch(initial_size=1000, end_time=0)]) + g4.deme("d2", epochs=[Epoch(initial_size=1000, end_time=0)]) g4.pulse(source="d1", dest="d2", proportion=0.01, time=100) self.assertFalse(g3.isclose(g4)) g3 = copy.deepcopy(g2) - g3.deme("d1", epochs=[Epoch(initial_size=1000)]) - g3.deme("d2", epochs=[Epoch(initial_size=1000)]) + g3.deme("d1", epochs=[Epoch(initial_size=1000, end_time=0)]) + g3.deme("d2", epochs=[Epoch(initial_size=1000, end_time=0)]) g3.pulse(source="d2", dest="d1", proportion=0.01, time=100) g4 = copy.deepcopy(g2) - g4.deme("d1", epochs=[Epoch(initial_size=1000)]) - g4.deme("d2", epochs=[Epoch(initial_size=1000)]) + g4.deme("d1", epochs=[Epoch(initial_size=1000, end_time=0)]) + g4.deme("d2", epochs=[Epoch(initial_size=1000, end_time=0)]) g4.pulse(source="d1", dest="d2", proportion=0.01, time=100) self.assertFalse(g3.isclose(g4)) def test_validate(self): g1 = demes.Graph(description="test", time_units="generations") g1.deme("a", epochs=[Epoch(initial_size=1, end_time=100)]) - g1.deme("b", epochs=[Epoch(initial_size=1, start_time=50)]) + g1.deme("b", epochs=[Epoch(initial_size=1, start_time=50, end_time=0)]) g1.validate() # @@ -1734,9 +1747,9 @@ def test_validate(self): def test_newly_created_objects_return(self): g = demes.Graph(description="test", time_units="generations") - d1 = g.deme("a", epochs=[Epoch(initial_size=1)]) + d1 = g.deme("a", epochs=[Epoch(initial_size=1, end_time=0)]) self.assertIsInstance(d1, Deme) - d2 = g.deme("b", epochs=[Epoch(initial_size=1, start_time=50)]) + d2 = g.deme("b", epochs=[Epoch(initial_size=1, start_time=50, end_time=0)]) self.assertIsInstance(d2, Deme) mig = g.migration(source="a", dest="b", rate=1e-4, end_time=25) @@ -1753,9 +1766,20 @@ def test_deme_end_time(self): g = demes.Graph(description="test", time_units="generations") g.deme( "a", - end_time=10, - epochs=[Epoch(end_time=100, initial_size=10), Epoch(initial_size=20)], + epochs=[ + Epoch(end_time=100, initial_size=10), + Epoch(initial_size=20, end_time=0), + ], ) + with self.assertRaises(ValueError): + g.deme( + "a", + end_time=10, + epochs=[ + Epoch(end_time=100, initial_size=10), + Epoch(initial_size=20, end_time=0), + ], + ) with self.assertRaises(ValueError): g.deme("b", epochs=[Epoch(initial_size=100, start_time=100, end_time=100)]) with self.assertRaises(ValueError): @@ -1772,37 +1796,28 @@ def test_ambiguous_epoch_times(self): ], ) - def test_epoch_end_time_from_start_time(self): - g = demes.Graph(description="test", time_units="generations") - g.deme( - "a", - end_time=10, - epochs=[Epoch(initial_size=10), Epoch(start_time=10, initial_size=20)], - ) - self.assertTrue(g["a"].epochs[0].end_time == 10) - class TestGraphToDict(unittest.TestCase): def test_finite_start_time(self): dg = demes.Graph(description="a", time_units="generations") - dg.deme("a", epochs=[Epoch(initial_size=100, start_time=100)]) + dg.deme("a", epochs=[Epoch(initial_size=100, start_time=100, end_time=0)]) d = dg.asdict() self.assertTrue(d["demes"][0]["epochs"][0]["start_time"] == dg["a"].start_time) def test_deme_selfing_rate(self): dg = demes.Graph(description="a", time_units="generations") - dg.deme("a", epochs=[Epoch(initial_size=100, selfing_rate=0.1)]) + dg.deme("a", epochs=[Epoch(initial_size=100, selfing_rate=0.1, end_time=0)]) d = dg.asdict() self.assertTrue(d["demes"][0]["epochs"][0]["selfing_rate"] == 0.1) def test_deme_cloning_rate(self): dg = demes.Graph(description="a", time_units="generations") - dg.deme("a", epochs=[Epoch(initial_size=100, cloning_rate=0.1)]) + dg.deme("a", epochs=[Epoch(initial_size=100, cloning_rate=0.1, end_time=0)]) d = dg.asdict() self.assertTrue(d["demes"][0]["epochs"][0]["cloning_rate"] == 0.1) d = dg.asdict_simplified() self.assertTrue(d["demes"][0]["epochs"][0]["cloning_rate"] == 0.1) - dg.deme("b", epochs=[Epoch(initial_size=200)]) + dg.deme("b", epochs=[Epoch(initial_size=200, end_time=0)]) d = dg.asdict_simplified() self.assertTrue("cloning_rate" not in d["demes"][1]) dg.deme( @@ -1881,14 +1896,16 @@ def test_fill_epoch_cloning_rates(self): def test_fill_description(self): dg = demes.Graph(description="a", time_units="generations") - dg.deme("a", description="described", epochs=[Epoch(initial_size=100)]) + dg.deme( + "a", description="described", epochs=[Epoch(initial_size=100, end_time=0)] + ) d = dg.asdict() self.assertTrue(d["demes"][0]["description"] == dg["a"].description) def test_fill_migration_bounds(self): dg = demes.Graph(description="a", time_units="generations") - dg.deme("a", epochs=[Epoch(initial_size=100)]) - dg.deme("b", epochs=[Epoch(initial_size=100)]) + dg.deme("a", epochs=[Epoch(initial_size=100, end_time=0)]) + dg.deme("b", epochs=[Epoch(initial_size=100, end_time=0)]) dg.migration(source="a", dest="b", rate=0.01, start_time=20, end_time=10) d = dg.asdict() self.assertTrue(d["migrations"]["asymmetric"][0]["start_time"] == 20) @@ -1912,15 +1929,15 @@ def test_bad_custom_attributes(self): def test_multiple_symmetric_migrations(self): g = demes.Graph(description="descr", time_units="generations") - g.deme("a", epochs=[Epoch(initial_size=100)]) - g.deme("b", epochs=[Epoch(initial_size=200)]) - g.deme("c", epochs=[Epoch(initial_size=300)]) + g.deme("a", epochs=[Epoch(initial_size=100, end_time=0)]) + g.deme("b", epochs=[Epoch(initial_size=200, end_time=0)]) + g.deme("c", epochs=[Epoch(initial_size=300, end_time=0)]) g.symmetric_migration(demes=["a", "b", "c"], rate=0.01) d = g.asdict_simplified() self.assertTrue(len(d["migrations"]["symmetric"]) == 1) self.assertTrue("asymmetric" not in d["migrations"]) - g.deme("d", epochs=[Epoch(initial_size=400)]) + g.deme("d", epochs=[Epoch(initial_size=400, end_time=0)]) g.symmetric_migration(demes=["a", "d"], rate=0.01) d = g.asdict_simplified() self.assertTrue(len(d["migrations"]["symmetric"]) == 2) @@ -1931,10 +1948,10 @@ def test_multiple_symmetric_migrations(self): def test_mix_sym_asym_migrations(self): g = demes.Graph(description="a", time_units="generations") - g.deme("a", epochs=[Epoch(initial_size=1, start_time=100)]) - g.deme("b", epochs=[Epoch(initial_size=1)]) - g.deme("c", epochs=[Epoch(initial_size=1)]) - g.deme("d", epochs=[Epoch(initial_size=1)]) + g.deme("a", epochs=[Epoch(initial_size=1, start_time=100, end_time=0)]) + g.deme("b", epochs=[Epoch(initial_size=1, end_time=0)]) + g.deme("c", epochs=[Epoch(initial_size=1, end_time=0)]) + g.deme("d", epochs=[Epoch(initial_size=1, end_time=0)]) g.symmetric_migration(demes=["a", "b"], rate=0.01) g.symmetric_migration(demes=["b", "c"], rate=0.01) g.symmetric_migration(demes=["a", "c", "d"], rate=0.01) diff --git a/tests/test_load_dump.py b/tests/test_load_dump.py index d3ca967b..e6805e2a 100644 --- a/tests/test_load_dump.py +++ b/tests/test_load_dump.py @@ -53,10 +53,16 @@ def jacobs_papuans(): epochs=[Epoch(end_time=9750, initial_size=N_DeniAnc)], ) # Altai Denisovan (sampling lineage) - g.deme("DenAltai", ancestors=["Den2"], epochs=[Epoch(initial_size=5083)]) + g.deme( + "DenAltai", ancestors=["Den2"], epochs=[Epoch(initial_size=5083, end_time=0)] + ) # Introgressing Denisovan lineages 1 and 2 - g.deme("DenI1", ancestors=["Den2"], epochs=[Epoch(initial_size=N_archaic)]) - g.deme("DenI2", ancestors=["Den1"], epochs=[Epoch(initial_size=N_archaic)]) + g.deme( + "DenI1", ancestors=["Den2"], epochs=[Epoch(initial_size=N_archaic, end_time=0)] + ) + g.deme( + "DenI2", ancestors=["Den1"], epochs=[Epoch(initial_size=N_archaic, end_time=0)] + ) g.deme( "Nea", @@ -64,7 +70,7 @@ def jacobs_papuans(): epochs=[Epoch(end_time=3375, initial_size=N_archaic)], ) # Altai Neanderthal (sampling lineage) - g.deme("NeaAltai", ancestors=["Nea"], epochs=[Epoch(initial_size=826)]) + g.deme("NeaAltai", ancestors=["Nea"], epochs=[Epoch(initial_size=826, end_time=0)]) # Introgressing Neanderthal lineage g.deme( "NeaI", ancestors=["Nea"], epochs=[Epoch(end_time=883, initial_size=N_archaic)] @@ -75,7 +81,7 @@ def jacobs_papuans(): ancestors=["ancestral_hominin"], epochs=[Epoch(end_time=2218, initial_size=41563)], ) - g.deme("Africa", ancestors=["AMH"], epochs=[Epoch(initial_size=48433)]) + g.deme("Africa", ancestors=["AMH"], epochs=[Epoch(initial_size=48433, end_time=0)]) g.deme( "Ghost1", ancestors=["AMH"], @@ -91,7 +97,9 @@ def jacobs_papuans(): ancestors=["Ghost1"], epochs=[Epoch(end_time=1758, initial_size=N_ghost)], ) - g.deme("Ghost3", ancestors=["Ghost2"], epochs=[Epoch(initial_size=N_ghost)]) + g.deme( + "Ghost3", ancestors=["Ghost2"], epochs=[Epoch(initial_size=N_ghost, end_time=0)] + ) g.deme( "Papua", ancestors=["Ghost1"], @@ -110,8 +118,14 @@ def jacobs_papuans(): demes.Epoch(end_time=1293, initial_size=12971), ], ) - g.deme("WestEurasia", ancestors=["Eurasia"], epochs=[Epoch(initial_size=6962)]) - g.deme("EastAsia", ancestors=["Eurasia"], epochs=[Epoch(initial_size=9025)]) + g.deme( + "WestEurasia", + ancestors=["Eurasia"], + epochs=[Epoch(initial_size=6962, end_time=0)], + ) + g.deme( + "EastAsia", ancestors=["Eurasia"], epochs=[Epoch(initial_size=9025, end_time=0)] + ) g.symmetric_migration( demes=["Africa", "Ghost3"], rate=1.79e-4, start_time=T_Eu_bottleneck @@ -174,7 +188,7 @@ def test_bad_format_param(self): def test_bad_filename_param(self): g = demes.Graph(description="test", time_units="generations") - g.deme("A", epochs=[Epoch(initial_size=1000)]) + g.deme("A", epochs=[Epoch(initial_size=1000, end_time=0)]) class F: pass @@ -201,7 +215,7 @@ def check_dumps_simple(self, *, format, simplified): generation_time=42, ) for id, N in zip("ABCD", [100, 200, 300, 400]): - g.deme(id, epochs=[Epoch(initial_size=N)]) + g.deme(id, epochs=[Epoch(initial_size=N, end_time=0)]) string = demes.dumps(g, format=format, simplified=simplified) assert "description" in string assert g.description in string @@ -230,7 +244,7 @@ def check_dumps_simple(self, *, format, simplified): assert "cloning_rate" not in string g1 = copy.deepcopy(g) - g1.deme("E", epochs=[Epoch(initial_size=100, selfing_rate=0.1)]) + g1.deme("E", epochs=[Epoch(initial_size=100, selfing_rate=0.1, end_time=0)]) string = demes.dumps(g1, format=format, simplified=simplified) assert "selfing_rate" in string assert "0.1" in string @@ -238,7 +252,7 @@ def check_dumps_simple(self, *, format, simplified): assert "cloning_rate" not in string g1 = copy.deepcopy(g) - g1.deme("E", epochs=[Epoch(initial_size=100, cloning_rate=0.1)]) + g1.deme("E", epochs=[Epoch(initial_size=100, cloning_rate=0.1, end_time=0)]) string = demes.dumps(g1, format=format, simplified=simplified) if simplified: assert "selfing_rate" not in string @@ -313,7 +327,8 @@ def test_loads_json_simple(self): "id": "A", "epochs": [ { - "initial_size": 100 + "initial_size": 100, + "end_time": 0 } ] }, @@ -321,7 +336,8 @@ def test_loads_json_simple(self): "id": "B", "epochs": [ { - "initial_size": 100 + "initial_size": 100, + "end_time": 0 } ] }, @@ -332,7 +348,8 @@ def test_loads_json_simple(self): "epochs": [ { "start_time": 500, - "initial_size": 100 + "initial_size": 100, + "end_time": 0 } ] } @@ -359,13 +376,16 @@ def test_loads_yaml_simple(self): - id: A epochs: - initial_size: 100 + end_time: 0 - id: B epochs: - initial_size: 100 + end_time: 0 - id: C epochs: - initial_size: 100 start_time: 500 + end_time: 0 ancestors: [A, B] proportions: [0.1, 0.9] """ @@ -401,7 +421,7 @@ def check_dump_and_load_simple(self, *, format, simplified): generation_time=42, ) for id, N in zip("ABCD", [100, 200, 300, 400]): - g1.deme(id, epochs=[Epoch(initial_size=N)]) + g1.deme(id, epochs=[Epoch(initial_size=N, end_time=0)]) with tempfile.TemporaryDirectory() as tmpdir: tmpfile = pathlib.Path(tmpdir) / "temp.txt" demes.dump(g1, tmpfile, format=format, simplified=simplified) @@ -547,12 +567,12 @@ def test_float_subclass(self): ) self.check_dump_load_roundtrip(g) - g.deme("B", epochs=[Epoch(initial_size=N[0])]) + g.deme("B", epochs=[Epoch(initial_size=N[0], end_time=0)]) g.deme( "C", ancestors=["A", "B"], proportions=[fractions.Fraction(1, 3), fractions.Fraction(2, 3)], - epochs=[Epoch(initial_size=N[0], start_time=T[1])], + epochs=[Epoch(initial_size=N[0], start_time=T[1], end_time=0)], ) self.check_dump_load_roundtrip(g) diff --git a/tests/test_moments.py b/tests/test_moments.py index c99fec88..66a50dce 100644 --- a/tests/test_moments.py +++ b/tests/test_moments.py @@ -95,9 +95,15 @@ def test_num_lineages(self): # simple admix model g = demes.Graph(description="test", time_units="generations") g.deme(id="anc", epochs=[Epoch(initial_size=100, end_time=100)]) - g.deme(id="pop1", epochs=[Epoch(initial_size=100)], ancestors=["anc"]) - g.deme(id="pop2", epochs=[Epoch(initial_size=100)], ancestors=["anc"]) - g.deme(id="pop3", epochs=[Epoch(initial_size=100)], ancestors=["anc"]) + g.deme( + id="pop1", epochs=[Epoch(initial_size=100, end_time=0)], ancestors=["anc"] + ) + g.deme( + id="pop2", epochs=[Epoch(initial_size=100, end_time=0)], ancestors=["anc"] + ) + g.deme( + id="pop3", epochs=[Epoch(initial_size=100, end_time=0)], ancestors=["anc"] + ) g.deme( id="pop", initial_size=100, @@ -121,7 +127,7 @@ def test_num_lineages(self): # test basic results against moments implementation def test_one_pop(self): g = demes.Graph(description="test", time_units="generations") - g.deme(id="Pop", epochs=[Epoch(initial_size=1000)]) + g.deme(id="Pop", epochs=[Epoch(initial_size=1000, end_time=0)]) fs = moments_.SFS(g, ["Pop"], [20]) fs_m = moments.Demographics1D.snm([20]) self.assertTrue(np.allclose(fs.data, fs_m.data)) @@ -143,14 +149,22 @@ def test_more_than_5_demes(self): g = demes.Graph(description="test", time_units="generations") g.deme(id="anc", epochs=[Epoch(initial_size=1000, end_time=1000)]) for i in range(6): - g.deme(id=f"pop{i}", epochs=[Epoch(initial_size=1000)], ancestors=["anc"]) + g.deme( + id=f"pop{i}", + epochs=[Epoch(initial_size=1000, end_time=0)], + ancestors=["anc"], + ) with self.assertRaises(ValueError): moments_.SFS(g, ["pop{i}" for i in range(6)], [10 for i in range(6)]) g = demes.Graph(description="test", time_units="generations") g.deme(id="anc", epochs=[Epoch(initial_size=1000, end_time=1000)]) for i in range(3): - g.deme(id=f"pop{i}", ancestors=["anc"], epochs=[Epoch(initial_size=1000)]) + g.deme( + id=f"pop{i}", + ancestors=["anc"], + epochs=[Epoch(initial_size=1000, end_time=0)], + ) with self.assertRaises(ValueError): moments_.SFS( g, @@ -161,7 +175,7 @@ def test_more_than_5_demes(self): def test_one_pop_ancient_samples(self): g = demes.Graph(description="test", time_units="generations") - g.deme(id="Pop", epochs=[Epoch(initial_size=1000)]) + g.deme(id="Pop", epochs=[Epoch(initial_size=1000, end_time=0)]) fs = moments_.SFS(g, ["Pop", "Pop"], [20, 4], sample_times=[0, 100]) fs_m = moments.Demographics1D.snm([24]) fs_m = moments.Manips.split_1D_to_2D(fs_m, 20, 4) @@ -196,7 +210,7 @@ def test_simple_merge(self): id="Pop", ancestors=["Source1", "Source2"], proportions=[0.8, 0.2], - epochs=[Epoch(initial_size=4000, start_time=10)], + epochs=[Epoch(initial_size=4000, start_time=10, end_time=0)], ) fs = moments_.SFS(g, ["Pop"], [20]) @@ -210,13 +224,21 @@ def test_simple_merge(self): def test_simple_admixture(self): g = demes.Graph(description="test", time_units="generations") g.deme(id="Anc", epochs=[Epoch(initial_size=1000, end_time=100)]) - g.deme(id="Source1", epochs=[Epoch(initial_size=2000)], ancestors=["Anc"]) - g.deme(id="Source2", epochs=[Epoch(initial_size=3000)], ancestors=["Anc"]) + g.deme( + id="Source1", + epochs=[Epoch(initial_size=2000, end_time=0)], + ancestors=["Anc"], + ) + g.deme( + id="Source2", + epochs=[Epoch(initial_size=3000, end_time=0)], + ancestors=["Anc"], + ) g.deme( id="Pop", ancestors=["Source1", "Source2"], proportions=[0.8, 0.2], - epochs=[Epoch(initial_size=4000, start_time=10)], + epochs=[Epoch(initial_size=4000, start_time=10, end_time=0)], ) fs = moments_.SFS(g, ["Source1", "Source2", "Pop"], [10, 10, 10]) @@ -272,8 +294,14 @@ def nu_func(t): def test_simple_pulse_model(self): g = demes.Graph(description="test", time_units="generations") g.deme(id="anc", epochs=[Epoch(initial_size=1000, end_time=100)]) - g.deme(id="source", epochs=[Epoch(initial_size=1000)], ancestors=["anc"]) - g.deme(id="dest", epochs=[Epoch(initial_size=1000)], ancestors=["anc"]) + g.deme( + id="source", + epochs=[Epoch(initial_size=1000, end_time=0)], + ancestors=["anc"], + ) + g.deme( + id="dest", epochs=[Epoch(initial_size=1000, end_time=0)], ancestors=["anc"] + ) g.pulse(source="source", dest="dest", time=10, proportion=0.1) fs = moments_.SFS(g, ["source", "dest"], [20, 20]) @@ -287,9 +315,15 @@ def test_simple_pulse_model(self): def test_n_way_split(self): g = demes.Graph(description="three-way", time_units="generations") g.deme(id="anc", epochs=[Epoch(initial_size=1000, end_time=10)]) - g.deme(id="deme1", epochs=[Epoch(initial_size=1000)], ancestors=["anc"]) - g.deme(id="deme2", epochs=[Epoch(initial_size=1000)], ancestors=["anc"]) - g.deme(id="deme3", epochs=[Epoch(initial_size=1000)], ancestors=["anc"]) + g.deme( + id="deme1", epochs=[Epoch(initial_size=1000, end_time=0)], ancestors=["anc"] + ) + g.deme( + id="deme2", epochs=[Epoch(initial_size=1000, end_time=0)], ancestors=["anc"] + ) + g.deme( + id="deme3", epochs=[Epoch(initial_size=1000, end_time=0)], ancestors=["anc"] + ) ns = [10, 15, 20] fs = moments_.SFS(g, ["deme1", "deme2", "deme3"], ns) self.assertTrue(np.all([fs.sample_sizes[i] == ns[i] for i in range(len(ns))])) @@ -330,7 +364,7 @@ def test_n_way_admixture(self): id="merged", ancestors=["source1", "source2", "source3"], proportions=[0.5, 0.2, 0.3], - epochs=[Epoch(initial_size=1000, start_time=10)], + epochs=[Epoch(initial_size=1000, start_time=10, end_time=0)], ) ns = [10] fs = moments_.SFS(g, ["merged"], ns) @@ -347,14 +381,26 @@ def test_n_way_admixture(self): g = demes.Graph(description="three-way admix", time_units="generations") g.deme(id="anc", epochs=[Epoch(initial_size=1000, end_time=100)]) - g.deme(id="source1", epochs=[Epoch(initial_size=1000)], ancestors=["anc"]) - g.deme(id="source2", epochs=[Epoch(initial_size=1000)], ancestors=["anc"]) - g.deme(id="source3", epochs=[Epoch(initial_size=1000)], ancestors=["anc"]) + g.deme( + id="source1", + epochs=[Epoch(initial_size=1000, end_time=0)], + ancestors=["anc"], + ) + g.deme( + id="source2", + epochs=[Epoch(initial_size=1000, end_time=0)], + ancestors=["anc"], + ) + g.deme( + id="source3", + epochs=[Epoch(initial_size=1000, end_time=0)], + ancestors=["anc"], + ) g.deme( id="admixed", ancestors=["source1", "source2", "source3"], proportions=[0.5, 0.2, 0.3], - epochs=[Epoch(initial_size=1000, start_time=10)], + epochs=[Epoch(initial_size=1000, start_time=10, end_time=0)], ) ns = [10] fs = moments_.SFS(g, ["admixed"], ns)