Skip to content

Commit b0e48ef

Browse files
committed
Merge branch 'master' into multitrait
2 parents ade0a88 + 9232b7e commit b0e48ef

16 files changed

Lines changed: 1762 additions & 857 deletions

EidosScribe/EidosHelpFunctions.rtf

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6686,6 +6686,24 @@ The task keys recognized, and the tasks they govern, are:\
66866686
\f3\fs20 \cf2 Typically, a dictionary of task keys and thread counts is read from a file and set up with this function at initialization time, but it is also possible to change new task thread counts dynamically. If Eidos is not configured to run multithreaded, this function has no effect.\
66876687
\pard\pardeftab397\li720\fi-446\ri720\sb180\sa60\partightenfactor0
66886688

6689+
\f1\fs18 \cf2 (string$)readLine(void)\
6690+
\pard\pardeftab397\li547\ri720\sb60\sa60\partightenfactor0
6691+
6692+
\f0\b\fs20 \cf2 Reads a line of input from the \'93standard input\'94 file.
6693+
\f3\b0 This function is intended to allow for communication between a running Eidos script (such as a SLiM simulation) and an external process that is sending input to it. It reads one line from the standard input and returns it as a singleton
6694+
\f1\fs18 string
6695+
\f3\fs20 . This is done with the C++ function
6696+
\f1\fs18 std::getline()
6697+
\f3\fs20 , using the
6698+
\f1\fs18 std::cin
6699+
\f3\fs20 file. If that call returns false (typically because the end-of-file was reached or there was a read error of some kind), the empty string
6700+
\f1\fs18 ""
6701+
\f3\fs20 will be returned. The
6702+
\f1\fs18 system()
6703+
\f3\fs20 function can also be helpful for related situations.\
6704+
This function will raise an error if called in a GUI environment such as EidosScribe, SLiMgui, or SLiMguiLegacy, since there is no standard input set up for the running script in those environments.\
6705+
\pard\pardeftab397\li720\fi-446\ri720\sb180\sa60\partightenfactor0
6706+
66896707
\f1\fs18 \cf2 (void)rm([Ns\'a0variableNames\'a0=\'a0NULL])\
66906708
\pard\pardeftab397\li547\ri720\sb60\sa60\partightenfactor0
66916709

QtSLiM/help/EidosHelpFunctions.html

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -648,6 +648,9 @@
648648
"UNIQUE_MUTRUNS"<span class="Apple-tab-span"> </span></span>uniquing mutation runs (internal bookkeeping)<span class="s2"><br>
649649
"SURVIVAL"<span class="Apple-tab-span"> </span></span>survival evaluation (no callbacks)</p>
650650
<p class="p5">Typically, a dictionary of task keys and thread counts is read from a file and set up with this function at initialization time, but it is also possible to change new task thread counts dynamically.<span class="Apple-converted-space">  </span>If Eidos is not configured to run multithreaded, this function has no effect.</p>
651+
<p class="p4">(string$)readLine(void)</p>
652+
<p class="p5"><b>Reads a line of input from the “standard input” file.</b><span class="Apple-converted-space">  </span>This function is intended to allow for communication between a running Eidos script (such as a SLiM simulation) and an external process that is sending input to it.<span class="Apple-converted-space">  </span>It reads one line from the standard input and returns it as a singleton <span class="s2">string</span>.<span class="Apple-converted-space">  </span>This is done with the C++ function <span class="s2">std::getline()</span>, using the <span class="s2">std::cin</span> file.<span class="Apple-converted-space">  </span>If that call returns false (typically because the end-of-file was reached or there was a read error of some kind), the empty string <span class="s2">""</span> will be returned.<span class="Apple-converted-space">  </span>The <span class="s2">system()</span> function can also be helpful for related situations.</p>
653+
<p class="p5">This function will raise an error if called in a GUI environment such as EidosScribe, SLiMgui, or SLiMguiLegacy, since there is no standard input set up for the running script in those environments.</p>
651654
<p class="p4">(void)rm([Ns variableNames = NULL])</p>
652655
<p class="p5"><b>Removes variables</b> from the Eidos namespace; in other words, it causes the variables to become undefined.<span class="Apple-converted-space">  </span>Variables are specified by their <span class="s2">string</span> name in the <span class="s2">variableNames</span> parameter.<span class="Apple-converted-space">  </span>If the optional <span class="s2">variableNames</span> parameter is <span class="s2">NULL</span> (the default), <i>all</i> variables will be removed (be careful!).</p>
653656
<p class="p5">In SLiM 3, there was an optional parameter <span class="s2">removeConstants</span> that, if <span class="s2">T</span>, allowed you to remove defined constants (and then potentially redefine them to have a different value).<span class="Apple-converted-space">  </span>The <span class="s2">removeConstants</span> parameter was removed in SLiM 4, since the <span class="s2">defineGlobal()</span> function now provides the ability to define (and redefine) global variables that are not constant.</p>

VERSIONS

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,8 @@ development head (in the master branch):
1818
add the PCG random number generator, switch to pcg32_fast and pcg64_fast, remove all use of the old taus2 and MT19937-64 generators; note this completely breaks backward reproducibility
1919
fix various bugs involving conflicts between defined constants and other symbols, including #573 and #574; this sets new definition rules that could break some existing scripts (but is unlikely to)
2020
fix #575, QtSLiM terminates early when single-stepping with a rescheduled script block
21-
----- last merge into multitrait was done here
21+
fix #579, crash in models with (a) tree-seq recording, (b) multiple chromosomes, AND (c) rejection of proposed offspring in modifyChild()
22+
add readLine() function for obtaining stdin input to running models, thanks to Chris Talbot
2223

2324

2425
multitrait branch:

core/species.cpp

Lines changed: 58 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -5842,8 +5842,62 @@ void Species::RetractNewIndividual()
58425842
// around the code since it seems to keep coming back...
58435843
//current_new_individual_ = nullptr;
58445844

5845-
for (TreeSeqInfo &tsinfo : treeseq_)
5846-
tsk_table_collection_truncate(&tsinfo.tables_, &tsinfo.table_position_);
5845+
size_t trees_count = treeseq_.size();
5846+
5847+
if (trees_count > 0)
5848+
{
5849+
TreeSeqInfo &tsinfo_0 = treeseq_[0];
5850+
5851+
// BCH 12/1/2025: The base table collection can restore its bookmarked position directly;
5852+
// that will reset the bookmarked positions in all of the shared tables as well.
5853+
tsk_table_collection_truncate(&tsinfo_0.tables_, &tsinfo_0.table_position_);
5854+
5855+
// BCH 12/1/2025: In the multichrom case we need to protect against a segfault inside
5856+
// tsk_table_collection_truncate() for the secondary table collections. This is because
5857+
// they have NULL for their various column pointers, and tsk_table_collection_truncate()
5858+
// accesses index 0 of every offset column to get the offset for row 0. (It is always
5859+
// for row 0 in the shared tables because they are zeroed out; their num_rows was zero
5860+
// in RecordTablePosition().) See https://github.com/MesserLab/SLiM/issues/579 for details.
5861+
// BEWARE: This code will need updating if new shared tables are added, or new columns
5862+
// are added within the existing shared table. Any offset column that is accessed in the
5863+
// ..._truncate() functions for the shared tables needs to be protected here.
5864+
tsk_size_t zero_value = 0;
5865+
tsk_size_t *pointer_to_zero_value = &zero_value;
5866+
5867+
for (size_t trees_index = 1; trees_index < trees_count; ++trees_index)
5868+
{
5869+
TreeSeqInfo &tsinfo_i = treeseq_[trees_index];
5870+
5871+
#if DEBUG
5872+
// This protection scheme relies upon the bookmarked row being zero for shared tables;
5873+
// only the zeroth element of each offset column is set up by the hack here.
5874+
if ((tsinfo_i.table_position_.nodes != 0) ||
5875+
(tsinfo_i.table_position_.individuals != 0) ||
5876+
(tsinfo_i.table_position_.populations != 0))
5877+
EIDOS_TERMINATION << "ERROR (Species::RetractNewIndividual): (internal error) tree sequence bookmark for a shared table in a secondary table collection is non-zero." << EidosTerminate();
5878+
#endif
5879+
5880+
tsinfo_i.tables_.nodes.metadata_offset = pointer_to_zero_value;
5881+
tsinfo_i.tables_.individuals.location_offset = pointer_to_zero_value;
5882+
tsinfo_i.tables_.individuals.parents_offset = pointer_to_zero_value;
5883+
tsinfo_i.tables_.individuals.metadata_offset = pointer_to_zero_value;
5884+
tsinfo_i.tables_.populations.metadata_offset = pointer_to_zero_value;
5885+
5886+
tsk_table_collection_truncate(&tsinfo_i.tables_, &tsinfo_i.table_position_);
5887+
5888+
tsinfo_i.tables_.nodes.metadata_offset = NULL;
5889+
tsinfo_i.tables_.individuals.location_offset = NULL;
5890+
tsinfo_i.tables_.individuals.parents_offset = NULL;
5891+
tsinfo_i.tables_.individuals.metadata_offset = NULL;
5892+
tsinfo_i.tables_.populations.metadata_offset = NULL;
5893+
}
5894+
}
5895+
5896+
// The above code boils down to this, which was the old code before #579 came along.
5897+
// It is the same apart from the complicated protection against segfaults:
5898+
//
5899+
// for (TreeSeqInfo &tsinfo : treeseq_)
5900+
// tsk_table_collection_truncate(&tsinfo.tables_, &tsinfo.table_position_);
58475901
}
58485902

58495903
void Species::RecordNewHaplosome(slim_position_t *p_breakpoints, int p_breakpoints_count, Haplosome *p_new_haplosome,
@@ -7585,7 +7639,7 @@ void Species::WriteTreeSequence(std::string &p_recording_tree_path, bool p_simpl
75857639
// Add in the mutation.parent information; valid tree sequences need parents, but we don't keep them while running
75867640
ret = tsk_table_collection_build_index(&output_tables, 0);
75877641
if (ret < 0) handle_error("tsk_table_collection_build_index", ret);
7588-
ret = tsk_table_collection_compute_mutation_parents(&output_tables, 0);
7642+
ret = tsk_table_collection_compute_mutation_parents(&output_tables, TSK_NO_CHECK_INTEGRITY);
75897643
if (ret < 0) handle_error("tsk_table_collection_compute_mutation_parents", ret);
75907644

75917645
{
@@ -7987,7 +8041,7 @@ void Species::CrosscheckTreeSeqIntegrity(void)
79878041
ret = tsk_table_collection_build_index(tables_copy, 0);
79888042
if (ret < 0) handle_error("tsk_table_collection_build_index", ret);
79898043

7990-
ret = tsk_table_collection_compute_mutation_parents(tables_copy, 0);
8044+
ret = tsk_table_collection_compute_mutation_parents(tables_copy, TSK_NO_CHECK_INTEGRITY);
79918045
if (ret < 0) handle_error("tsk_table_collection_compute_mutation_parents", ret);
79928046

79938047
}

eidos/eidos_functions.cpp

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -369,6 +369,7 @@ const std::vector<EidosFunctionSignature_CSP> &EidosInterpreter::BuiltInFunction
369369
signatures->emplace_back((EidosFunctionSignature *)(new EidosFunctionSignature("fileExists", Eidos_ExecuteFunction_fileExists, kEidosValueMaskLogical | kEidosValueMaskSingleton))->AddString_S(gEidosStr_filePath));
370370
signatures->emplace_back((EidosFunctionSignature *)(new EidosFunctionSignature("flushFile", Eidos_ExecuteFunction_flushFile, kEidosValueMaskLogical | kEidosValueMaskSingleton))->AddString_S(gEidosStr_filePath));
371371
signatures->emplace_back((EidosFunctionSignature *)(new EidosFunctionSignature("readFile", Eidos_ExecuteFunction_readFile, kEidosValueMaskString))->AddString_S(gEidosStr_filePath));
372+
signatures->emplace_back((EidosFunctionSignature *)(new EidosFunctionSignature("readLine", Eidos_ExecuteFunction_readLine, kEidosValueMaskString | kEidosValueMaskSingleton)));
372373
signatures->emplace_back((EidosFunctionSignature *)(new EidosFunctionSignature("setwd", Eidos_ExecuteFunction_setwd, kEidosValueMaskString | kEidosValueMaskSingleton))->AddString_S("path"));
373374
signatures->emplace_back((EidosFunctionSignature *)(new EidosFunctionSignature("tempdir", Eidos_ExecuteFunction_tempdir, kEidosValueMaskString | kEidosValueMaskSingleton)));
374375
signatures->emplace_back((EidosFunctionSignature *)(new EidosFunctionSignature("writeFile", Eidos_ExecuteFunction_writeFile, kEidosValueMaskLogical | kEidosValueMaskSingleton))->AddString_S(gEidosStr_filePath)->AddString("contents")->AddLogical_OS("append", gStaticEidosValue_LogicalF)->AddLogical_OS("compress", gStaticEidosValue_LogicalF));

eidos/eidos_functions.h

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -250,6 +250,7 @@ EidosValue_SP Eidos_ExecuteFunction_filesAtPath(const std::vector<EidosValue_SP>
250250
EidosValue_SP Eidos_ExecuteFunction_flushFile(const std::vector<EidosValue_SP> &p_arguments, EidosInterpreter &p_interpreter);
251251
EidosValue_SP Eidos_ExecuteFunction_getwd(const std::vector<EidosValue_SP> &p_arguments, EidosInterpreter &p_interpreter);
252252
EidosValue_SP Eidos_ExecuteFunction_readFile(const std::vector<EidosValue_SP> &p_arguments, EidosInterpreter &p_interpreter);
253+
EidosValue_SP Eidos_ExecuteFunction_readLine(const std::vector<EidosValue_SP> &p_arguments, EidosInterpreter &p_interpreter);
253254
EidosValue_SP Eidos_ExecuteFunction_setwd(const std::vector<EidosValue_SP> &p_arguments, EidosInterpreter &p_interpreter);
254255
EidosValue_SP Eidos_ExecuteFunction_tempdir(const std::vector<EidosValue_SP> &p_arguments, EidosInterpreter &p_interpreter);
255256
EidosValue_SP Eidos_ExecuteFunction_writeFile(const std::vector<EidosValue_SP> &p_arguments, EidosInterpreter &p_interpreter);

eidos/eidos_functions_files.cpp

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,9 @@
2727
#include <stdio.h>
2828
#include <dirent.h>
2929
#include <fstream>
30+
#include <iostream>
3031
#include <sys/stat.h>
32+
#include <unistd.h>
3133

3234
#include "../eidos_zlib/zlib.h"
3335

@@ -220,6 +222,37 @@ EidosValue_SP Eidos_ExecuteFunction_readFile(const std::vector<EidosValue_SP> &p
220222
return result_SP;
221223
}
222224

225+
// (string$)readLine(void)
226+
EidosValue_SP Eidos_ExecuteFunction_readLine(const std::vector<EidosValue_SP> &p_arguments, EidosInterpreter &p_interpreter)
227+
{
228+
#pragma unused (p_arguments, p_interpreter)
229+
230+
// This function was implemented by Chris Talbot 11/19/25 for use in reinforcement learning environments.
231+
// Associated with issue #576.
232+
233+
#ifdef EIDOS_GUI
234+
// Cannot be called inside a GUI since there is (presumably) no stdin set up for the script to read.
235+
EIDOS_TERMINATION << "ERROR (Eidos_ExecuteFunction_readLine): function readLine() is not available in GUI environments (SLiMgui, SLiMguiLegacy, or EidosScribe)." << EidosTerminate(nullptr);
236+
#else
237+
// Read a single line from stdin (command-line mode with interactive terminal)
238+
std::string line;
239+
240+
if (std::getline(std::cin, line))
241+
{
242+
// Control for CRLF vs. LF line endings
243+
if (!line.empty() && line[line.size() - 1] == '\r')
244+
line.pop_back();
245+
246+
return EidosValue_SP(new (gEidosValuePool->AllocateChunk()) EidosValue_String(line));
247+
}
248+
else
249+
{
250+
// EOF or error reading from stdin; return empty string
251+
return gStaticEidosValue_StringEmpty;
252+
}
253+
#endif
254+
}
255+
223256
// (string$)setwd(string$ path)
224257
EidosValue_SP Eidos_ExecuteFunction_setwd(const std::vector<EidosValue_SP> &p_arguments, __attribute__((unused)) EidosInterpreter &p_interpreter)
225258
{

treerec/_README

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,8 @@ The code in `tskit/kastore` is from [kastore/c](https://github.com/tskit-dev/kas
88

99
**Changelog:**
1010

11+
- *30 November 2025*: updated to tskit 1.0.0
12+
- *24 September 2025*: updated to tskit C_1.2.0 (=1.0.0b2) (still kastore C_2.1.1)
1113
- *15 April 2025*: updated to tskit C_1.1.4 (=0.6.2) (still kastore C_2.1.1)
1214
- *7 November 2023*: updated to tskit C_1.1.2 (=0.5.5) (still kastore C_2.1.1)
1315
- *2 June 2022*: updated to tskit C_1.0.0 and kastore C_2.1.1

treerec/implementation.md

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -222,3 +222,47 @@ To read in a tree sequence, SLiM requires that:
222222
This is because of how `__TabulateSubpopulationsFromTreeSequence` works;
223223
probably it could be made more general, but it isn't.
224224

225+
## Multiple chromosomes and multiple tree sequences
226+
227+
In SLiM 5.0 we added support for simulating multiple chromosomes in SLiM, and that added
228+
some wrinkles to how tree-sequence recording is implemented in SLiM. In particular, we
229+
now keep one tree sequence per chromosome being simulated. In `species.h` we now define
230+
a struct named `TreeSeqInfo` that keeps the `tsk_table_collection_t`, in particular, as
231+
well as a couple of other bits of information. Each Species then keeps its own `std::vector`
232+
of `TreeSeqInfo` structs, named `treeseq_`. Where operations used to involve the table
233+
collection, they now typically involve a loop over the elements of `treeseq_` to perform
234+
the operation on each table collection in turn.
235+
236+
The main complication here is that all of these table collections share three tskit tables:
237+
the node, individual, and population tables. This means that the table collections all
238+
have a shared structure, and that needs to be preserved across operations like simplify.
239+
The shared tables are kept by the first chromosome's table collection, in `treeseq_[0]`.
240+
The other table collections zero-fill their node, individual, and population tables most
241+
of the time. That means that, in that state, they are not compliant with tskit
242+
requirements, and trying to use them will often produce a segfault due to a dereference
243+
of a NULL pointer. That is deliberate and useful; it makes it easy to debug situations
244+
where the table collections are being used when they should not be. Sometimes we want the
245+
table collections to actually be usable. For that, `Species::CopySharedTablesIn()` will
246+
do a bitwise, shallow copy of the shared tables into a given table collection; it should
247+
be matched by `DisconnectCopiedSharedTables()` as soon as the operation is done, restoring
248+
the zero-filled table state. See https://github.com/tskit-dev/tskit/pull/2665 for
249+
Jerome's original multi-chromosome parallel simplification example, from which this design
250+
was derived.
251+
252+
The end goal is that this will allow parallel simplification to happen in SLiM. The code
253+
in `Species::SimplifyAllTreeSequences()` now implements the extra bookkeeping needed to
254+
maintain the shared table structure, so the design is ready to be parallelized when I
255+
return to the parallelization project.
256+
257+
Single-chromosome simulations still get saved out as a .trees tree sequence file. With
258+
multiple chromosomes, we now save out a "trees archive" with one .trees file per chromosome.
259+
This is not directly supported on the tskit side at the moment; you can just iterate over
260+
the files in the trees archive and process them as you wish in Python. An example of this
261+
is provided in the SimHumanity paper, https://doi.org/10.47248/hpgg2505040006.
262+
263+
There is some new top-level metadata associated with trees archives and multiple chromosomes.
264+
This is detailed in section 29.1 of the SLiM manual. In particular, there are new top-level
265+
keys `this_chromosome` and `chromosomes` that should be provided in every .trees file. The
266+
`chromosomes` key provides a table of all of the chromosomes involved in the trees archive.
267+
The `this_chromosome` key provides information about the particular chromosome represented
268+
by one particular .trees file in the trees archive.

0 commit comments

Comments
 (0)