Skip to content

Commit

Permalink
enable slim on windows
Browse files Browse the repository at this point in the history
escape eidos

utf-8 script file

re fix

maybe better temp file handling?

dry run fix?

even more escapes

changelog
  • Loading branch information
petrelharp committed Jan 6, 2025
1 parent a011bf1 commit 15779b8
Show file tree
Hide file tree
Showing 4 changed files with 225 additions and 206 deletions.
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

0 comments on commit 15779b8

Please sign in to comment.