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

Issue:
read new section on time units
You can read the new section I wrote at
  https://tskit.dev/pyslim/docs/latest/time_units.html

The last section ("Converting to SLiM time and back") is not new (although I made edits); however, the stuff before that is new and should have a wide-ish audience. It'd be nice if someone had a read of it to see if you agree and if it makes sense. @bhaller?


Discussion / hints:
Hey.  Catching up on things.  First of all, in reading your doc I realized that the doc for SLiM 4 had not been updated correctly to reflect the new policy (which you state correctly in what you wrote) that the defaunt time unit is now `"ticks"` in all cases.  I've just rewritten the doc as follows:

> The timeUnit parameter controls the time unit stated in the tree sequence when it is saved (which can be accessed through tskit APIs); it has no effect on the running simulation whatsoever.  The default value, NULL, means that a time unit of "ticks" will be used for all model types.  (In SLiM 3.7 / 3.7.1, NULL implied a time unit of "generations" for WF models, but "ticks" for nonWF models; given the new multispecies timescale parameters in SLiM 4, a default of "ticks" makes sense in all cases since now even in WF models one tick might not equal one biological generation.)  It may be helpful to set timeUnit to "generations" explicitly when modeling non-overlapping generations in which one tick equals one generation, to tell tskit that the time unit does in fact represent biological generations; doing so may avoid warnings from tskit or msprime regarding the time unit, in cases such as recapitation where the simulation timescale is important.

I'll probably rip a new PDF of the manual to reflect this change.  But anyhow, you write that it is always `"ticks"` so this is just a doc problem on my end, and I don't think affects anything you have done/written.

Edits:

- reword "one spin around SLiM’s life cycle corresponds to" to "corresponds to one spin around SLiM’s life cycle" to make it less confusing :->

- "and you want to put neutral mutations with msprime" – "put in", or I like "overlay"

- "we need to divide the coalescence rate by T ," – there seems to be a space after T and before the comma, the line breaks before the comma

- "and so rate of r Morgans" – "a rate of"

- "Since msprime (by default) runs a continuous-time simulation, there is no harm in adjusting time units in msprime this way, but it is confusing." – so, is there a way to make all this less confusing?  Can the user rescale the time used by their tree sequence, in Python, so that then they can do mutation overlay, recapitation, etc., without having these confusing timescale issues because their tree sequence is now in whatever time unit is "natural" for tskit?  That seems like it would be the best; maybe that's a feature request for tskit, or for pyslim?  Basically a "rescale to units of generations" call, which would be passed a rescaling factor T, and would both rescale all times in the tree sequence, and change the time unit to "generations".

- "The definition depends on what we mean by the various “average”s, but this is not the place to explain the distinctions."  Hmm – maybe it _is_, actually?  I mean, if not here, where?  This kind of leaves the reader wondering what they're missing, and worried that their lack of understanding of this might mean that they're doing something wrong in their analysis.

- "then to take the average across all individuals alive at a given time of the age..." – would be easier to parse with commas: "then to take the average, across all individuals alive at a given time, of the age..."

- "assign to each individual a tag that records the average age of their parents" – hmm, interesting script.  I was expecting you to do this analysis on the Python side, but I guess once simplification happens there's not enough information left in the tree sequence to determine parental ages at birth.  So you have to do it in SLiM, hmm.  Should this be something that SLiM tracks automatically, as it now tracks lifetime reproductive output automatically?  I guess when doing these timescale adjustments and so forth, this is something that it will be very common for users to need to figure out; so maybe built-in support would be useful, especially since many models will already be using `tag` for other purposes, making your recipe difficult to adapt.  I think the memory/time overhead of adding automatic tracking of this – maybe not as the mean of the parental ages, but instead as the age of each parent separately – would be pretty inconsequential.

- "measures the number of time steps (“tick”) since" – should be plural, "ticks"

- "it is “first → “early → birth → late”, while in a nonWF model, it is “first → “birth → early → late”" – I'd get rid of the quotes around these (the italics makes them easy to visually separate anyway); in any case, note that there is an extra quote before "early" in both sequences

- "Usually, the first set of individuals are created in the early() phase of tick 1" – I'm not sure I'd say this is "usual".  Creating the initial subpop in late() is also quite common.  "Often" would be better.

- "As the simulation goes along, tskit time ago is recorded as minus one times the number of birth phases so far" – I'd add " – multiplied by minus one because the simulation is, in fact, going forward in time, which is a negative number of time-ago units" or some such.

- "but in a WF they are not" – "WF model"

- "To see the nonWF model" – "nonWF model schematic"

- "birth happens between the “early()” and “late()” stages" – I would get rid of the quotes around these, here and elsewhere; unnecessary and just makes it harder to read

- "The stage that the tree sequence was saved is recorded" – "The stage in which the tree sequence was saved is recorded"

- "(This means you must specify the stage of the block in your SLiM script, since the stage defaults to “early()”!)" – happily, this default to early() is now gone, so you can delete this sentence.

- Maybe it can somehow be made more clear, witha bounding box that is drawn connected to the tabs, or "blockquote" indentation, or something, that the tabs only switch out the table itself, and not the text below?  I read the content for the WF tab, and then when I switched to the nonWF tab I expected the text below the table to change to talk about nonWF stuff; in particular, I expected the sentence "However, in WF models, birth happens between the “early()” and “late()” stages..." to go away, since I was now reading about nonWF models.  I was initially confused that that text did not change to match my tab choice above.  Not sure what the right fix is, with what you are able to do in the styling supported by whatever doc tools you're using...

Overall, LGTM!  Whew!

But I think this review was very helpful for leading to the two ideas above: that SLiM could keep track of parent ages, and that there could be an easy way to rescale a given tree sequence from ticks to generations, given a biological generation time (perhaps automatically derived from SLiM's proposed automatic recording of parent ages!) as a scaling factor.  I think if we do those two thing this whole story becomes much, much simpler for users.

I also think that a pass over the SLiM manual's chapter 17, with an eye towards adding mention of this and other real-world issues, and adding cross-links into the pyslim documentation, would be a Very Good Idea.  I would suggest that you and I ought to do this kind of separately-but-together: do it one subsection at a time, reading and thinking separately and then having a shared-screen discussion and revision of the manual for that one subsection, then moving on to the next.  If we do one subsection at a time, we can spread this work over a longer period of time and make the project less monolithic.  It feels important to do, though; SLiM users need to know about these sorts of issues, and need to go to the pyslim doc to read about them, and the existing general statement to "Go read the pyslim doc, it's important" is presumably ignored/forgotten by most users.
> "Since msprime (by default) runs a continuous-time simulation, there is no harm in adjusting time units in msprime this way, but it is confusing." – so, is there a way to make all this less confusing? Can the user rescale the time used by their tree sequence, in Python, so that then they can do mutation overlay, recapitation, etc., without having these confusing timescale issues because their tree sequence is now in whatever time unit is "natural" for tskit? That seems like it would be the best; maybe that's a feature request for tskit, or for pyslim? Basically a "rescale to units of generations" call, which would be passed a rescaling factor T, and would both rescale all times in the tree sequence, and change the time unit to "generations".

This is tempting, but it's not at all clear how we'd actually implement this. Maybe just having a method to rescale all the times (so one could convert to generations) would make things a lot easier? I'm not totally convinced it would, because 'generations' is more abstract ('years' is better), and the notion of generation time is slippery (depends on context; the correct notion of generation time for effective population size is actually slightly different than for mutation rate...).
> This is tempting, but it's not at all clear how we'd actually implement this. Maybe just having a method to rescale all the times (so one could convert to generations) would make things a lot easier? I'm not totally convinced it would, because 'generations' is more abstract ('years' is better), and the notion of generation time is slippery (depends on context; the correct notion of generation time for effective population size is actually slightly different than for mutation rate...).

Hmm.  Well, I certainly don't understand all the ins and outs of this issue.  But your doc suggests ways to rescale various parameters passed in to msprime to compensate for the difference between the tree sequence's timescale and msprime's intrinsic timescale.  All I'm proposing is to provide a method that does that rescaling up front, rather than for every call to msprime, fixing the timescale of the tree sequence once and for all, so that those rescaling issues are put to rest and the user can stop worrying about them from then on.  There are doubtless difficult issues to definitively pin down with respect to this – but those same issues would exist equally for rescaling the parameters passed to msprime, no?  So that doesn't seem like an argument against providing such a method – the same objection applies equally to doing the scaling with each call to msprime, no?
Hm - perhaps msprime methods could have a `generation_time` argument; or tree sequences could have a `generation_time` property. Seems like there's gotchas with either proposal. It's definitely an msprime proposal, though, not a pyslim one.
> Should this be something that SLiM tracks automatically, as it now tracks lifetime reproductive output automatically? 

That could be quite useful, yes! We should do some testing though first, to double check that we're measuring generation time appropriately.
> "birth happens between the “early()” and “late()” stages" – I would get rid of the quotes around these, here and elsewhere; unnecessary and just makes it harder to read

I think we need something that makes it super clear we're talking about "the early() stage" instead of "an early period of time", if you know what I mean. But, maybe italics + () will do it?
> I also think that a pass over the SLiM manual's chapter 17, with an eye towards adding mention of this and other real-world issues, and adding cross-links into the pyslim documentation, would be a Very Good Idea.

Yep; this is rather unfortunately a complex topic. (mostly thanks to interfacing with coalescent theory land, really)
> Hm - perhaps msprime methods could have a `generation_time` argument; or tree sequences could have a `generation_time` property. Seems like there's gotchas with either proposal. It's definitely an msprime proposal, though, not a pyslim one.

Hmm.  I feel like shifting the timescale from SLiM's conception of time (ticks) to msprime's (generations) is very natural as a pyslim feature.  I'm surprised you're not on board with this, it seems like a slam dunk to me.  Just rescale all the times in the tree sequence by the factor provided by the user, change the time unit tag, and the timescale issue is thereby resolved, almost completely painlessly.
> > "birth happens between the “early()” and “late()” stages" – I would get rid of the quotes around these, here and elsewhere; unnecessary and just makes it harder to read
> 
> I think we need something that makes it super clear we're talking about "the early() stage" instead of "an early period of time", if you know what I mean. But, maybe italics + () will do it?

Even without the italics I think the parentheses are pretty unambiguous.  Anybody who has used SLiM at all knows what that syntactic convention means.
>  I read the content for the WF tab, and then when I switched to the nonWF tab I expected the text below the table to change to talk about nonWF stuff;

Er, it certainly should go away? Here's what I see when I click on "nonWF"?
![Screenshot from 2022-08-15 06-40-44](https://user-images.githubusercontent.com/1046249/184646410-c5a25084-e8b2-4bc4-9adc-0803640b7a5d.png)

> > Hm - perhaps msprime methods could have a `generation_time` argument; or tree sequences could have a `generation_time` property. Seems like there's gotchas with either proposal. It's definitely an msprime proposal, though, not a pyslim one.
> 
> Hmm. I feel like shifting the timescale from SLiM's conception of time (ticks) to msprime's (generations) is very natural as a pyslim feature. I'm surprised you're not on board with this, it seems like a slam dunk to me. Just rescale all the times in the tree sequence by the factor provided by the user, change the time unit tag, and the timescale issue is thereby resolved, almost completely painlessly.

Good point; I guess I am not totally on board because I would rather work in units of years than generations - it's more concrete. But, maybe it is the right thing to do? One bother is we'd not be able to shift the origination time attribute of mutations (since this is an int).

Fix patch:
diff --git a/docs/time_units.md b/docs/time_units.md
index ad7b0c90..7cfb9b54 100644
--- a/docs/time_units.md
+++ b/docs/time_units.md
@@ -31,20 +31,29 @@ ts = tskit.load("example_sim.trees")
 There are a number of subtle ways that time units can trip up the user;
 below are explanations of how to deal with most of these topics.
 At issue is the fact that tree sequences produced by SLiM
-have times in units of "ticks":
-
+have times in units of "ticks", rather than generations. 
+If we query such a tree sequence for its time units we see 
+its return value
 ```{code-cell}
 ts.time_units
 ```
 
-... and it can be quite conceptually tricky to make sure that time units
+In general this mean it can be conceptually tricky to make sure that time units
 are consistent across different stages of simulation.
 
+(Note: it *is* possible to set the time units to whatever you want,
+using the ``timeUnit`` parameter to ``initializeTreeSeq( )``,
+but this only affects this label in metadata (i.e., the output of
+``ts.time_units``, and does not actually change how times are recorded
+in the tree sequence.)
+
+
 ## Mutation rates with msprime
 
 Only you really know how much time in the real world
+corresponds to one spin around SLiM's life cycle.
 one spin around SLiM's life cycle corresponds to.
-So, SLiM does not take a stand on what its time units actually are,
+So, SLiM does not specify what its time units actually are,
 and refers to them as "ticks".
 Previous versions of SLiM called these "generations",
 but this is only correct for simulations in which all organisms die every tick.
@@ -55,7 +64,7 @@ or maybe you need to multiply by a (mean) generation time known from empirical d
 However, it becomes important to know what the time units are
 when interfacing with msprime.
 For instance, suppose you've done a WF simulation for an organism whose mean generation time
-in the real world is 30 years, and you want to put neutral mutations with msprime.
+in the real world is 30 years, and you want to overlay neutral mutations using msprime.
 The mutation rate passed to msprime is in units of mutations per unit-of-time-in-the-tree-sequence,
 so since you did a WF simulation, the units of time in the tree sequence are in generations,
 and you should give it mutation rate in mutations per generation.
@@ -74,17 +83,17 @@ is {math}`1/2N`, where {math}`N` is the (effective) size of that population.
 This is the correct rate, *in units of coalescences per generation*.
 Since it's "per generation", this means that if you want to recapitate a tree sequence
 generated by SLiM in which one tick is not one generation,
-then you need to adjust the effective population sizes accordingly.
+then **you need to adjust the effective population sizes accordingly**.
 For instance, suppose as above that we did a nonWF simulation
 in which one tick = one year, for a species with generation time 30 years,
 and we want to recapitate with an effective size of 10,000 diploids.
 An effective size of 10,000 diploids means one coalescence per pair of lineages
 every 20,000 generations;
 since one unit of time in the tree sequence equals 1/30th of a generation,
-this is one coalescence per pair of lineages every 600,000 generations,
+this is one coalescence per pair of lineages every 600,000 time units,
 which we can get msprime to do by asking for an effective population size of 300,000.
 In other words, if a generation in our tree sequence is {math}`T` units of time,
-then we need to divide the coalescence rate by {math}`T`,
+then we need to divide the coalescence rate by {math}`T,`
 which we do by multiplying effective population sizes by {math}`T`.
 
 (A better way of thinking about this could be to say that since the mean time to coalescence
@@ -96,7 +105,7 @@ Since these are rates, they are also in units of something "per time",
 and so just as for mutation rates, we need to ensure they are in the correct units.
 Recombination rates in particular come in units of "per generation"
 (since a Morgan is in units of recombinations per generation),
-and so rate of {math}`r` Morgans would need to be divided by {math}`T`
+and so a rate of {math}`r` Morgans would need to be divided by {math}`T`
 before it's passed to msprime for recapitation.
 Since msprime (by default) runs a continuous-time simulation,
 there is no harm in adjusting time units in msprime this way,
@@ -133,7 +142,7 @@ with warnings.catch_warnings(record=True) as w:
 ```
 
 Think of this warning as a friendly reminder to pay attention to your time units.
-To make this warning dissappear, the easiest option is to do as the warning suggests
+To make this warning disappear, the easiest option is to do as the warning suggests
 and just ask python to ignore it:
 
 ```{code-cell}
@@ -160,7 +169,7 @@ print(f"Time units after: {ts.time_units}")
 ## Example: calculating generation times in a SLiM simulation
 
 In a nonWF simulation, the generation time is not something that you get to explicitly set:
-rather, like poulation size it is an emergent property of the rules of the simulation.
+rather, like population size it is an emergent property of the rules of the simulation.
 So, it's important to be able to calculate what the (average) generation time actually is
 in a given simulation (keeping in mind that the answer may vary across time and space).
 This is not really a pyslim operation at all,
@@ -170,10 +179,14 @@ The definition of generation time appropriate for comparison to a coalescent sim
 is "average amount of time between birth of parent and birth of child".
 (Note that this is the birth of the *average* child in some sense,
 not the average time to each parent's first child!)
-The definition depends on what we mean by the various "average"s,
-but this is not the place to explain the distinctions.
-An easy and convenient definition is then to take the average across all individuals
-alive at a given time of the age of their parents at the times of their births.
+Therefore,
+an easy and convenient way to measure this is to
+record for each individual their parents' ages at the times of their births,
+and then take the average across all individuals alive at a given time.
+(This is only one possible choice,
+and other choices are usually but not always equivalent,
+but a discussion of the options and distinctions
+would make this tutorial lengthy and confusing.
 
 Here is a script that computes this.
 In the simulation, females' fecundity increases with their age,
@@ -254,7 +267,7 @@ 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
-("tick") since the start of the simulation,
+("ticks") since the start of the simulation,
 and times in the tree sequence record how long before the end of the simulation.
 However, there are quite a few details, and off-by-one errors are easy to make,
 so we'll spell it out here.
@@ -266,9 +279,9 @@ without needing to actually do any conversions yourself.
 SLiM's time counter is called the "tick".
 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 "*first* {math}`\to` "*early* {math}`\to` *birth* {math}`\to` *late*",
-while in a nonWF model, it is "*first* {math}`\to` "*birth* {math}`\to` *early* {math}`\to` *late*".
-Usually, the first set of individuals are created in the *early()* phase of tick 1,
+in a WF model, it is *first* {math}`\to` *early* {math}`\to` *birth* {math}`\to` *late*",
+while in a nonWF model, it is *first* {math}`\to` *birth* {math}`\to` *early* {math}`\to` *late*.
+Often, 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.
@@ -281,16 +294,41 @@ SLiM also keeps track of "how many birth phases of the life cycle have happened
 (the column "# births" in the tables).
 As the simulation goes along,
 tskit time ago is recorded as minus one times the number of birth phases so far.
+(We multiply by -1 because as we move forward in time,
+we accumulate a negative number of "time-ago" units.)
 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 ("tick" and "number of birth phases")
-are always in sync; but in a WF they are not (during *first* and *early*).
+are always in sync; but in a WF model they are not (during *first* and *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*.
 
+When the tree sequence is written out, SLiM records the value of its current tick
+and the current stage,
+which can be found in the metadata: ``ts.metadata['SLiM']['tick']``,
+and ``ts.metadata['SLiM']['stage']``.
+The "SLiM time" referred to by a ``time`` 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 ``time`` from ``ts.metadata['SLiM']['tick']``
+and possibly adding or subtracting one depending on the current stage.
+The fiddly details are worked out by the {func}`.slim_time` method,
+which takes a ``stage`` argument.
+
+Some of the other methods in pyslim -- those that depend on {func}`.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 can add another factor of plus or minus one to the calculations.)
+This argument, ``remembered_stage``, defaults to *late()*;
+we recommend that you also default to always Remembering individuals, and saving out the tree sequence,
+during *late()* as well, unless you have good reason not to.
+
+### Detailed description of conversion
+
+
 To help keep all this straight, here are schematics for WF and nonWF models.
-(To see the nonWF model, click on the tab.)
+(To see the nonWF model schematic, click on the "nonWF" tab.)
 
 ```{tabbed} WF model
 
@@ -301,8 +339,8 @@ The answer to this depends on what stage, unfortunately.
 Let {math}`k` be the value of the tick counter, {math}`n` be the total number of ticks,
 i.e., the value of ``ts.metadata["SLiM"]["tick"]``,
 {math}`t` be the tskit time,
-{math}`x=1` if the stage is "first" or "early' and 0 otherwise,
-and {math}`y=1` if the tree sequence was output in "late".
+{math}`x=1` if the stage is *first()* or *early()* and 0 otherwise,
+and {math}`y=1` if the tree sequence was output in *late()*.
 Then, as can be verified from the table below, {math}`k + t = n + x + y - 1`.
 The right-hand columns show {math}`t`, i.e.,
 what the **tskit time ago** is at the corresponding point in the SLiM simulation.
@@ -347,7 +385,7 @@ Let {math}`k` be the value of the tick counter, {math}`n` be the total number of
 i.e., the value of ``ts.metadata["SLiM"]["tick"]``,
 let {math}`t` be the tskit time,
 {math}`x=1` if the stage is "first" and 0 otherwise,
-and {math}`y=1` if the tree sequence was output in "early" or "late".
+and {math}`y=1` if the tree sequence was output in *early()* or *late()*.
 Then, as can be verified from the table below, {math}`k + t = n + x + y - 1`.
 The right-hand columns show {math}`t`, i.e.,
 what the **tskit time ago** is at the corresponding point in the SLiM simulation.
@@ -381,43 +419,4 @@ what the **tskit time ago** is at the corresponding point in the SLiM simulation
 |       n            |       late          |       n            |                                |                            | treeSeqOutput {math}`\to`  |         0        |
 
 ```
-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 ``community.tick``
-within SLiM at the point in time thus referenced)
-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.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``
-to SLiM time as follows:
-
-```{code-cell}
-def slim_time(ts, time, stage):
-  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
-```
-
-This is what is computed by the {func}`.slim_time` method
-(which also has a ``stage`` argument).
-
-Some of the other methods in pyslim -- those that depend on {func}`.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()";
-we recommend that you also default to always Remembering individuals, and saving out the tree sequence,
-during "late()" as well, unless you have good reason not to.
-(This means you *must specify* the stage of the block in your SLiM script,
-since the stage defaults to "early()"!)
 
diff --git a/setup.py b/setup.py
index dfb978db..84f4f202 100644
--- a/setup.py
+++ b/setup.py
@@ -31,8 +31,8 @@
       install_requires=[
           'msprime>=1.0.1',
           'tskit',
-          'kastore',
-          'numpy'],
+          'numpy'
+      ],
       extras_require={
           'dev': [],
       },
@@ -42,4 +42,4 @@
           'Bug Reports': 'https://github.com/tskit-dev/pyslim/issues',
           'Source': 'https://github.com/tskit-dev/pyslim',
       },
-      )
+)


Test patch:
diff --git a/tests/__init__.py b/tests/__init__.py
index 9381aec8..56c4e424 100644
--- a/tests/__init__.py
+++ b/tests/__init__.py
@@ -4,14 +4,12 @@
 import os
 import json
 import random
-import base64
 import warnings
 
 import pyslim
 import tskit
 import msprime
 import pytest
-import attr
 import numpy as np
 
 
diff --git a/tests/test_spatial.py b/tests/test_spatial.py
index 01315852..1f55bfbd 100644
--- a/tests/test_spatial.py
+++ b/tests/test_spatial.py
@@ -1,7 +1,6 @@
 """
 Test cases for tree sequences.
 """
-import pickle
 import random
 import os
 
diff --git a/tests/test_tree_sequence.py b/tests/test_tree_sequence.py
index 3a884f94..a06e7f1f 100644
--- a/tests/test_tree_sequence.py
+++ b/tests/test_tree_sequence.py
@@ -1,7 +1,6 @@
 """
 Test cases for tree sequences.
 """
-import pickle
 import random
 import numpy as np
 import os