diff --git a/stdpopsim/cli.py b/stdpopsim/cli.py index c27268745..e261db38d 100644 --- a/stdpopsim/cli.py +++ b/stdpopsim/cli.py @@ -35,8 +35,6 @@ pass -IS_WINDOWS = sys.platform.startswith("win") - logger = logging.getLogger(__name__) @@ -1041,47 +1039,44 @@ def time_or_model( "This option may provided multiple times.", ) - # SLiM is not available for windows. - if not IS_WINDOWS: - - def slim_exec(path): - # Hack to set the SLIM environment variable at parse time, - # before get_version() can be called. - os.environ["SLIM"] = path - return path - - slim_parser = top_parser.add_argument_group("SLiM specific parameters") - slim_parser.add_argument( - "--slim-path", - metavar="PATH", - type=slim_exec, - default=None, - help="Full path to `slim' executable.", - ) - slim_parser.add_argument( - "--slim-script", - action="store_true", - default=False, - help="Write script to stdout and exit without running SLiM.", - ) - slim_parser.add_argument( - "--slim-scaling-factor", - metavar="Q", - default=1, - type=float, - help="Rescale model parameters by Q to speed up simulation. " - "See SLiM manual: `5.5 Rescaling population sizes to " - "improve simulation performance`. " - "[default=%(default)s].", - ) - slim_parser.add_argument( - "--slim-burn-in", - metavar="X", - default=10, - type=float, - help="Length of the burn-in phase, in units of N generations " - "[default=%(default)s].", - ) + def slim_exec(path): + # Hack to set the SLIM environment variable at parse time, + # before get_version() can be called. + os.environ["SLIM"] = path + return path + + slim_parser = top_parser.add_argument_group("SLiM specific parameters") + slim_parser.add_argument( + "--slim-path", + metavar="PATH", + type=slim_exec, + default=None, + help="Full path to `slim' executable.", + ) + slim_parser.add_argument( + "--slim-script", + action="store_true", + default=False, + help="Write script to stdout and exit without running SLiM.", + ) + slim_parser.add_argument( + "--slim-scaling-factor", + metavar="Q", + default=1, + type=float, + help="Rescale model parameters by Q to speed up simulation. " + "See SLiM manual: `5.5 Rescaling population sizes to " + "improve simulation performance`. " + "[default=%(default)s].", + ) + slim_parser.add_argument( + "--slim-burn-in", + metavar="X", + default=10, + type=float, + help="Length of the burn-in phase, in units of N generations " + "[default=%(default)s].", + ) subparsers = top_parser.add_subparsers(dest="subcommand") subparsers.required = True diff --git a/stdpopsim/slim_engine.py b/stdpopsim/slim_engine.py index 5c1a114ce..916267683 100644 --- a/stdpopsim/slim_engine.py +++ b/stdpopsim/slim_engine.py @@ -1631,21 +1631,27 @@ def simulate( run_slim = not slim_script - mktemp = functools.partial(tempfile.NamedTemporaryFile, mode="w") + tempdir = tempfile.TemporaryDirectory(prefix="stdpopsim_") + ts_filename = os.path.join(tempdir.name, f"{os.urandom(3).hex()}.trees") @contextlib.contextmanager def script_file_f(): - f = mktemp(suffix=".slim") if not slim_script else sys.stdout - yield f + if run_slim: + fname = os.path.join(tempdir.name, f"{os.urandom(3).hex()}.slim") + f = open(fname, "w") + else: + fname = "stdout" + f = sys.stdout + yield f, fname # Don't close sys.stdout. - if not slim_script: + if run_slim: f.close() - with script_file_f() as script_file, mktemp(suffix=".ts") as ts_file: - + with script_file_f() as sf: + script_file, script_filename = sf recap_epoch = slim_makescript( script_file, - ts_file.name, + ts_filename, demographic_model, contig, sample_sets, @@ -1663,7 +1669,7 @@ def script_file_f(): return None self._run_slim( - script_file.name, + script_filename, slim_path=slim_path, seed=seed, dry_run=dry_run, @@ -1673,7 +1679,7 @@ def script_file_f(): if dry_run: return None - ts = tskit.load(ts_file.name) + ts = tskit.load(ts_filename) ts = _add_dfes_to_metadata(ts, contig) if _recap_and_rescale: @@ -1713,7 +1719,6 @@ def _run_slim( if slim_path is None: slim_path = self.slim_path() - # SLiM v3.6 sends `stop()` output to stderr, which we rely upon. self._assert_min_version("4.0", slim_path) slim_cmd = [slim_path] @@ -2002,6 +2007,4 @@ def recap_and_rescale( return ts -# SLiM does not currently work on Windows. -if sys.platform != "win32": - stdpopsim.register_engine(_SLiMEngine()) +stdpopsim.register_engine(_SLiMEngine()) diff --git a/tests/test_slim_engine.py b/tests/test_slim_engine.py index 16a24e09f..e5db61370 100644 --- a/tests/test_slim_engine.py +++ b/tests/test_slim_engine.py @@ -3,7 +3,6 @@ """ import os import io -import sys import tempfile import math from unittest import mock @@ -21,7 +20,6 @@ import stdpopsim.cli from .test_cli import capture_output -IS_WINDOWS = sys.platform.startswith("win") slim_path = os.environ.get("SLIM", "slim") @@ -33,7 +31,6 @@ def count_mut_types(ts): return [num_neutral, abs(len(selection_coeffs) - num_neutral)] -@pytest.mark.skipif(IS_WINDOWS, reason="SLiM not available on windows") class TestAPI: def test_bad_params(self): engine = stdpopsim.get_engine("slim") @@ -151,7 +148,7 @@ def test_script_generation(self): def test_recombination_map(self): engine = stdpopsim.get_engine("slim") species = stdpopsim.get_species("HomSap") - contig = species.get_contig("chr1", genetic_map="HapMapII_GRCh37") + contig = species.get_contig("chr21", genetic_map="HapMapII_GRCh37") model = stdpopsim.PiecewiseConstantSize(100) samples = {"pop_0": 5} engine.simulate( @@ -277,8 +274,8 @@ def test_recap_and_rescale_on_external_slim_run(self, tmp_path): scriptfile = tmp_path / "slim.script" engine = stdpopsim.get_engine("slim") species = stdpopsim.get_species("HomSap") - contig = species.get_contig("chr1", length_multiplier=0.01) - model = stdpopsim.PiecewiseConstantSize(species.population_size) + contig = species.get_contig("chr19", length_multiplier=0.01) + model = stdpopsim.PiecewiseConstantSize(species.population_size / 10) samples = {"pop_0": 5} seed = 1024 out, _ = capture_output( @@ -291,8 +288,8 @@ def test_recap_and_rescale_on_external_slim_run(self, tmp_path): seed=seed, ) out = re.sub( - 'defineConstant\\("trees_file.+;', - f'defineConstant("trees_file", "{treefile}");', + r'defineConstant\("trees_file.+;', + rf'defineConstant("trees_file", "{treefile}");', out, ) with open(scriptfile, "w") as f: @@ -404,7 +401,6 @@ def test_allele_codes(self): assert all([x.isnumeric() for x in alleles]) -@pytest.mark.skipif(IS_WINDOWS, reason="SLiM not available on windows") class TestCLI: def docmd(self, _cmd): cmd = (f"-q -e slim --slim-burn-in 0 {_cmd} -l 0.001 -c chr1 -s 1234").split() @@ -642,8 +638,8 @@ def test_chromosomal_segment_with_dfe_annotation(self): assert ts.sequence_length == contig.length self.verify_slim_sim(ts, num_samples=10) - # tmp_path is a pytest fixture @pytest.mark.filterwarnings("ignore::stdpopsim.SLiMScalingFactorWarning") + @pytest.mark.usefixtures("tmp_path") def test_errors(self, tmp_path): lines = [ "\t".join(["chr22", "100000", "145000"]), @@ -763,7 +759,6 @@ def test_bad_slim_path(self): os.environ["SLIM"] = saved_slim_env -@pytest.mark.skipif(IS_WINDOWS, reason="SLiM not available on windows") class TestWarningsAndErrors: """ Checks that warning messages are printed when appropriate. @@ -1050,7 +1045,7 @@ def test_warning_when_scaling(self, scaling_factor): class TestSlimAvailable: """ - Checks whether SLiM is available or not on platforms that support it. + Checks whether SLiM is available or not. """ def test_parser_has_options(self): @@ -1058,11 +1053,7 @@ def test_parser_has_options(self): with mock.patch("sys.exit", autospec=True): _, stderr = capture_output(parser.parse_args, ["--help"]) # On windows we should have no "slim" options - assert IS_WINDOWS == ("slim" not in stderr) - - def test_engine_available(self): - all_engines = [engine.id for engine in stdpopsim.all_engines()] - assert IS_WINDOWS == ("slim" not in all_engines) + assert "slim" in stderr def get_test_contig( @@ -1110,7 +1101,6 @@ def allele_frequency(self, ts): return af -@pytest.mark.skipif(IS_WINDOWS, reason="SLiM not available on windows") class TestRecombinationMap(PiecewiseConstantSizeMixin): def verify_recombination_map(self, contig, ts): Q = ts.metadata["SLiM"]["user_metadata"]["Q"] @@ -1176,7 +1166,6 @@ def test_off_by_one(self): assert list(ts.breakpoints()) == [0.0, midpoint, contig.length] -@pytest.mark.skipif(IS_WINDOWS, reason="SLiM not available on windows") class TestGenomicElementTypes(PiecewiseConstantSizeMixin): mut_params = { @@ -1786,7 +1775,6 @@ def test_dominance_coeff_list(self): self.verify_mutation_rates(contig, ts) -@pytest.mark.skipif(IS_WINDOWS, reason="SLiM not available on windows") class TestLogfile(PiecewiseConstantSizeMixin): # tmp_path is a pytest fixture def test_logfile(self, tmp_path): @@ -1810,7 +1798,6 @@ def test_logfile(self, tmp_path): assert np.all(data[:, 2] == 0.0) -@pytest.mark.skipif(IS_WINDOWS, reason="SLiM not available on windows") class TestDrawMutation(PiecewiseConstantSizeMixin): def test_draw_mutation(self): contig = get_test_contig() @@ -2115,7 +2102,6 @@ def test_bad_time(self): ) -@pytest.mark.skipif(IS_WINDOWS, reason="SLiM not available on windows") class TestAlleleFrequencyConditioning(PiecewiseConstantSizeMixin): @pytest.mark.filterwarnings("ignore::stdpopsim.SLiMScalingFactorWarning") def test_drawn_mutation_not_lost(self): @@ -2325,7 +2311,6 @@ def test_no_drawn_mutation(self): ) -@pytest.mark.skipif(IS_WINDOWS, reason="SLiM not available on windows") class TestChangeMutationFitness(PiecewiseConstantSizeMixin): # Testing stdpopsim.ChangeMutationFitness is challenging, because # the side-effects are not deterministic. But if we condition on fixation @@ -2503,7 +2488,6 @@ def test_bad_GenerationAfter_times(self): ) -@pytest.mark.skipif(IS_WINDOWS, reason="SLiM not available on windows") class TestExtendedEvents(PiecewiseConstantSizeMixin): def test_bad_extended_events(self): engine = stdpopsim.get_engine("slim") @@ -2523,7 +2507,6 @@ def test_bad_extended_events(self): ) -@pytest.mark.skipif(IS_WINDOWS, reason="SLiM not available on windows") class TestSelectiveSweep(PiecewiseConstantSizeMixin): @staticmethod def _get_island_model(Ne=1000, migration_rate=0.01): @@ -2969,7 +2952,6 @@ def test_sweep_with_background_selection(self, tmp_path): assert np.all(p1_outside_sweep <= 1) -@pytest.mark.skipif(IS_WINDOWS, reason="SLiM not available on windows") class TestSelectionCoeffFromMutation: species = stdpopsim.get_species("HomSap") @@ -3043,7 +3025,6 @@ def test_errors(self): stdpopsim.selection_coeff_from_mutation(ts, "bar") -@pytest.mark.skipif(IS_WINDOWS, reason="SLiM not available on windows") class TestPloidy: """ Test that population sizes used in SLiM engine are scaled correctly