From 1003fb74492699165748534d2e60ee0b737580a6 Mon Sep 17 00:00:00 2001 From: peter Date: Wed, 24 Jun 2026 16:14:37 -0700 Subject: [PATCH] switch to building slim from source a start at new metadata more testing for add_mutation_metadata test that catches duplicated substitution bug in SLiM added passing around ts_metadata dev slim in docs build? --- .github/workflows/docs.yml | 3 +- .github/workflows/lint.yml | 2 +- .github/workflows/tests.yml | 27 +- CHANGELOG.rst | 47 +- docs/metadata.md | 127 +- docs/previous_versions.md | 79 +- docs/python_api.md | 36 +- docs/rapid_adaptation.slim | 2 +- docs/selection.slim | 2 +- docs/time_units.md | 4 +- docs/tutorial.md | 197 ++- docs/vignette_coalescent_diversity.md | 77 +- docs/vignette_continuing.md | 30 +- docs/vignette_parallel_phylo.md | 9 +- docs/vignette_space.md | 14 +- pyproject.toml | 3 +- pyslim/_version.py | 4 +- pyslim/methods.py | 349 +++-- pyslim/slim_metadata.py | 1373 +++++++++++++---- pyslim/slim_tree_sequence.py | 59 +- tests/__init__.py | 39 +- tests/conftest.py | 3 +- tests/recipe_specs.py | 32 +- tests/test_annotation.py | 314 +++- tests/test_metadata.py | 195 ++- tests/test_provenance.py | 338 ++-- ..._v3_tests.sh => make_old_file_versions.sh} | 16 + tests/test_recipes/recipe_WF.slim | 37 +- tests/test_recipes/recipe_WF.v5.2.trees | Bin 0 -> 36020 bytes tests/test_recipes/recipe_WF_X.v5.2.trees | Bin 0 -> 134436 bytes tests/test_recipes/recipe_WF_Y.v5.2.trees | Bin 0 -> 55716 bytes tests/test_recipes/recipe_adds_old_muts.slim | 30 + .../chromosome_A.trees | Bin 0 -> 75780 bytes .../chromosome_FL.trees | Bin 0 -> 40948 bytes .../chromosome_H.trees | Bin 0 -> 52420 bytes .../chromosome_HF.trees | Bin 0 -> 47996 bytes .../chromosome_HM.trees | Bin 0 -> 47068 bytes .../chromosome_ML.trees | Bin 0 -> 38284 bytes .../chromosome_W.trees | Bin 0 -> 40676 bytes .../chromosome_X.trees | Bin 0 -> 66364 bytes .../chromosome_Y.trees | Bin 0 -> 38644 bytes .../chromosome_Z.trees | Bin 0 -> 61932 bytes .../chromosome_nY.trees | Bin 0 -> 37900 bytes .../recipe_chromosomes_adds_muts.slim | 62 +- tests/test_recipes/recipe_no_simplify.slim | 22 + tests/test_recipes/recipe_nonWF.slim | 38 +- tests/test_recipes/recipe_nonWF.v5.2.trees | Bin 0 -> 25044 bytes tests/test_recipes/recipe_nucleotides_WF.slim | 45 +- .../recipe_nucleotides_nonWF.slim | 46 +- tests/test_recipes/recipe_with_traits.slim | 101 ++ tests/test_tree_sequence.py | 463 ++++-- uv.lock | 54 +- 52 files changed, 3246 insertions(+), 1033 deletions(-) rename tests/test_recipes/{make_v3_tests.sh => make_old_file_versions.sh} (80%) create mode 100644 tests/test_recipes/recipe_WF.v5.2.trees create mode 100644 tests/test_recipes/recipe_WF_X.v5.2.trees create mode 100644 tests/test_recipes/recipe_WF_Y.v5.2.trees create mode 100644 tests/test_recipes/recipe_adds_old_muts.slim create mode 100644 tests/test_recipes/recipe_all_the_chromosome_types.v5.2.trees/chromosome_A.trees create mode 100644 tests/test_recipes/recipe_all_the_chromosome_types.v5.2.trees/chromosome_FL.trees create mode 100644 tests/test_recipes/recipe_all_the_chromosome_types.v5.2.trees/chromosome_H.trees create mode 100644 tests/test_recipes/recipe_all_the_chromosome_types.v5.2.trees/chromosome_HF.trees create mode 100644 tests/test_recipes/recipe_all_the_chromosome_types.v5.2.trees/chromosome_HM.trees create mode 100644 tests/test_recipes/recipe_all_the_chromosome_types.v5.2.trees/chromosome_ML.trees create mode 100644 tests/test_recipes/recipe_all_the_chromosome_types.v5.2.trees/chromosome_W.trees create mode 100644 tests/test_recipes/recipe_all_the_chromosome_types.v5.2.trees/chromosome_X.trees create mode 100644 tests/test_recipes/recipe_all_the_chromosome_types.v5.2.trees/chromosome_Y.trees create mode 100644 tests/test_recipes/recipe_all_the_chromosome_types.v5.2.trees/chromosome_Z.trees create mode 100644 tests/test_recipes/recipe_all_the_chromosome_types.v5.2.trees/chromosome_nY.trees create mode 100644 tests/test_recipes/recipe_no_simplify.slim create mode 100644 tests/test_recipes/recipe_nonWF.v5.2.trees create mode 100644 tests/test_recipes/recipe_with_traits.slim diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml index c79fac76..c5a3749c 100644 --- a/.github/workflows/docs.yml +++ b/.github/workflows/docs.yml @@ -11,6 +11,7 @@ jobs: Docs: permissions: contents: read - uses: tskit-dev/.github/.github/workflows/docs.yml@v19 + uses: tskit-dev/.github/.github/workflows/docs.yml@v21 with: install-slim: true + install-slim-branch: "multitrait" diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index 8b29520b..1fc031fd 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -10,4 +10,4 @@ jobs: Lint: permissions: contents: read - uses: tskit-dev/.github/.github/workflows/lint.yml@v19 + uses: tskit-dev/.github/.github/workflows/lint.yml@v21 diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index c41d6293..223052fc 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -12,7 +12,7 @@ permissions: jobs: packaging: name: Python packaging - uses: tskit-dev/.github/.github/workflows/python-packaging.yml@v19 + uses: tskit-dev/.github/.github/workflows/python-packaging.yml@v21 test: name: Python @@ -93,6 +93,7 @@ jobs: git clone https://github.com/messerlab/SLiM.git mkdir -p SLiM/Release cd SLiM/windows_compat/gnulib + git checkout multitrait # <-- note multitrait branch!! touch --date="`date`" aclocal.m4 Makefile.am configure configure.ac config.h.in Makefile.in cd ../.. cd Release @@ -104,9 +105,27 @@ jobs: pip install uv uv sync --locked --group test --no-default-groups - - name: Install SLiM (macOS / Linux) - if: matrix.os == 'macos-latest' || matrix.os == 'ubuntu-24.04' - run: micromamba install slim -y + # UNCOMMENT THIS when the below is commented again + # - name: Install SLiM (macOS / Linux) + # if: matrix.os == 'macos-latest' || matrix.os == 'ubuntu-24.04' + # run: micromamba install slim -y + + - name: Install development SLiM + # This should be COMMENTED OUT for release versions, + # since this builds SLiM from github head. + # Also note that this checks out the multitrait branch!! + if: (matrix.os == 'macos-latest' || matrix.os == 'ubuntu-24.04') && steps.cache.outputs.cache-hit != 'true' + # If we want to re-build slim from a new commit to the slim repo + # we may need to bump the cache key above. + shell: bash -l {0} + run: | + git clone https://github.com/messerlab/SLiM.git + mkdir -p SLiM/Release + cd SLiM/Release + git checkout multitrait + cmake -DCMAKE_BUILD_TYPE=Release .. + make -j 2 + - name: Run tests run: | diff --git a/CHANGELOG.rst b/CHANGELOG.rst index f61487d2..a31bb709 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -2,7 +2,52 @@ [1.1.2] - 2026-XX-XX ******************** -In development +**Breaking changes**: + +- The release of SLiM 6.0, changes to metadata (see below) mean that accessing + top-level metadata (e.g., `ts.metadata["SLiM"]`) more than a few times in + a script will take a long time. Scripts that previously ran quickly may take a + prohibitively long. See the documentation for simple changes that fix the problem: + https://tskit.dev/pyslim/docs/latest/previous_versions.html + +- The SLiM tree sequence file version number has changed to 1.0. Use `pyslim.update` + to convert your tree sequence file to this format. + +- Metadata for SLiM's mutations are no longer stored along with the tskit mutations, + because mutation stacking allows each tskit mutation to be associated with more + than one SLiM mutation. Now, metadata for each unique mutation is stored in + top-level metadata, under `ts.metadata["SLiM_mutation_list"]`. The recommended + way to access this information is by obtaining the SLiM ID-to-metadata dict + returned by `pyslim.mutation_metadata(ts)`. + +- Previously, `msprime.sim_mutations` with the `msprime.SLiMMutationModel` + would record SLiM metadata along with each new mutation. However, msprime + does not modify top-level metadata, and so the method `add_mutation_metadata` + should be used after adding SLiM mutations. + +- This is a SLiM change, but top-level metadata is now encoded using the `json+struct` + codec now provided by tskit (so that the mutation metadata is not too large/slow). + +- The top-level and individual metadata schemas now depend on the number of traits + in the model. The methods `slim_tree_sequence_metadata_schema` and + `slim_individual_metadata_schema` can be used to produce correct schema. + +**Bug fixes:** + +- In some previous versions, converting files produced by a yet-older version of SLiM + to the previously-current file version dropped some information from metadata: + nucleotide values for mutations, and pedigree parent IDs for individuals. This only + may have affected users using `pyslim.convert(ts)` in a previous version of pyslim + on a tree sequence `ts` with SLiM file version prior to 0.9. + +**New features**: + +- SLiM now includes in metadata information about the effects of mutations on + quantitative traits, the values of traits for individuals, and the values of + various "tags" defined in SLiM. + +- `default_slim_metadata` can now take additional arguments to modify the returned + values. ******************** [1.1.1] - 2026-03-06 diff --git a/docs/metadata.md b/docs/metadata.md index e4255d51..8332cdb2 100644 --- a/docs/metadata.md +++ b/docs/metadata.md @@ -20,7 +20,7 @@ import random random.seed(23) ts = tskit.load("example_sim.trees") -tables = ts.tables +tables = ts.dump_tables() ``` ```{eval-rst} @@ -37,7 +37,7 @@ tables = ts.tables ## Overview SLiM puts SLiM-specific information into the *metadata* for the tree sequence, -as well as for each populations, individuals, nodes and mutations. +as well as for each population, individual, node and mutation. Here is a quick reference to what information is available: see the SLiM manual for the more technical writeup. A good way to get a generic metadata example is with {func}`.default_slim_metadata`. @@ -55,9 +55,26 @@ and `ts.metadata["SLiM"]` contains information about the simulation: - `spatial_dimensionality`: for instance, `""` or `"x"` or `"xy"` (etcetera) - `spatial_periodicity`: whether space wraps around in some directions (same format as dimensionality) - `stage`: the *stage* of the life cycle at which the file was written out (either `"first"`, `"early"`, or `"late"`) +- `name`: the *name* of this species in SLiM +- `this_chromosome`: contains, for the chromosome in SLiM recorded in this tree sequence + * `id`: SLiM's ID + * `index`: the index of this chromosome in the list of chromosomes + * `symbol`: the user-assigned symbol + * `type`: specifies inheritance type, e.g., `"A"` for autosome +- `chromosomes`: (optional) a list of all chromosomes in the simulation, + for each containing the same information as for `this_chromosome` +- `traits`: a list of information for each of the traits: + * `index`: the index of the trait in SLiM + * `name`: the name in SLiM for the trait + * `type`: additive, multiplicative, or logistic + * `baselineOffset`, `baselineAccumulation`: a value added to all traits, and whether the effect of substitutions + accumulate in that value + * `directFitnessEffect`: whether the trait has a direct effect on fitness + * `individualOffsetMean`, `individualOffsetSD`: parameters governing the individual-level offsets + (i.e., "environment" effects) **Populations:** -Information about each SLiM-produced population is written to metatadata. +Information about each SLiM-produced population is written to metadata. The format uses JSON and is extensible, so other keys may be present and some keys may be missing (for instance, there are no spatial bounds in a nonspatial simulation). The metadata may be `None` for populations @@ -68,7 +85,7 @@ that SLiM did not use. The keys that SLiM uses are: - `description`: a string describing the population - `selfing_fraction`, `female_cloning_fraction`, `male_cloning_fraction`, and `sex_ratio`: only present when applicable (e.g., in WF simulations) - `bounds_x0`, `bounds_x1`, `bounds_y0`, `bounds_y1`, `bounds_z0`, and `bounds_z1`: the spatial bounds, when applicable -- `migration_records`: A *list* of entries decribing migration between populations in a WF model. +- `migration_records`: A *list* of entries describing migration between populations in a WF model. **Individuals:** Each individual produced by SLiM contains the following metadata: @@ -81,27 +98,91 @@ Each individual produced by SLiM contains the following metadata: - `subpopulation`: the subpopulation within SLiM the individual was in at the time the file was written out - `sex`: the sex of the individual (either {data}`.INDIVIDUAL_TYPE_FEMALE`, {data}`.INDIVIDUAL_TYPE_MALE`, or {data}`.INDIVIDUAL_TYPE_HERMAPHRODITE`) - `flags`: additional information; currently only recording whether the individual was a "migrant" or not (see the SLiM manual) +- `tag`, `tagF`: the corresponding properties in SLiM: default values returned by pyslim + are the special values that SLiM uses to mean that the values are unset +- `tagL0`, `tagL0_set`, etcetera: again, the corresponding properties in SLiM; + the purpose of `tagLX_set` is to record whether the tag has been set in the simulation +- `per_trait`: a list of information about the trait values for this individual; these are in the same order + as the traits listed in top-level metadata; + * `phenotype`: the trait value + * `offset`: the individual's offset (i.e., the "environmental effect") **Nodes:** Each "node" produced by SLiM (i.e., "genome" within SLiM) has: -- 'slim_id': the unique ID associated with the genome by SLiM -- 'is_null': whether the genome is a "null" genome (in which case it isn't +- `slim_id`: the unique ID associated with the genome by SLiM +- `is_vacant`: records the genome is a "vacant" genome (in which case it isn't really there, so shouldn't have any mutations or relationships in the tree - sequence!) -- 'genome_type': the 'type' of this genome (0 for autosome, 1 for X, 2 for Y) + sequence!) - see [](sec_overview_vacant_nodes) for more explanation **Mutations:** -Each mutation's metadata is a dictionary with a single key, `"mutation_list"`, -whose entry is a *list* of metadata dictionaries corresponding to the mutations that are "stacked", -i.e., all present, in all genomes inheriting from this (tskit) mutation. -So, `ts.mutation(12).metadata["mutation_list"]` is a list, each of whose entries contains: +Prior to SLiM 6.0, mutation metadata was associated with the tskit mutation objects. +Now, this is stored in top-level metadata, under ``ts.metadata["SLiM_mutation_list"]``. +Each entry +- `mutation_id`: the numeric ID of mutation in SLiM - `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 `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`) +- `per_trait`: a list of information in the same order as the traits in top-level metadata, recording for each: + * `effect_size`: the effect on the trait of this mutation + * `dominance`: its dominance coefficient + * `hemizygous_dominance`: its hemizygous dominance coefficient (see the SLiM manual) +- `padding`: this is simply empty bytes, here for byte-alignment reasons, and is always `None` + + +(sec_metadata_using_top_level)= + +## Using top-level metadata + +If you are going to be using information from top-level metadata, +it is good practice to extract the metadata as a separate python object once +and refer to that object, since otherwise you can incur runtime penalties +for decoding and copying the metadata every time you call `ts.metadata`. +This can be substantial, given the amount of mutation information +in top-level metadata. +For instance, to subtract baseline offsets from individual's trait values, +we might do: +```{code-cell} +md = ts.metadata +traits = md["SLiM"]["traits"] +values = [ + [x['phenotype'] - y["baselineOffset"] for x, y in zip(ind.metadata['per_trait'], traits)] + for ind in ts.individuals() +] +``` +If we instead inserted ``ts.metadata["SLiM"]["traits"]`` directly into the loop, +this would become infeasibly slow. + +In some more detail: +each time python evaluates ``ts.metadata`` (e.g., using ``ts.metadata["SLiM"]``) +a new copy of the metadata dict is decoded and returned. Furthermore, a number +of `pyslim` functions need to look up information from metadata under the hood. +For instance, previously it was acceptable to run +``[pyslim.slim_time(ts, mut.time) for mut in ts.mutations()]``. +However, this could now easily take hours even for moderately-sized simulations. +There are several recommendations for how to mitigate this: + +- If you use information from top-level metadata, make a copy of it + and refer to that copy instead: so, ``ts_metadata = ts.metadata`` + after ``ts = tskit.load(...)`` and then use ``ts_metadata``. However, + be careful that you use the correct metadata object corresponding + to the correct tree sequence object! + +- Use a single pyslim function call rather than many calls to the same one + within a loop. For instance, run: + ``slim_times = pyslim.slim_time(ts, ts.mutations_time)`` and extract + slim times from this vector. Similarly, use {func}`.nodes_vacant` + instead of {func}`.node_is_vacant`. + +- Some pyslim methods will accept a pre-extracted metadata dictionary + as an optional argument. If this is not provided, those methods will + extract the metadata again. The methods that now take a `ts_metadata` argument are: + {func}`.individual_ages`, + {func}`.individual_ages_at`, + {func}`.individuals_alive_at`, and + {func}`.slim_time`. (sec_metadata_tools)= @@ -110,10 +191,20 @@ So, `ts.mutation(12).metadata["mutation_list"]` is a list, each of whose entries The dictionaries describing the schema for these metadata entries are available in `pyslim.slim_metadata_schemas`. -Furthermore, this method may be useful in working with metadata: +Furthermore, these methods may be useful in working with metadata: ```{eval-rst} .. autofunction:: default_slim_metadata + +.. autofunction:: slim_tree_sequence_metadata_schema + +.. autofunction:: slim_individual_metadata_schema + +.. autofunction:: slim_node_metadata_schema + +.. autofunction:: set_tree_sequence_metadata + +.. autofunction:: set_metadata_schemas ``` @@ -125,13 +216,13 @@ see {ref}`tskit's metadata documentation `. ### Top-level metadata The entries of the top-level metadata dict are *read-only*. -So, you might think that +So, although you might think that `tables.metadata["SLiM"]["model_type"] = "nonWF"` would switch the model type, -but this in fact (silently) does nothing. To modify the top-level metadata, -we must (a) work with tables (as tree sequences are immutable, and (b) +this in fact (silently) does nothing. To modify the top-level metadata, +we must (a) work with tables (as tree sequences are immutable), and (b) extract the metadata dict, modify the dict, and copy it back in. -Instead, you should do +So, the code for modifying top-level metadata is: ```{code-cell} md = tables.metadata md["SLiM"]["model_type"] = "nonWF" diff --git a/docs/previous_versions.md b/docs/previous_versions.md index 3c1f74da..ce067fa2 100644 --- a/docs/previous_versions.md +++ b/docs/previous_versions.md @@ -16,7 +16,7 @@ kernelspec: import pyslim, tskit, msprime ts = tskit.load("example_sim.trees") -tables = ts.tables +tables = ts.dump_tables() ``` @@ -25,6 +25,83 @@ tables = ts.tables # Migrating from previous versions of pyslim +## 1.2 + +Release 1.2 goes along with SLiM v6, which introduces support for traits. +It also changes the format for storing mutation metadata: now this is stored +in top-level metadata. + +1. Each time python evaluates ``ts.metadata`` (e.g., using ``ts.metadata["SLiM"]``) +a new copy of the metadata dict is decoded and returned. In large SLiM simulations, +this can take seconds, so we should avoid doing it many times. Furthermore, a number +of pyslim functions need to look up information from metadata under the hood. +See [](sec_metadata_using_top_level) for more discussion and examples. +In particular: + + - The method {func}`.node_is_vacant` necessarily uses metadata and acts + only on a single node. This method is now deprecated; + use {func}`.nodes_vacant` instead. + + - Some pyslim methods will accept a pre-extracted metadata dictionary + as an optional ``ts_metadata`` argument; see [](sec_metadata_using_top_level). + Furthermore, {func}`.is_current_version` now accepts top-level metadata directly + as an alternative to the tree sequence. + +2. If you are using `msprime` to generate mutations, you need to use +{func}`.add_mutation_metadata` after generating mutations to add the +information about these that SLiM expects to top-level metadata. +For instance: + +```{code-cell} +next_id = pyslim.next_slim_mutation_id(ts) +ts = pyslim.add_mutation_metadata( + msprime.sim_mutations( + ts, + rate=1e-8, + model=msprime.SLiMMutationModel(type=0, next_id=next_id), + ), + mutation_type=0, +) +``` +Here the ``mutation_type`` argument to {func}`.add_mutation_metadata` +is the important one; the ``type`` argument to ``SLiMMutationModel`` +is now deprecated, and will be effectively ignored. + +3. Instead of looking up metadata for mutations in `mut.metadata`, you need +to pull this information out of top-level metadata using the SLiM ID as a key. +In brief, if `mut` is a mutation, then you should replace +`mut.metadata["mutation_list"][j]` +with `mut_metadata[int(mut.derived_state.split(",")[j])]`, +where `mut_metadata` is the output of {func}`.mutation_metadata`. +For instance, where before you might have done: + +```python +mut = ts.mutation(0) +for k, md in zip(mut.derived_state.split(","), mut.metadata["mutation_list"]): + print(f"SLiM ID: {k}") + print(f"Metadata: {md}") +``` + +Now, you would do: + +```{code-cell} +mut_metadata = pyslim.mutation_metadata(ts) +mut = ts.mutation(0) +for k in mut.derived_state.split(","): + md = mut_metadata[int(k)] + print(f"SLiM ID: {k}") + print(f"Metadata: {md}") +``` + +The function {func}`.mutation_metadata` pulls information out of +`ts.metadata["SLiM_mutation_list"]`. It is useful for two reasons: +first, it puts the information into a dict, so you can look up information +using the SLiM mutation ID instead of searching through the list to find it. +Second, it caches the information: every time you access +`ts.metadata["SLiM_mutation_list"]`, it makes a new, decoded copy +of the entire metadata dictionary. This can be **very slow** if it is done +repeatedly. + ## 1.1 Release 1.1 goes along with SLiM v5, which introduces multichromosome support. diff --git a/docs/python_api.md b/docs/python_api.md index b570cd69..51e96d23 100644 --- a/docs/python_api.md +++ b/docs/python_api.md @@ -18,7 +18,7 @@ from IPython.display import SVG import numpy as np ts = tskit.load("example_sim.trees") -tables = ts.tables +tables = ts.dump_tables() ``` ```{eval-rst} @@ -38,6 +38,7 @@ Here is a quick reference to some of the methods: .. autosummary:: recapitate + mutation_metadata annotate individuals_alive_at individual_ages @@ -47,6 +48,9 @@ Here is a quick reference to some of the methods: has_vacant_samples node_is_vacant slim_time + next_slim_mutation_id + add_mutation_metadata + add_mutation_metadata_tables convert_alleles generate_nucleotides population_size @@ -89,6 +93,11 @@ Here is a quick reference to some of the methods: .. autofunction:: set_slim_state ``` +```{eval-rst} +.. autofunction:: add_mutation_metadata +.. autofunction:: add_mutation_metadata_tables +``` + ## Summarizing tree sequences Additionally, ``pyslim`` contains the following methods: @@ -119,6 +128,10 @@ Additionally, ``pyslim`` contains the following methods: ## Utilities +```{eval-rst} +.. autofunction:: mutation_metadata +``` + ```{eval-rst} .. autofunction:: slim_time ``` @@ -131,24 +144,23 @@ Additionally, ``pyslim`` contains the following methods: .. autofunction:: has_vacant_samples ``` +```{eval-rst} +.. autofunction:: nodes_vacant +``` + ```{eval-rst} .. autofunction:: node_is_vacant ``` +```{eval-rst} +.. autofunction:: is_current_version +``` + ## Metadata -SLiM-specific metadata is made visible to the user by ``.metadata`` properties. -For instance: -```{code-cell} -ts.individual(4).metadata -``` -shows that the fifth individual in the tree sequence was given pedigree ID ``495999`` by SLiM, -had parents with pedigree IDs ``493739`` and ``494784``, -was age 10 at the time that they died (or the simulation ended), -lived in subpopulation 1, -was female (because ``sex`` matches ``pyslim.INDIVIDUAL_TYPE_FEMALE``, below), -and has no additional metadata flags. +SLiM-specific metadata is made visible to the user by ``.metadata`` properties, +described in [](sec_metadata). ### Annotation diff --git a/docs/rapid_adaptation.slim b/docs/rapid_adaptation.slim index ed2ed497..909eeecf 100644 --- a/docs/rapid_adaptation.slim +++ b/docs/rapid_adaptation.slim @@ -1,5 +1,5 @@ initialize() { - initializeTreeSeq(); + initializeTreeSeq(timeUnit="generations"); initializeMutationRate(1e-8); initializeMutationType("m1", 0.5, "e", 0.1); initializeGenomicElementType("g1", m1, 1.0); diff --git a/docs/selection.slim b/docs/selection.slim index 3c1860b4..b6173896 100644 --- a/docs/selection.slim +++ b/docs/selection.slim @@ -1,7 +1,7 @@ initialize() { initializeSLiMModelType("WF"); - initializeTreeSeq(); + initializeTreeSeq(timeUnit="generations"); initializeMutationRate(1e-6); initializeMutationType("m1", 0.5, "e", -0.1); initializeMutationType("m2", 0.5, "e", 0.5); diff --git a/docs/time_units.md b/docs/time_units.md index f997c49c..419651b1 100644 --- a/docs/time_units.md +++ b/docs/time_units.md @@ -226,7 +226,8 @@ so we expect generation time to go up at first. ```{code-cell} gts = tskit.load("generation_time.trees") -gentimes = gts.metadata["SLiM"]["user_metadata"]["generation_times"] +gts_metadata = gts.metadata +gentimes = gts_metadata["SLiM"]["user_metadata"]["generation_times"] fig, ax = plt.subplots(figsize=(12, 6), dpi=300) ax.set_xlabel("tick") @@ -266,7 +267,6 @@ Furthermore, since we already have mutations up until 100 time units ago, we need to put mutations on only previous to that time. ```{code-cell} -gentimes = gts.metadata["SLiM"]["user_metadata"]["generation_times"] gt = np.mean(gentimes[-50:]) recomb_rate = 1e-8 # per generation Ne = 1000 # generations diff --git a/docs/tutorial.md b/docs/tutorial.md index 35fc967e..b7ff0517 100644 --- a/docs/tutorial.md +++ b/docs/tutorial.md @@ -315,8 +315,8 @@ can be done with the {meth}`tskit.TreeSequence.simplify` method: ```{code-cell} import numpy as np rng = np.random.default_rng(seed=3) -alive_inds = pyslim.individuals_alive_at(rts, 0) -keep_indivs = rng.choice(alive_inds, 100, replace=False) +alive_indivs = pyslim.individuals_alive_at(rts, 0) +keep_indivs = rng.choice(alive_indivs, 100, replace=False) keep_nodes = [] for i in keep_indivs: keep_nodes.extend(rts.individual(i).nodes) @@ -364,11 +364,13 @@ This works as follows: ```{code-cell} next_id = pyslim.next_slim_mutation_id(sts) -ts = msprime.sim_mutations( +ts = pyslim.add_mutation_metadata( + msprime.sim_mutations( sts, rate=1e-8, model=msprime.SLiMMutationModel(type=0, next_id=next_id), keep=True, + ) ) print(f"The tree sequence now has {ts.num_mutations} mutations,\n" @@ -466,18 +468,18 @@ simulation (i.e., zero time units ago), we could do: ```{code-cell} orig_ts = tskit.load("example_sim.trees") -alive = pyslim.individuals_alive_at(orig_ts, 0) +alive_indivs = pyslim.individuals_alive_at(orig_ts, 0) -print(f"There are {len(alive)} individuals alive in the final generation.") +print(f"There are {len(alive_indivs)} individuals alive in the final generation.") ``` -Here, ``alive`` is a vector of *individual* IDs, +Here, ``alive_indivs`` is a vector of *individual* IDs, so one way to take a sample of living individuals and write their SNPs to a VCF is: ```{code-cell} rng = np.random.default_rng(seed=1) -keep_indivs = rng.choice(alive, 100, replace=False) +keep_indivs = rng.choice(alive_indivs, 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) @@ -517,10 +519,10 @@ For this reason, if at this point we try to extract genotypes for all of the alive individuals, we encounter a (somewhat confusing) error: ```{code-cell} +alive_indivs = pyslim.individuals_alive_at(ts, 0) try: - alive = pyslim.individuals_alive_at(ts, 0) with open("example_snps.vcf", "w") as vcffile: - ts.write_vcf(vcffile, individuals=alive) + ts.write_vcf(vcffile, individuals=alive_indivs) except Exception as e: print ("Error:") print (e) @@ -537,7 +539,7 @@ using {meth}`is_sample() `: ```{code-cell} indivlist = [] -for i in pyslim.individuals_alive_at(ts, 0): +for i in alive_indivs: ind = ts.individual(i) if ts.node(ind.nodes[0]).is_sample(): indivlist.append(i) @@ -571,9 +573,9 @@ we could do: ```{code-cell} orig_ts = tskit.load("migrants.trees") -alive = pyslim.individuals_alive_at(orig_ts, 0) +alive_indivs = pyslim.individuals_alive_at(orig_ts, 0) num_alive = [0 for _ in range(orig_ts.num_populations)] -for i in alive: +for i in alive_indivs: ind = orig_ts.individual(i) ind_population = orig_ts.node(ind.nodes[0]).population num_alive[ind_population] += 1 @@ -652,7 +654,7 @@ print(f"There are {ts.num_mutations} mutations across {ts.num_trees} distinct\n" ## Individual metadata -Each ``Mutation``, ``Population``, ``Node``, and ``Individual``, as well as the tree +Each ``Population``, ``Node``, and ``Individual``, as well as the tree sequence as a whole, carries additional information stored by SLiM in its ``metadata`` property. A fuller description of metadata in general is given in [](sec_metadata), but as a quick introduction, here is the information available @@ -687,7 +689,12 @@ produced by SLiM. This is described in more detail in the SLiM manual, but brief - ``flags`` holds additional information about the individual recorded by SLiM (currently, only whether the individual has migrated or not: see [](sec_constants_and_flags)). - +- the ``tag`` entries contain the correspondly-named "tags" in SLiM, + and for the logical tags ``tagLX``, the ``tagLX_set`` records whether or not + that tag was "set" (as opposed to remaining unset). + The funny values in ``tag`` and ``tagF`` are those special values that SLiM uses to + record that *those* entries were not set either. +- the ``per_trait`` entry is a list of information, one for each trait in the simulation. We can use this metadata in many ways, for example, to create an age distribution by sex: @@ -697,7 +704,8 @@ 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 pyslim.individuals_alive_at(ts, 0): +alive_indivs = pyslim.individuals_alive_at(ts, 0) +for i in alive_indivs: ind = ts.individual(i) age_table[ind.metadata["age"], ind.metadata["sex"]] += 1 @@ -728,8 +736,8 @@ This can be done using the numpy arrays returned by {func}`.individual_ages` and `.individuals_population` as follows: ```{code-cell} -alive = pyslim.individuals_alive_at(ts, 0) -adults = alive[pyslim.individual_ages(ts)[alive] > 2] +ages = pyslim.individual_ages(ts) +adults = alive_indivs[ages[alive_indivs] > 2] pops = [ [i for i in adults if ts.individual(i).metadata['subpopulation'] == k] for k in [1, 2] @@ -979,22 +987,40 @@ stored in the mutation metadata. To modify the mutations to be under selection, see [](sec_vignette_coalescent_diversity). ```{code-cell} -ts = msprime.sim_mutations( +ts = pyslim.add_mutation_metadata( + 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: +The resulting mutations are in SLiM format. +Now, each `mutation` object in the tree sequence represents +some number of SLiM mutations, whose SLiM IDs are stored in the `derived_state`. +For instance, here's which SLiM mutation(s) the first mutation +in the tree sequence represents: +```{code-cell} +ds = ts.mutation(0).derived_state +print(f"SLiM IDs: {ds}") +``` +To see the information about these, we pull their information out +using {func}`.mutation_metadata`, which provides a dictionary +indexed by the SLiM IDs: ```{code-cell} :tags: ["remove-output"] -ts.mutation(0) +mut_metadata = pyslim.mutation_metadata(ts) +for sid in ds.split(","): + print(mut_metadata[int(sid)]) ``` ```{code-cell} :tags: ["remove-input"] -util.pp(ts.mutation(0)) +for sid in ds.split(","): + util.pp(mut_metadata[int(sid)]) ``` +**Important:** the {func}`.mutation_metadata`-returned dictionary +is indexed by **ints**, not strings, so be sure to convert your +SLiM IDs to ints before looking them up! Finally, we write this out to a file that can be loaded in to SLiM: ```{code-cell} @@ -1053,24 +1079,31 @@ Now, mutations have a ``nucleotide`` property in metadata that is not ``-1``: ```{code-cell} :tags: ["remove-output"] +mut_metadata = pyslim.mutation_metadata(ts) m = ts.mutation(0) +md = [mut_metadata[int(k)] for k in m.derived_state.split(",")] print(m) +for x in md: + print(x) ``` ```{code-cell} :tags: ["remove-input"] util.pp(m) +for x in md: + util.pp(x) ``` We can see which nucleotide is the derived state produced by each mutation - by indexing the {data}`.NUCLEOTIDES` object: +by indexing the {data}`.NUCLEOTIDES` object: ```{code-cell} for k in range(3): m = ts.mutation(k) print(f"Mutation {k}: position {ts.site(m.site).position}, time {m.time}") - for ml in m.metadata['mutation_list']: - print(f" nucleotide: {pyslim.NUCLEOTIDES[ml['nucleotide']]}") + for sid in m.derived_state.split(","): + md = mut_metadata[int(sid)] + print(f" nucleotide: {pyslim.NUCLEOTIDES[md['nucleotide']]}") ``` Here's a script minimally modified from the above to be nucleotide-based: @@ -1108,73 +1141,108 @@ print(f"Number of sites: {ts.num_sites}\n" ``` Note that there are more mutations than sites; -that's because some sites (looks like 24 of them) have multiple mutations. +that's because some sites have multiple mutations. The information about the mutation is put in the mutation's metadata. Here's the first mutation: ```{code-cell} :tags: ["remove-output"] +mut_metadata = pyslim.mutation_metadata(ts) m = ts.mutation(0) +md = [mut_metadata[int(k)] for k in m.derived_state.split(",")] print(m) +for x in md: + print(x) ``` + ```{code-cell} :tags: ["remove-input"] util.pp(m) +for x in md: + util.pp(x) ``` -Here, `m.site` tells us the ID of the *site* on the genome that the mutation occurred at, + +Since we haven't explicitly defined any traits in this simulation, +the only trait is fitness, and the `effect_size` listed under `per_trait` +for this mutation is simply its selection coefficient. +Furthermore, `m.site` tells us the ID of the *site* on the genome that the mutation occurred at, and we can pull up information about that with the `ts.site( )` method: + ```{code-cell} :tags: ["remove-output"] -ts.site(m.site) +s = ts.site(m.site) +md = [ + mut_metadata[int(k)] for m in s.mutations + for k in m.derived_state.split(",") +] +print(s) +for x in md: + print(x) ``` + ```{code-cell} :tags: ["remove-input"] -util.pp(ts.site(m.site)) +util.pp(s) +for x in md: + util.pp(x) ``` + This mutation occurred at position 54 along the genome (from `site.position`) which previously had no mutations (since `site.ancestral_state` is the empty string, `''`) -and was given SLiM mutation ID 1653896 (`m.derived_state`). -The metadata (`m.metadata`, a dict) tells us that -the mutation has selection coefficient 1.5597 and occurred in population 1 in generation 827, -which was 172 generations ago. +and was given SLiM mutation ID 1997358 (`m.derived_state`). +The metadata (`mut_metadata[1997358]`, a dict) tells us that +the mutation has selection coefficient -0.1129 and occurred in population 1 in generation 999, +which was 0 generations ago. 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: +Note that `m.time` and the `slim_time` entry in metadata 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 "tick" counter. -Also note that the mutation's metadata is a *list* of metadata entries. +Also note that each mutation may have associated a *list* of SLiM mutations, +each with their own metadata. That's because of SLiM's mutation stacking feature. We know that some sites have more than one mutation, so to get an example let's pull out one such mutation. -In this case, -`m.metadata['mutation_list']` is a list of length one, -so the mutation was not stacked on top of previous ones. Let's pull out a mutation that was stacked on top of another one: + ```{code-cell} :tags: ["remove-output"] for m in ts.mutations(): if m.parent != tskit.NULL: break +pm = ts.mutation(m.parent) +md = [mut_metadata[int(k)] for k in m.derived_state.split(",")] +pmd = [mut_metadata[int(k)] for k in pm.derived_state.split(",")] + print(m) -print(ts.mutation(m.parent)) +for x in md: + print(x) +print(pm) +for x in pmd: + print(x) ``` + ```{code-cell} :tags: ["remove-input"] util.pp(m) +for x in md: + util.pp(x) util.pp(ts.mutation(m.parent)) +for x in pmd: + util.pp(x) ``` -This mutation (which is `ts.mutation(1020)` in the tree sequence) -was the result of SLiM adding a new mutation of type `m1` and selection coefficient -0.0032 -on top of an existing mutation, also of type `m1` and with selection coefficient 0.3086. -This happened at generation 999 (i.e., at tskit time 0.0 time units ago), -and the older mutation occurred at generation 274 (at tskit time 725 time units ago). -The older mutation has SLiM mutation ID 547531, -and the newer mutation had SLiM mutation ID 1998096, -so the resulting "derived state" is `'1998096,547531'`. +This mutation (which is `ts.mutation(330)` in the tree sequence) +was the result of SLiM adding a new mutation of type `m1` and selection coefficient -0.1547 +on top of an existing mutation, of type `m2` and with (whopping) selection coefficient 1.737. +This happened at generation 998 (i.e., at tskit time 1.0 time units ago), +and the older mutation occurred at generation 83 (at tskit time 916 time units ago). +The older mutation has SLiM mutation ID 1994163, +and the newer mutation had SLiM mutation ID 164833, +so the resulting "derived state" is `'1994163,164833'`. Now that we understand how SLiM mutations are stored in a tree sequence, let's look at the allele frequencies. @@ -1189,12 +1257,12 @@ print(afs.astype('int')) ``` (The `span_normalise=False` argument gives us counts rather than a density per unit length.) -This shows us that there are 4169 alleles that are found among the tree sequence's samples -that are not present in any of our 10 samples, 96 that are present in just one, etcetera. +This shows us that there are 3929 alleles that are found among the tree sequence's samples +that are not present in any of our 10 samples, 585 that are present in just one, etcetera. The surprisingly large number that are near 50% frequency are perhaps positively selected and on their way to fixation: we can check if that's true next. -You may have noticed that the sum of the allele frequency spectrum is 5243, -which is not obviously related to the number of mutations (6044) *or* the number of sites (6020). +You may have noticed that the sum of the allele frequency spectrum is 5029, +which is not obviously related to the number of mutations (5861) *or* the number of sites (5848). That's because each derived allele that is inherited by some but not all of the samples in the tree sequence is counted in the polarised allele frequency spectrum: Fixed mutations, or mutations that were entirely "overwritten" by subsequent mutations, @@ -1206,9 +1274,11 @@ afs_total = 0 for v in ts.variants(): if len(set(v.genotypes)) > 1: afs_total += len(set(v.genotypes) - set([0])) -print(afs_total) +print(afs_total, sum(afs)) ``` +These are equal, verifying our interpretation. + At time of writing, we don't have a built-in ``allele_frequency`` method, so we'll use the following snippet: @@ -1236,7 +1306,8 @@ mut_type = np.zeros(ts.num_sites) for j, s in enumerate(ts.sites()): mt = [] for m in s.mutations: - for md in m.metadata["mutation_list"]: + for sid in m.derived_state.split(","): + md = mut_metadata[int(sid)] mt.append(md["mutation_type"]) if len(set(mt)) > 1: mut_type[j] = 3 @@ -1261,32 +1332,34 @@ print(mut_afs) The first column gives the AFS among these 10 samples for the deleterious alleles, the second for the beneficial mutations; -the third column for the seven sites that had both types of mutation. +the third column for the few sites that had both types of mutation. Interestingly, there are similar numbers of both types of mutation at intermediate frequency: perhaps because beneficial mutations are sweeping linked deleterious alleles along with them. -Many fewer benefical alleles are at low frequency: -3,666 deleterious alleles are not found in our sample of 10 genomes, -while only 486 beneficial alleles are. +Many fewer benefical alleles are at low frequency, however. Finally, let's pull out information on the allele with the largest selection coefficient. ```{code-cell} :tags: ["remove-output"] sel_coeffs = np.array([ - sum(md["selection_coeff"] for md in m.metadata["mutation_list"]) + sum(mut_metadata[int(k)]["per_trait"][0]["effect_size"] + for k in m.derived_state.split(",")) for m in ts.mutations() ]) which_max = np.argmax(sel_coeffs) m = ts.mutation(which_max) +print(f"Max selection coefficient: {sel_coeffs[which_max]} for site {m.site}") ts.site(m.site) ``` + ```{code-cell} :tags: ["remove-input"] +print(f"Max selection coefficient: {sel_coeffs[which_max]} for site {m.site}") util.pp(ts.site(m.site)) ``` -This allele had a whopping selection coefficient of 4.94 -and appeared about halfway through the simulation. +This allele had a whopping selection coefficient of 5.69 +and appeared fairly late in the simulation. Let's find its frequency in the full population: ```{code-cell} @@ -1296,7 +1369,7 @@ print(f"The allele is found in {full_freqs[m.site][0]} copies\n" ``` The allele is above 50% in the population, so it is probably on its way to fixation. -Using its SLiM ID (which is shown in its derived state, ``1616148``), +Using its SLiM ID (which is shown in its derived state, ``305447``), we could reload the tree sequence into SLiM, restart the simulation, and use its ID to track its subsequent progression. @@ -1331,4 +1404,4 @@ Also known as "gotchas". 4. SLiM requires that the two nodes corresponding to the haplosomes of each individual are adjacent in the node table, and are sorted by haplosome ID. SLiM always writes out tree sequences like this, but it is possible to make - tree sequences in python that are leval otherwise but don't satisfy this requirement. + tree sequences in python that are legal otherwise but don't satisfy this requirement. diff --git a/docs/vignette_coalescent_diversity.md b/docs/vignette_coalescent_diversity.md index 589d1bd1..43dc4aa8 100644 --- a/docs/vignette_coalescent_diversity.md +++ b/docs/vignette_coalescent_diversity.md @@ -166,21 +166,26 @@ mut_map = msprime.RateMap( position=breaks, rate=[0.03e-8, 0.003e-8, 0.03e-8]) mut_model = msprime.SLiMMutationModel(type=2) -ots = msprime.sim_mutations( +ots = pyslim.add_mutation_metadata( + msprime.sim_mutations( ots, rate=mut_map, model=mut_model, keep=True, - random_seed=12) + random_seed=12), + mutation_type=2, +) print(f"The tree sequence now has {ots.num_mutations} mutations, at " f"{ots.num_sites} distinct sites.") ``` -Note the ``type=2`` argument to {class}`msprime.SLiMMutationModel`: +Note the ``type=2`` argument to {func}`.add_mutation_metadata`: this means the mutations will be of type "m2" in SLiM (and, so you must initialize that mutation type in the recipe that loads this tree sequence in). Now, we'll assign selection coefficients. +This is easier than in versions of SLiM before 6.0, +because we simply want to assign each mutation an independent selection coefficient Recall that to accomodate mutation stacking in SLiM, a mutation metadata entry is in fact a *list* of metadata entries, one for each of the SLiM mutations that are stacked at this position. @@ -193,25 +198,12 @@ SLiM mutation ID ``k``. ```{code-cell} rng = np.random.default_rng(seed=1234) -tables = ots.tables -tables.mutations.clear() -mut_map = {} -for m in ots.mutations(): - md_list = m.metadata["mutation_list"] - slim_ids = m.derived_state.split(",") - assert len(slim_ids) == len(md_list) - for sid, md in zip(slim_ids, md_list): - if sid not in mut_map: - mut_map[sid] = rng.exponential(scale=0.04) - md["selection_coeff"] = mut_map[sid] - _ = tables.mutations.append( - m.replace(metadata={"mutation_list": md_list}) - ) - -# check we didn't mess anything up -assert tables.mutations.num_rows == ots.num_mutations -print(f"The selection coefficients range from {min(mut_map.values()):0.2e}") -print(f"to {max(mut_map.values()):0.2e}.") +ts_metadata = ots.metadata +for md in ts_metadata["SLiM_mutation_list"]: + md["per_trait"][0]["effect_size"] = rng.exponential(scale=0.04) + +tables = ots.dump_tables() +tables.metadata = ts_metadata ``` @@ -222,15 +214,15 @@ We can see this with ``tables.metadata``: ```{code-cell} :tags: ['remove-output'] -tables.metadata +tables.metadata["SLiM"] ``` ```{code-cell} :tags: ['remove-input'] -util.pp(tables.metadata) +util.pp(tables.metadata["SLiM"]) ``` -We should edit this to match our planned slimulation -- particularly the ``model_type`` (WF or nonWF) and the ``tick``. +We should edit this to match our planned slimulation - +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, @@ -246,7 +238,6 @@ edit the metadata, let's make sure, and then we'll write the tree sequence to a file. ```{code-cell} -ts_metadata = tables.metadata ts_metadata["SLiM"]["model_type"] = "WF" tables.metadata = ts_metadata ots = tables.tree_sequence() @@ -290,7 +281,7 @@ This runs quickly, since it's only 100 generations. First, let's look at what mutations are present. ```{code-cell} ts = tskit.load("vignette_annotated.trees") -num_stacked = np.array([len(m.metadata["mutation_list"]) for m in ts.mutations()]) +num_stacked = np.array([len(m.derived_state.split(",")) 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 @@ -324,7 +315,9 @@ nodes_by_time = [ts.samples(time=t) for t in times] num_nodes = np.array([len(x) for x in nodes_by_time]) p = ts.sample_count_stat(nodes_by_time, lambda x: x/num_nodes, 2, windows='sites', strict=False, span_normalise=False, polarised=True) -s = np.array([sum([sum([md["selection_coeff"] for md in m.metadata["mutation_list"]]) +mut_metadata = pyslim.mutation_metadata(ts) +s = np.array([sum([sum([mut_metadata[int(k)]["per_trait"][0]["effect_size"] + for k in m.derived_state.split(",")]) for m in site.mutations]) for site in ts.sites()]) ``` @@ -407,12 +400,15 @@ next_id = pyslim.next_slim_mutation_id(ts) neutral_mut_model = msprime.SLiMMutationModel( type=1, next_id=next_id) -mts = msprime.sim_mutations( +mts = pyslim.add_mutation_metadata( + msprime.sim_mutations( ts, rate=neutral_mut_map, model=neutral_mut_model, keep=True, - random_seed=35) + random_seed=35), + mutation_type=1, +) print(f"The tree sequence now has {mts.num_mutations} mutations,") print(f"at {mts.num_sites} distinct sites.") ``` @@ -431,8 +427,9 @@ we'll pull out a tree that had a lot of mutations on it, and print a picture of it, with mutations labeled by their type: ```{code-cell} +mut_metadata = pyslim.mutation_metadata(mts) for t in mts.trees(): - mt = [max([u['mutation_type'] for u in m.metadata['mutation_list']]) for m in t.mutations()] + mt = [max([mut_metadata[int(k)]['mutation_type'] for k in m.derived_state.split(",")]) for m in t.mutations()] if t.num_mutations > 12: break @@ -482,28 +479,36 @@ If you wanted some other arrangement (e.g., to have m1 stack on top of m2), you could go through and modify derived states and metadata appropriately. Let's check there are any sites with stacked mutations of different types in the simulation. -There is indeed one such site: +There are indeed: ```{code-cell} :tags: ['remove-output'] for site in mts.sites(): if len(site.mutations) > 1: - types = [set([md["mutation_type"] for md in mut.metadata["mutation_list"]]) + types = [set([mut_metadata[int(k)]["mutation_type"] for k in mut.derived_state.split(",")]) for mut in site.mutations] if max(map(len, types)) > 1: print(site) + for mut in site.mutations: + print(mut) + for k in mut.derived_state.split(","): + print(mut_metadata[int(k)]) ``` ```{code-cell} :tags: ['remove-input'] for site in mts.sites(): if len(site.mutations) > 1: - types = [set([md["mutation_type"] for md in mut.metadata["mutation_list"]]) + types = [set([mut_metadata[int(k)]["mutation_type"] for k in mut.derived_state.split(",")]) for mut in site.mutations] if max(map(len, types)) > 1: util.pp(site) + for mut in site.mutations: + util.pp(mut) + for k in mut.derived_state.split(","): + util.pp(mut_metadata[int(k)]) ``` -Here, a neutral mutation has been put down on top of a selected mutation, +In each of these, a neutral mutation has been put down on top of a selected mutation, but stacked, so that any samples inheriting either of these mutations carries the selected mutation. For more discussion of how this works, see {class}`msprime.SLiMMutationModel`. diff --git a/docs/vignette_continuing.md b/docs/vignette_continuing.md index 3aff6616..80d0536d 100644 --- a/docs/vignette_continuing.md +++ b/docs/vignette_continuing.md @@ -81,17 +81,18 @@ so first we check that all the existing mutations are of a different type. rts = pyslim.recapitate(ts, ancestral_Ne=1000, recombination_rate=1e-8, random_seed=6) # check type m0 is not used: -mut_types = set([md['mutation_type'] - for mut in ts.mutations() - for md in mut.metadata['mutation_list']]) +mut_metadata = pyslim.mutation_metadata(rts) +mut_types = set([md['mutation_type'] for md in mut_metadata.values()]) print(f"Keeping {rts.num_mutations} existing mutations of type(s) {mut_types}.") assert 0 not in mut_types # add type m0 mutations next_id = pyslim.next_slim_mutation_id(rts) -rts = msprime.sim_mutations( +rts = pyslim.add_mutation_metadata( + msprime.sim_mutations( rts, rate=1e-8, random_seed=7, keep=True, model=msprime.SLiMMutationModel(type=0, next_id=next_id) + ) ) p = rts.sample_count_stat( @@ -143,7 +144,7 @@ new_ts = msprime.sim_mutations( new_ts, rate=1e-8, random_seed=10, keep=True, model=msprime.SLiMMutationModel(type=0) ) -new_tables = new_ts.tables +new_tables = new_ts.dump_tables() # check that the spurious samples are 20000 and 20001 for n in (20000, 20001): assert n in new_ts.samples() @@ -158,13 +159,11 @@ randomly assign each to a node at the end of the SLiM simulation, and merge them. ```{code-cell} - new_nodes = np.where(new_tables.nodes.time == new_time)[0] print(f"There are {len(new_nodes)} nodes from the start of the new simulation.") -# There are 4425 nodes from the start of the new simulation. slim_nodes = rts.samples(time=0) -assert(len(slim_nodes) == 20000) +assert len(slim_nodes) == 20000 # randomly give new_nodes IDs in rts node_map = np.repeat(tskit.NULL, new_tables.nodes.num_rows) @@ -173,15 +172,15 @@ node_map[new_nodes] = np.random.choice(slim_nodes, len(new_nodes), replace=False # shift times: in nodes and mutations # since tree sequences are not mutable, we do this in the tables directly # also, unmark the nodes at the end of the SLiM simulation as samples -tables = rts.tables +tables = rts.dump_tables() tables.nodes.flags = tables.nodes.flags & ~np.uint32(tskit.NODE_IS_SAMPLE) tables.nodes.time = tables.nodes.time + new_time tables.mutations.time = tables.mutations.time + new_time # merge the two sets of tables tables.union(new_tables, node_map, - add_populations=False, - check_shared_equality=False) + add_populations=False, + check_shared_equality=False) # get back the tree sequence full_ts = tables.tree_sequence() @@ -243,3 +242,12 @@ to be identical in the two tree sequences, so ``union`` by default throws an err We don't expect that in this case, because, for instance, there could be a mutation above one of the terminal nodes in the SLiM tree sequence; this would clearly not be present in the new tree sequence. + +*Note:* sharp-eyed readers will note that the call to ``sim_mutations`` was not wrapped in +{func}`.add_mutation_metadata`. If we wanted to read this tree sequence into SLiM again +we'd need to add mutation metadata for these last mutations. +The easiest place to do this would be a call to {func}`.add_mutation_metadata_tables` +just after the ``union`` +(thus avoiding an extra conversion to tree sequence); +this will add metadata for only those mutations not already recorded. +We've left that step out of the code here for simplicity. diff --git a/docs/vignette_parallel_phylo.md b/docs/vignette_parallel_phylo.md index efa70f2f..d75eb921 100644 --- a/docs/vignette_parallel_phylo.md +++ b/docs/vignette_parallel_phylo.md @@ -110,10 +110,7 @@ f.close() Here's the result. Again, don't worry about the details, but you can see that the file encodes the phylogeny through a bunch of ``child : parent`` "rules": -```{code-cell} -:tags: ["hide-input"] -%%bash -cat sims.make +```{literalinclude} sims.make ``` With the makefile in hand, @@ -304,9 +301,7 @@ samples per population. ```{code-cell} rng = np.random.default_rng(seed=123) 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]) +ind_pops = tsu.individuals_population[ind_alive] subsample_indivs = [ rng.choice(ind_alive[ind_pops == pop_ids[name]], 2) for name in pops diff --git a/docs/vignette_space.md b/docs/vignette_space.md index 3c1d3ea2..fc879413 100644 --- a/docs/vignette_space.md +++ b/docs/vignette_space.md @@ -98,13 +98,17 @@ and so individuals may live for more than one time step (even up to age 10, it s Let's check that all these individuals are alive at either (a) today or (b) 1000 time steps ago. ```{code-cell} +slim_ts_md = slim_ts.metadata for t in [0, 1000]: - alive = pyslim.individuals_alive_at(slim_ts, t) + alive = pyslim.individuals_alive_at(slim_ts, t, ts_metadata=slim_ts_md) print(f"There were {len(alive)} individuals alive {t} time steps in the past.") ``` -And, 1242 + 1255 is 2497, the total number of individuals. -So, this all checks out. +We can add these numbers to get the total number of individuals, so this all checks out. +On a separate note: here we used the optional ``ts_metadata`` argument +to {func}`.individuals_alive_at`: see [](sec_metadata_using_top_level) for discussion: +this is not important here because we only run the method twice, but this would be essential +if we had a longer list of times. ## Recapitation and mutation @@ -142,11 +146,13 @@ 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 = msprime.sim_mutations( +ts = pyslim.add_mutation_metadata( + msprime.sim_mutations( recap_ts, rate=1e-8, model=msprime.SLiMMutationModel(type=0), keep=True, + ) ) ts.dump("spatial_sim.recap.trees") diff --git a/pyproject.toml b/pyproject.toml index e86faeab..0a43898e 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -31,7 +31,7 @@ keywords = ["tree sequences", "tskit"] requires-python = ">=3.11" dependencies = [ "msprime>=1.0.1", - "tskit", + "tskit>=1.0.3", "numpy", ] @@ -56,6 +56,7 @@ test = [ "tskit", "msprime", "pandas", + "frozendict", ] docs = [ diff --git a/pyslim/_version.py b/pyslim/_version.py index 3b480d79..8eea699a 100644 --- a/pyslim/_version.py +++ b/pyslim/_version.py @@ -5,6 +5,6 @@ except Exception: pyslim_version = "unknown" -slim_file_version = "0.9" +slim_file_version = "1.0" # other file versions that require no modification -compatible_slim_file_versions = ["0.9"] +compatible_slim_file_versions = ["1.0"] diff --git a/pyslim/methods.py b/pyslim/methods.py index 19c12206..cf84634c 100644 --- a/pyslim/methods.py +++ b/pyslim/methods.py @@ -12,6 +12,7 @@ set_metadata_schemas, set_tree_sequence_metadata, ) +from .slim_tree_sequence import mutation_metadata from .util import unique_labels_by_group @@ -43,7 +44,7 @@ def _mark_not_samples(tables, nodes): ) -def _chromosome_index(ts): +def _chromosome_index(ts_metadata): """ For a tree sequence produced by a multichromosome simulation, returns the index of the chromosome whose information is stored in this tree sequence @@ -52,18 +53,19 @@ def _chromosome_index(ts): ``ts.metadata['SLiM']['this_chromosome']``, and provides the index of this chromosome into `ts.metadata['SLiM']['chromosomes']``, if present. - :param tskit.TreeSequence ts: The tree sequence or table collection. + :param dict ts_metadata: The top-level metadata from a tree sequence + or table collection. """ if not ( - isinstance(ts.metadata, dict) - and "SLiM" in ts.metadata - and "this_chromosome" in ts.metadata["SLiM"] + isinstance(ts_metadata, dict) + and "SLiM" in ts_metadata + and "this_chromosome" in ts_metadata["SLiM"] ): raise ValueError( "The tree sequence does not have the necessary " "information in top-level metadata." ) - k = ts.metadata["SLiM"]["this_chromosome"]["index"] + k = ts_metadata["SLiM"]["this_chromosome"]["index"] return k @@ -88,15 +90,17 @@ def _is_chrom_vacant(k, b): return (b >> i & 1) > 0 -def has_vacant_samples(ts): +def has_vacant_samples(ts, _ts_metadata=None): """ Returns whether the tree sequence has vacant sample nodes. See :meth:`remove_vacant`. :param tskit.TreeSequence ts: The tree sequence. """ + if _ts_metadata is None: + _ts_metadata = ts.metadata out = False - k = _chromosome_index(ts) + k = _chromosome_index(_ts_metadata) for n in ts.samples(): md = ts.node(n).metadata if md is not None: @@ -106,8 +110,45 @@ def has_vacant_samples(ts): return out +def nodes_vacant(ts): + """ + Evaluates which nodes in the tree sequence are vacant: returns a boolean + vector whose k-th element is True if the k-th node is labelled as *vacant* + in the node's metadata recorded by SLiM. A vacant node represents a blank + placeholder in SLiM: either a "null haplosome" (used as placeholders for + sex chromosomes and other chromosome types not of consistent ploidy in all + individuals) or simply an unused node for haploid chromosome types. See + :meth:`remove_vacant`. + + + :param tskit.TreeSequence ts: The tree sequence. + :return boolean ndarray: + """ + # not using chrom_index here because we expect people to call this on lots of nodes + k = ts.metadata["SLiM"]["this_chromosome"]["index"] + out = np.array( + [ + node.metadata is not None and _is_chrom_vacant(k, node.metadata["is_vacant"]) + for node in ts.nodes() + ], + dtype="bool", + ) + return out + + def node_is_vacant(ts, node): """ + **DEPRECATED:** use :func:`.nodes_vacant` instead. This function requires + top-level metadata access, which can be costly, so it is much better to do, + for instance: + + .. code-block:: python + + vacant = nodes_vacant(ts) + for node in ts.nodes(): + # instead of node_is_vacant(ts, node), use: + vacant[node.id] + Returns True if the node is labelled as *vacant* in the node's metadata recorded by SLiM. A vacant node represents a blank placeholder in SLiM: either a "null haplosome" (used as placeholders for sex chromosomes and other @@ -117,12 +158,19 @@ def node_is_vacant(ts, node): :param tskit.TreeSequence ts: The tree sequence. :param tskit.Node node: The node object. """ + warnings.warn( + "The node_is_vacant method is deprecated: changes in SLiM v6 " + " means that repeated use of this method will be unreasonably slow, " + "so it will be removed in a future version of pyslim: " + "obtain this information from pyslim.vacant_nodes( ) instead.", + FutureWarning, + ) # not using chrom_index here because we expect people to call this on lots of nodes k = ts.metadata["SLiM"]["this_chromosome"]["index"] return node.metadata is not None and _is_chrom_vacant(k, node.metadata["is_vacant"]) -def _record_vacant_tables(tables): +def _record_vacant_tables(tables, ts_metadata): """ Sets the NODE_IS_VACANT_SAMPLE flag for all vacant, sample nodes. See :meth:`remove_vacant`. @@ -135,7 +183,7 @@ def _record_vacant_tables(tables): "flags are being overwritten; this may mean you've already run " "remove_vacant and so don't need to run it again." ) - k = _chromosome_index(tables) + k = _chromosome_index(ts_metadata) dn = tables.nodes.asdict() dn["flags"] &= ~NODE_IS_VACANT_SAMPLE @@ -156,7 +204,7 @@ def _remove_vacant_sample_flags(tables): tables.nodes.set_columns(**dn) -def remove_vacant(ts): +def remove_vacant(ts, _ts_metadata=None): """ Remove sample flags from all vacant nodes. @@ -177,23 +225,27 @@ def remove_vacant(ts): :param tskit.TreeSequence ts: The tree sequence. """ + if _ts_metadata is None: + _ts_metadata = ts.metadata tables = ts.dump_tables() - remove_vacant_tables(tables) + remove_vacant_tables(tables, _ts_metadata) return tables.tree_sequence() -def remove_vacant_tables(tables): +def remove_vacant_tables(tables, _ts_metadata=None): """ Does the work of :meth:`remove_vacant`, modifying ``tables`` in place. :param tskit.TableCollection tables: The tables underlying a tree sequence. """ - _record_vacant_tables(tables) + if _ts_metadata is None: + _ts_metadata = tables.metadata + _record_vacant_tables(tables, _ts_metadata) is_vacant = np.where(tables.nodes.flags & NODE_IS_VACANT_SAMPLE > 0)[0] _mark_not_samples(tables, is_vacant) -def restore_vacant(ts): +def restore_vacant(ts, _ts_metadata=None): """ The inverse of :meth:`remove_vacant`. @@ -204,19 +256,23 @@ def restore_vacant(ts): :param tskit.TreeSequence ts: The tree sequence. """ + if _ts_metadata is None: + _ts_metadata = ts.metadata tables = ts.dump_tables() - restore_vacant_tables(tables) + restore_vacant_tables(tables, _ts_metadata) return tables.tree_sequence() -def restore_vacant_tables(tables): +def restore_vacant_tables(tables, _ts_metadata=None): """ Does the work of :meth:`restore_vacant`, modifying ``tables`` in place. :param tskit.TableCollection tables: The tables underlying a tree sequence. """ + if _ts_metadata is None: + _ts_metadata = tables.metadata is_vacant = np.where(tables.nodes.flags & NODE_IS_VACANT_SAMPLE > 0)[0] - k = _chromosome_index(tables) + k = _chromosome_index(_ts_metadata) for j in is_vacant: n = tables.nodes[j] if n.metadata is None: @@ -273,14 +329,15 @@ def recapitate(ts, ancestral_Ne=None, *, keep_vacant=False, **kwargs): vacant sample nodes. Default: False. :param dict kwargs: Any other arguments to :func:`msprime.sim_ancestry`. """ - is_current_version(ts, _warn=True) + ts_metadata = ts.metadata + is_current_version(ts_metadata, _warn=True) # we need to ask msprime to *not* simulate from any 'vacant' haplosomes; # which we do by marking these as not samples; note that `initial_state` # can take a TableCollection, not just a TreeSequence - has_vacant = has_vacant_samples(ts) + has_vacant = has_vacant_samples(ts, ts_metadata) if has_vacant: - ts = remove_vacant(ts) + ts = remove_vacant(ts, ts_metadata) if ancestral_Ne is not None: if "demography" in kwargs: @@ -338,11 +395,90 @@ def recapitate(ts, ancestral_Ne=None, *, keep_vacant=False, **kwargs): recap = msprime.sim_ancestry(initial_state=ts, **kwargs) if has_vacant and keep_vacant: - recap = restore_vacant(recap) + recap = restore_vacant(recap, ts_metadata) return recap +def add_mutation_metadata(ts, mutation_type=0, remove_unused=False): + """ + Returns a new tree sequence with default information added to the top-level metadata + for each mutation in the tree sequence for which that information is not already present. + To do this, this method looks for all SLiM IDs that are found in the derived + state of some mutation but are not represented in the top-level metadata + (see :func:`.mutation_metadata`). This function then adds entries to that top-level + metadata with default values (see :func:`.default_slim_metadata`), + except that (a) the ``mutation_type`` can be specified; + and (b) the ``slim_time`` is set using the ``tick`` value in top-level metadata + and the ``time`` of the oldest tskit mutation in which the SLiM mutation occurs. + + :param tskit.TreeSequence ts: The tree sequence to transform. + :param int mutation_type: The numeric ID of the mutation type in SLiM. + :param bool remove_unused: Whether to also remove from metadata information about any + mutations not seen in the derived states of the tree sequence. + """ + tables = ts.dump_tables() + add_mutation_metadata_tables( + tables, mutation_type=mutation_type, remove_unused=remove_unused + ) + return tables.tree_sequence() + + +def add_mutation_metadata_tables(tables, mutation_type=0, remove_unused=False): + """ + Modifies the tables in place to add metadata for any mutations for which it is missing; + see :func:`.add_mutation_metadata`. + + :param tskit.TableCollection tables: The table collection to be modified. + :param int mutation_type: The numeric ID of the mutation type in SLiM. + :param bool remove_unused: Whether to also remove from metadata information about any + mutations not seen in the derived states of the tree sequence. + """ + ts_metadata = tables.metadata + if ( + not isinstance(ts_metadata, dict) + or "SLiM" not in ts_metadata + or "SLiM_mutation_list" not in ts_metadata + ): + raise ValueError( + "Top-level metadata schema is not correct: " + "do you need to run pyslim.annotate()?" + ) + existing_muts = {x["mutation_id"] for x in ts_metadata["SLiM_mutation_list"]} + mut_ids = [ + (int(j), mut.time) + for mut in tables.mutations + for j in mut.derived_state.split(",") + ] + mut_ids.sort() + mut_ids = np.array(mut_ids, dtype="int") # floors times + # remove duplicate IDs, keeping the last (most recent) + keep = np.full(len(mut_ids), True, dtype="bool") + keep[np.where(np.diff(mut_ids[:, 0]) == 0)[0]] = False + mut_ids = mut_ids[keep, :] + mut_ids[:, 1] = slim_time( + tables, mut_ids[:, 1], stage="late", ts_metadata=ts_metadata + ) + # this assumes mutations were added in late(), which is what SLiM does + ts_metadata["SLiM_mutation_list"].extend( + [ + default_slim_metadata( + "mutation_list_entry", + mutation_id=int(j), + mutation_type=mutation_type, + slim_time=int(t), + ) + for j, t in mut_ids + if j not in existing_muts + ] + ) + if remove_unused and len(mut_ids) < len(ts_metadata["SLiM_mutation_list"]): + ts_metadata["SLiM_mutation_list"] = [ + x for x in ts_metadata["SLiM_mutation_list"] if x["mutation_id"] in mut_ids + ] + tables.metadata = ts_metadata + + def convert_alleles(ts): """ Returns a modified tree sequence in which alleles have been replaced by @@ -379,25 +515,28 @@ def convert_alleles(ts): # so we must guess which is the most recent, by choosing the one that # has the largest SLiM time, doesn't appear in the parent list, or has # the lagest SLiM ID. - nuc_inds = tables.mutations.metadata_vector( - ["mutation_list", 0, "nucleotide"], dtype="int" - ) - num_stacked = np.array([len(m.metadata["mutation_list"]) for m in ts.mutations()]) - for k in np.where(num_stacked > 1)[0]: + mut_metadata = mutation_metadata(ts) + mut_ids = np.array([x["mutation_id"] for x in mut_metadata.values()], dtype="int") + alleles = np.array([x["nucleotide"] for x in mut_metadata.values()], dtype="int") + # mut_inds will map from tskit-mutations to slim-mutations + mut_inds = ts.mutations_derived_state.copy() + num_stacked = np.strings.count(mut_inds, ",") + mut_inds[num_stacked > 0] = "-1" + mut_inds = mut_inds.astype("int", copy=False) + for k in np.where(num_stacked > 0)[0]: mut = ts.mutation(k) if mut.parent == tskit.NULL: pids = [] else: pids = ts.mutation(mut.parent).derived_state.split(",") x = [ - (md["slim_time"], i not in pids, int(i), j) - for j, (i, md) in enumerate( - zip(mut.derived_state.split(","), mut.metadata["mutation_list"]) - ) + (mut_metadata[int(i)]["slim_time"], i not in pids, int(i), j) + for j, i in enumerate(mut.derived_state.split(",")) ] x.sort() - j = x[-1][3] - nuc_inds[k] = mut.metadata["mutation_list"][j]["nucleotide"] + mut_inds[k] = x[-1][2] + assert np.all(mut_inds >= 0), "This should not occur: please file a bug report." + nuc_inds = alleles[np.searchsorted(mut_ids, mut_inds)] if np.any(nuc_inds == -1): raise ValueError("All mutations must be nucleotide mutations.") da = np.array(NUCLEOTIDES)[nuc_inds] @@ -405,7 +544,6 @@ def convert_alleles(ts): k = tables.sites.position.astype("int") aa = np.frombuffer(ts.reference_sequence.data.encode("utf-8"), dtype="S1")[k] tables.sites.packset_ancestral_state(aa.tobytes().decode("utf-8")) - return tables.tree_sequence() @@ -461,11 +599,11 @@ def generate_nucleotides(ts, reference_sequence=None, keep=True, seed=None): raise ValueError( "Reference sequence must be a string of A, C, G, and T only." ) - + ts_metadata = ts.metadata + mut_info = mutation_metadata(ts, _ts_metadata=ts_metadata) tables = ts.dump_tables() if reference_sequence is not None: tables.reference_sequence.data = reference_sequence - tables.mutations.clear() sets = [[k for k in range(4) if k != i] for i in range(4)] states = np.full((ts.num_mutations,), -1) k = tables.sites.position.astype("int") @@ -484,9 +622,9 @@ def generate_nucleotides(ts, reference_sequence=None, keep=True, seed=None): pa = states[mut.parent] pds = ts.mutation(mut.parent).derived_state.split(",") this_da = pa - ml = mut.metadata max_time = -np.inf - for i, md in zip(mut.derived_state.split(","), ml["mutation_list"]): + for i in mut.derived_state.split(","): + md = mut_info[int(i)] da = md["nucleotide"] if da == -1 or not keep: if i in muts: @@ -502,22 +640,26 @@ def generate_nucleotides(ts, reference_sequence=None, keep=True, seed=None): this_da = da max_time = md["slim_time"] states[mut.id] = this_da - tables.mutations.append(mut.replace(metadata=ml)) - md = tables.metadata - md["SLiM"]["nucleotide_based"] = True - tables.metadata = md + ts_metadata["SLiM"]["nucleotide_based"] = True + ts_metadata["SLiM_mutation_list"] = list(mut_info.values()) + tables.metadata = ts_metadata return tables.tree_sequence() -def individual_ages(ts): +def individual_ages(ts, ts_metadata=None): """ 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"]``. + :param tskit.TreeSequence ts: The tree sequence. + :param dict ts_metadata: Optionally, the top-level metadata for ``ts``. If + this does not match the actual top-level metadata, incorrect values may result. :return: An array of ages of individuals. """ - if ts.metadata["SLiM"]["model_type"] != "WF": + if ts_metadata is None: + ts_metadata = ts.metadata + if ts_metadata["SLiM"]["model_type"] != "WF": ages = ts.tables.individuals.metadata_vector("age") else: ages = np.zeros(ts.num_individuals, dtype="int") @@ -525,7 +667,13 @@ def individual_ages(ts): def individuals_alive_at( - ts, time, stage="late", remembered_stage=None, population=None, samples_only=False + ts, + time, + stage="late", + remembered_stage=None, + population=None, + samples_only=False, + ts_metadata=None, ): """ Returns an array giving the IDs of all individuals that are known to be @@ -576,26 +724,29 @@ def individuals_alive_at( 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. + :param dict ts_metadata: Optionally, the top-level metadata for ``ts``. If + this does not match the actual top-level metadata, incorrect values may result. """ - is_current_version(ts, _warn=True) if stage not in ("late", "early", "first"): raise ValueError( f"Unknown stage '{stage}': should be either 'first', 'early' or 'late'." ) - + if ts_metadata is None: + ts_metadata = ts.metadata + is_current_version(ts_metadata, _warn=True) if remembered_stage is None: - remembered_stage = ts.metadata["SLiM"]["stage"] + remembered_stage = ts_metadata["SLiM"]["stage"] if remembered_stage not in ("late", "early", "first"): raise ValueError( f"Unknown remembered_stage '{remembered_stage}': " "should be either 'first', 'early' or 'late'." ) - if remembered_stage != ts.metadata["SLiM"]["stage"]: + 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" + 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." ) @@ -615,12 +766,12 @@ def individuals_alive_at( # let x = 1 if the stage is 'first' or (is 'early' and WF) # and y = 1 if remembered stage is 'late' or (is 'early' and nonWF); # then t = time + x + y - 1 . - is_wf = ts.metadata["SLiM"]["model_type"] == "WF" + is_wf = ts_metadata["SLiM"]["model_type"] == "WF" x = stage == "first" or (stage == "early" and is_wf) y = remembered_stage == "late" or (remembered_stage == "early" and not is_wf) t = time + x + y - 1 birth_times = ts.individuals_time - ages = individual_ages(ts) + ages = individual_ages(ts, ts_metadata) if is_wf: alive_bool = birth_times == t else: @@ -643,7 +794,9 @@ def individuals_alive_at( return np.where(alive_bool)[0] -def individual_ages_at(ts, time, stage="late", remembered_stage="late"): +def individual_ages_at( + ts, time, stage="late", remembered_stage="late", ts_metadata=None +): """ 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. @@ -669,20 +822,24 @@ def individual_ages_at(ts, time, stage="late", remembered_stage="late"): 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. + :param dict ts_metadata: Optionally, the top-level metadata for ``ts``. If + this does not match the actual top-level metadata, incorrect values may result. """ + if ts_metadata is None: + ts_metadata = ts.metadata ages = np.repeat(np.nan, ts.num_individuals) alive = individuals_alive_at( - ts, time, stage=stage, remembered_stage=remembered_stage + ts, time, stage=stage, remembered_stage=remembered_stage, ts_metadata=ts_metadata ) # to convert individuals_time to number of ticks ago we subtract (y - 1), so - is_wf = ts.metadata["SLiM"]["model_type"] == "WF" + is_wf = ts_metadata["SLiM"]["model_type"] == "WF" y = remembered_stage == "late" or (remembered_stage == "early" and not is_wf) t = time + y - 1 ages[alive] = ts.individuals_time[alive] - t return ages -def slim_time(ts, time, stage="late"): +def slim_time(ts, time, stage="late", ts_metadata=None): """ 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 @@ -707,17 +864,26 @@ def slim_time(ts, time, stage="late"): this may not return what you expect. See :ref:`sec_metadata_converting_times` for more discussion. + This method accesses top-level metadata, which may be a costly operation, + so if this method will be called many times, it is recommended to + extract this to a variable (e.g., ``ts_metadata = ts.metadata``) and pass it + to this method (as ``ts_metadata``). However, beware: if ``ts_metadata`` + is not in sync with the actual top-level metadata, incorrect values may result. + :param tskit.TreeSequence ts: A SLiM-compatible TreeSequence. :param numpy.ndarray time: An array of times to be converted. :param str stage: The stage of the SLiM life cycle that the SLiM time should be computed for. + :param dict ts_metadata: Optionally, the top-level metadata for ``ts``. """ - is_current_version(ts, _warn=True) - is_wf = ts.metadata["SLiM"]["model_type"] == "WF" - remembered_stage = ts.metadata["SLiM"]["stage"] + if ts_metadata is None: + ts_metadata = ts.metadata + is_current_version(ts_metadata, _warn=True) + is_wf = ts_metadata["SLiM"]["model_type"] == "WF" + remembered_stage = ts_metadata["SLiM"]["stage"] x = stage == "first" or (stage == "early" and is_wf) y = remembered_stage == "late" or (remembered_stage == "early" and not is_wf) - slim_time = ts.metadata["SLiM"]["tick"] - time + x + y - 1 + slim_time = ts_metadata["SLiM"]["tick"] - time + x + y - 1 return slim_time @@ -920,7 +1086,7 @@ def annotate(ts, **kwargs): :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.) + with defaults. (If False, information about mutations is unchanged.) """ tables = ts.dump_tables() annotate_tables(tables, **kwargs) @@ -965,12 +1131,15 @@ def annotate_tables( top_metadata["tick"] = tick top_metadata["cycle"] = cycle top_metadata["stage"] = stage - set_tree_sequence_metadata(tables, **top_metadata) + md = tables.metadata + if isinstance(md, dict) and "SLiM_mutation_list" in md: + top_metadata["SLiM_mutation_list"] = md["SLiM_mutation_list"] + ts_metadata = 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) + _annotate_sites_mutations(tables, ts_metadata=ts_metadata) if reference_sequence is not None: tables.reference_sequence.data = reference_sequence @@ -1109,46 +1278,46 @@ def _annotate_populations(tables): tables.populations[j] = p.replace(metadata=md) -def _annotate_sites_mutations(tables): +def _annotate_sites_mutations(tables, ts_metadata): """ 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 + for SLiM to load in a tree sequence. This means adding metadata to the + SLiM_mutation_list in top-level metadata. 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 + This will replace any information already in the metadata, and the derived + state columns of the Mutation table. We set slim_time in metadata so that - tick = floor(tskit time) + slim_time """ - if len(tables.mutations.metadata) > 0: + if ( + isinstance(ts_metadata, dict) + and "SLiM_mutation_list" in ts_metadata + and len(ts_metadata["SLiM_mutation_list"]) > 0 + ): warnings.warn( - "The provided tree sequence already has some mutations with " + "The provided tree sequence already has top-level mutation " "metadata; this metadata will be overwritten." ) 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( + slim_time = ts_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 + mutation_list = [ + { + "mutation_id": j, + "mutation_type": default_mut["mutation_type"], + "per_trait": default_mut["per_trait"], + "subpopulation": default_mut["subpopulation"], + "slim_time": int(st), + "nucleotide": default_mut["nucleotide"], + "padding": None, + } + for j, st in enumerate(slim_time) ] - mdb, mdo = tskit.pack_bytes(mutation_metadata) + ts_metadata["SLiM_mutation_list"] = mutation_list + tables.metadata = ts_metadata + dsb, dso = tskit.pack_bytes([str(j).encode() for j in range(num_mutations)]) tables.mutations.set_columns( site=tables.mutations.site, node=tables.mutations.node, @@ -1156,8 +1325,6 @@ def _annotate_sites_mutations(tables): derived_state=dsb, derived_state_offset=dso, parent=tables.mutations.parent, - metadata=mdb, - metadata_offset=mdo, ) tables.sites.set_columns( position=tables.sites.position, diff --git a/pyslim/slim_metadata.py b/pyslim/slim_metadata.py index 0fb4144b..e515f559 100644 --- a/pyslim/slim_metadata.py +++ b/pyslim/slim_metadata.py @@ -1,6 +1,8 @@ +import copy import json import warnings +import numpy as np import tskit from ._version import * # noqa F403 @@ -38,227 +40,309 @@ def is_vacant_num_bytes(num_chromosomes): _raw_slim_metadata_schemas = { "tree_sequence": { "$schema": "http://json-schema.org/schema#", - "codec": "json", - "examples": [ - { - "SLiM": { - "file_version": "0.9", - "name": "fox", - "description": "foxes on Catalina island", - "cycle": 123, - "tick": 123, - "model_type": "WF", - "this_chromosome": { - "id": 1, - "index": 0, - "symbol": "1", - "name": "autosome_1", - "type": "A", - }, - "chromosomes": [ - {"id": 1, "symbol": "1", "name": "autosome_1", "type": "A"}, - {"id": 35, "symbol": "MT", "name": "mtDNA", "type": "HF"}, - ], - "nucleotide_based": False, - "separate_sexes": True, - "spatial_dimensionality": "xy", - "spatial_periodicity": "x", + "codec": "json+struct", + "json": { + "codec": "json", + "description": "SLiM schema for JSON top-level metadata.", + "examples": [ + { + "SLiM": { + "chromosomes": [ + {"id": 1, "name": "autosome_1", "symbol": "1", "type": "A"}, + {"id": 35, "name": "mtDNA", "symbol": "MT", "type": "HF"}, + ], + "cycle": 123, + "description": "foxes on Catalina island", + "file_version": "1.0", + "model_type": "WF", + "name": "fox", + "nucleotide_based": False, + "separate_sexes": True, + "spatial_dimensionality": "xy", + "spatial_periodicity": "x", + "this_chromosome": { + "id": 1, + "index": 0, + "name": "autosome_1", + "symbol": "1", + "type": "A", + }, + "tick": 123, + "traits": [ + {"index": 0, "name": "simT", "type": "multiplicative"} + ], + } } - } - ], - "properties": { - "SLiM": { - "description": "Top-level metadata for a SLiM tree sequence, file format version 0.9", - "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", - }, - "this_chromosome": { - "description": "The chromosome represented by the tree sequence in this file.", - "properties": { - "id": { - "description": "An integer identifier for the chromosome, unique within this set of tree sequences; often the chromosome number in the organism being represented, such as 1.", - "type": "integer", - }, - "index": { - "description": "The (zero-based) index of this chromosome in the chromosomes metadata array (if present), which should match the information given here.", - "type": "integer", - }, - "symbol": { - "description": 'A short string symbol for the chromosome, unique within this set of tree sequences, such as "1" or "MT".', - "type": "string", - }, - "name": { - "description": "A user-specified name for the chromosome, such as an accession identifier.", - "type": "string", - }, - "type": { - "description": "The type of chromosome, as specified by SLiM.", - "type": "string", + ], + "properties": { + "SLiM": { + "description": "Top-level metadata for a SLiM tree sequence, file format version 1.0", + "properties": { + "chromosomes": { + "description": "The chromosomes represented by the collection of tree sequences, of which this tree sequence is one member.", + "items": { + "properties": { + "id": { + "description": "An integer identifier for the chromosome, unique within this set of tree sequences; often the chromosome number in the organism being represented, such as 1.", + "type": "integer", + }, + "name": { + "description": "A user-specified name for the chromosome, such as an accession identifier.", + "type": "string", + }, + "symbol": { + "description": 'A short string symbol for the chromosome, unique within this set of tree sequences, such as "1" or "MT".', + "type": "string", + }, + "type": { + "description": "The type of chromosome, as specified by SLiM.", + "type": "string", + }, + }, + "required": ["id", "symbol", "type"], + "type": "object", }, + "type": "array", }, - "required": ["id", "index", "symbol", "type"], - "type": "object", - }, - "chromosomes": { - "description": "The chromosomes represented by the collection of tree sequences, of which this tree sequence is one member.", - "items": { + "cycle": { + "description": "The 'SLiM cycle' counter when this tree sequence was recorded.", + "type": "integer", + }, + "description": { + "description": "A user-configurable description of the species represented by this tree sequence.", + "type": "string", + }, + "file_version": { + "description": "The SLiM 'file format version' of this tree sequence.", + "type": "string", + }, + "model_type": { + "description": "The model type used for the last part of this simulation (WF or nonWF).", + "enum": ["WF", "nonWF"], + "type": "string", + }, + "name": { + "description": "The SLiM species name represented by this tree sequence.", + "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", + }, + "this_chromosome": { + "description": "The chromosome represented by the tree sequence in this file.", "properties": { "id": { "description": "An integer identifier for the chromosome, unique within this set of tree sequences; often the chromosome number in the organism being represented, such as 1.", "type": "integer", }, - "symbol": { - "description": 'A short string symbol for the chromosome, unique within this set of tree sequences, such as "1" or "MT".', - "type": "string", + "index": { + "description": "The (zero-based) index of this chromosome in the chromosomes metadata array (if present), which should match the information given here.", + "type": "integer", }, "name": { "description": "A user-specified name for the chromosome, such as an accession identifier.", "type": "string", }, + "symbol": { + "description": 'A short string symbol for the chromosome, unique within this set of tree sequences, such as "1" or "MT".', + "type": "string", + }, "type": { "description": "The type of chromosome, as specified by SLiM.", "type": "string", }, }, - "required": ["id", "symbol", "type"], + "required": ["id", "index", "symbol", "type"], "type": "object", }, - "type": "array", - }, - "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", - "this_chromosome", - "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": { - "mutation_type": { - "binaryFormat": "i", - "description": "The index of this mutation's mutationType.", - "index": 1, - "type": "integer", - }, - "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", - }, - "selection_coeff": { - "binaryFormat": "f", - "description": "This mutation's selection coefficient.", - "index": 2, - "type": "number", - }, - "slim_time": { - "binaryFormat": "i", - "description": "The SLiM tick counter when this mutation occurred.", - "index": 4, + "tick": { + "description": "The 'SLiM tick' counter when this tree sequence was recorded.", "type": "integer", }, - "subpopulation": { - "binaryFormat": "i", - "description": "The ID of the subpopulation this mutation occurred in.", - "index": 3, - "type": "integer", + "traits": { + "description": "The traits defined for this tree sequence; each mutation and individual will have per-trait metadata.", + "items": { + "properties": { + "baselineAccumulation": { + "description": "Whether the baseline offset includes accumulated effects from fixed (substituted) mutations.", + "type": "boolean", + }, + "baselineOffset": { + "description": "The baseline offset of the trait.", + "type": "number", + }, + "directFitnessEffect": { + "description": "Whether the trait's effects are used directly as fitness effects.", + "type": "boolean", + }, + "index": { + "description": "The integer index for the trait; indices must be sequential starting from zero.", + "type": "integer", + }, + "individualOffsetMean": { + "description": "The mean of the trait's individual offset distribution (which might or might not be used).", + "type": "number", + }, + "individualOffsetSD": { + "description": "The standard deviation of the trait's individual offset distribution (which might or might not be used).", + "type": "number", + }, + "name": { + "description": "The string name for the trait.", + "type": "string", + }, + "type": { + "description": "The type of the trait; this must be 'additive', 'multiplicative', or 'logistic'.", + "enum": [ + "additive", + "multiplicative", + "logistic", + ], + "type": "string", + }, + }, + "required": ["index", "name", "type"], + "type": "object", + }, + "type": "array", }, }, "required": [ - "mutation_type", - "selection_coeff", - "subpopulation", - "slim_time", - "nucleotide", + "model_type", + "tick", + "file_version", + "spatial_dimensionality", + "spatial_periodicity", + "this_chromosome", + "separate_sexes", + "nucleotide_based", + "traits", ], "type": "object", - }, - "noLengthEncodingExhaustBuffer": True, - "type": "array", - } + } + }, + "required": ["SLiM"], + "type": "object", + }, + "struct": { + "codec": "struct", + "description": "SLiM schema for binary top-level metadata.", + "properties": { + "SLiM_mutation_list": { + "arrayLengthFormat": "Q", + "items": { + "additionalProperties": False, + "properties": { + "mutation_id": { + "binaryFormat": "q", + "description": "The SLiM mutation ID for this mutation.", + "index": 1, + "type": "integer", + }, + "mutation_type": { + "binaryFormat": "i", + "description": "The id of this mutation's mutationType.", + "index": 2, + "type": "integer", + }, + "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", + }, + "padding": { + "binaryFormat": "3x", + "description": "Padding bytes for alignment", + "index": 6, + "type": "null", + }, + "per_trait": { + "index": 7, + "items": { + "additionalProperties": False, + "properties": { + "dominance": { + "binaryFormat": "f", + "description": "The dominance coefficient for this trait.", + "index": 2, + "type": "number", + }, + "effect_size": { + "binaryFormat": "f", + "description": "The effect size for this trait.", + "index": 1, + "type": "number", + }, + "hemizygous_dominance": { + "binaryFormat": "f", + "description": "The hemizygous dominance coefficient for this trait.", + "index": 3, + "type": "number", + }, + }, + "required": [ + "dominance", + "effect_size", + "hemizygous_dominance", + ], + "type": "object", + }, + "length": 1, # NOTE this may need to be changed to match the number of traits! + "type": "array", + }, + "slim_time": { + "binaryFormat": "i", + "description": "The SLiM tick counter when this mutation occurred.", + "index": 4, + "type": "integer", + }, + "subpopulation": { + "binaryFormat": "i", + "description": "The ID of the subpopulation this mutation occurred in.", + "index": 3, + "type": "integer", + }, + }, + "required": [ + "mutation_id", + "mutation_type", + "slim_time", + "subpopulation", + "nucleotide", + "per_trait", + ], + "type": "object", + }, + "type": "array", + } + }, + "required": ["SLiM_mutation_list"], + "type": "object", }, - "required": ["mutation_list"], - "type": "object", }, + "edge": None, + "site": None, + "mutation": None, "node": { "$schema": "http://json-schema.org/schema#", "additionalProperties": False, @@ -269,12 +353,12 @@ def is_vacant_num_bytes(num_chromosomes): "slim_id": { "binaryFormat": "q", "description": "The 'pedigree ID' of the haplosomes associated with this node in SLiM.", - "index": 0, + "index": 1, "type": "integer", }, "is_vacant": { "description": "A vector of byte (uint8_t) values, with each bit representing whether the node represents a vacant position, either unused or a null haplosome (1), or a non-null haplosome (0), in the corresponding chromosome. This field encodes vacancy for all of the chromosomes in the model, not just the chromosome represented in this file (so that the node table is identical across all chromosomes for a multi-chromosome model). Each chromosome receives one bit here; there are two node table entries per individual, used for the two haplosomes of every chromosome, so only one bit is needed in each entry (making two bits total per chromosome, across the two node table entries). The least significant bit of the first byte is used first (for one haplosome of the first chromosome); the most significant bit of the last byte is used last. The number of bytes present in this field is indicated by this schema's 'binaryFormat' field, which is variable (!), and can also be deduced from the number of chromosomes in the model as given in the top-level 'chromosomes' metadata key, which should always be present if this metadata is present.", - "index": 1, + "index": 2, "type": "array", "length": 1, # MAY NEED TO BE CHANGED (in SLiM code is "%d") "items": {"type": "number", "binaryFormat": "B"}, @@ -285,8 +369,8 @@ def is_vacant_num_bytes(num_chromosomes): }, "individual": { "$schema": "http://json-schema.org/schema#", - "additionalProperties": False, "codec": "struct", + "type": "object", "description": "SLiM schema for individual metadata.", "examples": [ { @@ -295,64 +379,170 @@ def is_vacant_num_bytes(num_chromosomes): "pedigree_id": 123, "pedigree_p1": 12, "pedigree_p2": 23, + "per_trait": [{"offset": 1.0, "phenotype": 1.1}], "sex": 0, "subpopulation": 0, + "tag": 1, + "tagF": 5.5, + "tagL0_set": True, + "tagL0": True, + "tagL1_set": True, + "tagL1": False, + "tagL2_set": False, + "tagL2": False, + "tagL3_set": False, + "tagL3": False, + "tagL4_set": False, + "tagL4": False, } ], "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).", + "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", + "binaryFormat": "q", + "description": "The 'pedigree ID' of this individual in SLiM.", }, "pedigree_p1": { - "binaryFormat": "q", - "description": "The 'pedigree ID' of this individual's first parent in SLiM.", "index": 2, "type": "integer", + "binaryFormat": "q", + "description": "The 'pedigree ID' of this individual's first parent in SLiM.", }, "pedigree_p2": { + "index": 3, + "type": "integer", "binaryFormat": "q", "description": "The 'pedigree ID' of this individual's second parent in SLiM.", - "index": 3, + }, + "age": { + "index": 4, "type": "integer", + "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).", + }, + "subpopulation": { + "index": 5, + "type": "integer", + "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).", }, "sex": { + "index": 6, + "type": "integer", "binaryFormat": "i", "description": "The sex of the individual (0 for female, 1 for male, -1 for hermaphrodite).", - "index": 6, + }, + "flags": { + "index": 7, "type": "integer", + "binaryFormat": "I", + "description": "Other information about the individual: see 'flags'.", }, - "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, + "tag": { + "index": 8, "type": "integer", + "binaryFormat": "q", + "description": "The `tag` property of this individual; INT64_MIN if unset.", + }, + "tagF": { + "index": 9, + "type": "number", + "binaryFormat": "d", + "description": "The `tagF` property of this individual; -DBL_MAX if unset.", + }, + "tagL0_set": { + "index": 10, + "type": "boolean", + "binaryFormat": "?", + "description": "A flag indicating whether the `tagL0` property is set; if false, accessing `tagL0` is invalid.", + }, + "tagL0": { + "index": 11, + "type": "boolean", + "binaryFormat": "?", + "description": "The `tagL0` property of this individual; only valid if `tagL0_set` is true.", + }, + "tagL1_set": { + "index": 12, + "type": "boolean", + "binaryFormat": "?", + "description": "A flag indicating whether the `tagL1` property is set; if false, accessing `tagL1` is invalid.", + }, + "tagL1": { + "index": 13, + "type": "boolean", + "binaryFormat": "?", + "description": "The `tagL1` property of this individual; only valid if `tagL1_set` is true.", + }, + "tagL2_set": { + "index": 14, + "type": "boolean", + "binaryFormat": "?", + "description": "A flag indicating whether the `tagL2` property is set; if false, accessing `tagL2` is invalid.", + }, + "tagL2": { + "index": 15, + "type": "boolean", + "binaryFormat": "?", + "description": "The `tagL2` property of this individual; only valid if `tagL2_set` is true.", + }, + "tagL3_set": { + "index": 16, + "type": "boolean", + "binaryFormat": "?", + "description": "A flag indicating whether the `tagL3` property is set; if false, accessing `tagL3` is invalid.", + }, + "tagL3": { + "index": 17, + "type": "boolean", + "binaryFormat": "?", + "description": "The `tagL3` property of this individual; only valid if `tagL3_set` is true.", + }, + "tagL4_set": { + "index": 18, + "type": "boolean", + "binaryFormat": "?", + "description": "A flag indicating whether the `tagL4` property is set; if false, accessing `tagL4` is invalid.", + }, + "tagL4": { + "index": 19, + "type": "boolean", + "binaryFormat": "?", + "description": "The `tagL4` property of this individual; only valid if `tagL4_set` is true.", + }, + "per_trait": { + "index": 20, + "type": "array", + "length": 1, # MAY NEED TO BE CHANGED (in SLiM code is "%d") + "items": { + "additionalProperties": False, + "properties": { + "phenotype": { + "index": 1, + "type": "number", + "binaryFormat": "d", + "description": "The phenotype for this trait.", + }, + "offset": { + "index": 2, + "type": "number", + "binaryFormat": "d", + "description": "The individual offset for this trait.", + }, + }, + "required": ["offset", "phenotype"], + "type": "object", + }, }, }, + "additionalProperties": False, "required": [ "pedigree_id", "pedigree_p1", @@ -360,9 +550,21 @@ def is_vacant_num_bytes(num_chromosomes): "age", "subpopulation", "sex", + "tag", + "tagF", + "tagL0_set", + "tagL0", + "tagL1_set", + "tagL1", + "tagL2_set", + "tagL2", + "tagL3_set", + "tagL3", + "tagL4_set", + "tagL4", "flags", + "per_trait", ], - "type": "object", }, "population": { "$schema": "http://json-schema.org/schema#", @@ -464,6 +666,45 @@ def is_vacant_num_bytes(num_chromosomes): } +def slim_tree_sequence_metadata_schema(num_traits=1): + """ + The top-level metadata schema depends on the number of traits, and + {data}`.slim_metadata_schemas` + returns the schema for a single-trait simulation. This function + returns the correct schema for a simulation with arbitrary number of + traits. (The resulting schemas only differ in the + "length" of the "per_trait" property of + ``schema["properties"]["SLiM_mutation_list"]["items"]``). + + :param int num_traits: The number of traits in the model. + :return tskit.MetadataSchema: The metadata schema to be used + in the node table. + """ + schema = _raw_slim_metadata_schemas["tree_sequence"] + schema["struct"]["properties"]["SLiM_mutation_list"]["items"]["properties"][ + "per_trait" + ]["length"] = num_traits + return tskit.MetadataSchema(schema) + + +def slim_individual_metadata_schema(num_traits=1): + """ + The individual metadata schema depends on the number of traits, and + {data}`.slim_metadata_schemas` + returns the schema for a single-trait simulation. This function + returns the correct schema for a simulation with arbitrary number of + traits. (The resulting schemas only differ in the + "length" of the "per_trait" property.) + + :param int num_traits: The number of traits in the model. + :return tskit.MetadataSchema: The metadata schema to be used + in the node table. + """ + schema = _raw_slim_metadata_schemas["individual"] + schema["properties"]["per_trait"]["length"] = num_traits + return tskit.MetadataSchema(schema) + + def slim_node_metadata_schema(num_chromosomes=1): """ Unlike other schema, the node metadata schema depends on the number of @@ -510,12 +751,15 @@ def slim_node_metadata_schema(num_chromosomes=1): """ -def default_slim_metadata(name, num_chromosomes=1): +def default_slim_metadata(name, num_chromosomes=1, num_traits=1, **kwargs): """ Returns default metadata of type ``name``, where ``name`` is one of "tree_sequence", "edge", "site", "mutation", "mutation_list_entry", "node", "individual", or "population". + Additional kwargs are used to update the resulting metadata + (without validity checking). + :param str name: The type of metadata requested. :rtype dict: """ @@ -540,21 +784,26 @@ def default_slim_metadata(name, num_chromosomes=1): "type": "A", }, "chromosomes": [{"id": 1, "index": 0, "symbol": "A", "type": "A"}], - } + "traits": [{"index": 0, "name": "simT", "type": "multiplicative"}], + }, + "SLiM_mutation_list": [], } elif name == "edge": out = None elif name == "site": out = None elif name == "mutation": - out = {"mutation_list": []} + out = None elif name == "mutation_list_entry": out = { + "mutation_id": 0, "mutation_type": 0, - "selection_coeff": 0.0, "subpopulation": tskit.NULL, "slim_time": 0, "nucleotide": -1, + "per_trait": num_traits + * [{"effect_size": 0.0, "dominance": 0.5, "hemizygous_dominance": 1.0}], + "padding": None, } elif name == "node": out = { @@ -570,6 +819,19 @@ def default_slim_metadata(name, num_chromosomes=1): "flags": 0, "pedigree_p1": tskit.NULL, "pedigree_p2": tskit.NULL, + "tag": np.iinfo(np.int64).min, + "tagF": np.finfo(np.float64).min, + "tagL0_set": False, + "tagL0": False, + "tagL1_set": False, + "tagL1": False, + "tagL2_set": False, + "tagL2": False, + "tagL3_set": False, + "tagL3": False, + "tagL4_set": False, + "tagL4": False, + "per_trait": num_traits * [{"phenotype": np.nan, "offset": 1.0}], } elif name == "population": out = { @@ -594,6 +856,8 @@ def default_slim_metadata(name, num_chromosomes=1): "'edge', 'site', 'mutation', 'mutation_list_entry', 'node', " "'individual', or 'population'." ) + if out is not None: + out.update(kwargs) return out @@ -606,6 +870,7 @@ def set_tree_sequence_metadata( tables, model_type, tick, + *, cycle=None, spatial_dimensionality="", spatial_periodicity="", @@ -618,35 +883,58 @@ def set_tree_sequence_metadata( chromosomes=None, file_version=None, set_table_schemas=True, + traits=None, + SLiM_mutation_list=None, ): if file_version is None: file_version = slim_file_version - if isinstance(tables.metadata, bytes): - if len(tables.metadata) > 0: + if traits is None: + traits = [{"index": 0, "name": "simT", "type": "multiplicative"}] + num_traits = len(traits) + schema_dict = slim_tree_sequence_metadata_schema(num_traits).schema + old_schema_dict = tables.metadata_schema.schema + old_json_schema_dict = {} + old_struct_schema_dict = {} + tmd = tables.metadata + if isinstance(tmd, bytes): + if len(tmd) > 0: raise ValueError( "Tree sequence has top-level metadata but no schema: this is a problem " "since pyslim is trying to add to the metadata." ) - schema_dict = slim_metadata_schemas["tree_sequence"].schema metadata_dict = {} else: # 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 old_schema_dict["codec"] == "json": + old_json_schema_dict = tables.metadata_schema.schema + else: + assert old_schema_dict["codec"] == "json+struct", ( + "You are using an unexpected codec; " + "please raise an issue on pyslim if " + "you need this functionality." + ) + old_json_schema_dict = tables.metadata_schema.schema["json"] + old_struct_schema_dict = tables.metadata_schema.schema["struct"] if cycle is None: cycle = tick - defaults = default_slim_metadata("tree_sequence") + if chromosomes is None: + num_chromosomes = 1 + else: + num_chromosomes = len(chromosomes) + defaults = default_slim_metadata( + "tree_sequence", num_chromosomes=num_chromosomes, num_traits=num_traits + ) if this_chromosome is None: this_chromosome = defaults["SLiM"]["this_chromosome"] if chromosomes is None: chromosomes = defaults["SLiM"]["chromosomes"] - assert schema_dict["codec"] == "json" - assert schema_dict["type"] == "object" - if "properties" not in schema_dict: - schema_dict["properties"] = {} - schema_dict["properties"]["SLiM"] = slim_metadata_schemas["tree_sequence"].schema[ - "properties" - ]["SLiM"] + if "properties" in old_json_schema_dict: + schema_dict["json"]["properties"].update(old_json_schema_dict["properties"]) + if "properties" in old_struct_schema_dict: + schema_dict["struct"]["properties"].update(old_struct_schema_dict["properties"]) + if SLiM_mutation_list is None: + SLiM_mutation_list = [] tables.metadata_schema = tskit.MetadataSchema(schema_dict) metadata_dict["SLiM"] = { "model_type": model_type, @@ -662,16 +950,19 @@ def set_tree_sequence_metadata( "description": description, "this_chromosome": this_chromosome, "chromosomes": chromosomes, + "traits": traits, } + metadata_dict["SLiM_mutation_list"] = SLiM_mutation_list tables.metadata = metadata_dict + return metadata_dict -def set_metadata_schemas(tables, num_chromosomes=1): +def set_metadata_schemas(tables, num_chromosomes=1, num_traits=1): tables.edges.metadata_schema = slim_metadata_schemas["edge"] tables.sites.metadata_schema = slim_metadata_schemas["site"] tables.mutations.metadata_schema = slim_metadata_schemas["mutation"] tables.nodes.metadata_schema = slim_node_metadata_schema(num_chromosomes) - tables.individuals.metadata_schema = slim_metadata_schemas["individual"] + tables.individuals.metadata_schema = slim_individual_metadata_schema(num_traits) tables.populations.metadata_schema = slim_metadata_schemas["population"] @@ -683,6 +974,161 @@ 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 == "0.9": + pre_1_0_tree_sequence = { + "$schema": "http://json-schema.org/schema#", + "codec": "json", + "examples": [ + { + "SLiM": { + "file_version": "0.9", + "name": "fox", + "description": "foxes on Catalina island", + "cycle": 123, + "tick": 123, + "model_type": "WF", + "this_chromosome": { + "id": 1, + "index": 0, + "symbol": "1", + "name": "autosome_1", + "type": "A", + }, + "chromosomes": [ + {"id": 1, "symbol": "1", "name": "autosome_1", "type": "A"}, + {"id": 35, "symbol": "MT", "name": "mtDNA", "type": "HF"}, + ], + "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.9", + "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", + }, + "this_chromosome": { + "description": "The chromosome represented by the tree sequence in this file.", + "properties": { + "id": { + "description": "An integer identifier for the chromosome, unique within this set of tree sequences; often the chromosome number in the organism being represented, such as 1.", + "type": "integer", + }, + "index": { + "description": "The (zero-based) index of this chromosome in the chromosomes metadata array (if present), which should match the information given here.", + "type": "integer", + }, + "symbol": { + "description": 'A short string symbol for the chromosome, unique within this set of tree sequences, such as "1" or "MT".', + "type": "string", + }, + "name": { + "description": "A user-specified name for the chromosome, such as an accession identifier.", + "type": "string", + }, + "type": { + "description": "The type of chromosome, as specified by SLiM.", + "type": "string", + }, + }, + "required": ["id", "index", "symbol", "type"], + "type": "object", + }, + "chromosomes": { + "description": "The chromosomes represented by the collection of tree sequences, of which this tree sequence is one member.", + "items": { + "properties": { + "id": { + "description": "An integer identifier for the chromosome, unique within this set of tree sequences; often the chromosome number in the organism being represented, such as 1.", + "type": "integer", + }, + "symbol": { + "description": 'A short string symbol for the chromosome, unique within this set of tree sequences, such as "1" or "MT".', + "type": "string", + }, + "name": { + "description": "A user-specified name for the chromosome, such as an accession identifier.", + "type": "string", + }, + "type": { + "description": "The type of chromosome, as specified by SLiM.", + "type": "string", + }, + }, + "required": ["id", "symbol", "type"], + "type": "object", + }, + "type": "array", + }, + "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", + "this_chromosome", + "separate_sexes", + "nucleotide_based", + ], + "type": "object", + } + }, + "required": ["SLiM"], + "type": "object", + } + + ms = pre_1_0_tree_sequence + if name == "tree_sequence" and file_version == "0.8": pre_0_9_tree_sequence = { "$schema": "http://json-schema.org/schema#", @@ -963,6 +1409,90 @@ def _old_metadata_schema(name, file_version): } ms = pre_0_7_population + if name == "individual" and file_version in ["0.7", "0.8", "0.9"]: + pre_1_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, + } + ], + "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", + } + ms = pre_1_0_individual + if name == "individual" and file_version in [ "0.1", "0.2", @@ -1019,6 +1549,87 @@ def _old_metadata_schema(name, file_version): } ms = pre_0_7_individual + if name == "mutation" and file_version in [ + "0.3", + "0.4", + "0.5", + "0.6", + "0.7", + "0.8", + "0.9", + ]: + mutation_pre_1_0 = { + "$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": { + "mutation_type": { + "binaryFormat": "i", + "description": "The index of this mutation's mutationType.", + "index": 1, + "type": "integer", + }, + "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", + }, + "selection_coeff": { + "binaryFormat": "f", + "description": "This mutation's selection coefficient.", + "index": 2, + "type": "number", + }, + "slim_time": { + "binaryFormat": "i", + "description": "The SLiM tick counter when this mutation occurred.", + "index": 4, + "type": "integer", + }, + "subpopulation": { + "binaryFormat": "i", + "description": "The ID of the subpopulation this mutation occurred in.", + "index": 3, + "type": "integer", + }, + }, + "required": [ + "mutation_type", + "selection_coeff", + "subpopulation", + "slim_time", + "nucleotide", + ], + "type": "object", + }, + "noLengthEncodingExhaustBuffer": True, + "type": "array", + } + }, + "required": ["mutation_list"], + "type": "object", + } + ms = mutation_pre_1_0 + if name == "mutation" and file_version in ["0.1", "0.2"]: mutation_pre_0_3 = { "$schema": "http://json-schema.org/schema#", @@ -1072,6 +1683,33 @@ def _old_metadata_schema(name, file_version): } ms = mutation_pre_0_3 + if name == "node" and file_version == "0.9": + node_0_9 = { + "$schema": "http://json-schema.org/schema#", + "additionalProperties": False, + "codec": "struct", + "description": "SLiM schema for node metadata.", + "examples": [{"slim_id": 123, "is_vacant": 0}], + "properties": { + "slim_id": { + "binaryFormat": "q", + "description": "The 'pedigree ID' of the haplosomes associated with this node in SLiM.", + "index": 0, + "type": "integer", + }, + "is_vacant": { + "description": "A vector of byte (uint8_t) values, with each bit representing whether the node represents a vacant position, either unused or a null haplosome (1), or a non-null haplosome (0), in the corresponding chromosome. This field encodes vacancy for all of the chromosomes in the model, not just the chromosome represented in this file (so that the node table is identical across all chromosomes for a multi-chromosome model). Each chromosome receives one bit here; there are two node table entries per individual, used for the two haplosomes of every chromosome, so only one bit is needed in each entry (making two bits total per chromosome, across the two node table entries). The least significant bit of the first byte is used first (for one haplosome of the first chromosome); the most significant bit of the last byte is used last. The number of bytes present in this field is indicated by this schema's 'binaryFormat' field, which is variable (!), and can also be deduced from the number of chromosomes in the model as given in the top-level 'chromosomes' metadata key, which should always be present if this metadata is present.", + "index": 1, + "type": "array", + "length": 1, # MAY NEED TO BE CHANGED (in SLiM code is "%d") + "items": {"type": "number", "binaryFormat": "B"}, + }, + }, + "required": ["slim_id", "is_vacant"], + "type": ["object", "null"], + } + ms = node_0_9 + if name == "node" and file_version in [ "0.1", "0.2", @@ -1119,24 +1757,66 @@ def _old_metadata_schema(name, file_version): return ms +def _make_mutation_list(mutations, file_version): + # Prior to 1.0, mutation metadata was a list of entries like: + # {'mutation_type': 1, 'selection_coeff': -0.1, + # 'subpopulation': 1, 'slim_time': 5, 'nucleotide': -1} + # with mutation id stored in the derived state. + # + # As of 1.0, this lives in the top-level ts.metadata['SLiM_mutation_list'], + # with entries like + # {'mutation_id': 87, 'mutation_type': 1, 'subpopulation': 1, 'slim_time': 5, + # 'nucleotide': -1, 'padding': None, + # 'per_trait': [{'effect_size': -0.1, 'dominance': 0.5, + # 'hemizygous_dominance': 1.0}]} + if mutations.metadata_schema == tskit.MetadataSchema(None): + mutations.metadata_schema = _old_metadata_schema("mutation", file_version) + mutation_list = [] + for mut in mutations: + for sid, md in zip(mut.derived_state.split(","), mut.metadata["mutation_list"]): + if "nucleotide" not in md: + md["nucleotide"] = -1 + md["mutation_id"] = int(sid) + md["per_trait"] = [ + { + "effect_size": md["selection_coeff"], + "dominance": 0.5, # WE DON'T KNOW THIS + "hemizygous_dominance": 1.0, # OR THIS + }, + ] + del md["selection_coeff"] + md["padding"] = None + mutation_list.append(md) + return mutation_list + + 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. + Tests whether the metadata 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. + This method may be provided either a TreeSequence or TableCollection directly, + or the metadata from one of these. The latter is useful because + accessing top-level metadata can be a costly operation. + + :param dict ts: Either the top-level metadata of a tree sequence, + or a TreeSequence or TableCollection that carries this metadata. :return bool: Whether the tree sequence is the current version. """ + if ( + isinstance(ts, tskit.TreeSequence) + or isinstance(ts, tskit.TableCollection) + or isinstance(ts, tskit.ImmutableTableCollection) + ): + ts = ts.metadata out = ( - isinstance(ts.metadata, dict) - and ("SLiM" in ts.metadata) - and (ts.metadata["SLiM"]["file_version"] == slim_file_version) + isinstance(ts, dict) + and ("SLiM" in ts) + and (ts["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. " + "This tree sequence is not the current SLiM format. " "Use `pyslim.update( )` to update the tree sequence." ) return out @@ -1161,10 +1841,13 @@ def update_tables(tables): """ # 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): + md = tables.metadata + if not (isinstance(md, dict) and "SLiM" in md): # 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. + # Recovering from provenance has also been useful for operations + # that discard metadata (eg as msprime did prior to 0.7.5). values = default_slim_metadata("tree_sequence")["SLiM"] prov = None file_version = "unknown" @@ -1192,25 +1875,38 @@ def update_tables(tables): values[k] = record["slim"][k] except: raise ValueError("Failed to obtain metadata from provenance.") - set_tree_sequence_metadata(tables, **values) + md = set_tree_sequence_metadata(tables, **values) - file_version = tables.metadata["SLiM"]["file_version"] + file_version = md["SLiM"]["file_version"] if file_version != slim_file_version: warnings.warn( - "This is a version {} SLiM tree sequence.".format(file_version) - + " If you write this out to a file, " - + "it will be converted to version {}.".format(slim_file_version) + f"This is a version {file_version} SLiM tree sequence. " + "If you write this out to a file, " + f"it will be converted to version {slim_file_version}." ) - # the only tables to have metadata schema changed thus far - # are nodes, 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"] + assert ( + "struct" not in old_schema.schema + or "SLiM_mutation_list" not in old_schema.schema["struct"]["properties"] + ) + # we should get the number of traits from the metadata, + # but for old file versions, this won't be present + assert ( + "json" not in old_schema.schema + or "traits" not in old_schema.schema["json"]["properties"] + ) + md["SLiM_mutation_list"] = _make_mutation_list( + tables.mutations, file_version + ) + num_traits = 1 + new_schema = slim_tree_sequence_metadata_schema(num_traits=num_traits) + new_properties = new_schema.asdict()["json"]["properties"]["SLiM"][ + "required" + ] tables.metadata_schema = new_schema - defaults = default_slim_metadata("tree_sequence") + defaults = default_slim_metadata("tree_sequence", num_traits=num_traits) for k in new_properties: if k not in md["SLiM"]: if k == "tick": @@ -1226,63 +1922,71 @@ def update_tables(tables): tables.nodes.clear() if nodes.metadata_schema == tskit.MetadataSchema(None): nodes.metadata_schema = old_schema - assert "chromosomes" not in tables.metadata - # if chromosomes was in metadata - # we should use its length to get num_chroms, - # but old file versions did not have this. - num_chroms = 1 + if "chromosomes" not in md["SLiM"]: + num_chroms = 1 + else: + num_chroms = len(md["SLiM"]["chromosomes"]) new_schema = slim_node_metadata_schema(num_chroms) tables.nodes.metadata_schema = new_schema - not_vacant = [0] # single chromosome - yes_vacant = [1] - gt = None - for n in nodes: - md = n.metadata - if len(md) > 0: - md["is_vacant"] = yes_vacant if md["is_null"] else not_vacant - if not md["is_null"]: - if gt is None: - gt = md["genome_type"] - else: - assert md["genome_type"] == gt, ( - "Inconsistent tables: " - f"mismatching genome types {gt} and " - f"{md['genome_type']} in node metadata." - ) - del md["is_null"] - del md["genome_type"] - tables.nodes.append(n.replace(metadata=md)) - # flags for node genome type pre-0.9: - # confusingly and sub-optimally, these were redundant: - # all non-null nodes in the same sim would have the same genome type - assert gt is not None - GENOME_TYPE_AUTOSOME = 0 - GENOME_TYPE_X = 1 - GENOME_TYPE_Y = 2 - top_md = tables.metadata - i = top_md["SLiM"]["this_chromosome"]["index"] - # 'chromosomes' is not required so won't be inserted, - # so we won't update it also - assert "chromosomes" not in top_md["SLiM"]["this_chromosome"], ( - "" - "This is an unexpected result: if you hit this, " - "please file a bug at " - "https://github.com/tskit-dev/pyslim." - ) - if gt == GENOME_TYPE_X: - top_md["SLiM"]["this_chromosome"]["type"] = "X" - top_md["SLiM"]["this_chromosome"]["symbol"] = "X" - # if "chromosomes" in tables.metadata['SLiM']: - # top_md['SLiM']['chromosomes'][i]['type'] = "X" - # top_md['SLiM']['chromosomes'][i]['symbol'] = "X" - elif gt == GENOME_TYPE_Y: - top_md["SLiM"]["this_chromosome"]["type"] = "-Y" - top_md["SLiM"]["this_chromosome"]["symbol"] = "Y" - # top_md['SLiM']['chromosomes'][i]['type'] = "Y" - # top_md['SLiM']['chromosomes'][i]['symbol'] = "Y" + new_node_schema = new_schema + if file_version in ("0.1", "0.2", "0.3", "0.4", "0.5", "0.6", "0.7", "0.8"): + # 0.8->0.9 switched from is_null to is_vacant, + # and moved chromosome type from node metadata to top-level + not_vacant = [0] # single chromosome + yes_vacant = [1] + gt = None + for n in nodes: + md = n.metadata + if len(md) > 0: + md["is_vacant"] = yes_vacant if md["is_null"] else not_vacant + if not md["is_null"]: + if gt is None: + gt = md["genome_type"] + else: + assert md["genome_type"] == gt, ( + "Inconsistent tables: " + f"mismatching genome types {gt} and " + f"{md['genome_type']} in node metadata." + ) + del md["is_null"] + del md["genome_type"] + tables.nodes.append(n.replace(metadata=md)) + # flags for node genome type pre-0.9: + # confusingly and sub-optimally, these were redundant: + # all non-null nodes in the same sim would have the same genome type + GENOME_TYPE_AUTOSOME = 0 + GENOME_TYPE_X = 1 + GENOME_TYPE_Y = 2 + top_md = tables.metadata + i = top_md["SLiM"]["this_chromosome"]["index"] + # 'chromosomes' is not required so won't be inserted, + # so we won't update it also + assert "chromosomes" not in top_md["SLiM"]["this_chromosome"], ( + "" + "This is an unexpected result: if you hit this, " + "please file a bug at " + "https://github.com/tskit-dev/pyslim." + ) + if gt == GENOME_TYPE_X: + top_md["SLiM"]["this_chromosome"]["type"] = "X" + top_md["SLiM"]["this_chromosome"]["symbol"] = "X" + # if "chromosomes" in tables.metadata['SLiM']: + # top_md['SLiM']['chromosomes'][i]['type'] = "X" + # top_md['SLiM']['chromosomes'][i]['symbol'] = "X" + elif gt == GENOME_TYPE_Y: + top_md["SLiM"]["this_chromosome"]["type"] = "-Y" + top_md["SLiM"]["this_chromosome"]["symbol"] = "Y" + # top_md['SLiM']['chromosomes'][i]['type'] = "Y" + # top_md['SLiM']['chromosomes'][i]['symbol'] = "Y" + else: + assert gt == GENOME_TYPE_AUTOSOME + tables.metadata = top_md else: - assert gt == GENOME_TYPE_AUTOSOME - tables.metadata = top_md + assert file_version == "0.9" + # just needs recoding (and doesn't really need that, we just + # changed the index of some entries in the schema) + for n in nodes: + tables.nodes.append(n) old_schema = _old_metadata_schema("population", file_version) if old_schema is not None: @@ -1302,15 +2006,33 @@ def update_tables(tables): tables.individuals.clear() if inds.metadata_schema == tskit.MetadataSchema(None): inds.metadata_schema = old_schema - new_schema = slim_metadata_schemas["individual"] + num_traits = len(tables.metadata["SLiM"]["traits"]) + new_schema = slim_individual_metadata_schema(num_traits=num_traits) tables.individuals.metadata_schema = new_schema - defaults = default_slim_metadata("individual") - d = {} - for k in ["pedigree_p1", "pedigree_p2"]: - d[k] = defaults[k] + # new(er) additions are pedigree_pX in 0.7 + # and per_trait in 1.0 + defaults = default_slim_metadata("individual", num_traits=num_traits) for ind in inds: md = ind.metadata - md.update(d) + for k in [ + "pedigree_p1", + "pedigree_p2", + "tag", + "tagF", + "tagL0", + "tagL0_set", + "tagL1", + "tagL1_set", + "tagL2", + "tagL2_set", + "tagL3", + "tagL3_set", + "tagL4", + "tagL4_set", + ]: + md.setdefault(k, defaults[k]) + if "per_trait" not in md: + md["per_trait"] = copy.deepcopy(defaults["per_trait"]) tables.individuals.append(ind.replace(metadata=md)) old_schema = _old_metadata_schema("mutation", file_version) @@ -1321,10 +2043,8 @@ def update_tables(tables): 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)) + # drop metadata: it should have been copied into top-level above + tables.mutations.append(mut.replace(metadata=None)) if file_version == "0.1": # shift times @@ -1364,7 +2084,6 @@ def update_tables(tables): 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 95de0333..f6bc9c0a 100644 --- a/pyslim/slim_tree_sequence.py +++ b/pyslim/slim_tree_sequence.py @@ -10,6 +10,45 @@ def load(*args, **kwargs): raise RuntimeError("This method has been removed: use tskit.load( ) instead.") +def mutation_metadata(ts, check=True, _ts_metadata=None): + """ + Returns a dictionary whose keys are the numeric SLiM IDs of mutations, + and whose values are metadata entries for those mutations. + *Note:* this is indexed by integers, not strings, so if you obtain SLiM IDs + from something like ``mut.derived_state.split(",")``, you must convert + the result to integers before looking up metadata! + + This is a simple extraction function that places the list of metadata entries + stored in ``ts.metadata["SLiM_mutation_list"]`` in a dictionary + indexed by SLiM ID. It is recommended to extract this information once + and use the result in script, because calling this function many times + (or, even just referring to ``ts.metadata`` many times) + can slow down scripts considerably. + + :param tskit.TreeSequence ts: The tree sequence. + + :returns dict: A dictionary of metadata entries, indexed by SLiM ID + and in sorted order by SLiM ID. + """ + if _ts_metadata is None: + _ts_metadata = ts.metadata + # Note that dictionaries preserve insertion order + ml = _ts_metadata["SLiM_mutation_list"] + ml.sort(key=lambda x: x["mutation_id"]) + out = {mut["mutation_id"]: mut for mut in ml} + if check: + ids = {int(j) for x in ts.mutations_derived_state for j in x.split(",")} + for k in ids: + if k not in out: + raise ValueError( + "Top-level mutation metadata is missing " + f"information for mutation ID {k}: " + "do you need to run " + "pyslim.add_mutation_metadata(ts)?" + ) + return out + + def mutation_at(ts, node, position, time=None): """ Finds the mutation present in the genome of ``node`` at ``position``, @@ -59,7 +98,7 @@ def mutation_at(ts, node, position, time=None): return out -def nucleotide_at(ts, node, position, time=None): +def nucleotide_at(ts, node, position, time=None, mut_metadata=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 @@ -69,20 +108,34 @@ def nucleotide_at(ts, node, position, time=None): at ``position`` inherited by ``node`` that occurred at or before ``time`` ago. + This method uses a dictionary of mutation metadata, computed by + :meth:`mut_metadata`. This step can be expensive if there are + many mutations, so this can be pre-computed and passed in as + ``mutations``. If not provided, it will be computed. + :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. + :param dict mut_metadata: If provided, a dictionary mapping + mutation ID to metadata, as returned by ``pyslim.mutation_metadata(ts)``. :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.") + if mut_metadata is None: + mut_metadata = mutation_metadata(ts) 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"] + _, k = max( + [ + (mut_metadata[int(j)]["slim_time"], int(j)) + for j in mut.derived_state.split(",") + ] + ) + out = mut_metadata[k]["nucleotide"] return out diff --git a/tests/__init__.py b/tests/__init__.py index 0996d1a3..376b6ce8 100644 --- a/tests/__init__.py +++ b/tests/__init__.py @@ -20,6 +20,25 @@ class PyslimTestCase: Base class for test cases in pyslim. """ + def assert_indiv_metadata_equal(self, a, b): + # individual metadata can have nan's in it, thus this function + assert isinstance(a, dict) + assert type(a) == type(b) + assert a.keys() == b.keys() + for k in a: + if k != "per_trait": + assert a[k] == b[k] + apt = a["per_trait"] + bpt = b["per_trait"] + assert len(apt) == len(bpt) + for x, y in zip(apt, bpt): + assert x.keys() == y.keys() + for k in x: + if k == "phenotype": + assert (np.isnan(x[k]) and np.isnan(y[k])) or (x[k] == y[k]) + else: + assert x[k] == y[k] + def verify_haplotype_equality(self, ts, slim_ts): assert ts.num_sites == slim_ts.num_sites for j, v1, v2 in zip(range(ts.num_sites), ts.variants(), slim_ts.variants()): @@ -53,9 +72,8 @@ def assertMetadataEqual(self, t1, t2): assert t1.metadata_schema == t2.metadata_schema assert t1.metadata == t2.metadata # and now check the underlying bytes - # TODO: use the public interface if https://github.com/tskit-dev/tskit/issues/832 happens - md1 = t1._ll_tables.metadata - md2 = t2._ll_tables.metadata + md1 = t1.metadata_bytes + md2 = t2.metadata_bytes assert md1 == md2 def verify_trees_equal(self, ts1, ts2): @@ -76,10 +94,6 @@ def verify_trees_equal(self, ts1, ts2): if n.metadata is not None: map2[n.metadata["slim_id"]] = j assert set(map1.keys()) == set(map2.keys()) - print(ts1) - print(map1) - print(ts2) - print(map2) sids = list(map1.keys()) for sid in sids: n1 = ts1.node(map1[sid]) @@ -88,22 +102,13 @@ def verify_trees_equal(self, ts1, ts2): assert n1.metadata == n2.metadata i1 = ts1.individual(n1.individual) i2 = ts2.individual(n2.individual) - if i1.metadata != i2.metadata: - print("i1: ", i1.metadata) - print("i2: ", i2.metadata) - assert i1.metadata == i2.metadata + self.assert_indiv_metadata_equal(i1.metadata, i2.metadata) for _ in range(10): pos = random.uniform(0, ts1.sequence_length) t1 = ts1.at(pos) t2 = ts2.at(pos) for _ in range(10): a, b = random.choices(sids, k=2) - print(a, b, map1[a], map1[b], map2[a], map2[b]) - print(t1) - print("a", t1.time(map1[a])) - print("b", t1.time(map1[b])) - print("1", t1.tmrca(map1[a], map1[b])) - print("2", t2.tmrca(map2[a], map2[b])) assert t1.tmrca(map1[a], map1[b]) == t2.tmrca(map2[a], map2[b]) def assertTableCollectionsEqual( diff --git a/tests/conftest.py b/tests/conftest.py index 0a3fa875..6895b5ea 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -7,6 +7,7 @@ import pytest import tskit from filelock import FileLock +from frozendict import frozendict from .recipe_specs import recipe_specs @@ -73,7 +74,7 @@ def load_ts(self, path): c, e = os.path.splitext(cfile) if e == ".trees": out[c] = tskit.load(os.path.join(path, cfile)) - return out + return frozendict(out) def __init__(self, out_dir): # Note: the 'key' below cannot match a 'key' in recipe_specs diff --git a/tests/recipe_specs.py b/tests/recipe_specs.py index 283daff1..d31c2f69 100644 --- a/tests/recipe_specs.py +++ b/tests/recipe_specs.py @@ -12,11 +12,16 @@ # retained: has retained individuals # multipop: has more than one population # multichrom: has more than one chromosome +# traits: has more than just the usual trait +# no_simplify: does not run simplify when writing out +# old_mutations: uses addMutation to add back in some previously lost mutations +# record_mutations: whether mutations and substitutions and reference sequence +# are in top-level metadata # long: kinda big # (chromosome type) # All files are of the form `tests/test_recipes/{key}` recipe_specs = { - "recipe_nonWF.slim": {"nonWF": True, "pedigree": True}, + "recipe_nonWF.slim": {"nonWF": True, "pedigree": True, "record_mutations": True}, "recipe_nonWF_X.slim": {"nonWF": True, "pedigree": True, "X": True}, "recipe_nonWF_Y.slim": {"nonWF": True, "pedigree": True, "Y": True}, "recipe_nonWF_H.slim": {"nonWF": True, "pedigree": True, "H": True}, @@ -78,14 +83,23 @@ }, "recipe_long_nonWF.slim": {"nonWF": True, "long": True}, "recipe_old_nonWF.slim": {"nonWF": True, "remembered_first": True}, - "recipe_WF.slim": {"WF": True, "pedigree": True}, + "recipe_WF.slim": {"WF": True, "pedigree": True, "record_mutations": True}, + "recipe_no_simplify.slim": {"WF": True, "no_simplify": True}, "recipe_long_WF.slim": {"WF": True, "long": True}, "recipe_WF_migration.slim": {"WF": True, "pedigree": True, "multipop": True}, - "recipe_nucleotides_WF.slim": {"WF": True, "pedigree": True, "nucleotides": True}, + "recipe_nucleotides_WF.slim": { + "WF": True, + "pedigree": True, + "nucleotides": True, + "record_mutations": True, + "refseq": True, + }, "recipe_nucleotides_nonWF.slim": { "nonWF": True, "pedigree": True, "nucleotides": True, + "record_mutations": True, + "refseq": True, }, "recipe_nucleotides_plus_others.slim": { "WF": True, @@ -177,7 +191,10 @@ "adds_mutations": True, "nucleotides": True, "non-nucleotides": True, + "record_mutations": True, + "refseq": True, }, + "recipe_adds_old_muts.slim": {"WF": True, "old_mutations": True}, "recipe_many_chromosomes.slim": { "nonWF": True, "pedigree": True, @@ -189,6 +206,15 @@ "multichrom": True, "H-": True, }, + "recipe_with_traits.slim": { + "WF": True, + "traits": True, + "multichrom": True, + "begun_late": True, + "X": True, + "Y": True, + "H": True, + }, } for x in ("first", "early", "late"): diff --git a/tests/test_annotation.py b/tests/test_annotation.py index 6336bc44..ead0a046 100644 --- a/tests/test_annotation.py +++ b/tests/test_annotation.py @@ -15,17 +15,33 @@ import pyslim import tests -from .recipe_specs import restarted_recipe_eq +from .recipe_specs import recipe_eq, restarted_recipe_eq def mutcontext(ts): - if ts.num_mutations > 0: + md = ts.metadata + if ( + isinstance(md, dict) + and "SLiM_mutation_list" in md + and len(md["SLiM_mutation_list"]) > 0 + ): handler = pytest.warns(Warning, match="already has.*metadata") else: handler = contextlib.nullcontext() return handler +def canonicalise_tables(tables): + md = tables.metadata + num_muts = len(md["SLiM_mutation_list"]) + md["SLiM_mutation_list"] = list( + pyslim.mutation_metadata(tables, check=False).values() + ) + assert num_muts == len(md["SLiM_mutation_list"]) + tables.metadata = md + tables.canonicalise() + + def verify_slim_restart_equality(in_ts_dict, out_ts_dict, check_prov=True): """ Check for equality, in everything but the last provenance. @@ -37,9 +53,9 @@ def verify_slim_restart_equality(in_ts_dict, out_ts_dict, check_prov=True): if check_prov: assert in_ts.num_provenances + 1 == out_ts.num_provenances in_tables = in_ts.dump_tables() - in_tables.canonicalise() + canonicalise_tables(in_tables) out_tables = out_ts.dump_tables() - out_tables.canonicalise() + canonicalise_tables(out_tables) in_tables.assert_equals(out_tables, ignore_provenance=True) @@ -103,13 +119,14 @@ 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) - for mdl in md["mutation_list"]: - assert mdl["mutation_type"] == 0 - assert mdl["selection_coeff"] == 0.0 - assert mdl["subpopulation"] == tskit.NULL + for mdl in ts.metadata["SLiM_mutation_list"]: + assert mdl["mutation_type"] == 0 + assert mdl["per_trait"] == [ + {"effect_size": 0.0, "dominance": 0.5, "hemizygous_dominance": 1.0} + ] + assert mdl["subpopulation"] == tskit.NULL + # assert mdl["slim_time"] == 0 # this is set to something meaningful + assert mdl["nucleotide"] == -1 for n in ts.nodes(): md = n.metadata if not n.is_sample(): @@ -220,17 +237,17 @@ def verify_remapping(self, ts, rts, subpop_map): assert m == rm assert ts.num_mutations == rts.num_mutations + mut_info = pyslim.mutation_metadata(ts) + rmut_info = pyslim.mutation_metadata(rts) + assert len(mut_info) == len(rmut_info) # mutations may have changed order - tsm = {m.derived_state: m for m in ts.mutations()} - rtsm = {m.derived_state: m for m in rts.mutations()} + tsm = {m.derived_state for m in ts.mutations()} + rtsm = {m.derived_state for m in rts.mutations()} for x in tsm: assert x in rtsm - m = tsm[x] - rm = rtsm[x] - 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"]): + for sid in x.split(","): + x = mut_info[int(sid)].copy() + y = rmut_info[int(sid)] x["subpopulation"] = fwd_map[x["subpopulation"]] assert x == y @@ -239,7 +256,7 @@ def verify_remapping(self, ts, rts, subpop_map): md = i.metadata rmd = ri.metadata md["subpopulation"] = fwd_map[md["subpopulation"]] - assert md == rmd + self.assert_indiv_metadata_equal(md, rmd) def test_annotate_errors(self, helper_functions): for ts in helper_functions.get_msprime_examples(): @@ -279,6 +296,11 @@ def test_warns_overwriting_mutations(self, helper_functions): ts, rate=1, random_seed=12, model=msprime.SLiMMutationModel(type=1) ) assert ts.num_mutations > 0 + t = ts.dump_tables() + t.metadata_schema = pyslim.slim_metadata_schemas["tree_sequence"] + t.metadata = pyslim.default_slim_metadata("tree_sequence") + ts = t.tree_sequence() + ts = pyslim.add_mutation_metadata(ts) with pytest.warns(Warning, match="already has.*metadata"): slim_ts = pyslim.annotate(ts, model_type="WF", tick=1) @@ -335,45 +357,35 @@ def test_just_simulate(self, helper_functions, tmp_path): def test_basic_annotation(self, helper_functions, tmp_path): for ts in helper_functions.get_msprime_examples(): - for do_mutations in [False, True]: - tick = 4 - cycle = 1 - stage = "late" - if do_mutations: - handler = mutcontext(ts) - else: - handler = contextlib.nullcontext() - with handler: - slim_ts = pyslim.annotate( - ts, - model_type="WF", - tick=tick, - cycle=cycle, - stage=stage, - annotate_mutations=do_mutations, - ) - assert slim_ts.metadata["SLiM"]["model_type"] == "WF" - 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) + tick = 4 + cycle = 1 + stage = "late" + with mutcontext(ts): + slim_ts = pyslim.annotate( + ts, + model_type="WF", + tick=tick, + cycle=cycle, + stage=stage, + annotate_mutations=True, ) - self.verify_annotated_trees(ts, slim_ts) - if not do_mutations: - self.verify_haplotype_equality(ts, slim_ts) - self.verify_defaults(slim_ts) - self.verify_provenance(slim_ts) - # try loading this into SLiM - loaded_ts = helper_functions.run_msprime_restart( - {"default": slim_ts}, tmp_path, multichrom=False, WF=True - )["default"] - self.verify_annotated_tables(loaded_ts, slim_ts) - self.verify_annotated_trees(loaded_ts, slim_ts) - self.verify_haplotype_equality(loaded_ts, slim_ts) + slim_ts_md = slim_ts.metadata["SLiM"] + assert slim_ts_md["model_type"] == "WF" + assert slim_ts_md["tick"] == tick + assert slim_ts_md["cycle"] == cycle + assert slim_ts_md["stage"] == stage + assert slim_ts_md["file_version"] == pyslim.slim_file_version + self.verify_annotated_tables(ts, slim_ts, check_alleles=False) + self.verify_annotated_trees(ts, slim_ts) + self.verify_defaults(slim_ts) + self.verify_provenance(slim_ts) + # try loading this into SLiM + loaded_ts = helper_functions.run_msprime_restart( + {"default": slim_ts}, tmp_path, multichrom=False, WF=True + )["default"] + self.verify_annotated_tables(loaded_ts, slim_ts) + self.verify_annotated_trees(loaded_ts, slim_ts) + self.verify_haplotype_equality(loaded_ts, slim_ts) def test_annotate_refseq(self): ts = msprime.sim_ancestry(2, sequence_length=10, random_seed=77) @@ -447,22 +459,22 @@ def test_annotate_nodes(self, helper_functions): # not testing SLiM because needs annotation of indivs to make sense def test_annotate_mutations(self, helper_functions): + # test workflow of annotating and editing + rng = np.random.default_rng(seed=123) for ts in helper_functions.get_msprime_examples(): with mutcontext(ts): 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] + md = tables.metadata + metadata = md["SLiM_mutation_list"] + # these are stored as "f" meaning "float" (not "double") + selcoefs = rng.uniform(size=len(metadata)).astype("float32") for j in range(len(metadata)): - 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] - ) + metadata[j]["per_trait"][0]["effect_size"] = selcoefs[j] + tables.metadata = md 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]) + for j, md in enumerate(new_ts.metadata["SLiM_mutation_list"]): + assert md["per_trait"][0]["effect_size"] == selcoefs[j] def test_dont_annotate_mutations(self): # Test the option to not overwrite mutation annotations @@ -828,11 +840,14 @@ def test_remapping(self, helper_functions, tmp_path): 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), + ts = pyslim.add_mutation_metadata( + msprime.sim_mutations( + ts, + rate=1e-2, + random_seed=9, + model=msprime.SLiMMutationModel(type=1), + ), + mutation_type=1, ) assert ts.num_mutations > 0 for subpop_map in ( @@ -947,8 +962,8 @@ def test_reload_annotate(self, restart_name, recipe, helper_functions, tmp_path) in_ts = {} for chrom, ts in recipe["ts"].items(): tables = ts.dump_tables() - metadata = [m.metadata for m in tables.mutations] - has_nucleotides = tables.metadata["SLiM"]["nucleotide_based"] + metadata = tables.metadata + has_nucleotides = metadata["SLiM"]["nucleotide_based"] if has_nucleotides: nucs = [random.choice([0, 1, 2, 3]) for _ in metadata] refseq = "".join( @@ -957,17 +972,13 @@ def test_reload_annotate(self, restart_name, recipe, helper_functions, tmp_path) k=int(ts.sequence_length), ), ) - for n, md in zip(nucs, metadata): - for m in md["mutation_list"]: - m["nucleotide"] = n + for n, md in zip(nucs, metadata["SLiM_mutation_list"]): + md["nucleotide"] = n tables.reference_sequence.data = refseq - for md in metadata: - for m in md["mutation_list"]: - m["selection_coeff"] = random.random() - ms = tables.mutations.metadata_schema - tables.mutations.packset_metadata( - [ms.validate_and_encode_row(r) for r in metadata] - ) + for md in metadata["SLiM_mutation_list"]: + for x in md["per_trait"]: + x["effect_size"] = random.random() + tables.metadata = metadata in_ts[chrom] = tables.tree_sequence() # put it through SLiM (which just reads in and writes out) out_ts = helper_functions.run_slim_restart( @@ -1046,3 +1057,136 @@ def test_restarts_and_runs_simplified( it = in_ts[k] ot = out_ts[k] assert ot.metadata["SLiM"]["tick"] >= it.metadata["SLiM"]["tick"] + + +class TestAddMutationMetadata(tests.PyslimTestCase): + def test_add_mutation_metadata_errors(self): + ts = msprime.sim_ancestry(10, random_seed=5) + ts = msprime.sim_mutations( + ts, rate=5, random_seed=3, model=msprime.SLiMMutationModel(type=0) + ) + # bad metadata schema + t = ts.dump_tables() + t.metadata_schema = tskit.MetadataSchema(None) + bad_ts = t.tree_sequence() + with pytest.raises(ValueError, match="metadata schema is not"): + _ = pyslim.add_mutation_metadata(bad_ts) + t.metadata_schema = tskit.MetadataSchema.permissive_json() + t.metadata = {} + bad_ts = t.tree_sequence() + with pytest.raises(ValueError, match="metadata schema is not"): + _ = pyslim.add_mutation_metadata(bad_ts) + t.metadata["SLiM"] = pyslim.default_slim_metadata("tree_sequence")["SLiM"] + bad_ts = t.tree_sequence() + with pytest.raises(ValueError, match="metadata schema is not"): + _ = pyslim.add_mutation_metadata(bad_ts) + + def test_add_mutation_metadata_mutation_type(self): + ts = msprime.sim_ancestry(10, random_seed=5) + ts = msprime.sim_mutations( + ts, rate=5, random_seed=3, model=msprime.SLiMMutationModel(type=0) + ) + t = ts.dump_tables() + t.metadata_schema = pyslim.slim_metadata_schemas["tree_sequence"] + t.metadata = pyslim.default_slim_metadata("tree_sequence") + ts = t.tree_sequence() + for k in (0, 1, 5): + new_ts = pyslim.add_mutation_metadata(ts, mutation_type=k) + for x in new_ts.metadata["SLiM_mutation_list"]: + assert x["mutation_type"] == k + + @pytest.mark.parametrize( + "recipe", + recipe_eq( + exclude=( + "long", + "no_simplify", + "multichrom", + "everyone", + "init_mutated", + "old_mutations", + ) + ), + indirect=True, + ) + def test_add_mutation_metadata(self, recipe): + for _, ts in recipe["ts"].items(): + tables = ts.dump_tables() + md = tables.metadata + md["SLiM_mutation_list"] = [] + tables.metadata = md + new_ts = pyslim.add_mutation_metadata( + tables.tree_sequence(), + ) + old_metadata = pyslim.mutation_metadata(ts) + new_metadata = pyslim.mutation_metadata(new_ts) + assert len(old_metadata) == len(new_metadata) + for k in old_metadata: + assert 0 == new_metadata[k]["mutation_type"] # default + assert old_metadata[k]["slim_time"] == new_metadata[k]["slim_time"] + assert old_metadata[k]["mutation_id"] == new_metadata[k]["mutation_id"] + + @pytest.mark.parametrize( + "recipe", + recipe_eq( + exclude=( + "long", + "no_simplify", + "multichrom", + "everyone", + "init_mutated", + "old_mutations", + ) + ), + indirect=True, + ) + def test_add_mutation_metadata_keeps(self, recipe): + for _, ts in recipe["ts"].items(): + if ts.num_mutations > 15: + tables = ts.dump_tables() + md = tables.metadata + metadata = md["SLiM_mutation_list"] + del metadata[:2] + del metadata[10:] + kept_ids = {m["mutation_id"] for m in metadata} + md["SLiM_mutation_list"] = metadata + tables.metadata = md + new_ts = pyslim.add_mutation_metadata( + tables.tree_sequence(), + ) + old_metadata = pyslim.mutation_metadata(ts) + new_metadata = pyslim.mutation_metadata(new_ts) + assert len(old_metadata) == len(new_metadata) + for k in old_metadata: + if k in kept_ids: + assert old_metadata[k] == new_metadata[k] + else: + assert 0 == new_metadata[k]["mutation_type"] # default + assert ( + old_metadata[k]["slim_time"] == new_metadata[k]["slim_time"] + ) + assert ( + old_metadata[k]["mutation_id"] + == new_metadata[k]["mutation_id"] + ) + + def test_add_mutation_metadata_removes(self): + ts = msprime.sim_ancestry(10, random_seed=5) + ts = msprime.sim_mutations( + ts, rate=5, random_seed=3, model=msprime.SLiMMutationModel(type=0) + ) + ts = pyslim.add_mutation_metadata(pyslim.annotate(ts, model_type="WF", tick=1)) + sts = ts.simplify([0, 1]) + assert sts.num_mutations < ts.num_mutations + assert ts.metadata["SLiM_mutation_list"] == sts.metadata["SLiM_mutation_list"] + nsts = pyslim.add_mutation_metadata(sts, remove_unused=True) + mut_info = pyslim.mutation_metadata(ts) + nmut_info = pyslim.mutation_metadata(nsts) + mut_ids = np.unique( + [int(k) for mut in nsts.mutations() for k in mut.derived_state.split(",")] + ) + assert len(mut_ids) == len(nsts.metadata["SLiM_mutation_list"]) + for k in mut_ids: + assert k in nmut_info + assert k in mut_info + assert mut_info[k] == nmut_info[k] diff --git a/tests/test_metadata.py b/tests/test_metadata.py index b0a44336..e472e8ff 100644 --- a/tests/test_metadata.py +++ b/tests/test_metadata.py @@ -12,6 +12,20 @@ from .recipe_specs import recipe_eq +def assert_nan_equal(a, b): + if isinstance(a, float): + assert (a == b) or (np.isnan(a) and np.isnan(b)) + elif isinstance(a, dict) and isinstance(b, dict): + assert a.keys() == b.keys() + for k in a: + assert_nan_equal(a[k], b[k]) + elif isinstance(a, list) and isinstance(b, list): + for x, y in zip(a, b, strict=True): + assert_nan_equal(x, y) + else: + assert a == b + + class TestMetadataSchemas(tests.PyslimTestCase): def validate_table_metadata(self, table): ms = table.metadata_schema @@ -46,35 +60,47 @@ def test_default_metadata(self): schema = pyslim.slim_metadata_schemas[k] entry = pyslim.default_slim_metadata(k) sd = schema.asdict() - if sd is not None: - for p in sd["properties"]: - assert p in entry - encoded = schema.validate_and_encode_row(entry) - decoded = schema.decode_row(encoded) - if entry is None: - assert decoded is None + if k != "tree_sequence": + if sd is not None: + for p in sd["properties"]: + assert p in entry + encoded = schema.validate_and_encode_row(entry) + decoded = schema.decode_row(encoded) + if entry is None: + assert decoded is None + else: + # some defaults have nans, which are not equal + assert_nan_equal(entry, decoded) else: + assert k == "tree_sequence" + for p in sd["json"]["properties"]: + assert p in entry + encoded = schema.validate_and_encode_row(entry) + decoded = schema.decode_row(encoded) + assert entry == decoded + entry["SLiM_mutation_list"].append( + pyslim.default_slim_metadata("mutation_list_entry") + ) + encoded = schema.validate_and_encode_row(entry) + decoded = schema.decode_row(encoded) + assert entry == decoded + entry["SLiM_mutation_list"].append( + pyslim.default_slim_metadata("mutation_list_entry") + ) + encoded = schema.validate_and_encode_row(entry) + decoded = schema.decode_row(encoded) assert entry == decoded - schema = pyslim.slim_metadata_schemas["mutation"] - entry = pyslim.default_slim_metadata("mutation") - entry["mutation_list"].append( - pyslim.default_slim_metadata("mutation_list_entry") - ) - encoded = schema.validate_and_encode_row(entry) - decoded = schema.decode_row(encoded) - assert entry == decoded - entry["mutation_list"].append( - pyslim.default_slim_metadata("mutation_list_entry") - ) - encoded = schema.validate_and_encode_row(entry) - decoded = schema.decode_row(encoded) - assert entry == decoded def test_slim_metadata_schema_equality(self, recipe): num_chromosomes = len(recipe["ts"]) for ts in recipe["ts"].values(): t = ts.dump_tables() - assert t.metadata_schema == pyslim.slim_metadata_schemas["tree_sequence"] + num_traits = len(t.metadata["SLiM"]["traits"]) + ts_schema = pyslim.slim_metadata_schemas["tree_sequence"].asdict() + ts_schema["struct"]["properties"]["SLiM_mutation_list"]["items"][ + "properties" + ]["per_trait"]["length"] = num_traits + assert t.metadata_schema.asdict() == ts_schema assert t.edges.metadata_schema == pyslim.slim_metadata_schemas["edge"] assert t.sites.metadata_schema == pyslim.slim_metadata_schemas["site"] assert ( @@ -85,10 +111,9 @@ def test_slim_metadata_schema_equality(self, recipe): (num_chromosomes + 7) / 8 ) assert t.nodes.metadata_schema.asdict() == node_schema - assert ( - t.individuals.metadata_schema - == pyslim.slim_metadata_schemas["individual"] - ) + ind_schema = pyslim.slim_metadata_schemas["individual"].asdict() + ind_schema["properties"]["per_trait"]["length"] = num_traits + assert t.individuals.metadata_schema.asdict() == ind_schema assert ( t.populations.metadata_schema == pyslim.slim_metadata_schemas["population"] @@ -117,18 +142,25 @@ class TestTreeSequenceMetadata(tests.PyslimTestCase): def validate_slim_metadata(self, t): # t could be tables or a tree sequence schema = t.metadata_schema.schema - assert "SLiM" in schema["properties"] - assert "SLiM" in t.metadata + assert schema["codec"] == "json+struct" + assert "SLiM" in schema["json"]["properties"] + tmd = t.metadata + assert "SLiM" in tmd for k in pyslim.default_slim_metadata("tree_sequence")["SLiM"]: - assert k in schema["properties"]["SLiM"]["properties"] - assert k in t.metadata["SLiM"] + assert k in schema["json"]["properties"]["SLiM"]["properties"] + assert k in tmd["SLiM"] + sml = schema["struct"]["properties"] + assert "SLiM_mutation_list" in sml + for k in pyslim.default_slim_metadata("mutation_list_entry"): + assert k in sml["SLiM_mutation_list"]["items"]["properties"] def validate_model_type(self, tsdict, model_type): for _, ts in tsdict.items(): - assert ts.metadata["SLiM"]["file_version"] == pyslim.slim_file_version - assert ts.metadata["SLiM"]["model_type"] == model_type - assert ts.metadata["SLiM"]["tick"] > 0 - assert ts.metadata["SLiM"]["tick"] >= np.max(ts.tables.nodes.time) + md = ts.metadata + assert md["SLiM"]["file_version"] == pyslim.slim_file_version + assert md["SLiM"]["model_type"] == model_type + assert md["SLiM"]["tick"] > 0 + assert md["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): @@ -156,19 +188,59 @@ def test_set_tree_sequence_metadata_keeps(self, recipe): tables.metadata = dummy_metadata pyslim.set_tree_sequence_metadata(tables, "nonWF", 0) schema = tables.metadata_schema.schema + tmd = tables.metadata for k in dummy_metadata: if len(x) > 0: - assert k in schema["properties"] - assert k in tables.metadata - assert tables.metadata[k] == dummy_metadata[k] + assert k in schema["json"]["properties"] + assert k in tmd + assert tmd[k] == dummy_metadata[k] self.validate_slim_metadata(tables) - assert tables.metadata["SLiM"]["model_type"] == "nonWF" - assert tables.metadata["SLiM"]["tick"] == 0 + assert tmd["SLiM"]["model_type"] == "nonWF" + assert tmd["SLiM"]["tick"] == 0 + + @pytest.mark.parametrize("recipe", arbitrary_recipe, indirect=True) + def test_set_tree_sequence_metadata_keeps_struct(self, recipe): + # make sure doesn't overwrite other stuff + ts = list(recipe["ts"].values())[0] + json_props = {"num": {"type": "number"}} + struct_props = {"binnum": {"type": "integer", "binaryFormat": "i", "default": 0}} + schema_dict = { + "codec": "json+struct", + "type": "object", + "json": {"codec": "json", "type": "object", "properties": json_props}, + "struct": {"struct": "json", "type": "object", "properties": struct_props}, + } + dummy_schema = tskit.MetadataSchema(schema_dict) + dummy_metadata = {"num": 12, "binnum": 42} + tables = ts.dump_tables() + tables.metadata_schema = dummy_schema + tables.metadata = dummy_metadata + pyslim.set_tree_sequence_metadata(tables, "nonWF", 0) + schema = tables.metadata_schema.schema + tmd = tables.metadata + for k in dummy_metadata: + if k in json_props: + assert schema["json"]["properties"][k] == json_props[k] + else: + assert schema["struct"]["properties"][k] == struct_props[k] + assert k in tmd + assert tmd[k] == dummy_metadata[k] + self.validate_slim_metadata(tables) + assert tmd["SLiM"]["model_type"] == "nonWF" + assert tmd["SLiM"]["tick"] == 0 @pytest.mark.parametrize("recipe", arbitrary_recipe, indirect=True) def test_set_tree_sequence_metadata(self, recipe): ts = list(recipe["ts"].values())[0] tables = ts.dump_tables() + chroms = [ + {"id": 1, "name": "autosome_1", "symbol": "1", "type": "A", "index": 0}, + {"id": 35, "name": "mtDNA", "symbol": "MT", "type": "HF", "index": 1}, + ] + traits = [ + {"index": 0, "name": "theTrait", "type": "additive"}, + {"index": 1, "name": "perfectness", "type": "multiplicative"}, + ] pyslim.set_tree_sequence_metadata( tables, "WF", @@ -179,16 +251,23 @@ def test_set_tree_sequence_metadata(self, recipe): spatial_periodicity="y", separate_sexes=False, nucleotide_based=True, + this_chromosome=chroms[1], + chromosomes=chroms, + traits=traits, ) self.validate_slim_metadata(tables) - assert tables.metadata["SLiM"]["model_type"] == "WF" - 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 + tmd = tables.metadata + assert tmd["SLiM"]["model_type"] == "WF" + assert tmd["SLiM"]["tick"] == 99 + assert tmd["SLiM"]["cycle"] == 40 + assert tmd["SLiM"]["stage"] == "early" + assert tmd["SLiM"]["spatial_dimensionality"] == "xy" + assert tmd["SLiM"]["spatial_periodicity"] == "y" + assert tmd["SLiM"]["separate_sexes"] == False + assert tmd["SLiM"]["nucleotide_based"] == True + assert tmd["SLiM"]["chromosomes"] == chroms + assert tmd["SLiM"]["this_chromosome"] == chroms[1] + assert tmd["SLiM"]["traits"] == traits @pytest.mark.parametrize("recipe", recipe_eq("WF"), indirect=True) def test_WF_model_type(self, recipe): @@ -199,12 +278,12 @@ def test_nonWF_model_type(self, recipe): self.validate_model_type(recipe["ts"], "nonWF") @pytest.mark.parametrize( - "recipe", recipe_eq(exclude=["user_metadata", "multichrom"]), indirect=True + "recipe", + recipe_eq(exclude=["user_metadata", "multichrom", "record_mutations"]), + indirect=True, ) def test_recover_metadata(self, recipe): # msprime <=0.7.5 discards metadata, but we can recover it from provenance - # HOWEVER: multichromosome information is not saved - # but this is not something we need to maintain any more. for _, ts in recipe["ts"].items(): tables = ts.dump_tables() tables.metadata_schema = tskit.MetadataSchema(None) @@ -212,13 +291,12 @@ def test_recover_metadata(self, recipe): pyslim.update_tables(tables) md = tables.metadata assert "SLiM" in md - for k in ts.metadata["SLiM"]: - if k in ("chromosomes", "this_chromosome"): - continue # TODO: see https://github.com/MesserLab/SLiM/issues/520 + tsmd = ts.metadata["SLiM"] + for k in tsmd: 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] + if k != "description" or tsmd[k] != "": + assert tsmd[k] == md["SLiM"][k] @pytest.mark.parametrize( "recipe", recipe_eq("recipe_with_metadata.slim"), indirect=True @@ -267,10 +345,9 @@ def test_nucleotides(self, recipe): -1, 0, 1, 2, or 3. """ for _, ts in recipe["ts"].items(): - for mut in ts.mutations(): - for u in mut.metadata["mutation_list"]: - assert u["nucleotide"] >= -1 - assert u["nucleotide"] <= 3 + for u in ts.metadata["SLiM_mutation_list"]: + assert u["nucleotide"] >= -1 + assert u["nucleotide"] <= 3 class TestMultichrom(tests.PyslimTestCase): diff --git a/tests/test_provenance.py b/tests/test_provenance.py index d95b275d..71568b22 100644 --- a/tests/test_provenance.py +++ b/tests/test_provenance.py @@ -122,6 +122,18 @@ old_provenance_examples = [_slim_v3_0_example, _slim_v3_1_example, _slim_v3_3_1_example] +def yield_ts(path): + out = {} + if os.path.isfile(path): + yield tskit.load(path) + elif os.path.isdir(path): + chroms = os.listdir(path) + for cfile in os.listdir(path): + _, e = os.path.splitext(cfile) + if e == ".trees": + yield tskit.load(os.path.join(path, cfile)) + + class TestProvenance(tests.PyslimTestCase): script_dir = os.path.dirname(os.path.realpath(__file__)) @@ -183,6 +195,18 @@ def get_0_8_slim_examples(self): ]: yield tskit.load(filename) + def get_0_9_slim_examples(self): + for filename in [ + os.path.join(self.script_dir, "test_recipes", "recipe_WF.v5.2.trees"), + os.path.join(self.script_dir, "test_recipes", "recipe_WF_X.v5.2.trees"), + os.path.join(self.script_dir, "test_recipes", "recipe_WF_Y.v5.2.trees"), + os.path.join(self.script_dir, "test_recipes", "recipe_nonWF.v5.2.trees"), + os.path.join( + self.script_dir, "test_recipes", "recipe_WF_many_chromosomes.v5.2.trees" + ), + ]: + yield from yield_ts(filename) + def get_mixed_slim_examples(self): for filename in [ os.path.join( @@ -209,6 +233,115 @@ def verify_upgrade(self, ts): for x in t: _ = ms.validate_and_encode_row(x.metadata) + def verify_consistency(self, ts, pts, file_version): + # Check for stuff we know should be copied over verbatim + # 0.1-0.4 we had no metadata schemas; we could pull those old ones + # from slim_metadata.py but we're not + self.verify_top_level_consistency(ts, pts, file_version) + self.verify_nodes_consistency(ts, pts, file_version) + self.verify_edges_consistency(ts, pts, file_version) + self.verify_sites_consistency(ts, pts, file_version) + self.verify_mutations_consistency(ts, pts, file_version) + self.verify_individuals_consistency(ts, pts, file_version) + self.verify_populations_consistency(ts, pts, file_version) + + def verify_top_level_consistency(self, ts, pts, file_version): + # 0.1-0.7: + # model_type, generation, spatial_dimesionality, spatial_periodicity, + # separate_sexes, nucleotide_based + # 0.8: + # changed generation to tick + # 0.9: + # added this_chromosome + # 1.0: + # added traits + if file_version not in ("0.1", "0.2", "0.3", "0.4"): + md = ts.metadata["SLiM"] + pmd = pts.metadata["SLiM"] + for k in ( + "model_type", + "spatial_dimensionality", + "spatial_periodicity", + "separate_sexes", + "nucleotide_based", + ): + assert md[k] == pmd[k] + k = pk = "tick" + if file_version in ("0.5", "0.6", "0.7"): + k = "generation" + assert md[k] == pmd[pk] + k = "this_chromosome" + if file_version == "0.9": + assert md[k] == pmd[k] + + def verify_edges_consistency(self, ts, pts, file_version): + # no metadata + ts.tables.edges.assert_equals(pts.tables.edges, ignore_metadata=True) + + def verify_sites_consistency(self, ts, pts, file_version): + # no metadata + ts.tables.sites.assert_equals(pts.tables.sites, ignore_metadata=True) + + def verify_mutations_consistency(self, ts, pts, file_version): + ts.tables.mutations.assert_equals(pts.tables.mutations, ignore_metadata=True) + # As of 1.0, metadata moved to top level + if file_version not in ("0.1", "0.2", "0.3", "0.4"): + ptsmd = pts.metadata + num_traits = len(ptsmd["SLiM"]["traits"]) + mut_info = {x["mutation_id"]: x for x in ptsmd["SLiM_mutation_list"]} + for mut in ts.mutations(): + for sid, md in zip( + mut.derived_state.split(","), mut.metadata["mutation_list"] + ): + assert int(sid) in mut_info + mi = mut_info[int(sid)] + for k in ("mutation_type", "subpopulation", "slim_time"): + assert mi[k] == md[k] + assert len(mi["per_trait"]) == 1 + if "nucleotide" in md: + assert mi["nucleotide"] == md["nucleotide"] + assert len(mi["per_trait"]) == num_traits + # we're only converting from single-trait slim so far + assert num_traits == 1 + assert mi["per_trait"][0]["effect_size"] == md["selection_coeff"] + + def verify_individuals_consistency(self, ts, pts, file_version): + ts.tables.individuals.assert_equals(pts.tables.individuals, ignore_metadata=True) + # This has: + # pedigree_id, age, subpopulation, sex, flags + # Starting in 0.7 also: + # pedigree_p1, pedigree_p2 + # Starting in 1.0: per_trait + for a, b in zip(ts.individuals(), pts.individuals()): + if file_version not in ("0.1", "0.2", "0.3", "0.4"): + for k in ("pedigree_id", "age", "subpopulation", "sex", "flags"): + assert a.metadata[k] == b.metadata[k] + if file_version not in ("0.5", "0.6"): + for k in ("pedigree_p1", "pedigree_p2"): + assert a.metadata[k] == b.metadata[k] + + def verify_nodes_consistency(self, ts, pts, file_version): + # 0.1-0.8: had slim_id, is_null, genome_type + # 0.9: removed genome_type + # and changed is_null to is_vacant + # 1.0: same as 0.9 but changed some indexes + if file_version != "0.1": + # 0.1 had a shift in time we're not checking here + ts.tables.nodes.assert_equals(pts.tables.nodes, ignore_metadata=True) + for a, b in zip(ts.nodes(), pts.nodes()): + if file_version in ("0.5", "0.6", "0.7", "0.8"): + assert a.metadata["slim_id"] == b.metadata["slim_id"] + elif file_version == "0.9": + assert a.metadata == b.metadata + + def verify_populations_consistency(self, ts, pts, file_version): + ts.tables.populations.assert_equals(pts.tables.populations, ignore_metadata=True) + # This has a whole bunch of things, none of which are required. + for a, b in zip(ts.individuals(), pts.individuals()): + if file_version not in ("0.1", "0.2", "0.3", "0.4"): + for k in a.metadata: + assert a.metadata[k] == b.metadata[k] + def test_convert_0_1_files(self): for ts in self.get_0_1_slim_examples(): assert not pyslim.is_current_version(ts) @@ -216,24 +349,16 @@ def test_convert_0_1_files(self): pts = pyslim.update(ts) assert pyslim.is_current_version(pts) self.verify_upgrade(pts) + self.verify_consistency(ts, pts, file_version="0.1") 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["model_type"] == pts.metadata["SLiM"]["model_type"] - 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()) - 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) + ptsmd = pts.metadata + assert isinstance(ptsmd, dict) + assert "SLiM" in ptsmd + assert record["model_type"] == ptsmd["SLiM"]["model_type"] + assert record["generation"] == ptsmd["SLiM"]["tick"] def test_convert_0_2_files(self): for ts in self.get_0_2_slim_examples(): @@ -242,26 +367,16 @@ def test_convert_0_2_files(self): pts = pyslim.update(ts) assert pyslim.is_current_version(pts) self.verify_upgrade(pts) + self.verify_consistency(ts, pts, file_version="0.2") 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) + ptsmd = pts.metadata + assert isinstance(ptsmd, dict) + assert "SLiM" in ptsmd + assert record["parameters"]["model_type"] == ptsmd["SLiM"]["model_type"] + assert record["slim"]["generation"] == ptsmd["SLiM"]["tick"] def test_convert_0_3_files(self): for ts in self.get_0_3_slim_examples(): @@ -270,26 +385,16 @@ def test_convert_0_3_files(self): pts = pyslim.update(ts) assert pyslim.is_current_version(pts) self.verify_upgrade(pts) + self.verify_consistency(ts, pts, file_version="0.3") 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) + ptsmd = pts.metadata + assert isinstance(ptsmd, dict) + assert "SLiM" in ptsmd + assert record["parameters"]["model_type"] == ptsmd["SLiM"]["model_type"] + assert record["slim"]["generation"] == ptsmd["SLiM"]["tick"] def test_convert_0_4_files(self): # Note that with version 0.5 and above, we *don't* get information from @@ -300,26 +405,16 @@ def test_convert_0_4_files(self): pts = pyslim.update(ts) assert pyslim.is_current_version(pts) self.verify_upgrade(pts) + self.verify_consistency(ts, pts, file_version="0.4") 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) + ptsmd = pts.metadata + assert isinstance(ptsmd, dict) + assert "SLiM" in ptsmd + assert record["parameters"]["model_type"] == ptsmd["SLiM"]["model_type"] + assert record["slim"]["generation"] == ptsmd["SLiM"]["tick"] def test_convert_0_5_files(self): for ts in self.get_0_5_slim_examples(): @@ -328,26 +423,16 @@ def test_convert_0_5_files(self): pts = pyslim.update(ts) assert pyslim.is_current_version(pts) self.verify_upgrade(pts) + self.verify_consistency(ts, pts, file_version="0.5") 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) + ptsmd = pts.metadata + assert isinstance(ptsmd, dict) + assert "SLiM" in ptsmd + assert record["parameters"]["model_type"] == ptsmd["SLiM"]["model_type"] + assert record["slim"]["generation"] == ptsmd["SLiM"]["tick"] def test_convert_0_6_files(self): for ts in self.get_0_6_slim_examples(): @@ -356,26 +441,16 @@ def test_convert_0_6_files(self): pts = pyslim.update(ts) assert pyslim.is_current_version(pts) self.verify_upgrade(pts) + self.verify_consistency(ts, pts, file_version="0.6") 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) + ptsmd = pts.metadata + assert isinstance(ptsmd, dict) + assert "SLiM" in ptsmd + assert record["parameters"]["model_type"] == ptsmd["SLiM"]["model_type"] + assert record["slim"]["generation"] == ptsmd["SLiM"]["tick"] def test_convert_0_7_files(self): for ts in self.get_0_7_slim_examples(): @@ -384,26 +459,16 @@ def test_convert_0_7_files(self): pts = pyslim.update(ts) assert pyslim.is_current_version(pts) self.verify_upgrade(pts) + self.verify_consistency(ts, pts, file_version="0.7") 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) + ptsmd = pts.metadata + assert isinstance(ptsmd, dict) + assert "SLiM" in ptsmd + assert record["parameters"]["model_type"] == ptsmd["SLiM"]["model_type"] + assert record["slim"]["generation"] == ptsmd["SLiM"]["tick"] def test_convert_0_8_files(self): for ts in self.get_0_8_slim_examples(): @@ -412,18 +477,17 @@ def test_convert_0_8_files(self): pts = pyslim.update(ts) assert pyslim.is_current_version(pts) self.verify_upgrade(pts) + self.verify_consistency(ts, pts, file_version="0.8") 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"]["tick"] == pts.metadata["SLiM"]["tick"] + ptsmd = pts.metadata + assert isinstance(ptsmd, dict) + assert "SLiM" in ptsmd + assert record["parameters"]["model_type"] == ptsmd["SLiM"]["model_type"] + assert record["slim"]["tick"] == ptsmd["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()) genome_type = None for n in samples: @@ -432,7 +496,7 @@ def test_convert_0_8_files(self): genome_type = md["genome_type"] break assert genome_type is not None - chromosome_type = pts.metadata["SLiM"]["this_chromosome"]["type"] + chromosome_type = ptsmd["SLiM"]["this_chromosome"]["type"] GENOME_TYPE_AUTOSOME = 0 GENOME_TYPE_X = 1 GENOME_TYPE_Y = 2 @@ -442,13 +506,24 @@ def test_convert_0_8_files(self): assert chromosome_type == "X" elif genome_type == GENOME_TYPE_Y: assert chromosome_type == "-Y" - 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_0_9_files(self): + for ts in self.get_0_9_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) + self.verify_consistency(ts, pts, file_version="0.9") + 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) + ptsmd = pts.metadata + assert isinstance(ptsmd, dict) + assert "SLiM" in ptsmd + assert record["parameters"]["model_type"] == ptsmd["SLiM"]["model_type"] + assert record["slim"]["tick"] == ptsmd["SLiM"]["tick"] def test_convert_mixed_files(self): for ts in self.get_mixed_slim_examples(): @@ -461,14 +536,13 @@ def test_convert_mixed_files(self): 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"] + ptsmd = pts.metadata + assert isinstance(ptsmd, dict) + assert "SLiM" in ptsmd + assert record["parameters"]["model_type"] == ptsmd["SLiM"]["model_type"] + assert record["slim"]["generation"] == ptsmd["SLiM"]["tick"] assert list(ts.samples()) == list(pts.samples()) - assert np.array_equal(ts.tables.nodes.flags, pts.tables.nodes.flags) + assert np.array_equal(ts.tables.nodes.flags, pts.nodes_flags) samples = list(ts.samples()) t = ts.first() pt = pts.first() diff --git a/tests/test_recipes/make_v3_tests.sh b/tests/test_recipes/make_old_file_versions.sh similarity index 80% rename from tests/test_recipes/make_v3_tests.sh rename to tests/test_recipes/make_old_file_versions.sh index eb88af9b..ab6b48f9 100644 --- a/tests/test_recipes/make_v3_tests.sh +++ b/tests/test_recipes/make_old_file_versions.sh @@ -95,4 +95,20 @@ $SLIMDIR/slim recipe_WF_X.slim && mv out.trees recipe_WF_X.${TAG}.trees $SLIMDIR/slim recipe_WF_Y.slim && mv out.trees recipe_WF_Y.${TAG}.trees git add -f recipe_nonWF.${TAG}.trees recipe_WF.${TAG}.trees recipe_WF_X.${TAG}.trees recipe_WF_Y.${TAG}.trees +# To make the v5.2 files: + +TAG=v5.2 +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 +$SLIMDIR/slim recipe_WF_X.slim && mv out.trees recipe_WF_X.${TAG}.trees +$SLIMDIR/slim recipe_WF_Y.slim && mv out.trees recipe_WF_Y.${TAG}.trees +$SLIMDIR/slim recipe_all_the_chromosome_types.slim && mv out.trees recipe_all_the_chromosome_types.${TAG}.trees +git add -f recipe_nonWF.${TAG}.trees recipe_WF.${TAG}.trees recipe_WF_X.${TAG}.trees recipe_WF_Y.${TAG}.trees recipe_all_the_chromosome_types.${TAG}.trees + + diff --git a/tests/test_recipes/recipe_WF.slim b/tests/test_recipes/recipe_WF.slim index 25032360..11702500 100644 --- a/tests/test_recipes/recipe_WF.slim +++ b/tests/test_recipes/recipe_WF.slim @@ -10,14 +10,49 @@ initialize() initializeGenomicElementType("g1", m1, 1.0); initializeGenomicElement(g1, 0, 99); initializeRecombinationRate(1e-2); + defineGlobal("MD", Dictionary()); } 1 early() { sim.addSubpop("p1", 10); } +// MUTATION/GENOTYPE INFO +1 first() { // mutation information + MD.setValue("mutations", Dictionary()); +} +mutation() { + muts = MD.getValue("mutations"); + nuc = mut.mutationType.nucleotideBased ? mut.nucleotide else "N"; + m = Dictionary( + "chromosome_id", mut.chromosome.id, + "mutationType", mut.mutationType.id, + "nucleotide", nuc, + "position", mut.position, + "originTick", mut.originTick + ); + muts.setValue(asString(mut.id), m); + return T; +} +10 late() { + subs = Dictionary(); + for (mut in sim.substitutions) { + nuc = mut.mutationType.nucleotideBased ? mut.nucleotide else "N"; + m = Dictionary( + "chromosome_id", mut.chromosome.id, + "mutationType", mut.mutationType.id, + "nucleotide", nuc, + "position", mut.position, + "fixationTick", mut.fixationTick + ); + subs.setValue(asString(mut.id), m); + } + MD.setValue("substitutions", subs); +} + +// OUTPUT/FINISH 10 late() { - sim.treeSeqOutput(TREES_FILE); + sim.treeSeqOutput(TREES_FILE, metadata=MD); catn("Done."); sim.simulationFinished(); } diff --git a/tests/test_recipes/recipe_WF.v5.2.trees b/tests/test_recipes/recipe_WF.v5.2.trees new file mode 100644 index 0000000000000000000000000000000000000000..936f1cd1e6108b42d3485d5b7d086e9e6954f606 GIT binary patch literal 36020 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 L%?uXvJ=6aeKf~fK literal 0 HcmV?d00001 diff --git a/tests/test_recipes/recipe_WF_X.v5.2.trees b/tests/test_recipes/recipe_WF_X.v5.2.trees new file mode 100644 index 0000000000000000000000000000000000000000..d09e122b71d6b273ca7e36590ec7b532794efdce GIT binary patch literal 134436 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 literal 0 HcmV?d00001 diff --git a/tests/test_recipes/recipe_all_the_chromosome_types.v5.2.trees/chromosome_HF.trees b/tests/test_recipes/recipe_all_the_chromosome_types.v5.2.trees/chromosome_HF.trees new file mode 100644 index 0000000000000000000000000000000000000000..473023a98775eed4c72ee8ad77dcd7263d527394 GIT binary patch literal 47996 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 ZgQc;xg|XS0wNq1t;jyVfCV|5<{|O?iJiGt^ literal 0 HcmV?d00001 diff --git a/tests/test_recipes/recipe_all_the_chromosome_types.v5.2.trees/chromosome_HM.trees b/tests/test_recipes/recipe_all_the_chromosome_types.v5.2.trees/chromosome_HM.trees new file mode 100644 index 0000000000000000000000000000000000000000..1fb4b3f5537efd3d98e71c0b97aeb74783f5b434 GIT binary patch literal 47068 zcmeI534B~veeXpak`NLSAcTal+zcU>gO(ZXaxhr&7AJN#D~p2@F_K31;E_hmj2z3U z!3A2DvX!=krLT2gfdU1Z77FP?>++z5vZbX^TA&YTp%hZXqm)N!DewC`_x$hNZ7jt} z3!nGN*gu_n&iQZWf0ldB-OkIdp4fHvnNK+LVO?EaSAZ|PpFj8GcdDm7c5Zm^NFTrZ zap8dvd}=m$girdI=RecO&p1CkSczYD>-QPY|EMt9b+_mD(T>5xea_!|e$!|CD~U1v z-%s(M@%WA^6B*{evmYHMpXGCn-|R^+#Ukdv3q8O2|2ba&O8h43k`(_L7lb(`&*;*j z`LEyeoBuEH{HLTDQgJK)2G9RQpC5SsmFzeE-Jbsmp8wz~`OW^O=l4O^y`JCVDYK)f z`Txb9@I0S$!G+<$U__aJVESM0`7iQhcl&}oCZ8=zwf6U(l>YB~{cR zDO9mq{a;D(f8Fyx{`APy+ykq>AA0`B8r9RngM!67gl7M_-cYOmmwNt{>^HHOdBQV% z!o8mVM=SWx_y;`yImX`|9(>3;gb}7fEB{1_|MjCO!VXbuf7hKPzqOw!&u{);t$y== z!}FW}jo;*VFu%+4UyIi`>*V<`-Rkc@JpYqD`LPY*!L_EKgBZ(xf^RrpOjp;YkfCpd6R7F$ z@%(<+>?-cQfBLti_!n;v`R}m`>R`WR-|qRXyLQin4A)ozSKzOCerrF^*?0f=e=4Q_ z_4BLaxAyb$6#u=A)$yDEU+?+te82H<$bWShbo^`n`}I`$7oWd6esk}8QuZHxLCAl6 zh4I(y{}az|^M%*EFy!CS(eg0E^nW(Rf9yqJ{@xY(kCp$Msq%NfJmg==e^!70?)h#0 zd&|$Qj^FHmxSx2~{NwIdhWxjMLC3%5zY9I1%|Fh4RmifYBWIXl`iD~df#*Nn3fMu8 z@n7rtZT#rID`a?+4G10h&Hl3IH}a3%9Wt!Ye4_KuA~6ldq%52y+ld-A#++4GgBAIH=?5oUs%EfY{vfv3C)xG;# zbM?l2xz)c=p0D`B1C>UzT3@Ix%vKJfUK*yV3(ZQSMe0<&FbZ=5SEb;1^(fVO4 zxY<8jX|`ff)kG|6d^t&`p)JaYvq~1~vz3?#S3$9Ov%b`ri4(M{^OgCfR%BwMdZ02p z)g+-)vRM-`nK5lKmeP^b?2cqOv!ht(*|xl9HExYe7QJsC;bFV3!nm1`jCq4niDByw z6Bp}?OEu?>FfL{7v}V7_%}evrbF)Gni;en$%0hWz2ICu*8G1rC#iYbYWdv0+zZj>E z5!lX_#kLBxAWG-kOkdu};Vkpz=>GEmT|0rKPHk!=sFQIx%x(v|+o&q$MYJGY!=m$F2UJ4%ih5nFF%y;r%8uFt-J{06nLB2F-4RtnX|8{Q- zkAHH*R&#%~)z@lNDotnVS^Tw2cMs!O^JuLFbbi)3UCScn>w1~y5IXOx+~QN&H4lHhLi*Bw5tAg=e7KK)J~zR-sX;&iX}=_}FwyrZv(Dm{f6W~MVpX|@_mGcBl@sLnL1i<+{KylY2wXAp`C zSSoqEZ~M-v?bmGHe$)2NH%{!B+PP(NV)Ml0 z#MI91+isYc+_Jf_{?1g9Zr)dE?W;6`*1l>pUY<{kPSMf2564*R6PpG&y8boel?i&d+4|DA>D>O#4(ywxpssmN-k zY9}ccRTVhh30Fjge{c&dKi-cL;y$&2p`Jo(d6A+Psx0I8RvHCL>|0i*l-o0h9zRhvns8;C-N-h>2_l6PL(R_JvU!%@wT5)~QHE~8Bc^&Th(FuNjt0zbb_nyRQH;p;my~Zb*`+qldLb>+GvLZL z>6H;FO~aGI6p`NACcQNxr9XL6m?F}f+oU%~q>Rs=6sCyu%B^i$j?D*S5ix8L@zxz} zVvdb?JR*iIBEE8Go0wxGF5!P~2YeCr&X{P9BWkL^Q;RVoUx@j*7bnxcXO?GbI?F>g zSFKg1{FQ2Ix!5<3Df7(UYE!aFT*Woo+IR3~Jj$Gopu zVF2N(DOcaP<{hOjaxIZ-VzbOa7IaO#T&uR05q5AHSxukI9J0zNI1^?mqvhs&1+GM@ z(`sfl)o!qAzG{Q3bZJ3zEtUE3;rlnwwW+LT!dQ`*lUg^*^`dv!T>T(y$@wp7m1e-K zVH4LTYfPzvYO_{en1#^lMsrnfvJMkf;_9P=JKC)v-Uu`hdV9)CE!W|vlospJoyLNw zA+172SR;e+4z_c$t$Mz-`I^``qE19>%(f-JhowLhs9u?@?S2RWMVS-buD4RP!f$u~?*oBg+lIiR2CbRrn`p~OfNGPV6MKhhJ>%uiY9_3?r!xm z(VDC7U22r4aZZdOnuB;VPR;_~KhnD0Y}h%`5v&bn>Pt-Q8@9Sim(ERg!kZ73*_bn9 ztAVihbylSO2SZQ1Yv*Zm`GG21{+)HrQR@u1=<)Nlu>KgE{cW3|`{u1$4Z6YG%_=Jk zOY@p4g=4uJ)9DeJCX_R(#Hx@XTaIoI!r5lTA~W7cH0^h7OHEsTPIe3p;OR0?EWqT} zWC7vmy@Jrbdo>7BdmZ!3t6x> z_Ldi_&G}%uLU)Q6tS4wL&Fl+UkC*aWia+h93nEu_AF4F!z3v>j4o$2*!)`!(Y?s7L zXs0yWqffcfC@;H3y6>>8^Y7SK#h~WC`ciE+VDKTLvc=bo@FtDcv}s}a=J;qR%7v^!s|**qC0S%XwEmG?6A*Fgo#Oo_NUIFa_P1+ zQ@K0;zwEjXb%S=-`)}(8-svZ3{(ii2W1pMZWY!z$KGDAZ$^@-_9@Hh$_5?E=h+~~~ zx?ZnU$_rAFo^7sD!M^e=6T)!S!vXX94&pnkfzoO;>i88=kX$c?CysdOq{+_f9BQ56 zR#XR<4;9*jO1pw{S6U}BAhsl1m3A%X4n>wpK<;SjE{|%Mv8-jrIFIeP-$dvtXLWzE)D3YG(I*oR2m)~9V-ou4!ER& z((uUOKxtrjT*u{18b#*{xu{(wXyWoUdzc8!YJ$s2|ddE+R@9b<3~;OY@{pc;>l3}PL1F*-Oz zv1J%u9d!m{95Ff^DXnD`$KX&=5G$$}qht<@4Aa8oAnJ-5K@Eq) zauNkZxHHX_K#q|*F~D6Kq4q}*ilr0{QRAxeBCa188AFaL125n~m*=#OQc0uZV-yoD z6aXV+DMl6tMo6c0$Uy+W@1jzB9aCvA9(j}q!MH>9Ms`u|9SvP3U9N<3G2MoA*Puvq z&9gKzhW!*`cogRjIbTx;qqv!<8YgKO?b-yKr9pKrsvUo!SuJl2t7(=+WZ-g2O?3P|I?`~lC{N(F5m`nbmC0k!k7EpeMOl&Z%OrK50le-!MFSmB3!;AyA((O_fr7|Q zLnBTo(hz|jTs%ImZi256Mmtx5(4+REa?w^0hlL~ZCQYrS5BO6Yp=8u=5h9fd zSK+KtN+Nx(1<5v784MKa8Df+Q`Y_fZXLL;7gK`ErYs@8{LMTRRZ1)y8w1uA9`Im5Iz2&qU~To)>|af>!`NPx3nOM(``(*TLJsyTEUQ_klkE z$H0fdN5Ln-r@`mI7r~doSHU;Hx4?J7zkwfuhw&W4Y2cCI9Pn81c<@9JfTw}WKpz+c zV_*ZA09(LSK<{qt0=IzMKpD(|{a_I+fkWW=;6>o2;FaLD;4bhc@T=g>;BDZY;BN4{ z;P=5FgAaj^f=_}^gU^94g0FzDfp3CugMS4-06zk!@;ru~(Krh{8k`R<0s*)Lbc0?n z07k(xzy#O|t_IhFNw6E-4yM5z*bnNU2@Zn8;HSaQf}aDg2EPFA0>2E7f;WS=gLi}X zfcJwx0LQ?G!JmUqflq_K244VQ244f;1m6PR1>Xlh1gEUwIX&=5a1J;RTnL^Fo(j4_ z4=8~Va5>lrwt%a^wO|*x8SDXNFbDR7MX&@8ffs-mgO`C_+Y4IT~72N!_=JPlk1 z`oRzw2OGc@U@N!^Tni?_Zg4x826JFPsDl<*2G0jC0xt!x1g`~mfnNqk!JEO`!MnkG z!27`;f@9!A;G^IZ;4i>u!RNu3z*oUPf#cvi;CtZTLD$168#n`;4ITp?4=w@$xCC^A z9#8@!U_E#i*bJ@&JHYkeMsO>b0yAJQ*bf%L5;z220A2)M0$u@r9=r~`2^Otp0UrP#1b+rT20jJ;3j7WDJMb0okKjJ=ZSb$)2jD-!Dcn#!9h?Oo4bBJlUM$_w z{S?X-8h6}J;pyO%c!KWACK5F4u8C9Vo^66c2^os6`^S>8BUflfOQI}dNvNf>CM{Lr zGAt)9iwsM!$R8P&1ckID?2uszicErpW>`+FCF7us3zcwUp|p}Dg;)|!Ub(axJJiM! zGBSo`+L&ZyEKMZH(u7bMmhoxAW1>i~WW1gcn(!1XM zEIo08WF)lYmNp?ng3`j|MMOTr(yb|hqEOwYJ5V@Jk8xhx4gM3GRNsqBoCa}!Q1R9X^V zSK$&4%24DME+IoKCzc_KgkiZ3O327iC?O#uQ*)ViD$^5|=6Wnak?W6yS_w@gWGv0t zA&QKagp5qX&B(~86-z>Brf`{hk-_@G874+xppKda*Lm! z$g~d$O_U|0B~v)bII+-7`;gb~XW=q~QpOHhny@3cvJ)*R(;FlsVc5zP83$z=ZpJ~G zhMSR*Nyu&58A}rhlCg4Co&-xmXl_*|C{%Na1eGOI4T+kQ#}bCEvKpi*{9ZwUP_Q_aQz4{sw#l=-$FvTz`KO z=mWZ6a2+Uv2GBi#Uj(|||308=`=0~<1a$5Gk#xZ)f?n`Ua4mQacpi8m_yzE0@Vnqo z!C!-~gMR~OQpFd69;Sic1@Hp!8t`l2cfkJwe+9k@z6VaHM_mBc zf%V{Oum{Y8=Yv;+qu{r}2f?SoKY)J)r}2{7<3TqV2Ume-gBmytUIl&?+ztK&{3ZB% z@Llk5UNSo$tOa9WJGd3>2R{W~34R6q7B~j}0(=Gh3pn*`zPkV}1*70fup2xV+zDO* z{s(wB_+xM{_%iqoSi=qL^T5-<2-pU00ab7a{2cgY@GkI2;8WmB;QxSA_~^r9!6jfA zYy~%ieP9{99J~p<6MO)C68vxQZSXK&rh5!{Di{J=z)fH;I0#+_-UxmZ{2}-R_&e|| zppTV28Wg}F*bHt2bKnl}Qt$@w4)6!yiA(f>g|*a)r%)1V1n41Ni`6}%sO1pE#72KaYy7B4G4 z3D|dFe{A0&u#as;Pv5&IEaP7xFp8;Kwb^p7Ohzf;GpoFR)qdWzuwHL4R{WFLQ#19- z+#DYE6T(_HoZ--d&!CIs2|jo5jC`!1L**lGm#@Oa#1E zyD=Bs$(9!r;d4vkr!8D z-e|w-9=>$q9yo3{fQ-iccg)>$uzFH8=k2D*C-s?`r3UXs$F%2u>>$tUPdEWaeO%93ak_Cl zqEVjYh+ll&K(vCz!PlrMgTDw~c~R>_in(G3I15U=uOx9H6#%QJb3i$^j{o|Qpq(qndrR%H^MWJDV>F6OZ@fLp&$3lI#4F#4UTHd(EX$Xjii=0(mOS}g^p+>tikmO3 ztx2}osIrM)p`h}}F3FNkl~Fzrq+2qjOQE$d(Odb%CmCjw$}Ss~FFRzjj*Umh%9m{M zN}tIxI`f<2qKn(BH1SEU%4B6wn%N*8t2fD)Zly`CwNKHiO!A%8mz7;IMK8Z8luo57 z6s_baE}f!Lnw8J!l_&dDRvnAySwLyxRVY5?D^LEGeA!@hR#t`aK4ChPZgRzc4lo&( zr}CF1_-Vj-^X+>5=`4OO|Mj&V2yG|6v(=!tZUwX?4sr z-^rHzbY48J=UwCR^l?DG{3@H3CmYi=o#K5tv_qsBjBhMOCm&kgvp5zMx4vX~@$jk` zkJ7J6#cv>fy@&E|;#kn}ZXfE{ez%h*SUerh<>c*;rOk6(2MPu8HwlHftb@dbcv|}S zh|hn~3Fuz#^DKOok1Moe$^8Z5!s|Rt*Bgin7LSL&M!G@iZz2BH4z%wiU3iyATu%D< zeH~eU!)O`CS2@MAT&Ic<2>km6UWntr*SE1ajYe7=;k<%b;J#qaU93fiKn6SdLDX7 zkE4&c(C<+sj6>&l!vM!|3=&Vn&XEc=k4R&LG+{IcW2B9H=$#dV<|lDHgScS>$7wv1 zG=rUoH9tB9$N|H(40uPGKTFO3s-f()uanMaQP3ysg@Ss3Me!z|tC)AkyD+D_9O@u%gP zpj2plk-m)~fyuYf*dqBUI?GRoCg1F~e5)sum(Dl3w0=uBdpqWPLX&Uw5x+k$ehV!> zZFf4~=qy~RJXXGyKJTEZ_7*`QCoBD@ykV#@8u7O`ndN|19ieUpn2&XL{55 zX+3Fr(`WofZ*&$~ek6aRQ6@u^Xa2L$_>DU7{FZNeR*I+Pm_D=1;wI11O|S81(j$WK z!0a%3v)ANj_2!-*zR636Mi+$}qa%0FNl%iB6;F!K;ue}-E1$`=bPLTN zCg12xj`3SOqWAhO-}o#)lJ5l@o$0Z-@mZMWvvlLLFwJjzBRh@IhbG7JEi}HgUSkh7 zhDXM4>1n<+e>!e_rq@D~WBC@Q%hyR>#x5_w($joa9+PkRW{<_qjvsIMFw_h2pQWY4 zIE@{5qDiMeSaIpK>l;6g(5{2Tb*FuC3JF4-#*QCBx^Sk4r9YCmVAoC7a4ekdq3bh( zT`SSG8sRjL(+LG#KM~ILAaJ%QJnu9~)bGC2B}>2aPLmYB@(GM^Al9ctF}E{^;nb{r>2_)7M97T8ryWzvF9k zrBuK3PSdwD^rIa_bdQVZbj4J^^G>@cqSIG4^gDX$lOnn&M|4k#=n7HVQ=_y?qO_+) zX}ZE1%3J4;j<3;|JHoX7C@oA;|5A%pYt(hG0ibIm7)iXfQ>-k4$*U&w}XA40e%YnEO-rgBX~1- zH@F9U5PTeb7JLbO1AG_gnN5A?<1yfg;8LJ{S=NKi;2NMkTJ-&n8aM!sfR}?`1iu3G zJm>F#KLmdY?gf7f{sCxjs>;I=bq^1W2P5F1p=d?t0e+G>9;Q{Q=I zGZH?X8h%^WnHznUSo^kw_64@vUbHR3(t`UGFG|aNUa(dR_LUcF?z7rKcPZS6g$%aN zNoN+xblz?rhHFW4f_$I|)ka)r;+h94SLjp#GAm&;OireUwf zpmj%`0?>9s6L^-Yp$}K{IYMl3z9w~W=`T{HAm7;ci{8ZPF9MRNP5bjz#y6l+u}tM!njZOJ<4JKWt_i~Cq~LWxpB z^Za*^efPo5;fKLv%|k6w?Ev4;@G<_`=!@au9!@*}w>JI|;o7kA+INcKJWy^_U2E#T zm=3^~?gQLFGonJK^^x$|r5U8ou^EMHuWgIkg-kYZeT6st#IbMot?0Y$YhyfXqtPeW zUs*PT{D+gvwL8koO&JiCSiK5!Z4)XL+ULcdW78-0?YYZ%vH3do6+9{EA0cZ$=ofAf z>UPUWK3*0Z^fHg%Jul~(!`uyjV(o*D*=*Kls;-LMnCD94dJ=B34;A|-G#PVOcpT4$ zWXc;2)zMRL&o1~+QR>0FTCH%~VYklUboNlV3Lik?e}#?V!GBj6czx;c0-wIx@H|g% zp{c6t{M1x~)?( zEx1FMU$s%#DyX7*KUvki6=$?fCeqEV~1 znfg2nW#;kltaVBjT20mhHmeUuu4J{dvB|R{m7>guH1c`7Tr!mg*`k{B(q*?=u|f@|uW znEW*jNchh+j2aEJ9IQ<(jZ_zy1q6kIVM~6e{1zSodO0Jg#s|TvHOI!g+ zWVjOhZFp0ja#^L347t+vOTIf8uo**QC)V~}$^H_TZHOFh;uhO&Ipvp7x+=m=vVw4P z=*eAI$2W+c*s=YlEo~PVk9UGa6&6aP52Oh097i?!OE7LrFfLxY%P3qPV>qGJaT;qx z`#YaZacLHPk!Z%`8QM06 zopU=IbL&YNGm<0GjL9>!Z47(!b~fhblQL#BN1_>%XK33P?f%=@8gdhvtL#r>IU>!R zJX71|Xn)~^#zcoAH<7tYbH;N-nmKu zM{<_e20wBWnX9$uL}PKTv2BC23v)te!a?lIoXBjSSZ-@`6eNx_-^c2v ze^F_1swYV~QJ(1iH%AWzzuoD07PJ+C?qqo89{>mBs5Sv#jp1 zRo}KqZbi0X51Xs)DR8?t+92-=?Z>@gGA(ox0lO;C>kiXH?`%Q?mC?eKt0iQhNN$UI zoZwuAZktCsdxnwuzEz0o~X z=^a=XikUC7R@)0(XBTAFUQs;worN9KlL(&>>djS|gZLm-nTWW}DsEn%=b46Y1= zTf^XHpSU#)w>q!vDO|ZTjPLaEUYJ88!a?D|*RT+2sLo1yDR?@a=6Mrb+1lIf3Ki;C z8Fi7NyXtOxL(Glw<@xD)O%rHuT~E06DG4E`nC{e{cthP3ZBmM?o?u7FWYVNf9XCL% z^?(U_KG!eGHEP^%Am8k6FsRk%<0(6QeZ`gCP+|Pj?N(LT4P1HYE>(JmeUW0lC)q%{ z_tGe65<7ZJA?e{g3;fqZ9=h-+S>4y%xC7Jo6sSQr+U#jDB31BUc#WX1S)OMk+umNX zb|aIk9&|n`ITY(j`3%n{Jt4t3#fi!0!abmed;>i}h+#5^Cl7>DlszQs1yE zEmoCy2BA?~4#&JbEw-`jLm&;6iyJh?Mc2x&mO_XVq>H7#xoT^H?QVB*i9pg{nqw{V!m~W}Zpt3(KukS=ruj zb1QY-+(+@fm3tN#PHdzMuDx;cx*I1|Q1=K!Tm2oJ6)IcVBD&jX-LTtZEr_!Pi{5T^ zNrst9c&Rh&V2ogUE8$_gs9`+HU_r)U;oOHCU1Wd~w8g6+I>jtfr+aARoIyfIVe9&| zv99w_C9b=u^zkn@H6xcwV%X1k+<}e+N31fEbfVRUmy9}`Df|odT!e7Mp&7d zW;4n0;i1u4_A8t&mZrcSe%_L(>7uz9u%I%PxBc#Lb$Xpa>PvMv{PZ!&mSY{_k!cB#P zf?94oBfUFs=2E;KJ`Gaq%II2KYW-ViFYPjGUw`{2U=G+`)+MhH@`Rnc-=DfbSgDm4 znLGH;0x}gY4zm?1J9)-jFke16#pdkw#`r*1Dnp51(mIt?<3i3wc)D-L@F%9T=G$7+60% zxV~5nA7tV0W5MJmeiM_MCbn(cw2i-;CN@p*f6_&^kvOU7#J0)FiOI>3WMXm~(QPDd z+ZLzhSX`462y{G~Hjz(Kq_AuE#`f_;M-O@6ArIU?57_g2_B^0HFKEvp+VhF_T;mfr zhVt$C$AXVv;=@aQ*zLp1eAwf|J|7l+Sn}b34+niXk1$DVb>-f_hHv&ANOI`79aOv*H$0*Vb?Yv_hHwS zKJLS=?LO|quB&|9hh10uxDUH___z)Oobo#W!=t2B${{ii5q=mGQaWvFT#D Vmp$DFd)Xa+wp5xKoSPn+{(o&7lDq%_ literal 0 HcmV?d00001 diff --git a/tests/test_recipes/recipe_all_the_chromosome_types.v5.2.trees/chromosome_ML.trees b/tests/test_recipes/recipe_all_the_chromosome_types.v5.2.trees/chromosome_ML.trees new file mode 100644 index 0000000000000000000000000000000000000000..09bd52f42870d6db1acb1c50fc05ad67ec0c95f5 GIT binary patch literal 38284 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 bb(L|ld)Iuq)ZaB<%+KYAivt60f9d}LRtL{R literal 0 HcmV?d00001 diff --git a/tests/test_recipes/recipe_all_the_chromosome_types.v5.2.trees/chromosome_W.trees b/tests/test_recipes/recipe_all_the_chromosome_types.v5.2.trees/chromosome_W.trees new file mode 100644 index 0000000000000000000000000000000000000000..740a592db2bbe9e71ea9ba50789d42a6bd692402 GIT binary patch literal 40676 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 literal 0 HcmV?d00001 diff --git a/tests/test_recipes/recipe_all_the_chromosome_types.v5.2.trees/chromosome_Z.trees b/tests/test_recipes/recipe_all_the_chromosome_types.v5.2.trees/chromosome_Z.trees new file mode 100644 index 0000000000000000000000000000000000000000..252e4426ec9c5017bd25642df8a58c4722a63c75 GIT binary patch literal 61932 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 literal 0 HcmV?d00001 diff --git a/tests/test_recipes/recipe_chromosomes_adds_muts.slim b/tests/test_recipes/recipe_chromosomes_adds_muts.slim index e53e7332..2ffd3299 100644 --- a/tests/test_recipes/recipe_chromosomes_adds_muts.slim +++ b/tests/test_recipes/recipe_chromosomes_adds_muts.slim @@ -20,10 +20,11 @@ initialize() { for (id in ids, symbol in symbols, type in types) { initializeChromosome(id, length, type, symbol); - initializeAncestralNucleotides(paste0(rep("A", length))); + initializeAncestralNucleotides(randomNucleotides(length)); initializeRecombinationRate(1e-5); initializeGenomicElement(g1, 0, length-1); } + defineGlobal("MD", Dictionary()); } 1 early() { sim.addSubpop("p1", 10); @@ -34,13 +35,68 @@ initialize() { inds = p1.individuals; haps = inds.haplosomesForChromosomes(chrom, includeNulls=F); if (length(haps) > 0) { - sample(haps, 1 + asInteger(length(haps)/2)).addNewDrawnMutation(m2, rdunif(10, 0, chrom.length-1)); + mutl = sample(haps, 1 + asInteger(length(haps)/2)).addNewDrawnMutation(m2, rdunif(10, 0, chrom.length-1)); + muts = MD.getValue("mutations"); + for (mut in mutl) { + nuc = mut.mutationType.nucleotideBased ? mut.nucleotide else "N"; + m = Dictionary( + "chromosome_id", mut.chromosome.id, + "mutationType", mut.mutationType.id, + "nucleotide", nuc, + "position", mut.position, + "originTick", mut.originTick + ); + muts.setValue(asString(mut.id), m); + } } } } + +// MUTATION/GENOTYPE INFO +1 first() { // reference sequences + refseqs = Dictionary(); + for (chrom in sim.chromosomes) { + refseqs.setValue("chr" + chrom.id, chrom.ancestralNucleotides()); + } + MD.setValue("reference_sequence", refseqs); +} + +1 first() { // mutation information + MD.setValue("mutations", Dictionary()); +} +mutation() { + muts = MD.getValue("mutations"); + nuc = mut.mutationType.nucleotideBased ? mut.nucleotide else "N"; + m = Dictionary( + "chromosome_id", mut.chromosome.id, + "mutationType", mut.mutationType.id, + "nucleotide", nuc, + "position", mut.position, + "originTick", mut.originTick + ); + muts.setValue(asString(mut.id), m); + return T; +} +10 late() { + subs = Dictionary(); + for (mut in sim.substitutions) { + nuc = mut.mutationType.nucleotideBased ? mut.nucleotide else "N"; + m = Dictionary( + "chromosome_id", mut.chromosome.id, + "mutationType", mut.mutationType.id, + "nucleotide", nuc, + "position", mut.position, + "fixationTick", mut.fixationTick + ); + subs.setValue(asString(mut.id), m); + } + MD.setValue("substitutions", subs); +} + +// OUTPUT/FINISH 10 late() { - sim.treeSeqOutput(TREES_FILE); + sim.treeSeqOutput(TREES_FILE, metadata=MD); catn("Done."); sim.simulationFinished(); } diff --git a/tests/test_recipes/recipe_no_simplify.slim b/tests/test_recipes/recipe_no_simplify.slim new file mode 100644 index 00000000..ceb49b0a --- /dev/null +++ b/tests/test_recipes/recipe_no_simplify.slim @@ -0,0 +1,22 @@ +initialize() +{ + setSeed(23); + if (!exists("TREES_FILE")) defineGlobal("TREES_FILE", "out.trees"); + initializeSLiMOptions(keepPedigrees=T); + initializeTreeSeq(simplificationRatio=INF, timeUnit="generations"); + initializeMutationRate(1e-2); + initializeMutationType("m1", 0.5, "f", -0.1); + initializeGenomicElementType("g1", m1, 1.0); + initializeGenomicElement(g1, 0, 99); + initializeRecombinationRate(1e-2); +} + +1 early() { + sim.addSubpop("p1", 10); +} + +10 late() { + sim.treeSeqOutput(TREES_FILE, simplify=F); + catn("Done."); + sim.simulationFinished(); +} diff --git a/tests/test_recipes/recipe_nonWF.slim b/tests/test_recipes/recipe_nonWF.slim index 72727ad8..7174a853 100644 --- a/tests/test_recipes/recipe_nonWF.slim +++ b/tests/test_recipes/recipe_nonWF.slim @@ -12,6 +12,7 @@ initialize() initializeGenomicElement(g1, 0, 99); initializeRecombinationRate(1e-2); defineConstant("K", 10); + defineGlobal("MD", Dictionary()); } reproduction() { @@ -26,8 +27,43 @@ early() { p1.fitnessScaling = K / p1.individualCount; } + +// MUTATION/GENOTYPE INFO +1 first() { // mutation information + MD.setValue("mutations", Dictionary()); +} +mutation() { + muts = MD.getValue("mutations"); + nuc = mut.mutationType.nucleotideBased ? mut.nucleotide else "N"; + m = Dictionary( + "chromosome_id", mut.chromosome.id, + "mutationType", mut.mutationType.id, + "nucleotide", nuc, + "position", mut.position, + "originTick", mut.originTick + ); + muts.setValue(asString(mut.id), m); + return T; +} +10 late() { + subs = Dictionary(); + for (mut in sim.substitutions) { + nuc = mut.mutationType.nucleotideBased ? mut.nucleotide else "N"; + m = Dictionary( + "chromosome_id", mut.chromosome.id, + "mutationType", mut.mutationType.id, + "nucleotide", nuc, + "position", mut.position, + "fixationTick", mut.fixationTick + ); + subs.setValue(asString(mut.id), m); + } + MD.setValue("substitutions", subs); +} + +// OUTPUT/FINISH 10 late() { - sim.treeSeqOutput(TREES_FILE); + sim.treeSeqOutput(TREES_FILE, metadata=MD); catn("Done."); sim.simulationFinished(); } diff --git a/tests/test_recipes/recipe_nonWF.v5.2.trees b/tests/test_recipes/recipe_nonWF.v5.2.trees new file mode 100644 index 0000000000000000000000000000000000000000..137d2b0325285ecc3a7330110074153fae35a1f7 GIT binary patch literal 25044 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 literal 0 HcmV?d00001 diff --git a/tests/test_recipes/recipe_nucleotides_WF.slim b/tests/test_recipes/recipe_nucleotides_WF.slim index 29baf777..32b30e9b 100644 --- a/tests/test_recipes/recipe_nucleotides_WF.slim +++ b/tests/test_recipes/recipe_nucleotides_WF.slim @@ -12,14 +12,57 @@ initialize() { initializeGenomicElementType("g1", m1, 1.0, mmJukesCantor(4e-2)); initializeGenomicElement(g1, 0, L-1); initializeRecombinationRate(1e-2); + defineGlobal("MD", Dictionary()); } 1 early() { sim.addSubpop("p1", 10); } +// MUTATION/GENOTYPE INFO +1 first() { // reference sequences + refseqs = Dictionary(); + for (chrom in sim.chromosomes) { + refseqs.setValue("chr" + chrom.id, chrom.ancestralNucleotides()); + } + MD.setValue("reference_sequence", refseqs); +} + +1 first() { // mutation information + MD.setValue("mutations", Dictionary()); +} +mutation() { + muts = MD.getValue("mutations"); + nuc = mut.mutationType.nucleotideBased ? mut.nucleotide else "N"; + m = Dictionary( + "chromosome_id", mut.chromosome.id, + "mutationType", mut.mutationType.id, + "nucleotide", nuc, + "position", mut.position, + "originTick", mut.originTick + ); + muts.setValue(asString(mut.id), m); + return T; +} +10 late() { + subs = Dictionary(); + for (mut in sim.substitutions) { + nuc = mut.mutationType.nucleotideBased ? mut.nucleotide else "N"; + m = Dictionary( + "chromosome_id", mut.chromosome.id, + "mutationType", mut.mutationType.id, + "nucleotide", nuc, + "position", mut.position, + "fixationTick", mut.fixationTick + ); + subs.setValue(asString(mut.id), m); + } + MD.setValue("substitutions", subs); +} + +// OUTPUT/FINISH 10 late() { - sim.treeSeqOutput(TREES_FILE); + sim.treeSeqOutput(TREES_FILE, metadata=MD); catn("Done."); sim.simulationFinished(); } diff --git a/tests/test_recipes/recipe_nucleotides_nonWF.slim b/tests/test_recipes/recipe_nucleotides_nonWF.slim index 0ae0dc03..5896984f 100644 --- a/tests/test_recipes/recipe_nucleotides_nonWF.slim +++ b/tests/test_recipes/recipe_nucleotides_nonWF.slim @@ -12,6 +12,7 @@ initialize() { initializeGenomicElement(g1, 0, L-1); initializeRecombinationRate(1e-2); defineConstant("K", 10); + defineGlobal("MD", Dictionary()); } reproduction() { @@ -26,8 +27,51 @@ early() { p1.fitnessScaling = K / p1.individualCount; } + +// MUTATION/GENOTYPE INFO +1 first() { // reference sequences + refseqs = Dictionary(); + for (chrom in sim.chromosomes) { + refseqs.setValue("chr" + chrom.id, chrom.ancestralNucleotides()); + } + MD.setValue("reference_sequence", refseqs); +} + +1 first() { // mutation information + MD.setValue("mutations", Dictionary()); +} +mutation() { + muts = MD.getValue("mutations"); + nuc = mut.mutationType.nucleotideBased ? mut.nucleotide else "N"; + m = Dictionary( + "chromosome_id", mut.chromosome.id, + "mutationType", mut.mutationType.id, + "nucleotide", nuc, + "position", mut.position, + "originTick", mut.originTick + ); + muts.setValue(asString(mut.id), m); + return T; +} +10 late() { + subs = Dictionary(); + for (mut in sim.substitutions) { + nuc = mut.mutationType.nucleotideBased ? mut.nucleotide else "N"; + m = Dictionary( + "chromosome_id", mut.chromosome.id, + "mutationType", mut.mutationType.id, + "nucleotide", nuc, + "position", mut.position, + "fixationTick", mut.fixationTick + ); + subs.setValue(asString(mut.id), m); + } + MD.setValue("substitutions", subs); +} + +// OUTPUT/FINISH 10 late() { - sim.treeSeqOutput(TREES_FILE); + sim.treeSeqOutput(TREES_FILE, metadata=MD); catn("Done."); sim.simulationFinished(); } diff --git a/tests/test_recipes/recipe_with_traits.slim b/tests/test_recipes/recipe_with_traits.slim new file mode 100644 index 00000000..72602d26 --- /dev/null +++ b/tests/test_recipes/recipe_with_traits.slim @@ -0,0 +1,101 @@ +initialize() { + setSeed(23); + if (!exists("TREES_FILE")) defineGlobal("TREES_FILE", "out.trees"); + initializeSLiMOptions(keepPedigrees=T); + initializeTreeSeq(timeUnit="generations"); + defineConstant("I1", 5.0); + defineConstant("I2", -5.0); + defineConstant("OPT1", 10.0); + defineConstant("OPT2", 10.0); + defineConstant("SD1", 2.0); + defineConstant("SD2", 2.0); + + initializeSex(); + + popgen1T = initializeTrait("popgen1T", "m", 1.0, 0.0, 0.01, directFitnessEffect=T); + popgen2T = initializeTrait("popgen2T", "m", 1.0, 0.0, 0.01, directFitnessEffect=T); + n1T = initializeTrait("n1T", "m", directFitnessEffect=T); + n2T = initializeTrait("n2T", "m", directFitnessEffect=F); + + quant1T = initializeTrait("quant1T", "a", I1, 0.0, 0.01, directFitnessEffect=F); + quant2T = initializeTrait("quant2T", "a", I2, 0.0, 0.01, directFitnessEffect=F); + n3T = initializeTrait("n3T", "a", directFitnessEffect=F, baselineAccumulation=F); + + + logistic1T = initializeTrait("logistic1T", "l", 0.0, 0.01, 0.01, directFitnessEffect=T); + initializeMutationType("m1", 0.4, "f", 0.0); + initializeMutationType("m2", 0.4, "e", 0.05); + m2.setEffectSizeDistributionForTrait(c(n1T, n2T), "f", 0.0); + m2.setEffectSizeDistributionForTrait(c(quant1T, quant2T), "n", 0.0, 0.1); + m2.setEffectSizeDistributionForTrait(c(logistic1T), "n", -0.05, 0.1); + + initializeMutationType("m3", 0.4, "g", -0.05, 1.0); + m3.setEffectSizeDistributionForTrait(c(n1T, n2T), "f", 0.0); + m3.setEffectSizeDistributionForTrait(c(quant1T, quant2T), "n", 0.0, 0.1); + m3.setEffectSizeDistributionForTrait(c(logistic1T), "n", -0.05, 0.1); + + c(m2,m3).setEffectSizeDistributionForTrait(n3T, "n", -5.0, 0.5); + + c(m2,m3).setDefaultDominanceForTrait(c(popgen2T, quant2T), NAN); + + c(m2,m3).logMutationData(T, trait=NULL, effectSize=T, dominance=T); + + initializeGenomicElementType("g1", m1, 1.0); + initializeGenomicElementType("g2", 1:3, c(3, 1, 2)); + + ids = 1:5; + symbols = c(1, 2, "X", "Y", "MT"); + lengths = rdunif(5, 1e7, 2e7); + types = c("A", "A", "X", "Y", "H"); + names = c("A1", "A2", "X", "Y", "MT"); + + for (id in ids, symbol in symbols, length in lengths, type in types, name in names) + { + initializeChromosome(id, length, type, symbol, name); + initializeMutationRate(1e-7); + initializeRecombinationRate(1e-8); + + if (id == 1) + initializeGenomicElement(g1); // autosome 1 is pure-neutral, using only m1 + else + initializeGenomicElement(g2); // autosome 2 is a mix, using m1 / m2 / m3 + } +} + +mutation(m2) { + // set random dominance effects for the popgen1T and quant1T and logistic1TDominance traits + // other effects are generated as specified by the mutation type DES + mut.popgen1TDominance = runif(1); + mut.quant1TDominance = runif(1); + mut.logistic1TDominance = runif(1); + return T; +} +mutation(m3) { + // set random dominance effects for the popgen1T and quant1T and logistic1TDominance traits + // other effects are generated as specified by the mutation type DES + mut.popgen1TDominance = runif(1); + mut.quant1TDominance = runif(1); + mut.logistic1TDominance = runif(1); + return T; +} + +1 late() { + sim.addSubpop("p1", 20); +} + +1: late() { + inds = sim.subpopulations.individuals; + sim.demandPhenotype(NULL, c(sim.quant1T, sim.quant2T)); + phenotypes_q1 = inds.quant1T; + phenotypes_q2 = inds.quant2T; + fitnessEffect_q1 = dnorm(phenotypes_q1, OPT1, SD1) / dnorm(0.0, 0.0, SD1); + fitnessEffect_q2 = dnorm(phenotypes_q2, OPT2, SD2) / dnorm(0.0, 0.0, SD2); + inds.fitnessScaling = fitnessEffect_q1 * fitnessEffect_q2; +} + + +10 late() { + sim.treeSeqOutput(TREES_FILE); + catn("Done."); + sim.simulationFinished(); +} diff --git a/tests/test_tree_sequence.py b/tests/test_tree_sequence.py index 57f9b0ea..18a96111 100644 --- a/tests/test_tree_sequence.py +++ b/tests/test_tree_sequence.py @@ -2,6 +2,7 @@ Test cases for tree sequences. """ +import copy import json import random import sys @@ -17,6 +18,18 @@ from .recipe_specs import recipe_eq, restarted_recipe_eq +def run_with_ts_metadata(f, ts_metadata, *args, **kwargs): + # check for equality in a method with and without passing in + # the ts_metadata argument + a = f(*args, **kwargs) + new_kwargs = copy.deepcopy(kwargs) + new_kwargs["ts_metadata"] = ts_metadata + b = f(*args, **new_kwargs) + assert len(a) == len(b) + np.testing.assert_equal(a, b) + return a + + def mutations_above(ts, node, pos): for s in ts.sites(): if s.position == pos: @@ -42,26 +55,65 @@ def naive_mutation_at(ts, node, pos, time=None): return mut_id +def verify_mutation_metadata(ts): + # Verify that all derived states are properly accounted for + # in mutation metadata. + mdl = ts.metadata["SLiM_mutation_list"] + mut_info = {str(mut["mutation_id"]): mut for mut in mdl} + assert len(mut_info) == len(mdl) + for mut in ts.mutations(): + for j in mut.derived_state.split(","): + assert j in mut_info + + +class TestMutationMetadata(tests.PyslimTestCase): + @pytest.mark.parametrize("recipe", recipe_eq("multichrom"), indirect=True) + def test_mutation_IDs_unique(self, recipe): + ids = set() + for _, ts in recipe["ts"].items(): + mut_info = pyslim.mutation_metadata(ts) + new_ids = set(mut_info.keys()) + assert len(ids.intersection(new_ids)) == 0 + ids = ids.union(new_ids) + + def test_mutation_metadata(self, recipe): + # test that mutation metadata is properly present + for _, ts in recipe["ts"].items(): + verify_mutation_metadata(ts) + + @pytest.mark.parametrize("recipe", [next(recipe_eq())], indirect=True) + def test_check(self, recipe): + for _, ts in recipe["ts"].items(): + assert ts.num_mutations > 5 + t = ts.dump_tables() + md = t.metadata + del md["SLiM_mutation_list"][5:] + t.metadata = md + ts = t.tree_sequence() + mut_info = pyslim.mutation_metadata(ts, check=False) + assert len(mut_info) == 5 + with pytest.raises(ValueError, match="missing information for mutation"): + _ = pyslim.mutation_metadata(ts) + + class TestSlimTime(tests.PyslimTestCase): # Tests for slim_time() - @pytest.mark.parametrize("recipe", recipe_eq(exclude="long"), indirect=True) + @pytest.mark.parametrize( + "recipe", recipe_eq(exclude=["long", "old_mutations"]), indirect=True + ) def test_slim_time(self, recipe): for _, ts in recipe["ts"].items(): - 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 == pyslim.slim_time(ts, mut.time) + muts = pyslim.mutation_metadata(ts) # 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 == pyslim.slim_time(ts, mut.time, stage="early") + stage = "early" if "init_mutated" in recipe else None + slim_times = pyslim.slim_time(ts, ts.mutations_time, stage=stage) + for t, mut in zip(slim_times, ts.mutations()): + mut_time = max( + [muts[int(j)]["slim_time"] for j in mut.derived_state.split(",")] + ) + assert mut_time == t class TestNextMutationID(tests.PyslimTestCase): @@ -99,12 +151,15 @@ def test_reload_slim(self, recipe, helper_functions, tmp_path): ) next_id = pyslim.next_slim_mutation_id(rts) T = max(1, rts.segregating_sites(mode="branch", span_normalise=False)) - mts = msprime.sim_mutations( - rts, - rate=max(6e-4, 10 / T), - keep=True, - model=msprime.SLiMMutationModel(type=1, next_id=next_id), - random_seed=135, + mts = pyslim.add_mutation_metadata( + msprime.sim_mutations( + rts, + rate=max(6e-4, 10 / T), + keep=True, + model=msprime.SLiMMutationModel(type=1, next_id=next_id), + random_seed=135, + ), + mutation_type=1, ) assert mts.num_mutations > rts.num_mutations recapped[chrom] = mts @@ -127,11 +182,16 @@ def test_reload_slim(self, recipe, helper_functions, tmp_path): assert chrom in recipe["ts"] assert pyslim.next_slim_mutation_id(mts) == pyslim.next_slim_mutation_id(ts) assert ts.num_mutations == recapped[chrom].num_mutations - a = ts.metadata - a["SLiM"].pop("user_metadata", None) - b = recipe["ts"][chrom].metadata - b["SLiM"].pop("user_metadata", None) - assert a == b + ots = recipe["ts"][chrom] + assert ts.metadata["SLiM"] == ots.metadata["SLiM"] + mut_info = pyslim.mutation_metadata(ts) + omut_info = pyslim.mutation_metadata(ots) + assert len(mut_info) == len(ts.metadata["SLiM_mutation_list"]) + assert len(omut_info) == len(ots.metadata["SLiM_mutation_list"]) + # we've added new mutations but originals should all be there + for k in omut_info: + assert k in mut_info + assert omut_info[k] == mut_info[k] def test_invalid_derived_state(self): ts = msprime.sim_ancestry( @@ -156,10 +216,12 @@ class TestRecapitate(tests.PyslimTestCase): """ def check_recap_consistency(self, ts, recap, with_ancestral_Ne=True): - 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"] + tsmd = ts.metadata + remd = recap.metadata + assert tsmd["SLiM"]["tick"] == remd["SLiM"]["tick"] + assert tsmd["SLiM"]["cycle"] == remd["SLiM"]["cycle"] + assert tsmd["SLiM"]["stage"] == remd["SLiM"]["stage"] + assert tsmd["SLiM"]["name"] == remd["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(): @@ -264,6 +326,7 @@ def test_unique_names(self): assert names[0] == "ancestral" assert names[-2] == "ancestral_ancestral" + @pytest.mark.parametrize("recipe", recipe_eq(exclude="no_simplify"), indirect=True) def test_recapitation(self, recipe): for _, ts in recipe["ts"].items(): recomb_rate = 1.0 / ts.sequence_length @@ -282,7 +345,9 @@ def test_recapitation(self, recipe): assert t.num_roots == 1 assert recap.node(t.root).time >= old_root_time - @pytest.mark.parametrize("recipe", recipe_eq(exclude="long"), indirect=True) + @pytest.mark.parametrize( + "recipe", recipe_eq(exclude=["long", "no_simplify"]), indirect=True + ) def test_with_recomb_map(self, recipe): for _, ts in recipe["ts"].items(): recomb_rate = 1.0 / ts.sequence_length @@ -333,9 +398,10 @@ def test_first_gen_nodes(self, recipe): # (note this will fail if some populations were started at different # times than others or if the tick has been changed) for _, ts in recipe["ts"].items(): - root_time = ts.metadata["SLiM"]["tick"] - is_wf = ts.metadata["SLiM"]["model_type"] == "WF" - remembered_stage = ts.metadata["SLiM"]["stage"] + tsmd = ts.metadata + root_time = tsmd["SLiM"]["tick"] + is_wf = tsmd["SLiM"]["model_type"] == "WF" + remembered_stage = tsmd["SLiM"]["stage"] if (not is_wf) or (remembered_stage != "late"): root_time -= 1 if (not is_wf) and ("begun_first" in recipe): @@ -344,12 +410,10 @@ def test_first_gen_nodes(self, recipe): root_time -= 1 if is_wf and ("begun_late" in recipe): root_time -= 1 + vacant = pyslim.nodes_vacant(ts) for t in ts.trees(): for u in t.roots: - assert ( - pyslim.node_is_vacant(ts, ts.node(u)) - or ts.node(u).time == root_time - ) + assert vacant[u] or ts.node(u).time == root_time class TestIndividualAges(tests.PyslimTestCase): @@ -384,14 +448,23 @@ def test_mismatched_remembered_stage(self, recipe): def test_population(self, recipe): for _, ts in recipe["ts"].items(): individual_populations = ts.individuals_population - all_inds = pyslim.individuals_alive_at(ts, 0) + ts_metadata = ts.metadata + all_inds = run_with_ts_metadata( + pyslim.individuals_alive_at, ts_metadata, ts, 0 + ) assert len(all_inds) > 0 for p in range(ts.num_populations): - sub_inds = pyslim.individuals_alive_at(ts, 0, population=p) + sub_inds = pyslim.individuals_alive_at( + ts, 0, population=p, ts_metadata=ts_metadata + ) assert set(sub_inds) == set(all_inds[individual_populations == p]) - sub_inds = pyslim.individuals_alive_at(ts, 0, population=[p]) + sub_inds = pyslim.individuals_alive_at( + ts, 0, population=[p], ts_metadata=ts_metadata + ) assert set(sub_inds) == set(all_inds[individual_populations == p]) - sub_inds = pyslim.individuals_alive_at(ts, 0, population=np.arange(p)) + sub_inds = pyslim.individuals_alive_at( + ts, 0, population=np.arange(p), ts_metadata=ts_metadata + ) assert set(sub_inds) == set(all_inds[individual_populations != p]) @pytest.mark.parametrize( @@ -399,9 +472,14 @@ def test_population(self, recipe): ) def test_samples_only(self, recipe): for _, ts in recipe["ts"].items(): - all_inds = pyslim.individuals_alive_at(ts, 0) + ts_metadata = ts.metadata + all_inds = run_with_ts_metadata( + pyslim.individuals_alive_at, ts_metadata, ts, 0 + ) assert set(all_inds) == set( - pyslim.individuals_alive_at(ts, 0, samples_only=False) + pyslim.individuals_alive_at( + ts, 0, samples_only=False, ts_metadata=ts_metadata + ) ) sub_inds = np.random.choice( all_inds, size=min(len(all_inds), 4), replace=False @@ -440,18 +518,19 @@ def test_after_simplify(self, recipe): @pytest.mark.parametrize("recipe", recipe_eq("pedigree"), indirect=True) def test_ages(self, recipe): for _, ts in recipe["ts"].items(): + ts_metadata = ts.metadata info = recipe["info"] remembered_stage = "late" if "remembered_first" in recipe: remembered_stage = "first" elif "remembered_early" in recipe: remembered_stage = "early" - assert remembered_stage == ts.metadata["SLiM"]["stage"] - max_time_ago = ts.metadata["SLiM"]["tick"] + assert remembered_stage == ts_metadata["SLiM"]["stage"] + max_time_ago = ts_metadata["SLiM"]["tick"] if remembered_stage in ("first", "early"): max_time_ago -= 1 for time in range(0, max_time_ago): - slim_tick = ts.metadata["SLiM"]["tick"] - time + slim_tick = ts_metadata["SLiM"]["tick"] - time check_stages = ("first", "early", "late") if time == 0: if remembered_stage == "first": @@ -470,10 +549,18 @@ def test_ages(self, recipe): check_stages = ("late",) for stage in check_stages: alive = pyslim.individuals_alive_at( - ts, time, stage=stage, remembered_stage=remembered_stage + ts, + time, + stage=stage, + remembered_stage=remembered_stage, + ts_metadata=ts_metadata, ) ages = pyslim.individual_ages_at( - ts, time, stage=stage, remembered_stage=remembered_stage + ts, + time, + stage=stage, + remembered_stage=remembered_stage, + ts_metadata=ts_metadata, ) for ind in ts.individuals(): ind_time = ts.node(ind.nodes[0]).time @@ -489,7 +576,7 @@ def test_ages(self, recipe): assert slim_alive == pyslim_alive if slim_alive: slim_age = info[slim_id]["age"][(slim_tick, stage)] - if ts.metadata["SLiM"]["model_type"] == "WF": + if ts_metadata["SLiM"]["model_type"] == "WF": # SLiM records -1 but we return 0 in late and 1 in early slim_age = 0 + (stage in ("first", "early")) assert ages[ind.id] == slim_age @@ -595,9 +682,10 @@ def test_post_simplify(self, recipe): for _, ts in recipe["ts"].items(): rng = np.random.default_rng(seed=3) individual_times = ts.individuals_time + md_tick = ts.metadata["SLiM"]["tick"] keep_indivs = rng.choice( # assumes tick hasn't been changed - np.where(individual_times < ts.metadata["SLiM"]["tick"] - 1)[0], + np.where(individual_times < md_tick - 1)[0], size=30, replace=False, ) @@ -694,14 +782,66 @@ def test_pedigree_parents(self, recipe): gfolks = [] for a in set(info[sid]["parents"]) - set(ts_p): gfolks.extend(info[a]["parents"]) - print("===== ", hasp, ind) - print("sid: ", sid, "ts_p: ", ts_p) - print("slim_p: ", slim_p) - print(gfolks) + # print("===== ", hasp, ind) + # print("sid: ", sid, "ts_p: ", ts_p) + # print("slim_p: ", slim_p) + # print(gfolks) for a in set(ts_p) - set(slim_p): assert a in gfolks +class TestMutationConsistency(tests.PyslimTestCase): + """ + Test for consistency between what SLiM has written down in top-level metadata + and what's in the tree sequence + """ + + @pytest.mark.parametrize("recipe", recipe_eq("refseq"), indirect=True) + def test_reference_sequence_consistency(self, recipe): + for n, ts in recipe["ts"].items(): + tsmd = ts.metadata + chrom_id = tsmd["SLiM"]["this_chromosome"]["id"] + assert ts.has_reference_sequence() + ref = list( + tsmd["SLiM"]["user_metadata"]["reference_sequence"][0][f"chr{chrom_id}"][ + 0 + ] + ) + ts_ref = ts.reference_sequence.data + subs = [ + x[0] + for x in tsmd["SLiM"]["user_metadata"]["substitutions"][0].values() + if x[0]["chromosome_id"][0] == chrom_id + ] + subs.sort(key=lambda x: (x["position"][0], x["fixationTick"][0])) + for s in subs: + nuc = s["nucleotide"][0] + if nuc != "N": + ref[s["position"][0]] = nuc + assert ts_ref == "".join(ref) + + @pytest.mark.parametrize("recipe", recipe_eq("record_mutations"), indirect=True) + def test_mutation_consistency(self, recipe): + for n, ts in recipe["ts"].items(): + tsmd = ts.metadata + chrom_id = tsmd["SLiM"]["this_chromosome"]["id"] + # this is just making these things not lists, mostly + debug_info = { + int(k): {x: y[0] for x, y in v[0].items()} + for k, v in tsmd["SLiM"]["user_metadata"]["mutations"][0].items() + } + mut_info = pyslim.mutation_metadata(ts) + for mut in ts.mutations(): + for k in mut.derived_state.split(","): + k = int(k) + assert k in debug_info or mut_info[k]["mutation_id"] == 2 + assert k in mut_info + assert debug_info[k]["chromosome_id"] == chrom_id + assert debug_info[k]["position"] == ts.site(mut.site).position + assert debug_info[k]["mutationType"] == mut_info[k]["mutation_type"] + assert debug_info[k]["originTick"] == mut_info[k]["slim_time"] + + class TestReferenceSequence(tests.PyslimTestCase): """ Test for operations involving the reference sequence @@ -749,6 +889,7 @@ def test_nucleotide_at_errors(self, recipe): def test_mutation_at(self, recipe): rng = random.Random(42) for _, ts in recipe["ts"].items(): + L = int(min(50000, ts.sequence_length)) for _ in range(min(10, ts.num_sites)): site = rng.choice(ts.sites()) pos = site.position @@ -766,10 +907,8 @@ def test_mutation_at(self, recipe): a = pyslim.mutation_at(ts, node, pos, time=time) b = naive_mutation_at(ts, node, pos, time=time) assert a == b - for _ in range(min(10, int(ts.sequence_length - ts.num_sites))): - pos = rng.choice( - list(set(range(int(ts.sequence_length))) - set(ts.sites_position)) - ) + for _ in range(min(10, int(L - ts.num_sites))): + pos = rng.choice(list(set(range(L)) - set(ts.sites_position))) tree = ts.at(pos) for _ in range(10): node = rng.randint(0, ts.num_nodes - 1) @@ -778,33 +917,63 @@ def test_mutation_at(self, recipe): for time in [None, ts.node(node).time, ut]: assert naive_mutation_at(ts, node, pos, time=time) == -1 + @pytest.mark.parametrize("recipe", recipe_eq("nucleotides"), indirect=True) def test_nucleotide_at(self, recipe): random.seed(42) for _, ts in recipe["ts"].items(): if ts.num_mutations > 0: + mut_metadata = pyslim.mutation_metadata(ts) mut_md = ts.mutation(0).metadata - has_nucleotides = mut_md["mutation_list"][0]["nucleotide"] >= 0 - if has_nucleotides: - assert ts.has_reference_sequence() - assert len(ts.reference_sequence.data) == ts.sequence_length - for _ in range(100): - node = random.randint(0, ts.num_nodes - 1) - pos = random.randint(0, int(ts.sequence_length) - 1) - tree = ts.at(pos) - parent = tree.parent(node) - 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 = 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) - if ts.site(mut.site).position == pos: - b = mut.metadata["mutation_list"][0]["nucleotide"] - assert a == b + tsmd = ts.metadata + # check we've got nucleotide mutations + nucs = np.array([x["nucleotide"] for x in tsmd["SLiM_mutation_list"]]) + assert np.sum(nucs >= 0) > 1 + mut_info = { + str(mut["mutation_id"]): mut for mut in tsmd["SLiM_mutation_list"] + } + assert ts.has_reference_sequence() + assert len(ts.reference_sequence.data) == ts.sequence_length + for _ in range(100): + node = random.randint(0, ts.num_nodes - 1) + pos = random.randint(0, int(ts.sequence_length) - 1) + tree = ts.at(pos) + parent = tree.parent(node) + 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 = pyslim.nucleotide_at( + ts, parent, pos, mut_metadata=mut_metadata + ) + c = pyslim.nucleotide_at( + ts, + node, + pos, + ts.node(parent).time, + mut_metadata=mut_metadata, + ) + assert b == c + for k in np.where(node == ts.tables.mutations.node)[0]: + mut = ts.mutation(k) + if ts.site(mut.site).position == pos: + b = mut_info[mut.derived_state.split(",")[0]][ + "nucleotide" + ] + assert a == b + + @pytest.mark.parametrize("recipe", [next(recipe_eq("nucleotides"))], indirect=True) + def test_nucleotide_at_without_mut_metadata(self, recipe): + random.seed(23) + for _, ts in recipe["ts"].items(): + assert ts.num_mutations > 0 + mut_metadata = pyslim.mutation_metadata(ts) + for _ in range(100): + node = random.randint(0, ts.num_nodes - 1) + pos = random.randint(0, int(ts.sequence_length) - 1) + a = pyslim.nucleotide_at(ts, node, pos) + b = pyslim.nucleotide_at(ts, node, pos, mut_metadata=mut_metadata) + assert a == b @pytest.mark.parametrize("recipe", recipe_eq("mutation_spectrum"), indirect=True) def test_nucleotide_spectrum(self, recipe): @@ -814,6 +983,7 @@ def test_nucleotide_spectrum(self, recipe): # access to the parental genome, so if two adjacent mutations # occur in the same meiosis then each will not know about the other. for _, ts in recipe["ts"].items(): + mut_info = pyslim.mutation_metadata(ts) mutation_spectrum = recipe["mutation_info"] M = { a + b + c + "," + d: 0 @@ -827,7 +997,7 @@ def test_nucleotide_spectrum(self, recipe): pos = ts.site(mut.site).position if pos > 0 and pos < ts.sequence_length - 1: nmuts += 1 - mut_list = mut.metadata["mutation_list"] + mut_list = [mut_info[int(k)] for k in mut.derived_state.split(",")] k = np.argmax([u["slim_time"] for u in mut_list]) derived_nuc = mut_list[k]["nucleotide"] left_nuc = pyslim.nucleotide_at( @@ -847,13 +1017,8 @@ def test_nucleotide_spectrum(self, recipe): ) 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] @@ -869,12 +1034,12 @@ def last_slim_mutations(self, ts): # (slim id, slim mutation metadata) of the slim mutation that is the # *most recent* one of any possibly stacked mutations. Note that it # is possible that this is ambiguous. + mut_info = pyslim.mutation_metadata(ts) for mut in ts.mutations(): slim_muts = { k: v - for k, v in zip( - mut.derived_state.split(","), mut.metadata["mutation_list"] - ) + for k, v in mut_info.items() + if str(k) in mut.derived_state.split(",") } if mut.parent == tskit.NULL: parent_slim_ids = [] @@ -938,10 +1103,7 @@ def scramble_mutations(self, ts): for m in ts.mutations(): a = np.array(m.derived_state.split(",")) ii = rng.permutation(len(a)) - ml = [m.metadata["mutation_list"][i] for i in ii] - t.mutations.append( - m.replace(derived_state=",".join(a[ii]), metadata={"mutation_list": ml}) - ) + t.mutations.append(m.replace(derived_state=",".join(a[ii]))) t.compute_mutation_parents() return t.tree_sequence() @@ -956,6 +1118,7 @@ def test_convert_alleles_errors(self): ts, model=msprime.SLiMMutationModel(type=1), rate=0.1, random_seed=23 ) assert mts.num_mutations > 0 + mts = pyslim.add_mutation_metadata(mts) mtt = mts.dump_tables() mtt.reference_sequence.data = "A" * int(mts.sequence_length) mts = mtt.tree_sequence() @@ -967,6 +1130,7 @@ def test_convert_alleles_errors(self): ) def test_convert_alleles(self, recipe): for _, ts in recipe["ts"].items(): + verify_mutation_metadata(ts) cts = pyslim.convert_alleles(ts) self.verify_converted_nucleotides(ts, cts) @@ -1021,18 +1185,19 @@ def test_generate_nucleotides_errors(self): def verify_generate_nucleotides(self, ts, check_transitions=False): # if check_transitions is True, verify that derived states differ # from parental states - which we try to do but is not guaranteed, - # for instance, if keep=True or in other weird situations. + # for instance, if keep=True, there was more than one mutation in + # single generation, or in other weird situations. assert ts.metadata["SLiM"]["nucleotide_based"] assert len(ts.reference_sequence.data) == ts.sequence_length + mut_info = pyslim.mutation_metadata(ts) muts = {} ts_muts = { j: v["nucleotide"] for j, (_, v) in enumerate(self.last_slim_mutations(ts)) } for mut in ts.mutations(): aa = ts.reference_sequence.data[int(ts.site(mut.site).position)] - for i, md in zip( - mut.derived_state.split(","), mut.metadata["mutation_list"] - ): + for i in mut.derived_state.split(","): + md = mut_info[int(i)] nuc = md["nucleotide"] assert nuc in [0, 1, 2, 3] if i in muts: @@ -1042,9 +1207,14 @@ def verify_generate_nucleotides(self, ts, check_transitions=False): if mut.parent == tskit.NULL: assert pyslim.NUCLEOTIDES[nuc] != aa else: - if ts.mutation(mut.parent).derived_state != mut.derived_state: - assert ts_muts[mut.parent] != ts_muts[mut.id] - + mp = ts.mutation(mut.parent) + if mp.derived_state != mut.derived_state: + assert (ts_muts[mut.parent] != ts_muts[mut.id]) or ( + len(mut.derived_state.split(",")) + > 1 + len(mp.derived_state.split(",")) + ) + + @pytest.mark.parametrize("recipe", recipe_eq(exclude="old_mutations"), indirect=True) def test_generate_nucleotides(self, recipe): for _, ts in recipe["ts"].items(): nts = pyslim.generate_nucleotides(ts, keep=False, seed=5) @@ -1061,8 +1231,10 @@ def test_generate_nucleotides_refseq(self): random_seed=10, ) ts = pyslim.annotate(ts, model_type="nonWF", tick=1) - mts = msprime.sim_mutations( - ts, model=msprime.SLiMMutationModel(type=1), rate=0.5, random_seed=23 + mts = pyslim.add_mutation_metadata( + msprime.sim_mutations( + ts, model=msprime.SLiMMutationModel(type=1), rate=0.5, random_seed=23 + ) ) refseq = "A" * int(mts.sequence_length) nts = pyslim.generate_nucleotides(mts, reference_sequence=refseq, seed=6) @@ -1072,35 +1244,42 @@ 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(ts, model_type="nonWF", tick=1) - mts1 = msprime.sim_mutations( - ts, model=msprime.SLiMMutationModel(type=1), rate=0.1, random_seed=23 + mts1 = pyslim.add_mutation_metadata( + msprime.sim_mutations( + ts, model=msprime.SLiMMutationModel(type=1), rate=0.1, random_seed=23 + ) ) - mts1.dump("out.trees") nts1 = pyslim.generate_nucleotides(mts1, seed=10, keep=False) assert nts1.num_mutations > 0 self.verify_generate_nucleotides(nts1, check_transitions=False) - mts2 = msprime.sim_mutations( - nts1, - model=msprime.SLiMMutationModel( - type=2, - next_id=nts1.num_mutations, - ), - rate=0.1, - random_seed=24, + mut_info1 = { + str(mut["mutation_id"]): mut for mut in nts1.metadata["SLiM_mutation_list"] + } + mts2 = pyslim.add_mutation_metadata( + msprime.sim_mutations( + nts1, + model=msprime.SLiMMutationModel( + type=2, + next_id=nts1.num_mutations, + ), + rate=0.1, + random_seed=24, + ) ) # keep defaults to True nts2 = pyslim.generate_nucleotides(mts2, seed=12) assert nts2.num_mutations > nts1.num_mutations + mut_info2 = { + str(mut["mutation_id"]): mut for mut in nts2.metadata["SLiM_mutation_list"] + } muts1 = {} for mut in nts1.mutations(): - for i, md in zip( - mut.derived_state.split(","), mut.metadata["mutation_list"] - ): + for i in mut.derived_state.split(","): + md = mut_info1[i] muts1[i] = md["nucleotide"] for mut in nts2.mutations(): - for i, md in zip( - mut.derived_state.split(","), mut.metadata["mutation_list"] - ): + for i in mut.derived_state.split(","): + md = mut_info2[i] if md["mutation_type"] == 1: assert i in muts1 assert muts1[i] == md["nucleotide"] @@ -1160,8 +1339,9 @@ def get_vacant_samples(self, ts): def verify_remove_vacant(self, ts, rts): vacant_samples = self.get_vacant_samples(ts) - for node in rts.nodes(): - assert not (pyslim.node_is_vacant(rts, node) and (node.is_sample() == 1)) + vacant = pyslim.nodes_vacant(ts) + for v, node in zip(vacant, rts.nodes()): + assert not (v and (node.is_sample() == 1)) assert (node.id in vacant_samples) == ( node.flags & pyslim.NODE_IS_VACANT_SAMPLE > 0 ) @@ -1262,14 +1442,23 @@ def test_has_vacant_msprime(self): tables.nodes.metadata_schema = pyslim.slim_metadata_schemas["node"] assert not pyslim.has_vacant_samples(tables.tree_sequence()) - def test_node_is_vacant(self, recipe): + def test_nodes_vacant(self, recipe): + np.random.seed(123) num_chromosomes = len(recipe["ts"]) for _, ts in recipe["ts"].items(): + test_nodes = np.random.choice( + np.arange(ts.num_nodes), size=min(1, ts.num_nodes), replace=False + ) k = ts.metadata["SLiM"]["this_chromosome"]["index"] - for node in ts.nodes(): + vacant = pyslim.nodes_vacant(ts) + for pv, node in zip(vacant, ts.nodes()): v = self.vacancy_values(node) isv = v is not None and v[k] - assert isv == pyslim.node_is_vacant(ts, node) + assert pv == isv + if node.id in test_nodes: + # node_is_vacant is kinda slow so don't test all of them + with pytest.warns(FutureWarning, match="deprecated"): + assert isv == pyslim.node_is_vacant(ts, node) for j in range(num_chromosomes, len(v)): assert not v[j] @@ -1394,6 +1583,16 @@ def test_no_change(self, restart_name, recipe, helper_functions, tmp_path): ) for chrom, ts in recipe["ts"].items(): self.verify_reset(ts, out_ts[chrom]) + # again!! + out_out_ts = helper_functions.run_slim_restart( + out_ts, + restart_name, + tmp_path, + "multichrom" in recipe, + WF="WF" in recipe, + ) + for chrom, ts in recipe["ts"].items(): + self.verify_reset(ts, out_out_ts[chrom]) @pytest.mark.parametrize( "restart_name, recipe", restarted_recipe_eq("no_op"), indirect=["recipe"] @@ -1425,16 +1624,20 @@ def test_set_individuals( ): in_ts = {} ts = list(recipe["ts"].values())[0] + tsmd = ts.metadata assert ( - "user_metadata" in ts.metadata["SLiM"] - and "reset_tick" in ts.metadata["SLiM"]["user_metadata"] + "user_metadata" in tsmd["SLiM"] + and "reset_tick" in tsmd["SLiM"]["user_metadata"] ), "Simulation not set up for this test." - reset_tick = ts.metadata["SLiM"]["user_metadata"]["reset_tick"][0] + reset_tick = tsmd["SLiM"]["user_metadata"]["reset_tick"][0] if time is None: - for time in range(ts.metadata["SLiM"]["tick"] + 1): - if pyslim.slim_time(ts, time) == reset_tick: - break - individuals = pyslim.individuals_alive_at(ts, time)[:num_indivs] + pytimes = run_with_ts_metadata( + pyslim.slim_time, tsmd, ts, np.arange(tsmd["SLiM"]["tick"] + 1) + ) + time = np.searchsorted(pytimes, reset_tick) + individuals = pyslim.individuals_alive_at(ts, time, ts_metadata=tsmd)[ + :num_indivs + ] for chrom, ts in recipe["ts"].items(): in_ts[chrom] = pyslim.set_slim_state(ts, time=time, individuals=individuals) out_ts = helper_functions.run_slim_restart( diff --git a/uv.lock b/uv.lock index 68ae0211..fbf98b33 100644 --- a/uv.lock +++ b/uv.lock @@ -645,6 +645,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/c7/4e/ce75a57ff3aebf6fc1f4e9d508b8e5810618a33d900ad6c19eb30b290b97/fonttools-4.61.1-py3-none-any.whl", hash = "sha256:17d2bf5d541add43822bcf0c43d7d847b160c9bb01d15d5007d84e2217aaa371", size = 1148996, upload-time = "2025-12-12T17:31:21.03Z" }, ] +[[package]] +name = "frozendict" +version = "2.4.7" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/90/b2/2a3d1374b7780999d3184e171e25439a8358c47b481f68be883c14086b4c/frozendict-2.4.7.tar.gz", hash = "sha256:e478fb2a1391a56c8a6e10cc97c4a9002b410ecd1ac28c18d780661762e271bd", size = 317082, upload-time = "2025-11-11T22:40:14.251Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/38/74/f94141b38a51a553efef7f510fc213894161ae49b88bffd037f8d2a7cb2f/frozendict-2.4.7-py3-none-any.whl", hash = "sha256:972af65924ea25cf5b4d9326d549e69a9a4918d8a76a9d3a7cd174d98b237550", size = 16264, upload-time = "2025-11-11T22:40:12.836Z" }, +] + [[package]] name = "greenlet" version = "3.3.2" @@ -1844,6 +1853,7 @@ dependencies = [ [package.dev-dependencies] dev = [ { name = "filelock" }, + { name = "frozendict" }, { name = "jupyter-book" }, { name = "matplotlib" }, { name = "msprime" }, @@ -1879,6 +1889,7 @@ packaging = [ ] test = [ { name = "filelock" }, + { name = "frozendict" }, { name = "msprime" }, { name = "pandas" }, { name = "pytest" }, @@ -1891,12 +1902,13 @@ test = [ requires-dist = [ { name = "msprime", specifier = ">=1.0.1" }, { name = "numpy" }, - { name = "tskit" }, + { name = "tskit", specifier = ">=1.0.3" }, ] [package.metadata.requires-dev] dev = [ { name = "filelock" }, + { name = "frozendict" }, { name = "jupyter-book", specifier = "<2" }, { name = "matplotlib" }, { name = "msprime" }, @@ -1932,6 +1944,7 @@ packaging = [ ] test = [ { name = "filelock" }, + { name = "frozendict" }, { name = "msprime" }, { name = "pandas" }, { name = "pytest" }, @@ -2798,23 +2811,38 @@ wheels = [ [[package]] name = "tskit" -version = "0.6.4" +version = "1.0.3" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "jsonschema" }, { name = "numpy" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/94/95/2c2d8bdaae4a3948181de68d1fac0569d9c937a42f7ccfcf097f9a428721/tskit-0.6.4.tar.gz", hash = "sha256:bdac1bb7e3ae3d1f562ec191b5d840156e082dd2adc6af7c41b170c4fb1be792", size = 874772, upload-time = "2025-05-21T18:18:18.343Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/ed/28/547acef423709fad5b70bbc68332c1fa0a4c1887ec4e76cb93a433a95bbb/tskit-0.6.4-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:510fd219f2c6d5d669e178a3ea1e4cd60d7fc0b40a50fae4859a6a8f35d2394a", size = 748024, upload-time = "2025-05-21T18:17:54.919Z" }, - { url = "https://files.pythonhosted.org/packages/88/0d/5816ee9ac9708f2e1def387e36b2d0763bdea2267db3ff4a55c17f079e85/tskit-0.6.4-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4b631d38c352b618c3ede33515cf97ffcbc24e3a4252b6979a80b1770434a534", size = 1322885, upload-time = "2025-05-21T18:17:56.958Z" }, - { url = "https://files.pythonhosted.org/packages/bd/db/b98964916b3f9c603f8ad92045db0c050ca4699a976051acdc1368085c3c/tskit-0.6.4-cp311-cp311-win_amd64.whl", hash = "sha256:e121226092816a1e36b2835aa0348e652c09bdf0604a936f63c4fd0d64ca6422", size = 470677, upload-time = "2025-05-21T18:17:58.584Z" }, - { url = "https://files.pythonhosted.org/packages/b4/cf/d2a1c6a6ad29b16310b60bf08dd5b1d1e4e0d23819c49284662373c9fc54/tskit-0.6.4-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:8fd5a0c94f302f5fe69a9f7a662b8ddf8219e4dc7325a1990b279afd084bb649", size = 749765, upload-time = "2025-05-21T18:18:00.234Z" }, - { url = "https://files.pythonhosted.org/packages/80/46/1630514e8a9f97a8f75e805a7872fb3083119d7e421309e05e99d5f5ca66/tskit-0.6.4-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f086e648a624004343882ca57f9365d5005cc2e472113b61874c9403c14be272", size = 1324515, upload-time = "2025-05-21T18:18:02.461Z" }, - { url = "https://files.pythonhosted.org/packages/07/44/2116904f37ffe1db0e675b4c70b70cdaf7ab38b474cd546f6ff9d46de4b9/tskit-0.6.4-cp312-cp312-win_amd64.whl", hash = "sha256:507eee5b20c5e47202d90a70143c601aef3b3ec70321b5251c2e6d896b1e0722", size = 470047, upload-time = "2025-05-21T18:18:04.185Z" }, - { url = "https://files.pythonhosted.org/packages/c5/d7/5ee3b118281f2a7032f5d38fca0ee7e11bf7cf1823d2dbfa5a991ccf3497/tskit-0.6.4-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:66f07b76f18ad576a7585b6e4d9e46d58994e3df1dd2d1808d32d294354695cf", size = 749773, upload-time = "2025-05-21T18:18:06.48Z" }, - { url = "https://files.pythonhosted.org/packages/f2/6d/6368c2ffbccb4d7ee8d82096f1ad0b3cb0d639b958c408589cceff09600e/tskit-0.6.4-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bfb40717382cf47b8844c1f73d73c1e7ca7f63e6a506e7f859f0980667677de2", size = 1324529, upload-time = "2025-05-21T18:18:08.674Z" }, - { url = "https://files.pythonhosted.org/packages/ee/b9/5b092c5b409cad0005b560622b34f9fd38fe0945939974671306fedef30a/tskit-0.6.4-cp313-cp313-win_amd64.whl", hash = "sha256:c6b1f1b22e5d55a906ee33bca33447500df0cf5b12ec20926773ca1e3c3c8931", size = 470097, upload-time = "2025-05-21T18:18:11.022Z" }, +sdist = { url = "https://files.pythonhosted.org/packages/22/f5/c23333b3ffd86cbe79520bbee85f050f815c16a625b014dbacbe486fb6c2/tskit-1.0.3.tar.gz", hash = "sha256:8a305b3bcf4145688be17962f6ea2ac20aa65db440ed0c02b49f9b4a3c00836b", size = 933435, upload-time = "2026-05-14T18:20:16.89Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f6/6c/7fdf85e62204319d77da52d2647e1e65c6dc94660a27d7f202fd134f42c9/tskit-1.0.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:a5d5f5e0d0ad53ac554563c76b91c5f2bb721d021b7c761606a9a72fe1e49d74", size = 526356, upload-time = "2026-05-14T18:19:40.482Z" }, + { url = "https://files.pythonhosted.org/packages/3b/ce/4ff9ba98eca28992250fa8eab25db5bc4a3c7b3af4ad46652f7d1b5b22c3/tskit-1.0.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:af978aa415f04fbabd613ad2933526110b4767e8e64f319ec03cefca9e2242bd", size = 496008, upload-time = "2026-05-14T18:19:42.332Z" }, + { url = "https://files.pythonhosted.org/packages/2f/3d/d4d8567a2bca51df46f0c7ad706678b106ce2ba094d8584d8829a24451c0/tskit-1.0.3-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f509b7b271dd80a68ee3f6bfdff614e3b8ac989dcc44dd8d36c25c8181443b79", size = 1395725, upload-time = "2026-05-14T18:19:43.83Z" }, + { url = "https://files.pythonhosted.org/packages/4c/f3/d5ce78eeeda65c4673966d00567f4166b9500843ce953c0dad927a2200c3/tskit-1.0.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:0722a4c1fc9c287c96a2141ae3587df6defed2a049cca7ab90479958c613835f", size = 1377087, upload-time = "2026-05-14T18:19:45.238Z" }, + { url = "https://files.pythonhosted.org/packages/f0/7c/9e53fb9f71bf89eb8327cb2a859d4c9764dc447a42d3407f7ec7c7a25fb8/tskit-1.0.3-cp311-cp311-win32.whl", hash = "sha256:3901c9fc02497e2c7e0ee5ca36ce5b65749f9c630e5ace1daa990963d8f5a63d", size = 449670, upload-time = "2026-05-14T18:19:46.641Z" }, + { url = "https://files.pythonhosted.org/packages/72/88/a80beb0adfb8ceade30b1913010c56e6bd4f837ef2f75aeb76222377d148/tskit-1.0.3-cp311-cp311-win_amd64.whl", hash = "sha256:0d374768d422e941f8ad6ffdba85d6ca1585467532f6f4945b171f18d6613285", size = 496418, upload-time = "2026-05-14T18:19:47.948Z" }, + { url = "https://files.pythonhosted.org/packages/56/1f/d6726034289911b326e74b8b5339c9d1ec6fe46bf203253ae8d90b146a89/tskit-1.0.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:3d035946888c9da5eefb93ca99c2ae3ad2cb7cb64bc6037588fe285f775ee67a", size = 528485, upload-time = "2026-05-14T18:19:49.299Z" }, + { url = "https://files.pythonhosted.org/packages/ad/ed/7fa3f43bc0d7569b75bc6a5c03bc14bd2e9de12a3f6a2f8b3251d505226a/tskit-1.0.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:31aee6a8b6d9a0bdae06f0b186fb7aad54d53f34050c0ee72a6caa560506950f", size = 496860, upload-time = "2026-05-14T18:19:50.716Z" }, + { url = "https://files.pythonhosted.org/packages/28/b1/fb828d0479be0f178e9f33b4483003a421b069e6331dc5f35066dc3029e4/tskit-1.0.3-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:fb07ead500c7e88c69677469b4f489c8997ad944103102b04d724c855b4b980b", size = 1398919, upload-time = "2026-05-14T18:19:52.1Z" }, + { url = "https://files.pythonhosted.org/packages/8b/3e/1c7adaae07a00ce009e692a6d171c7a0882d1ed9753017b78a7db7ae7d72/tskit-1.0.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:241192b12d310e49b21bfae524c76768a15d0ed3f7b950d6ee09dce1528b8973", size = 1378470, upload-time = "2026-05-14T18:19:53.881Z" }, + { url = "https://files.pythonhosted.org/packages/71/a1/10d12e9b7908775cf97ad9e6c3d94b4b970940c74a161cc9aaf540fa83c0/tskit-1.0.3-cp312-cp312-win32.whl", hash = "sha256:3e11c9fe328b27c0e4a3ff64a0fed972d2cd48a743c03c8bc339352664322ee8", size = 450010, upload-time = "2026-05-14T18:19:55.419Z" }, + { url = "https://files.pythonhosted.org/packages/2c/b7/d1842bec89cd4993958165d6da774e85ad79b9426baca571c39457a1b6b5/tskit-1.0.3-cp312-cp312-win_amd64.whl", hash = "sha256:66c83c7f971bc160fd29ca6169e1ee31023a5283ca88936df3eed1e568eee209", size = 496000, upload-time = "2026-05-14T18:19:56.75Z" }, + { url = "https://files.pythonhosted.org/packages/e6/10/aa8cc65060669b911946649cd1802f29640998c5b6ca78c4c845fb3c473d/tskit-1.0.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:d52e2ad0eb79e3b21da8a236d0f5eb888f9b18569e48f4b84a3875523e80ee22", size = 528459, upload-time = "2026-05-14T18:19:58.156Z" }, + { url = "https://files.pythonhosted.org/packages/eb/42/34d981123a335d80e3030e9b0a81a41f654c9cf1c5db0ac630d6b20d8bfd/tskit-1.0.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:5bd1744a020a518e5835b85971fea14ca827d76f778650eb37e3f44c6db0a711", size = 496838, upload-time = "2026-05-14T18:19:59.909Z" }, + { url = "https://files.pythonhosted.org/packages/65/9c/09b072c01b8e58a3137011851d4761daf957f2f86447bc2a2c25e0d5aa4a/tskit-1.0.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:405c20fad5256edd6abd1f8b514c5bc653d808102f509c4512c9b174507dbe84", size = 1399080, upload-time = "2026-05-14T18:20:01.353Z" }, + { url = "https://files.pythonhosted.org/packages/d0/bb/789b91f7a2deca4d50f649b685e2b3dca4b3358e36bf7a5b2b84709bb8d5/tskit-1.0.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:bc1c97f00ce8e79460aa9ff3b36987daf70f2dc267e36a32db77018dbefc822b", size = 1378631, upload-time = "2026-05-14T18:20:03.273Z" }, + { url = "https://files.pythonhosted.org/packages/c7/8b/3b2b19246b2ba3159456e4068887e505d7a2ec857ecd7ae7606d6c129fda/tskit-1.0.3-cp313-cp313-win32.whl", hash = "sha256:53444ef3b2fd7ea9ca863e562c33402e46a55e645887dd4d983686b607e3f788", size = 449996, upload-time = "2026-05-14T18:20:04.788Z" }, + { url = "https://files.pythonhosted.org/packages/23/fc/53ef7ad8b3f2183584af81b0ff046a660c3164530a1f768770dbd4d60cb7/tskit-1.0.3-cp313-cp313-win_amd64.whl", hash = "sha256:721ae7f02730ed91233a6da2a9d4b59632d1512877cd0d6fe948ea5d9ddc17e9", size = 496006, upload-time = "2026-05-14T18:20:06.577Z" }, + { url = "https://files.pythonhosted.org/packages/66/b9/91194216907ebf278d7a58288f3e64881e11a0eedcca7125a49e0bbdafdd/tskit-1.0.3-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:62a45d5e57f5a9181eb113c0dd478783ef3c4829856c0831ea2eba2cdf00a8de", size = 528594, upload-time = "2026-05-14T18:20:08.032Z" }, + { url = "https://files.pythonhosted.org/packages/6e/2c/0a888f4f140922c7a3945dbca5e444fa8052a620c784359673fe9eb1eda7/tskit-1.0.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:8df716837129652fc82d095d80fd4e23a728213a4b18de9233f3aebb073b60b4", size = 496866, upload-time = "2026-05-14T18:20:09.34Z" }, + { url = "https://files.pythonhosted.org/packages/2f/ce/56aa69b768734ede0b8b36c5ea2ce842d50c786b74cfefdfc036a1fd29da/tskit-1.0.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:151eefbbae9a339f3ea64980937fa89d6ac9fb371dba880a9b30791a461753ad", size = 1397616, upload-time = "2026-05-14T18:20:10.762Z" }, + { url = "https://files.pythonhosted.org/packages/6e/5a/b05eab4a4a32bce6a51f991033ed460a836aafc1f406eebc624831558d9b/tskit-1.0.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:2170cacd8ade47fd8f1510aca78549e09e9ba875c25496f0ccfb755e09c0b06b", size = 1377477, upload-time = "2026-05-14T18:20:12.649Z" }, + { url = "https://files.pythonhosted.org/packages/ea/c5/116419617468dc06428e507001d0a7b212116e9e8d336bd8c05a6531b2a0/tskit-1.0.3-cp314-cp314-win32.whl", hash = "sha256:24f8fffc1e1dd4154654e91bb7bac22078b3cadd9837e5c490225aa5c313b819", size = 455586, upload-time = "2026-05-14T18:20:14.165Z" }, + { url = "https://files.pythonhosted.org/packages/6e/7f/61818ed922629178abb07e45439d0af3bb5867b629fe392e9864df58ce5c/tskit-1.0.3-cp314-cp314-win_amd64.whl", hash = "sha256:cbe791757cbd4d060a9c06c270bbcacc932f60ce1d77e98dcea35c931d0b0aa9", size = 505409, upload-time = "2026-05-14T18:20:15.527Z" }, ] [[package]]