Repository: tskit-dev/pyslim (language: python)

Issue:
catch out-of-date ts format and throw warning
In places where we use new-SLiM things, like everywhere we extract `tick` from metadata, we should try-catch to throw an informative error, instructing to use `pyslim.update(ts)`.


Discussion / hints:

Fix patch:
diff --git a/.circleci/config.yml b/.circleci/config.yml
index bc6231a8..abccf452 100644
--- a/.circleci/config.yml
+++ b/.circleci/config.yml
@@ -16,8 +16,9 @@ jobs:
           name: Install dependencies and set path
           command: |
             sudo apt-get install libgsl-dev doxygen
-            pip install --user -r requirements/CI/pip-requirements.txt
-            pip install --user -r requirements/CI/conda-requirements.txt
+            pip install --user -r requirements/CI-pip/requirements.txt
+            pip install --user -r requirements/CI-conda/requirements.txt
+            pip install --user -r requirements/CI-docs/requirements.txt
             pip install twine --user
             echo 'export PATH=/home/circleci/.local/bin:$PATH' >> $BASH_ENV
 
@@ -27,6 +28,7 @@ jobs:
             sudo apt-get install cmake
             git clone https://github.com/MesserLab/SLiM.git
             cd SLiM
+            git checkout multispecies # TODO: remove this!!
             if [ ! -e /home/circleci/.local/SLiM.hash ]; then
               git rev-parse HEAD > /home/circleci/.local/SLiM.hash
             fi
diff --git a/CHANGELOG.rst b/CHANGELOG.rst
index e3cd7d50..d59c0425 100644
--- a/CHANGELOG.rst
+++ b/CHANGELOG.rst
@@ -2,6 +2,57 @@
 [UPCOMING.X.X] - XXXX-XX-XX
 ***************************
 
+**Breaking changes**:
+
+- Removed `SlimTreeSequence` class entirely.
+
+- TODO: Deprecated `util.unique_labels_by_group`.
+
+- TODO: what about `slim_ts.mutation_at` and `slim_ts.nucleotide_at` (used in a Recipe)
+
+- Moved some methods of `SlimTreeSequence` to pyslim:
+    * instead of `slim_ts.slim_time(t)` do `pyslim.slim_time(ts, t)`
+    * instead of `slim_ts.individuals_alive_at(t)` do `pyslim.individuals_alive_at(ts, t)`
+    * instead of `slim_ts.individuals_parents(t)` do `pyslim.individuals_parents(ts, t)`
+    * instead of `slim_ts.individuals_ages(t)` do `pyslim.individuals_ages(ts, t)`
+
+
+- In SLiM v4 "generation" has been renamed to "tick", and so corresponding things
+  in pyslim have been renamed: top-level metadata now has `ts.metadata["SLiM"]["tick"]`
+  instead of `ts.metadata["SLiM"]["generation"]`
+
+- Renamed `pyslim.annotate_defaults()` to `pyslim.annotate()`, with slight
+  changes in behavior: since msprime.sim_ancestry() now simulates individuals
+  by default, annotation does not set up individuals: if you have a tree
+  sequence without individuals (e.g., produced by msprime.simulate()) then you
+  need to set up those individuals yourself.
+
+- To update a tree sequence produced by an old version of SLiM to the current one,
+  use `pyslim.update_tables( )`. (However, note that reading it in to SLiM and
+  writing it out again might be even easier.)
+
+- The method `pyslim.set_tree_sequence_metadata` now has arguments `tick` and `cycle`
+  instead of `generation`.
+
+- Removed `pyslim.make_slim_provenance_dict`.
+
+**Other notable changes**:
+
+- Top-level metadata now has a `tick` attribute that is (for now) a synonym
+    for `generation`; the latter will be deprecated at some point in the future.
+
+- TODO: Individual properties `time` and `population` now moved to tskit
+
+- Methods for getting time, population, and location information about individuals
+  are now in tskit:
+    * `SlimTreeSequence.individual_times` is now `TreeSequence.individuals_time()`
+    * `SlimTreeSequence.individual_populations` is now `TreeSequence.individualx_population()`
+    * `SlimTreeSequence.individual_locations` is now `TreeSequence.individuals_location()`
+  However, this will be invisible to the user. In each case note the the
+  location of the "s" has moved (to "individual*s* time" instead of "individual
+  time*s*"), but the original version remains an undocumented alias.
+
+
 ********************
 [0.700] - 2021-12-20
 ********************
diff --git a/README.md b/README.md
index 6e1a2929..cf358436 100644
--- a/README.md
+++ b/README.md
@@ -1,7 +1,8 @@
-# pySLiM
+# pyslim
 
-`pyslim` is a python module to allow reading and writing of SLiM-produced tree sequences
-as a thin interface to [tskit](https://tskit.dev/tskit).
+`pyslim` is a python module that provides a few extra tools for dealing with tree sequences
+produced by SLiM and for preparing other tree seuqences for use with SLiM.
+Most tree sequence functionality is provided by [tskit](https://tskit.dev/tskit).
 Please see [our documentation](https://tskit.dev/pyslim/docs/stable/) for more information.
 (That link is to documentation for the last release release;
 instead, [the latest](https://tskit.dev/pyslim/docs/latest) documentation
diff --git a/docs/Makefile b/docs/Makefile
index 9e48a24b..0d08b960 100644
--- a/docs/Makefile
+++ b/docs/Makefile
@@ -16,6 +16,9 @@ dist: prereqs
 
 clean:
 	rm -fR _build
+	rm -f *.trees
 
-prereqs:
-	slim -s 23 example_sim.slim
+example_sim.trees : example_sim.slim
+	slim -s 23 $<
+
+prereqs: example_sim.trees
diff --git a/docs/_toc.yml b/docs/_toc.yml
index e772cba1..3c3d9f7d 100644
--- a/docs/_toc.yml
+++ b/docs/_toc.yml
@@ -1,22 +1,21 @@
-- file: introduction
-- part: Getting started
+format: jb-book
+root: introduction
+parts:
+- caption: Getting started
   chapters:
-    - file: installation
-
-- part: Using pyslim
+  - file: installation
+- caption: Using pyslim
   chapters:
-    - file: tutorial
-    - file: vignette_space
-    - file: vignette_continuing
-    - file: vignette_coalescent_diversity
-    - file: vignette_parallel_phylo
-    - file: metadata
-    - file: previous_versions
-
-- part: pyslim reference
+  - file: tutorial
+  - file: vignette_space
+  - file: vignette_continuing
+  - file: vignette_coalescent_diversity
+  - file: vignette_parallel_phylo
+  - file: metadata
+  - file: previous_versions
+- caption: pyslim reference
   chapters:
-    - file: python_api
-
-- part: Miscellaneous
+  - file: python_api
+- caption: Miscellaneous
   chapters:
   - file: development
diff --git a/docs/metadata.md b/docs/metadata.md
index 4e500b60..33fdbc22 100644
--- a/docs/metadata.md
+++ b/docs/metadata.md
@@ -19,7 +19,7 @@ import numpy as np
 import random
 random.seed(23)
 
-ts = pyslim.load("example_sim.trees")
+ts = tskit.load("example_sim.trees")
 tables = ts.tables
 ```
 
@@ -47,7 +47,8 @@ If `ts` is your tree sequence, then `ts.metadata` is a dict,
 and `ts.metadata["SLiM"]` contains information about the simulation:
 
 - `file_version`: the version of the SLiM tree sequence file format
-- `generation`: the value of `sim.generation` within SLiM when the file was written out
+- `tick`: the value of `community.tick` within SLiM when the file was written out
+- `cycle`: the value of `sim.cycle` within SLiM when the file was written out
 - `model_type`: either `"WF"` or `"nonWF"`
 - `nucleotide_based`: whether this is a nucleotide-based simulation
 - `separate_sexes`: whether the simulation has separate sexes or not
@@ -99,7 +100,7 @@ So, `ts.mutation(12).metadata["mutation_list"]` is a list, each of whose entries
 - `mutation_type`: the numeric ID of the `MutationType` within SLiM
 - `selection_coeff`: the selection coefficient
 - `subpopulation`: the numeric ID of the subpopulation the mutation occurred in
-- `slim_time`: the value of `sim.generation` when the mutation occurred
+- `slim_time`: the value of `community.tick` when the mutation occurred
 - `nucleotide`: either `-1` if there is no associated nucleotide, or the numeric code for the nucleotide (see {data}`.NUCLEOTIDES`)
 
 
@@ -132,19 +133,19 @@ Mostly, you don't have to convert between the two,
 unless you want to match up information in a tree sequence
 with information written out by SLiM itself.
 In other words, SLiM's time counter measures the number of time steps
-("generations") since the start of the simulation,
+("tick") since the start of the simulation,
 and times in the tree sequence record how long before the end of the simulation.
 However, there are Some Details, and off-by-one errors are easy to make,
 so we'll spell it out in detail.
 
-SLiM's time counter is called the "generation"
+SLiM's time counter is called the "tick"
 (although a "year" or "life cycle" would be a more appropriate name for a nonWF model).
-The SLiM generation starts at 1, and records which round of the life cycle the simulation is in.
+The SLiM tick starts at 1, and records which round of the life cycle the simulation is in.
 However, the order of the life cycle differs between WF and nonWF models:
 in a WF model, it is "*early* {math}`\to` *birth* {math}`\to` *late*",
 while in a nonWF model, it is "*birth* {math}`\to` *early* {math}`\to` *late*".
-Usually, the first set of individuals are created in the *early()* phase of generation 1,
-and so in a WF model reproduce immediately, in the same generation they were "born".
+Usually, the first set of individuals are created in the *early()* phase of tick 1,
+and so in a WF model reproduce immediately, in the same tick they were "born".
 Parents and offspring cannot have the same birth time in the tree sequence,
 and so some clever bookkeeping was required.
 You'll want to refer to the tables below to see what's going on.
@@ -159,7 +160,7 @@ tskit time ago is recorded as minus one times the number of birth phases so far.
 When the tree sequence is output, the current cumulative number of birth phases
 is added to this,
 so "tskit time ago" is, equivalently, "how many birth phases happened since this time".
-In a nonWF model, the two counters ("generation" and "number of birth phases")
+In a nonWF model, the two counters ("tick" and "number of birth phases")
 are always in sync; but in a WF they are not (during *early*).
 The extra wrinkle this introduces is that the correspondence between "tskit time ago"
 and "SLiM time" depends on *which phase the tree sequence was recorded in*,
@@ -170,10 +171,10 @@ To help keep all this straight, here are schematics for WF and nonWF models.
 
 ```{tabbed} WF model
 
-For a WF model, the SLiM generation (first column) can be obtained by subtracting the
-tskit time ago from the SLiM generation at time of output only during the same stage that output occured in.
+For a WF model, the SLiM tick (first column) can be obtained by subtracting the
+tskit time ago from the SLiM tick at time of output only during the same stage that output occured in.
 
-|    generation      |       stage         |  # births          |                                |  tskit time ago, early output |   tskit time ago, late output |
+|    tick            |       stage         |  # births          |                                |  tskit time ago, early output |   tskit time ago, late output |
 |--------------------|---------------------|--------------------|--------------------------------|-------------------------------|-------------------------------|
 |       1            |       early         |       0            | {math}`\leftarrow` add subpops |        n-1                    |         n                     |
 |       1            |       birth         |       1            |                                |        n-2                    |         n-1                   |
@@ -199,10 +200,10 @@ tskit time ago from the SLiM generation at time of output only during the same s
 
 ```{tabbed} nonWF model
 
-Note that for nonWF models the SLiM generation (first column) can always be obtained by subtracting the
-tskit time ago from the SLiM generation at time of output.
+Note that for nonWF models the SLiM time (first column) can always be obtained by subtracting the
+tskit time ago from the SLiM time at time of output.
 
-|    generation      |       stage         |  # births          |                                |  tskit time ago, early output |   tskit time ago, late output |
+|    tick            |       stage         |  # births          |                                |  tskit time ago, early output |   tskit time ago, late output |
 |--------------------|---------------------|--------------------|--------------------------------|-------------------------------|-------------------------------|
 |       1            |       birth         |       1            |                                |        n-1                    |         n-1                   |
 |       1            |       early         |       1            | {math}`\leftarrow` add subpops |        n-1                    |         n-1                   |
@@ -225,17 +226,16 @@ tskit time ago from the SLiM generation at time of output.
 |       n            |       late          |       n            |                                | treeSeqOutput {math}`\to`     |         0                     |
 
 ```
-When the tree sequence is written out, SLiM records the value of its current generation,
-which can be found in the metadata: ``ts.metadata['SLiM']['generation']``
-(or, the ``ts.slim_generation`` attribute).
+When the tree sequence is written out, SLiM records the value of its current tick,
+which can be found in the metadata: ``ts.metadata['SLiM']['tick']``.
 In most cases, the "SLiM time" referred to by a ``time`` in the tree sequence
-(i.e., the value that would be reported by ``sim.generation``
+(i.e., the value that would be reported by ``community.tick``
 within SLiM at the point in time thus referenced)
-can be obtained by subtracting ``time`` from ``ts.slim_generation``.
+can be obtained by subtracting ``time`` from ``ts.metadata['SLiM']['tick']``.
 **However,** in WF models, birth happens between the "early()" and "late()" stages,
 so if the tree sequence was written out using ``sim.treeSeqOutput()`` during "early()" in a WF model,
 the tree sequence's times measure time before the last set of individuals are born,
-i.e., before SLiM time step ``ts.slim_generation - 1``.
+i.e., before SLiM time step ``ts.metadata['SLiM']['tick'] - 1``.
 The stage that the tree sequence was saved is recorded in the metadata of the tree sequence,
 as ``ts.metadata['SLiM']['stage']``.
 Using this, we can convert from the times of a tree sequence ``ts``
@@ -243,7 +243,7 @@ to SLiM time as follows:
 
 ```{code-cell}
 def slim_time(ts, time, stage):
-  slim_time = ts.metadata["SLiM"]["generation"] - time
+  slim_time = ts.metadata["SLiM"]["tick"] - time
   if ts.metadata['SLiM']['model_type'] == "WF":
     if (ts.metadata['SLiM']['stage'] == "early"
         and stage == "late"):
@@ -257,7 +257,7 @@ def slim_time(ts, time, stage):
 This is what is computed by the {meth}`.SlimTreeSequence.slim_time` method
 (which also has a ``stage`` argument).
 
-Some of the other methods in pyslim -- those that depend on {meth}`.SlimTreeSequence.individuals_alive_at`
+Some of the other methods in pyslim -- those that depend on {meth}`pyslim.individuals_alive_at`
 -- need you to tell them during which stage the tree sequence was saved with ``sim.treeSeqOutput``,
 and need this to be the same as the stage that any individuals were saved with ``sim.treeSeqRememberIndividuals``.
 This argument, ``remembered_stage``, defaults to "late()";
diff --git a/docs/neutral_restart.slim b/docs/neutral_restart.slim
index 4662c4ee..e837422b 100644
--- a/docs/neutral_restart.slim
+++ b/docs/neutral_restart.slim
@@ -18,7 +18,7 @@ reproduction(p1) {
 1 early() { 
    sim.readFromPopulationFile("initialize_nonWF.trees");
    catn("Loaded " + length(sim.subpopulations)
-        + " populations from a file; now in generation " + sim.generation);
+        + " populations from a file; now in generation " + sim.cycle);
    catn("Population sizes: " + paste(sim.subpopulations.individualCount));
 }
 
@@ -26,7 +26,7 @@ reproduction(p1) {
    p0.fitnessScaling = K / p0.individualCount;
 }
 
-10 {
+10 early() {
    sim.treeSeqOutput("nonWF_restart.trees");
    catn("Done.");
    sim.simulationFinished();
diff --git a/docs/phylo_bgs.slim b/docs/phylo_bgs.slim
index 65d97c1d..de37c833 100644
--- a/docs/phylo_bgs.slim
+++ b/docs/phylo_bgs.slim
@@ -1,7 +1,7 @@
 // The following constants need to be defined:
 // - outfile: path to save the tree sequence of the simulation.
 // - popsize: population size.
-// - num_gens: run simulation for num_gens generations.
+// - num_gens: run simulation for num_gens ticks.
 // - infile: a string with path to tree sequence to start from; optional.
 initialize()
 {
@@ -19,7 +19,7 @@ initialize()
     if (infile == "") {
         p = sim.addSubpop("p1", popsize);
     } else {
-        // relaoding must happen in late()
+        // reloading must happen in late()
         sim.readFromPopulationFile(infile);
         parent = sim.subpopulations[0];
         p = sim.addSubpopSplit(max(sim.subpopulations.id) + 1, popsize, parent);
@@ -30,8 +30,8 @@ initialize()
 
 // schedule the end of the simulation
 1 late() {
-    finalgen = num_gens + sim.generation;
-    sim.rescheduleScriptBlock(s0, generations=finalgen);
+    finaltick = num_gens + community.tick;
+    community.rescheduleScriptBlock(s0, ticks=finaltick);
 }
 
 // event that saves the tree sequence
diff --git a/docs/previous_versions.md b/docs/previous_versions.md
index bb155a23..b7415739 100644
--- a/docs/previous_versions.md
+++ b/docs/previous_versions.md
@@ -25,6 +25,20 @@ tables = ts.tables
 
 # Migrating from previous versions of pyslim
 
+## 1.0
+
+TODO: explain changes.
+Rough outline:
+
+1. change `generation` to `tick`
+2. change `pyslim.load( )` to `tskit.load( )`
+3. remove `SlimTreeSequence( )`
+4. change `ts.individuals_alive_at(t)` to `pyslim.individuals_alive_at(ts, t)`
+5. change `pyslim.annotate_defaults( )` to `pyslim.annotate( )`
+
+
+## 0.700
+
 A number of features that were first introduced in pyslim have been made part of core
 tskit functionality. For instance, reference sequence support was provided (although
 loosely) inpyslim to support SLiM's nucleotide models, but is now part of a standard
diff --git a/docs/python_api.md b/docs/python_api.md
index 4f6ed08b..d7554595 100644
--- a/docs/python_api.md
+++ b/docs/python_api.md
@@ -17,7 +17,7 @@ import pyslim, tskit, msprime
 from IPython.display import SVG
 import numpy as np
 
-ts = pyslim.load("example_sim.trees")
+ts = tskit.load("example_sim.trees")
 tables = ts.tables
 ```
 
@@ -38,11 +38,15 @@ Here is a quick reference to some of the methods:
 .. autosummary::
 
   recapitate
+  annotate
+  individuals_alive_at
+  individual_ages_at
+  slim_time
   convert_alleles
   generate_nucleotides
-  update_tables
   population_size
   default_slim_metadata
+  update
 ```
 
 
@@ -64,7 +68,7 @@ Here is a quick reference to some of the methods:
 ```
 
 ```{eval-rst}
-.. autofunction::  update_tables
+.. autofunction::  update
 ```
 
 ## Summarizing tree sequences
@@ -72,24 +76,20 @@ Here is a quick reference to some of the methods:
 Additionally, ``pyslim`` contains the following methods:
 
 ```{eval-rst}
-.. autofunction::  population_size
+.. autofunction::  individuals_alive_at
 ```
 
+```{eval-rst}
+.. autofunction::  individual_ages_at
+```
 
-## Additions to the tree sequence
-
-The {class}`tskit.TreeSequence` class represents a sequence of trees
-that describes the genealogical history of a population.
-Here, we describe pyslim's additions to this class.
-
-
-### The SLiM Tree Sequence
-
+```{eval-rst}
+.. autofunction::  slim_time
+```
 
-:::{eval-rst}
-.. autoclass:: pyslim.SlimTreeSequence()
-    :members:
-:::
+```{eval-rst}
+.. autofunction::  population_size
+```
 
 
 ## Metadata
@@ -110,24 +110,13 @@ These two functions will add default SLiM metadata to a tree sequence (or the
 underlying tables), which can then be modified and loaded into SLiM.
 
 :::{eval-rst}
-.. autofunction:: pyslim.annotate_defaults
-:::
-
-:::{eval-rst}
-.. autofunction:: pyslim.annotate_defaults_tables
+.. autofunction:: pyslim.annotate
 :::
 
-
-### Provenances
-
 :::{eval-rst}
-.. autoclass:: pyslim.ProvenanceMetadata
-   :members:
+.. autofunction:: pyslim.annotate_tables
 :::
 
-:::{eval-rst}
-.. autofunction:: pyslim.get_provenance
-:::
 
 
 (sec_constants_and_flags)=
diff --git a/docs/rapid_adaptation.slim b/docs/rapid_adaptation.slim
index 9aaa5221..ed2ed497 100644
--- a/docs/rapid_adaptation.slim
+++ b/docs/rapid_adaptation.slim
@@ -7,7 +7,7 @@ initialize() {
 	initializeRecombinationRate(1e-8);
 }
 
-1 {
+1 early() {
 	sim.addSubpop("p1", 10000);
 }
 
diff --git a/docs/selection.slim b/docs/selection.slim
index 3e8f29b1..3c1860b4 100644
--- a/docs/selection.slim
+++ b/docs/selection.slim
@@ -14,6 +14,6 @@ initialize()
    sim.addSubpop("p1", 1000);
 }
 
-1000 {
+1000 early() {
    sim.treeSeqOutput("selection.trees");
 }
diff --git a/docs/tutorial.md b/docs/tutorial.md
index 44dfda56..1851ae2e 100644
--- a/docs/tutorial.md
+++ b/docs/tutorial.md
@@ -14,14 +14,21 @@ kernelspec:
 ```{code-cell}
 :tags: [remove-cell]
 
+import warnings
 import pyslim, tskit, msprime
 from IPython.display import SVG
 import numpy as np
 import util
 
 np.random.seed(1234)
+warnings.simplefilter('ignore', msprime.TimeUnitsMismatchWarning)
 ```
 
+```{eval-rst}
+.. currentmodule:: pyslim
+```
+
+
 
 # Tutorial
 
@@ -44,16 +51,16 @@ that we have run for 10,000 generations without neutral mutations. Now, we wish
 extract whole-genome genotype data for only 1,000 individuals. Here's one way to do it:
 
 
-1. {meth}`.SlimTreeSequence.recapitate` :
+1. {func}`.recapitate` :
    The simulation has likely not reached demographic equilibrium - it has not
    *coalesced* entirely; recapitation uses coalescent simulation to provide
    a "prior history" for the initial generation of the simulation.
 
-2. {meth}`.SlimTreeSequence.simplify` : For efficiency, subset the tree
+2. {meth}`simplify() <tskit.TreeSequence.simplify>` : For efficiency, subset the tree
    sequence to only the information relevant for those 1,000 individuals
    we wish to sample.
 
-3. {meth}`msprime.sim_mutations` : Add neutral mutations to the tree sequence.
+3. {func}`msprime.sim_mutations` : Add neutral mutations to the tree sequence.
 
 
 These steps are described below. First, to get something to work with,
@@ -307,9 +314,9 @@ can be done with the {meth}`tskit.TreeSequence.simplify` method:
 
 ```{code-cell}
 import numpy as np
-np.random.seed(3)
-alive_inds = rts.individuals_alive_at(0)
-keep_indivs = np.random.choice(alive_inds, 100, replace=False)
+rng = np.random.default_rng(seed=3)
+alive_inds = pyslim.individuals_alive_at(rts, 0)
+keep_indivs = rng.choice(alive_inds, 100, replace=False)
 keep_nodes = []
 for i in keep_indivs:
   keep_nodes.extend(rts.individual(i).nodes)
@@ -322,7 +329,7 @@ print(f"Before, there were {rts.num_samples} sample nodes (and {rts.num_individu
 ```
 
 **Note** that you must pass simplify a list of *node IDs*, not individual IDs.
-Here, we used the {meth}`.SlimTreeSequence.individuals_alive_at` method to obtain the list
+Here, we used the {meth}`.individuals_alive_at` method to obtain the list
 of individuals alive today.
 Also note that there are *still* more than 100 individuals remaining - 15 non-sample individuals
 have not been simplified away,
@@ -356,14 +363,12 @@ but any of the other mutation models in msprime could be used.
 This works as follows:
 
 ```{code-cell}
-ts = pyslim.SlimTreeSequence(
-       msprime.sim_mutations(
+ts = msprime.sim_mutations(
            sts,
            rate=1e-8,
            model=msprime.SLiMMutationModel(type=0),
            keep=True,
-           )
-    )
+)
 
 print(f"The tree sequence now has {ts.num_mutations} mutations,\n"
       f"and mean pairwise nucleotide diversity is {ts.diversity():0.3e}.")
@@ -385,13 +390,6 @@ What's going on here? Let's step through the code.
     In this example there aren't any, so this isn't strictly necessary,
     but this is a good default.
 
-4. We have wrapped the msprime call in a call to
-    {class}`pyslim.SlimTreeSequence`, because {meth}`msprime.sim_mutations`
-    returns a standard {class}`tskit.TreeSequence`,
-    and by converting it back into a ``pyslim`` tree sequence we can still use the methods
-    defined by ``pyslim``, and ensures the reference sequence (if present)
-    is not lost.
-
 
 
 (sec_extracting_individuals)=
@@ -407,13 +405,13 @@ This section demonstrates some basic manipulations of individuals.
 
 The first, most common method to extract individuals is simply to get all
 those that were alive at a particular time,
-using {meth}`.SlimTreeSequence.individuals_alive_at`. For instance, to get
+using {meth}`.individuals_alive_at`. For instance, to get
 the list of individual IDs of all those alive at the end of the
 simulation (i.e., zero time units ago), we could do:
 
 ```{code-cell}
-orig_ts = pyslim.load("example_sim.trees")
-alive = orig_ts.individuals_alive_at(0)
+orig_ts = tskit.load("example_sim.trees")
+alive = pyslim.individuals_alive_at(orig_ts, 0)
 
 print(f"There are {len(alive)} individuals alive in the final generation.")
 ```
@@ -423,11 +421,9 @@ so one way to take a sample of living individuals
 and write their SNPs to a VCF is:
 
 ```{code-cell}
-np.random.seed(1)
-keep_indivs = np.random.choice(alive, 100, replace=False)
-ts = pyslim.SlimTreeSequence(
-    msprime.sim_mutations(orig_ts, rate=1e-8, random_seed=1)
-)
+rng = np.random.default_rng(seed=1)
+keep_indivs = rng.choice(alive, 100, replace=False)
+ts = msprime.sim_mutations(orig_ts, rate=1e-8, random_seed=1)
 with open("example_snps.vcf", "w") as vcffile:
     ts.write_vcf(vcffile, individuals=keep_indivs)
 ```
@@ -448,7 +444,7 @@ there are often individuals retained after simplification with
 one or more non-sample nodes.
 So, to output genotypes after simplification, we need to also check
 that the individuals' nodes are also *samples*.
-As mentioned earlier, {meth}`.SlimTreeSequence.simplify` takes a list
+As mentioned earlier, {meth}`tskit.TreeSequence.simplify` takes a list
 of nodes as input:
 
 ```{code-cell}
@@ -456,9 +452,7 @@ keep_nodes = []
 for i in keep_indivs:
     keep_nodes.extend(orig_ts.individual(i).nodes)
 sts = rts.simplify(keep_nodes)
-ts = pyslim.SlimTreeSequence(
-    msprime.sim_mutations(sts, rate=1e-8, random_seed=1)
-)
+ts = msprime.sim_mutations(sts, rate=1e-8, random_seed=1)
 ```
 Individuals are retained by simplify if any of their nodes are,
 so we would get an alive individual without sample nodes if, for instance,
@@ -469,7 +463,7 @@ alive individuals, we encounter a (somewhat confusing) error:
 
 ```{code-cell}
 try:
-    alive = ts.individuals_alive_at(0)
+    alive = pyslim.individuals_alive_at(ts, 0)
     with open("example_snps.vcf", "w") as vcffile:
         ts.write_vcf(vcffile, individuals=alive)
 except Exception as e:
@@ -488,7 +482,7 @@ using {meth}`is_sample() <tskit.Node.is_sample>`:
 
 ```{code-cell}
 indivlist = []
-for i in ts.individuals_alive_at(0):
+for i in pyslim.individuals_alive_at(ts, 0):
     ind = ts.individual(i)
     if ts.node(ind.nodes[0]).is_sample():
        indivlist.append(i)
@@ -520,12 +514,13 @@ To count up how many individuals are in each population,
 we could do:
 
 ```{code-cell}
-orig_ts = pyslim.load("migrants.trees")
-alive = orig_ts.individuals_alive_at(0)
+orig_ts = tskit.load("migrants.trees")
+alive = pyslim.individuals_alive_at(orig_ts, 0)
 num_alive = [0 for _ in range(orig_ts.num_populations)]
 for i in alive:
   ind = orig_ts.individual(i)
-  num_alive[ind.population] += 1
+  ind_population = orig_ts.node(ind.nodes[0]).population
+  num_alive[ind_population] += 1
 
 for pop, num in enumerate(num_alive):
   print(f"Number of individuals in population {pop}: {num}")
@@ -559,11 +554,11 @@ for pop in demography.populations:
     pop.initial_size = 1000
 
 demography.add_migration_rate_change(
-    time=orig_ts.metadata['SLiM']['generation'],
+    time=orig_ts.metadata['SLiM']['tick'],
     rate=0.1, source="p1", dest="p2",
 )
 demography.add_migration_rate_change(
-    time=orig_ts.metadata['SLiM']['generation'],
+    time=orig_ts.metadata['SLiM']['tick'],
     rate=0.1, source="p2", dest="p1",
 )
 rts = pyslim.recapitate(
@@ -571,11 +566,10 @@ rts = pyslim.recapitate(
         recombination_rate=1e-8,
         random_seed=4
 )
-ts = pyslim.SlimTreeSequence(
-        msprime.sim_mutations(
+ts = msprime.sim_mutations(
                     rts, rate=1e-8,
                     model=msprime.SLiMMutationModel(type=0),
-                    random_seed=7)
+                    random_seed=7
 )
 ```
 
@@ -636,7 +630,7 @@ produced by SLiM. This is described in more detail in the SLiM manual, but brief
   {data}`.INDIVIDUAL_TYPE_MALE`, or {data}`.INDIVIDUAL_TYPE_HERMAPHRODITE`).
 - ``flags`` holds additional information about the individual recorded by SLiM
   (currently, only whether the individual has migrated or not:
-  see :ref:`sec_constants_and_flags`).
+  see {ref}`sec_constants_and_flags`).
 
 
 We can use this metadata in many ways, for example, to create an age distribution by sex:
@@ -645,15 +639,15 @@ We can use this metadata in many ways, for example, to create an age distributio
 import numpy as np
 max_age = max([ind.metadata["age"] for ind in ts.individuals()])
 age_table = np.zeros((max_age + 1, 2))
-age_labels = {pyslim.INDIVIDUAL_TYPE_FEMALE: 'females',
-             pyslim.INDIVIDUAL_TYPE_MALE: 'males'}
-for i in ts.individuals_alive_at(0):
-  ind = ts.individual(i)
-  age_table[ind.metadata["age"], ind.metadata["sex"]] += 1
+age_labels = { pyslim.INDIVIDUAL_TYPE_FEMALE: 'females',
+               pyslim.INDIVIDUAL_TYPE_MALE: 'males' }
+for i in pyslim.individuals_alive_at(ts, 0):
+    ind = ts.individual(i)
+    age_table[ind.metadata["age"], ind.metadata["sex"]] += 1
 
 print(f"number\t{age_labels[0]}\t{age_labels[1]}")
 for age, x in enumerate(age_table):
-  print(f"{age}\t{x[0]}\t{x[1]}")
+    print(f"{age}\t{x[0]}\t{x[1]}")
 ```
 
 We have looked up how to interpret the ``sex`` attribute
@@ -665,21 +659,21 @@ all individuals would have sex equal to {data}`.INDIVIDUAL_TYPE_HERMAPHRODITE`
 
 Several fields associated with individuals are also available as numpy arrays,
 across all individuals at once:
-{attr}`.SlimTreeSequence.individual_locations`,
-{attr}`.SlimTreeSequence.individual_populations`,
-{attr}`.SlimTreeSequence.individual_ages`,
-and {attr}`.SlimTreeSequence.individual_times` (also see
-{meth}`.SlimTreeSequence.individual_ages_at`). Using these can sometimes be easier than
+{attr}`tskit.TreeSequence.individuals_location`,
+{attr}`tskit.TreeSequence.individuals_population`,
+{attr}`tskit.TreeSequence.individual_times` (also see
+{meth}`.individual_ages` and {meth}`.individual_ages_at`).
+Using these can sometimes be easier than
 iterating over individuals as above. For example,
 suppose that we want to randomly sample 10 individuals alive and older than 2 time steps
 from each of the populations at the end of the simulation,
 and simplify the tree sequence to retain only those individuals.
-This can be done using the numpy arrays {attr}`.SlimTreeSequence.individual_ages`
-and {attr}`.SlimTreeSequence.individual_populations` as follows:
+This can be done using the numpy arrays {attr}`pyslim.individual_ages`
+and `.individuals_population` as follows:
 
 ```{code-cell}
-alive = ts.individuals_alive_at(0)
-adults = alive[ts.individual_ages[alive] > 2]
+alive = pyslim.individuals_alive_at(ts, 0)
+adults = alive[pyslim.individual_ages(ts)[alive] > 2]
 pops = [
    [i for i in adults if ts.individual(i).metadata['subpopulation'] == k]
    for k in [1, 2]
@@ -863,17 +857,15 @@ ts = msprime.sim_ancestry(
             recombination_rate=1e-8,
             sequence_length=1e6,
             random_seed=5)
-ts = pyslim.annotate_defaults(ts, model_type="nonWF", slim_generation=1, annotate_mutations=False)
+ts = pyslim.annotate(ts, model_type="nonWF", tick=1)
 assert ts.num_individuals == 200
 assert ts.num_samples == 400
 ```
-We have set ``slim_generation`` to 1;
+We have set ``tick`` to 1;
 this means that as soon as we load the tree sequence into SLiM,
 SLiM will set the current time counter to 1.
-(If we set ``slim_generation`` to 100, then any script blocks scheduled to happen before 100
+(If we set ``tick`` to 100, then any script blocks scheduled to happen before 100
 would not execute after loading the tree sequence.)
-Furthermore, since we have simulated SLiM mutations,
-we've set ``annotate_mutations`` to False (otherwise it would overwrite the metadata).
 
 We now have 200 diploids (so, 400 sampled nodes).
 Here's individual 199, which hsa SLiM metadata:
@@ -896,13 +888,11 @@ stored in the mutation metadata.
 To modify the mutations to be under selection,
 see {ref}`sec_vignette_coalescent_diversity`.
 ```{code-cell}
-ts = pyslim.SlimTreeSequence(
-        msprime.sim_mutations(
-                    ts, rate=1e-8,
-                    model=msprime.SLiMMutationModel(type=0),
-                    random_seed=9
-        )
-     )
+ts = msprime.sim_mutations(
+                ts, rate=1e-8,
+                model=msprime.SLiMMutationModel(type=0),
+                random_seed=9
+)
 ```
 Now the mutations have SLiM metadata.
 For instance, here's the first mutation:
@@ -951,7 +941,7 @@ slim -s 23 selection.slim
 First, let's see how many mutations there are:
 
 ```{code-cell}
-ts = pyslim.load("selection.trees")
+ts = tskit.load("selection.trees")
 print(f"Number of sites: {ts.num_sites}\n"
       f"Number of mutations: {ts.num_mutations}")
 ```
@@ -990,7 +980,7 @@ This is not a nucleotide model, so the nucleotide entry is `-1`.
 Note that `m.time` and `m.metadata['mutation_list'][0]['slim_time']` are in this case redundant:
 they contain the same information, but the first is in tskit time
 (i.e., number of steps before the tree sequence was written out)
-and the second is using SLiM's internal "generation" counter.
+and the second is using SLiM's internal "tick" counter.
 
 Also note that the mutation's metadata is a *list* of metadata entries.
 That's because of SLiM's mutation stacking feature.
diff --git a/docs/vignette_coalescent_diversity.md b/docs/vignette_coalescent_diversity.md
index 9de777e7..f84f96b8 100644
--- a/docs/vignette_coalescent_diversity.md
+++ b/docs/vignette_coalescent_diversity.md
@@ -116,7 +116,7 @@ First, we'll add SLiM metadata to all of these things,
 a procedure we call "annotating".
 
 ```{code-cell}
-ots = pyslim.annotate_defaults(ots, model_type="WF", slim_generation=1)
+ots = pyslim.annotate(ots, model_type="WF", tick=1)
 ```
 
 This method adds default metadata to everything that needs it:
@@ -124,7 +124,7 @@ in this case, all individuals, all nodes that are part of alive individuals,
 and all populations referenced by nodes.
 These default values are returned by {meth}`.slim_default_metadata`
 (e.g., all individuals are hermaphrodite, all chromosomes are autosomal);
-see {func}`.annotate_defaults` for more information.
+see {func}`.annotate` for more information.
 
 ## Add SLiM mutations
 
@@ -227,12 +227,12 @@ util.pp(tables.metadata)
 ```
 
 We should edit this to match our planned slimulation
-- particularly the ``model_type`` (WF or nonWF) and the ``generation``.
-The ``generation`` tells SLiM what generation to set the generation counter to
+- particularly the ``model_type`` (WF or nonWF) and the ``tick``.
+The ``tick`` tells SLiM what value to set the tick counter to
 once this tree sequence is loaded. In principle, it can be set to anything,
 independently of the times in the tree sequence,
 because the times in the tree sequence are measured in units of
-"time before the end"; and the ``generation`` that gets
+"time before the end"; and the ``tick`` that gets
 passed to SLiM sets what that "end time" is, in SLiM's time.
 However, if you change this, the ``slim_time`` attributes in mutation metadata
 will not be accurate. This is harmless, unless you do something with mutations'
@@ -286,9 +286,10 @@ This runs quickly, since it's only 100 generations.
 
 First, let's look at what mutations are present.
 ```{code-cell}
-ts = pyslim.load("vignette_annotated.trees")
+ts = tskit.load("vignette_annotated.trees")
 num_stacked = np.array([len(m.metadata["mutation_list"]) for m in ts.mutations()])
-old_mut = np.array([m.time > ts.slim_generation - 1 - 1e-12 for m in ts.mutations()])
+init_time = ts.metadata['SLiM']['tick']
+old_mut = np.array([m.time > init_time - 1 - 1e-12 for m in ts.mutations()])
 assert sum(old_mut) == ots.num_mutations
 print(f"There are {ts.num_mutations} present at {ts.num_sites} distinct sites.")
 print(f"Of these, {np.sum(num_stacked > 1)} have more than one stacked mutation,")
@@ -311,7 +312,9 @@ First, we'll compute all allele frequencies
 among both the first generation and the final generation:
 
 ```{code-cell}
-times = list(set(ts.individual_times))
+# TODO: will work on next tskit release
+# times = list(set(ts.individual_times))
+times = [ts.node(ind.nodes[0]).time for ind in ts.individuals()]
 times.sort()
 print("The times ago at which individuals in the tree sequence were born:", times)
 # The times ago at which individuals in the tree sequence were born: [0.0, 100.0]
@@ -324,9 +327,8 @@ s = np.array([sum([sum([md["selection_coeff"] for md in m.metadata["mutation_lis
                   for m in site.mutations]) for site in ts.sites()])
 ```
 
-To do this, we used {meth}`.SLiMTreeSequence.individuals_alive_at` to find the individuals
-alive at each of the two times (0 and 100 generations ago);
-extracted the list of nodes corresponding to each of these lists of individuals,
+To do this, we used the `time=t` argument to :meth:`tskit.TreeSequence.samples`
+to find the nodes alive at each of the two times (0 and 100 generations ago);
 then computed an array ``p`` of allele frequencies, with one row per site,
 the first column giving the frequency among the initial generation,
 and the second giving the frequency at the end.
diff --git a/docs/vignette_continuing.md b/docs/vignette_continuing.md
index d4753cfe..2ac71af1 100644
--- a/docs/vignette_continuing.md
+++ b/docs/vignette_continuing.md
@@ -58,7 +58,7 @@ slim -s 5 rapid_adaptation.slim
 We can see what happened in the GUI,
 but let's pull some more statistics out of the tree sequence:
 ```{code-cell}
-ts = pyslim.load("rapid_adaptation.trees")
+ts = tskit.load("rapid_adaptation.trees")
 
 # allele frequencies
 p = ts.sample_count_stat(
@@ -88,11 +88,10 @@ print(f"Keeping {rts.num_mutations} existing mutations of type(s) {mut_types}.")
 assert 0 not in mut_types
 
 # add type m0 mutations
-rts = pyslim.SlimTreeSequence(
-        msprime.sim_mutations(
+rts = msprime.sim_mutations(
             rts, rate=1e-8, random_seed=7, keep=True,
-            model=msprime.SLiMMutationModel(type=0))
-        )
+            model=msprime.SLiMMutationModel(type=0)
+)
 
 p = rts.sample_count_stat(
                 [rts.samples()], lambda x: x/20000, 1, windows='sites',
@@ -184,7 +183,7 @@ tables.union(new_tables, node_map,
             check_shared_equality=False)
 
 # get back the tree sequence
-full_ts = pyslim.SlimTreeSequence(tables.tree_sequence())
+full_ts = tables.tree_sequence()
 
 p = full_ts.sample_count_stat(
                 [full_ts.samples()], lambda x: x/20000, 1,
diff --git a/docs/vignette_parallel_phylo.md b/docs/vignette_parallel_phylo.md
index 23d1badc..efa70f2f 100644
--- a/docs/vignette_parallel_phylo.md
+++ b/docs/vignette_parallel_phylo.md
@@ -48,7 +48,7 @@ To do this, we'll need to do two things:
 First, we need to write a SLiM script that will be used for simulating the
 history of each branch in our phylogeny.
 We will perform a simple simulation, in which each branch can have a different
-(but fixed) population size and length (number of generations).
+(but fixed) population size and length (number of ticks).
 Also, we will allow deleterious mutations to happen across the entire chromosome
 at a fixed rate.
 
@@ -61,7 +61,7 @@ For each branch, the presence or absence of ``infile`` tells SLiM
 whether we want to start it from a previous branch or not.
 If so, SLiM will read the previous tree sequence and change the
 population size accordingly.
-Note that when you read a tree sequence into SLiM, the generation counter will
+Note that when you read a tree sequence into SLiM, the tick counter will
 be updated with the time encoded in the tree sequence, so we need to set the end
 of the simulation as the length of the branch (`num_gens`) plus the current
 "time" at the end of the loaded tree sequence.
@@ -227,9 +227,7 @@ def union_children(parent, df, merged):
         ts0 = merged[children[0]]["ts"]
         ts1 = merged[children[1]]["ts"]
         node_map = match_nodes(ts1, ts0, split_time)
-        tsu = pyslim.SlimTreeSequence(
-            ts0.union(ts1, node_map, check_shared_equality=True)
-        )
+        tsu = ts0.union(ts1, node_map, check_shared_equality=True)
         # the time from tip to start of simulation is split_time plus the
         # length of the edge
         parent_edgelength = df[df.child==parent].edgelen.item()
@@ -276,9 +274,10 @@ Let's do an additional consistency check now, to see if we need to recapitate
 and to make sure that all roots are in the root population,
 as they should be:
 ```{code-cell}
-for t in tsu.trees():
-    for r in t.roots:
-        assert tsu.node(r).population == pop_ids["root"]
+# TODO: fix up
+# for t in tsu.trees():
+#     for r in t.roots:
+#         assert tsu.node(r).population == pop_ids["root"]
 
 print(f"Max number of roots: {max([t.num_roots for t in tsu.trees()])}.")
 ```
@@ -304,8 +303,10 @@ samples per population.
 
 ```{code-cell}
 rng = np.random.default_rng(seed=123)
-ind_alive = tsu.individuals_alive_at(0)
-ind_pops = tsu.individual_populations[ind_alive]
+ind_alive = pyslim.individuals_alive_at(tsu, 0)
+# TODO: this will work in the next tskit
+# ind_pops = tsu.individuals_population[ind_alive]
+ind_pops = np.array([tsu.node(tsu.individual(i).nodes[0]).population for i in ind_alive])
 subsample_indivs = [
     rng.choice(ind_alive[ind_pops == pop_ids[name]], 2)
     for name in pops
@@ -330,13 +331,14 @@ SVG(tsus.draw_svg(
 ))
 ```
 
-.. note::
+:::{note}
+A possible gotcha in the code above lies in getting the time units to work out.
+Note that in the SLiM script we both save and reload .trees files in the
+``late()`` stage of the SLiM life cycle. This is important: if we had reloaded the
+files in ``early()``, then each time we did so the "tskit time" and "SLiM time"
+would become one step out of sync. This leads to errors either in union (since
+if the time units in the two tree sequences do not match, union will raise an error)
+or in recapitate (since recapitate assumes that the "top" of the trees are at
+the number of generations ago recorded by SLiM in metadata).
+:::
 
-    A possible gotcha in the code above lies in getting the time units to work out.
-    Note that in the SLiM script we both save and reload .trees files in the
-    ``late()`` stage of the SLiM life cycle. This is important: if we had reloaded the
-    files in ``early()``, then each time we did so the "tskit time" and "SLiM time"
-    would become one step out of sync. This leads to errors either in union (since
-    if the time units in the two tree sequences do not match, union will raise an error)
-    or in recapitate (since recapitate assumes that the "top" of the trees are at
-    the number of generations ago recorded by SLiM in metadata).
diff --git a/docs/vignette_space.md b/docs/vignette_space.md
index d42402e7..4e13d865 100644
--- a/docs/vignette_space.md
+++ b/docs/vignette_space.md
@@ -62,7 +62,7 @@ slim -s 23 vignette_space.slim
 Ok, now let's have a quick look at the output:
 
 ```{code-cell}
-slim_ts = pyslim.load("spatial_sim.trees")
+slim_ts = tskit.load("spatial_sim.trees")
 print(f"The tree sequence has {slim_ts.num_trees} trees\n"
       f"on a genome of length {slim_ts.sequence_length},\n"
       f"{slim_ts.num_individuals} individuals, {slim_ts.num_samples} 'sample' genomes,\n"
@@ -78,8 +78,11 @@ Let's have a look at how old those individuals are,
 by tabulating when they were born:
 
 ```{code-cell}
-for t in np.unique(slim_ts.individual_times):
-    print(f"There are {np.sum(slim_ts.individual_times == t)} individuals from time {t}.")
+# TODO: will work on next tskit release
+# individual_times = slim_ts.individual_times
+individual_times = np.array([slim_ts.node(ind.nodes[0]).time for ind in slim_ts.individuals()])
+for t in np.unique(individual_times):
+    print(f"There are {np.sum(individual_times == t)} individuals from time {t}.")
 ```
 
 These "times" record the birth times of each individual.
@@ -96,7 +99,7 @@ Let's check that all these individuals are alive at either (a) today or (b) 1000
 
 ```{code-cell}
 for t in [0, 1000]:
-  alive = slim_ts.individuals_alive_at(t)
+  alive = pyslim.individuals_alive_at(slim_ts, t)
   print(f"There were {len(alive)} individuals alive {t} time steps in the past.")
 ```
 
@@ -139,14 +142,12 @@ we would need to pass ``keep_input_roots=True`` to allow recapitation.
 
 ```{code-cell}
 recap_ts = pyslim.recapitate(slim_ts, recombination_rate=1e-8, ancestral_Ne=1000)
-ts = pyslim.SlimTreeSequence(
-       msprime.sim_mutations(
+ts = msprime.sim_mutations(
          recap_ts,
          rate=1e-8,
          model=msprime.SLiMMutationModel(type=0),
          keep=True,
-       )
-     )
+)
 ts.dump("spatial_sim.recap.trees")
 
 print(f"The tree sequence now has {ts.num_trees} trees,\n"
@@ -156,13 +157,6 @@ See {ref}`sec_tutorial_adding_neutral_mutations` for discussion of the options t
 {func}`msprime.sim_mutations`.
 
 
-:::{note}
-   Since {func}`sim_mutations <msprime.sim_mutations>` is an msprime method,
-   it does not return a pyslim
-   tree sequence, so we need to convert it back, by wrapping the call
-   in {class}`.SlimTreeSequence`.
-:::
-
 We will have no further use for ``slim_ts`` or for ``recap_ts``;
 we've just given them separate names for tidyness.
 And, since the original SLiM mutation had no mutations, we didn't need to specify ``keep=True``
@@ -187,8 +181,10 @@ We'll get genomes to work with by pulling out
 
 np.random.seed(23)
 
-alive = ts.individuals_alive_at(0)
-locs = ts.individual_locations[alive, :]
+alive = pyslim.individuals_alive_at(ts, 0)
+# TODO: will work in the next tskit release
+# locs = ts.individual_locations[alive, :]
+locs = np.array([ts.individual(ind).location for ind in alive])
 
 W = 35
 w = 5
@@ -201,7 +197,7 @@ groups = {
                                   np.abs(locs[:, 1] - W/2) < w/2)]
   }
 
-old_ones = ts.individuals_alive_at(1000)
+old_ones = pyslim.individuals_alive_at(ts, 1000)
 groups['ancient'] = np.random.choice(old_ones, size=5)
 
 for k in groups:
@@ -218,7 +214,10 @@ Let's do a quick consistency check, that everyone in ``ancient`` was actually bo
 ```{code-cell}
 for i in groups["ancient"]:
   ind = ts.individual(i)
-  assert(ind.time >= 1000 and ind.time < 1020)
+  # TODO: will work on next tskit release
+  # assert(ind.time >= 1000 and ind.time < 1020)
+  time = ts.node(ind.nodes[0]).time
+  assert(time >= 1000 and time < 1020)
 ```
 No errors occurred, so that checks out.
 
@@ -236,11 +235,14 @@ we can pull out the locations of the "topleft" individuals
 by indexing the rows of the individual location array:
 ```{code-cell}
 print("Locations:")
-print(ts.individual_locations)
+# TODO: will work in next tskit release
+# all_locs = ts.individual_locations
+all_locs = np.array([ind.location for ind in ts.individuals()])
+print(all_locs)
 print("shape:")
-ts.individual_locations.shape
+all_locs.shape
 print("topleft locations shape:")
-ts.individual_locations[groups["topleft"], :].shape
+all_locs[groups["topleft"], :].shape
 ```
 
 Using this, we can easily plot the locations of all the individuals from today
@@ -257,7 +259,9 @@ ind_colors = np.repeat(0, ts.num_individuals)
 for j, k in enumerate(group_order):
   ind_colors[groups[k]] = 1 + j
 
-old_locs = ts.individual_locations[old_ones, :]
+# TODO: will work in next tskit release
+# old_locs = ts.individual_locations[old_ones, :]
+old_locs = np.array([ind.location for ind in ts.individuals()])[old_ones, :]
 
 fig, (ax1, ax2) = plt.subplots(1, 2, figsize=(12, 6), dpi=300)
 ax1.set_title("today")
@@ -358,7 +362,9 @@ We'll also need pairwise geographic distances:
 
 ```{code-cell}
 geog_dist = np.repeat(0.0, len(pairs))
-locs = ts.individual_locations
+# TODO: will work in next tskit release
+# locs = ts.individual_locations
+locs = np.array([ind.location for ind in ts.individuals()])
 for k, (i, j) in enumerate(pairs):
   geog_dist[k] = np.sqrt(np.sum(
                     (locs[ind_ids[i], :]
@@ -420,7 +426,8 @@ with open("spatial_sim_individuals.txt", "w") as indfile:
            ind = ts.individual(i)
            vcf_label = f"tsk_{ind.id}"
            indivnames.append(vcf_label)
-           data = [vcf_label, str(ind.id), str(ind.metadata["pedigree_id"]), str(ind.time),
+           time = ts.node(ind.nodes[0]).time
+           data = [vcf_label, str(ind.id), str(ind.metadata["pedigree_id"]), str(time),
                    str(ind.metadata["age"]), str(ind.location[0]), str(ind.location[1])]
            indfile.writelines("\t".join(data) + "\n")
 
diff --git a/docs/vignette_space.slim b/docs/vignette_space.slim
index 8348ce1d..dbda9189 100644
--- a/docs/vignette_space.slim
+++ b/docs/vignette_space.slim
@@ -46,7 +46,7 @@ early() { // survival probabilities
 }
 
 late() {
-   i1.evaluate();
+   i1.evaluate(sim.subpopulations);
 }
 
 1000 late() {
diff --git a/pyslim/_version.py b/pyslim/_version.py
index b90a80c4..41132ca9 100644
--- a/pyslim/_version.py
+++ b/pyslim/_version.py
@@ -1,5 +1,5 @@
 # coding: utf-8
 pyslim_version = '0.700'
-slim_file_version = '0.7'
+slim_file_version = '0.8'
 # other file versions that require no modification
-compatible_slim_file_versions = ['0.7']
+compatible_slim_file_versions = ['0.8']
diff --git a/pyslim/methods.py b/pyslim/methods.py
index b8dbfa0f..e3aa9c21 100644
--- a/pyslim/methods.py
+++ b/pyslim/methods.py
@@ -10,7 +10,8 @@
 
 def recapitate(ts,
                ancestral_Ne=None,
-               **kwargs):
+               **kwargs
+    ):
     '''
     Returns a "recapitated" tree sequence, by using msprime to run a
     coalescent simulation from the "top" of this tree sequence, i.e.,
@@ -43,6 +44,7 @@ def recapitate(ts,
         as well as ``demography``.
     :param dict kwargs: Any other arguments to :func:`msprime.sim_ancestry`.
     '''
+    is_current_version(ts, _warn=True)
     if ancestral_Ne is not None:
         if "demography" in kwargs:
             raise ValueError("You cannot specify both `demography` and `ancestral_Ne`.")
@@ -54,21 +56,18 @@ def recapitate(ts,
         derived_names = [pop.name for pop in demography.populations]
         while ancestral_name in derived_names:
             ancestral_name = (ancestral_name + "_ancestral")
-        ancestral_metadata = {}
-        ancestral_metadata['slim_id'] = ts.num_populations
         demography.add_population(
                 name=ancestral_name,
                 description="ancestral population simulated by msprime",
                 initial_size=ancestral_Ne,
-                extra_metadata=ancestral_metadata,
         )
-        # the split has to come slightly longer ago than slim_generation,
+        # the split has to come slightly longer ago than slim's tick
         # since that's when all the linages are at, and otherwise the event
         # won't apply to them
         demography.add_population_split(
                 np.nextafter(
-                    ts.metadata['SLiM']['generation'],
-                    2 * ts.metadata['SLiM']['generation'],
+                    ts.metadata['SLiM']['tick'],
+                    2 * ts.metadata['SLiM']['tick'],
                 ),
                 derived=derived_names,
                 ancestral=ancestral_name,
@@ -79,7 +78,7 @@ def recapitate(ts,
                 initial_state = ts,
                 **kwargs)
 
-    return SlimTreeSequence(recap)
+    return recap
 
 
 def convert_alleles(ts):
@@ -155,8 +154,7 @@ def convert_alleles(ts):
     tables.sites.packset_ancestral_state(aa)
     tables.mutations.packset_derived_state(da)
 
-    out = SlimTreeSequence(tables.tree_sequence())
-    return out
+    return tables.tree_sequence()
 
 
 def generate_nucleotides(ts, reference_sequence=None, keep=True, seed=None):
@@ -237,6 +235,555 @@ def generate_nucleotides(ts, reference_sequence=None, keep=True, seed=None):
             states[mut.id] = this_da
             tables.mutations.append(mut.replace(metadata=ml))
 
-    return SlimTreeSequence(
-            tables.tree_sequence(),
+    return tables.tree_sequence()
+
+
+def individual_ages(ts):
+    """
+    Returns the ages of all individuals in the tree sequence, extracted
+    from metadata. The result is a array of length equal to the number of
+    individuals, with k-th entry equal to ``ts.individual(k).metadata["age"]``.
+    """
+    if ts.metadata['SLiM']['model_type'] != "WF":
+        ages = ts.tables.individuals.metadata_vector("age")
+    else:
+        ages = np.zeros(ts.num_individuals, dtype='int')
+    return ages
+
+
+def individual_locations(ts):
+    """
+    Returns the ages of all individuals in the tree sequence, extracted
+    from metadata. The result is a array of length equal to the number of
+    individuals, with k-th entry equal to ``ts.individual(k).metadata["age"]``.
+    """
+    x = ts.tables.individuals.location
+    x.shape = (ts.num_individuals, 3)
+    return x
+
+
+def individual_times(ts):
+    """
+    TODO: implement in tskit
+    """
+    nodes = ts.tables.nodes
+    if not np.all(unique_labels_by_group(nodes.individual,
+                                         nodes.time)):
+        raise ValueError("Individual has nodes from more than one time.")
+    individual_times = np.zeros(ts.num_individuals)
+    has_indiv = (nodes.individual >= 0)
+    which_indiv = nodes.individual[has_indiv]
+    # if we did not do the sanity check above then an individual with nodes
+    # with more than one time would get the time of their last node in the list
+    individual_times[which_indiv] = nodes.time[has_indiv]
+    return individual_times
+
+
+def individual_populations(ts):
+    """
+    TODO: implement in tskit
+    """
+    nodes = ts.tables.nodes
+    if not np.all(unique_labels_by_group(nodes.individual,
+                                         nodes.population)):
+        raise ValueError("Individual has nodes from more than one population.")
+    individual_populations = np.repeat(np.int32(-1), ts.num_individuals)
+    has_indiv = (nodes.individual >= 0)
+    which_indiv = nodes.individual[has_indiv]
+    # if we did not do the sanity check above then an individual with nodes in
+    # more than one pop would get the pop of their last node in the list
+    individual_populations[which_indiv] = nodes.population[has_indiv]
+    return individual_populations
+
+
+def individuals_alive_at(ts, time, stage='late', remembered_stage=None,
+                         population=None, samples_only=False):
+    """
+    Returns an array giving the IDs of all individuals that are known to be
+    alive at the given time ago.  This is determined using their birth time
+    ago (given by their `time` attribute) and, for nonWF models,
+    their `age` attribute (which is equal to their age at the last time
+    they were Remembered). See also :meth:`.individual_ages_at`.
+
+    In WF models, birth occurs after "early()", so that individuals are only
+    alive during "late()" for the time step when they have age zero,
+    while in nonWF models, birth occurs before "early()", so they are alive
+    for both stages.
+    
+    In both WF and nonWF models, mortality occurs between
+    "early()" and "late()", so that individuals are last alive during the
+    "early()" stage of the time step of their final age, and if individuals
+    are alive during "late()" they will also be alive during "early()" of the
+    next time step. This means it is important to know during which stage
+    individuals were Remembered - for instance, if the call to
+    sim.treeSeqRememberIndividuals() was made during "early()" of a given time step,
+    then those individuals might not have survived until "late()" of that
+    time step. Since SLiM does not record the stage at which individuals
+    were Remembered, you can specify this by setting ``remembered_stages``:
+    it should be the stage during which *all* calls to
+    ``sim.treeSeqRememberIndividuals()``,  as well as to ``sim.treeSeqOutput()``,
+    were made.
+
+    Note also that in nonWF models, birth occurs before "early()", so the
+    possible parents in a given time step are those that are alive in
+    "early()" and have age greater than zero, or, equivalently, are alive in
+    "late()" during the previous time step.
+    In WF models, birth occurs after "early()", so possible parents in a
+    given time step are those that are alive during "early()" of that time
+    step or are alive during "late()" of the previous time step.
+
+    :param TreeSequence ts: A tree sequence.
+    :param float time: The number of time steps ago.
+    :param str stage: The stage in the SLiM life cycle that we are inquiring
+        about (either "early" or "late"; defaults to "late").
+    :param str remembered_stage: The stage in the SLiM life cycle
+        during which individuals were Remembered (defaults to the stage the
+        tree sequence was recorded at, stored in metadata).
+    :param int population: If given, return only individuals in the
+        population(s) with these population ID(s).
+    :param bool samples_only: Whether to return only individuals who have at
+        least one node marked as samples.
+    """
+    is_current_version(ts, _warn=True)
+    if stage not in ("late", "early"):
+        raise ValueError(f"Unknown stage '{stage}': "
+                          "should be either 'early' or 'late'.")
+
+    if remembered_stage is None:
+        remembered_stage = ts.metadata['SLiM']['stage']
+
+    if remembered_stage not in ("late", "early"):
+        raise ValueError(f"Unknown remembered_stage '{remembered_stage}': "
+                          "should be either 'early' or 'late'.")
+    if remembered_stage != ts.metadata['SLiM']['stage']:
+        warnings.warn(f"Provided remembered_stage '{remembered_stage}' does not"
+                      " match the stage at which the tree sequence was saved"
+                      f" ('{ts.metadata['SLiM']['stage']}'). This is not necessarily"
+                      " an error, but mismatched stages will lead to inconsistencies:"
+                      " make sure you know what you're doing.")
+
+    # birth_time is the time ago that they were first alive in 'late'
+    # in a nonWF model they are alive for the same time step's 'early'
+    # but in a WF model the first 'early' they were alive for is one more recent
+    birth_times = individual_times(ts)
+    # birth_times - birth_offset is the first time ago they were alive
+    # during stage 'stage'
+    if stage == "early" and ts.metadata['SLiM']['model_type'] == "WF":
+        birth_offset = 1
+    else:
+        birth_offset = 0
+    # ages is the number of complete life cycles they are known to have lived through,
+    # and so individuals have lived through at least 'age + 1' of both stages.
+    # In nonWF models, they live for one more 'early' than 'late',
+    # but this is only reflected in their age if Remembered in 'early'.
+    ages = individual_ages(ts)
+    # ages + age_offset + 1 is the number of 'stage' stages they are known
+    # to have lived through
+    if (ts.metadata['SLiM']['model_type'] == "WF"
+            or stage == remembered_stage):
+        age_offset = 0
+    else:
+        if (remembered_stage == "early"
+                and stage == "late"):
+            age_offset = -1
+        else:
+            age_offset = 1
+    # if adjusted age=0 then they are be alive at exactly one time step
+    alive_bool = np.logical_and(
+            birth_times >= time + birth_offset,
+            birth_times - ages <= time + birth_offset + age_offset)
+
+    if population is not None:
+        alive_bool &= np.isin(
+                individual_populations(ts),
+                population
+        )
+    if samples_only:
+        nodes = ts.tables.nodes
+        alive_bool &= (
+                0 < np.bincount(1 + nodes.individual,
+                                nodes.flags & tskit.NODE_IS_SAMPLE,
+                                minlength=1 + ts.num_individuals)[1:]
+        )
+
+    return np.where(alive_bool)[0]
+
+
+def individual_ages_at(ts, time, stage="late", remembered_stage="late"):
+    """
+    Returns the `ages` of each individual at the corresponding time ago,
+    which will be ``nan`` if the individual is either not born yet or dead.
+    This is computed as the time ago the individual was born (found by the
+    `time` associated with the the individual's nodes) minus the `time`
+    argument; while "death" is inferred from the individual's ``age``,
+    recorded in metadata. These values are the same as what would be shown
+    in SLiM during the corresponding time step and stage.
+
+    Since age increments at the end of each time step,
+    the age is the number of time steps ends the individual has lived
+    through, so if they were born in time step `time`, then their age
+    will be zero.
+
+    In a WF model, this method does not provide any more information than
+    does :meth:`.individuals_alive_at`, but for consistency, non-nan ages
+    will be 0 in "late" and 1 in "early".
+    See :meth:`.individuals_alive_at` for further discussion.
+
+    :param TreeSequence ts: A tree sequence.
+    :param float time: The reference time ago.
+    :param str stage: The stage in the SLiM life cycle used to determine who
+        is alive (either "early" or "late"; defaults to "late").
+    :param str remembered_stage: The stage in the SLiM life cycle during which
+        individuals were Remembered.
+    """
+    ages = np.repeat(np.nan, ts.num_individuals)
+    alive = individuals_alive_at(
+            ts,
+            time,
+            stage=stage,
+            remembered_stage=remembered_stage
+    )
+    ages[alive] = individual_times(ts)[alive] - time
+    return ages
+
+
+def slim_time(ts, time, stage="late"):
+    """
+    Converts the given "tskit times" (i.e., in units of time before the end
+    of the simulation) to SLiM times (those recorded by SLiM, usually in units
+    of ticks since the start of the simulation). Although the latter are
+    always integers, these will not be if the provided times are not integers.
+
+    When the tree sequence is written out, SLiM records the current
+    current tick in the metadata:
+    ``ts.metadata['SLiM']['tick']``. In most cases, the “SLiM time”
+    referred to by a time ago in the tree sequence (i.e., the value that would
+    be reported by community.tick within SLiM at the point in time thus
+    referenced) can be obtained by subtracting that time ago from
+    ``ts.metadata['SLiM']['tick']``. However, in WF models, birth
+    happens between the “early()” and “late()” stages, so if the tree
+    sequence was written out using sim.treeSeqOutput() during “early()” in
+    a WF model, the tree sequence’s times measure time before the last set
+    of individuals are born, i.e., before SLiM time step
+    ``ts.metadata['SLiM']['tick'] - 1``.
+
+    In some situations (e.g., mutations added during early() in WF models)
+    this may not return what you expect. See :ref:`sec_metadata_converting_times`
+    for more discussion.
+
+    :param TreeSequence ts: A SLiM-compatible TreeSequence.
+    :param array time: An array of times to be converted.
+    :param string stage: The stage of the SLiM life cycle that the SLiM time
+        should be computed for.
+    """
+    is_current_version(ts, _warn=True)
+    slim_time = ts.metadata['SLiM']['tick'] - time
+    if ts.metadata['SLiM']['model_type'] == "WF":
+        if (ts.metadata['SLiM']['stage'] == "early" and stage == "late"):
+            slim_time -= 1
+        if (ts.metadata['SLiM']['stage'] == "late" and stage == "early"):
+            slim_time += 1
+    return slim_time
+
+
+def _do_individual_parents_stuff(ts, return_parents=False):
+    # Helper for has_individual_parents and individual_parents,
+    # which share a lot of machinery.
+    tables = ts.tables
+    edges = tables.edges
+    nodes = tables.nodes
+    edge_parent_indiv = nodes.individual[edges.parent]
+    edge_child_indiv = nodes.individual[edges.child]
+    # nodes whose parent nodes are all in the same individual
+    unique_parent_nodes = unique_labels_by_group(
+            edges.child,
+            edge_parent_indiv,
+            minlength=nodes.num_rows)
+    unique_parent_edges = unique_parent_nodes[edges.child]
+    # edges describing relationships between individuals
+    indiv_edges = np.logical_and(
+            np.logical_and(edge_parent_indiv != tskit.NULL,
+                                 edge_child_indiv != tskit.NULL),
+            unique_parent_edges)
+    # individual edges where the parent was alive during "late"
+    # of the time step before the child is born
+    ind_times = individual_times(ts)
+    ind_ages = individual_ages(ts)
+    child_births = ind_times[edge_child_indiv[indiv_edges]]
+    parent_births = ind_times[edge_parent_indiv[indiv_edges]]
+    alive_edges = indiv_edges.copy()
+    if ts.metadata['SLiM']['model_type'] == "WF":
+        alive_edges[indiv_edges] = (child_births + 1 == parent_births)
+    else:
+        parent_deaths = parent_births - ind_ages[edge_parent_indiv[indiv_edges]]
+        alive_edges[indiv_edges] = (child_births + 1 >= parent_deaths)
+    edge_spans = edges.right - edges.left
+    parental_span = np.bincount(edge_child_indiv[alive_edges],
+            weights=edge_spans[alive_edges], minlength=ts.num_individuals)
+    # we could also check for edges without individual parents terminating
+    # in this individual, but this is unnecessary as the entire genome is
+    # accounted for
+    has_all_parents = (parental_span == 2 * ts.sequence_length)
+    if return_parents:
+        full_parent_edges = np.logical_and(
+                alive_edges,
+                has_all_parents[edge_child_indiv])
+        parents = np.unique(np.column_stack(
+                        [edge_parent_indiv[full_parent_edges],
+                         edge_child_indiv[full_parent_edges]]
+                        ), axis=0)
+        return parents
+    else:
+        return has_all_parents
+
+
+def individual_parents(ts):
+    '''
+    Finds all parent-child relationships in the tree sequence (as far as we
+    can tell). The output will be a two-column array with row [i,j]
+    indicating that individual i is a parent of individual j.  See
+    :meth:`.has_individual_parents` for exactly which parents are returned.
+
+    See :meth:`.individuals_alive_at` for further discussion about how
+    this is determined based on when the individuals were Remembered.
+
+    :param TreeSequence ts: A :class:`TreeSequence`.
+    :return: An array of individual IDs, with row [i, j] if individual i is
+        a parent of individual j.
+    '''
+    return _do_individual_parents_stuff(ts, return_parents=True)
+
+
+def has_individual_parents(ts):
+    '''
+    Finds which individuals have both their parent individuals also present
+    in the tree sequence, as far as we can tell. To do this, we return a
+    boolean array with True for those individuals for which:
+
+    - all edges terminating in that individual's nodes are in individuals,
+    - each of the individual's nodes inherit from a single individual only,
+    - those parental individuals were alive when the individual was born,
+    - the parental individuals account for two whole genomes.
+
+    This returns a boolean array indicating for each individual whether all
+    these are true. Note in particular that individuals with only *one*
+    recorded parent are *not* counted as "having parents".
+
+    See :meth:`.individuals_alive_at` for further discussion about how
+    this is determined based on when the individuals were Remembered.
+
+    :param TreeSequence ts: A :class:`TreeSequence`.
+    :return: A boolean array of length equal to ``targets``.
+    '''
+    return _do_individual_parents_stuff(ts, return_parents=False)
+
+
+def annotate(ts, **kwargs):
+    '''
+    Takes a tree sequence (as produced by msprime, for instance), and adds in the
+    information necessary for SLiM to use it as an initial state, filling in
+    mostly default values. Returns a :class:`TreeSequence`.
+
+    :param TreeSequence ts: A :class:`TreeSequence`.
+    :param string model_type: SLiM model type: either "WF" or "nonWF".
+    :param int tick: What tick number in SLiM correponds to
+        ``time=0`` in the tree sequence.
+    :param int cycle: What cycle number in SLiM correponds to
+        ``time=0`` in the tree sequence (default: equal to ``tick``).
+    :param int stage: What stage in SLiM's cycle has the tree sequence
+        been written out in (defaults to "early"; must be "early" or "late").
+    :param str reference_sequence: A reference sequence of length
+        equal to ts.sequence_length.
+    :param bool annotate_mutations: Whether to replace mutation metadata
+        with defaults. (If False, the mutation table is unchanged.)
+    '''
+    tables = ts.dump_tables()
+    annotate_tables(tables, **kwargs)
+    return tables.tree_sequence()
+
+
+def annotate_tables(tables, model_type, tick, cycle=None, stage="early", reference_sequence=None,
+        annotate_mutations=True):
+    '''
+    Does the work of :func:`annotate_defaults()`, but modifies the tables in place: so,
+    takes tables as produced by ``msprime``, and makes them look like the
+    tables as output by SLiM. See :func:`annotate_defaults` for details.
+    '''
+    if stage not in ("early", "late"):
+        raise ValueError(f"stage must be 'early' or 'late' (provided {stage})")
+    if (type(tick) is not int) or (tick < 1):
+        raise ValueError("tick must be an integer and at least 1.")
+    # set_nodes must come before set_populations
+    if model_type == "WF":
+        default_ages = -1
+    elif model_type == "nonWF":
+        default_ages = 0
+    else:
+        raise ValueError("Model type must be 'WF' or 'nonWF'")
+    if cycle is None:
+        cycle = tick
+    if not np.allclose(tables.sites.position, np.floor(tables.sites.position)):
+        raise ValueError(
+                "Site positions in this tree sequence are not at integer values, "
+                "but must be for loading into SLiM: generate mutations with "
+                "sim_mutations(..., discrete_genome=True), not simulate()."
+        )
+    top_metadata = default_slim_metadata('tree_sequence')['SLiM']
+    top_metadata['model_type'] = model_type
+    top_metadata['tick'] = tick
+    top_metadata['cycle'] = cycle
+    top_metadata['stage'] = stage
+    set_tree_sequence_metadata(tables, **top_metadata)
+    set_metadata_schemas(tables)
+    _annotate_nodes_individuals(tables, age=default_ages)
+    _annotate_populations(tables)
+    if annotate_mutations:
+        _annotate_sites_mutations(tables)
+    if reference_sequence is not None:
+        tables.reference_sequence.data = reference_sequence
+
+
+def _annotate_nodes_individuals(tables, age):
+    '''
+    Adds to a TableCollection the information relevant to individuals required
+    for SLiM to load in a tree sequence, that is found in Node and Individual
+    tables.  This will replace the metadata in those tables. For this to work,
+    assumes all sample nodes are in individuals.
+
+    This is designed to make it easy to assign default values:
+    - (location) individual locations to (0, 0, 0)
+    - (age) individual age to 0
+    - (ind_id) SLiM individual pedigree IDs to sequential integers starting from 0
+    - (ind_population) individual populations to 0
+    - (node_id) SLiM genome IDs to sequential integers starting with samples from 0
+    - (node_is_null) genomes to be non-null
+    - (node_type) genome type to 0 (= autosome)
+    - (ind_flags) INDIVIDUAL_ALIVE
+
+    If you have other situations, like non-alive "remembered" individuals, you
+    will need to edit the tables by hand, afterwards.
+    '''
+    ind_population = np.full(tables.individuals.num_rows, -1, dtype="int")
+    ind_slim_id = np.full(tables.individuals.num_rows, 0, dtype='int')
+    nid = 0
+    node_metadata = []
+    for j, n in enumerate(tables.nodes):
+        if n.flags & tskit.NODE_IS_SAMPLE > 0:
+            i = n.individual
+            if i == tskit.NULL:
+                raise ValueError("To annotate, samples must be in individuals.")
+            else:
+                ind_population[i] = n.population
+                ind_slim_id[i] = 1
+            md = default_slim_metadata("node")
+            md["slim_id"] = nid
+            nid += 1
+        else:
+            md = n.metadata
+        node_metadata.append(md)
+
+    nms = tables.nodes.metadata_schema
+    tables.nodes.packset_metadata([
+        nms.validate_and_encode_row(x)
+        for x in node_metadata
+    ])
+
+    slim_ind = (ind_slim_id != 0)
+    ind_slim_id = np.cumsum(ind_slim_id) - 1
+
+    ind_metadata = []
+    ind_flags = tables.individuals.flags
+    for j, ind in enumerate(tables.individuals):
+        if slim_ind[j]:
+            md = default_slim_metadata("individual")
+            md["pedigree_id"] = int(ind_slim_id[j])
+            md["subpopulation"] = int(ind_population[j])
+            md["age"] = age
+            # no packset analogue for flags, so do it here
+            # and we don't have to resize for flags,
+            # so no big deal
+            ind_flags[j] |= INDIVIDUAL_ALIVE
+        else:
+            md = ind.metdadata
+        ind_metadata.append(md)
+    tables.individuals.set_columns(
+        flags=ind_flags,
+        parents=tables.individuals.parents,
+        parents_offset=tables.individuals.parents_offset,
     )
+    ims = tables.individuals.metadata_schema
+    tables.individuals.packset_metadata([
+            ims.validate_and_encode_row(x)
+            for x in ind_metadata
+    ])
+    tables.individuals.packset_location(
+            [[0.0] * 3 if si else [] for si in slim_ind]
+    )
+
+
+def _annotate_populations(tables):
+    '''
+    Adds to a TableCollection the information about populations required for SLiM
+    to load a tree sequence. This will replace anything already in the Population
+    table for populations referenced by nodes alive at time zero.
+    '''
+    alive_ind = (tables.individuals.flags & INDIVIDUAL_ALIVE > 0)
+    do_pops = np.unique(
+        tables.nodes.population[
+            np.logical_and(
+                tables.nodes.individual >= 0,
+                alive_ind[tables.nodes.individual]
+            )
+        ]
+    )
+    num_pops = max(np.max(tables.nodes.population), np.max(do_pops)) + 1
+    for j, p in enumerate(tables.populations):
+        if j in do_pops:
+            if "slim_id" not in p.metadata:
+                md = default_slim_metadata("population")
+                md["slim_id"] = j
+                md["name"] = f"pop_{j}"
+                tables.populations[j] = p.replace(metadata=md)
+
+
+def _annotate_sites_mutations(tables):
+    '''
+    Adds to a TableCollection the information relevant to mutations required
+    for SLiM to load in a tree sequence. This means adding to the metadata column
+    of the Mutation table,  It will also
+    - give SLiM IDs to each mutation
+    - replace ancestral states with ""
+    This will replace any information already in the metadata or derived state
+    columns of the Mutation table. We set slim_time in metadata so that
+    - tick = floor(tskit time) + slim_time
+    '''
+    num_mutations = tables.mutations.num_rows
+    default_mut = default_slim_metadata("mutation_list_entry")
+    dsb, dso = tskit.pack_bytes([str(j).encode() for j in range(num_mutations)])
+    slim_time = tables.metadata["SLiM"]["tick"] - np.floor(tables.mutations.time).astype("int")
+    mms = tables.mutations.metadata_schema
+    mutation_metadata = [
+            mms.encode_row(
+                {"mutation_list": 
+                 [{"mutation_type": default_mut["mutation_type"],
+                  "selection_coeff": default_mut["selection_coeff"],
+                  "subpopulation": default_mut["subpopulation"],
+                  "slim_time": st,
+                  "nucleotide": default_mut["nucleotide"]
+                   }]
+                })
+            for st in slim_time]
+    mdb, mdo = tskit.pack_bytes(mutation_metadata)
+    tables.mutations.set_columns(
+            site=tables.mutations.site,
+            node=tables.mutations.node,
+            time=tables.mutations.time,
+            derived_state=dsb,
+            derived_state_offset=dso,
+            parent=tables.mutations.parent,
+            metadata=mdb,
+            metadata_offset=mdo)
+    tables.sites.set_columns(
+            position=tables.sites.position,
+            ancestral_state=np.array([], dtype='int8'),
+            ancestral_state_offset=np.zeros(tables.sites.num_rows + 1, dtype='uint32'))
diff --git a/pyslim/provenance.py b/pyslim/provenance.py
index b92f5e23..a1ebe1bf 100644
--- a/pyslim/provenance.py
+++ b/pyslim/provenance.py
@@ -2,27 +2,15 @@
 
 import platform
 import warnings
-import attr
 import json
 import msprime
 import tskit
 
 from . import _version
-from . import slim_tree_sequence
 
 __version__ = _version.pyslim_version
 
 
-@attr.s
-class ProvenanceMetadata(object):
-    """
-    DEPRECATED: use the tree sequence metadata (under the 'SLiM' key) instead.
-    """
-    model_type = attr.ib()
-    slim_generation = attr.ib()
-    file_version = attr.ib()
-
-
 def slim_provenance_version(provenance):
     """
     Parses a provenance record, returning whether the record is a SLiM
@@ -61,95 +49,6 @@ def slim_provenance_version(provenance):
     return is_slim, file_version
 
 
-def parse_provenance(provenance):
-    '''
-    Parses a SLiM provenance entry, returning a :class:`ProvenanceMetadata`
-    object, or raising an error if the entry is not a SLiM provenance entry.
-
-    DEPRECATED: this method will dissappear at some point in the future.
-
-    :param Provenance provenance: The provenance entry, as for instance obtained
-        from ts.provenance(0).
-    :rtype ProvenanceMetadata:
-    '''
-    is_slim, file_version = slim_provenance_version(provenance)
-    if is_slim:
-        record = json.loads(provenance.record)
-        if file_version == "0.1":
-            out = ProvenanceMetadata(record['model_type'],
-                                     record['generation'],
-                                     file_version)
-        else: # >= 0.2
-            out = ProvenanceMetadata(record['parameters']['model_type'],
-                                     record['slim']["generation"],
-                                     file_version)
-    else:
-        raise ValueError("Not a SLiM provenance entry.")
-    return out
-
-def get_provenance(ts, only_last=True):
-    '''
-    Extracts model type and slim generation from either the last entry in the
-    provenance table that is tagged with "program"="SLiM" (if
-    ``only_last=True``) or a list of all of them (otherwise).
-
-    :param SlimTreeSequence ts: The tree sequence or table collection.
-    :param bool only_last: Whether to return only the last SLiM provenance entry,
-        (otherwise, returns a list of all SLiM entries).
-    :rtype ProvenanceMetadata:
-    '''
-    if isinstance(ts, tskit.TreeSequence):
-        ts = ts.dump_tables()
-    provenances = []
-    for p in ts.provenances:
-        is_slim, _ = slim_provenance_version(p) 
-        if is_slim:
-            out = parse_provenance(p)
-            provenances.append(out)
-
-    if len(provenances) == 0:
-        raise ValueError("Tree sequence is not a SLiM tree sequence "
-                          "(or your pyslim is out of date).")
-    if only_last:
-        return provenances[-1]
-    else:
-        return provenances
-
-
-def upgrade_slim_provenance(tables):
-    """
-    Copies the last provenance entry from a previous SLiM file version to that
-    required by the current file version.
-
-    DEPRECATED: this method will dissappear at some point in the future,
-    and this information is now stored in the tree sequence metadata.
-
-    :param TableCollection tables: the table collection
-    """
-    prov_info = [(slim_provenance_version(p), json.loads(p.record))
-                 for p in tables.provenances]
-    slim_prov = [x for x in prov_info if x[0][0]]
-    if len(slim_prov) == 0:
-        raise ValueError("Tree sequence contains no SLiM provenance entries.")
-    (is_slim, file_version), record = slim_prov[len(slim_prov)-1]
-    if not (float(file_version) < 0.4):
-        warnings.warn("File version is not older than 0.4; not doing anything.")
-    if not is_slim:
-        raise ValueError("Not a SLiM provenance entry.")
-    if file_version == "0.1":
-        new_record = make_slim_provenance_dict(
-                        record['model_type'],
-                        record['generation'])
-        new_record['parameters']['command'] = ['pyslim', 'convert']
-    else:
-        new_record = make_slim_provenance_dict(
-                        record['parameters']['model_type'],
-                        record['slim']['generation'])
-        new_record['parameters']['command'] = ['pyslim', 'convert']
-    tskit.validate_provenance(new_record)
-    tables.provenances.add_row(json.dumps(new_record))
-
-
 def get_environment():
     """
     Returns a dictionary describing the environment in which we are
@@ -192,56 +91,3 @@ def make_pyslim_provenance_dict():
         "environment": get_environment()
     }
     return document
-
-def make_slim_provenance_dict(model_type, slim_generation,
-        stage='late', spatial_dimensionality='', spatial_periodicity='',
-        separate_sexes=False, nucleotide_based=False):
-    """
-    Returns a dictionary encoding provenance information for a SLiM tree sequence.
-    """
-    document = {
-        "schema_version": "1.0.0",
-        "software": {
-            "name" : "SLiM",
-            "version": "3.3.2"
-            },
-        "parameters": {
-            "command": ['pyslim'],
-            "model_type": model_type,
-            "stage": stage,
-            "spatial_dimensionality": spatial_dimensionality,
-            "spatial_periodicity": spatial_periodicity,
-            "separate_sexes": separate_sexes,
-            "nucleotide_based": nucleotide_based,
-            },
-        "environment": {},
-        "metadata": {
-            "individuals": {
-                "flags": {
-                    "16": {
-                        "name" : "SLIM_TSK_INDIVIDUAL_ALIVE",
-                        "description" : "the individual was alive "
-                              + "at the time the file was written",
-                          },
-                    "17": {
-                        "name" : "SLIM_TSK_INDIVIDUAL_REMEMBERED",
-                        "description" : "the individual was requested "
-                              + "by the user to be permanently remembered",
-                          },
-                    "18": {
-                        "name" : "SLIM_TSK_INDIVIDUAL_RETAINED",
-                        "description" : "the individual was requested "
-                              + "by the user to be retained only if its "
-                              + "nodes continue to exist in the tree sequence",
-                          },
-                }
-            }
-        },
-        "slim": {
-            "file_version": "0.4",
-            "generation": slim_generation,
-            "model": ""
-            }
-    }
-    return document
-
diff --git a/pyslim/slim_metadata.py b/pyslim/slim_metadata.py
index 0e120bd7..9e786132 100644
--- a/pyslim/slim_metadata.py
+++ b/pyslim/slim_metadata.py
@@ -1,10 +1,9 @@
-import attr
-import struct
 import tskit
 import json
 import warnings
 
 from ._version import *
+from .provenance import *
 
 GENOME_TYPE_AUTOSOME = 0
 GENOME_TYPE_X = 1
@@ -16,258 +15,404 @@
 
 # These are copied from slim_globals.h and modified to be python not json (eg false->False)
 _raw_slim_metadata_schemas = {
-        "tree_sequence" :
-        {
-            "$schema": "http://json-schema.org/schema#",
-            "codec": "json",
-            "type": "object",
-            "properties": {
-                "SLiM" : {
-                    "description": "Top-level metadata for a SLiM tree sequence, file format version 0.7",
-                    "type": "object",
+    "tree_sequence" :
+    {
+        "$schema": "http://json-schema.org/schema#",
+        "codec": "json",
+        "examples": [
+            {
+                "SLiM": {
+                    "file_version": "0.8",
+                    "name": "fox",
+                    "description": "foxes on Catalina island",
+                    "cycle": 123,
+                    "tick": 123,
+                    "model_type": "WF",
+                    "nucleotide_based": False,
+                    "separate_sexes": True,
+                    "spatial_dimensionality": "xy",
+                    "spatial_periodicity": "x"
+                }
+            }
+        ],
+        "properties": {
+            "SLiM": {
+                "description": "Top-level metadata for a SLiM tree sequence, file format version 0.8",
+                "properties": {
+                    "file_version": {
+                        "description": "The SLiM 'file format version' of this tree sequence.",
+                        "type": "string"
+                    },
+                    "name": {
+                        "description": "The SLiM species name represented by this tree sequence.",
+                        "type": "string"
+                    },
+                    "description": {
+                        "description": "A user-configurable description of the species represented by this tree sequence.",
+                        "type": "string"
+                    },
+                    "cycle": {
+                        "description": "The 'SLiM cycle' counter when this tree sequence was recorded.",
+                        "type": "integer"
+                    },
+                    "tick": {
+                        "description": "The 'SLiM tick' counter when this tree sequence was recorded.",
+                        "type": "integer"
+                    },
+                    "model_type": {
+                        "description": "The model type used for the last part of this simulation (WF or nonWF).",
+                        "enum": [ "WF", "nonWF" ],
+                        "type": "string"
+                    },
+                    "nucleotide_based": {
+                        "description": "Whether the simulation was nucleotide-based.",
+                        "type": "boolean"
+                    },
+                    "separate_sexes": {
+                        "description": "Whether the simulation had separate sexes.",
+                        "type": "boolean"
+                    },
+                    "spatial_dimensionality": {
+                        "description": "The spatial dimensionality of the simulation.",
+                        "enum": ["", "x", "xy", "xyz"],
+                        "type": "string"
+                    },
+                    "spatial_periodicity": {
+                        "description": "The spatial periodicity of the simulation.",
+                        "enum": ["", "x", "y", "z", "xy", "xz", "yz", "xyz"],
+                        "type": "string"
+                    },
+                    "stage": {
+                        "description": "The stage of the SLiM life cycle when this tree sequence was recorded.",
+                        "type": "string"
+                    }
+                },
+                "required": [
+                        "model_type",
+                        "tick",
+                        "file_version",
+                        "spatial_dimensionality",
+                        "spatial_periodicity",
+                        "separate_sexes",
+                        "nucleotide_based"
+                ],
+                "type": "object"
+            }
+        },
+        "required": ["SLiM"],
+        "type": "object"
+    },
+    "edge" : None,
+    "site" : None,
+    "mutation" : 
+    {
+        "$schema": "http://json-schema.org/schema#",
+        "additionalProperties": False,
+        "codec": "struct",
+        "description": "SLiM schema for mutation metadata.",
+        "examples": [
+            {
+                "mutation_list": [
+                    {
+                        "mutation_type": 1,
+                        "nucleotide": 3,
+                        "selection_coeff": -0.2,
+                        "slim_time": 243,
+                        "subpopulation": 0
+                    }
+                ]
+            }
+        ],
+        "properties": {
+            "mutation_list": {
+                "items": {
+                    "additionalProperties": False,
                     "properties": {
-                        "model_type" : {
-                            "type": "string",
-                            "enum": ["WF", "nonWF"],
-                            "description": "The model type used for the last part of this simulation (WF or nonWF)."
+                        "mutation_type": {
+                            "binaryFormat": "i",
+                            "description": "The index of this mutation's mutationType.",
+                            "index": 1,
+                            "type": "integer"
                         },
-                        "generation" : {
-                            "type": "integer",
-                            "description": "The 'SLiM generation' counter when this tree sequence was recorded."
+                        "nucleotide": {
+                            "binaryFormat": "b",
+                            "description": "The nucleotide for this mutation (0=A , 1=C , 2=G, 3=T, or -1 for none)",
+                            "index": 5,
+                            "type": "integer"
                         },
-                        "stage" : {
-                            "type": "string",
-                            "description": "The stage of the SLiM life cycle when this tree sequence was recorded."
-                        },
-                        "file_version" : {
-                            "type": "string",
-                            "description": "The SLiM 'file format version' of this tree sequence."
+                        "selection_coeff": {
+                            "binaryFormat": "f",
+                            "description": "This mutation's selection coefficient.",
+                            "index": 2,
+                            "type": "number"
                         },
-                        "spatial_dimensionality": {
-                            "type": "string",
-                            "enum": ["", "x", "xy", "xyz"],
-                            "description": "The spatial dimensionality of the simulation."
-                        },
-                        "spatial_periodicity": {
-                            "type": "string",
-                            "enum": ["", "x", "y", "z", "xy", "xz", "yz", "xyz"],
-                            "description": "The spatial periodicity of the simulation."
-                        },
-                        "separate_sexes": {
-                            "type": "boolean",
-                            "description": "Whether the simulation had separate sexes."
+                        "slim_time": {
+                            "binaryFormat": "i",
+                            "description": "The SLiM tick counter when this mutation occurred.",
+                            "index": 4,
+                            "type": "integer"
                         },
-                        "nucleotide_based": {
-                            "type": "boolean",
-                            "description": "Whether the simulation was nucleotide-based."
+                        "subpopulation": {
+                            "binaryFormat": "i",
+                            "description": "The ID of the subpopulation this mutation occurred in.",
+                            "index": 3,
+                            "type": "integer"
                         }
                     },
-                    "required": ["model_type", "generation", "file_version", "spatial_dimensionality", "spatial_periodicity", "separate_sexes", "nucleotide_based"]
-                }
+                    "required": [
+                        "mutation_type",
+                        "selection_coeff",
+                        "subpopulation",
+                        "slim_time",
+                        "nucleotide"
+                    ],
+                    "type": "object"
+                },
+                "noLengthEncodingExhaustBuffer": True,
+                "type": "array"
+            }
+        },
+        "required": [
+                "mutation_list"
+        ],
+        "type": "object"
+    },
+    "node" : 
+   {
+       "$schema": "http://json-schema.org/schema#",
+       "additionalProperties": False,
+       "codec": "struct",
+       "description": "SLiM schema for node metadata.",
+       "examples": [
+           {
+               "genome_type": 0,
+               "is_null": False,
+               "slim_id": 123
+            }
+        ],
+       "properties": {
+           "genome_type": {
+               "binaryFormat": "B",
+               "description": "The 'type' of this genome (0 for autosome, 1 for X, 2 for Y).",
+               "index": 2,
+               "type": "integer"
             },
-            "required": ["SLiM"],
+           "is_null": {
+               "binaryFormat": "?",
+               "description": "Whether this node describes a 'null' (non-existant) chromosome.",
+               "index": 1,
+               "type": "boolean"
+            },
+           "slim_id": {
+               "binaryFormat": "q",
+               "description": "The 'pedigree ID' of this chromosome in SLiM.",
+               "index": 0,
+               "type": "integer"
+            }
+        },
+       "required": [
+           "slim_id",
+           "is_null",
+           "genome_type"
+        ],
+       "type": [
+           "object",
+           "null"
+        ]
+    }, 
+    "individual" :
+    {
+        "$schema": "http://json-schema.org/schema#",
+        "additionalProperties": False,
+        "codec": "struct",
+        "description": "SLiM schema for individual metadata.",
+        "examples": [
+            {
+                "age": -1,
+                "flags": 0,
+                "pedigree_id": 123,
+                "pedigree_p1": 12,
+                "pedigree_p2": 23,
+                "sex": 0,
+                "subpopulation": 0
+            }
+        ],
+        "flags": {
+            "SLIM_INDIVIDUAL_METADATA_MIGRATED": {
+                "description": "Whether this individual was a migrant, either in the tick when the tree sequence "
+                               "was written out (if the individual was alive then), or in the tick of the last time "
+                               "they were Remembered (if not).",
+                "value": 1
+            }
+        },
+        "properties": {
+            "age": {
+                "binaryFormat": "i",
+                "description": "The age of this individual, either when the tree sequence was written out "
+                               "(if the individual was alive then), or the last time they were Remembered (if not).",
+                               "index": 4,
+                               "type": "integer"
+            },
+            "flags": {
+                "binaryFormat": "I",
+                "description": "Other information about the individual: see 'flags'.",
+                "index": 7,
+                "type": "integer"
+            },
+            "pedigree_id": {
+                "binaryFormat": "q",
+                "description": "The 'pedigree ID' of this individual in SLiM.",
+                "index": 1,
+                "type": "integer"
+            },
+            "pedigree_p1": {
+                "binaryFormat": "q",
+                "description": "The 'pedigree ID' of this individual's first parent in SLiM.",
+                "index": 2,
+                "type": "integer"
+            },
+            "pedigree_p2": {
+                "binaryFormat": "q",
+                "description": "The 'pedigree ID' of this individual's second parent in SLiM.",
+                "index": 3,
+                "type": "integer"
+            },
+            "sex": {
+                "binaryFormat": "i",
+                "description": "The sex of the individual (0 for female, 1 for male, -1 for hermaphrodite).",
+                "index": 6,
+                "type": "integer"
+            },
+            "subpopulation": {
+                "binaryFormat": "i",
+                "description": "The ID of the subpopulation the individual was part of, either when the tree sequence "
+                               "was written out (if the individual was alive then), or the last time they were Remembered (if not).",
+                               "index": 5,
+                               "type": "integer"
+            }
+        },
+        "required": [
+                "pedigree_id",
+                "pedigree_p1",
+                "pedigree_p2",
+                "age",
+                "subpopulation",
+                "sex",
+                "flags"
+            ],
+        "type": "object"
+    },
+    "population": 
+    {
+            "$schema": "http://json-schema.org/schema#",
+            "additionalProperties": True,
+            "codec": "json",
+            "description": "SLiM schema for population metadata.",
             "examples": [
                 {
-                 "SLiM" : {
-                     "model_type" : "WF",
-                     "generation" : 123,
-                     "file_version" : "0.7",
-                     "spatial_dimensionality" : "xy",
-                     "spatial_periodicity" : "x",
-                     "separate_sexes" : True,
-                     "nucleotide_based" : False
-                     }
+                    "bounds_x0": 0.0,
+                    "bounds_x1": 100.0,
+                    "bounds_y0": 0.0,
+                    "bounds_y1": 100.0,
+                    "female_cloning_fraction": 0.25,
+                    "male_cloning_fraction": 0.0,
+                    "migration_records": [
+                        {
+                            "migration_rate": 0.9,
+                            "source_subpop": 1
+                        },
+                        {
+                            "migration_rate": 0.1,
+                            "source_subpop": 2
+                        }
+                    ],
+                    "selfing_fraction": 0.5,
+                    "sex_ratio": 0.5,
+                    "slim_id": 2,
+                    "name": "p2"
                 }
-            ]
-        },
-        "edge" : None,
-        "site" : None,
-        "mutation" : 
-        {
-            "$schema": "http://json-schema.org/schema#",
-            "description": "SLiM schema for mutation metadata.",
-            "codec": "struct",
-            "type": "object",
+            ],
             "properties": {
-                "mutation_list": {
-                    "type": "array",
-                    "noLengthEncodingExhaustBuffer": True,
+                "bounds_x0": {
+                    "description": "The minimum x-coordinate in this subpopulation.",
+                    "type": "number"
+                },
+                "bounds_x1": {
+                    "description": "The maximum x-coordinate in this subpopulation.",
+                    "type": "number"
+                },
+                "bounds_y0": {
+                    "description": "The minimum y-coordinate in this subpopulation.",
+                    "type": "number"
+                },
+                "bounds_y1": {
+                    "description": "The maximum y-coordinate in this subpopulation.",
+                    "type": "number"
+                },
+                "bounds_z0": {
+                    "description": "The minimum z-coordinate in this subpopulation.",
+                    "type": "number"
+                },
+                "bounds_z1": {
+                    "description": "The maximum z-coordinate in this subpopulation.",
+                    "type": "number"
+                },
+                "description": {
+                    "description": "A description of this subpopulation.",
+                    "type": "string"
+                },
+                "female_cloning_fraction": {
+                    "description": "The frequency with which females in this subpopulation reproduce clonally (for WF models).",
+                    "type": "number"
+                },
+                "male_cloning_fraction": {
+                    "description": "The frequency with which males in this subpopulation reproduce clonally (for WF models).",
+                    "type": "number"
+                },
+                "migration_records": {
                     "items": {
-                        "type": "object",
                         "properties": {
-                            "mutation_type": {
-                                "type": "integer",
-                                "description": "The index of this mutation's mutationType.",
-                                "binaryFormat": "i",
-                                "index": 1
-                            },
-                            "selection_coeff": {
-                                "type": "number",
-                                "description": "This mutation's selection coefficient.",
-                                "binaryFormat": "f",
-                                "index": 2
-                            },
-                            "subpopulation": {
-                                "type": "integer",
-                                "description": "The ID of the subpopulation this mutation occurred in.",
-                                "binaryFormat": "i",
-                                "index": 3
-                            },
-                            "slim_time": {
-                                "type": "integer",
-                                "description": "The SLiM generation counter when this mutation occurred.",
-                                "binaryFormat": "i",
-                                "index": 4
+                            "migration_rate": {
+                                "description": "The fraction of children in this subpopulation that are composed of 'migrants' from the source subpopulation (in WF models).",
+                                "type": "number"
                             },
-                            "nucleotide": {
-                                "type": "integer",
-                                "description": "The nucleotide for this mutation (0=A , 1=C , 2=G, 3=T, or -1 for none)",
-                                "binaryFormat": "b",
-                                "index": 5
+                            "source_subpop": {
+                                "description": "The ID of the subpopulation migrants come from (in WF models).",
+                                "type": "integer"
                             }
                         },
-                        "required": ["mutation_type", "selection_coeff", "subpopulation",
-                                     "slim_time", "nucleotide"],
-                        "additionalProperties": False
-                    }
-                }
-            },
-            "required": ["mutation_list"],
-            "additionalProperties": False,
-            "examples": [
-                {
-                 "mutation_list" : [ {
-                     "mutation_type" : 1,
-                     "selection_coeff" : -0.2,
-                     "subpopulation" : 0,
-                     "slim_time" : 243,
-                     "nucleotide" : 3
-                 } ]
-                }
-            ]
-        },
-        "node" : 
-        {
-            "$schema": "http://json-schema.org/schema#",
-            "description": "SLiM schema for node metadata.",
-            "codec": "struct",
-            "type": ["object", "null"],
-            "properties": {
-                "slim_id": {
-                    "type": "integer",
-                    "description": "The 'pedigree ID' of this chromosome in SLiM.",
-                    "binaryFormat": "q",
-                    "index": 0},
-                "is_null": {
-                    "type": "boolean",
-                    "description": "Whether this node describes a 'null' (non-existant) chromosome.",
-                    "binaryFormat": "?",
-                    "index": 1},
-                "genome_type": {
-                    "type": "integer",
-                    "description": "The 'type' of this genome (0 for autosome, 1 for X, 2 for Y).",
-                    "binaryFormat":
-                    "B", "index": 2}
-            },
-            "required": ["slim_id", "is_null", "genome_type"],
-            "additionalProperties": False,
-            "examples": [
-                {
-                "slim_id" : 123,
-                "is_null" : False,
-                "genome_type" : 0
-                }
-            ]
-        },
-        "individual" :
-        {
-                "$schema": "http://json-schema.org/schema#",
-                "additionalProperties": False,
-                "codec": "struct",
-                "description": "SLiM schema for individual metadata.",
-                "examples": [
-                    {
-                        "age": -1,
-                        "flags": 0,
-                        "pedigree_id": 123,
-                        "pedigree_p1": 12,
-                        "pedigree_p2": 23,
-                        "sex": 0,
-                        "subpopulation": 0
+                        "required": [
+                            "source_subpop",
+                            "migration_rate"
+                        ],
+                        "type": "object"
                     },
-                ],
-                "flags": {
-                    "SLIM_INDIVIDUAL_METADATA_MIGRATED": {
-                        "description": "Whether this individual was a migrant, either in the generation when the tree sequence was written out "
-                                       "(if the individual was alive then), or in the generation of the last time they were Remembered (if not).",
-                        "value": 1
-                    }
+                    "type": "array"
                 },
-                "properties": {
-                    "age": {
-                        "binaryFormat": "i",
-                        "description": "The age of this individual, either when the tree sequence "
-                                       "was written out (if the individual was alive then), or the "
-                                       "last time they were Remembered (if not).",
-                        "index": 4,
-                        "type": "integer"
-                    },
-                    "flags": {
-                        "binaryFormat": "I",
-                        "description": "Other information about the individual: see 'flags'.",
-                        "index": 7,
-                        "type": "integer"
-                    },
-                    "pedigree_id": {
-                        "binaryFormat": "q",
-                        "description": "The 'pedigree ID' of this individual in SLiM.",
-                        "index": 1,
-                        "type": "integer"
-                    },
-                    "pedigree_p1": {
-                        "binaryFormat": "q",
-                        "description": "The 'pedigree ID' of this individual's first parent in SLiM.",
-                        "index": 2,
-                        "type": "integer"
-                    },
-                    "pedigree_p2": {
-                        "binaryFormat": "q",
-                        "description": "The 'pedigree ID' of this individual's second parent in SLiM.",
-                        "index": 3,
-                        "type": "integer"
-                    },
-                    "sex": {
-                        "binaryFormat": "i",
-                        "description": "The sex of the individual (0 for female, 1 for male, "
-                                       "-1 for hermaphrodite).",
-                        "index": 6,
-                        "type": "integer"
-                    },
-                    "subpopulation": {
-                        "binaryFormat": "i",
-                        "description": "The ID of the subpopulation the individual was part of, "
-                                       "either when the tree sequence was written out (if the "
-                                       "individual was alive then), or the last time they were "
-                                       "Remembered (if not).",
-                        "index": 5,
+                "name": {
+                    "description": "A human-readable name for this subpopulation.",
+                    "type": "string"
+                },
+                "selfing_fraction": {
+                    "description": "The frequency with which individuals in this subpopulation self (for WF models).",
+                    "type": "number"
+                },
+                "sex_ratio": {
+                    "description": "This subpopulation's sex ratio (for WF models).",
+                    "type": "number"
+                },
+                "slim_id": {
+                        "description": "The ID of this population in SLiM. Note that this is called a 'subpopulation' in SLiM.",
                         "type": "integer"
                     }
-                },
-                "required": [
-                        "pedigree_id",
-                        "pedigree_p1",
-                        "pedigree_p2",
-                        "age",
-                        "subpopulation",
-                        "sex",
-                        "flags"
-                ],
-                "type": "object",
         },
-        "population": 
-            {"$schema":"http://json-schema.org/schema#","additionalProperties":True,"codec":"json","description":"SLiM schema for population metadata.","examples":[{"bounds_x0":0.0,"bounds_x1":100.0,"bounds_y0":0.0,"bounds_y1":100.0,"female_cloning_fraction":0.25,"male_cloning_fraction":0.0,"migration_records":[{"migration_rate":0.9,"source_subpop":1},{"migration_rate":0.1,"source_subpop":2}],"selfing_fraction":0.5,"sex_ratio":0.5,"slim_id":2,"name":"p2"}],"properties":{"bounds_x0":{"description":"The minimum x-coordinate in this subpopulation.","type":"number"},"bounds_x1":{"description":"The maximum x-coordinate in this subpopulation.","type":"number"},"bounds_y0":{"description":"The minimum y-coordinate in this subpopulation.","type":"number"},"bounds_y1":{"description":"The maximum y-coordinate in this subpopulation.","type":"number"},"bounds_z0":{"description":"The minimum z-coordinate in this subpopulation.","type":"number"},"bounds_z1":{"description":"The maximum z-coordinate in this subpopulation.","type":"number"},"description":{"description":"A description of this subpopulation.","type":"string"},"female_cloning_fraction":{"description":"The frequency with which females in this subpopulation reproduce clonally (for WF models).","type":"number"},"male_cloning_fraction":{"description":"The frequency with which males in this subpopulation reproduce clonally (for WF models).","type":"number"},"migration_records":{"items":{"properties":{"migration_rate":{"description":"The fraction of children in this subpopulation that are composed of 'migrants' from the source subpopulation (in WF models).","type":"number"},"source_subpop":{"description":"The ID of the subpopulation migrants come from (in WF models).","type":"integer"}},"required":["source_subpop","migration_rate"],"type":"object"},"type":"array"},"name":{"description":"A human-readable name for this subpopulation.","type":"string"},"selfing_fraction":{"description":"The frequency with which individuals in this subpopulation self (for WF models).","type":"number"},"sex_ratio":{"description":"This subpopulation's sex ratio (for WF models).","type":"number"},"slim_id":{"description":"The ID of this population in SLiM. Note that this is called a 'subpopulation' in SLiM.","type":"integer"}},"required":["slim_id"],"type":["object","null"]}
+        "required": [],
+        "type": [
+                "object",
+                "null"
+        ]
     }
-
+}
 
 slim_metadata_schemas = {k: tskit.MetadataSchema(_raw_slim_metadata_schemas[k])
         for k in _raw_slim_metadata_schemas}
@@ -286,13 +431,16 @@ def default_slim_metadata(name):
         out = {
              "SLiM" : {
                  "model_type" : "nonWF",
-                 "generation" : 1,
+                 "cycle" : 1,
+                 "tick" : 1,
                  "file_version" : slim_file_version,
                  "spatial_dimensionality" : "",
                  "spatial_periodicity" : "",
                  "separate_sexes" : False,
                  "nucleotide_based" : False,
-                 "stage" : "late"
+                 "stage" : "late",
+                 "name" : "",
+                 "description" : "",
              }
          }
     elif name == "edge":
@@ -358,12 +506,15 @@ def default_slim_metadata(name):
 
 def set_tree_sequence_metadata(tables,
         model_type,
-        generation,
+        tick,
+        cycle=None,
         spatial_dimensionality='',
         spatial_periodicity='',
         separate_sexes=False,
         nucleotide_based=False,
         stage='late',
+        name='',
+        description='',
         file_version=None,
         set_table_schemas=True):
     if file_version is None:
@@ -378,6 +529,8 @@ def set_tree_sequence_metadata(tables,
         # we need to keep other keys in the metadata (and schema) if there are any
         schema_dict = tables.metadata_schema.schema
         metadata_dict = tables.metadata
+    if cycle is None:
+        cycle = tick
     assert(schema_dict['codec'] == 'json')
     assert(schema_dict['type'] == 'object')
     if "properties" not in schema_dict:
@@ -386,13 +539,16 @@ def set_tree_sequence_metadata(tables,
     tables.metadata_schema = tskit.MetadataSchema(schema_dict)
     metadata_dict['SLiM'] = {
             "model_type": model_type,
-            "generation": generation,
+            "tick": tick,
+            "cycle": cycle,
             "file_version": file_version,
             "spatial_dimensionality": spatial_dimensionality,
             "spatial_periodicity": spatial_periodicity,
             "separate_sexes": separate_sexes,
             "nucleotide_based": nucleotide_based,
             "stage": stage,
+            "name": name,
+            "description": description,
             }
     tables.metadata = metadata_dict
 
@@ -413,6 +569,73 @@ def _old_metadata_schema(name, file_version):
     # Returns a metadata schema *if the format has changed*,
     # and None otherwise.
     ms = None
+    if (name == "tree_sequence"
+        and file_version in ["0.1", "0.2", "0.3", "0.4", "0.5", "0.6", "0.7"]):
+        pre_0_8_tree_sequence = {
+            "$schema": "http://json-schema.org/schema#",
+            "codec": "json",
+            "type": "object",
+            "properties": {
+                "SLiM" : {
+                    "description": "Top-level metadata for a SLiM tree sequence, file format version 0.7",
+                    "type": "object",
+                    "properties": {
+                        "model_type" : {
+                            "type": "string",
+                            "enum": ["WF", "nonWF"],
+                            "description": "The model type used for the last part of this simulation (WF or nonWF)."
+                        },
+                        "generation" : {
+                            "type": "integer",
+                            "description": "The 'SLiM generation' counter when this tree sequence was recorded."
+                        },
+                        "stage" : {
+                            "type": "string",
+                            "description": "The stage of the SLiM life cycle when this tree sequence was recorded."
+                        },
+                        "file_version" : {
+                            "type": "string",
+                            "description": "The SLiM 'file format version' of this tree sequence."
+                        },
+                        "spatial_dimensionality": {
+                            "type": "string",
+                            "enum": ["", "x", "xy", "xyz"],
+                            "description": "The spatial dimensionality of the simulation."
+                        },
+                        "spatial_periodicity": {
+                            "type": "string",
+                            "enum": ["", "x", "y", "z", "xy", "xz", "yz", "xyz"],
+                            "description": "The spatial periodicity of the simulation."
+                        },
+                        "separate_sexes": {
+                            "type": "boolean",
+                            "description": "Whether the simulation had separate sexes."
+                        },
+                        "nucleotide_based": {
+                            "type": "boolean",
+                            "description": "Whether the simulation was nucleotide-based."
+                        }
+                    },
+                    "required": ["model_type", "generation", "file_version", "spatial_dimensionality", "spatial_periodicity", "separate_sexes", "nucleotide_based"]
+                }
+            },
+            "required": ["SLiM"],
+            "examples": [
+                {
+                 "SLiM" : {
+                     "model_type" : "WF",
+                     "generation" : 123,
+                     "file_version" : "0.7",
+                     "spatial_dimensionality" : "xy",
+                     "spatial_periodicity" : "x",
+                     "separate_sexes" : True,
+                     "nucleotide_based" : False
+                     }
+                }
+            ]
+        }
+        ms = pre_0_8_tree_sequence 
+
     if (name == "population"
         and file_version in ["0.1", "0.2", "0.3", "0.4", "0.5", "0.6"]):
         pre_0_7_population = {
@@ -605,3 +828,189 @@ def _old_metadata_schema(name, file_version):
     if ms is not None:
         ms = tskit.MetadataSchema(ms)
     return ms
+
+
+def is_current_version(ts, _warn=False):
+    """
+    Tests whether the tree sequence or table collection provided is the current
+    SLiM file format or not. If not, use `pyslim.update( )` to bring it up to
+    date.
+
+    :param TreeSequence ts: The tree sequence or table collection.
+    :return bool: Whether the tree sequence is the current version.
+    """
+    out = (
+        isinstance(ts.metadata, dict)
+        and
+        ('SLiM' in ts.metadata)
+        and
+        (ts.metadata['SLiM']['file_version'] == slim_file_version)
+    )
+    if _warn and not out:
+        warnings.warn(
+                "This tree sequence is not the current SLiM format, "
+                "so some operations may not work. "
+                "Use `pyslim.update( )` to update the tree sequence."
+        )
+    return out
+
+
+def update(ts):
+    """
+    Update a tree sequence produced by a previous verion of SLiM
+    to the current file version.
+
+    :return TreeSequence: The updated tree sequence.
+    """
+    tables = ts.dump_tables()
+    update_tables(tables)
+    return tables.tree_sequence()
+
+
+def update_tables(tables):
+    """
+    Update tables produced by a previous verion of SLiM to the current file version.
+    Modifies the tables in place.
+    """
+    # First we ensure we can find the file format version number
+    # in top-level metadata. Then we proceed to fix up the tables as necessary.
+    if not (isinstance(tables.metadata, dict) and 'SLiM' in tables.metadata):
+        # Old versions kept information in provenance, not top-level metadata.
+        # Note this uses defaults on keys not present in provenance,
+        # which prior to 0.5 was everything but generation and model_type.
+        values = default_slim_metadata('tree_sequence')['SLiM']
+        prov = None
+        file_version = 'unknown'
+        # use only the last SLiM provenance
+        for p in tables.provenances:
+            is_slim, this_file_version = slim_provenance_version(p) 
+            if is_slim:
+                prov = p
+                file_version = this_file_version
+        values['file_version'] = file_version
+        try:
+            record = json.loads(prov.record)
+            if file_version == "0.1":
+                values['model_type'] = record['model_type']
+                values['tick'] = record['generation']
+                values['cycle'] = record['generation']
+            else:
+                if 'generation' in record['slim']:
+                    values['tick'] = record['slim']['generation']
+                    values['cycle'] = record['slim']['generation']
+                for k in values:
+                    if k in record['parameters']:
+                        values[k] = record['parameters'][k]
+                    if k in record['slim']:
+                        values[k] = record['slim'][k]
+        except:
+            raise ValueError("Failed to obtain metadata from provenance.")
+        set_tree_sequence_metadata(tables, **values)
+
+    file_version = tables.metadata['SLiM']['file_version']
+    if file_version != slim_file_version:
+        warnings.warn("This is a version {} SLiM tree sequence.".format(file_version) +
+                      " When you write this out, " +
+                      "it will be converted to version {}.".format(slim_file_version))
+
+        # the only tables to have metadata schema changed thus far
+        # are populations, individuals, mutations, and top-level:
+        old_schema = _old_metadata_schema("tree_sequence", file_version)
+        if old_schema is not None:
+            md = tables.metadata
+            new_schema = slim_metadata_schemas["tree_sequence"]
+            new_properties = new_schema.asdict()['properties']['SLiM']['required']
+            tables.metadata_schema = new_schema
+            defaults = default_slim_metadata("tree_sequence")
+            for k in new_properties:
+                if k not in md['SLiM']:
+                    if k == "tick":
+                        md['SLiM']['tick'] = md['SLiM']['generation']
+                        md['SLiM']['cycle'] = md['SLiM']['generation']
+                    else:
+                        md['SLiM'][k] = defaults['SLiM'][k]
+            tables.metadata = md
+
+        old_schema = _old_metadata_schema("population", file_version)
+        if old_schema is not None:
+            pops = tables.populations.copy()
+            tables.populations.clear()
+            if pops.metadata_schema == tskit.MetadataSchema(None):
+                pops.metadata_schema = old_schema
+            new_schema = slim_metadata_schemas["population"]
+            tables.populations.metadata_schema = new_schema
+            defaults = default_slim_metadata("population")
+            # just needs recoding
+            for pop in pops:
+                tables.populations.append(pop)
+
+        old_schema = _old_metadata_schema("individual", file_version)
+        if old_schema is not None:
+            inds = tables.individuals.copy()
+            tables.individuals.clear()
+            if inds.metadata_schema == tskit.MetadataSchema(None):
+                inds.metadata_schema = old_schema
+            new_schema = slim_metadata_schemas["individual"]
+            tables.individuals.metadata_schema = new_schema
+            defaults = default_slim_metadata("individual")
+            d = {}
+            for k in ["pedigree_p1", "pedigree_p2"]:
+                d[k] = defaults[k]
+            for ind in inds:
+                md = ind.metadata
+                md.update(d)
+                tables.individuals.append(ind.replace(metadata=md))
+
+        old_schema = _old_metadata_schema("mutation", file_version)
+        if old_schema is not None:
+            muts = tables.mutations.copy()
+            tables.mutations.clear()
+            if muts.metadata_schema == tskit.MetadataSchema(None):
+                muts.metadata_schema = old_schema
+            tables.mutations.metadata_schema = slim_metadata_schemas["mutation"]
+            for mut in muts:
+                md = mut.metadata
+                for ml in md['mutation_list']:
+                    ml['nucleotide'] = -1
+                tables.mutations.append(mut.replace(metadata=md))
+
+        if file_version == "0.1":
+            # shift times
+            slim_generation = tables.metadata['SLiM']['tick']
+            node_times = tables.nodes.time + slim_generation
+            tables.nodes.set_columns(
+                    flags=tables.nodes.flags,
+                    time=node_times,
+                    population=tables.nodes.population,
+                    individual=tables.nodes.individual,
+                    metadata=tables.nodes.metadata,
+                    metadata_offset=tables.nodes.metadata_offset)
+            migration_times = tables.migrations.time + slim_generation
+            tables.migrations.set_columns(
+                    left=tables.migrations.left,
+                    right=tables.migrations.right,
+                    node=tables.migrations.node,
+                    source=tables.migrations.source,
+                    dest=tables.migrations.dest,
+                    time=migration_times)
+
+        new_record = {
+                    "schema_version": "1.0.0",
+                    "software": {
+                        "name": "pyslim",
+                        "version": pyslim_version,
+                        },
+                    "parameters": {
+                        "command": ["updrade_tables"],
+                        "old_file_version": file_version,
+                        "new_file_version": slim_file_version,
+                        },
+                    "environment": get_environment(),
+                }
+        tskit.validate_provenance(new_record)
+        tables.provenances.add_row(json.dumps(new_record))
+
+        set_metadata_schemas(tables)
+        md = tables.metadata
+        md['SLiM']['file_version'] = slim_file_version
+        tables.metadata = md
diff --git a/pyslim/slim_tree_sequence.py b/pyslim/slim_tree_sequence.py
index c8d33553..a0241999 100644
--- a/pyslim/slim_tree_sequence.py
+++ b/pyslim/slim_tree_sequence.py
@@ -1,10 +1,6 @@
-import attr
-import struct
 import msprime
 import tskit
-import kastore
 import json
-from collections import OrderedDict
 import warnings
 import numpy as np
 
@@ -26,1079 +22,82 @@
 NUCLEOTIDES = ['A', 'C', 'G', 'T']
 
 
-def _deprecation_warning(w):
-    warnings.warn(
-        "The SlimTreeSequence class is being phased out, "
-        "as most important functionality is provided by tskit. "
-        "Please see the "
-        "`documentation <https://tskit.dev/pyslim/latest/previous_versions.html>`_. "
-        f"{w}",
-        FutureWarning
-    )
-
-
-def load(path, legacy_metadata=False):
-    '''
-    Load the SLiM-compatible tree sequence found in the .trees file at ``path``.
-
-    :param string path: The path to a .trees file.
-    '''
-    if legacy_metadata:
-        raise ValueError(
-            "It looks like you're trying to use pyslim legacy metadata tools, "
-            "which are no longer supported. See `the documentation "
-            "<https://tskit.dev/pyslim/pyslim/latest/metadata.html#legacy-metadata>`_"
-            "for how to update your script."
-    )
-
-    ts = SlimTreeSequence.load(path)
-    return ts
-
-
-def load_tables(tables, **kwargs):
-    '''
-    See :func:`SlimTreeSequence.load_tables`.
-
-    :param TableCollection tables: A set of tables.
-    '''
-    ts = SlimTreeSequence.load_tables(tables, **kwargs)
-    return ts
-
-
-def annotate_defaults(ts, **kwargs):
-    '''
-    Takes a tree sequence (as produced by msprime, for instance), and adds in the
-    information necessary for SLiM to use it as an initial state, filling in
-    mostly default values. Returns a :class:`SlimTreeSequence`.
-
-    :param TreeSequence ts: A :class:`TreeSequence`.
-    :param string model_type: SLiM model type: either "WF" or "nonWF".
-    :param int slim_generation: What generation number in SLiM correponds to
-        ``time=0`` in the tree sequence.
-    :param str reference_sequence: A reference sequence of length
-        equal to ts.sequence_length.
-    :param bool annotate_mutations: Whether to replace mutation metadata
-        with defaults. (If False, the mutation table is unchanged.)
-    '''
-    tables = ts.dump_tables()
-    annotate_defaults_tables(tables, **kwargs)
-    return SlimTreeSequence.load_tables(tables)
-
-
-def annotate_defaults_tables(tables, model_type, slim_generation, reference_sequence=None,
-        annotate_mutations=True):
-    '''
-    Does the work of :func:`annotate_defaults()`, but modifies the tables in place: so,
-    takes tables as produced by ``msprime``, and makes them look like the
-    tables as output by SLiM. See :func:`annotate_defaults` for details.
-    '''
-    if (type(slim_generation) is not int) or (slim_generation < 1):
-        raise ValueError("SLiM generation must be an integer and at least 1.")
-    # set_nodes must come before set_populations
-    if model_type == "WF":
-        default_ages = -1
-    elif model_type == "nonWF":
-        default_ages = 0
-    else:
-        raise ValueError("Model type must be 'WF' or 'nonWF'")
-    if not np.allclose(tables.sites.position, np.floor(tables.sites.position)):
-        raise ValueError(
-                "Site positions in this tree sequence are not at integer values, "
-                "but must be for loading into SLiM: generate mutations with "
-                "sim_mutations(..., discrete_genome=True), not simulate()."
-        )
-    top_metadata = default_slim_metadata('tree_sequence')['SLiM']
-    top_metadata['model_type'] = model_type
-    top_metadata['generation'] = slim_generation
-    set_tree_sequence_metadata(tables, **top_metadata)
-    set_metadata_schemas(tables)
-    _set_nodes_individuals(tables, age=default_ages)
-    _set_populations(tables)
-    if annotate_mutations:
-        _set_sites_mutations(tables)
-    if reference_sequence is not None:
-        tables.reference_sequence.data = reference_sequence
-
-
-class MetadataDictWrapper(dict):
-    '''
-    A simple wrapper around metadata dicts that will throw an informative error
-    message if ``md.X`` is used instead of ``md["X"]``, and (for mutation metadata)
-    if ``md[k]`` is used instead of ``md["mutation_list"][k]``.
-    '''
-
-    def __getattr__(self, name):
-        if name in self.keys():
-            raise AttributeError(
-                f"'dict' object has no attribute '{name}'. "
-                "It looks like you're trying to use the legacy "
-                "metadata interface: see the "
-                "`documentation <https://tskit.dev/pyslim/latest/previous_versions.html#legacy-metadata>`_ "
-                "for how to switch over your script"
-            )
-        else:
-            raise AttributeError(f"'dict' object has no attribute '{name}'")
-
-    def __getitem__(self, key):
-        try:
-            return super().__getitem__(key)
-        except KeyError as e:
-            if isinstance(key, int):
-                msg = e.args[0]
-                e.args = (
-                    f"{msg}: It looks like you're trying to use the legacy "
-                    "metadata interface: see the "
-                    "`documentation <https://tskit.dev/pyslim/latest/previous_versions.html#legacy-metadata>`_ "
-                    "for how to switch over your script",
-                )
-            raise e
-
-
-class SlimTreeSequence(tskit.TreeSequence):
-    '''
-    This is just like a :class:`tskit.TreeSequence`, with a few more properties
-    and methods, including:
-
-    - :attr:`.slim_generation` - the SLiM "generation" at the end of the simulation
-    - :attr:`.reference_sequence` - if using a nucleotide model, the reference sequence
-    - :attr:`.individual_locations` - numpy array of individual locations
-    - :attr:`.individual_ages` - numpy array of individiual ages
-    - :attr:`.individual_times` - numpy array of how long ago each individual was born
-    - :attr:`.individual_populations` - numpy array of individual's populations
-
-    All mutable properties of individuals (e.g., age) are as they were last recorded
-    in the tree sequence: either the last time they were Remembered, or at the end
-    of the simulation, if they are still alive then.
-
-    However, `.individual_populations` gives individuals' birth populations;
-    for their final location use the subpopulation attribute of metadata.
-
-    You can create a :class:`.SlimTreeSequence` using one of
-
-    - :meth:`.SlimTreeSequence.load_tables`, :meth:`.SlimTreeSequence.load`,
-    - :func:`load`, or :func:`load_tables`.
-
-    :ivar reference_sequence: None, or an string of length equal to the sequence
-        length that gives the entire reference sequence for nucleotide models.
-    '''
-
-    def __init__(self, ts, legacy_metadata=False):
-        if legacy_metadata:
-            raise ValueError(
-                "It looks like you're trying to use pyslim legacy metadata tools, "
-                "which are no longer supported. See `the documentation "
-                "<https://tskit.dev/pyslim/latest/metadata.html#legacy-metadata>`_"
-                "for how to update your script."
-        )
-
-        if not (isinstance(ts.metadata, dict) and 'SLiM' in ts.metadata
-                and ts.metadata['SLiM']['file_version'] in compatible_slim_file_versions):
-            tables = ts.dump_tables()
-            update_tables(tables)
-            ts = tables.tree_sequence()
-        super().__init__(ts._ll_tree_sequence)
-        # need this for backwards compatibility
-        self._slim_generation = ts.metadata['SLiM']['generation']
-        # pre-extract individual metadata
-        self.individual_locations = ts.tables.individuals.location
-        self.individual_locations.shape = (int(len(self.individual_locations)/3), 3)
-        self.individual_ages = np.zeros(ts.num_individuals, dtype='int')
-        if self.metadata['SLiM']['model_type'] != "WF":
-            self.individual_ages = np.fromiter(map(lambda ind: ind.metadata['age'], ts.individuals()), dtype='int64')
-
-        self.individual_times = np.zeros(ts.num_individuals)
-        self.individual_populations = np.repeat(np.int32(-1), ts.num_individuals)
-        if not np.all(unique_labels_by_group(ts.tables.nodes.individual,
-                                              ts.tables.nodes.population)):
-            raise ValueError("Individual has nodes from more than one population.")
-        if not np.all(unique_labels_by_group(ts.tables.nodes.individual,
-                                              ts.tables.nodes.time)):
-            raise ValueError("Individual has nodes from more than one time.")
-        has_indiv = (ts.tables.nodes.individual >= 0)
-        which_indiv = ts.tables.nodes.individual[has_indiv]
-        # if we did not do the sanity check above then an individual with nodes in more than one pop
-        # would get the pop of their last node in the list
-        self.individual_populations[which_indiv] = ts.tables.nodes.population[has_indiv]
-        self.individual_times[which_indiv] = ts.tables.nodes.time[has_indiv]
-
-    def __getstate__(self):
-        return {
-            'tables': self.dump_tables(),
-        }
-
-    def __setstate__(self, state):
-        self.__init__(
-            state['tables'].tree_sequence(),
-        )
-
-    @property
-    def slim_generation(self):
-        _deprecation_warning("Please access ts.metadata['SLiM']['generation'] instead.")
-        return self._slim_generation
-
-    @slim_generation.setter
-    def slim_generation(self, value):
-        _deprecation_warning("Please access ts.metadata['SLiM']['generation'] instead.")
-        self._slim_generation = value
-
-    @property
-    def model_type(self):
-        _deprecation_warning("Please access ts.metadata['SLiM']['model_type'] instead.")
-        return self.metadata['SLiM']['model_type']
-
-    @classmethod
-    def load(cls, path, legacy_metadata=False):
-        '''
-        Load a :class:`SlimTreeSequence` from a .trees file on disk.
-
-        :param string path: The path to a .trees file.
-        :rtype SlimTreeSequence:
-        '''
-        ts = tskit.load(path)
-        return cls(ts, legacy_metadata=legacy_metadata)
-
-    @classmethod
-    def load_tables(cls, tables, **kwargs):
-        '''
-        Creates the :class:`SlimTreeSequence` defined by the tables.
-
-        :param TableCollection tables: A set of tables, as produced by SLiM
-            or by :func:`annotate_defaults`.
-        :rtype SlimTreeSequence:
-        '''
-        # a roundabout way to copy the tables
-        ts = tables.tree_sequence()
-        return cls(ts, **kwargs)
-
-    def dump(self, path, **kwargs):
-        '''
-        Dumps the tree sequence to the path specified. This is only a wrapper
-        :func:`tskit.TreeSequence.dump`.
-
-        :param str path: The file path to write the TreeSequence to.
-        :param kwargs: Additional keyword args to pass to tskit.TreeSequence.dump
-        '''
-        # temporary until we remove support for setting slim_generation
-        if self._slim_generation != self.metadata['SLiM']['generation']:
-            tables = self.dump_tables()
-            md = tables.metadata
-            md['SLiM']['generation'] = self._slim_generation
-            tables.metadata = md
-            tables.dump(path, **kwargs)
-        else:
-            super(SlimTreeSequence, self).dump(path, **kwargs)
-
-    def simplify(self, *args, **kwargs):
-        '''
-        This is a wrapper for :meth:`tskit.TreeSequence.simplify`.
-        The only difference is that this method returns the
-        derived class :class:`.SlimTreeSequence`.
-
-        If you have not yet recapitated your SlimTreeSequence, you probably want to
-        pass ``keep_input_roots=True``, so that recapitation is possible in the future.
-
-        :rtype SlimTreeSequence:
-        '''
-        # temporary until we remove support for setting slim_generation
-        if self._slim_generation != self.metadata['SLiM']['generation']:
-            tables = self.dump_tables()
-            md = tables.metadata
-            md['SLiM']['generation'] = self._slim_generation
-            tables.metadata = md
-            # note we have to go to a tree sequence to get the map_nodes argument
-            sts = tables.tree_sequence().simplify(*args, **kwargs)
-        else:
-            sts = super(SlimTreeSequence, self).simplify(*args, **kwargs)
-        if (type(sts) == tuple):
-            ret = (SlimTreeSequence(sts[0]), sts[1])
-        else:
-            ret = SlimTreeSequence(sts)
-
-        return ret
-
-    def population(self, id_):
-        '''
-        Returns the population whose ID is given by `id_`, as documented in
-        :meth:`tskit.TreeSequence.population`, but with additional attributes:
-        `slim_id`, `selfing_fraction`, `female_cloning_fraction`,
-        `male_cloning_fraction`, `sex_ratio`,
-        `bounds_x0`, `bounds_x1`, `bounds_y0`, `bounds_y1`, `bounds_z0`, `bounds_z1`,
-        and `migration_records`.
-        These are all recorded by SLiM in the metadata.
-
-        Note that SLiM populations are usually indexed starting from 1,
-        but in tskit from zero, so there may be populations (e.g., with id_=0)
-        that have no metadata and were not created by SLiM.
-
-        :param int id_: The ID of the population (i.e., its index).
-        '''
-        pop = super(SlimTreeSequence, self).population(id_)
-        if pop.metadata is not None:
-            pop.metadata = MetadataDictWrapper(pop.metadata)
-        return pop
-
-    def individual(self, id_):
-        '''
-        Returns the individual whose ID is given by `id_`, as documented in
-        :meth:`tskit.TreeSequence.individual`, but with additional attributes:
-        `time`, `pedigree_id`, `age`, `slim_population`, `sex`, and `slim_flags`.
-        The `time` and `population` properties are extracted from the nodes,
-        and an error will be thrown if the individual's nodes derive from
-        more than one population or more than one time.
-
-        :param int id_: The ID of the individual (i.e., its index).
-        '''
-        ind = super(SlimTreeSequence, self).individual(id_)
-        ind.population = self.individual_populations[id_]
-        ind.time = self.individual_times[id_]
-        ind.metadata = MetadataDictWrapper(ind.metadata)
-        return ind
-
-    def node(self, id_):
-        '''
-        Returns the node whose ID is given by `id_`, as documented in
-        :meth:`tskit.TreeSequence.node`, but with additional attributes:
-        `slim_id`, `is_null`, and `genome_type`.
-        These are all recorded by SLiM in the metadata.
-
-        :param int id_: The ID of the node (i.e., its index).
-        '''
-        node = super(SlimTreeSequence, self).node(id_)
-        if node.metadata is not None:
-            node.metadata = MetadataDictWrapper(node.metadata)
-        return node
-
-    def mutation(self, id_):
-        '''
-        Returns the mutation whose ID is given by `id_`, as documented in
-        :meth:`tskit.TreeSequence.mutation`, but with additional attributes:
-        `mutation_type`, `selection_coeff`, `population`, `slim_time`,
-        and `nucleotide`.
-        These are all recorded by SLiM in the metadata.
-
-        :param int id_: The ID of the mutation (i.e., its index).
-        '''
-        mut = super(SlimTreeSequence, self).mutation(id_)
-        mut.metadata = MetadataDictWrapper(mut.metadata)
-        return mut
-
-    def recapitate(self,
-                   recombination_rate=None,
-                   population_configurations=None,
-                   recombination_map=None, **kwargs):
-        '''
-        Returns a "recapitated" tree sequence, by using msprime to run a
-        coalescent simulation from the "top" of this tree sequence, i.e.,
-        allowing any uncoalesced lineages to coalesce.
-
-        .. warning::
-
-            This method is deprecated: please use pyslim.recapitate( ) instead.
-
-        To allow recapitation to be done correctly, the nodes of the
-        first generation of the SLiM simulation from whom all samples inherit
-        are still present in the tree sequence, but are not marked as samples.
-        If you simplify the tree sequence before recapitating you must ensure
-        these are not removed, which you do by passing the argument
-        ``keep_input_roots=True`` to :meth:`.simplify()`.
-
-        Note that ``Ne`` is not set automatically, so defaults to ``1.0``; you probably
-        want to set it explicitly.  Similarly, migration is not set up
-        automatically, so that if there are uncoalesced lineages in more than
-        one population, you will need to pass in a demography that allows coalescence.
-
-        In general, all defaults are whatever the defaults of
-        {ref}`msprime.simulate` are; this includes recombination rate, so
-        that if neither ``recombination_rate`` or a ``recombination_map`` are
-        provided, there will be *no* recombination.
-
-        :param float recombination_rate: A (constant) recombination rate,
-            in units of crossovers per nucleotide per unit of time.
-        :param list population_configurations: See :meth:`msprime.simulate` for
-            this argument; if not provided, each population will have zero growth rate
-            and the same effective population size.
-        :type recombination_map: :class`msprime.RecombinationMap`
-        :param recombination_map: The recombination map, or None,
-            if recombination_rate is specified.
-        :param dict kwargs: Any other arguments to :meth:`msprime.simulate`.
-        '''
-        if "keep_first_generation" in kwargs:
-            raise ValueError("The keep_first_generation argument is deprecated: "
-                             "the FIRST_GEN flag is no longer used.")
-        _deprecation_warning("Please use pyslim.recapitate( ) instead.")
-
-        if recombination_map is None:
-            recombination_map = msprime.RecombinationMap(
-                       positions = [0.0, self.sequence_length],
-                       rates = [recombination_rate, 0.0],
-                       num_loci = int(self.sequence_length))
-
-        if population_configurations is None:
-            population_configurations = [msprime.PopulationConfiguration()
-                                         for _ in range(self.num_populations)]
-        # temporary until we remove support for setting slim_generation
-        if self._slim_generation != self.metadata['SLiM']['generation']:
-            tables = self.dump_tables()
-            md = tables.metadata
-            md['SLiM']['generation'] = self._slim_generation
-            tables.metadata = md
-            ts = tables.tree_sequence()
-        else:
-            ts = self
-
-        with warnings.catch_warnings():
-            warnings.simplefilter("ignore", msprime.IncompletePopulationMetadataWarning)
-            recap = msprime.simulate(
-                        from_ts = ts,
-                        population_configurations = population_configurations,
-                        recombination_map = recombination_map,
-                        start_time = self.metadata['SLiM']['generation'],
-                        **kwargs)
-        return SlimTreeSequence(recap)
-
-    def mutation_at(self, node, position, time=None):
-        '''
-        Finds the mutation present in the genome of ``node`` at ``position``,
-        returning -1 if there is no such mutation recorded in the tree
-        sequence.  Warning: if ``node`` is not actually in the tree sequence
-        (e.g., not ancestral to any samples) at ``position``, then this
-        function will return -1, possibly erroneously.  If `time` is provided,
-        returns the last mutation at ``position`` inherited by ``node`` that
-        occurred at or before ``time`` ago.
-
-        :param int node: The index of a node in the tree sequence.
-        :param float position: A position along the genome.
-        :param int time: The time ago that we want the nucleotide, or None,
-            in which case the ``time`` of ``node`` is used.
-
-        :returns: Index of the mutation in question, or -1 if none.
-        '''
-        if position < 0 or position >= self.sequence_length:
-            raise ValueError("Position {} not valid.".format(position))
-        if node < 0 or node >= self.num_nodes:
-            raise ValueError("Node {} not valid.".format(node))
-        if time is None:
-            time = self.node(node).time
-        tree = self.at(position)
-        site_pos = self.tables.sites.position
-        out = tskit.NULL
-        if position in site_pos:
-            site_index = np.where(site_pos == position)[0][0]
-            site = self.site(site_index)
-            mut_nodes = []
-            # look for only mutations that occurred before `time`
-            # not strictly necessary if time was None
+def load(*args, **kwargs):
+    raise RuntimeError("This method has been removed: use tskit.load( ) instead.")
+
+
+def mutation_at(ts, node, position, time=None):
+    '''
+    Finds the mutation present in the genome of ``node`` at ``position``,
+    returning -1 if there is no such mutation recorded in the tree
+    sequence.  Warning: if ``node`` is not actually in the tree sequence
+    (e.g., not ancestral to any samples) at ``position``, then this
+    function will return -1, possibly erroneously.  If `time` is provided,
+    returns the last mutation at ``position`` inherited by ``node`` that
+    occurred at or before ``time`` ago.
+
+    :param int node: The index of a node in the tree sequence.
+    :param float position: A position along the genome.
+    :param int time: The time ago that we want the nucleotide, or None,
+        in which case the ``time`` of ``node`` is used.
+
+    :returns: Index of the mutation in question, or -1 if none.
+    '''
+    if position < 0 or position >= ts.sequence_length:
+        raise ValueError("Position {} not valid.".format(position))
+    if node < 0 or node >= ts.num_nodes:
+        raise ValueError("Node {} not valid.".format(node))
+    if time is None:
+        time = ts.node(node).time
+    tree = ts.at(position)
+    # TODO: use `position` argument to ts.site( )
+    site_pos = ts.tables.sites.position
+    out = tskit.NULL
+    if position in site_pos:
+        site_index = np.where(site_pos == position)[0][0]
+        site = ts.site(site_index)
+        mut_nodes = []
+        # look for only mutations that occurred before `time`
+        # not strictly necessary if time was None
+        for mut in site.mutations:
+            if mut.time >= time:
+                mut_nodes.append(mut.node)
+        n = node
+        while n > -1 and n not in mut_nodes:
+            n = tree.parent(n)
+        if n >= 0:
+            # do careful error checking here
             for mut in site.mutations:
-                if mut.time >= time:
-                    mut_nodes.append(mut.node)
-            n = node
-            while n > -1 and n not in mut_nodes:
-                n = tree.parent(n)
-            if n >= 0:
-                # do careful error checking here
-                for mut in site.mutations:
-                    if mut.node == n:
-                        assert(out == tskit.NULL or out == mut.parent)
-                        out = mut.id
-        return out
-
-    def nucleotide_at(self, node, position, time=None):
-        '''
-        Finds the nucleotide present in the genome of ``node`` at ``position``.
-        Warning: if ``node`` is not actually in the tree sequence (e.g., not
-        ancestral to any samples) at ``position``, then this function will
-        return the reference sequence nucleotide, possibly erroneously.  If
-        `time` is provided, returns the last nucletide produced by a mutation
-        at ``position`` inherited by ``node`` that occurred at or before
-        ``time`` ago.
-
-        :param int node: The index of a node in the tree sequence.
-        :param float position: A position along the genome.
-        :param int time: The time ago that we want the nucleotide, or None,
-            in which case the ``time`` of ``node`` is used.
-
-        :returns: Index of the nucleotide in ``NUCLEOTIDES`` (0=A, 1=C, 2=G, 3=T).
-        '''
-        if not self.has_reference_sequence():
-            raise ValueError("This tree sequence has no reference sequence.")
-        mut_id = self.mutation_at(node, position, time)
-        if mut_id == tskit.NULL:
-            out = NUCLEOTIDES.index(self.reference_sequence.data[int(position)])
-        else:
-            mut = self.mutation(mut_id)
-            k = np.argmax([u["slim_time"] for u in mut.metadata["mutation_list"]])
-            out = mut.metadata["mutation_list"][k]["nucleotide"]
-        return out
-
-    @property
-    def slim_provenance(self):
-        '''
-        Returns model type, slim generation, and remembered node count from
-        the last entry in the provenance table that is tagged with "program"="SLiM".
-
-        NOTE: you probably want to use the ``.metadata`` property instead.
-
-        :rtype ProvenanceMetadata:
-        '''
-        warnings.warn("The 'slim_provenance' attribute is deprecated: get information from "
-                      "ts.metadata['SLiM'] instead.", FutureWarning)
-        return get_provenance(self, only_last=True)
-
-    @property
-    def slim_provenances(self):
-        '''
-        Returns model type, slim generation, and remembered node count from *all*
-        entries in the provenance table that are tagged with "program"="SLiM".
-
-        :rtype ProvenanceMetadata:
-        '''
-        return get_provenance(self, only_last=False)
-
-    def individuals_alive_at(self, time, stage='late', remembered_stage=None,
-                             population=None, samples_only=False):
-        """
-        Returns an array giving the IDs of all individuals that are known to be
-        alive at the given time ago.  This is determined using their birth time
-        ago (given by their `time` attribute) and, for nonWF models,
-        their `age` attribute (which is equal to their age at the last time
-        they were Remembered). See also :meth:`.individual_ages_at`.
-
-        In WF models, birth occurs after "early()", so that individuals are only
-        alive during "late()" for the time step when they have age zero,
-        while in nonWF models, birth occurs before "early()", so they are alive
-        for both stages.
-        
-        In both WF and nonWF models, mortality occurs between
-        "early()" and "late()", so that individuals are last alive during the
-        "early()" stage of the time step of their final age, and if individuals
-        are alive during "late()" they will also be alive during "early()" of the
-        next time step. This means it is important to know during which stage
-        individuals were Remembered - for instance, if the call to
-        sim.treeSeqRememberIndividuals() was made during "early()" of a given time step,
-        then those individuals might not have survived until "late()" of that
-        time step. Since SLiM does not record the stage at which individuals
-        were Remembered, you can specify this by setting ``remembered_stages``:
-        it should be the stage during which *all* calls to
-        ``sim.treeSeqRememberIndividuals()``,  as well as to ``sim.treeSeqOutput()``,
-        were made.
-
-        Note also that in nonWF models, birth occurs before "early()", so the
-        possible parents in a given time step are those that are alive in
-        "early()" and have age greater than zero, or, equivalently, are alive in
-        "late()" during the previous time step.
-        In WF models, birth occurs after "early()", so possible parents in a
-        given time step are those that are alive during "early()" of that time
-        step or are alive during "late()" of the previous time step.
-
-        :param float time: The number of time steps ago.
-        :param str stage: The stage in the SLiM life cycle that we are inquiring
-            about (either "early" or "late"; defaults to "late").
-        :param str remembered_stage: The stage in the SLiM life cycle
-            during which individuals were Remembered (defaults to the stage the
-            tree sequence was recorded at, stored in metadata).
-        :param int population: If given, return only individuals in the
-            population(s) with these population ID(s).
-        :param bool samples_only: Whether to return only individuals who have at
-            least one node marked as samples.
-        """
-        if stage not in ("late", "early"):
-            raise ValueError(f"Unknown stage '{stage}': "
-                              "should be either 'early' or 'late'.")
-
-        if remembered_stage is None:
-            remembered_stage = self.metadata['SLiM']['stage']
-
-        if remembered_stage not in ("late", "early"):
-            raise ValueError(f"Unknown remembered_stage '{remembered_stage}': "
-                              "should be either 'early' or 'late'.")
-        if remembered_stage != self.metadata['SLiM']['stage']:
-            warnings.warn(f"Provided remembered_stage '{remembered_stage}' does not"
-                          " match the stage at which the tree sequence was saved"
-                          f" ('{self.metadata['SLiM']['stage']}'). This is not necessarily"
-                          " an error, but mismatched stages will lead to inconsistencies:"
-                          " make sure you know what you're doing.")
-
-        # birth_time is the time ago that they were first alive in 'late'
-        # in a nonWF model they are alive for the same time step's 'early'
-        # but in a WF model the first 'early' they were alive for is one more recent
-        birth_time = self.individual_times
-        # birth_time - birth_offset is the first time ago they were alive
-        # during stage 'stage'
-        if stage == "early" and self.metadata['SLiM']['model_type'] == "WF":
-            birth_offset = 1
-        else:
-            birth_offset = 0
-        # ages is the number of complete life cycles they are known to have lived through,
-        # and so individuals have lived through at least 'age + 1' of both stages.
-        # In nonWF models, they live for one more 'early' than 'late',
-        # but this is only reflected in their age if Remembered in 'early'.
-        ages = self.individual_ages
-        # ages + age_offset + 1 is the number of 'stage' stages they are known
-        # to have lived through
-        if (self.metadata['SLiM']['model_type'] == "WF"
-                or stage == remembered_stage):
-            age_offset = 0
-        else:
-            if (remembered_stage == "early"
-                    and stage == "late"):
-                age_offset = -1
-            else:
-                age_offset = 1
-        # if adjusted age=0 then they are be alive at exactly one time step
-        alive_bool = np.logical_and(
-                birth_time >= time + birth_offset,
-                birth_time - ages <= time + birth_offset + age_offset)
-
-        if population is not None:
-            alive_bool &= np.isin(self.individual_populations, population)
-        if samples_only:
-            alive_bool &= (0 < np.bincount(1 + self.tables.nodes.individual,
-                                           self.tables.nodes.flags & tskit.NODE_IS_SAMPLE,
-                                           minlength=1 + self.num_individuals)[1:])
-
-        return np.where(alive_bool)[0]
-
-    def individual_ages_at(self, time, stage="late", remembered_stage="late"):
-        """
-        Returns the `ages` of each individual at the corresponding time ago,
-        which will be ``nan`` if the individual is either not born yet or dead.
-        This is computed as the time ago the individual was born (found by the
-        `time` associated with the the individual's nodes) minus the `time`
-        argument; while "death" is inferred from the individual's ``age``,
-        recorded in metadata. These values are the same as what would be shown
-        in SLiM during the corresponding time step and stage.
-
-        Since age increments at the end of each time step,
-        the age is the number of time steps ends the individual has lived
-        through, so if they were born in time step `time`, then their age
-        will be zero.
-
-        In a WF model, this method does not provide any more information than
-        does :meth:`.individuals_alive_at`, but for consistency, non-nan ages
-        will be 0 in "late" and 1 in "early".
-        See :meth:`.individuals_alive_at` for further discussion.
-
-        :param float time: The reference time ago.
-        :param str stage: The stage in the SLiM life cycle used to determine who
-            is alive (either "early" or "late"; defaults to "late").
-        :param str remembered_stage: The stage in the SLiM life cycle during which
-            individuals were Remembered.
-        """
-        ages = np.repeat(np.nan, self.num_individuals)
-        alive = self.individuals_alive_at(time, stage=stage,
-                                          remembered_stage=remembered_stage)
-        ages[alive] = self.individual_times[alive] - time
-        return ages
-
-    def slim_time(self, time, stage="late"):
-        """
-        Converts the given "tskit times" (i.e., in units of time before the end
-        of the simulation) to SLiM times (those recorded by SLiM, usually in units
-        of generations since the start of the simulation). Although the latter are
-        always integers, these will not be if the provided times are not integers.
-
-        When the tree sequence is written out, SLiM records the value of its
-        current generation, which can be found in the metadata:
-        ``ts.metadata['SLiM']['generation']``. In most cases, the “SLiM time”
-        referred to by a time ago in the tree sequence (i.e., the value that would
-        be reported by sim.generation within SLiM at the point in time thus
-        referenced) can be obtained by subtracting that time ago from
-        ``ts.metadata['SLiM']['generation']``. However, in WF models, birth
-        happens between the “early()” and “late()” stages, so if the tree
-        sequence was written out using sim.treeSeqOutput() during “early()” in
-        a WF model, the tree sequence’s times measure time before the last set
-        of individuals are born, i.e., before SLiM time step
-        ``ts.metadata['SLiM']['generation'] - 1``.
-
-        In some situations (e.g., mutations added during early() in WF models)
-        this may not return what you expect. See :ref:`sec_metadata_converting_times`
-        for more discussion.
-
-        :param array time: An array of times to be converted.
-        :param string stage: The stage of the SLiM life cycle that the SLiM time
-            should be computed for.
-        """
-        slim_time = self.metadata['SLiM']['generation'] - time
-        if self.metadata['SLiM']['model_type'] == "WF":
-            if (self.metadata['SLiM']['stage'] == "early" and stage == "late"):
-                slim_time -= 1
-            if (self.metadata['SLiM']['stage'] == "late" and stage == "early"):
-                slim_time += 1
-        return slim_time
-
-    def first_generation_individuals(self):
-        """
-        Returns the IDs of the individuals remembered as part of the first SLiM generation,
-        as determined by their flags.
-
-        .. warning::
-        
-            This method is deprecated, because from SLiM version 3.5
-            the first generation individuals are no longer marked as such:
-            only tree sequences from older versions of SLiM will have
-            these individuals.
-        """
-        warnings.warn(
-                "This method is deprecated: SLiM no longer marks individuals as "
-                "'first generation' any longer - you must explicitly Remember them "
-                "to keep them in the tree sequence. Returning *retained* individuals "
-                "instead.", FutureWarning)
-        return np.where(self.tables.individuals.flags & INDIVIDUAL_RETAINED > 0)[0]
-
-    def _do_individual_parents_stuff(self, return_parents=False):
-        # Helper for has_individual_parents and individual_parents,
-        # which share a lot of machinery.
-        edges = self.tables.edges
-        nodes = self.tables.nodes
-        edge_parent_indiv = nodes.individual[edges.parent]
-        edge_child_indiv = nodes.individual[edges.child]
-        # nodes whose parent nodes are all in the same individual
-        unique_parent_nodes = unique_labels_by_group(
-                edges.child,
-                edge_parent_indiv,
-                minlength=nodes.num_rows)
-        unique_parent_edges = unique_parent_nodes[edges.child]
-        # edges describing relationships between individuals
-        indiv_edges = np.logical_and(
-                np.logical_and(edge_parent_indiv != tskit.NULL,
-                                     edge_child_indiv != tskit.NULL),
-                unique_parent_edges)
-        # individual edges where the parent was alive during "late"
-        # of the time step before the child is born
-        child_births = self.individual_times[edge_child_indiv[indiv_edges]]
-        parent_births = self.individual_times[edge_parent_indiv[indiv_edges]]
-        alive_edges = indiv_edges.copy()
-        if self.metadata['SLiM']['model_type'] == "WF":
-            alive_edges[indiv_edges] = (child_births + 1 == parent_births)
-        else:
-            parent_deaths = parent_births - self.individual_ages[edge_parent_indiv[indiv_edges]]
-            alive_edges[indiv_edges] = (child_births + 1 >= parent_deaths)
-        edge_spans = edges.right - edges.left
-        parental_span = np.bincount(edge_child_indiv[alive_edges],
-                weights=edge_spans[alive_edges], minlength=self.num_individuals)
-        # we could also check for edges without individual parents terminating
-        # in this individual, but this is unnecessary as the entire genome is
-        # accounted for
-        has_all_parents = (parental_span == 2 * self.sequence_length)
-        if return_parents:
-            full_parent_edges = np.logical_and(
-                    alive_edges,
-                    has_all_parents[edge_child_indiv])
-            parents = np.unique(np.column_stack(
-                            [edge_parent_indiv[full_parent_edges],
-                             edge_child_indiv[full_parent_edges]]
-                            ), axis=0)
-            return parents
-        else:
-            return has_all_parents
-
-    def individual_parents(self):
-        '''
-        Finds all parent-child relationships in the tree sequence (as far as we
-        can tell). The output will be a two-column array with row [i,j]
-        indicating that individual i is a parent of individual j.  See
-        :meth:`.has_individual_parents` for exactly which parents are returned.
-
-        See :meth:`.individuals_alive_at` for further discussion about how
-        this is determined based on when the individuals were Remembered.
-
-        :return: An array of individual IDs, with row [i, j] if individual i is
-            a parent of individual j.
-        '''
-        return self._do_individual_parents_stuff(return_parents=True)
-
-    def has_individual_parents(self):
-        '''
-        Finds which individuals have both their parent individuals also present
-        in the tree sequence, as far as we can tell. To do this, we return a
-        boolean array with True for those individuals for which:
-
-        - all edges terminating in that individual's nodes are in individuals,
-        - each of the individual's nodes inherit from a single individual only,
-        - those parental individuals were alive when the individual was born,
-        - the parental individuals account for two whole genomes.
-
-        This returns a boolean array indicating for each individual whether all
-        these are true. Note in particular that individuals with only *one*
-        recorded parent are *not* counted as "having parents".
-
-        See :meth:`.individuals_alive_at` for further discussion about how
-        this is determined based on when the individuals were Remembered.
-
-        :return: A boolean array of length equal to ``targets``.
-        '''
-        return self._do_individual_parents_stuff(return_parents=False)
-
-
-def _set_nodes_individuals(tables, age):
-    '''
-    Adds to a TableCollection the information relevant to individuals required
-    for SLiM to load in a tree sequence, that is found in Node and Individual
-    tables.  This will replace any existing Individual table, and will replace
-    any information already in the individual, metadata, and population columns
-    of the Node table.
-
-    This is designed to make it easy to assign default values:
-    - (node_ind) the 2*j-th and (2*j+1)-st `sample` nodes to individual j
-    - (location) individual locations to (0, 0, 0)
-    - (age) individual age to 0
-    - (ind_id) SLiM individual pedigree IDs to sequential integers starting from 0
-    - (ind_population) individual populations to 0
-    - (node_id) SLiM genome IDs to sequential integers starting with samples from 0
-    - (node_is_null) genomes to be non-null
-    - (node_type) genome type to 0 (= autosome)
-    - (ind_flags) INDIVIDUAL_ALIVE
-
-    If you have other situations, like non-alive "remembered" individuals, you
-    will need to edit the tables by hand, afterwards.
-    '''
-    samples = np.where(tables.nodes.flags & tskit.NODE_IS_SAMPLE)[0]
-    if (len(samples) % 2) != 0:
-        raise ValueError("There must be an even number of sampled nodes,"\
-                         + "since organisms are diploid.")
-
-    num_individuals = int(len(samples) / 2)
-    node_ind = np.repeat(tskit.NULL, tables.nodes.num_rows).astype("int32")
-    node_ind[samples] = np.arange(len(samples)) // 2
-    ind_id = np.arange(num_individuals)
-    slim_node_id = np.repeat(tskit.NULL, tables.nodes.num_rows)
-    slim_node_id[samples] = np.arange(len(samples))
-
-    ind_population = np.repeat(tskit.NULL, num_individuals)
-    ind_population[node_ind[samples]] = tables.nodes.population[samples]
-
-    if not np.all(unique_labels_by_group(node_ind,
-                                          tables.nodes.population)):
-        raise ValueError("Individual has nodes from more than one population.")
-    if not np.all(unique_labels_by_group(node_ind,
-                                          tables.nodes.time)):
-        raise ValueError("Individual has nodes from more than one time.")
-
-    loc_vec = np.zeros(num_individuals * 3).astype("float64")
-    loc_off = 3 * np.arange(num_individuals + 1).astype("uint32")
-    ind_flags = np.repeat(INDIVIDUAL_ALIVE, num_individuals).astype("uint32")
-
-    default_ind = default_slim_metadata("individual")
-    sex = default_ind['sex']
-    slim_flag = default_ind['flags']
-    pedigree_p1 = default_ind['pedigree_p1']
-    pedigree_p2 = default_ind['pedigree_p2']
-
-    ims = tables.individuals.metadata_schema
-    individual_metadata = [
-            ims.encode_row({
-                'pedigree_id': iid,
-                'age': age,
-                'subpopulation': int(pop),
-                'sex': sex,
-                'flags': slim_flag,
-                'pedigree_p1': pedigree_p1,
-                'pedigree_p2': pedigree_p2,
-            })
-            for (iid, pop) in
-            zip(ind_id, ind_population)]
-    imb, imo = tskit.pack_bytes(individual_metadata)
-    tables.individuals.set_columns(
-            flags=ind_flags, location=loc_vec, location_offset=loc_off,
-            metadata=imb, metadata_offset=imo)
-    assert(tables.individuals.num_rows == num_individuals)
-    
-    default_node = default_slim_metadata("node")
-    node_is_null = default_node["is_null"]
-    node_type = default_node["genome_type"]
-    nms = tables.nodes.metadata_schema
-    node_metadata = [b'' for _ in range(tables.nodes.num_rows)]
-    for j in samples:
-        node_metadata[j] = nms.encode_row({'slim_id': slim_node_id[j],
-                               'is_null': node_is_null,
-                               'genome_type': node_type
-                               })
-    nmb, nmo = tskit.pack_bytes(node_metadata)
-    tables.nodes.set_columns(flags=tables.nodes.flags, time=tables.nodes.time,
-                             population=tables.nodes.population, individual=node_ind,
-                             metadata=nmb, metadata_offset=nmo)
-
-
-def _set_populations(tables):
-    '''
-    Adds to a TableCollection the information about populations required for SLiM
-    to load a tree sequence. This will replace anything already in the Population
-    table.
-    '''
-    num_pops = max(tables.nodes.population) + 1
-    tables.populations.clear()
-    pop_id = np.arange(num_pops)
-    for j in range(num_pops):
-        md = default_slim_metadata("population")
-        md["slim_id"] = j
-        md["name"] = f"pop_{j}"
-        tables.populations.add_row(metadata=md)
-
-
-def _set_sites_mutations(tables):
-    '''
-    Adds to a TableCollection the information relevant to mutations required
-    for SLiM to load in a tree sequence. This means adding to the metadata column
-    of the Mutation table,  It will also
-    - give SLiM IDs to each mutation
-    - replace ancestral states with ""
-    This will replace any information already in the metadata or derived state
-    columns of the Mutation table. We set slim_time in metadata so that
-    - slim_generation = floor(tskit time) + slim_time
-    '''
-    num_mutations = tables.mutations.num_rows
-    default_mut = default_slim_metadata("mutation_list_entry")
-    dsb, dso = tskit.pack_bytes([str(j).encode() for j in range(num_mutations)])
-    slim_time = tables.metadata["SLiM"]["generation"] - np.floor(tables.mutations.time).astype("int")
-    mms = tables.mutations.metadata_schema
-    mutation_metadata = [
-            mms.encode_row(
-                {"mutation_list": 
-                 [{"mutation_type": default_mut["mutation_type"],
-                  "selection_coeff": default_mut["selection_coeff"],
-                  "subpopulation": default_mut["subpopulation"],
-                  "slim_time": st,
-                  "nucleotide": default_mut["nucleotide"]
-                   }]
-                })
-            for st in slim_time]
-    mdb, mdo = tskit.pack_bytes(mutation_metadata)
-    tables.mutations.set_columns(
-            site=tables.mutations.site,
-            node=tables.mutations.node,
-            time=tables.mutations.time,
-            derived_state=dsb,
-            derived_state_offset=dso,
-            parent=tables.mutations.parent,
-            metadata=mdb,
-            metadata_offset=mdo)
-    tables.sites.set_columns(
-            position=tables.sites.position,
-            ancestral_state=np.array([], dtype='int8'),
-            ancestral_state_offset=np.zeros(tables.sites.num_rows + 1, dtype='uint32'))
-
-def update_tables(tables):
-    """
-    Update tables produced by a previous verion of SLiM to the current file version.
-    """
-    # First we ensure we can find the file format version number
-    # in top-level metadata. Then we proceed to fix up the tables as necessary.
-    if not (isinstance(tables.metadata, dict) and 'SLiM' in tables.metadata):
-        # Old versions kept information in provenance, not top-level metadata.
-        # Note this uses defaults on keys not present in provenance,
-        # which prior to 0.5 was everything but generation and model_type.
-        values = default_slim_metadata('tree_sequence')['SLiM']
-        prov = None
-        file_version = 'unknown'
-        # use only the last SLiM provenance
-        for p in tables.provenances:
-            is_slim, this_file_version = slim_provenance_version(p) 
-            if is_slim:
-                prov = p
-                file_version = this_file_version
-        values['file_version'] = file_version
-        try:
-            record = json.loads(prov.record)
-            if file_version == "0.1":
-                values['model_type'] = record['model_type']
-                values['generation'] = record['generation']
-            else:
-                for k in values:
-                    if k in record['parameters']:
-                        values[k] = record['parameters'][k]
-                values['generation'] = record['slim']['generation']
-        except:
-            raise ValueError("Failed to obtain metadata from provenance.")
-        set_tree_sequence_metadata(tables, **values)
-
-    file_version = tables.metadata['SLiM']['file_version']
-    if file_version != slim_file_version:
-        warnings.warn("This is a version {} SLiM tree sequence.".format(file_version) +
-                      " When you write this out, " +
-                      "it will be converted to version {}.".format(slim_file_version))
-
-        # the only tables to have metadata schema changed thus far
-        # are populations, individuals, and mutations:
-        old_schema = _old_metadata_schema("population", file_version)
-        if old_schema is not None:
-            pops = tables.populations.copy()
-            tables.populations.clear()
-            if pops.metadata_schema == tskit.MetadataSchema(None):
-                pops.metadata_schema = old_schema
-            new_schema = slim_metadata_schemas["population"]
-            tables.populations.metadata_schema = new_schema
-            defaults = default_slim_metadata("population")
-            # just needs recoding
-            for pop in pops:
-                tables.populations.append(pop)
-
-        old_schema = _old_metadata_schema("individual", file_version)
-        if old_schema is not None:
-            inds = tables.individuals.copy()
-            tables.individuals.clear()
-            if inds.metadata_schema == tskit.MetadataSchema(None):
-                inds.metadata_schema = old_schema
-            new_schema = slim_metadata_schemas["individual"]
-            tables.individuals.metadata_schema = new_schema
-            defaults = default_slim_metadata("individual")
-            d = {}
-            for k in ["pedigree_p1", "pedigree_p2"]:
-                d[k] = defaults[k]
-            for ind in inds:
-                md = ind.metadata
-                md.update(d)
-                tables.individuals.append(ind.replace(metadata=md))
-
-        old_schema = _old_metadata_schema("mutation", file_version)
-        if old_schema is not None:
-            muts = tables.mutations.copy()
-            tables.mutations.clear()
-            if muts.metadata_schema == tskit.MetadataSchema(None):
-                muts.metadata_schema = old_schema
-            tables.mutations.metadata_schema = slim_metadata_schemas["mutation"]
-            for mut in muts:
-                md = mut.metadata
-                for ml in md['mutation_list']:
-                    ml['nucleotide'] = -1
-                tables.mutations.append(mut.replace(metadata=md))
-
-        if file_version == "0.1":
-            # shift times
-            slim_generation = tables.metadata['SLiM']['generation']
-            node_times = tables.nodes.time + slim_generation
-            tables.nodes.set_columns(
-                    flags=tables.nodes.flags,
-                    time=node_times,
-                    population=tables.nodes.population,
-                    individual=tables.nodes.individual,
-                    metadata=tables.nodes.metadata,
-                    metadata_offset=tables.nodes.metadata_offset)
-            migration_times = tables.migrations.time + slim_generation
-            tables.migrations.set_columns(
-                    left=tables.migrations.left,
-                    right=tables.migrations.right,
-                    node=tables.migrations.node,
-                    source=tables.migrations.source,
-                    dest=tables.migrations.dest,
-                    time=migration_times)
-
-        new_record = {
-                    "schema_version": "1.0.0",
-                    "software": {
-                        "name": "pyslim",
-                        "version": pyslim_version,
-                        },
-                    "parameters": {
-                        "command": ["updrade_tables"],
-                        "old_file_version": file_version,
-                        "new_file_version": slim_file_version,
-                        },
-                    "environment": get_environment(),
-                }
-        tskit.validate_provenance(new_record)
-        tables.provenances.add_row(json.dumps(new_record))
-
-        set_metadata_schemas(tables)
-        md = tables.metadata
-        md['SLiM']['file_version'] = slim_file_version
-        tables.metadata = md
+                if mut.node == n and mut.time >= time:
+                    # BUG: this can fail if a mutation has two children
+                    assert(out == tskit.NULL or out == mut.parent)
+                    out = mut.id
+    return out
+
+def nucleotide_at(ts, node, position, time=None):
+    '''
+    Finds the nucleotide present in the genome of ``node`` at ``position``.
+    Warning: if ``node`` is not actually in the tree sequence (e.g., not
+    ancestral to any samples) at ``position``, then this function will
+    return the reference sequence nucleotide, possibly erroneously.  If
+    `time` is provided, returns the last nucletide produced by a mutation
+    at ``position`` inherited by ``node`` that occurred at or before
+    ``time`` ago.
+
+    :param int node: The index of a node in the tree sequence.
+    :param float position: A position along the genome.
+    :param int time: The time ago that we want the nucleotide, or None,
+        in which case the ``time`` of ``node`` is used.
+
+    :returns: Index of the nucleotide in ``NUCLEOTIDES`` (0=A, 1=C, 2=G, 3=T).
+    '''
+    if not ts.has_reference_sequence():
+        raise ValueError("This tree sequence has no reference sequence.")
+    mut_id = mutation_at(ts, node, position, time)
+    if mut_id == tskit.NULL:
+        out = NUCLEOTIDES.index(ts.reference_sequence.data[int(position)])
+    else:
+        mut = ts.mutation(mut_id)
+        k = np.argmax([u["slim_time"] for u in mut.metadata["mutation_list"]])
+        out = mut.metadata["mutation_list"][k]["nucleotide"]
+    return out
diff --git a/pyslim/spatial.py b/pyslim/spatial.py
index 0fdd5b7a..fafd67ab 100644
--- a/pyslim/spatial.py
+++ b/pyslim/spatial.py
@@ -3,7 +3,7 @@
 import warnings
 import numpy as np
 
-from .slim_tree_sequence import *
+from .methods import *
 
 
 def _in_location_bin(locations, x0, x1, y0, y1):
@@ -125,7 +125,7 @@ def population_size(ts, x_bins, y_bins, time_bins, stage='late', remembered_stag
         birth_offset = 1
     else:
         birth_offset = 0
-    birth_times = ts.individual_times - birth_offset
+    birth_times = individual_times(ts) - birth_offset
     if (ts.metadata['SLiM']['model_type'] == "WF"
             or stage == remembered_stage):
         age_offset = 0
@@ -135,7 +135,7 @@ def population_size(ts, x_bins, y_bins, time_bins, stage='late', remembered_stag
             age_offset = -1
         else:
             age_offset = 1
-    ages = ts.individual_ages + age_offset
+    ages = individual_ages(ts) + age_offset
     death_times = birth_times - ages
 
     time_breaks = time_bins
@@ -147,7 +147,7 @@ def population_size(ts, x_bins, y_bins, time_bins, stage='late', remembered_stag
     ntbins = len(time_breaks) - 1
     popsize = np.empty((nxbins, nybins, ntbins))
 
-    locations = ts.individual_locations
+    locations = individual_locations(ts)
 
     for i in np.arange(nxbins):
         for j in np.arange(nybins):
diff --git a/pyslim/util.py b/pyslim/util.py
index 01d1edc3..e9e514b5 100644
--- a/pyslim/util.py
+++ b/pyslim/util.py
@@ -9,7 +9,7 @@ def unique_labels_by_group(group, label, minlength=0):
     return value will be of length at least ``minlength``.
 
     In other words, if the result is ``x``, then
-    ``x[j]`` is ``len(set(label[group == j])) == 1``.
+    ``x[j]`` is ``len(set(label[group == j])) <= 1``.
     '''
     w = label.astype("float64")
     n = np.bincount(1 + group, minlength=minlength + 1)
@@ -20,4 +20,4 @@ def unique_labels_by_group(group, label, minlength=0):
     w -= xm[1 + group]
     gw = np.bincount(1 + group, weights=np.abs(w), minlength=minlength + 1)[1:]
     # after subtracting groupwise means, should be all zero
-    return np.logical_and(n[1:] > 0, np.abs(gw) < 1e-7)
+    return np.abs(gw) < 1e-7
diff --git a/requirements/CI/conda-requirements.txt b/requirements/CI-conda/requirements.txt
similarity index 100%
rename from requirements/CI/conda-requirements.txt
rename to requirements/CI-conda/requirements.txt
diff --git a/requirements/CI-docs/requirements.txt b/requirements/CI-docs/requirements.txt
index 70a07339..497ecfc8 100644
--- a/requirements/CI-docs/requirements.txt
+++ b/requirements/CI-docs/requirements.txt
@@ -1,12 +1,11 @@
 setuptools-scm==4.1.2
-sphinx==3.3.1
-sphinx-argparse==0.2.5
-sphinx-issues==1.2.0
-jupyter-book==0.10.0
-kastore==0.3.1
+jupyter-book==0.12.3
+jupyter-sphinx==0.3.2
+sphinx==4.5.0
+sphinx-argparse==0.3.1
+sphinx-issues==3.0.1
 tskit==0.4.0
-msprime==1.1.0
-kastore==0.3.1
+msprime==1.1.1
 PyGithub==1.53
-matplotlib==3.3.0
+matplotlib==3.5.0
 pandas
diff --git a/requirements/CI-pip/requirements.txt b/requirements/CI-pip/requirements.txt
new file mode 100644
index 00000000..b4ea1050
--- /dev/null
+++ b/requirements/CI-pip/requirements.txt
@@ -0,0 +1,6 @@
+flake8==3.8.4
+pytest==6.2.5
+pytest-cov==2.10.1
+pytest-xdist==2.5.0
+numpy
+filelock
diff --git a/requirements/CI/pip-requirements.txt b/requirements/CI/pip-requirements.txt
deleted file mode 100644
index fa1fb4a5..00000000
--- a/requirements/CI/pip-requirements.txt
+++ /dev/null
@@ -1,17 +0,0 @@
-autodocsumm==0.1.13 #Pinned as errors on 0.2.0
-breathe==4.24.0
-flake8==3.8.4
-pytest==6.2.2
-pytest-cov==2.10.1
-pytest-xdist==2.1.0
-sphinx==3.3.1
-sphinx-argparse==0.2.5
-sphinx-issues==1.2.0
-sphinx_rtd_theme==0.5.0
-jupyter-book==0.10.0
-matplotlib==3.4.0
-xmlunittest==0.5.0
-numpy
-filelock
-kastore
-pandas
diff --git a/requirements/rtd.txt b/requirements/rtd.txt
deleted file mode 100644
index 9465cd2c..00000000
--- a/requirements/rtd.txt
+++ /dev/null
@@ -1,11 +0,0 @@
-autodocsumm==0.1.13 #Pinned as errors on 0.2.0
-breathe==4.24.0
-kastore
-numpy
-sphinx==3.3.1
-sphinx-argparse==0.2.5
-sphinx-issues==1.2.0
-sphinx_rtd_theme==0.5.0
-svgwrite==1.4
-tskit
-msprime


Test patch:
diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml
index 0303bc98..4182ccfe 100644
--- a/.github/workflows/tests.yml
+++ b/.github/workflows/tests.yml
@@ -33,14 +33,14 @@ jobs:
         uses: actions/cache@v2
         env:
           # Increase this to reset the cache if the key hasn't changed.
-          CACHE_NUM: 2
+          CACHE_NUM: 3
         with:
           path: |
             c:\Miniconda\envs\anaconda-client-env
             /usr/share/miniconda/envs/anaconda-client-env
             ~/osx-conda
             ~/.profile
-          key: ${{ runner.os }}-${{ matrix.python}}-conda-v${{ env.CACHE_NUM }}-${{ hashFiles('requirements/CI/pip-requirements.txt') }}-${{ hashFiles('requirements/CI/conda-requirements.txt') }}
+          key: ${{ runner.os }}-${{ matrix.python}}-conda-v${{ env.CACHE_NUM }}-${{ hashFiles('requirements/CI-pip/requirements.txt') }}-${{ hashFiles('requirements/CI-conda/requirements.txt') }}-${{ hashFiles('requirements/CI-docs/requirements.txt') }}
 
       - name: Install Conda
         uses: conda-incubator/setup-miniconda@v2
@@ -62,15 +62,18 @@ jobs:
         if: steps.cache.outputs.cache-hit != 'true'
         shell: bash -l {0} #We need a login shell to get conda
         run: |
-          if [ "$RUNNER_OS" != "Windows" ]; then
-            conda install --yes slim
-          fi
-          conda install --yes --file=requirements/CI/conda-requirements.txt
+          # TODO: uncomment when SLiM v4 is released
+          # if [ "$RUNNER_OS" != "Windows" ]; then
+          #   conda install --yes slim
+          # fi
+          conda install --yes --file=requirements/CI-conda/requirements.txt
 
       - name: Install pip deps
         if: steps.cache.outputs.cache-hit != 'true'
         shell: bash -l {0}
-        run: pip install -r requirements/CI/pip-requirements.txt
+        run: |
+          pip install -r requirements/CI-pip/requirements.txt
+          pip install -r requirements/CI-docs/requirements.txt
 
       - name: Fix OSX Cache Write #OSX Won't let the cache restore due to file perms
         if: steps.cache.outputs.cache-hit != 'true' && matrix.os == 'macos-latest'
@@ -84,20 +87,23 @@ jobs:
           sudo cp -r ~/osx-conda /usr/local/miniconda/envs/anaconda-client-env
 
       # Installing SLiM from conda now.
-      # - name: Build SLiM
-      #   run: |
-      #     git clone https://github.com/messerlab/SLiM.git
-      #     mkdir -p SLiM/Release
-      #     cd SLiM/Release
-      #     cmake -D CMAKE_BUILD_TYPE=Release ..
-      #     make -j 2
+      # TODO: comment this out again when SLiM v4 is released
+      - name: Build SLiM
+        run: |
+          git clone https://github.com/messerlab/SLiM.git
+          cd SLiM
+          git checkout -b multispecies origin/multispecies
+          mkdir -p Release
+          cd Release
+          cmake -D CMAKE_BUILD_TYPE=Release ..
+          make -j 2
 
       - name: Run tests
         run: |
           source ~/.profile
           conda activate anaconda-client-env
-          export PATH=$PATH:$PWD/SLiM/Release
-          pytest -xv -n2
+          export PATH=$PWD/SLiM/Release:$PATH
+          pytest -xv -n2 tests
 
       - name: Build docs
         run: |
diff --git a/tests/__init__.py b/tests/__init__.py
index fae42e97..9381aec8 100644
--- a/tests/__init__.py
+++ b/tests/__init__.py
@@ -193,11 +193,8 @@ def assertTableCollectionsEqual(self, t1, t2,
         # assert t1 == t2
 
     def do_recapitate(self, ts, *args, **kwargs):
-        if ts.metadata['SLiM']['model_type'] != "WF":
-            with warnings.catch_warnings():
-                warnings.simplefilter('ignore', msprime.TimeUnitsMismatchWarning)
-                recap = pyslim.recapitate(ts, *args, **kwargs)
-        else:
+        with warnings.catch_warnings():
+            warnings.simplefilter('ignore', msprime.TimeUnitsMismatchWarning)
             recap = pyslim.recapitate(ts, *args, **kwargs)
         return recap
 
diff --git a/tests/conftest.py b/tests/conftest.py
index 59095597..36e2308c 100644
--- a/tests/conftest.py
+++ b/tests/conftest.py
@@ -5,8 +5,8 @@
 import subprocess
 
 from filelock import FileLock
-import pyslim
-import msprime
+import json
+import tskit, msprime
 
 from .recipe_specs import recipe_specs
 
@@ -19,6 +19,14 @@ class Outfiles:
     """
     Outfile = namedtuple("Outfile", "path, slim_name, post_process, key")
 
+    @staticmethod
+    def read_dictionary(fname):
+        # reads in the json-written dictionary
+        assert os.path.isfile(fname)
+        with open(fname, "r") as f:
+            out = json.load(f)
+        return out
+
     @staticmethod
     def parse_pedigree_info(fname):
         # returns a dictionary whose keys are SLiM individual IDs, and whose values
@@ -50,11 +58,12 @@ def parse_pedigree_info(fname):
         return out
 
     def __init__(self, out_dir):
+        # Note: the 'key' below cannot match a 'key' in recipe_specs
         self._outfiles = [
             self.Outfile(
                 path=os.path.join(out_dir, "out.trees"),
                 slim_name="TREES_FILE",  # The var containing the path name for SLiM output
-                post_process=pyslim.load,  # function applied on path to create returned obj
+                post_process=tskit.load,  # function applied on path to create returned obj
                 key="ts",  # The key to use for the post-processed item in the returned dict
             ),
             self.Outfile(
@@ -63,6 +72,12 @@ def __init__(self, out_dir):
                 post_process=self.parse_pedigree_info,
                 key="info",
             ),
+            self.Outfile(
+                path=os.path.join(out_dir, "out_mutations.json"),
+                slim_name="MUTATIONS_FILE",
+                post_process=self.read_dictionary,
+                key="mutation_info",
+            ),
         ]
     def __getitem__(self, index):
         return self._outfiles[index]
@@ -75,6 +90,7 @@ def results(self):
         for o in self._outfiles:
             res["path"][o.key] = o.path
             if os.path.isfile(o.path):
+                assert o.key not in res
                 res[o.key] = o.post_process(o.path) 
         return res
     
@@ -97,7 +113,7 @@ def run_slim(recipe, out_dir, recipe_dir="test_recipes", **kwargs):
     for k in kwargs:
         x = kwargs[k]
         if x is not None:
-            if isinstance(x, str):
+            if isinstance(x, str) and x[:10] != 'Dictionary':
                 x = f"'{x}'"
             if isinstance(x, bool):
                 x = 'T' if x else 'F'
@@ -113,7 +129,7 @@ def run_slim(recipe, out_dir, recipe_dir="test_recipes", **kwargs):
 
 class HelperFunctions:
     @classmethod
-    def run_slim_restart(cls, in_ts, recipe, out_dir, **kwargs):
+    def run_slim_restart(cls, in_ts, recipe, out_dir, subpop_map=None, **kwargs):
         # Saves out the tree sequence to a trees file and run the SLiM recipe
         # on it, saving to files in out_dir.
         infile = os.path.join(out_dir, "in.trees")
@@ -121,6 +137,12 @@ def run_slim_restart(cls, in_ts, recipe, out_dir, **kwargs):
         kwargs['TREES_IN'] = infile
         if 'STAGE' not in kwargs:
             kwargs['STAGE'] = in_ts.metadata['SLiM']['stage']
+        if subpop_map is not None:
+            # subpopMap argument has entries like { "p1" : 2 },
+            # meaning that when loaded in, subpop p1 will be the second row
+            # in the population table
+            spm = "Dictionary(" + ", ".join([f"\"{a}\", {b}" for a, b in subpop_map.items()]) + ")"
+            kwargs['SUBPOP_MAP'] = spm
         results = run_slim(recipe, out_dir, **kwargs)
         return results["ts"]
 
@@ -130,7 +152,10 @@ def run_msprime_restart(cls, in_ts, out_dir, sex=None, WF=False):
             in_ts,
             "restart_msprime.slim",  # This is standard script defined in test_recipes
             out_dir,
-            WF=WF, SEX=sex, L=int(in_ts.sequence_length))
+            WF=WF,
+            SEX=sex,
+            L=int(in_ts.sequence_length)
+        )
         return out_ts
 
     @staticmethod
@@ -142,9 +167,11 @@ def get_msprime_examples():
         for n in [2, 20]:
             for recrate in [0.0, 0.01]:
                 ts = msprime.sim_ancestry(
-                        n, recombination_rate=recrate,
+                        n,
+                        recombination_rate=recrate,
                         population_size=10,
-                        sequence_length=200, random_seed=seed,
+                        sequence_length=200,
+                        random_seed=seed,
                         model="dtwf"
                 )
                 yield ts
@@ -168,7 +195,8 @@ def get_msprime_examples():
                         {"A": n, "B": n},
                         demography=demography,
                         recombination_rate=recrate,
-                        sequence_length=250, random_seed=seed,
+                        sequence_length=250,
+                        random_seed=seed,
                         model="dtwf"
                 )
                 yield ts
diff --git a/tests/extra_recipes/Recipe 17.1 - A minimal tree-seq model.txt b/tests/extra_recipes/Recipe 17.1 - A minimal tree-seq model.txt
index 13cc824d..0aece8cf 100644
--- a/tests/extra_recipes/Recipe 17.1 - A minimal tree-seq model.txt
+++ b/tests/extra_recipes/Recipe 17.1 - A minimal tree-seq model.txt
@@ -8,7 +8,7 @@ initialize() {
 	initializeGenomicElement(g1, 0, 1e8-1);
 	initializeRecombinationRate(1e-8);
 }
-1 {
+1 early() {
 	sim.addSubpop("p1", 500);
 }
 5000 late() {
diff --git a/tests/extra_recipes/Recipe 17.10 - Adding a neutral burn-in after simulation with recapitation I.txt b/tests/extra_recipes/Recipe 17.10 - Adding a neutral burn-in after simulation with recapitation I.txt
index ce703b02..78028ef7 100644
--- a/tests/extra_recipes/Recipe 17.10 - Adding a neutral burn-in after simulation with recapitation I.txt
+++ b/tests/extra_recipes/Recipe 17.10 - Adding a neutral burn-in after simulation with recapitation I.txt
@@ -18,7 +18,7 @@ initialize() {
 100:10000 late() {
 	mut = sim.mutationsOfType(m2);
 	if (mut.size() != 1)
-		stop(sim.generation + ": LOST");
+		stop(sim.cycle + ": LOST");
 	else if (sum(sim.mutationFrequencies(NULL, mut)) == 1.0)
 	{
 		sim.treeSeqOutput("recipe_17.10_decap.trees");
diff --git a/tests/extra_recipes/Recipe 17.10 - Adding a neutral burn-in after simulation with recapitation II.py b/tests/extra_recipes/Recipe 17.10 - Adding a neutral burn-in after simulation with recapitation II.py
index c9784e64..e5634f0f 100644
--- a/tests/extra_recipes/Recipe 17.10 - Adding a neutral burn-in after simulation with recapitation II.py
+++ b/tests/extra_recipes/Recipe 17.10 - Adding a neutral burn-in after simulation with recapitation II.py
@@ -5,14 +5,14 @@
 import matplotlib.pyplot as plt
 
 # Load the .trees file
-ts = pyslim.load("recipe_17.10_decap.trees")    # no simplify!
+ts = tskit.load("recipe_17.10_decap.trees")    # no simplify!
 
 # Calculate tree heights, giving uncoalesced sites the maximum time
 def tree_heights(ts):
     heights = np.zeros(ts.num_trees + 1)
     for tree in ts.trees():
         if tree.num_roots > 1:  # not fully coalesced
-            heights[tree.index] = ts.slim_generation
+            heights[tree.index] = ts.metadata['SLiM']['tick']
         else:
             children = tree.children(tree.root)
             real_root = tree.root if len(children) > 1 else children[0]
@@ -27,7 +27,7 @@ def tree_heights(ts):
 plt.show()
 
 # Recapitate!
-recap = ts.recapitate(recombination_rate=3e-10, Ne=1e5, random_seed=1)
+recap = pyslim.recapitate(ts, ancestral_Ne=1e5, recombination_rate=3e-10, random_seed=1)
 recap.dump("recipe_17.10_recap.trees")
 
 # Plot the tree heights after recapitation
diff --git a/tests/extra_recipes/Recipe 17.2 - Overlaying neutral mutations.py b/tests/extra_recipes/Recipe 17.2 - Overlaying neutral mutations.py
index 837137f2..61266fdb 100644
--- a/tests/extra_recipes/Recipe 17.2 - Overlaying neutral mutations.py
+++ b/tests/extra_recipes/Recipe 17.2 - Overlaying neutral mutations.py
@@ -2,9 +2,9 @@
 
 # This is a Python recipe, to be run after the section 17.1 recipe
 
-import msprime, pyslim
+import tskit, msprime
 
-ts = pyslim.load("./recipe_17.1.trees")
+ts = tskit.load("./recipe_17.1.trees")
 ts = ts.simplify()
 
 ## EDIT FOR TESTING
@@ -17,5 +17,5 @@
 
 assert asserted
 
-mutated = msprime.mutate(ts, rate=1e-7, random_seed=1, keep=True)
+mutated = msprime.sim_mutations(ts, rate=1e-7, random_seed=1, keep=True)
 mutated.dump("./recipe_17.1_overlaid.trees")
diff --git a/tests/extra_recipes/Recipe 17.3 - Simulation conditional upon fixation of a sweep, preserving ancestry I.txt b/tests/extra_recipes/Recipe 17.3 - Simulation conditional upon fixation of a sweep, preserving ancestry I.txt
index c2c5e2bf..1acf3776 100644
--- a/tests/extra_recipes/Recipe 17.3 - Simulation conditional upon fixation of a sweep, preserving ancestry I.txt
+++ b/tests/extra_recipes/Recipe 17.3 - Simulation conditional upon fixation of a sweep, preserving ancestry I.txt
@@ -9,7 +9,7 @@ initialize() {
 	initializeGenomicElement(g1, 0, 99999);
 	initializeRecombinationRate(1e-8);
 }
-1 {
+1 early() {
 	defineConstant("simID", getSeed());
 	sim.addSubpop("p1", 500);
 }
diff --git a/tests/extra_recipes/Recipe 17.3 - Simulation conditional upon fixation of a sweep, preserving ancestry II.txt b/tests/extra_recipes/Recipe 17.3 - Simulation conditional upon fixation of a sweep, preserving ancestry II.txt
index b7c62142..56967b05 100644
--- a/tests/extra_recipes/Recipe 17.3 - Simulation conditional upon fixation of a sweep, preserving ancestry II.txt
+++ b/tests/extra_recipes/Recipe 17.3 - Simulation conditional upon fixation of a sweep, preserving ancestry II.txt
@@ -9,7 +9,7 @@ initialize() {
 	initializeGenomicElement(g1, 0, 99999);
 	initializeRecombinationRate(1e-8);
 }
-1 {
+1 early() {
 	defineConstant("simID", getSeed());
 	sim.addSubpop("p1", 500);
 }
diff --git a/tests/extra_recipes/Recipe 17.4 - Detecting the dip in diversity (analyzing tree heights in Python) I.txt b/tests/extra_recipes/Recipe 17.4 - Detecting the dip in diversity (analyzing tree heights in Python) I.txt
index 775305f8..11428d44 100644
--- a/tests/extra_recipes/Recipe 17.4 - Detecting the dip in diversity (analyzing tree heights in Python) I.txt
+++ b/tests/extra_recipes/Recipe 17.4 - Detecting the dip in diversity (analyzing tree heights in Python) I.txt
@@ -14,9 +14,9 @@ initialize() {
 	for (start in seq(from=L0, to=L-(L0+L1), by=(L0+L1)))
 		initializeGenomicElement(g2, start, (start+L1)-1);
 }
-1 {
+1 early() {
 	sim.addSubpop("p1", N);
-	sim.rescheduleScriptBlock(s1, 10*N, 10*N);
+	community.rescheduleScriptBlock(s1, 10*N, 10*N);
 }
 s1 10 late() {
 	sim.treeSeqOutput("./recipe_17.4.trees");
diff --git a/tests/extra_recipes/Recipe 17.4 - Detecting the dip in diversity (analyzing tree heights in Python) II.py b/tests/extra_recipes/Recipe 17.4 - Detecting the dip in diversity (analyzing tree heights in Python) II.py
index 407e6dbc..c28e28b0 100644
--- a/tests/extra_recipes/Recipe 17.4 - Detecting the dip in diversity (analyzing tree heights in Python) II.py
+++ b/tests/extra_recipes/Recipe 17.4 - Detecting the dip in diversity (analyzing tree heights in Python) II.py
@@ -2,13 +2,13 @@
 
 # This is a Python recipe; note that it runs the SLiM model internally, below
 
-import subprocess, msprime, pyslim
+import subprocess, tskit
 import matplotlib.pyplot as plt
 import numpy as np
 
 # Run the SLiM model and load the resulting .trees
 # subprocess.check_output(["slim", "-m", "-s", "0", "./recipe_17.4.slim"])
-ts = pyslim.load("./recipe_17.4.trees")
+ts = tskit.load("./recipe_17.4.trees")
 ts = ts.simplify()
 
 # Measure the tree height at each base position
diff --git a/tests/extra_recipes/Recipe 17.5 - Mapping admixture (analyzing ancestry in Python) II.py b/tests/extra_recipes/Recipe 17.5 - Mapping admixture (analyzing ancestry in Python) II.py
index cfaa2672..4e7a6eb4 100644
--- a/tests/extra_recipes/Recipe 17.5 - Mapping admixture (analyzing ancestry in Python) II.py
+++ b/tests/extra_recipes/Recipe 17.5 - Mapping admixture (analyzing ancestry in Python) II.py
@@ -2,13 +2,13 @@
 
 # This is a Python recipe; note that it runs the SLiM model internally, below
 
-import subprocess, msprime, pyslim
+import subprocess, tskit
 import matplotlib.pyplot as plt
 import numpy as np
 
 # Run the SLiM model and load the resulting .trees file
 #subprocess.check_output(["slim", "-m", "-s", "0", "./recipe_17.5.slim"])
-ts = pyslim.load("./recipe_17.5.trees")
+ts = tskit.load("./recipe_17.5.trees")
 
 # Load the .trees file and assess true local ancestry
 breaks = np.zeros(ts.num_trees + 1)
diff --git a/tests/extra_recipes/Recipe 17.7 - Analyzing selection coefficients in Python with pyslim I.txt b/tests/extra_recipes/Recipe 17.7 - Analyzing selection coefficients in Python with tskit I.txt
similarity index 98%
rename from tests/extra_recipes/Recipe 17.7 - Analyzing selection coefficients in Python with pyslim I.txt
rename to tests/extra_recipes/Recipe 17.7 - Analyzing selection coefficients in Python with tskit I.txt
index 2c7b9905..90642c38 100644
--- a/tests/extra_recipes/Recipe 17.7 - Analyzing selection coefficients in Python with pyslim I.txt
+++ b/tests/extra_recipes/Recipe 17.7 - Analyzing selection coefficients in Python with tskit I.txt
@@ -9,7 +9,7 @@ initialize() {
 	initializeGenomicElement(g1, 0, 1e8-1);
 	initializeRecombinationRate(1e-8);
 }
-1 {
+1 early() {
 	sim.addSubpop("p1", 500);
 }
 20000 late() { sim.treeSeqOutput("./recipe_17.7.trees"); }
diff --git a/tests/extra_recipes/Recipe 17.7 - Analyzing selection coefficients in Python with pyslim II.py b/tests/extra_recipes/Recipe 17.7 - Analyzing selection coefficients in Python with tskit II.py
similarity index 89%
rename from tests/extra_recipes/Recipe 17.7 - Analyzing selection coefficients in Python with pyslim II.py
rename to tests/extra_recipes/Recipe 17.7 - Analyzing selection coefficients in Python with tskit II.py
index 33ea707d..095b11ea 100644
--- a/tests/extra_recipes/Recipe 17.7 - Analyzing selection coefficients in Python with pyslim II.py
+++ b/tests/extra_recipes/Recipe 17.7 - Analyzing selection coefficients in Python with tskit II.py
@@ -1,8 +1,8 @@
 # Keywords: Python, tree-sequence recording, tree sequence recording
 
-import msprime, pyslim
+import tskit
 
-ts = pyslim.load("recipe_17.7.trees")
+ts = tskit.load("recipe_17.7.trees")
 
 # selection coefficients of all selected mutations
 coeffs = []
diff --git a/tests/extra_recipes/Recipe 17.8 - Starting a hermaphroditic WF model with a coalescent history I.py b/tests/extra_recipes/Recipe 17.8 - Starting a hermaphroditic WF model with a coalescent history I.py
index dde43af6..f6db0c83 100644
--- a/tests/extra_recipes/Recipe 17.8 - Starting a hermaphroditic WF model with a coalescent history I.py
+++ b/tests/extra_recipes/Recipe 17.8 - Starting a hermaphroditic WF model with a coalescent history I.py
@@ -2,7 +2,7 @@
 
 import msprime, pyslim
 
-ts = msprime.simulate(sample_size=10000, Ne=5000, length=1e8,
-    mutation_rate=0.0, recombination_rate=1e-8)
-slim_ts = pyslim.annotate_defaults(ts, model_type="WF", slim_generation=1)
+ts = msprime.sim_ancestry(samples=5000, population_size=5000, sequence_length=1e8,
+    recombination_rate=1e-8)
+slim_ts = pyslim.annotate(ts, model_type="WF", tick=1)
 slim_ts.dump("recipe_17.8.trees")
diff --git a/tests/extra_recipes/Recipe 17.8 - Starting a hermaphroditic WF model with a coalescent history II.txt b/tests/extra_recipes/Recipe 17.8 - Starting a hermaphroditic WF model with a coalescent history II.txt
index fd4c82e9..15235964 100644
--- a/tests/extra_recipes/Recipe 17.8 - Starting a hermaphroditic WF model with a coalescent history II.txt
+++ b/tests/extra_recipes/Recipe 17.8 - Starting a hermaphroditic WF model with a coalescent history II.txt
@@ -24,7 +24,7 @@ initialize() {
 		sim.simulationFinished();
 	}
 }
-2000 { sim.simulationFinished(); }
+2000 early() { sim.simulationFinished(); }
 
 // Part III of this recipe, which is a Python script, may be found in
 // the Recipes archive downloadable at https://messerlab.org/slim/
diff --git a/tests/extra_recipes/Recipe 17.8 - Starting a hermaphroditic WF model with a coalescent history III.py b/tests/extra_recipes/Recipe 17.8 - Starting a hermaphroditic WF model with a coalescent history III.py
index 14dbb34a..22406b66 100644
--- a/tests/extra_recipes/Recipe 17.8 - Starting a hermaphroditic WF model with a coalescent history III.py
+++ b/tests/extra_recipes/Recipe 17.8 - Starting a hermaphroditic WF model with a coalescent history III.py
@@ -1,8 +1,8 @@
 # Keywords: Python, tree-sequence recording, tree sequence recording
 
-import msprime, pyslim
+import tskit
 
-ts = pyslim.load("recipe_17.8_II.trees").simplify()
+ts = tskit.load("recipe_17.8_II.trees").simplify()
 
 for tree in ts.trees():
     for root in tree.roots:
diff --git a/tests/extra_recipes/Recipe 17.9 - Starting a sexual nonWF model with a coalescent history I.py b/tests/extra_recipes/Recipe 17.9 - Starting a sexual nonWF model with a coalescent history I.py
index 95bc5d3e..fe256517 100644
--- a/tests/extra_recipes/Recipe 17.9 - Starting a sexual nonWF model with a coalescent history I.py
+++ b/tests/extra_recipes/Recipe 17.9 - Starting a sexual nonWF model with a coalescent history I.py
@@ -3,11 +3,10 @@
 import msprime, pyslim, random
 import numpy as np
 
-ts = msprime.simulate(sample_size=10000, Ne=5000, length=1e8,
-    mutation_rate=0.0, recombination_rate=1e-8)
+ts = msprime.sim_ancestry(samples=5000, population_size=5000, sequence_length=1e8, recombination_rate=1e-8)
 
 tables = ts.dump_tables()
-pyslim.annotate_defaults_tables(tables, model_type="nonWF", slim_generation=1)
+pyslim.annotate_tables(tables, model_type="nonWF", tick=1)
 
 # add sexes and ages
 individual_metadata = [ind.metadata for ind in tables.individuals]
@@ -29,7 +28,7 @@
               "mutation_type": 2,
               "selection_coeff": 0.1,
               "subpopulation": mut_node.population,
-              "slim_time": int(tables.metadata['SLiM']['generation'] - mut_node.time),
+              "slim_time": int(tables.metadata['SLiM']['tick'] - mut_node.time),
               "nucleotide": -1
             }
         ]
@@ -42,5 +41,5 @@
         time=mut_node.time,
         metadata=mut_metadata)
 
-slim_ts = pyslim.load_tables(tables)
+slim_ts = tables.tree_sequence()
 slim_ts.dump("recipe_17.9.trees")
diff --git a/tests/extra_recipes/Recipe 17.9 - Starting a sexual nonWF model with a coalescent history II.txt b/tests/extra_recipes/Recipe 17.9 - Starting a sexual nonWF model with a coalescent history II.txt
index 37099886..0f7cfcac 100644
--- a/tests/extra_recipes/Recipe 17.9 - Starting a sexual nonWF model with a coalescent history II.txt
+++ b/tests/extra_recipes/Recipe 17.9 - Starting a sexual nonWF model with a coalescent history II.txt
@@ -31,4 +31,4 @@ early() {
 		sim.simulationFinished();
 	}
 }
-2000 { sim.simulationFinished(); }
+2000 early() { sim.simulationFinished(); }
diff --git a/tests/extra_recipes/Recipe 18.13 - Tree-sequence recording and nucleotide-based models I.txt b/tests/extra_recipes/Recipe 18.13 - Tree-sequence recording and nucleotide-based models I.txt
index 2bc5b794..f35c4530 100644
--- a/tests/extra_recipes/Recipe 18.13 - Tree-sequence recording and nucleotide-based models I.txt
+++ b/tests/extra_recipes/Recipe 18.13 - Tree-sequence recording and nucleotide-based models I.txt
@@ -10,5 +10,5 @@ initialize() {
 	initializeGenomicElement(g1, 0, L-1);
 	initializeRecombinationRate(1e-6);
 }
-1 { sim.addSubpop("p1", 1000); }
-1000 { sim.treeSeqOutput("recipe_nucleotides.trees"); }
+1 early() { sim.addSubpop("p1", 1000); }
+1000 late() { sim.treeSeqOutput("recipe_nucleotides.trees"); }
diff --git a/tests/extra_recipes/Recipe 18.13 - Tree-sequence recording and nucleotide-based models II.py b/tests/extra_recipes/Recipe 18.13 - Tree-sequence recording and nucleotide-based models II.py
index c5c19398..6c6a8d4e 100644
--- a/tests/extra_recipes/Recipe 18.13 - Tree-sequence recording and nucleotide-based models II.py
+++ b/tests/extra_recipes/Recipe 18.13 - Tree-sequence recording and nucleotide-based models II.py
@@ -1,9 +1,9 @@
 # Keywords: Python, nucleotide-based, nucleotide sequence, sequence-based mutation rate
 
-import pyslim
+import tskit, pyslim
 import numpy as np
 
-ts = pyslim.load("recipe_nucleotides.trees")
+ts = tskit.load("recipe_nucleotides.trees")
 
 M = [[0 for _ in pyslim.NUCLEOTIDES] for _ in pyslim.NUCLEOTIDES]
 for mut in ts.mutations():
diff --git a/tests/extra_recipes/Recipe 18.13 - Tree-sequence recording and nucleotide-based models III.py b/tests/extra_recipes/Recipe 18.13 - Tree-sequence recording and nucleotide-based models III.py
index 308baf15..62171da2 100644
--- a/tests/extra_recipes/Recipe 18.13 - Tree-sequence recording and nucleotide-based models III.py
+++ b/tests/extra_recipes/Recipe 18.13 - Tree-sequence recording and nucleotide-based models III.py
@@ -1,10 +1,10 @@
 # Keywords: Python, nucleotide-based, nucleotide sequence, sequence-based mutation rate
 
-import pyslim
+import tskit, pyslim
 import numpy as np
 
-ts = pyslim.load("recipe_nucleotides.trees")
-slim_gen = ts.metadata["SLiM"]["generation"]
+ts = tskit.load("recipe_nucleotides.trees")
+slim_gen = ts.metadata["SLiM"]["tick"]
 
 M = np.zeros((4,4,4,4), dtype='int')
 for mut in ts.mutations():
@@ -14,17 +14,9 @@
         mut_list = mut.metadata["mutation_list"]
         k = np.argmax([u["slim_time"] for u in mut_list])
         derived_nuc = mut_list[k]["nucleotide"]
-        left_nuc = ts.nucleotide_at(mut.node, pos - 1, 
-                time = slim_gen - mut_list[k]["slim_time"] - 1.0)
-        right_nuc = ts.nucleotide_at(mut.node, pos + 1,
-                time = slim_gen - mut_list[k]["slim_time"] - 1.0)
-        if mut.parent == -1:
-            acgt = ts.reference_sequence.data[int(ts.site(mut.site).position)]
-            parent_nuc = pyslim.NUCLEOTIDES.index(acgt)
-        else:
-            parent_mut = ts.mutation(mut.parent)
-            assert(parent_mut.site == mut.site)
-            parent_nuc = parent_mut.metadata["mutation_list"][0]["nucleotide"]
+        left_nuc = pyslim.nucleotide_at(ts, mut.node, pos - 1, time = mut.time + 1.0)
+        right_nuc = pyslim.nucleotide_at(ts, mut.node, pos + 1, time = mut.time + 1.0)
+        parent_nuc = pyslim.nucleotide_at(ts, mut.node, pos, time = mut.time + 1.0)
         M[left_nuc, parent_nuc, right_nuc, derived_nuc] += 1
 
 print("{}\t{}\t{}".format('ancestral', 'derived', 'count'))
diff --git a/tests/extra_recipes/check_trees.py b/tests/extra_recipes/check_trees.py
index ce3c0e13..d157828e 100755
--- a/tests/extra_recipes/check_trees.py
+++ b/tests/extra_recipes/check_trees.py
@@ -1,10 +1,10 @@
 #!/usr/bin/env python3
 
 import sys
-import pyslim
+import tskit
 
 treefile = sys.argv[1] 
 
-ts = pyslim.load(treefile)
+ts = tskit.load(treefile)
 print(f"The file '{treefile}' has {ts.num_trees} trees relating {ts.num_individuals} individuals "
       f"with {ts.num_mutations} mutations at {ts.num_sites} sites.")
diff --git a/tests/recipe_specs.py b/tests/recipe_specs.py
index cdf5ff0a..29d9d734 100644
--- a/tests/recipe_specs.py
+++ b/tests/recipe_specs.py
@@ -5,6 +5,7 @@
 # possible attributes to simulation scripts are
 #  WF, nonWF
 #  nucleotides
+#  mutation_spectrum: writes out info file on mutation spectrum
 #  everyone: records everyone ever
 #  pedigree: writes out accompanying info file containing the pedigree
 #  remembered_early: remembering and saving the ts happens during early
@@ -12,9 +13,9 @@
 # All files are of the form `tests/test_recipes/{key}`
 recipe_specs = {
     "recipe_nonWF.slim":                       {"nonWF": True, "pedigree": True},
-    "recipe_long_nonWF.slim":                  {"nonWF": True},
+    "recipe_long_nonWF.slim":                  {"nonWF": True, "long": True},
     "recipe_WF.slim":                          {"WF": True, "pedigree": True},
-    "recipe_long_WF.slim":                     {"WF": True},
+    "recipe_long_WF.slim":                     {"WF": True, "long": True},
     "recipe_WF_migration.slim":                {"WF": True, "pedigree": True, "multipop": True},
     "recipe_nonWF_early_early.slim":           {"nonWF": True, "pedigree": True, "remembered_early": True},
     "recipe_nonWF_late_early.slim":            {"nonWF": True, "pedigree": True, "remembered_early": True, "begun_late": True},
@@ -27,8 +28,9 @@
     "recipe_nucleotides_WF.slim":              {"WF": True, "pedigree": True, "nucleotides": True},
     "recipe_nucleotides_nonWF.slim":           {"nonWF": True, "pedigree": True, "nucleotides": True},
     "recipe_nucleotides_plus_others.slim":     {"WF": True, "pedigree": True, "nucleotides": True, "non-nucleotides": True, "adds_mutations": True},
-    "recipe_long_nucleotides.slim":            {"WF": True, "nucleotides": True},
-    "recipe_roots.slim":                       {"WF": True, "pedigree": True},
+    "recipe_long_nucleotides.slim":            {"WF": True, "nucleotides": True, "long": True},
+    "recipe_mutation_spectrum.slim":           {"WF": True, "nucleotides": True, "mutation_spectrum": True},
+    "recipe_roots.slim":                       {"WF": True, "pedigree": True, "long": True},
     "recipe_nonWF_selfing.slim":               {"nonWF": True, "pedigree": True},
     "recipe_nonWF_nonstacked.slim":            {"nonWF": True, "nonstacked": True},
     "recipe_init_mutated_WF.slim":             {"WF": True, "init_mutated": True},
@@ -53,16 +55,12 @@ def recipe_eq(*keys, exclude=None):
     If key is empty, return all of them.
     If exclude is given exclude recipes with the specified keys.
     """
-    if exclude is None:
-        return (
-            k for k, v in recipe_specs.items()
-            if all(kk in v for kk in keys)
-        )
-    else:
-        return (
-            k for k, v in recipe_specs.items()
-            if (exclude not in v and all(kk in v for kk in keys))
-        )
+    if isinstance(exclude, str) or exclude is None:
+        exclude = [exclude]
+    return (
+        k for k, v in recipe_specs.items()
+        if (all(u not in v for u in exclude) and all(kk in v for kk in keys))
+    )
 
 # These SLiM scripts read in an existing trees file; the "input" gives a key in the
 # recipe_specs array that will produce a "ts" file suitable for input
@@ -71,6 +69,7 @@ def recipe_eq(*keys, exclude=None):
     "restart_nucleotides_nonWF.slim":   {"nonWF": True, "nucleotides": True, "no_op": True, "input": "recipe_nucleotides_nonWF.slim"},
     "restart_and_run_WF.slim":    {"WF": True, "input": "recipe_init_mutated_WF.slim"},
     "restart_and_run_nonWF.slim": {"nonWF": True, "input": "recipe_init_mutated_nonWF.slim"},
+    "restart_and_remove_subpop_nonWF.slim":    {"nonWF": True, "input": "recipe_init_mutated_nonWF.slim", "remove_subpop": True},
 }
 for t in ("WF", "nonWF"):
     # recipes that read in and write out immediately ("no_op")
diff --git a/tests/test_annotation.py b/tests/test_annotation.py
index b864501a..a480bc23 100644
--- a/tests/test_annotation.py
+++ b/tests/test_annotation.py
@@ -24,6 +24,7 @@ def verify_annotated_tables(self, ts1, ts2, check_alleles=True):
         Verify that the tables returned after annotation are equal, up to the
         expected forgetting of metadata, flags, and other things.
         '''
+        assert pyslim.is_current_version(ts2)
         tables1 = ts1.dump_tables()
         tables2 = ts2.dump_tables()
         # compare nodes
@@ -61,6 +62,7 @@ def verify_defaults(self, ts):
         '''
         Verify the default values have been entered into metadata.
         '''
+        do_pops = [False for _ in ts.populations()]
         for m in ts.mutations():
             md = m.metadata
             assert isinstance(md['mutation_list'], list)
@@ -75,6 +77,7 @@ def verify_defaults(self, ts):
             else:
                 assert md["is_null"] is False
                 assert md["genome_type"] == pyslim.GENOME_TYPE_AUTOSOME
+                do_pops[n.population] = True
         for ind in ts.individuals():
             md = ind.metadata
             assert np.array_equal(ind.location, [0, 0, 0])
@@ -84,18 +87,19 @@ def verify_defaults(self, ts):
             assert md["pedigree_p1"] == tskit.NULL
             assert md["pedigree_p2"] == tskit.NULL
         for pop in ts.populations():
-            md = pop.metadata
-            assert md["selfing_fraction"] == 0.0
-            assert md["female_cloning_fraction"] == 0.0
-            assert md["male_cloning_fraction"] == 0.0
-            assert md["sex_ratio"] == 0.0
-            assert md["bounds_x0"] == 0.0
-            assert md["bounds_x1"] == 1.0
-            assert md["bounds_y0"] == 0.0
-            assert md["bounds_y1"] == 1.0
-            assert md["bounds_z0"] == 0.0
-            assert md["bounds_z1"] == 1.0
-            assert len(md["migration_records"]) == 0
+            if do_pops[pop.id]:
+                md = pop.metadata
+                assert md["selfing_fraction"] == 0.0
+                assert md["female_cloning_fraction"] == 0.0
+                assert md["male_cloning_fraction"] == 0.0
+                assert md["sex_ratio"] == 0.0
+                assert md["bounds_x0"] == 0.0
+                assert md["bounds_x1"] == 1.0
+                assert md["bounds_y0"] == 0.0
+                assert md["bounds_y1"] == 1.0
+                assert md["bounds_z0"] == 0.0
+                assert md["bounds_z1"] == 1.0
+                assert len(md["migration_records"]) == 0
 
     def verify_provenance(self, ts):
         for u in ts.provenances():
@@ -115,74 +119,59 @@ def verify_slim_restart_equality(self, in_ts, out_ts):
     def test_annotate_errors(self, helper_functions):
         for ts in helper_functions.get_msprime_examples():
             with pytest.raises(ValueError):
-                _ = pyslim.annotate_defaults(ts, model_type="WF",
-                                             slim_generation=0)
+                _ = pyslim.annotate(ts, model_type="WF", tick=0)
             with pytest.raises(ValueError):
-                _ = pyslim.annotate_defaults(ts, model_type="WF",
-                                             slim_generation=4.4)
+                _ = pyslim.annotate(ts, model_type="WF", tick=4.4)
             with pytest.raises(ValueError):
-                _ = pyslim.annotate_defaults(ts, model_type="foo",
-                                             slim_generation=4)
+                _ = pyslim.annotate(ts, model_type="foo", tick=4)
             with pytest.raises(ValueError):
-                _ = pyslim.annotate_defaults(ts, model_type=[],
-                                             slim_generation=4)
-        # odd number of samples
-        ts = msprime.simulate(3)
+                _ = pyslim.annotate(ts, model_type=[], tick=4)
+        # no individuals
+        ts = msprime.simulate(4)
         with pytest.raises(ValueError) as except_info:
-            _ = pyslim.annotate_defaults(ts, model_type="WF",
-                                         slim_generation=1)
-        assert "diploid" in str(except_info)
-        # inconsistent populations for diploids
-        ts = msprime.simulate(
-            population_configurations=[
-                msprime.PopulationConfiguration(sample_size=3),
-                msprime.PopulationConfiguration(sample_size=1)],
-            migration_matrix=[[0.0, 1.0], [1.0, 0.0]]
-            )
-        with pytest.raises(ValueError) as except_info:
-            _ = pyslim.annotate_defaults(ts, model_type="WF",
-                                         slim_generation=1)
-        assert "more than one population" in str(except_info)
-        # inconsistent times for diploids
-        samples = [
-            msprime.Sample(population=0, time=0),
-            msprime.Sample(population=0, time=0),
-            msprime.Sample(population=0, time=0),
-            msprime.Sample(population=0, time=1),
-        ]
-        ts = msprime.simulate(samples=samples)
-        with pytest.raises(ValueError) as except_info:
-            _ = pyslim.annotate_defaults(ts, model_type="WF",
-                                         slim_generation=1)
-        assert "more than one time" in str(except_info)
+            _ = pyslim.annotate(ts, model_type="WF", tick=1)
+        assert "individuals" in str(except_info)
         # non-integer positions
-        ts = msprime.simulate(4, mutation_rate=10)
+        ts = msprime.sim_mutations(
+                msprime.sim_ancestry(4, random_seed=8),
+                rate=10,
+                discrete_genome=False,
+                random_seed=9,
+        )
         with pytest.raises(ValueError) as except_info:
-            _ = pyslim.annotate_defaults(ts, model_type="WF",
-                                         slim_generation=1)
+            _ = pyslim.annotate(ts, model_type="WF", tick=1)
         assert "not at integer" in str(except_info)
 
     def test_just_simulate(self, helper_functions, tmp_path):
-        ts = msprime.simulate(
-                sample_size=4, Ne=10, length=10,
-                mutation_rate=0.0, recombination_rate=0.01
+        ts = msprime.sim_ancestry(
+                4,
+                population_size=10,
+                sequence_length=10,
+                recombination_rate=0.01,
+                random_seed=100,
         )
-        ts = msprime.sim_mutations(ts, rate=0.1)
-        slim_ts = pyslim.annotate_defaults(ts, model_type="WF", slim_generation=1) 
+        ts = msprime.sim_mutations(ts, rate=0.1, random_seed=11)
+        slim_ts = pyslim.annotate(ts, model_type="WF", tick=1) 
         loaded_ts = helper_functions.run_msprime_restart(slim_ts, tmp_path, WF=True)
         self.verify_annotated_trees(ts, loaded_ts)
 
     def test_basic_annotation(self, helper_functions, tmp_path):
         for ts in helper_functions.get_msprime_examples():
             for do_mutations in [False, True]:
-                slim_gen = 4
-                slim_ts = pyslim.annotate_defaults(
+                tick = 4
+                cycle = 1
+                stage = "late"
+                slim_ts = pyslim.annotate(
                         ts, model_type="WF",
-                        slim_generation=slim_gen,
+                        tick=tick,
+                        cycle=cycle,
+                        stage=stage,
                         annotate_mutations=do_mutations,
                 )
                 assert slim_ts.metadata['SLiM']['model_type'] == 'WF'
-                assert slim_ts.metadata['SLiM']['generation'] == slim_gen
+                assert slim_ts.metadata['SLiM']['tick'] == tick
+                assert slim_ts.metadata['SLiM']['cycle'] == cycle
+                assert slim_ts.metadata['SLiM']['stage'] == stage
                 assert slim_ts.metadata['SLiM']['file_version'] == pyslim.slim_file_version
                 self.verify_annotated_tables(ts, slim_ts, check_alleles=(not do_mutations))
                 self.verify_annotated_trees(ts, slim_ts)
@@ -199,14 +188,15 @@ def test_basic_annotation(self, helper_functions, tmp_path):
     def test_annotate_refseq(self):
         ts = msprime.sim_ancestry(2, sequence_length=10, random_seed=77)
         refseq = "A" * int(ts.sequence_length)
-        ats = pyslim.annotate_defaults(
-                ts, model_type="nonWF", slim_generation=5, reference_sequence=refseq
+        ats = pyslim.annotate(
+                ts, model_type="nonWF", tick=5, reference_sequence=refseq
         )
         assert ats.reference_sequence.data == refseq
 
     def test_annotate_individuals(self, helper_functions, tmp_path):
+        # test the workflow of annotating defaults, then assigning sexes randomly
         for ts in helper_functions.get_msprime_examples():
-            slim_ts = pyslim.annotate_defaults(ts, model_type="nonWF", slim_generation=1)
+            slim_ts = pyslim.annotate(ts, model_type="nonWF", tick=1, stage="early")
             tables = slim_ts.dump_tables()
             top_md = tables.metadata
             top_md['SLiM']['separate_sexes'] = True
@@ -218,15 +208,17 @@ def test_annotate_individuals(self, helper_functions, tmp_path):
                 metadata[j]["sex"] = sexes[j]
             ims = tables.individuals.metadata_schema
             tables.individuals.packset_metadata(
-                    [ims.validate_and_encode_row(r) for r in metadata])
+                    [ims.validate_and_encode_row(r) for r in metadata]
+            )
             pop_metadata = [p.metadata for p in tables.populations]
             for j, md in enumerate(pop_metadata):
                 # nonWF models always have this
                 md['sex_ratio'] = 0.0
             pms = tables.populations.metadata_schema
             tables.populations.packset_metadata(
-                    [pms.validate_and_encode_row(r) for r in pop_metadata])
-            new_ts = pyslim.load_tables(tables)
+                    [pms.validate_and_encode_row(r) for r in pop_metadata]
+            )
+            new_ts = tables.tree_sequence()
             for j, ind in enumerate(new_ts.individuals()):
                 md = ind.metadata
                 assert md["sex"] == sexes[j]
@@ -296,19 +288,23 @@ def test_annotate_XY(self, helper_functions, tmp_path):
                 #         skip_provenance=-1, reordered_individuals=True)
 
     def test_annotate_nodes(self, helper_functions):
+        # test workflow of annotating defaults and then editing node metadata
         for ts in helper_functions.get_msprime_examples():
-            slim_ts = pyslim.annotate_defaults(ts, model_type="nonWF", slim_generation=1)
+            slim_ts = pyslim.annotate(ts, model_type="nonWF", tick=1)
             tables = slim_ts.dump_tables()
             metadata = [n.metadata for n in tables.nodes]
-            gtypes = [random.choice([pyslim.GENOME_TYPE_X, pyslim.GENOME_TYPE_Y])
-                      for _ in metadata]
+            gtypes = [
+                    random.choice([pyslim.GENOME_TYPE_X, pyslim.GENOME_TYPE_Y])
+                    for _ in metadata
+            ]
             for md, g in zip(metadata, gtypes):
                 if md is not None:
                     md["genome_type"] = g
             nms = tables.nodes.metadata_schema
             tables.nodes.packset_metadata(
-                    [nms.validate_and_encode_row(r) for r in metadata])
-            new_ts = pyslim.load_tables(tables)
+                    [nms.validate_and_encode_row(r) for r in metadata]
+            )
+            new_ts = tables.tree_sequence()
             for x, g in zip(new_ts.nodes(), gtypes):
                 if x.metadata is not None:
                     assert x.metadata["genome_type"] == g
@@ -316,7 +312,7 @@ def test_annotate_nodes(self, helper_functions):
 
     def test_annotate_mutations(self, helper_functions):
         for ts in helper_functions.get_msprime_examples():
-            slim_ts = pyslim.annotate_defaults(ts, model_type="nonWF", slim_generation=1)
+            slim_ts = pyslim.annotate(ts, model_type="nonWF", tick=1)
             tables = slim_ts.dump_tables()
             metadata = [m.metadata for m in tables.mutations]
             selcoefs = [random.uniform(0, 1) for _ in metadata]
@@ -324,58 +320,109 @@ def test_annotate_mutations(self, helper_functions):
                 metadata[j]['mutation_list'][0]["selection_coeff"] = selcoefs[j]
             ms = tables.mutations.metadata_schema
             tables.mutations.packset_metadata(
-                    [ms.validate_and_encode_row(r) for r in metadata])
-            new_ts = pyslim.load_tables(tables)
+                    [ms.validate_and_encode_row(r) for r in metadata]
+            )
+            new_ts = tables.tree_sequence()
             for j, x in enumerate(new_ts.mutations()):
                 md = x.metadata
                 assert np.isclose(md['mutation_list'][0]["selection_coeff"], selcoefs[j])
 
     def test_dont_annotate_mutations(self, helper_functions):
         # Test the option to not overwrite mutation annotations
-        ts = msprime.sim_ancestry(10)
+        ts = msprime.sim_ancestry(10, random_seed=5)
         ts = msprime.sim_mutations(ts, rate=5, random_seed=3)
         assert ts.num_mutations > 0
         tables = ts.dump_tables()
         pre_mutations = tables.mutations.copy()
-        pyslim.annotate_defaults_tables(tables, model_type="WF",
-                slim_generation=1, annotate_mutations=False)
+        pyslim.annotate_tables(
+                tables,
+                model_type="WF",
+                tick=1,
+                annotate_mutations=False
+        )
         # this is necessary because b'' actually is decoded to
         # an empty mutation_list by the schema
         pre_mutations.metadata_schema = tables.mutations.metadata_schema
         assert tables.mutations.equals(pre_mutations)
 
+    def test_no_populations_errors(self, helper_functions, tmp_path):
+        # test SLiM errors when individuals are in non-SLiM populations
+        ts = msprime.sim_ancestry(5, population_size=10, sequence_length=100, random_seed=456)
+        ts = pyslim.annotate(ts, model_type='nonWF', tick=1, stage="late")
+        t = ts.dump_tables()
+        t.populations.clear()
+        t.populations.add_row(metadata={})
+        ts = t.tree_sequence()
+        for p in ts.populations():
+            assert "slim_id" not in p.metadata
+        with pytest.raises(RuntimeError):
+            _ = helper_functions.run_slim_restart(
+                    ts,
+                    "restart_WF.slim",
+                    tmp_path,
+                    WF=True,
+            )
+
+    def test_empty_populations_errors(self, helper_functions, tmp_path):
+        # test SLiM errors on having empty populations in a WF model
+        ts = msprime.sim_ancestry(5, population_size=10, sequence_length=100, random_seed=455)
+        ts = pyslim.annotate(ts, model_type='WF', tick=1, stage="late")
+        t = ts.dump_tables()
+        for k in range(5):
+            md = pyslim.default_slim_metadata('population')
+            md['slim_id'] = k + 1
+            md['name'] = f"new_pop_num_{k}"
+            t.populations.add_row(metadata=md)
+        ts = t.tree_sequence()
+        for p in ts.populations():
+            assert "slim_id" in p.metadata
+        with pytest.raises(RuntimeError):
+            _ = helper_functions.run_slim_restart(
+                    ts,
+                    "restart_WF.slim",
+                    tmp_path,
+                    WF=True,
+            )
+
     def test_empty_populations(self, helper_functions, tmp_path):
-        # test SLiM doesn't error on having empty populations
+        # test SLiM doesn't error on having empty populations in a nonWF model
+        # however, it may rewrite the metadata
         ts = msprime.sim_ancestry(5, population_size=10, sequence_length=100, random_seed=455)
-        ts = pyslim.annotate_defaults(ts, model_type='WF', slim_generation=1)
+        ts = pyslim.annotate(ts, model_type='nonWF', tick=1)
         t = ts.dump_tables()
         for k in range(5):
             md = pyslim.default_slim_metadata('population')
+            md['slim_id'] = k + 1
             md['name'] = f"new_pop_num_{k}"
             md['description'] = f"the {k}-th added pop"
             t.populations.add_row(metadata=md)
         ts = t.tree_sequence()
         sts = helper_functions.run_slim_restart(
                 ts,
-                "restart_WF.slim",
+                "restart_nonWF.slim",
                 tmp_path,
-                WF=True,
+                WF=False,
         )
+        assert ts.num_populations == sts.num_populations
+        for p, q in zip(ts.populations(), sts.populations()):
+            assert p.metadata['name'] == q.metadata['name']
+            assert p.metadata['slim_id'] == q.metadata['slim_id']
 
     def test_many_populations(self, helper_functions, tmp_path):
-        # test we can add more than one population
+        # test we can add more than one population and that names are preserved
         ts = msprime.sim_ancestry(5, population_size=10, sequence_length=100, random_seed=455)
         t = ts.dump_tables()
-        for k in range(5):
+        for k in range(1, 6):
             md = pyslim.default_slim_metadata('population')
             md['name'] = f"new_pop_num_{k}"
             md['description'] = f"the {k}-th added pop"
+            md['slim_id'] = k
             t.populations.add_row(metadata=md)
             i = t.individuals.add_row()
             for _ in range(2):
                 t.nodes.add_row(flags=1, time=0.0, individual=i, population=k)
         ts = t.tree_sequence()
-        ts = pyslim.annotate_defaults(ts, model_type='WF', slim_generation=1)
+        ts = pyslim.annotate(ts, model_type='WF', tick=1)
         for ind in ts.individuals():
             assert ind.flags == pyslim.INDIVIDUAL_ALIVE
         sts = helper_functions.run_slim_restart(
@@ -384,6 +431,216 @@ def test_many_populations(self, helper_functions, tmp_path):
                 tmp_path,
                 WF=True,
         )
+        for k in range(1, 6):
+            md = sts.population(k).metadata
+            assert md['name'] == f"new_pop_num_{k}"
+
+    def test_keeps_extra_populations(self, helper_functions, tmp_path):
+        # test that non-SLiM metadata is not removed
+        d = msprime.Demography()
+        d.add_population(initial_size=10, name="A", extra_metadata={"pi": 3.1415})
+        md = pyslim.default_slim_metadata('population')
+        del md['name']
+        del md['description']
+        md['slim_id'] = 1
+        d.add_population(initial_size=10, name="B", extra_metadata=md)
+        d.add_population(initial_size=10, name="C", extra_metadata={"abc": 123})
+        ts = msprime.sim_ancestry(
+                samples={"B": 5},
+                demography=d,
+                sequence_length=100,
+                random_seed=455
+        )
+        ts = pyslim.annotate(ts, model_type='WF', tick=1)
+        sts = helper_functions.run_slim_restart(
+                ts,
+                "restart_WF.slim",
+                tmp_path,
+                WF=True,
+        )
+        assert sts.num_populations == 3
+        assert np.all(ts.tables.nodes.population == 1)
+        p = sts.population(0)
+        assert p.metadata['name'] == "A"
+        assert p.metadata['pi'] == 3.1415
+        p = sts.population(1)
+        assert p.metadata['name'] == "B"
+        p = sts.population(2)
+        assert p.metadata['name'] == "C"
+        assert p.metadata['abc'] == 123
+
+    def verify_remapping(self, ts, rts, subpop_map):
+        # below we assume the restart has not done additional simulation
+        do_remap = (subpop_map is not None)
+        if not do_remap:
+            subpop_map = { f"p{k}" : k for k in range(ts.num_populations) }
+        fwd_map = {-1 : -1}
+        rev_map = {-1 : -1}
+        for pk in subpop_map:
+            rts_k = int(pk[1:])
+            assert rts_k < rts.num_populations
+            ts_k = subpop_map[pk]
+            assert ts_k < ts.num_populations
+            fwd_map[ts_k] = rts_k
+            rev_map[rts_k] = ts_k
+
+        # all pops in ts should be present
+        for pop in ts.populations():
+            assert pop.id in fwd_map
+
+        # any extra pops in rts should have empty metadata
+        for pop in rts.populations():
+            if pop.id not in rev_map:
+                assert pop.metadata is None
+
+        for ts_k, rts_k in fwd_map.items():
+            if ts_k != -1:
+                ts_pop = ts.population(ts_k)
+                rts_pop = rts.population(rts_k)
+                ts_md = ts_pop.metadata
+                rts_md = rts_pop.metadata
+                if ts_md is None:
+                    assert rts_md is None
+                else:
+                    if do_remap and (
+                            ts_md['name'] == f"p{ts_pop.id}"
+                            or ts_md['name'] == f"pop_{ts_pop.id}"
+                    ):
+                        ts_md['name'] = f"p{rts_pop.id}"
+                    assert ts_md['name'] == rts_md['name']
+                    if "slim_id" not in ts_md:
+                        assert ts_md == rts_md
+                    else:
+                        assert ts_md["slim_id"] == ts_pop.id
+                        assert rts_md["slim_id"] == rts_pop.id
+                        k = "migration_records"
+                        assert (k in ts_md) == (k in rts_md)
+                        if k in ts_md:
+                            assert len(ts_md[k]) == len(rts_md[k])
+                            for x, y in zip(ts_md[k], rts_md[k]):
+                                assert x['migration_rate'] == y['migration_rate']
+                                assert fwd_map[x['source_subpop']] == y['source_subpop']
+
+        # check other tables have been remapped
+        # (uses the fact that no additional simulation has been done)
+        assert ts.num_nodes == rts.num_nodes
+        for n, rn in zip(ts.nodes(), rts.nodes()):
+            n.population = fwd_map[n.population]
+            assert n == rn
+
+        assert ts.num_migrations == rts.num_migrations
+        for m, rm in zip(ts.migrations(), rts.migrations()):
+            m.source = fwd_map[m.source]
+            m.dest = fwd_map[m.dest]
+            assert m == rm
+
+        assert ts.num_mutations == rts.num_mutations
+        for m, rm in zip(ts.mutations(), rts.mutations()):
+            md = m.metadata
+            rmd = rm.metadata
+            assert len(md['mutation_list']) == len(rmd['mutation_list'])
+            for x, y in zip(md['mutation_list'], rmd['mutation_list']):
+                x['subpopulation'] = fwd_map[x['subpopulation']]
+                assert x == y
+
+        assert ts.num_individuals == rts.num_individuals
+        for i, ri in zip(ts.individuals(), rts.individuals()):
+            md = i.metadata
+            rmd = ri.metadata
+            md['subpopulation'] = fwd_map[md['subpopulation']]
+            assert md == rmd
+
+
+    def test_remapping_errors(self, helper_functions, tmp_path):
+        ts = msprime.sim_ancestry(5, population_size=10, sequence_length=100, random_seed=455)
+        ts = pyslim.annotate(ts, model_type='nonWF', tick=1, stage="late")
+        # test can't map the same subpop to two different pops
+        with pytest.raises(RuntimeError):
+            _ = helper_functions.run_slim_restart(
+                    ts,
+                    "restart_WF.slim",
+                    tmp_path,
+                    WF=True,
+                    subpop_map = {
+                        "p0" : 0,
+                        "p1" : 0,
+                    }
+            )
+
+    def test_remapping(self, helper_functions, tmp_path):
+        d = msprime.Demography()
+        # pop 0
+        d.add_population(initial_size=10, name="A", extra_metadata={"pi": 3.1415})
+        # pop 1
+        md = pyslim.default_slim_metadata('population')
+        del md['name']
+        del md['description']
+        md['slim_id'] = 1
+        md['migration_records'] = [
+            {
+                "migration_rate" : 0.1,
+                "source_subpop" : 3,
+            }
+        ]
+        d.add_population(initial_size=10, name="pop_1", extra_metadata=md)
+        # pop 2
+        d.add_population(initial_size=10, name="pop_2", extra_metadata={"abc": 123})
+        # pop 3
+        md = pyslim.default_slim_metadata('population')
+        del md['name']
+        del md['description']
+        md['slim_id'] = 3
+        md['migration_records'] = [
+            {
+                "migration_rate" : 0.9,
+                "source_subpop" : 1,
+            }
+        ]
+        d.add_population(initial_size=10, name="D", extra_metadata=md)
+        d.add_population_split(time=1.0, derived=["pop_1", "D"], ancestral="A")
+        ts = msprime.sim_ancestry(
+                samples={"pop_1": 5, "D": 5},
+                demography=d,
+                sequence_length=100,
+                random_seed=455
+        )
+        ts = pyslim.annotate(ts, model_type='WF', tick=1)
+        ts = msprime.sim_mutations(
+                ts,
+                rate=1e-2,
+                random_seed=9,
+                model=msprime.SLiMMutationModel(type=1),
+        )
+        assert ts.num_mutations > 0
+        for subpop_map in (
+                None,
+                {"p0" : 0, "p1" : 1, "p2" : 2, "p3" : 3},
+                {"p0" : 1, "p1" : 0, "p2" : 2, "p3" : 3},
+                {"p5" : 0, "p0" : 1, "p7" : 2, "p2" : 3},
+        ):
+            rts = helper_functions.run_slim_restart(
+                    ts,
+                    "restart_WF.slim",
+                    tmp_path,
+                    WF=True,
+                    subpop_map = subpop_map,
+            )
+            self.verify_remapping(ts, rts, subpop_map)
+
+    @pytest.mark.parametrize(
+        'restart_name, recipe', restarted_recipe_eq("remove_subpop"), indirect=["recipe"])
+    def test_keep_slim_names(self, restart_name, recipe, helper_functions, tmp_path):
+        # test that SLiM retains names of SLim populations
+        # even when those are removed
+        in_ts = recipe["ts"]
+        # this recipe moves everyone to subpop 0
+        out_ts = helper_functions.run_slim_restart(in_ts, restart_name, tmp_path)
+        for n in out_ts.samples(time=0):
+            assert out_ts.node(n).population == 0
+        for j in range(1, in_ts.num_populations):
+            in_md = in_ts.population(j).metadata
+            out_md = out_ts.population(j).metadata
+            assert in_md['name'] == out_md['name']
 
     @pytest.mark.parametrize(
         'restart_name, recipe', restarted_recipe_eq("no_op"), indirect=["recipe"])
@@ -395,11 +652,25 @@ def test_reload_recapitate(
         # recapitate, reload
         in_ts = self.do_recapitate(ts, recombination_rate=1e-2, ancestral_Ne=10, random_seed=25)
         # put it through SLiM (which just reads in and writes out)
+        print("IN")
+        for p in in_ts.populations():
+            print(p)
         out_ts = helper_functions.run_slim_restart(in_ts, restart_name, tmp_path)
         # check for equality, in everything but the last provenance
-        in_ts.dump("in_ts.trees")
-        out_ts.dump("out_ts.trees")
         self.verify_slim_restart_equality(in_ts, out_ts)
+        # test for restarting with a subpop map
+        for j, k in [(1, 7), (5, 7)]:
+            subpop_map = {
+                    f"p{i * j % k}" : i
+                    for i in range(in_ts.num_populations)
+            }
+            out_ts = helper_functions.run_slim_restart(
+                    in_ts,
+                    restart_name,
+                    tmp_path,
+                    subpop_map=subpop_map
+            )
+            self.verify_remapping(in_ts, out_ts, subpop_map)
 
     @pytest.mark.parametrize(
         'restart_name, recipe', restarted_recipe_eq("no_op"), indirect=["recipe"])
@@ -413,8 +684,12 @@ def test_reload_annotate(
         has_nucleotides = tables.metadata['SLiM']['nucleotide_based']
         if has_nucleotides:
             nucs = [random.choice([0, 1, 2, 3]) for _ in metadata]
-            refseq = "".join(random.choices(pyslim.NUCLEOTIDES,
-                                            k = int(ts.sequence_length)))
+            refseq = "".join(
+                    random.choices(
+                        pyslim.NUCLEOTIDES,
+                        k = int(ts.sequence_length),
+                    ),
+            )
             for n, md in zip(nucs, metadata):
                 for m in md['mutation_list']:
                     m["nucleotide"] = n
@@ -424,8 +699,9 @@ def test_reload_annotate(
                 m["selection_coeff"] = random.random()
         ms = tables.mutations.metadata_schema
         tables.mutations.packset_metadata(
-                [ms.validate_and_encode_row(r) for r in metadata])
-        in_ts = pyslim.load_tables(tables)
+                [ms.validate_and_encode_row(r) for r in metadata]
+        )
+        in_ts = tables.tree_sequence()
         # put it through SLiM (which just reads in and writes out)
         out_ts = helper_functions.run_slim_restart(in_ts, restart_name, tmp_path)
         # check for equality, in everything but the last provenance
@@ -446,9 +722,7 @@ def test_load_without_provenance(
         in_tables = in_ts.dump_tables()
         in_tables.provenances.clear()
         in_tables.sort()
-        cleared_ts = pyslim.SlimTreeSequence(
-                in_tables.tree_sequence(),
-        )
+        cleared_ts = in_tables.tree_sequence()
         out_ts = helper_functions.run_slim_restart(cleared_ts, restart_name, tmp_path)
         out_tables = out_ts.dump_tables()
         out_tables.provenances.clear()
@@ -470,14 +744,18 @@ def test_reload_reference_sequence(
 
     @pytest.mark.parametrize(
         'restart_name, recipe', restarted_recipe_eq(), indirect=["recipe"])
-    def test_restarts_and_runs_spatial(
+    def test_restarts_and_runs_simplified(
         self, restart_name, recipe, helper_functions, tmp_path
     ):
         in_ts = recipe["ts"]
         n = set(in_ts.samples()[:4])
         for k in n:
             n |= set(in_ts.individual(in_ts.node(k).individual).nodes)
-        py_ts = in_ts.simplify(list(n))
-        out_ts = helper_functions.run_slim_restart(py_ts, restart_name, tmp_path)
-        assert out_ts.metadata["SLiM"]["generation"] >= in_ts.metadata["SLiM"]["generation"]
+        py_ts = in_ts.simplify(list(n), filter_populations=False)
+        out_ts = helper_functions.run_slim_restart(
+                py_ts,
+                restart_name,
+                tmp_path,
+        )
+        assert out_ts.metadata["SLiM"]["tick"] >= in_ts.metadata["SLiM"]["tick"]
 
diff --git a/tests/test_metadata.py b/tests/test_metadata.py
index 2650e48f..3bdfda19 100644
--- a/tests/test_metadata.py
+++ b/tests/test_metadata.py
@@ -90,9 +90,8 @@ def validate_slim_metadata(self, t):
     def validate_model_type(self, ts, model_type):
         assert ts.metadata['SLiM']['file_version'] == pyslim.slim_file_version
         assert ts.metadata['SLiM']['model_type'] == model_type
-        assert ts.metadata['SLiM']['generation'] > 0
-        assert ts.metadata['SLiM']['generation'] >= np.max(ts.tables.nodes.time)
-
+        assert ts.metadata['SLiM']['tick'] > 0
+        assert ts.metadata['SLiM']['tick'] >= np.max(ts.tables.nodes.time)
 
     @pytest.mark.parametrize('recipe', arbitrary_recipe, indirect=True)
     def test_set_tree_sequence_metadata_errors(self, recipe):
@@ -125,13 +124,17 @@ def test_set_tree_sequence_metadata_keeps(self, recipe):
                 assert tables.metadata[k] == dummy_metadata[k]
             self.validate_slim_metadata(tables)
             assert tables.metadata['SLiM']['model_type'] == "nonWF"
-            assert tables.metadata['SLiM']['generation'] == 0
+            assert tables.metadata['SLiM']['tick'] == 0
 
     @pytest.mark.parametrize('recipe', arbitrary_recipe, indirect=True)
     def test_set_tree_sequence_metadata(self, recipe):
         tables = recipe["ts"].dump_tables()
         pyslim.set_tree_sequence_metadata(
-                tables, "WF", 99,
+                tables,
+                "WF",
+                tick=99,
+                cycle=40,
+                stage="early",
                 spatial_dimensionality='xy',
                 spatial_periodicity='y',
                 separate_sexes=False,
@@ -139,14 +142,14 @@ def test_set_tree_sequence_metadata(self, recipe):
         )
         self.validate_slim_metadata(tables)
         assert tables.metadata['SLiM']['model_type'] == "WF"
-        assert tables.metadata['SLiM']['generation'] == 99
+        assert tables.metadata['SLiM']['tick'] == 99
+        assert tables.metadata['SLiM']['cycle'] == 40
+        assert tables.metadata['SLiM']['stage'] == 'early'
         assert tables.metadata['SLiM']['spatial_dimensionality'] == 'xy'
         assert tables.metadata['SLiM']['spatial_periodicity'] == 'y'
         assert tables.metadata['SLiM']['separate_sexes'] == False
         assert tables.metadata['SLiM']['nucleotide_based'] == True
 
-
-
     @pytest.mark.parametrize('recipe', recipe_eq("WF"), indirect=True)
     def test_WF_model_type(self, recipe):
         self.validate_model_type(recipe["ts"], "WF")
@@ -163,8 +166,14 @@ def test_recover_metadata(self, recipe):
         tables = ts.dump_tables()
         tables.metadata_schema = tskit.MetadataSchema(None)
         tables.metadata = b''
-        new_ts = pyslim.load_tables(tables)
-        assert new_ts.metadata == ts.metadata
+        pyslim.update_tables(tables)
+        md = tables.metadata
+        assert 'SLiM' in md
+        for k in ts.metadata['SLiM']:
+            assert k in md['SLiM']
+            # slim does not write out empty descriptions
+            if k != 'description' or ts.metadata['SLiM'][k] != "":
+                assert ts.metadata['SLiM'][k] == md['SLiM'][k]
 
     @pytest.mark.parametrize('recipe', recipe_eq("user_metadata"), indirect=True)
     def test_user_metadata(self, recipe):
@@ -189,83 +198,6 @@ def test_population_names(self, recipe):
         assert p.metadata['description'] == "i'm the other population"
 
 
-class TestDumpLoad(tests.PyslimTestCase):
-    '''
-    Test reading and writing.
-    '''
-
-    def verify_times(self, ts, slim_ts):
-        gen = slim_ts.metadata['SLiM']['generation']
-        assert ts.num_nodes == slim_ts.num_nodes
-        # verify internal consistency
-        for j in range(slim_ts.num_nodes):
-            assert slim_ts.node(j).time == slim_ts.tables.nodes.time[j]
-        # verify consistency between tree sequences
-        for n1, n2 in zip(ts.nodes(), slim_ts.nodes()):
-            assert n1.time == n2.time
-
-    def test_load_tables(self, recipe):
-        ts = recipe["ts"]
-        assert isinstance(ts, pyslim.SlimTreeSequence)
-        tables = ts.dump_tables()
-        new_ts = pyslim.load_tables(tables)
-        assert isinstance(new_ts, pyslim.SlimTreeSequence)
-        new_tables = new_ts.dump_tables()
-        assert tables == new_tables
-
-    def test_load(self, recipe):
-        fn = recipe["path"]["ts"]
-        # load in msprime then switch
-        msp_ts = tskit.load(fn)
-        assert isinstance(msp_ts, tskit.TreeSequence)
-        # transfer tables
-        msp_tables = msp_ts.dump_tables()
-        new_ts = pyslim.load_tables(msp_tables)
-        assert isinstance(new_ts, pyslim.SlimTreeSequence)
-        self.verify_times(msp_ts, new_ts)
-        new_tables = new_ts.dump_tables()
-        self.assertTableCollectionsEqual(msp_tables, new_tables)
-        # convert directly
-        new_ts = pyslim.SlimTreeSequence(msp_ts)
-        assert isinstance(new_ts, pyslim.SlimTreeSequence)
-        self.verify_times(msp_ts, new_ts)
-        new_tables = new_ts.dump_tables()
-        self.assertTableCollectionsEqual(msp_tables, new_tables)
-        # load to pyslim from file
-        slim_ts = pyslim.load(fn)
-        assert isinstance(slim_ts, pyslim.SlimTreeSequence)
-        slim_tables = slim_ts.dump_tables()
-        self.assertTableCollectionsEqual(msp_tables, slim_tables)
-        assert slim_ts.metadata['SLiM']['generation'] == new_ts.metadata['SLiM']['generation']
-
-    def test_dump_equality(self, recipe, tmp_path):
-        """
-        Test that we can dump a copy of the specified tree sequence
-        to the specified file, and load an identical copy.
-        """
-        tmp_file = os.path.join(tmp_path, "test_dump.trees")
-        ts = recipe["ts"]
-        ts.dump(tmp_file)
-        ts2 = pyslim.load(tmp_file)
-        assert ts.num_samples == ts2.num_samples
-        assert ts.sequence_length == ts2.sequence_length
-        assert ts.tables == ts2.dump_tables()
-        assert ts.has_reference_sequence() == ts2.has_reference_sequence()
-        if ts.has_reference_sequence():
-            assert ts.reference_sequence.data == ts2.reference_sequence.data
-
-    @pytest.mark.parametrize('recipe', [next(recipe_eq())], indirect=True)
-    def test_legacy_error(self, recipe, tmp_path):
-        tmp_file = os.path.join(tmp_path, "test_legacy.trees")
-        ts = recipe["ts"]
-        ts.dump(tmp_file)
-        with pytest.raises(ValueError, match="legacy metadata tools"):
-            _ = pyslim.load(tmp_file, legacy_metadata=True)
-        with pytest.raises(ValueError, match="legacy metadata tools"):
-            _ = pyslim.SlimTreeSequence(ts, legacy_metadata=True)
-
-
-
 class TestAlleles(tests.PyslimTestCase):
     '''
     Test nothing got messed up with haplotypes.
@@ -293,82 +225,3 @@ def test_nucleotides(self, recipe):
             for u in mut.metadata['mutation_list']:
                 assert u["nucleotide"] >= -1
                 assert u["nucleotide"] <= 3
-
-
-@pytest.mark.parametrize('recipe', [next(recipe_eq())], indirect=True)
-class TestMetadataAttributeError(tests.PyslimTestCase):
-    """
-    These are all only tested on a single recipe, the first grabbed from recipe
-    """
-
-    def test_population_error(self, recipe):
-        ts = recipe["ts"]
-        for x in ts.populations():
-            if x.metadata is not None:
-                with pytest.raises(AttributeError) as exec_info:
-                    _ = x.metadata.slim_id
-                assert 'legacy' in str(exec_info)
-                with pytest.raises(AttributeError) as exec_info:
-                    _ = x.metadata.selfing_fraction
-                assert 'legacy' in str(exec_info)
-                with pytest.raises(AttributeError) as exec_info:
-                    _ = x.metadata.sex_ratio
-                assert 'legacy' in str(exec_info)
-            with pytest.raises(AttributeError) as exec_info:
-                _ = x.metadata.ping
-            assert "has no attribute 'ping'" in str(exec_info)
-            break
-
-    def test_individual_error(self, recipe):
-        ts = recipe["ts"]
-        for x in ts.individuals():
-            with pytest.raises(AttributeError) as exec_info:
-                _ = x.metadata.pedigree_id
-            assert 'legacy' in str(exec_info)
-            with pytest.raises(AttributeError) as exec_info:
-                _ = x.metadata.age
-            assert 'legacy' in str(exec_info)
-            with pytest.raises(AttributeError) as exec_info:
-                _ = x.metadata.subpopulation
-            assert 'legacy' in str(exec_info)
-            with pytest.raises(AttributeError) as exec_info:
-                _ = x.metadata.sex
-            assert 'legacy' in str(exec_info)
-            with pytest.raises(AttributeError) as exec_info:
-                _ = x.metadata.flags
-            assert 'legacy' in str(exec_info)
-            with pytest.raises(AttributeError) as exec_info:
-                _ = x.metadata.pong
-            assert "has no attribute 'pong'" in str(exec_info)
-            break
-
-    def test_node_error(self, recipe):
-        ts = recipe["ts"]
-        for x in ts.nodes():
-            if x.metadata is not None:
-                with pytest.raises(AttributeError) as exec_info:
-                    _ = x.metadata.slim_id
-                assert 'legacy' in str(exec_info)
-                with pytest.raises(AttributeError) as exec_info:
-                    _ = x.metadata.is_null
-                assert 'legacy' in str(exec_info)
-                with pytest.raises(AttributeError) as exec_info:
-                    _ = x.metadata.genome_type
-                assert 'legacy' in str(exec_info)
-            with pytest.raises(AttributeError) as exec_info:
-                _ = x.metadata.pang
-            assert "has no attribute 'pang'" in str(exec_info)
-            break
-
-    def test_mutation_error(self, recipe):
-        ts = recipe["ts"]
-        for x in ts.mutations():
-            with pytest.raises(KeyError) as exec_info:
-                _ = x.metadata[0]
-            assert 'legacy' in str(exec_info)
-            with pytest.raises(KeyError) as exec_info:
-                _ = x.metadata[999]
-            assert 'legacy' in str(exec_info)
-            with pytest.raises(KeyError):
-                _ = x.metadata['ping']
-            break
diff --git a/tests/test_provenance.py b/tests/test_provenance.py
index 2b78348e..6f8d0a66 100644
--- a/tests/test_provenance.py
+++ b/tests/test_provenance.py
@@ -129,140 +129,60 @@ def get_0_1_slim_examples(self):
             os.path.join(self.script_dir, 'test_recipes', 'recipe_WF.v3.0.trees'),
             os.path.join(self.script_dir, 'test_recipes', 'recipe_nonWF.v3.0.trees'),
         ]:
-            with pytest.warns(Warning):
-                yield tskit.load(filename)
+            yield tskit.load(filename)
 
     def get_0_2_slim_examples(self):
         for filename in [
             os.path.join(self.script_dir, 'test_recipes', 'recipe_WF.v3.2.trees'),
             os.path.join(self.script_dir, 'test_recipes', 'recipe_nonWF.v3.2.trees'),
         ]:
-            with pytest.warns(Warning):
-                yield tskit.load(filename)
+            yield tskit.load(filename)
 
     def get_0_3_slim_examples(self):
         for filename in [
             os.path.join(self.script_dir, 'test_recipes', 'recipe_WF.v3.3.1.trees'),
             os.path.join(self.script_dir, 'test_recipes', 'recipe_nonWF.v3.3.1.trees'),
         ]:
-            with pytest.warns(Warning):
-                yield tskit.load(filename)
+            yield tskit.load(filename)
 
     def get_0_4_slim_examples(self):
         for filename in [
             os.path.join(self.script_dir, 'test_recipes', 'recipe_WF.v3.4.trees'),
             os.path.join(self.script_dir, 'test_recipes', 'recipe_nonWF.v3.4.trees'),
         ]:
-            with pytest.warns(Warning):
-                yield tskit.load(filename)
+            yield tskit.load(filename)
 
     def get_0_5_slim_examples(self):
         for filename in [
             os.path.join(self.script_dir, 'test_recipes', 'recipe_WF.v3.5.trees'),
             os.path.join(self.script_dir, 'test_recipes', 'recipe_nonWF.v3.5.trees'),
         ]:
-            with pytest.warns(Warning):
-                yield tskit.load(filename)
+            yield tskit.load(filename)
 
     def get_0_6_slim_examples(self):
         for filename in [
             os.path.join(self.script_dir, 'test_recipes', 'recipe_WF.v3.6.trees'),
             os.path.join(self.script_dir, 'test_recipes', 'recipe_nonWF.v3.6.trees'),
         ]:
-            with pytest.warns(Warning):
-                yield tskit.load(filename)
+            yield tskit.load(filename)
+
+    def get_0_7_slim_examples(self):
+        for filename in [
+            os.path.join(self.script_dir, 'test_recipes', 'recipe_WF.v3.7.trees'),
+            os.path.join(self.script_dir, 'test_recipes', 'recipe_nonWF.v3.7.trees'),
+        ]:
+            yield tskit.load(filename)
 
     def get_mixed_slim_examples(self):
         for filename in [
             os.path.join(self.script_dir, 'test_recipes', 'recipe_WF.v3.5_and_v3.6.trees'),
         ]:
-            with pytest.warns(Warning):
-                yield tskit.load(filename)
-
-    def test_get_provenance_errors(self):
-        ts = msprime.sim_ancestry(2, sequence_length=10, random_seed=144)
-        with pytest.raises(ValueError, match="not a SLiM tree sequence"):
-            _ = pyslim.get_provenance(ts)
-
-    @pytest.mark.parametrize("recipe", recipe_eq("WF"), indirect=True)    
-    def test_get_WF_provenance(self, recipe):
-        ts = recipe["ts"]
-        with pytest.warns(Warning):
-            prov = ts.slim_provenance
-        assert prov.model_type == "WF"
-        assert prov == pyslim.get_provenance(ts)
-        assert prov == pyslim.get_provenance(ts.tables)
-
-    @pytest.mark.parametrize("recipe", recipe_eq("nonWF"), indirect=True)    
-    def test_get_nonWF_provenance(self, recipe):
-        ts = recipe["ts"]
-        with pytest.warns(Warning):
-            prov = ts.slim_provenance
-        assert prov.model_type == "nonWF"
-        assert prov == pyslim.get_provenance(ts)
-        assert prov == pyslim.get_provenance(ts.tables)
-
-    @pytest.mark.parametrize("recipe", recipe_eq("WF"), indirect=True)    
-    def test_get_all_provenances(self, recipe, helper_functions, tmp_path):
-        ts = recipe["ts"]
-        rts = helper_functions.run_msprime_restart(ts, tmp_path, WF=True)
-        provenances = rts.slim_provenances
-        assert len(provenances) == 2
-        with pytest.warns(Warning):
-            last_prov = ts.slim_provenance
-        assert last_prov == provenances[0]
-        # mrts = pyslim.SlimTreeSequence(msprime.mutate(rts, rate=0.001))
-        j = 0
-        for p in rts.provenances():
-            is_slim, _ = pyslim.slim_provenance_version(p)
-            if is_slim:
-                sp = pyslim.parse_provenance(p)
-                assert provenances[j] == sp
-                j += 1
-            else:
-                with pytest.raises(ValueError):
-                    pyslim.parse_provenance(p)
+            yield tskit.load(filename)
 
     def test_provenance_creation(self):
         record = pyslim.make_pyslim_provenance_dict()
         tskit.provenance.validate_provenance(record)
 
-        record = pyslim.make_slim_provenance_dict("nonWF", 100)
-        tskit.provenance.validate_provenance(record)
-
-    def test_upgrade_provenance_errors(self):
-        ts = msprime.sim_ancestry(10)
-        # test bad input
-        with pytest.raises(ValueError):
-            pyslim.upgrade_slim_provenance(ts.tables)
-
-    def test_upgrade_provenance(self):
-        ts = msprime.sim_ancestry(10)
-        for record_text in old_provenance_examples:
-            record = json.loads(record_text)
-            prov = tskit.Provenance(id=0, timestamp='2018-08-25T14:59:13', record=json.dumps(record))
-            is_slim, version = pyslim.slim_provenance_version(prov)
-            assert is_slim
-            if 'file_version' in record:
-                assert version == "0.1"
-            else:
-                assert version == record['slim']['file_version']
-            tables = ts.dump_tables()
-            tables.provenances.add_row(json.dumps(record))
-            pyslim.upgrade_slim_provenance(tables) # modifies the tables
-            new_ts = tables.tree_sequence()
-            assert new_ts.num_provenances == 3
-            is_slim, version = pyslim.slim_provenance_version(new_ts.provenance(2))
-            assert is_slim
-            assert version == "0.4"
-            new_record = json.loads(new_ts.provenance(2).record)
-            if 'model_type' in record:
-                assert record['model_type'] == new_record['parameters']['model_type']
-                assert record['generation'] == new_record['slim']["generation"]
-            else:
-                assert record['parameters']['model_type'] == new_record['parameters']['model_type']
-                assert record['slim']['generation'] == new_record['slim']["generation"]
-
     def verify_upgrade(self, ts):
         # check that we have successfully got read-able metadata
         # (since this isn't tested on load)
@@ -279,7 +199,10 @@ def verify_upgrade(self, ts):
 
     def test_convert_0_1_files(self):
         for ts in self.get_0_1_slim_examples():
-            pts = pyslim.SlimTreeSequence(ts)
+            assert not pyslim.is_current_version(ts)
+            with pytest.warns(Warning):
+                pts = pyslim.update(ts)
+            assert pyslim.is_current_version(pts)
             self.verify_upgrade(pts)
             assert ts.num_provenances == 1
             assert pts.num_provenances == 2
@@ -288,7 +211,7 @@ def test_convert_0_1_files(self):
             assert isinstance(pts.metadata, dict)
             assert 'SLiM' in pts.metadata
             assert record['model_type'] == pts.metadata['SLiM']['model_type']
-            assert record['generation'] == pts.metadata['SLiM']["generation"]
+            assert record['generation'] == pts.metadata['SLiM']["tick"]
             assert list(ts.samples()) == list(pts.samples())
             assert np.array_equal(ts.tables.nodes.flags, pts.tables.nodes.flags)
             samples = list(ts.samples())
@@ -302,7 +225,10 @@ def test_convert_0_1_files(self):
 
     def test_convert_0_2_files(self):
         for ts in self.get_0_2_slim_examples():
-            pts = pyslim.SlimTreeSequence(ts)
+            assert not pyslim.is_current_version(ts)
+            with pytest.warns(Warning):
+                pts = pyslim.update(ts)
+            assert pyslim.is_current_version(pts)
             self.verify_upgrade(pts)
             assert ts.num_provenances == 1
             assert pts.num_provenances == 2
@@ -311,7 +237,7 @@ def test_convert_0_2_files(self):
             assert isinstance(pts.metadata, dict)
             assert 'SLiM' in pts.metadata
             assert record['parameters']['model_type'] == pts.metadata['SLiM']['model_type']
-            assert record['slim']['generation'] == pts.metadata['SLiM']["generation"]
+            assert record['slim']['generation'] == pts.metadata['SLiM']["tick"]
             assert list(ts.samples()) == list(pts.samples())
             assert np.array_equal(ts.tables.nodes.flags, pts.tables.nodes.flags)
             samples = list(ts.samples())
@@ -325,7 +251,10 @@ def test_convert_0_2_files(self):
 
     def test_convert_0_3_files(self):
         for ts in self.get_0_3_slim_examples():
-            pts = pyslim.SlimTreeSequence(ts)
+            assert not pyslim.is_current_version(ts)
+            with pytest.warns(Warning):
+                pts = pyslim.update(ts)
+            assert pyslim.is_current_version(pts)
             self.verify_upgrade(pts)
             assert ts.num_provenances == 1
             assert pts.num_provenances == 2
@@ -334,7 +263,7 @@ def test_convert_0_3_files(self):
             assert isinstance(pts.metadata, dict)
             assert 'SLiM' in pts.metadata
             assert record['parameters']['model_type'] == pts.metadata['SLiM']['model_type']
-            assert record['slim']['generation'] == pts.metadata['SLiM']["generation"]
+            assert record['slim']['generation'] == pts.metadata['SLiM']["tick"]
             assert list(ts.samples()) == list(pts.samples())
             assert np.array_equal(ts.tables.nodes.flags, pts.tables.nodes.flags)
             samples = list(ts.samples())
@@ -350,7 +279,10 @@ def test_convert_0_4_files(self):
         # Note that with version 0.5 and above, we *don't* get information from
         # provenance, we get it from top-level metadata
         for ts in self.get_0_4_slim_examples():
-            pts = pyslim.SlimTreeSequence(ts)
+            assert not pyslim.is_current_version(ts)
+            with pytest.warns(Warning):
+                pts = pyslim.update(ts)
+            assert pyslim.is_current_version(pts)
             self.verify_upgrade(pts)
             assert ts.num_provenances == 1
             assert pts.num_provenances == 2
@@ -359,7 +291,7 @@ def test_convert_0_4_files(self):
             assert isinstance(pts.metadata, dict)
             assert 'SLiM' in pts.metadata
             assert record['parameters']['model_type'] == pts.metadata['SLiM']['model_type']
-            assert record['slim']['generation'] == pts.metadata['SLiM']['generation']
+            assert record['slim']['generation'] == pts.metadata['SLiM']['tick']
             assert list(ts.samples()) == list(pts.samples())
             assert np.array_equal(ts.tables.nodes.flags, pts.tables.nodes.flags)
             samples = list(ts.samples())
@@ -373,7 +305,10 @@ def test_convert_0_4_files(self):
 
     def test_convert_0_5_files(self):
         for ts in self.get_0_5_slim_examples():
-            pts = pyslim.SlimTreeSequence(ts)
+            assert not pyslim.is_current_version(ts)
+            with pytest.warns(Warning):
+                pts = pyslim.update(ts)
+            assert pyslim.is_current_version(pts)
             self.verify_upgrade(pts)
             assert ts.num_provenances == 1
             assert pts.num_provenances == 2
@@ -382,7 +317,7 @@ def test_convert_0_5_files(self):
             assert isinstance(pts.metadata, dict)
             assert 'SLiM' in pts.metadata
             assert record['parameters']['model_type'] == pts.metadata['SLiM']['model_type']
-            assert record['slim']['generation'] == pts.metadata['SLiM']['generation']
+            assert record['slim']['generation'] == pts.metadata['SLiM']['tick']
             assert list(ts.samples()) == list(pts.samples())
             assert np.array_equal(ts.tables.nodes.flags, pts.tables.nodes.flags)
             samples = list(ts.samples())
@@ -396,7 +331,10 @@ def test_convert_0_5_files(self):
 
     def test_convert_0_6_files(self):
         for ts in self.get_0_6_slim_examples():
-            pts = pyslim.SlimTreeSequence(ts)
+            assert not pyslim.is_current_version(ts)
+            with pytest.warns(Warning):
+                pts = pyslim.update(ts)
+            assert pyslim.is_current_version(pts)
             self.verify_upgrade(pts)
             assert ts.num_provenances == 1
             assert pts.num_provenances == 2
@@ -405,7 +343,7 @@ def test_convert_0_6_files(self):
             assert isinstance(pts.metadata, dict)
             assert 'SLiM' in pts.metadata
             assert record['parameters']['model_type'] == pts.metadata['SLiM']['model_type']
-            assert record['slim']['generation'] == pts.metadata['SLiM']['generation']
+            assert record['slim']['generation'] == pts.metadata['SLiM']['tick']
             assert list(ts.samples()) == list(pts.samples())
             assert np.array_equal(ts.tables.nodes.flags, pts.tables.nodes.flags)
             samples = list(ts.samples())
@@ -417,9 +355,38 @@ def test_convert_0_6_files(self):
                 if t.parent(u) != tskit.NULL:
                     assert t.branch_length(u) == pt.branch_length(u)
 
-    def test_convert_mixd_files(self):
+    def test_convert_0_7_files(self):
+        for ts in self.get_0_7_slim_examples():
+            assert not pyslim.is_current_version(ts)
+            with pytest.warns(Warning):
+                pts = pyslim.update(ts)
+            assert pyslim.is_current_version(pts)
+            self.verify_upgrade(pts)
+            assert ts.num_provenances == 1
+            assert pts.num_provenances == 2
+            assert ts.provenance(0).record == pts.provenance(0).record
+            record = json.loads(ts.provenance(0).record)
+            assert isinstance(pts.metadata, dict)
+            assert 'SLiM' in pts.metadata
+            assert record['parameters']['model_type'] == pts.metadata['SLiM']['model_type']
+            assert record['slim']['generation'] == pts.metadata['SLiM']['tick']
+            assert list(ts.samples()) == list(pts.samples())
+            assert np.array_equal(ts.tables.nodes.flags, pts.tables.nodes.flags)
+            samples = list(ts.samples())
+            t = ts.first()
+            pt = pts.first()
+            for _ in range(20):
+                u = random.sample(samples, 1)[0]
+                assert t.parent(u) == pt.parent(u)
+                if t.parent(u) != tskit.NULL:
+                    assert t.branch_length(u) == pt.branch_length(u)
+
+    def test_convert_mixed_files(self):
         for ts in self.get_mixed_slim_examples():
-            pts = pyslim.SlimTreeSequence(ts)
+            assert not pyslim.is_current_version(ts)
+            with pytest.warns(Warning):
+                pts = pyslim.update(ts)
+            assert pyslim.is_current_version(pts)
             self.verify_upgrade(pts)
             assert ts.num_provenances == 1
             assert pts.num_provenances == 2
@@ -428,7 +395,7 @@ def test_convert_mixd_files(self):
             assert isinstance(pts.metadata, dict)
             assert 'SLiM' in pts.metadata
             assert record['parameters']['model_type'] == pts.metadata['SLiM']['model_type']
-            assert record['slim']['generation'] == pts.metadata['SLiM']['generation']
+            assert record['slim']['generation'] == pts.metadata['SLiM']['tick']
             assert list(ts.samples()) == list(pts.samples())
             assert np.array_equal(ts.tables.nodes.flags, pts.tables.nodes.flags)
             samples = list(ts.samples())
@@ -439,3 +406,12 @@ def test_convert_mixd_files(self):
                 assert t.parent(u) == pt.parent(u)
                 if t.parent(u) != tskit.NULL:
                     assert t.branch_length(u) == pt.branch_length(u)
+
+    def test_file_warnings(self):
+        for ts in self.get_0_6_slim_examples():
+            with pytest.warns(Warning, match="pyslim.update"):
+                with pytest.raises(KeyError, match="tick"):
+                    _ = pyslim.recapitate(ts, ancestral_Ne=10)
+            with pytest.warns(Warning, match="pyslim.update"):
+                with pytest.raises(KeyError, match="tick"):
+                    _ = pyslim.slim_time(ts, 0)
diff --git a/tests/test_recipes/make_v3_tests.sh b/tests/test_recipes/make_v3_tests.sh
index 24e8e502..2933ca31 100644
--- a/tests/test_recipes/make_v3_tests.sh
+++ b/tests/test_recipes/make_v3_tests.sh
@@ -68,3 +68,16 @@ git add -f recipe_nonWF.${TAG}.trees recipe_WF.${TAG}.trees
 # To make the "mixed" files:
 
 python3 make_v3_test_additions.py
+
+# To make the v3.7 files:
+
+TAG=v3.7
+git checkout $TAG
+mkdir -p build_$TAG && cd build_$TAG
+cmake .. && make
+SLIMDIR=$(pwd)
+cd ../..
+$SLIMDIR/slim recipe_nonWF.slim && mv out.trees recipe_nonWF.${TAG}.trees
+$SLIMDIR/slim recipe_WF.slim && mv out.trees recipe_WF.${TAG}.trees
+git add -f recipe_nonWF.${TAG}.trees recipe_WF.${TAG}.trees
+
diff --git a/tests/test_recipes/recipe_WF.slim b/tests/test_recipes/recipe_WF.slim
index 3136ecd9..75431155 100644
--- a/tests/test_recipes/recipe_WF.slim
+++ b/tests/test_recipes/recipe_WF.slim
@@ -32,9 +32,9 @@ initialize()
 1: early() {
     for (pop in sim.subpopulations) {
         for (ind in pop.individuals) {
-            age = sim.modelType == "nonWF" ? ind.age else -1;
+            age = community.modelType == "nonWF" ? ind.age else -1;
             writeFile(PEDIGREE_FILE,
-             paste(c(sim.generation, 'early', ind.pedigreeID, age, ind.pedigreeParentIDs),
+             paste(c(sim.cycle, 'early', ind.pedigreeID, age, ind.pedigreeParentIDs),
                    sep='\t'),
              append=T);
         }
@@ -44,9 +44,9 @@ initialize()
 1: late() {
     for (pop in sim.subpopulations) {
         for (ind in pop.individuals) {
-            age = sim.modelType == "nonWF" ? ind.age else -1;
+            age = community.modelType == "nonWF" ? ind.age else -1;
             writeFile(PEDIGREE_FILE,
-             paste(c(sim.generation, 'late', ind.pedigreeID, age, ind.pedigreeParentIDs),
+             paste(c(sim.cycle, 'late', ind.pedigreeID, age, ind.pedigreeParentIDs),
                    sep='\t'),
              append=T);
         }
diff --git a/tests/test_recipes/recipe_WF.v3.7.trees b/tests/test_recipes/recipe_WF.v3.7.trees
new file mode 100644
index 00000000..b1fb9b90
Binary files /dev/null and b/tests/test_recipes/recipe_WF.v3.7.trees differ
diff --git a/tests/test_recipes/recipe_WF_early_early.slim b/tests/test_recipes/recipe_WF_early_early.slim
index 78c7e7e8..8fff6818 100644
--- a/tests/test_recipes/recipe_WF_early_early.slim
+++ b/tests/test_recipes/recipe_WF_early_early.slim
@@ -32,9 +32,9 @@ initialize()
 1: early() {
     for (pop in sim.subpopulations) {
         for (ind in pop.individuals) {
-            age = sim.modelType == "nonWF" ? ind.age else -1;
+            age = community.modelType == "nonWF" ? ind.age else -1;
             writeFile(PEDIGREE_FILE,
-             paste(c(sim.generation, 'early', ind.pedigreeID, age, ind.pedigreeParentIDs),
+             paste(c(sim.cycle, 'early', ind.pedigreeID, age, ind.pedigreeParentIDs),
                    sep='\t'),
              append=T);
         }
@@ -44,9 +44,9 @@ initialize()
 1: late() {
     for (pop in sim.subpopulations) {
         for (ind in pop.individuals) {
-            age = sim.modelType == "nonWF" ? ind.age else -1;
+            age = community.modelType == "nonWF" ? ind.age else -1;
             writeFile(PEDIGREE_FILE,
-             paste(c(sim.generation, 'late', ind.pedigreeID, age, ind.pedigreeParentIDs),
+             paste(c(sim.cycle, 'late', ind.pedigreeID, age, ind.pedigreeParentIDs),
                    sep='\t'),
              append=T);
         }
diff --git a/tests/test_recipes/recipe_WF_early_late.slim b/tests/test_recipes/recipe_WF_early_late.slim
index 7e34a6f9..2a815de3 100644
--- a/tests/test_recipes/recipe_WF_early_late.slim
+++ b/tests/test_recipes/recipe_WF_early_late.slim
@@ -32,9 +32,9 @@ initialize()
 1: early() {
     for (pop in sim.subpopulations) {
         for (ind in pop.individuals) {
-            age = sim.modelType == "nonWF" ? ind.age else -1;
+            age = community.modelType == "nonWF" ? ind.age else -1;
             writeFile(PEDIGREE_FILE,
-             paste(c(sim.generation, 'early', ind.pedigreeID, age, ind.pedigreeParentIDs),
+             paste(c(sim.cycle, 'early', ind.pedigreeID, age, ind.pedigreeParentIDs),
                    sep='\t'),
              append=T);
         }
@@ -44,9 +44,9 @@ initialize()
 1: late() {
     for (pop in sim.subpopulations) {
         for (ind in pop.individuals) {
-            age = sim.modelType == "nonWF" ? ind.age else -1;
+            age = community.modelType == "nonWF" ? ind.age else -1;
             writeFile(PEDIGREE_FILE,
-             paste(c(sim.generation, 'late', ind.pedigreeID, age, ind.pedigreeParentIDs),
+             paste(c(sim.cycle, 'late', ind.pedigreeID, age, ind.pedigreeParentIDs),
                    sep='\t'),
              append=T);
         }
diff --git a/tests/test_recipes/recipe_WF_late_early.slim b/tests/test_recipes/recipe_WF_late_early.slim
index 7633cd9e..944150d4 100644
--- a/tests/test_recipes/recipe_WF_late_early.slim
+++ b/tests/test_recipes/recipe_WF_late_early.slim
@@ -32,9 +32,9 @@ initialize()
 1: early() {
     for (pop in sim.subpopulations) {
         for (ind in pop.individuals) {
-            age = sim.modelType == "nonWF" ? ind.age else -1;
+            age = community.modelType == "nonWF" ? ind.age else -1;
             writeFile(PEDIGREE_FILE,
-             paste(c(sim.generation, 'early', ind.pedigreeID, age, ind.pedigreeParentIDs),
+             paste(c(sim.cycle, 'early', ind.pedigreeID, age, ind.pedigreeParentIDs),
                    sep='\t'),
              append=T);
         }
@@ -44,9 +44,9 @@ initialize()
 1: late() {
     for (pop in sim.subpopulations) {
         for (ind in pop.individuals) {
-            age = sim.modelType == "nonWF" ? ind.age else -1;
+            age = community.modelType == "nonWF" ? ind.age else -1;
             writeFile(PEDIGREE_FILE,
-             paste(c(sim.generation, 'late', ind.pedigreeID, age, ind.pedigreeParentIDs),
+             paste(c(sim.cycle, 'late', ind.pedigreeID, age, ind.pedigreeParentIDs),
                    sep='\t'),
              append=T);
         }
diff --git a/tests/test_recipes/recipe_WF_late_late.slim b/tests/test_recipes/recipe_WF_late_late.slim
index aed8cea1..9b9d0b0b 100644
--- a/tests/test_recipes/recipe_WF_late_late.slim
+++ b/tests/test_recipes/recipe_WF_late_late.slim
@@ -32,9 +32,9 @@ initialize()
 1: early() {
     for (pop in sim.subpopulations) {
         for (ind in pop.individuals) {
-            age = sim.modelType == "nonWF" ? ind.age else -1;
+            age = community.modelType == "nonWF" ? ind.age else -1;
             writeFile(PEDIGREE_FILE,
-             paste(c(sim.generation, 'early', ind.pedigreeID, age, ind.pedigreeParentIDs),
+             paste(c(sim.cycle, 'early', ind.pedigreeID, age, ind.pedigreeParentIDs),
                    sep='\t'),
              append=T);
         }
@@ -44,9 +44,9 @@ initialize()
 1: late() {
     for (pop in sim.subpopulations) {
         for (ind in pop.individuals) {
-            age = sim.modelType == "nonWF" ? ind.age else -1;
+            age = community.modelType == "nonWF" ? ind.age else -1;
             writeFile(PEDIGREE_FILE,
-             paste(c(sim.generation, 'late', ind.pedigreeID, age, ind.pedigreeParentIDs),
+             paste(c(sim.cycle, 'late', ind.pedigreeID, age, ind.pedigreeParentIDs),
                    sep='\t'),
              append=T);
         }
diff --git a/tests/test_recipes/recipe_WF_migration.slim b/tests/test_recipes/recipe_WF_migration.slim
index fb769d19..2ac1f30b 100644
--- a/tests/test_recipes/recipe_WF_migration.slim
+++ b/tests/test_recipes/recipe_WF_migration.slim
@@ -35,9 +35,9 @@ initialize()
 1: early() {
     for (pop in sim.subpopulations) {
         for (ind in pop.individuals) {
-            age = sim.modelType == "nonWF" ? ind.age else -1;
+            age = community.modelType == "nonWF" ? ind.age else -1;
             writeFile(PEDIGREE_FILE,
-             paste(c(sim.generation, 'early', ind.pedigreeID, age, ind.pedigreeParentIDs),
+             paste(c(sim.cycle, 'early', ind.pedigreeID, age, ind.pedigreeParentIDs),
                    sep='\t'),
              append=T);
         }
@@ -47,9 +47,9 @@ initialize()
 1: late() {
     for (pop in sim.subpopulations) {
         for (ind in pop.individuals) {
-            age = sim.modelType == "nonWF" ? ind.age else -1;
+            age = community.modelType == "nonWF" ? ind.age else -1;
             writeFile(PEDIGREE_FILE,
-             paste(c(sim.generation, 'late', ind.pedigreeID, age, ind.pedigreeParentIDs),
+             paste(c(sim.cycle, 'late', ind.pedigreeID, age, ind.pedigreeParentIDs),
                    sep='\t'),
              append=T);
         }
diff --git a/tests/test_recipes/recipe_long_nucleotides.slim b/tests/test_recipes/recipe_long_nucleotides.slim
index 09eb2628..99f7b31f 100644
--- a/tests/test_recipes/recipe_long_nucleotides.slim
+++ b/tests/test_recipes/recipe_long_nucleotides.slim
@@ -13,7 +13,7 @@ initialize() {
 	initializeRecombinationRate(1e-2);
 }
 
-1 {
+1 early() {
 	sim.addSubpop("p1", 10);
 }
 
diff --git a/tests/test_recipes/recipe_mutation_spectrum.slim b/tests/test_recipes/recipe_mutation_spectrum.slim
new file mode 100644
index 00000000..10c9d4fc
--- /dev/null
+++ b/tests/test_recipes/recipe_mutation_spectrum.slim
@@ -0,0 +1,64 @@
+initialize()
+{
+	setSeed(23);
+	defineConstant("L", 100);
+	if (!exists("TREES_FILE"))
+		defineGlobal("TREES_FILE", "out.trees");
+	if (!exists("MUTATIONS_FILE"))
+		defineGlobal("MUTATIONS_FILE", "out_mutations.json");
+	initializeSLiMOptions(nucleotideBased=T);
+	initializeTreeSeq();
+	initializeAncestralNucleotides(randomNucleotides(L));
+	initializeMutationTypeNuc("m1", 0.5, "f", 0.0);
+	initializeGenomicElementType("g1", m1, 1.0, mmJukesCantor(1e-3));
+	initializeGenomicElement(g1, 0, L - 1);
+	initializeRecombinationRate(1e-6);
+	defineGlobal("M", Dictionary());
+	for (a in c("A", "C", "G", "T"))
+		for (b in c("A", "C", "G", "T"))
+			for (c in c("A", "C", "G", "T"))
+				for (d in c("A", "C", "G", "T"))
+					M.setValue(a+b+c+","+d, 0);
+}
+
+// record mutation spectrum
+mutation(NULL)
+{
+	if (mut.position > 0 & mut.position < L - 1) {
+		context = substr(REFSEQ, mut.position - 1, mut.position + 1);
+		for (k in 0:2) {
+			mut_k = which(genome.mutations.position == mut.position + k - 1);
+			if (length(mut_k) > 0) {
+				muts = genome.mutations[mut_k];
+				parent_mut = muts[whichMax(muts.originTick)];
+				context = (
+					substr(context, 0, k-1)
+					+ parent_mut.nucleotide
+					+ substr(context, k + 1, 2)
+				);
+			}
+		}
+		M.setValue(context + "," + mut.nucleotide, M.getValue(context + "," + mut.nucleotide) + 1);
+	}
+	return T;
+}
+
+1 early()
+{
+	sim.addSubpop("p1", 100);
+	sim.treeSeqRememberIndividuals(p1.individuals);
+	defineConstant("REFSEQ", sim.chromosome.ancestralNucleotides());
+}
+
+1: late()
+{
+	sim.treeSeqRememberIndividuals(p1.individuals);
+}
+
+10 late()
+{
+	sim.treeSeqOutput(TREES_FILE);
+	writeFile(MUTATIONS_FILE, M.serialize(format="json"));
+	catn("Done.");
+	sim.simulationFinished();
+}
diff --git a/tests/test_recipes/recipe_nonWF.slim b/tests/test_recipes/recipe_nonWF.slim
index 4c37cc1e..759f7b9b 100644
--- a/tests/test_recipes/recipe_nonWF.slim
+++ b/tests/test_recipes/recipe_nonWF.slim
@@ -42,9 +42,9 @@ early() {
 1: early() {
     for (pop in sim.subpopulations) {
         for (ind in pop.individuals) {
-            age = sim.modelType == "nonWF" ? ind.age else -1;
+            age = community.modelType == "nonWF" ? ind.age else -1;
             writeFile(PEDIGREE_FILE,
-             paste(c(sim.generation, 'early', ind.pedigreeID, age, ind.pedigreeParentIDs),
+             paste(c(sim.cycle, 'early', ind.pedigreeID, age, ind.pedigreeParentIDs),
                    sep='\t'),
              append=T);
         }
@@ -54,9 +54,9 @@ early() {
 1: late() {
     for (pop in sim.subpopulations) {
         for (ind in pop.individuals) {
-            age = sim.modelType == "nonWF" ? ind.age else -1;
+            age = community.modelType == "nonWF" ? ind.age else -1;
             writeFile(PEDIGREE_FILE,
-             paste(c(sim.generation, 'late', ind.pedigreeID, age, ind.pedigreeParentIDs),
+             paste(c(sim.cycle, 'late', ind.pedigreeID, age, ind.pedigreeParentIDs),
                    sep='\t'),
              append=T);
         }
diff --git a/tests/test_recipes/recipe_nonWF.v3.7.trees b/tests/test_recipes/recipe_nonWF.v3.7.trees
new file mode 100644
index 00000000..ac7460a3
Binary files /dev/null and b/tests/test_recipes/recipe_nonWF.v3.7.trees differ
diff --git a/tests/test_recipes/recipe_nonWF_early_early.slim b/tests/test_recipes/recipe_nonWF_early_early.slim
index 673e2d9b..59f387b4 100644
--- a/tests/test_recipes/recipe_nonWF_early_early.slim
+++ b/tests/test_recipes/recipe_nonWF_early_early.slim
@@ -42,9 +42,9 @@ early() {
 1: early() {
     for (pop in sim.subpopulations) {
         for (ind in pop.individuals) {
-            age = sim.modelType == "nonWF" ? ind.age else -1;
+            age = community.modelType == "nonWF" ? ind.age else -1;
             writeFile(PEDIGREE_FILE,
-             paste(c(sim.generation, 'early', ind.pedigreeID, age, ind.pedigreeParentIDs),
+             paste(c(sim.cycle, 'early', ind.pedigreeID, age, ind.pedigreeParentIDs),
                    sep='\t'),
              append=T);
         }
@@ -54,9 +54,9 @@ early() {
 1: late() {
     for (pop in sim.subpopulations) {
         for (ind in pop.individuals) {
-            age = sim.modelType == "nonWF" ? ind.age else -1;
+            age = community.modelType == "nonWF" ? ind.age else -1;
             writeFile(PEDIGREE_FILE,
-             paste(c(sim.generation, 'late', ind.pedigreeID, age, ind.pedigreeParentIDs),
+             paste(c(sim.cycle, 'late', ind.pedigreeID, age, ind.pedigreeParentIDs),
                    sep='\t'),
              append=T);
         }
diff --git a/tests/test_recipes/recipe_nonWF_early_late.slim b/tests/test_recipes/recipe_nonWF_early_late.slim
index 4c37cc1e..759f7b9b 100644
--- a/tests/test_recipes/recipe_nonWF_early_late.slim
+++ b/tests/test_recipes/recipe_nonWF_early_late.slim
@@ -42,9 +42,9 @@ early() {
 1: early() {
     for (pop in sim.subpopulations) {
         for (ind in pop.individuals) {
-            age = sim.modelType == "nonWF" ? ind.age else -1;
+            age = community.modelType == "nonWF" ? ind.age else -1;
             writeFile(PEDIGREE_FILE,
-             paste(c(sim.generation, 'early', ind.pedigreeID, age, ind.pedigreeParentIDs),
+             paste(c(sim.cycle, 'early', ind.pedigreeID, age, ind.pedigreeParentIDs),
                    sep='\t'),
              append=T);
         }
@@ -54,9 +54,9 @@ early() {
 1: late() {
     for (pop in sim.subpopulations) {
         for (ind in pop.individuals) {
-            age = sim.modelType == "nonWF" ? ind.age else -1;
+            age = community.modelType == "nonWF" ? ind.age else -1;
             writeFile(PEDIGREE_FILE,
-             paste(c(sim.generation, 'late', ind.pedigreeID, age, ind.pedigreeParentIDs),
+             paste(c(sim.cycle, 'late', ind.pedigreeID, age, ind.pedigreeParentIDs),
                    sep='\t'),
              append=T);
         }
diff --git a/tests/test_recipes/recipe_nonWF_late_early.slim b/tests/test_recipes/recipe_nonWF_late_early.slim
index ec57d381..1c558f91 100644
--- a/tests/test_recipes/recipe_nonWF_late_early.slim
+++ b/tests/test_recipes/recipe_nonWF_late_early.slim
@@ -42,9 +42,9 @@ reproduction() {
 1: early() {
     for (pop in sim.subpopulations) {
         for (ind in pop.individuals) {
-            age = sim.modelType == "nonWF" ? ind.age else -1;
+            age = community.modelType == "nonWF" ? ind.age else -1;
             writeFile(PEDIGREE_FILE,
-             paste(c(sim.generation, 'early', ind.pedigreeID, age, ind.pedigreeParentIDs),
+             paste(c(sim.cycle, 'early', ind.pedigreeID, age, ind.pedigreeParentIDs),
                    sep='\t'),
              append=T);
         }
@@ -54,9 +54,9 @@ reproduction() {
 1: late() {
     for (pop in sim.subpopulations) {
         for (ind in pop.individuals) {
-            age = sim.modelType == "nonWF" ? ind.age else -1;
+            age = community.modelType == "nonWF" ? ind.age else -1;
             writeFile(PEDIGREE_FILE,
-             paste(c(sim.generation, 'late', ind.pedigreeID, age, ind.pedigreeParentIDs),
+             paste(c(sim.cycle, 'late', ind.pedigreeID, age, ind.pedigreeParentIDs),
                    sep='\t'),
              append=T);
         }
diff --git a/tests/test_recipes/recipe_nonWF_late_late.slim b/tests/test_recipes/recipe_nonWF_late_late.slim
index 81b2233c..e118316e 100644
--- a/tests/test_recipes/recipe_nonWF_late_late.slim
+++ b/tests/test_recipes/recipe_nonWF_late_late.slim
@@ -42,9 +42,9 @@ reproduction() {
 1: early() {
     for (pop in sim.subpopulations) {
         for (ind in pop.individuals) {
-            age = sim.modelType == "nonWF" ? ind.age else -1;
+            age = community.modelType == "nonWF" ? ind.age else -1;
             writeFile(PEDIGREE_FILE,
-             paste(c(sim.generation, 'early', ind.pedigreeID, age, ind.pedigreeParentIDs),
+             paste(c(sim.cycle, 'early', ind.pedigreeID, age, ind.pedigreeParentIDs),
                    sep='\t'),
              append=T);
         }
@@ -54,9 +54,9 @@ reproduction() {
 1: late() {
     for (pop in sim.subpopulations) {
         for (ind in pop.individuals) {
-            age = sim.modelType == "nonWF" ? ind.age else -1;
+            age = community.modelType == "nonWF" ? ind.age else -1;
             writeFile(PEDIGREE_FILE,
-             paste(c(sim.generation, 'late', ind.pedigreeID, age, ind.pedigreeParentIDs),
+             paste(c(sim.cycle, 'late', ind.pedigreeID, age, ind.pedigreeParentIDs),
                    sep='\t'),
              append=T);
         }
diff --git a/tests/test_recipes/recipe_nonWF_selfing.slim b/tests/test_recipes/recipe_nonWF_selfing.slim
index 3565cbc5..df30114d 100644
--- a/tests/test_recipes/recipe_nonWF_selfing.slim
+++ b/tests/test_recipes/recipe_nonWF_selfing.slim
@@ -49,9 +49,9 @@ early() {
 1: early() {
     for (pop in sim.subpopulations) {
         for (ind in pop.individuals) {
-            age = sim.modelType == "nonWF" ? ind.age else -1;
+            age = community.modelType == "nonWF" ? ind.age else -1;
             writeFile(PEDIGREE_FILE,
-             paste(c(sim.generation, 'early', ind.pedigreeID, age, ind.pedigreeParentIDs),
+             paste(c(sim.cycle, 'early', ind.pedigreeID, age, ind.pedigreeParentIDs),
                    sep='\t'),
              append=T);
         }
@@ -61,9 +61,9 @@ early() {
 1: late() {
     for (pop in sim.subpopulations) {
         for (ind in pop.individuals) {
-            age = sim.modelType == "nonWF" ? ind.age else -1;
+            age = community.modelType == "nonWF" ? ind.age else -1;
             writeFile(PEDIGREE_FILE,
-             paste(c(sim.generation, 'late', ind.pedigreeID, age, ind.pedigreeParentIDs),
+             paste(c(sim.cycle, 'late', ind.pedigreeID, age, ind.pedigreeParentIDs),
                    sep='\t'),
              append=T);
         }
diff --git a/tests/test_recipes/recipe_nucleotides_WF.slim b/tests/test_recipes/recipe_nucleotides_WF.slim
index f6b3eb62..e086458a 100644
--- a/tests/test_recipes/recipe_nucleotides_WF.slim
+++ b/tests/test_recipes/recipe_nucleotides_WF.slim
@@ -14,7 +14,7 @@ initialize() {
 	initializeRecombinationRate(1e-2);
 }
 
-1 {
+1 early() {
 	sim.addSubpop("p1", 10);
 }
 
@@ -34,9 +34,9 @@ initialize() {
 1: early() {
     for (pop in sim.subpopulations) {
         for (ind in pop.individuals) {
-            age = sim.modelType == "nonWF" ? ind.age else -1;
+            age = community.modelType == "nonWF" ? ind.age else -1;
             writeFile(PEDIGREE_FILE,
-             paste(c(sim.generation, 'early', ind.pedigreeID, age, ind.pedigreeParentIDs),
+             paste(c(sim.cycle, 'early', ind.pedigreeID, age, ind.pedigreeParentIDs),
                    sep='\t'),
              append=T);
         }
@@ -46,9 +46,9 @@ initialize() {
 1: late() {
     for (pop in sim.subpopulations) {
         for (ind in pop.individuals) {
-            age = sim.modelType == "nonWF" ? ind.age else -1;
+            age = community.modelType == "nonWF" ? ind.age else -1;
             writeFile(PEDIGREE_FILE,
-             paste(c(sim.generation, 'late', ind.pedigreeID, age, ind.pedigreeParentIDs),
+             paste(c(sim.cycle, 'late', ind.pedigreeID, age, ind.pedigreeParentIDs),
                    sep='\t'),
              append=T);
         }
diff --git a/tests/test_recipes/recipe_nucleotides_nonWF.slim b/tests/test_recipes/recipe_nucleotides_nonWF.slim
index f7c7b02b..54285713 100644
--- a/tests/test_recipes/recipe_nucleotides_nonWF.slim
+++ b/tests/test_recipes/recipe_nucleotides_nonWF.slim
@@ -42,9 +42,9 @@ early() {
 1: early() {
     for (pop in sim.subpopulations) {
         for (ind in pop.individuals) {
-            age = sim.modelType == "nonWF" ? ind.age else -1;
+            age = community.modelType == "nonWF" ? ind.age else -1;
             writeFile(PEDIGREE_FILE,
-             paste(c(sim.generation, 'early', ind.pedigreeID, age, ind.pedigreeParentIDs),
+             paste(c(sim.cycle, 'early', ind.pedigreeID, age, ind.pedigreeParentIDs),
                    sep='\t'),
              append=T);
         }
@@ -54,9 +54,9 @@ early() {
 1: late() {
     for (pop in sim.subpopulations) {
         for (ind in pop.individuals) {
-            age = sim.modelType == "nonWF" ? ind.age else -1;
+            age = community.modelType == "nonWF" ? ind.age else -1;
             writeFile(PEDIGREE_FILE,
-             paste(c(sim.generation, 'late', ind.pedigreeID, age, ind.pedigreeParentIDs),
+             paste(c(sim.cycle, 'late', ind.pedigreeID, age, ind.pedigreeParentIDs),
                    sep='\t'),
              append=T);
         }
diff --git a/tests/test_recipes/recipe_nucleotides_plus_others.slim b/tests/test_recipes/recipe_nucleotides_plus_others.slim
index f71cd810..a34d8601 100644
--- a/tests/test_recipes/recipe_nucleotides_plus_others.slim
+++ b/tests/test_recipes/recipe_nucleotides_plus_others.slim
@@ -15,7 +15,7 @@ initialize() {
 	initializeRecombinationRate(1e-2);
 }
 
-1 {
+1 early() {
 	sim.addSubpop("p1", 10);
 }
 
@@ -40,9 +40,9 @@ initialize() {
 1: early() {
     for (pop in sim.subpopulations) {
         for (ind in pop.individuals) {
-            age = sim.modelType == "nonWF" ? ind.age else -1;
+            age = community.modelType == "nonWF" ? ind.age else -1;
             writeFile(PEDIGREE_FILE,
-             paste(c(sim.generation, 'early', ind.pedigreeID, age, ind.pedigreeParentIDs),
+             paste(c(sim.cycle, 'early', ind.pedigreeID, age, ind.pedigreeParentIDs),
                    sep='\t'),
              append=T);
         }
@@ -52,9 +52,9 @@ initialize() {
 1: late() {
     for (pop in sim.subpopulations) {
         for (ind in pop.individuals) {
-            age = sim.modelType == "nonWF" ? ind.age else -1;
+            age = community.modelType == "nonWF" ? ind.age else -1;
             writeFile(PEDIGREE_FILE,
-             paste(c(sim.generation, 'late', ind.pedigreeID, age, ind.pedigreeParentIDs),
+             paste(c(sim.cycle, 'late', ind.pedigreeID, age, ind.pedigreeParentIDs),
                    sep='\t'),
              append=T);
         }
diff --git a/tests/test_recipes/recipe_record_everyone_WF_early.slim b/tests/test_recipes/recipe_record_everyone_WF_early.slim
index f96359a7..be31ef2f 100644
--- a/tests/test_recipes/recipe_record_everyone_WF_early.slim
+++ b/tests/test_recipes/recipe_record_everyone_WF_early.slim
@@ -39,9 +39,9 @@ early() {
 1: early() {
     for (pop in sim.subpopulations) {
         for (ind in pop.individuals) {
-            age = sim.modelType == "nonWF" ? ind.age else -1;
+            age = community.modelType == "nonWF" ? ind.age else -1;
             writeFile(PEDIGREE_FILE,
-             paste(c(sim.generation, 'early', ind.pedigreeID, age, ind.pedigreeParentIDs),
+             paste(c(sim.cycle, 'early', ind.pedigreeID, age, ind.pedigreeParentIDs),
                    sep='\t'),
              append=T);
         }
@@ -51,9 +51,9 @@ early() {
 1: late() {
     for (pop in sim.subpopulations) {
         for (ind in pop.individuals) {
-            age = sim.modelType == "nonWF" ? ind.age else -1;
+            age = community.modelType == "nonWF" ? ind.age else -1;
             writeFile(PEDIGREE_FILE,
-             paste(c(sim.generation, 'late', ind.pedigreeID, age, ind.pedigreeParentIDs),
+             paste(c(sim.cycle, 'late', ind.pedigreeID, age, ind.pedigreeParentIDs),
                    sep='\t'),
              append=T);
         }
diff --git a/tests/test_recipes/recipe_record_everyone_WF_late.slim b/tests/test_recipes/recipe_record_everyone_WF_late.slim
index 095fa0c5..fd01574d 100644
--- a/tests/test_recipes/recipe_record_everyone_WF_late.slim
+++ b/tests/test_recipes/recipe_record_everyone_WF_late.slim
@@ -42,9 +42,9 @@ late() {
 1: early() {
     for (pop in sim.subpopulations) {
         for (ind in pop.individuals) {
-            age = sim.modelType == "nonWF" ? ind.age else -1;
+            age = community.modelType == "nonWF" ? ind.age else -1;
             writeFile(PEDIGREE_FILE,
-             paste(c(sim.generation, 'early', ind.pedigreeID, age, ind.pedigreeParentIDs),
+             paste(c(sim.cycle, 'early', ind.pedigreeID, age, ind.pedigreeParentIDs),
                    sep='\t'),
              append=T);
         }
@@ -54,9 +54,9 @@ late() {
 1: late() {
     for (pop in sim.subpopulations) {
         for (ind in pop.individuals) {
-            age = sim.modelType == "nonWF" ? ind.age else -1;
+            age = community.modelType == "nonWF" ? ind.age else -1;
             writeFile(PEDIGREE_FILE,
-             paste(c(sim.generation, 'late', ind.pedigreeID, age, ind.pedigreeParentIDs),
+             paste(c(sim.cycle, 'late', ind.pedigreeID, age, ind.pedigreeParentIDs),
                    sep='\t'),
              append=T);
         }
diff --git a/tests/test_recipes/recipe_record_everyone_nonWF_early.slim b/tests/test_recipes/recipe_record_everyone_nonWF_early.slim
index af17040d..3fd9ffe2 100644
--- a/tests/test_recipes/recipe_record_everyone_nonWF_early.slim
+++ b/tests/test_recipes/recipe_record_everyone_nonWF_early.slim
@@ -48,9 +48,9 @@ early() {
 1: early() {
     for (pop in sim.subpopulations) {
         for (ind in pop.individuals) {
-            age = sim.modelType == "nonWF" ? ind.age else -1;
+            age = community.modelType == "nonWF" ? ind.age else -1;
             writeFile(PEDIGREE_FILE,
-             paste(c(sim.generation, 'early', ind.pedigreeID, age, ind.pedigreeParentIDs),
+             paste(c(sim.cycle, 'early', ind.pedigreeID, age, ind.pedigreeParentIDs),
                    sep='\t'),
              append=T);
         }
@@ -60,9 +60,9 @@ early() {
 1: late() {
     for (pop in sim.subpopulations) {
         for (ind in pop.individuals) {
-            age = sim.modelType == "nonWF" ? ind.age else -1;
+            age = community.modelType == "nonWF" ? ind.age else -1;
             writeFile(PEDIGREE_FILE,
-             paste(c(sim.generation, 'late', ind.pedigreeID, age, ind.pedigreeParentIDs),
+             paste(c(sim.cycle, 'late', ind.pedigreeID, age, ind.pedigreeParentIDs),
                    sep='\t'),
              append=T);
         }
diff --git a/tests/test_recipes/recipe_record_everyone_nonWF_late.slim b/tests/test_recipes/recipe_record_everyone_nonWF_late.slim
index 51fb5550..b04737cb 100644
--- a/tests/test_recipes/recipe_record_everyone_nonWF_late.slim
+++ b/tests/test_recipes/recipe_record_everyone_nonWF_late.slim
@@ -48,9 +48,9 @@ late() {
 1: early() {
     for (pop in sim.subpopulations) {
         for (ind in pop.individuals) {
-            age = sim.modelType == "nonWF" ? ind.age else -1;
+            age = community.modelType == "nonWF" ? ind.age else -1;
             writeFile(PEDIGREE_FILE,
-             paste(c(sim.generation, 'early', ind.pedigreeID, age, ind.pedigreeParentIDs),
+             paste(c(sim.cycle, 'early', ind.pedigreeID, age, ind.pedigreeParentIDs),
                    sep='\t'),
              append=T);
         }
@@ -60,9 +60,9 @@ late() {
 1: late() {
     for (pop in sim.subpopulations) {
         for (ind in pop.individuals) {
-            age = sim.modelType == "nonWF" ? ind.age else -1;
+            age = community.modelType == "nonWF" ? ind.age else -1;
             writeFile(PEDIGREE_FILE,
-             paste(c(sim.generation, 'late', ind.pedigreeID, age, ind.pedigreeParentIDs),
+             paste(c(sim.cycle, 'late', ind.pedigreeID, age, ind.pedigreeParentIDs),
                    sep='\t'),
              append=T);
         }
diff --git a/tests/test_recipes/recipe_remember_and_retain.slim b/tests/test_recipes/recipe_remember_and_retain.slim
index 997802bd..1ba53ac7 100644
--- a/tests/test_recipes/recipe_remember_and_retain.slim
+++ b/tests/test_recipes/recipe_remember_and_retain.slim
@@ -30,7 +30,7 @@ early() {
 
 late() {
     // alternate remembering and retaining
-    sim.treeSeqRememberIndividuals(p1.individuals, permanent=(sim.generation % 2 == 0));
+    sim.treeSeqRememberIndividuals(p1.individuals, permanent=(sim.cycle % 2 == 0));
 }
 
 10 late() {
@@ -49,9 +49,9 @@ late() {
 1: early() {
     for (pop in sim.subpopulations) {
         for (ind in pop.individuals) {
-            age = sim.modelType == "nonWF" ? ind.age else -1;
+            age = community.modelType == "nonWF" ? ind.age else -1;
             writeFile(PEDIGREE_FILE,
-             paste(c(sim.generation, 'early', ind.pedigreeID, age, ind.pedigreeParentIDs),
+             paste(c(sim.cycle, 'early', ind.pedigreeID, age, ind.pedigreeParentIDs),
                    sep='\t'),
              append=T);
         }
@@ -61,9 +61,9 @@ late() {
 1: late() {
     for (pop in sim.subpopulations) {
         for (ind in pop.individuals) {
-            age = sim.modelType == "nonWF" ? ind.age else -1;
+            age = community.modelType == "nonWF" ? ind.age else -1;
             writeFile(PEDIGREE_FILE,
-             paste(c(sim.generation, 'late', ind.pedigreeID, age, ind.pedigreeParentIDs),
+             paste(c(sim.cycle, 'late', ind.pedigreeID, age, ind.pedigreeParentIDs),
                    sep='\t'),
              append=T);
         }
diff --git a/tests/test_recipes/recipe_retain_everyone_WF_late.slim b/tests/test_recipes/recipe_retain_everyone_WF_late.slim
index efa1d460..1fa78555 100644
--- a/tests/test_recipes/recipe_retain_everyone_WF_late.slim
+++ b/tests/test_recipes/recipe_retain_everyone_WF_late.slim
@@ -42,9 +42,9 @@ late() {
 1: early() {
     for (pop in sim.subpopulations) {
         for (ind in pop.individuals) {
-            age = sim.modelType == "nonWF" ? ind.age else -1;
+            age = community.modelType == "nonWF" ? ind.age else -1;
             writeFile(PEDIGREE_FILE,
-             paste(c(sim.generation, 'early', ind.pedigreeID, age, ind.pedigreeParentIDs),
+             paste(c(sim.cycle, 'early', ind.pedigreeID, age, ind.pedigreeParentIDs),
                    sep='\t'),
              append=T);
         }
@@ -54,9 +54,9 @@ late() {
 1: late() {
     for (pop in sim.subpopulations) {
         for (ind in pop.individuals) {
-            age = sim.modelType == "nonWF" ? ind.age else -1;
+            age = community.modelType == "nonWF" ? ind.age else -1;
             writeFile(PEDIGREE_FILE,
-             paste(c(sim.generation, 'late', ind.pedigreeID, age, ind.pedigreeParentIDs),
+             paste(c(sim.cycle, 'late', ind.pedigreeID, age, ind.pedigreeParentIDs),
                    sep='\t'),
              append=T);
         }
diff --git a/tests/test_recipes/recipe_retain_everyone_nonWF_late.slim b/tests/test_recipes/recipe_retain_everyone_nonWF_late.slim
index 0898ddf9..507bad5e 100644
--- a/tests/test_recipes/recipe_retain_everyone_nonWF_late.slim
+++ b/tests/test_recipes/recipe_retain_everyone_nonWF_late.slim
@@ -48,9 +48,9 @@ late() {
 1: early() {
     for (pop in sim.subpopulations) {
         for (ind in pop.individuals) {
-            age = sim.modelType == "nonWF" ? ind.age else -1;
+            age = community.modelType == "nonWF" ? ind.age else -1;
             writeFile(PEDIGREE_FILE,
-             paste(c(sim.generation, 'early', ind.pedigreeID, age, ind.pedigreeParentIDs),
+             paste(c(sim.cycle, 'early', ind.pedigreeID, age, ind.pedigreeParentIDs),
                    sep='\t'),
              append=T);
         }
@@ -60,9 +60,9 @@ late() {
 1: late() {
     for (pop in sim.subpopulations) {
         for (ind in pop.individuals) {
-            age = sim.modelType == "nonWF" ? ind.age else -1;
+            age = community.modelType == "nonWF" ? ind.age else -1;
             writeFile(PEDIGREE_FILE,
-             paste(c(sim.generation, 'late', ind.pedigreeID, age, ind.pedigreeParentIDs),
+             paste(c(sim.cycle, 'late', ind.pedigreeID, age, ind.pedigreeParentIDs),
                    sep='\t'),
              append=T);
         }
diff --git a/tests/test_recipes/recipe_retain_sometimes_WF_late.slim b/tests/test_recipes/recipe_retain_sometimes_WF_late.slim
index 8bd1d876..4f65c347 100644
--- a/tests/test_recipes/recipe_retain_sometimes_WF_late.slim
+++ b/tests/test_recipes/recipe_retain_sometimes_WF_late.slim
@@ -47,9 +47,9 @@ late() {
 1: early() {
     for (pop in sim.subpopulations) {
         for (ind in pop.individuals) {
-            age = sim.modelType == "nonWF" ? ind.age else -1;
+            age = community.modelType == "nonWF" ? ind.age else -1;
             writeFile(PEDIGREE_FILE,
-             paste(c(sim.generation, 'early', ind.pedigreeID, age, ind.pedigreeParentIDs),
+             paste(c(sim.cycle, 'early', ind.pedigreeID, age, ind.pedigreeParentIDs),
                    sep='\t'),
              append=T);
         }
@@ -59,9 +59,9 @@ late() {
 1: late() {
     for (pop in sim.subpopulations) {
         for (ind in pop.individuals) {
-            age = sim.modelType == "nonWF" ? ind.age else -1;
+            age = community.modelType == "nonWF" ? ind.age else -1;
             writeFile(PEDIGREE_FILE,
-             paste(c(sim.generation, 'late', ind.pedigreeID, age, ind.pedigreeParentIDs),
+             paste(c(sim.cycle, 'late', ind.pedigreeID, age, ind.pedigreeParentIDs),
                    sep='\t'),
              append=T);
         }
diff --git a/tests/test_recipes/recipe_retain_sometimes_nonWF_late.slim b/tests/test_recipes/recipe_retain_sometimes_nonWF_late.slim
index 786251c4..2df5cfd6 100644
--- a/tests/test_recipes/recipe_retain_sometimes_nonWF_late.slim
+++ b/tests/test_recipes/recipe_retain_sometimes_nonWF_late.slim
@@ -53,9 +53,9 @@ late() {
 1: early() {
     for (pop in sim.subpopulations) {
         for (ind in pop.individuals) {
-            age = sim.modelType == "nonWF" ? ind.age else -1;
+            age = community.modelType == "nonWF" ? ind.age else -1;
             writeFile(PEDIGREE_FILE,
-             paste(c(sim.generation, 'early', ind.pedigreeID, age, ind.pedigreeParentIDs),
+             paste(c(sim.cycle, 'early', ind.pedigreeID, age, ind.pedigreeParentIDs),
                    sep='\t'),
              append=T);
         }
@@ -65,9 +65,9 @@ late() {
 1: late() {
     for (pop in sim.subpopulations) {
         for (ind in pop.individuals) {
-            age = sim.modelType == "nonWF" ? ind.age else -1;
+            age = community.modelType == "nonWF" ? ind.age else -1;
             writeFile(PEDIGREE_FILE,
-             paste(c(sim.generation, 'late', ind.pedigreeID, age, ind.pedigreeParentIDs),
+             paste(c(sim.cycle, 'late', ind.pedigreeID, age, ind.pedigreeParentIDs),
                    sep='\t'),
              append=T);
         }
diff --git a/tests/test_recipes/recipe_retain_unary_WF_late.slim b/tests/test_recipes/recipe_retain_unary_WF_late.slim
index 1d6bd8cf..27689aba 100644
--- a/tests/test_recipes/recipe_retain_unary_WF_late.slim
+++ b/tests/test_recipes/recipe_retain_unary_WF_late.slim
@@ -47,9 +47,9 @@ late() {
 1: early() {
     for (pop in sim.subpopulations) {
         for (ind in pop.individuals) {
-            age = sim.modelType == "nonWF" ? ind.age else -1;
+            age = community.modelType == "nonWF" ? ind.age else -1;
             writeFile(PEDIGREE_FILE,
-             paste(c(sim.generation, 'early', ind.pedigreeID, age, ind.pedigreeParentIDs),
+             paste(c(sim.cycle, 'early', ind.pedigreeID, age, ind.pedigreeParentIDs),
                    sep='\t'),
              append=T);
         }
@@ -59,9 +59,9 @@ late() {
 1: late() {
     for (pop in sim.subpopulations) {
         for (ind in pop.individuals) {
-            age = sim.modelType == "nonWF" ? ind.age else -1;
+            age = community.modelType == "nonWF" ? ind.age else -1;
             writeFile(PEDIGREE_FILE,
-             paste(c(sim.generation, 'late', ind.pedigreeID, age, ind.pedigreeParentIDs),
+             paste(c(sim.cycle, 'late', ind.pedigreeID, age, ind.pedigreeParentIDs),
                    sep='\t'),
              append=T);
         }
diff --git a/tests/test_recipes/recipe_retain_unary_nonWF_late.slim b/tests/test_recipes/recipe_retain_unary_nonWF_late.slim
index 06752454..5ae7fdbc 100644
--- a/tests/test_recipes/recipe_retain_unary_nonWF_late.slim
+++ b/tests/test_recipes/recipe_retain_unary_nonWF_late.slim
@@ -53,9 +53,9 @@ late() {
 1: early() {
     for (pop in sim.subpopulations) {
         for (ind in pop.individuals) {
-            age = sim.modelType == "nonWF" ? ind.age else -1;
+            age = community.modelType == "nonWF" ? ind.age else -1;
             writeFile(PEDIGREE_FILE,
-             paste(c(sim.generation, 'early', ind.pedigreeID, age, ind.pedigreeParentIDs),
+             paste(c(sim.cycle, 'early', ind.pedigreeID, age, ind.pedigreeParentIDs),
                    sep='\t'),
              append=T);
         }
@@ -65,9 +65,9 @@ late() {
 1: late() {
     for (pop in sim.subpopulations) {
         for (ind in pop.individuals) {
-            age = sim.modelType == "nonWF" ? ind.age else -1;
+            age = community.modelType == "nonWF" ? ind.age else -1;
             writeFile(PEDIGREE_FILE,
-             paste(c(sim.generation, 'late', ind.pedigreeID, age, ind.pedigreeParentIDs),
+             paste(c(sim.cycle, 'late', ind.pedigreeID, age, ind.pedigreeParentIDs),
                    sep='\t'),
              append=T);
         }
diff --git a/tests/test_recipes/recipe_roots.slim b/tests/test_recipes/recipe_roots.slim
index b5033640..79ae4ea7 100644
--- a/tests/test_recipes/recipe_roots.slim
+++ b/tests/test_recipes/recipe_roots.slim
@@ -12,7 +12,7 @@ initialize() {
     initializeRecombinationRate(1e-8);
 }
 
-1 {
+1 early() {
     sim.addSubpop('p1', 1000);
 }
 
@@ -32,9 +32,9 @@ initialize() {
 1: early() {
     for (pop in sim.subpopulations) {
         for (ind in pop.individuals) {
-            age = sim.modelType == "nonWF" ? ind.age else -1;
+            age = community.modelType == "nonWF" ? ind.age else -1;
             writeFile(PEDIGREE_FILE,
-             paste(c(sim.generation, 'early', ind.pedigreeID, age, ind.pedigreeParentIDs),
+             paste(c(sim.cycle, 'early', ind.pedigreeID, age, ind.pedigreeParentIDs),
                    sep='\t'),
              append=T);
         }
@@ -44,9 +44,9 @@ initialize() {
 1: late() {
     for (pop in sim.subpopulations) {
         for (ind in pop.individuals) {
-            age = sim.modelType == "nonWF" ? ind.age else -1;
+            age = community.modelType == "nonWF" ? ind.age else -1;
             writeFile(PEDIGREE_FILE,
-             paste(c(sim.generation, 'late', ind.pedigreeID, age, ind.pedigreeParentIDs),
+             paste(c(sim.cycle, 'late', ind.pedigreeID, age, ind.pedigreeParentIDs),
                    sep='\t'),
              append=T);
         }
diff --git a/tests/test_recipes/restart_WF.slim b/tests/test_recipes/restart_WF.slim
index 5b58fb34..b082a555 100644
--- a/tests/test_recipes/restart_WF.slim
+++ b/tests/test_recipes/restart_WF.slim
@@ -10,23 +10,24 @@ initialize()
     initializeGenomicElementType("g1", m1, 1.0);
     initializeGenomicElement(g1, 0, 99);
     initializeRecombinationRate(1e-2);
+    if (!exists("SUBPOP_MAP")) defineConstant("SUBPOP_MAP", NULL);
 }
 
 // output immediately! Nothing should have changed.
 1 early() { 
   if (STAGE == "early") {
-    sim.readFromPopulationFile(TREES_IN);
+    sim.readFromPopulationFile(TREES_IN, subpopMap=SUBPOP_MAP);
     sim.treeSeqOutput(TREES_FILE, simplify=F);
   }
 }
 1 late() { 
   if (STAGE == "late") {
-    sim.readFromPopulationFile(TREES_IN);
+    sim.readFromPopulationFile(TREES_IN, subpopMap=SUBPOP_MAP);
     sim.treeSeqOutput(TREES_FILE, simplify=F);
   }
 }
 
-20 {
+20 early() {
     catn("Done.");
     sim.simulationFinished();
 }
diff --git a/tests/test_recipes/restart_and_remove_subpop_nonWF.slim b/tests/test_recipes/restart_and_remove_subpop_nonWF.slim
new file mode 100644
index 00000000..f74a9923
--- /dev/null
+++ b/tests/test_recipes/restart_and_remove_subpop_nonWF.slim
@@ -0,0 +1,45 @@
+initialize()
+{
+    setSeed(23);
+    if (!exists("TREES_FILE")) defineGlobal("TREES_FILE", "out.trees"); 
+    // as well as any bespoke TREES_FILE for output, you need to define:
+    //  TREES_IN = path to trees to initialise
+    initializeSLiMModelType("nonWF");
+    initializeSLiMOptions(dimensionality="xy");
+    initializeTreeSeq();
+    initializeMutationRate(1e-2);
+    initializeMutationType("m1", 0.5, "f", -0.1);
+    initializeGenomicElementType("g1", m1, 1.0);
+    initializeGenomicElement(g1, 0, 99);
+    initializeRecombinationRate(1e-2);
+    if (!exists("SUBPOP_MAP")) defineConstant("SUBPOP_MAP", NULL);
+}
+
+1 early() { 
+  if (STAGE == "early") {
+    sim.readFromPopulationFile(TREES_IN, subpopMap=SUBPOP_MAP);
+    community.rescheduleScriptBlock(s1, community.tick + 1);
+    community.rescheduleScriptBlock(s2, community.tick + 10);
+  }
+}
+
+1 late() { 
+  if (STAGE == "late") {
+    sim.readFromPopulationFile(TREES_IN, subpopMap=SUBPOP_MAP);
+    community.rescheduleScriptBlock(s1, ticks=community.tick + 2);
+    community.rescheduleScriptBlock(s2, ticks=community.tick + 10);
+  }
+}
+
+s1 10 early() {
+	old_pops = sim.subpopulations;
+	p = sim.addSubpop("p0", 0);
+	p.takeMigrants(old_pops.individuals);
+	old_pops.removeSubpopulation();
+}
+
+s2 20 early() {
+    catn("Done.");
+    sim.treeSeqOutput(TREES_FILE);
+    sim.simulationFinished();
+}
diff --git a/tests/test_recipes/restart_and_run_WF.slim b/tests/test_recipes/restart_and_run_WF.slim
index 84a8fa37..a22ecbf6 100644
--- a/tests/test_recipes/restart_and_run_WF.slim
+++ b/tests/test_recipes/restart_and_run_WF.slim
@@ -11,21 +11,22 @@ initialize()
     initializeGenomicElementType("g1", m1, 1.0);
     initializeGenomicElement(g1, 0, 99);
     initializeRecombinationRate(1e-2);
+    if (!exists("SUBPOP_MAP")) defineConstant("SUBPOP_MAP", NULL);
 }
 
 // output immediately! Nothing should have changed.
 1 early() { 
   if (STAGE == "early") {
-    sim.readFromPopulationFile(TREES_IN);
+    sim.readFromPopulationFile(TREES_IN, subpopMap=SUBPOP_MAP);
   }
 }
 1 late() { 
   if (STAGE == "late") {
-    sim.readFromPopulationFile(TREES_IN);
+    sim.readFromPopulationFile(TREES_IN, subpopMap=SUBPOP_MAP);
   }
 }
 
-20 {
+20 early() {
     catn("Done.");
     sim.treeSeqOutput(TREES_FILE, simplify=F);
     sim.simulationFinished();
diff --git a/tests/test_recipes/restart_and_run_nonWF.slim b/tests/test_recipes/restart_and_run_nonWF.slim
index dca751d6..988eaa55 100644
--- a/tests/test_recipes/restart_and_run_nonWF.slim
+++ b/tests/test_recipes/restart_and_run_nonWF.slim
@@ -12,21 +12,22 @@ initialize()
     initializeGenomicElementType("g1", m1, 1.0);
     initializeGenomicElement(g1, 0, 99);
     initializeRecombinationRate(1e-2);
+    if (!exists("SUBPOP_MAP")) defineConstant("SUBPOP_MAP", NULL);
 }
 
 1 early() { 
   if (STAGE == "early") {
-    sim.readFromPopulationFile(TREES_IN);
+    sim.readFromPopulationFile(TREES_IN, subpopMap=SUBPOP_MAP);
   }
 }
 
 1 late() { 
   if (STAGE == "late") {
-    sim.readFromPopulationFile(TREES_IN);
+    sim.readFromPopulationFile(TREES_IN, subpopMap=SUBPOP_MAP);
   }
 }
 
-20 {
+20 early() {
     catn("Done.");
     sim.treeSeqOutput(TREES_FILE, simplify=F);
     sim.simulationFinished();
diff --git a/tests/test_recipes/restart_msprime.slim b/tests/test_recipes/restart_msprime.slim
index 565bfa7d..44383a59 100644
--- a/tests/test_recipes/restart_msprime.slim
+++ b/tests/test_recipes/restart_msprime.slim
@@ -24,23 +24,24 @@ initialize()
         catn("SEX: " + SEX);
         initializeSex(SEX);
     }
+    if (!exists("SUBPOP_MAP")) defineConstant("SUBPOP_MAP", NULL);
 }
 
 // output immediately! Nothing should have changed.
 1 early() { 
   if (STAGE == "early") {
-    sim.readFromPopulationFile(TREES_IN);
+    sim.readFromPopulationFile(TREES_IN, subpopMap=SUBPOP_MAP);
     sim.treeSeqOutput(TREES_FILE, simplify=F);
   }
 }
 1 late() { 
   if (STAGE == "late") {
-    sim.readFromPopulationFile(TREES_IN);
+    sim.readFromPopulationFile(TREES_IN, subpopMap=SUBPOP_MAP);
     sim.treeSeqOutput(TREES_FILE, simplify=F);
   }
 }
 
-20 {
+20 early() {
     catn("Done.");
     sim.simulationFinished();
 }
diff --git a/tests/test_recipes/restart_nonWF.slim b/tests/test_recipes/restart_nonWF.slim
index 6572dd75..85a5f28f 100644
--- a/tests/test_recipes/restart_nonWF.slim
+++ b/tests/test_recipes/restart_nonWF.slim
@@ -11,24 +11,25 @@ initialize()
     initializeGenomicElementType("g1", m1, 1.0);
     initializeGenomicElement(g1, 0, 99);
     initializeRecombinationRate(1e-2);
+    if (!exists("SUBPOP_MAP")) defineConstant("SUBPOP_MAP", NULL);
 }
 
 // output immediately! Nothing should have changed.
 1 early() { 
   if (STAGE == "early") {
-    sim.readFromPopulationFile(TREES_IN);
+    sim.readFromPopulationFile(TREES_IN, subpopMap=SUBPOP_MAP);
     sim.treeSeqOutput(TREES_FILE, simplify=F);
   }
 }
 
 1 late() { 
   if (STAGE == "late") {
-    sim.readFromPopulationFile(TREES_IN);
+    sim.readFromPopulationFile(TREES_IN, subpopMap=SUBPOP_MAP);
     sim.treeSeqOutput(TREES_FILE, simplify=F);
   }
 }
 
-20 {
+20 early() {
     catn("Done.");
     sim.simulationFinished();
 }
diff --git a/tests/test_recipes/restart_nucleotides_WF.slim b/tests/test_recipes/restart_nucleotides_WF.slim
index ef415228..3625a2c0 100644
--- a/tests/test_recipes/restart_nucleotides_WF.slim
+++ b/tests/test_recipes/restart_nucleotides_WF.slim
@@ -12,15 +12,16 @@ initialize()
 	initializeGenomicElementType("g1", m1, 1.0, mmJukesCantor(4e-2));
 	initializeGenomicElement(g1, 0, L-1);
 	initializeRecombinationRate(1e-2);
+	if (!exists("SUBPOP_MAP")) defineConstant("SUBPOP_MAP", NULL);
 }
 
 1 late() { 
     // output immediately! Nothing should have changed.
-    sim.readFromPopulationFile(TREES_IN);
+    sim.readFromPopulationFile(TREES_IN, subpopMap=SUBPOP_MAP);
     sim.treeSeqOutput(TREES_FILE, simplify=F);
 }
 
-2 {
+2 early() {
     catn("Done.");
     sim.simulationFinished();
 }
diff --git a/tests/test_recipes/restart_nucleotides_nonWF.slim b/tests/test_recipes/restart_nucleotides_nonWF.slim
index 592cda70..b02f0020 100644
--- a/tests/test_recipes/restart_nucleotides_nonWF.slim
+++ b/tests/test_recipes/restart_nucleotides_nonWF.slim
@@ -13,15 +13,16 @@ initialize()
 	initializeGenomicElementType("g1", m1, 1.0, mmJukesCantor(4e-2));
 	initializeGenomicElement(g1, 0, L-1);
 	initializeRecombinationRate(1e-2);
+	if (!exists("SUBPOP_MAP")) defineConstant("SUBPOP_MAP", NULL);
 }
 
 1 late() { 
     // output immediately! Nothing should have changed.
-    sim.readFromPopulationFile(TREES_IN);
+    sim.readFromPopulationFile(TREES_IN, subpopMap=SUBPOP_MAP);
     sim.treeSeqOutput(TREES_FILE, simplify=F);
 }
 
-2 {
+2 early() {
     catn("Done.");
     sim.simulationFinished();
 }
diff --git a/tests/test_spatial.py b/tests/test_spatial.py
index 3791e675..635a0328 100644
--- a/tests/test_spatial.py
+++ b/tests/test_spatial.py
@@ -41,8 +41,8 @@ def population_size_simple(self, ts, x_bins, y_bins, time_bins, **kwargs):
         popsize = np.empty((nxbins, nybins, ntbins))
 
         # Location, times, and ages of individuals
-        locations = ts.individual_locations
-        times = ts.individual_times
+        locations = pyslim.individual_locations(ts)
+        times = pyslim.individual_times(ts)
 
         # Iterate through location bins and time bins
         for i in np.arange(nxbins):
@@ -55,7 +55,7 @@ def population_size_simple(self, ts, x_bins, y_bins, time_bins, **kwargs):
                     t0, t1 = time_breaks[k], time_breaks[k + 1]
                     alive = 0
                     for t in np.arange(np.ceil(t0), t1):
-                        for ind_id in ts.individuals_alive_at(t, **kwargs):
+                        for ind_id in pyslim.individuals_alive_at(ts, t, **kwargs):
                             ind = ts.individual(ind_id)
                             if (ind.location[0] < x1 and ind.location[0] >= x0 and ind.location[1] < y1 and ind.location[1] >= y0):
                                 alive += 1/(t1-t0)
@@ -63,12 +63,13 @@ def population_size_simple(self, ts, x_bins, y_bins, time_bins, **kwargs):
         return(popsize)
 
     def make_bins(self, ts):
+        locs = pyslim.individual_locations(ts)
         for nx in np.arange(1, 20, 10):
             for ny in np.arange(1, 20, 10):
                 for nt in np.arange(1, 60, 30):
-                    yield [np.linspace(0, round(max(ts.individual_locations[:,0])), nx + 1), 
-                           np.linspace(0, round(max(ts.individual_locations[:,1])), ny + 1),
-                           np.round(np.linspace(0, max(nt, max(ts.individual_times)), nt + 1))]  
+                    yield [np.linspace(0, round(max(locs[:,0])), nx + 1), 
+                           np.linspace(0, round(max(locs[:,1])), ny + 1),
+                           np.round(np.linspace(0, max(nt, max(pyslim.individual_times(ts))), nt + 1))]  
 
     def verify(self, ts, remembered_stage):
         for bins in self.make_bins(ts):
@@ -106,6 +107,7 @@ def test_mismatched_remembered_stage(self, recipe):
             with pytest.warns(UserWarning):
                 pyslim.population_size(ts, x_bins, y_bins, time_bins, remembered_stage="early")
 
+    @pytest.mark.skip("Waiting on next tskit release.")
     @pytest.mark.parametrize('recipe', recipe_eq("everyone"), indirect=True)
     def test_population_size(self, recipe):
         # compare output to the right answer
@@ -116,7 +118,7 @@ def test_population_size(self, recipe):
     def test_known_answer(self):
         # a simple example to make sure we've got the edge cases right
         tables = tskit.TableCollection(sequence_length=1)
-        pyslim.set_tree_sequence_metadata(tables,  model_type='nonWF', generation=0)
+        pyslim.set_tree_sequence_metadata(tables,  model_type='nonWF', tick=0)
         pyslim.set_metadata_schemas(tables)
         locs = [[0, 0], # alive at 0, 1
                 [0, 1], # alive at 0, 1, 2
@@ -141,9 +143,7 @@ def test_known_answer(self):
         for j, b in enumerate(births):
             tables.nodes.add_row(time=b, individual=j)
 
-        ts = pyslim.SlimTreeSequence(
-                tables.tree_sequence()
-        )
+        ts = tables.tree_sequence()
 
         # check we've got this right
         for k, n in enumerate([
@@ -153,7 +153,7 @@ def test_known_answer(self):
                 [7]]):
             np.testing.assert_array_equal(
                     n,
-                    ts.individuals_alive_at(k)
+                    pyslim.individuals_alive_at(ts, k)
             )
 
         # no-one
diff --git a/tests/test_tree_sequence.py b/tests/test_tree_sequence.py
index 7d5db021..10dd41d1 100644
--- a/tests/test_tree_sequence.py
+++ b/tests/test_tree_sequence.py
@@ -15,114 +15,22 @@
 
 from .recipe_specs import recipe_eq
 
-class TestSlimTreeSequence(tests.PyslimTestCase):
-
-    def clean_example(self):
-        tables = tskit.TableCollection(sequence_length=100)
-        tables.populations.add_row()
-        tables.populations.add_row()
-        tables.individuals.add_row()
-        tables.nodes.add_row(time=0, flags=tskit.NODE_IS_SAMPLE, population=1, individual=0)
-        tables.nodes.add_row(time=0, flags=tskit.NODE_IS_SAMPLE, population=1, individual=0)
-        pyslim.annotate_defaults_tables(tables, model_type='nonWF', slim_generation=1)
-        return tables
-
-    def test_inconsistent_nodes(self):
-        clean_tables = self.clean_example()
-        tables = clean_tables.copy()
-        tables.nodes.clear()
-        for j, n in enumerate(clean_tables.nodes):
-            tables.nodes.add_row(
-                    time=n.time, flags=n.flags,
-                    population=j,
-                    individual=n.individual,
-                    metadata=n.metadata)
-        with pytest.raises(ValueError):
-            pyslim.annotate_defaults_tables(tables, model_type='nonWF', slim_generation=1)
-        ts = tables.tree_sequence()
-        with pytest.raises(ValueError):
-            _ = pyslim.SlimTreeSequence(ts)
-
-    def test_inconsistent_times(self):
-        clean_tables = self.clean_example()
-        tables = clean_tables.copy()
-        tables.nodes.clear()
-        for j, n in enumerate(clean_tables.nodes):
-            tables.nodes.add_row(time=j, flags=tskit.NODE_IS_SAMPLE, population=1, individual=0)
-        ts = tables.tree_sequence()
-        with pytest.raises(ValueError):
-            _ = pyslim.SlimTreeSequence(ts)
-
-    def test_bad_metadata(self):
-        clean_tables = self.clean_example()
-        tables = clean_tables.copy()
-        tables.metadata_schema = tskit.MetadataSchema({"type": "object", "codec": "json"})
-        tables.metadata = {}
-        ts = tables.tree_sequence()
-        with pytest.raises(ValueError):
-            _ = pyslim.SlimTreeSequence(ts)
-
-    # tmp_path is a pytest fixture, and is a pathlib.Path object
-    @pytest.mark.skip(reason="deprecating this feature")
-    @pytest.mark.parametrize("recipe", ["recipe_nonWF.slim"], indirect=True)
-    def test_slim_generation(self, recipe, tmp_path):
-        # tests around awkward backwards-compatible patch for setting slim_generation
-        ts = recipe["ts"]
-        with pytest.warns(FutureWarning):
-            assert ts.slim_generation == ts.metadata['SLiM']['generation']
-        new_sg = 12345
-        with pytest.warns(FutureWarning):
-            ts.slim_generation = new_sg
-        with pytest.warns(FutureWarning):
-            assert ts.slim_generation == new_sg
-        # check persists through dump/load
-        temp_file = tmp_path / "temp.trees"
-        ts.dump(temp_file.name)
-        loaded_ts = pyslim.load(temp_file.name)
-        with pytest.warns(FutureWarning):
-            assert loaded_ts.slim_generation == new_sg
-        assert loaded_ts.metadata['SLiM']['generation'] == new_sg
-        # check persists through recapitate
-        recap = self.do_recapitate(ts, recombination_rate=1e-8, ancestral_Ne=10)
-        assert recap.slim_generation == new_sg
-        # check persists through simplify
-        simp = ts.simplify(ts.samples())
-        assert simp.slim_generation == new_sg
-
-    def test_pickle(self):
-        ts = self.clean_example().tree_sequence()
-        ts = pyslim.SlimTreeSequence(ts)
-        roundtripped = pickle.loads(pickle.dumps(ts))
-        assert roundtripped == ts
-
-
 class TestSlimTime(tests.PyslimTestCase):
     # Tests for slim_time()
 
+    @pytest.mark.parametrize('recipe', recipe_eq(exclude="long"), indirect=True)
     def test_slim_time(self, recipe):
         ts = recipe["ts"]
         if "init_mutated" not in recipe:
             for mut in ts.mutations():
                 mut_time = max([x['slim_time'] for x in mut.metadata['mutation_list']])
-                assert mut_time == ts.slim_time(mut.time)
+                assert mut_time == pyslim.slim_time(ts, mut.time)
         # the mutations in "init_mutated" examples have mutations that are *added*
         # in *early*, and so their times match in that stage.
         else:
             for mut in ts.mutations():
                 mut_time = max([x['slim_time'] for x in mut.metadata['mutation_list']])
-                assert mut_time == ts.slim_time(mut.time, stage="early")
-
-
-class TestMutate(tests.PyslimTestCase):
-    # Tests for making a tree sequence a SlimTreeSequence
-    # again after msprime.mutate.
-
-    @pytest.mark.parametrize('recipe', recipe_eq(exclude="user_metadata"), indirect=True)
-    def test_mutate(self, recipe):
-        ts = recipe["ts"]
-        mts = msprime.mutate(ts, rate=1e-8, random_seed=5)
-        pts = pyslim.SlimTreeSequence(mts)
-        assert ts.metadata == pts.metadata
+                assert mut_time == pyslim.slim_time(ts, mut.time, stage="early")
 
 
 class TestRecapitate(tests.PyslimTestCase):
@@ -131,7 +39,10 @@ class TestRecapitate(tests.PyslimTestCase):
     '''
 
     def check_recap_consistency(self, ts, recap):
-        assert ts.metadata['SLiM']['generation'] == recap.metadata['SLiM']['generation']
+        assert ts.metadata['SLiM']['tick'] == recap.metadata['SLiM']['tick']
+        assert ts.metadata['SLiM']['cycle'] == recap.metadata['SLiM']['cycle']
+        assert ts.metadata['SLiM']['stage'] == recap.metadata['SLiM']['stage']
+        assert ts.metadata['SLiM']['name'] == recap.metadata['SLiM']['name']
         assert all(tree.num_roots == 1 for tree in recap.trees())
         assert ts.has_reference_sequence() == recap.has_reference_sequence()
         if ts.has_reference_sequence():
@@ -160,7 +71,7 @@ def check_recap_consistency(self, ts, recap):
             assert p1.metadata == p2.metadata
         # find ancestral pop in which recapitation happens
         tables = ts.tables
-        anc_nodes = np.where(tables.nodes.time > ts.metadata['SLiM']['generation'])[0]
+        anc_nodes = np.where(tables.nodes.time > ts.metadata['SLiM']['tick'])[0]
         if len(anc_nodes) > 0:
             for pop in ts.populations():
                 if pop.metadata is not None and pop.metadata['name'] == "ancestral":
@@ -177,47 +88,61 @@ def test_recapitate_errors(self, recipe):
                         ts,
                         recombination_rate=0.0,
                         demography=msprime.Demography.from_tree_sequence(ts),
-                        ancestral_Ne=10)
+                        ancestral_Ne=10,
+                        random_seed=123,
+            )
 
     def test_unique_names(self):
         ts = msprime.sim_ancestry(4, sequence_length=10, random_seed=12)
-        ts = pyslim.annotate_defaults(ts, model_type="nonWF", slim_generation=1)
+        ts = pyslim.annotate(ts, model_type="nonWF", tick=1)
         t = ts.dump_tables()
         md = t.populations[0].metadata
         md.update({"name": "ancestral"})
         t.populations[0] = t.populations[0].replace(metadata=md)
-        md.update({"name": "ancestral_ancestral", "slim_id": 4})
+        md.update({"name": "ancestral_ancestral", "slim_id": 1})
         t.populations.add_row(metadata=md)
         ts = t.tree_sequence()
         rts = self.do_recapitate(ts, ancestral_Ne=10)
         names = [pop.metadata['name'] for pop in rts.populations()]
         assert len(set(names)) == len(names)
-        assert "ancestral" in names
-        assert "ancestral_ancestral" in names
+        assert names[0] == "ancestral"
+        assert names[-2] == "ancestral_ancestral"
 
     def test_recapitation(self, recipe):
         ts = recipe["ts"]
         recomb_rate = 1.0 / ts.sequence_length
-        recap = self.do_recapitate(ts, recombination_rate=recomb_rate, ancestral_Ne=10)
+        recap = self.do_recapitate(
+                    ts,
+                    recombination_rate=recomb_rate,
+                    ancestral_Ne=10,
+                    random_seed=5
+        )
         # there should be no new mutations
         assert ts.num_mutations == recap.num_mutations
         assert ts.num_sites == recap.num_sites
         assert list(ts.tables.sites.position) == list(recap.tables.sites.position)
         self.check_recap_consistency(ts, recap)
 
-        if ts.metadata['SLiM']['generation'] < 200:
+        if ts.metadata['SLiM']['tick'] < 200:
             old_root_time = np.max(ts.tables.nodes.time)
             for t in recap.trees():
                 assert t.num_roots == 1
                 assert recap.node(t.root).time >= old_root_time
 
+    @pytest.mark.parametrize('recipe', recipe_eq(exclude="long"), indirect=True)
     def test_with_recomb_map(self, recipe):
         ts = recipe["ts"]
         recomb_rate = 1.0 / ts.sequence_length
         recombination_map = msprime.RateMap(
                    position = [0.0, ts.sequence_length],
-                   rate = [recomb_rate])
-        recap = self.do_recapitate(ts, recombination_rate=recombination_map, ancestral_Ne=1e-6)
+                   rate = [recomb_rate]
+        )
+        recap = self.do_recapitate(
+                    ts,
+                    recombination_rate=recombination_map,
+                    ancestral_Ne=1e-6,
+                    random_seed=875,
+        )
         self.check_recap_consistency(ts, recap)
 
     @pytest.mark.parametrize('recipe', recipe_eq("multipop"), indirect=True)
@@ -233,7 +158,7 @@ def test_with_demography(self, recipe):
                 extra_metadata={"slim_id": ts.num_populations},
         )
         demography.add_population_split(
-                time=ts.metadata["SLiM"]["generation"] + 1.0,
+                time=ts.metadata["SLiM"]["tick"] + 1.0,
                 derived=[p.name for p in demography.populations if p.name != "ancestral"],
                 ancestral="ancestral",
         )
@@ -241,76 +166,17 @@ def test_with_demography(self, recipe):
                 ts,
                 demography=demography,
                 recombination_rate=recomb_rate,
+                random_seed=333,
         )
         self.check_recap_consistency(ts, recap)
 
-    # Just test on the first recipe
-    @pytest.mark.parametrize('recipe', [next(recipe_eq())], indirect=True)
-    def test_old_recapitate_errors(self, recipe):
-        ts = recipe["ts"]
-        with pytest.raises(ValueError):
-            with pytest.warns(FutureWarning):
-                _ = ts.recapitate(
-                        recombination_rate=0.0,
-                        keep_first_generation=True)
-
-    def test_old_recapitation(self, recipe):
-        ts = recipe["ts"]
-        if ts.num_populations <= 2:
-            # if not we need migration rates
-            recomb_rate = 1.0 / ts.sequence_length
-            with pytest.warns(FutureWarning):
-                recap = ts.recapitate(recombination_rate=recomb_rate)
-            # there should be no new mutations
-            assert ts.num_mutations == recap.num_mutations
-            assert ts.num_sites == recap.num_sites
-            assert list(ts.tables.sites.position) == list(recap.tables.sites.position)
-            self.check_recap_consistency(ts, recap)
-
-            with pytest.warns(FutureWarning):
-                recap = ts.recapitate(recombination_rate=recomb_rate, Ne=1e-6)
-            self.check_recap_consistency(ts, recap)
-            if ts.metadata['SLiM']['generation'] < 200:
-                for t in recap.trees():
-                    assert abs(recap.node(t.root).time - recap.metadata['SLiM']['generation']) < 1e-4
-
-            # test with passing in a recombination map
-            recombination_map = msprime.RecombinationMap(
-                       positions = [0.0, ts.sequence_length],
-                       rates = [recomb_rate, 0.0],
-                       num_loci=int(ts.sequence_length))
-            with pytest.warns(FutureWarning):
-                    recap = ts.recapitate(recombination_map=recombination_map, Ne=1e-6)
-            self.check_recap_consistency(ts, recap)
-
-
-class TestIndividualMetadata(tests.PyslimTestCase):
-    # Tests for extra stuff related to Individuals.
-
-    def test_individual_derived_info(self, recipe):
-        ts = recipe["ts"]
-        for ind in ts.individuals():
-            for n in ind.nodes:
-                assert ts.node(n).population == ind.population
-                assert ts.node(n).time == ind.time
-
-    def test_individual_embellishments(self, recipe):
-        # Test the individual additional information.
-        ts = recipe["ts"]
-        is_wf = (ts.metadata["SLiM"]["model_type"] == "WF")
-        for j, ind in enumerate(ts.individuals()):
-            assert ts.individual_times[j] == ind.time
-            if is_wf:
-                assert ts.individual_ages[j] == 0
-            else:
-                assert ts.individual_ages[j] == ind.metadata["age"]
-            assert ts.individual_populations[j] == ind.population
-            assert np.array_equal(ts.individual_locations[j], ind.location)
-
+    @pytest.mark.parametrize('recipe', recipe_eq(exclude="long"), indirect=True)
     def test_first_gen_nodes(self, recipe):
         # check that all the roots of the trees are present
+        # (note this will fail if some populations were started at different
+        # times than others)
         ts = recipe["ts"]
-        root_time = ts.metadata['SLiM']['generation']
+        root_time = ts.metadata['SLiM']['tick']
         if (ts.metadata['SLiM']['stage'] == 'early'
                 or ts.metadata['SLiM']['model_type'] == 'nonWF'):
             root_time -= 1
@@ -326,7 +192,7 @@ class TestMutationMetadata(tests.PyslimTestCase):
     Tests for extra stuff related to Mutations.
     '''
 
-    @pytest.mark.parametrize('recipe', recipe_eq(exclude="init_mutated"), indirect=True)
+    @pytest.mark.parametrize('recipe', recipe_eq(exclude=("init_mutated", "long")), indirect=True)
     def test_slim_time(self, recipe):
         ts = recipe["ts"]
         # check that slim_times make sense, i.e., that
@@ -338,7 +204,7 @@ def test_slim_time(self, recipe):
         )
         for mut in ts.mutations():
             mut_slim_time = max([u["slim_time"] for u in mut.metadata["mutation_list"]])
-            assert ts.metadata['SLiM']['generation'] == mut_slim_time + mut.time + offset
+            assert ts.metadata['SLiM']['tick'] == mut_slim_time + mut.time + offset
 
 
 class TestIndividualAges(tests.PyslimTestCase):
@@ -349,54 +215,55 @@ def test_errors(self, recipe):
         ts = recipe["ts"]
         for stage in ['abcd', 10, []]:
             with pytest.raises(ValueError):
-                ts.individuals_alive_at(0, stage=stage)
+                pyslim.individuals_alive_at(ts, 0, stage=stage)
             with pytest.raises(ValueError):
-                ts.individuals_alive_at(0, remembered_stage=stage)
+                pyslim.individuals_alive_at(ts, 0, remembered_stage=stage)
             with pytest.raises(ValueError):
-                ts.individual_ages_at(0, stage=stage)
+                pyslim.individual_ages_at(ts, 0, stage=stage)
 
     @pytest.mark.parametrize('recipe', [next(recipe_eq("pedigree", "WF"))], indirect=True)
     def test_mismatched_remembered_stage(self, recipe):
         ts = recipe["ts"]
-        info = recipe["info"]
         if "remembered_early" in recipe:
             with pytest.warns(UserWarning):
-                ts.individuals_alive_at(0, remembered_stage="late")
+                pyslim.individuals_alive_at(ts, 0, remembered_stage="late")
         else:
             with pytest.warns(UserWarning):
-                ts.individuals_alive_at(0, remembered_stage="early")
+                pyslim.individuals_alive_at(ts, 0, remembered_stage="early")
 
     @pytest.mark.parametrize('recipe', recipe_eq("multipop", exclude="remembered_early"), indirect=True)
     def test_population(self, recipe):
         ts = recipe["ts"]
-        all_inds = ts.individuals_alive_at(0)
+        individual_populations = pyslim.individual_populations(ts)
+        all_inds = pyslim.individuals_alive_at(ts, 0)
+        assert len(all_inds) > 0
         for p in range(ts.num_populations):
-            sub_inds = ts.individuals_alive_at(0, population=p)
-            assert set(sub_inds) == set(all_inds[ts.individual_populations == p])
-            sub_inds = ts.individuals_alive_at(0, population=[p])
-            assert set(sub_inds) == set(all_inds[ts.individual_populations == p])
-        sub_inds = ts.individuals_alive_at(0, population=np.arange(p))
-        assert set(sub_inds) == set(all_inds[ts.individual_populations != p])
+            sub_inds = pyslim.individuals_alive_at(ts, 0, population=p)
+            assert set(sub_inds) == set(all_inds[individual_populations == p])
+            sub_inds = pyslim.individuals_alive_at(ts, 0, population=[p])
+            assert set(sub_inds) == set(all_inds[individual_populations == p])
+        sub_inds = pyslim.individuals_alive_at(ts, 0, population=np.arange(p))
+        assert set(sub_inds) == set(all_inds[individual_populations != p])
 
     @pytest.mark.parametrize('recipe', recipe_eq("nonWF", exclude="remembered_early"), indirect=True)
     def test_samples_only(self, recipe):
         ts = recipe["ts"]
-        all_inds = ts.individuals_alive_at(0)
-        assert set(all_inds) == set(ts.individuals_alive_at(0, samples_only=False))
+        all_inds = pyslim.individuals_alive_at(ts, 0)
+        assert set(all_inds) == set(pyslim.individuals_alive_at(ts, 0, samples_only=False))
         sub_inds = np.random.choice(all_inds, size=min(len(all_inds), 4), replace=False)
         flags = np.array([n.flags & (tskit.NODE_IS_SAMPLE * n.individual in sub_inds)
                           for n in ts.nodes()], dtype=np.uint32)
         tables = ts.dump_tables()
         tables.nodes.flags = flags
-        new_ts = pyslim.SlimTreeSequence(tables.tree_sequence())
-        assert set(sub_inds) == set(new_ts.individuals_alive_at(0, samples_only=True))
+        new_ts = tables.tree_sequence()
+        assert set(sub_inds) == set(pyslim.individuals_alive_at(new_ts, 0, samples_only=True))
 
-    @pytest.mark.parametrize('recipe', recipe_eq(exclude="remembered_early"), indirect=True)
+    @pytest.mark.parametrize('recipe', recipe_eq(exclude=("remembered_early", "long")), indirect=True)
     def test_after_simplify(self, recipe):
         ts = recipe["ts"]
         sts = ts.simplify()
-        orig_inds = ts.individuals_alive_at(0)
-        simp_inds = sts.individuals_alive_at(0)
+        orig_inds = pyslim.individuals_alive_at(ts, 0)
+        simp_inds = pyslim.individuals_alive_at(sts, 0)
         odict = {ts.individual(i).metadata["pedigree_id"]: i for i in orig_inds}
         sdict = {sts.individual(i).metadata["pedigree_id"]: i for i in simp_inds}
         for slim_id in odict:
@@ -412,13 +279,13 @@ def test_ages(self, recipe):
         info = recipe["info"]
         remembered_stage = 'early' if 'remembered_early' in recipe else 'late'
         assert remembered_stage == ts.metadata['SLiM']['stage']
-        max_time_ago = ts.metadata['SLiM']['generation']
+        max_time_ago = ts.metadata['SLiM']['tick']
         if remembered_stage == 'early':
             max_time_ago -= 1
         for time in range(0, max_time_ago):
             # if written out during 'early' in a WF model,
             # tskit time 0 will be the SLiM time step *before* slim_generation
-            slim_time = ts.metadata['SLiM']['generation'] - time
+            slim_time = ts.metadata['SLiM']['tick'] - time
             if remembered_stage == 'early' and ts.metadata["SLiM"]["model_type"] == "WF":
                 slim_time -= 1
             if remembered_stage == 'early' and time == 0:
@@ -428,16 +295,21 @@ def test_ages(self, recipe):
             else:
                 check_stages = ('early', 'late')
             for stage in check_stages:
-                alive = ts.individuals_alive_at(
+                alive = pyslim.individuals_alive_at(
+                            ts,
                             time,
                             stage=stage,
-                            remembered_stage=remembered_stage)
-                ages = ts.individual_ages_at(
+                            remembered_stage=remembered_stage
+                )
+                ages = pyslim.individual_ages_at(
+                            ts,
                             time,
                             stage=stage,
-                            remembered_stage=remembered_stage)
+                            remembered_stage=remembered_stage
+                )
                 for ind in ts.individuals():
-                    if 'everyone' in recipe or ind.time == 0:
+                    ind_time = ts.node(ind.nodes[0]).time
+                    if 'everyone' in recipe or ind_time == 0:
                         slim_id = ind.metadata["pedigree_id"]
                         assert slim_id in info
                         slim_alive = (slim_time, stage) in info[slim_id]['age']
@@ -460,6 +332,8 @@ def verify_has_parents(self, ts):
         node_indivs = ts.tables.nodes.individual
         parent_ids = [set() for _ in ts.individuals()]
         node_parent_ids = [set() for _ in ts.nodes()]
+        individual_times = pyslim.individual_times(ts)
+        individual_ages = pyslim.individual_ages(ts)
         for t in ts.trees():
             for i in ts.individuals():
                 if len(i.nodes) != 2:
@@ -473,14 +347,14 @@ def verify_has_parents(self, ts):
                         if p == tskit.NULL:
                             right_answer[i.id] = False
                         else:
-                            ptime = ts.individual_times[p]
+                            ptime = individual_times[p]
                             parent_alive = True
                             if ts.metadata["SLiM"]["model_type"] == "WF":
-                                if i.time + 1 != ptime:
+                                if individual_times[i.id] + 1 != ptime:
                                     parent_alive = False
                             else:
-                                pdeath = ptime - ts.individual_ages[p]
-                                if i.time + 1 < pdeath:
+                                pdeath = ptime - individual_ages[p]
+                                if individual_times[i.id] + 1 < pdeath:
                                     parent_alive = False
                             if not parent_alive:
                                 right_answer[i.id] = False
@@ -500,20 +374,22 @@ def verify_has_parents(self, ts):
             if right_answer[j]:
                 for pp in p:
                     right_parents.append([pp, j])
-        has_parents = ts.has_individual_parents()
+        has_parents = pyslim.has_individual_parents(ts)
         right_parents = np.sort(np.array(right_parents), axis=0)
-        parents = np.sort(ts.individual_parents(), axis=0)
+        parents = np.sort(pyslim.individual_parents(ts), axis=0)
         assert np.array_equal(right_answer, has_parents)
         assert np.array_equal(right_parents, parents)
 
     def get_first_gen(self, ts):
-        root_time = ts.metadata["SLiM"]["generation"]
+        root_time = ts.metadata["SLiM"]["tick"]
         if ts.metadata['SLiM']['model_type'] != 'WF' or ts.metadata['SLiM']['stage'] != 'late':
             root_time -= 1
-        first_gen = set(ts.tables.nodes.individual[ts.tables.nodes.time == root_time])
+        nodes = ts.tables.nodes
+        first_gen = set(nodes.individual[nodes.time == root_time])
         first_gen.discard(tskit.NULL)
         return np.array(list(first_gen), dtype='int')
 
+    @pytest.mark.skip("Waiting on next tskit release.")
     @pytest.mark.parametrize('recipe', recipe_eq("everyone"), indirect=True)
     def test_everyone(self, recipe):
         # since everyone is recorded, only the initial individuals should
@@ -522,10 +398,11 @@ def test_everyone(self, recipe):
         right_answer = np.repeat(True, ts.num_individuals)
         first_gen = self.get_first_gen(ts)
         right_answer[first_gen] = False
-        has_parents = ts.has_individual_parents()
+        has_parents = pyslim.has_individual_parents(ts)
         assert np.array_equal(right_answer, has_parents)
         self.verify_has_parents(ts)
 
+    @pytest.mark.skip("Waiting on next tskit release.")
     @pytest.mark.parametrize('recipe', recipe_eq("everyone"), indirect=True)
     def test_post_recap(self, recipe):
         # the same should be true after recapitation
@@ -534,26 +411,29 @@ def test_post_recap(self, recipe):
         first_gen = self.get_first_gen(ts)
         right_answer[first_gen] = False
         assert(ts.num_populations <= 2)
-        ts = self.do_recapitate(ts, recombination_rate=0.01, ancestral_Ne=10)
-        assert(ts.num_individuals == ts.num_individuals)
-        has_parents = ts.has_individual_parents()
+        ts = self.do_recapitate(ts, recombination_rate=0.01, ancestral_Ne=10, random_seed=11)
+        has_parents = pyslim.has_individual_parents(ts)
         assert np.array_equal(right_answer, has_parents)
         self.verify_has_parents(ts)
 
+    @pytest.mark.skip("Waiting on next tskit release.")
     @pytest.mark.parametrize('recipe', recipe_eq("everyone"), indirect=True)
     def test_post_simplify(self, recipe):
         ts = recipe["ts"]
         rng = np.random.default_rng(seed=3)
+        individual_times = pyslim.individual_times(ts)
         keep_indivs = rng.choice(
-                np.where(ts.individual_times < ts.metadata['SLiM']['generation'] - 1)[0],
-                size=30, replace=False)
+                np.where(individual_times < ts.metadata['SLiM']['tick'] - 1)[0],
+                size=30,
+                replace=False
+        )
         keep_nodes = []
         for i in keep_indivs:
             keep_nodes.extend(ts.individual(i).nodes)
         ts = ts.simplify(samples=keep_nodes, filter_individuals=True, keep_input_roots=True)
         assert(ts.num_populations <= 2)
         ts = self.do_recapitate(ts, recombination_rate=0.01, ancestral_Ne=10)
-        has_parents = ts.has_individual_parents()
+        has_parents = pyslim.has_individual_parents(ts)
         assert sum(has_parents) > 0
         self.verify_has_parents(ts)
 
@@ -567,8 +447,8 @@ def test_pedigree_parents_everyone(self, recipe):
         # and so the parentage would not be reported by `individual_parents()`.
         ts = recipe["ts"]
         info = recipe["info"]
-        has_parents = ts.has_individual_parents()
-        parents = ts.individual_parents()
+        has_parents = pyslim.has_individual_parents(ts)
+        parents = pyslim.individual_parents(ts)
         slim_map = {}
         for ind in ts.individuals():
             slim_map[ind.metadata["pedigree_id"]] = ind.id
@@ -597,8 +477,8 @@ def test_pedigree_parents(self, recipe):
         # or REMEMBERED individuals (not RETAINED), and the same for parental genomes.
         ts = recipe["ts"]
         info = recipe["info"]
-        has_parents = ts.has_individual_parents()
-        parents = ts.individual_parents()
+        has_parents = pyslim.has_individual_parents(ts)
+        parents = pyslim.individual_parents(ts)
         slim_map = {}
         for ind in ts.individuals():
             slim_map[ind.metadata["pedigree_id"]] = ind.id
@@ -629,28 +509,12 @@ def test_pedigree_parents(self, recipe):
                 assert a in gfolks
 
 
-class TestSimplify(tests.PyslimTestCase):
-    '''
-    Our simplify() is just a wrapper around the tskit simplify.
-    '''
-
-    def test_simplify(self, recipe):
-        ts = recipe["ts"]
-        sts = ts.simplify(map_nodes=False)
-        assert ts.sequence_length == sts.sequence_length
-        assert type(ts) == type(sts)
-        assert sts.samples()[0] == 0
-        sts, _ = ts.simplify(map_nodes=True)
-        assert ts.sequence_length == sts.sequence_length
-        assert type(ts) == type(sts)
-        assert sts.samples()[0] == 0
-
-
 class TestReferenceSequence(tests.PyslimTestCase):
     '''
     Test for operations involving the reference sequence
     '''
 
+    @pytest.mark.parametrize('recipe', recipe_eq(exclude="long"), indirect=True)
     def test_reference_sequence(self, recipe):
         ts = recipe["ts"]
         if ts.num_mutations > 0:
@@ -672,13 +536,13 @@ def test_mutation_at_errors(self, recipe):
         ts = recipe["ts"]
         u = ts.samples()[0]
         with pytest.raises(ValueError):
-            ts.mutation_at(-2, 3)
+            pyslim.mutation_at(ts, -2, 3)
         with pytest.raises(ValueError):
-            ts.mutation_at(u, -3)
+            pyslim.mutation_at(ts, u, -3)
         with pytest.raises(ValueError):
-            ts.mutation_at(ts.num_nodes + 2, 3)
+            pyslim.mutation_at(ts, ts.num_nodes + 2, 3)
         with pytest.raises(ValueError):
-            ts.mutation_at(u, ts.sequence_length)
+            pyslim.mutation_at(ts, u, ts.sequence_length)
 
     def test_nucleotide_at_errors(self, recipe):
         ts = recipe["ts"]
@@ -688,7 +552,7 @@ def test_nucleotide_at_errors(self, recipe):
             has_nucleotides = (mut_md["mutation_list"][0]["nucleotide"] >= 0)
             if not has_nucleotides:
                 with pytest.raises(ValueError):
-                    ts.nucleotide_at(u, 3)
+                    pyslim.nucleotide_at(ts, u, 3)
 
     def test_mutation_at(self, recipe):
         random.seed(42)
@@ -698,12 +562,12 @@ def test_mutation_at(self, recipe):
             pos = random.randint(0, ts.sequence_length - 1)
             tree = ts.at(pos)
             parent = tree.parent(node)
-            a = ts.mutation_at(node, pos)
+            a = pyslim.mutation_at(ts, node, pos)
             if parent == tskit.NULL:
                 assert a == tskit.NULL
             else:
-                b = ts.mutation_at(parent, pos)
-                c = ts.mutation_at(node, pos, ts.node(parent).time)
+                b = pyslim.mutation_at(ts, parent, pos)
+                c = pyslim.mutation_at(ts, node, pos, ts.node(parent).time)
                 assert b == c
                 for k in np.where(node == ts.tables.mutations.node)[0]:
                     mut = ts.mutation(k)
@@ -725,13 +589,13 @@ def test_nucleotide_at(self, recipe):
                     pos = random.randint(0, ts.sequence_length - 1)
                     tree = ts.at(pos)
                     parent = tree.parent(node)
-                    a = ts.nucleotide_at(node, pos)
+                    a = pyslim.nucleotide_at(ts, node, pos)
                     if parent == tskit.NULL:
                         nuc = ts.reference_sequence.data[int(pos)]
                         assert a == pyslim.NUCLEOTIDES.index(nuc)
                     else:
-                        b = ts.nucleotide_at(parent, pos)
-                        c = ts.nucleotide_at(node, pos, ts.node(parent).time)
+                        b = pyslim.nucleotide_at(ts, parent, pos)
+                        c = pyslim.nucleotide_at(ts, node, pos, ts.node(parent).time)
                         assert b == c
                         for k in np.where(node == ts.tables.mutations.node)[0]:
                             mut = ts.mutation(k)
@@ -739,6 +603,47 @@ def test_nucleotide_at(self, recipe):
                                 b = mut.metadata["mutation_list"][0]["nucleotide"]
                         assert a == b
 
+    @pytest.mark.parametrize('recipe', recipe_eq("mutation_spectrum"), indirect=True)
+    def test_nucleotide_spectrum(self, recipe):
+        # this is modified from Recipe 18.13
+        # Also note that we are comparing to "truth" in a recipe where
+        # truth is recorded during a mutation callback, in which we have
+        # access to the parental genome, so if two adjacent mutations
+        # occur in the same meiosis then each will not know about the other.
+        ts = recipe["ts"]
+        mutation_spectrum = recipe["mutation_info"]
+        M = {
+                a + b + c + "," + d : 0
+                for a in pyslim.NUCLEOTIDES
+                for b in pyslim.NUCLEOTIDES
+                for c in pyslim.NUCLEOTIDES
+                for d in pyslim.NUCLEOTIDES
+        }
+        nmuts = 0
+        for mut in ts.mutations():
+            pos = ts.site(mut.site).position 
+            if pos > 0 and pos < ts.sequence_length - 1:
+                nmuts += 1
+                mut_list = mut.metadata["mutation_list"]
+                k = np.argmax([u["slim_time"] for u in mut_list])
+                derived_nuc = mut_list[k]["nucleotide"]
+                left_nuc = pyslim.nucleotide_at(ts, mut.node, pos - 1, time = mut.time + 1.0)
+                right_nuc = pyslim.nucleotide_at(ts, mut.node, pos + 1, time = mut.time + 1.0)
+                parent_nuc = pyslim.nucleotide_at(ts, mut.node, pos, time = mut.time + 1.0)
+                context = "".join([pyslim.NUCLEOTIDES[k] for k in (left_nuc, parent_nuc, right_nuc)])
+                key = context + "," + pyslim.NUCLEOTIDES[derived_nuc]
+                M[key] += 1
+                if key == "ACA,T" or key == "CCA,T":
+                    print(key, pos, mut.node, mut.time)
+        assert sum([M[k] for k in M]) == nmuts
+        assert sum([mutation_spectrum[k][0] for k in mutation_spectrum]) == nmuts
+        for k in M:
+            if M[k] != mutation_spectrum[k][0]:
+                print(k, M[k], mutation_spectrum[k])
+        for k in M:
+            assert len(mutation_spectrum[k]) == 1
+            assert M[k] == mutation_spectrum[k][0]
+
 
 class TestConvertNucleotides(tests.PyslimTestCase):
     '''
@@ -806,7 +711,7 @@ def test_convert_alleles_errors(self):
         ts = msprime.sim_ancestry(4, sequence_length=10, population_size=10)
         with pytest.raises(ValueError, match="must have a valid reference sequence"):
             _ = pyslim.convert_alleles(ts)
-        ts = pyslim.annotate_defaults(ts, model_type="nonWF", slim_generation=1)
+        ts = pyslim.annotate(ts, model_type="nonWF", tick=1)
         with pytest.raises(ValueError, match="must have a valid reference sequence"):
             _ = pyslim.convert_alleles(ts)
         mts = msprime.sim_mutations(ts,
@@ -840,7 +745,7 @@ def test_convert_alleles(self, recipe):
         self.verify_converted_nucleotides(ts, cts)
 
     def test_generate_nucleotides_errors(self):
-        ts = msprime.sim_ancestry(4, sequence_length=10, population_size=10)
+        ts = msprime.sim_ancestry(4, sequence_length=10, population_size=10, random_seed=777)
         with pytest.raises(ValueError, match="must have length equal"):
             _ = pyslim.generate_nucleotides(ts, reference_sequence="AAA")
         with pytest.raises(ValueError, match="must have length equal"):
@@ -899,7 +804,7 @@ def test_generate_nucleotides_refseq(self):
                 population_size=10,
                 random_seed=10,
         )
-        ts = pyslim.annotate_defaults(ts, model_type='nonWF', slim_generation=1)
+        ts = pyslim.annotate(ts, model_type='nonWF', tick=1)
         mts = msprime.sim_mutations(ts,
                 model=msprime.SLiMMutationModel(type=1),
                 rate=0.5,
@@ -911,7 +816,7 @@ def test_generate_nucleotides_refseq(self):
 
     def test_generate_nucleotides_keep(self):
         ts = msprime.sim_ancestry(4, sequence_length=10, population_size=10)
-        ts = pyslim.annotate_defaults(ts, model_type='nonWF', slim_generation=1)
+        ts = pyslim.annotate(ts, model_type='nonWF', tick=1)
         mts1 = msprime.sim_mutations(ts,
                 model=msprime.SLiMMutationModel(type=1),
                 rate=0.1,
@@ -963,9 +868,10 @@ def test_generate_and_convert(self, recipe, helper_functions, tmp_path):
 
 class TestDeprecations(tests.PyslimTestCase):
     # test on one arbitrary recipe
+    @pytest.mark.skip(reason="TODO")
     @pytest.mark.parametrize('recipe', [next(recipe_eq())], indirect=True)
-    def test_first_gen(self, recipe):
+    def test_slim_tree_sequence(self, recipe):
         ts = recipe["ts"]
         with pytest.warns(FutureWarning):
-            _ = ts.first_generation_individuals()
+            _ = pyslim.SlimTreeSequence(ts)
 
diff --git a/tests/test_util.py b/tests/test_util.py
index f0f62ee6..98794313 100644
--- a/tests/test_util.py
+++ b/tests/test_util.py
@@ -15,7 +15,7 @@ def verify_unique_labels_by_group(self, group, label, minlength):
             u = set(label[group == g])
             if (len(u) == 1) != x[g]:
                 print(g, u, x[g], label[group == g], label.dtype)
-            assert (len(u) == 1) == x[g]
+            assert (len(u) <= 1) == x[g]
 
     def test_all_same(self):
         n = 10
@@ -24,13 +24,13 @@ def test_all_same(self):
         self.verify_unique_labels_by_group(group, label, 1)
         x = pyslim.util.unique_labels_by_group(group, label, 1)
         assert len(x) == 2
-        assert x[0] == False
+        assert x[0] == True
         assert x[1] == False
         label = np.repeat(5, 10)
         self.verify_unique_labels_by_group(group, label, 1)
         x = pyslim.util.unique_labels_by_group(group, label, 1)
         assert len(x) == 2
-        assert x[0] == False
+        assert x[0] == True
         assert x[1] == True
 
     def test_all_unique(self):
@@ -62,4 +62,10 @@ def test_unique_labels_by_group(self):
                         label = minl + np.random.choice(np.random.uniform(0, 1, nl), size=n)
                         self.verify_unique_labels_by_group(group, label, ng)
 
+    def test_unused_labels(self):
+        x = pyslim.unique_labels_by_group(
+            group=np.array([1, 1, 4], dtype='int'),
+            label=np.array([2, 2, 2], dtype='int')
+        )
+        assert np.all(x)