Repository: popsim-consortium/stdpopsim (language: python)

Issue:
Some breaking changes in `msprime` 1.0 beta
`msprime.RateMap` now only takes keyword arguments, but we use positional arguments (as in `msprime.RateMap(positions, rates)`) in the tests.

That's an easy fix, but there may be some other breaking changes that haven't been noticed yet.


Discussion / hints:

Fix patch:
diff --git a/docs/_ext/speciescatalog.py b/docs/_ext/speciescatalog.py
index 13a641e7d..99023a7cb 100644
--- a/docs/_ext/speciescatalog.py
+++ b/docs/_ext/speciescatalog.py
@@ -211,7 +211,7 @@ def population_table(self, model):
             row += entry
 
             entry = nodes.entry()
-            entry += nodes.paragraph(text=f"{population.sampling_time}")
+            entry += nodes.paragraph(text=f"{population.default_sampling_time}")
             row += entry
 
             entry = nodes.entry()
diff --git a/requirements/CI/requirements.txt b/requirements/CI/requirements.txt
index 4c9635fe6..7eb60c048 100644
--- a/requirements/CI/requirements.txt
+++ b/requirements/CI/requirements.txt
@@ -10,7 +10,7 @@ sphinx-argparse==0.2.5
 sphinx-issues==1.2.0
 sphinx_rtd_theme==0.5.1
 sphinxcontrib-programoutput==0.16
-msprime==1.0.0a6
+msprime==1.0.0b1
 attrs==20.3.0
 appdirs==1.4.4
 humanize==3.3.0
diff --git a/requirements/development.txt b/requirements/development.txt
index 899e87d85..c9ddde63e 100644
--- a/requirements/development.txt
+++ b/requirements/development.txt
@@ -13,7 +13,7 @@ sphinx-argparse
 sphinx-issues
 sphinx_rtd_theme
 sphinxcontrib-programoutput
-msprime>=1.0.0a6
+msprime>=1.0.0b1
 attrs
 appdirs
 humanize
diff --git a/setup.py b/setup.py
index ca60ae80d..9b9ec976f 100644
--- a/setup.py
+++ b/setup.py
@@ -34,7 +34,7 @@
     python_requires=">=3.7",
     # NOTE: make sure this is the 'attrs' package, not 'attr'!
     install_requires=[
-        "msprime>=1.0.0a6",
+        "msprime>=1.0.0b1",
         "attrs>=19.1.0",
         "appdirs",
         "humanize",
diff --git a/stdpopsim/cli.py b/stdpopsim/cli.py
index 7002a6a4b..bf5fa0cda 100644
--- a/stdpopsim/cli.py
+++ b/stdpopsim/cli.py
@@ -581,7 +581,7 @@ def write_simulation_summary(engine, model, contig, samples, seed=None):
         sample_counts[s.population] += s.num_samples
     for p in range(0, model.num_sampling_populations):
         pop_name = model.populations[p].id
-        sample_time = model.populations[p].sampling_time
+        sample_time = model.populations[p].default_sampling_time
         dry_run_text += f"{indent * 2}{pop_name}: "
         dry_run_text += f"{sample_counts[p]} ({sample_time})\n"
     # Get information about relevant contig
diff --git a/stdpopsim/engines.py b/stdpopsim/engines.py
index 953d389fc..5f66d19be 100644
--- a/stdpopsim/engines.py
+++ b/stdpopsim/engines.py
@@ -124,8 +124,15 @@ class _MsprimeEngine(Engine):
             reasons={stdpopsim.CiteReason.ENGINE},
         )
     ]
+
     # We default to the first model in the list.
-    supported_models = ["hudson", "dtwf", "smc", "smc_prime"]
+    model_class_map = {
+        "hudson": msprime.StandardCoalescent,
+        "dtwf": msprime.DiscreteTimeWrightFisher,
+        "smc": msprime.SmcApproxCoalescent,
+        "smc_prime": msprime.SmcPrimeApproxCoalescent,
+    }
+
     model_citations = {
         "dtwf": [
             stdpopsim.Citation(
@@ -137,6 +144,46 @@ class _MsprimeEngine(Engine):
         ]
     }
 
+    @property
+    def supported_models(self):
+        return list(self.model_class_map.keys())
+
+    def _convert_model_spec(self, model_str, model_changes):
+        """
+        Convert the specified model specification into a form suitable
+        for sim_ancestry. The model param is a string or None. The
+        model_changes is either None or list of (time, model_str) tuples.
+        Also return the appropriate extra citations.
+        """
+        citations = []
+        if model_str is None:
+            model_str = "hudson"
+        else:
+            if model_str not in self.model_class_map:
+                raise ValueError(f"Unrecognised model '{model_str}'")
+            if model_str in self.model_citations:
+                citations.extend(self.model_citations[model_str])
+
+        if model_changes is None:
+            model = model_str
+        else:
+            model_list = []
+            last_t = 0
+            last_model = model_str
+            for t, model in model_changes:
+                if model not in self.supported_models:
+                    raise ValueError(f"Unrecognised model '{model}'")
+                if model in self.model_citations:
+                    citations.extend(self.model_citations[model])
+                duration = t - last_t
+                model_list.append(self.model_class_map[last_model](duration=duration))
+                last_model = model
+                last_t = t
+            model_list.append(self.model_class_map[last_model](duration=None))
+            model = model_list
+
+        return model, citations
+
     def simulate(
         self,
         demographic_model,
@@ -164,24 +211,11 @@ def simulate(
         :param dry_run: If True, ``end_time=0`` is passed to :meth:`msprime.simulate()`
             to initialise the simulation and then immediately return.
         :type dry_run: bool
-        :param \\**kwargs: Further arguments passed to :meth:`msprime.simulate()`
+        :param \\**kwargs: Further arguments passed to :meth:`msprime.sim_ancestry()`
         """
-        if msprime_model is None:
-            msprime_model = self.supported_models[0]
-        else:
-            if msprime_model not in self.supported_models:
-                raise ValueError(f"Unrecognised model '{msprime_model}'")
-            if msprime_model in self.model_citations:
-                self.citations.extend(self.model_citations[msprime_model])
-
-        if msprime_change_model is not None:
-            msprime_model = [msprime_model]
-            for t, model in msprime_change_model:
-                if model not in self.supported_models:
-                    raise ValueError(f"Unrecognised model '{model}'")
-                msprime_model.append((t, model))
-                if model in self.model_citations:
-                    self.citations.extend(self.model_citations[model])
+
+        model, citations = self._convert_model_spec(msprime_model, msprime_change_model)
+        self.citations.extend(citations)
 
         if "random_seed" in kwargs.keys():
             if seed is None:
@@ -202,7 +236,7 @@ def simulate(
             demography=demographic_model.model,
             ploidy=2,
             random_seed=seeds[0],
-            model=msprime_model,
+            model=model,
             end_time=0 if dry_run else None,
             **kwargs,
         )
diff --git a/stdpopsim/genetic_maps.py b/stdpopsim/genetic_maps.py
index 6c96487c3..3825d2466 100644
--- a/stdpopsim/genetic_maps.py
+++ b/stdpopsim/genetic_maps.py
@@ -114,7 +114,7 @@ def get_chromosome_map(self, id):
             # Extend map to the end of the chromosome.
             positions = np.append(recomb_map.position, chrom.length)
             rates = np.append(recomb_map.rate, 0)
-            recomb_map = msprime.RateMap(positions, rates)
+            recomb_map = msprime.RateMap(position=positions, rate=rates)
         elif map_length > chrom.length:
             warnings.warn(
                 f"Recombination map has length {map_length}, which is longer than"
diff --git a/stdpopsim/models.py b/stdpopsim/models.py
index 449facec7..a13110300 100644
--- a/stdpopsim/models.py
+++ b/stdpopsim/models.py
@@ -121,7 +121,7 @@ def __init__(
                 # moment.
                 msp_pop.allow_samples = True
                 if local_pop.sampling_time is not None:
-                    msp_pop.sampling_time = local_pop.sampling_time
+                    msp_pop.default_sampling_time = local_pop.sampling_time
                 else:
                     msp_pop.allow_samples = False
         else:
@@ -197,7 +197,7 @@ def get_samples(self, *args):
                     msprime.SampleSet(
                         num_samples=n,
                         population=pop_index,
-                        time=self.populations[pop_index].sampling_time,
+                        time=self.populations[pop_index].default_sampling_time,
                         ploidy=1,  # Avoid breaking too much at once.
                     )
                 )
diff --git a/stdpopsim/slim_engine.py b/stdpopsim/slim_engine.py
index a8a2da7b5..40668d92b 100644
--- a/stdpopsim/slim_engine.py
+++ b/stdpopsim/slim_engine.py
@@ -514,7 +514,9 @@ def msprime_rm_to_slim_rm(recombination_map):
     """
     Convert recombination map from start position coords to end position coords.
     """
-    rates = recombination_map.rate
+    rates = recombination_map.rate.copy()
+    # replace missing values with 0 recombination rate
+    rates[recombination_map.missing] = 0
     ends = [int(pos) - 1 for pos in recombination_map.position]
     return rates, ends[1:]
 
@@ -584,7 +586,7 @@ def fix_time(event):
     subpopulation_splits = []
     for i, epoch in enumerate(epochs):
         for de in epoch.demographic_events:
-            if isinstance(de, msprime.LineageMovementEvent):
+            if isinstance(de, msprime.demography.LineageMovementEvent):
                 # This is using internal msprime APIs here, but it's not worth
                 # updating before we change over to Demes.
                 for lm in de._as_lineage_movements():


Test patch:
diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml
index 66ad24754..ae9cad813 100644
--- a/.github/workflows/tests.yml
+++ b/.github/workflows/tests.yml
@@ -17,7 +17,9 @@ jobs:
     strategy:
       fail-fast: false
       matrix:
-        os: [ubuntu-18.04, windows-2019, macos-10.15]
+        # FIXME removing windows from CI until msprime 1.0 is released
+        # and we have conda packages again.
+        os: [ubuntu-18.04, macos-10.15]
         python: [3.7, 3.9]
     env:
       SLIM_TAG: 3.6
@@ -78,9 +80,8 @@ jobs:
         if: steps.cache.outputs.cache-hit != 'true'
         run: |
           conda install --yes --file=requirements/CI/conda.txt
-          # Install pre-release from non standard label until msprime
-          # 1.0 is tagged.
-          conda install -c conda-forge/label/msprime_rc msprime
+          # Install pre-release from pypi until 1.0 is tagged.
+          pip install --pre msprime
           if [ "$RUNNER_OS" != "Windows" ]; then
             conda install --yes slim==${{ env.SLIM_TAG }}
           fi
diff --git a/tests/test_slim_engine.py b/tests/test_slim_engine.py
index 797c37eb3..310761b81 100644
--- a/tests/test_slim_engine.py
+++ b/tests/test_slim_engine.py
@@ -326,6 +326,9 @@ def test_odd_sample_warning_for_even_samples(self):
         with self.assertWarns(stdpopsim.SLiMOddSampleWarning):
             capture_output(stdpopsim.cli.stdpopsim_main, cmd)
 
+    @unittest.skip("FIXME")
+    # This test seems to be broken - the warning is being issued as we can see it
+    # in the debug stream, but it's not getting captured here.
     def test_odd_sample_warning(self):
         cmd = "-q -e slim --slim-script HomSap -d OutOfAfrica_2T12 -L 100 4 5".split()
         with self.assertWarns(stdpopsim.SLiMOddSampleWarning):