Skip to content

Commit

Permalink
enable slim on windows
Browse files Browse the repository at this point in the history
  • Loading branch information
petrelharp committed Jul 6, 2024
1 parent f3769ae commit e141024
Show file tree
Hide file tree
Showing 3 changed files with 62 additions and 83 deletions.
81 changes: 38 additions & 43 deletions stdpopsim/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -35,8 +35,6 @@
pass


IS_WINDOWS = sys.platform.startswith("win")

logger = logging.getLogger(__name__)


Expand Down Expand Up @@ -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
Expand Down
29 changes: 16 additions & 13 deletions stdpopsim/slim_engine.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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,
Expand All @@ -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:
Expand Down Expand Up @@ -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]
Expand Down Expand Up @@ -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())
35 changes: 8 additions & 27 deletions tests/test_slim_engine.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@
"""
import os
import io
import sys
import tempfile
import math
from unittest import mock
Expand All @@ -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")


Expand All @@ -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")
Expand Down Expand Up @@ -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(
Expand Down Expand Up @@ -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(
Expand All @@ -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:
Expand Down Expand Up @@ -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()
Expand Down Expand Up @@ -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"]),
Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -1050,19 +1045,15 @@ 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):
parser = stdpopsim.cli.stdpopsim_cli_parser()
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(
Expand Down Expand Up @@ -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"]
Expand Down Expand Up @@ -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 = {
Expand Down Expand Up @@ -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):
Expand All @@ -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()
Expand Down Expand Up @@ -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):
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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")
Expand All @@ -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):
Expand Down Expand Up @@ -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")
Expand Down Expand Up @@ -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
Expand Down

0 comments on commit e141024

Please sign in to comment.