Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

enable slim on windows #1571

Merged
merged 1 commit into from
Jan 6, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions CHANGELOG.rst
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
**New features**:

- Added support for the SLiM engine on Windows. (:user:`petrelharp`, :pr:`1571`)

---------------------
[0.2.1a] - 2024-07-06
---------------------
Expand Down
81 changes: 38 additions & 43 deletions stdpopsim/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -36,8 +36,6 @@
pass


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

logger = logging.getLogger(__name__)


Expand Down Expand Up @@ -1066,47 +1064,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
101 changes: 64 additions & 37 deletions stdpopsim/slim_engine.py
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,12 @@

logger = logging.getLogger(__name__)


def _escape_eidos(s):
# this is for Windows paths passed as strings in Eidos
return "\\\\".join(s.split("\\"))


_slim_upper = """
initialize() {
if (!exists("dry_run"))
Expand Down Expand Up @@ -1198,7 +1204,7 @@ def fix_time(event):
recombination_rates=recomb_rates_str,
recombination_ends=recomb_ends_str,
generation_time=demographic_model.generation_time,
trees_file=trees_file,
trees_file=_escape_eidos(trees_file),
pop_names=f"c({pop_names_str})",
)
)
Expand Down Expand Up @@ -1517,7 +1523,7 @@ def matrix2str(
if logfile is not None:
printsc(
string.Template(_slim_logfile).substitute(
logfile=logfile,
logfile=_escape_eidos(str(logfile)),
loginterval=logfile_interval,
)
)
Expand Down Expand Up @@ -1658,21 +1664,32 @@ def simulate(

run_slim = not slim_script

mktemp = functools.partial(tempfile.NamedTemporaryFile, mode="w")

@contextlib.contextmanager
def script_file_f():
f = mktemp(suffix=".slim") if not slim_script else sys.stdout
yield f
# Don't close sys.stdout.
if not slim_script:
f.close()
def _slim_tempdir():
tempdir = tempfile.TemporaryDirectory(
prefix="stdpopsim_", ignore_cleanup_errors=True
)
ts_filename = os.path.join(tempdir.name, f"{os.urandom(3).hex()}.trees")

with script_file_f() as script_file, mktemp(suffix=".ts") as ts_file:
if run_slim:
script_filename = os.path.join(
tempdir.name, f"{os.urandom(3).hex()}.slim"
)
script_file = open(script_filename, "w", encoding="utf-8")
else:
script_filename = "stdout"
script_file = sys.stdout
yield script_file, script_filename, ts_filename
if run_slim:
script_file.close()
tempdir.cleanup()

with _slim_tempdir() as st:
script_file, script_filename, ts_filename = st

recap_epoch = slim_makescript(
script_file,
ts_file.name,
ts_filename,
demographic_model,
contig,
sample_sets,
Expand All @@ -1690,7 +1707,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 @@ -1700,24 +1717,26 @@ 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:
ts = self._recap_and_rescale(
ts,
seed,
recap_epoch,
contig,
slim_scaling_factor,
keep_mutation_ids_as_alleles,
extended_events,
)
ts = _add_dfes_to_metadata(ts, contig)
if _recap_and_rescale:
ts = self._recap_and_rescale(
ts,
seed,
recap_epoch,
contig,
slim_scaling_factor,
keep_mutation_ids_as_alleles,
extended_events,
)

if contig.inclusion_mask is not None:
ts = stdpopsim.utils.mask_tree_sequence(ts, contig.inclusion_mask, False)
if contig.exclusion_mask is not None:
ts = stdpopsim.utils.mask_tree_sequence(ts, contig.exclusion_mask, True)
if contig.inclusion_mask is not None:
ts = stdpopsim.utils.mask_tree_sequence(
ts, contig.inclusion_mask, False
)
if contig.exclusion_mask is not None:
ts = stdpopsim.utils.mask_tree_sequence(ts, contig.exclusion_mask, True)

return ts

Expand All @@ -1740,7 +1759,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 All @@ -1759,7 +1777,19 @@ def _run_slim(
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
) as proc:
for line in proc.stdout:
procout = proc.communicate()
try:
# we get procout == None with the patching of suprocess.Popen
# that we do in test_slim_engine.py::TestCLI::test_dry_run
outs, errs = procout
except ValueError:
warnings.warn(
stdpopsim.UnspecifiedSLiMWarning(
"Cannot get output from SLiM " "for unknown reasons."
)
)
outs, errs = "", ""
for line in outs.splitlines():
line = line.rstrip()
if line.startswith("WARNING: "):
warnings.warn(
Expand All @@ -1770,14 +1800,13 @@ def _run_slim(
line = line.replace("dbg(self.source); ", "")
logger.debug(line)

stderr = proc.stderr.read()
for line in stderr.splitlines():
for line in errs.splitlines():
if line.startswith("ERROR: "):
logger.error(line[len("ERROR: ") :])

if proc.returncode != 0:
raise SLiMException(
f"{slim_path} exited with code {proc.returncode}.\n{stderr}"
f"{slim_path} exited with code {proc.returncode}.\n{errs}"
)

def _simplify_remembered(self, ts):
Expand Down Expand Up @@ -2029,6 +2058,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())
Loading
Loading