diff --git a/CHANGELOG.rst b/CHANGELOG.rst index a31bb70..a429a44 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -40,6 +40,10 @@ 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. +- Since verison 1.1, the value of `pyslim.INDIVIDUAL_FLAG_MIGRATED` has been 2, + when in fact it should have been 1, so code using this flag to detect + migrants would have been wrong (and should have found no migrants, ever). + **New features**: - SLiM now includes in metadata information about the effects of mutations on diff --git a/docs/.gitignore b/docs/.gitignore index ae606f7..57d31a2 100644 --- a/docs/.gitignore +++ b/docs/.gitignore @@ -4,3 +4,4 @@ spatial_sim_genotypes.vcf example_snps.vcf sims.make example_sim.vcf +.ipynb_checkpoints diff --git a/docs/_config.yml b/docs/_config.yml index 4b0c8d5..3453b49 100644 --- a/docs/_config.yml +++ b/docs/_config.yml @@ -3,7 +3,7 @@ title: PySLiM manual author: Tskit Developers -copyright: "2021" +copyright: "2026" only_build_toc_files: true # logo: logo.png favicon: favicon.ico @@ -55,3 +55,4 @@ sphinx: myst_enable_extensions: - colon_fence - deflist + nb_merge_streams: True diff --git a/docs/_static/recomb_rates.tsv b/docs/_static/recomb_rates.tsv index ca4b1b7..a6f2d48 100644 --- a/docs/_static/recomb_rates.tsv +++ b/docs/_static/recomb_rates.tsv @@ -1,5 +1,6 @@ -end_position rate(cM/Mb) -15000000 3.2 -50000000 2.5 -85000000 0.25 -99999999 2.8 +position rate(cM/Mb) +0 3.2 +15000000 2.5 +50000000 0.25 +85000000 2.8 +100000000 0 diff --git a/docs/installation.md b/docs/installation.md index becb39d..0dbaab4 100644 --- a/docs/installation.md +++ b/docs/installation.md @@ -38,4 +38,4 @@ If you find a bug in ``pyslim`` or want to suggest an improvement, please If you have a question about using tree sequences, please ask it at [the tskit discussion page](https://github.com/tskit-dev/tskit/discussions). Finally, questions about SLiM should be directed to -[the SLiM mailing list](https://groups.google.com/forum/#!forum/slim-discuss). +[the slim-discuss mailing list](https://groups.google.com/forum/#!forum/slim-discuss). diff --git a/docs/introduction.md b/docs/introduction.md index 25d23d2..af3ac68 100644 --- a/docs/introduction.md +++ b/docs/introduction.md @@ -16,7 +16,8 @@ kernelspec: # Introduction This is the documentation for pyslim, a Python API -for reading and modifying {ref}`tskit` tree sequence files +for reading and modifying {ref}`tskit` +[tree sequence](https://tskit.dev/learn/) files produced by [SLiM](https://messerlab.org/slim/), or modifying files produced by other programs (e.g., {ref}`msprime`, @@ -25,13 +26,24 @@ and [tsinfer](https://tsinfer.readthedocs.io/)) for use in SLiM. SLiM can read and write *tree sequences*, which store genetic genealogies for entire populations. These can be used to efficiently encode both the state of the -population at various points during a simulation *as well as* the complete genomic +population at various times during a simulation *as well as* the complete genomic ancestry. Furthermore, SLiM can "load" a saved tree sequence file to recreate the exact state of the population at the time it was saved. -To do this, SLiM stores some additional information in the basic -tree sequence file. +To do this, SLiM stores some additional information (as {ref}`metadata`) +in the basic tree sequence file. First, you probably want to read the {ref}`sec_overview` for a description of what (and *who*) SLiM records in the tree sequence. Next, the {ref}`sec_tutorial` walks through the common workflows. + +(sec_citation)= + +# Citation + +If you use pyslim, please cite: + +> *Bridging forward-in-time and coalescent simulations using pyslim*, by +> Shyamalika Gopalan, Murillo F. Rodrigues, Peter L. Ralph, and Benjamin C. Haller +> [bioRxiv:2025.09.30.679676](https://doi.org/10.1101/2025.09.30.679676). + diff --git a/docs/metadata.md b/docs/metadata.md index 3d8962e..9f6ff4b 100644 --- a/docs/metadata.md +++ b/docs/metadata.md @@ -66,13 +66,13 @@ and `ts.metadata["SLiM"]` contains information about the simulation: - `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 + * `type`: `"additive"`, `"multiplicative"`, or `"logistic"` * `baselineOffsetFromUser`: a value added to all traits * `baselineOffsetFromSubstitutions`: the total effect of all substitutions on the traits at the time the tree sequence was saved * `baselineAccumulation`: 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 + * `individualOffsetMean`, `individualOffsetSD`: parameters governing the distribution of individual-level offsets (i.e., "environment" effects) **Populations:** @@ -83,7 +83,7 @@ in a nonspatial simulation). The metadata may be `None` for populations that SLiM did not use. The keys that SLiM uses are: - `slim_id`: the ID of this population in SLiM -- `name`: the name of the population (by default, `p0`, `p1`, etcetera) +- `name`: the name of the population (by default, `"p0"`, `"p1"`, etcetera) - `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 @@ -95,7 +95,7 @@ Each individual produced by SLiM contains the following metadata: - `pedigree_id`: the "pedigree ID", unique within the SLiM simulation - `pedigree_p1`, `pedigree_p2`: the pedigree IDs of the individuals' two parents (they may be equal in the case of selfing, or `-1` to indicate no - parent, in the case of the initial generation or for cloning) + parent, in the case of the initial generation) - `age`: the `.age` property within SLiM at the time the file was written out - `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`) @@ -106,21 +106,21 @@ Each individual produced by SLiM contains the following metadata: 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 + * `phenotype`: the trait value (which is `NAN` if the phenotype has not been calculated, or was invalidated) * `offset`: the individual's offset (i.e., the "environmental effect") **Nodes:** -Each "node" produced by SLiM (i.e., "genome" within SLiM) has: +Each "node" produced by SLiM (i.e., "haplosome" within SLiM) has: -- `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 +- `slim_id`: the unique ID associated with the haplosome by SLiM +- `is_vacant`: records that the node is a "vacant" node (in which case it isn't really there, so shouldn't have any mutations or relationships in the tree sequence!) - see [](sec_overview_vacant_nodes) for more explanation **Mutations:** 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 +Each entry contains: - `mutation_id`: the numeric ID of mutation in SLiM - `mutation_type`: the numeric ID of the `MutationType` within SLiM @@ -231,8 +231,10 @@ md["SLiM"]["model_type"] = "nonWF" tables.metadata = md ``` Modifying the top-level metadata -could be used to set spatial bounds on an annotated msprime simulation, for instance. -(This is recorded in the population metadata.) +could be used to declare that an annotated msprime simulation +is to be a spatial simulation in SLiM, +by changing the "dimensionality" property in top-level metadata +(and perhaps also the bounds in population metadata as well). ### Modifying SLiM metadata in tables @@ -254,7 +256,7 @@ tables.individuals.clear() for ind in ts.individuals(): md = ind.metadata md["age"] = random.choice([1,2,3,4]) - _ = tables.individuals.append( + tables.individuals.append( ind.replace(metadata=md) ) @@ -273,8 +275,11 @@ mod_ts.dump("modified_ts.trees") ### Metadata entries -SLiM records additional information in the metadata columns of Individual, Node, and Mutation tables, +SLiM records additional information in the metadata columns of Individual and Node, and Mutation tables, +and about mutations in top-level metadata, in a binary format using the python ``struct`` module. +However, this is transparently taken care of by tskit's metadata module, +so the user does not have to work directly with binary information or the struct module itself. See {ref}`tskit's metadata documentation ` for details on how this works. Nothing besides this binary information can be stored in the metadata of these tables if the tree sequence is to be used by SLiM, diff --git a/docs/overview.md b/docs/overview.md index 1a424df..ea7eb42 100644 --- a/docs/overview.md +++ b/docs/overview.md @@ -16,7 +16,7 @@ kernelspec: # Overview A tree sequence is a way of storing both the full genetic history and the genotypes -of a bunch of genomes. +of a potentially large collection of genome sequences. See {ref}`the tskit documentation` for more description of the tree sequence and underlying data structure, and definitions of the important terms. @@ -25,10 +25,10 @@ and the "focal" nodes are called *sample nodes* or simply *samples*. Many operations on tree sequences act on the sample nodes by default (see the {ref}`tskit data model` for more on this topic), and the tree sequence always describes the genealogy of the -entire genome of all the samples, at at least over the simulated time period. +entire genome of all the samples, at least over the simulated time period. (Other nodes in the tree sequence represent ancestral genomes about which we might have only partial information). -SLiM simulates diploid organisms, so each *individual* usually has two nodes; +For simulated diploid organisms, each *individual* has two nodes, so many operations you might want to do involve first finding the individuals you want, and then looking at their nodes. @@ -39,8 +39,10 @@ and then looking at their nodes. Suppose we've run a very small simulation with SLiM. The genetic relationships between the various diploid individuals who were alive over the course of the simulation might look something like the picture on the left below. Note that individuals (circles) are -diploid, so that each contains two chromosomes or *nodes* (shaded rectangles), +diploid, so that each contains two copies of the chromosome or *nodes* (shaded rectangles), and that relationships are between the *nodes*, not the individuals. +(The term SLiM uses for each of these copies-of-chromosomes is "haplosome", +a term we'll continue to use below.) At the end of the simulation we are typically only interested in the genetic relationships between the nodes in those individuals which are still alive; @@ -57,7 +59,10 @@ name: pedigree01 --- A conceptual diagram of **(A)** relationships between chromosomes of diploid individuals in a SLiM simulation, and -**(B)** which information is returned in the tree sequence. +**(B)** which information is retained in the tree sequence. +Only pedigree relationships are shown, not genetic relationships: +the diagram does not show which portions of genetic material were inherited +along each relationship. ``` (sec_left_in_tree_sequence)= @@ -90,18 +95,25 @@ which individuals they were in (the tutorial explains ways to [retain this information](sec_retaining_individuals). As well as the historical individuals, many historical *nodes* have been removed too, -along with with their genealogical relationships (i.e. the lines, which in +along with with their genealogical relationships (i.e., the lines, which in tree-sequence-speak are known as "edges"). The deleted nodes are simply those that are not needed to reconstruct the relationships between the sample nodes. For example, we -remove nodes leading to a dead end (e.g. in individuals who had no offspring). Similarly, -as time goes on, recombination events in conjunction with genetic drift can gradually -reduce the genetic contribution of parts of older genomes to the current generation. +remove nodes leading to a dead end (e.g., in individuals who had no offspring). +Similarly, as time goes on, recombination events in conjunction with genetic drift can +gradually reduce the genetic contribution of parts of older genomes to the current +generation. The generated tree sequence therefore need not contain historical nodes whose genetic -contribution to the samples has been whittled down to zero. Finally, to -reconstruct relationships between samples, strictly we only need to keep a node if it -represents the genetic *most recent common ancestor* (MRCA) of at least two samples. So -by default, we also remove historical nodes that are only "on the line to" a sample, but -do not represent a branching point (i.e. coalescent event) on the tree. +contribution to the samples has been whittled down to zero. +Finally, to reconstruct relationships between samples, we only need to keep a node if it +represents the genetic *most recent common ancestor* (MRCA) of at least two samples +at some point on the genome. +We will also need to keep nodes in the founding generation +to describe relationships of the samples to this initial time. +So by default, SLiM also removes historical nodes that are only "on the line to" a sample, +but do not represent a either a branching point (i.e. coalescent event) or a root +of a the marginal tree describing genetic relationships at some point on the genome. +(See [Kelleher et al 2018](https://doi.org/10.1371/journal.pcbi.1006581) +for a description of what relationships, exactly, are retained.) ## What else can I find out from the tree sequence? @@ -115,9 +127,8 @@ Most of this is stored as *metadata*: see [](sec_metadata). ## Vacant nodes: sex chromosomes and haploidy -Under the hood in SLiM, all individuals are diploid, -and so for each individual there are two *nodes*, -representing their two haplosomes +Under the hood, SLiM reserves two *nodes* for each individual +to represent their haplosomes (i.e., chromosome copies; see the SLiM manual). So, any individual that is *not* diploid for whichever chromosome the tree sequence represents will have in the tree sequence @@ -166,9 +177,10 @@ in all the tree sequences in a trees archive are identical. This is quite handy if you're going to analyze data from more than one chromosome, but also creates some potential pitfalls. -First, there will probably be nodes in the tree sequence -that are not represented in *any* of the trees. -This is simply because these are nodes that are needed for other chromosomes: +First, in one particular tree sequence of a trees archive, +there will probably be nodes that are not represented in *any* of the trees. +This is simply because these are nodes that are referenced by +the tree sequence for other chromosomes: perhaps the node in question is not ancestral to any of the samples on this chromosome, but it is on other chromosomes. The presence of these nodes is harmless, however: it will not affect @@ -193,7 +205,7 @@ if both were from the same multichromosome simulation, and vice-versa. Finally, this explains why vacant nodes must be included in the tree sequence: -in the presence of autosomes, every diploid individual needs two nodes, +in the presence of diploid autosomes, every diploid individual is allocated two nodes, but for an individual that has less than two copies of a given chromosome, some of those nodes will not represent an actual chromosome copy. Those are "vacant", as described above. diff --git a/docs/phenotypes.slim b/docs/phenotypes.slim new file mode 100644 index 0000000..0624047 --- /dev/null +++ b/docs/phenotypes.slim @@ -0,0 +1,66 @@ +initialize() { + setSeed(123); + initializeTreeSeq(); + defineConstant("START1", -5.0); + defineConstant("START2", 5.0); + defineConstant("OPT1", 20.0); + defineConstant("OPT2", -20.0); + initializeTrait("trait1", "additive", baselineOffset=START1, individualOffsetSD=1.0); + initializeTrait("trait2", "additive", baselineOffset=START2, individualOffsetSD=1.0); + + initializeMutationType("m1", NAN, "f", 0.0); // neutral + initializeMutationType("m2", NAN, "f", 0.0); // QTLs + m2.color = "red"; + m2.logMutationData(enable=T, effectSize=T); + m2.convertToSubstitution = F; // necessary to disentagle from baseline offset + + // g1 is a neutral region, g2 is a QTL + initializeGenomicElementType("g1", m1, 1.0); + initializeGenomicElementType("g2", c(m1,m2), c(1.0, 0.1)); + + // chromosome of length 100 kb with two QTL regions + initializeGenomicElement(g1, 0, 39999); + initializeGenomicElement(g2, 40000, 49999); + initializeGenomicElement(g1, 50000, 79999); + initializeGenomicElement(g2, 80000, 89999); + initializeGenomicElement(g1, 90000, 99999); + initializeRecombinationRate(1e-8); + initializeMutationRate(1e-7); + + // QTL-related constants used below + defineConstant("QTL_mu", c(0.0, 0.0)); + defineConstant("QTL_cov", 0.25); + defineConstant("QTL_sigma", matrix(c(1,QTL_cov,QTL_cov,1), nrow=2)); + } + +1 late() { + sim.addSubpop("p1", 500); +} + +mutation(m2) { + // draw mutational effects for the new m2 mutation + effects = rmvnorm(1, QTL_mu, QTL_sigma); + mut.setEffectSizeForTrait(NULL, effects); + return T; +} + +late() { + sim.demandPhenotype(NULL, NULL); + + inds = sim.subpopulations.individuals; + effects1 = 1.0 + dnorm(inds.trait1, OPT1, 15.0) * 10.0; + effects2 = 1.0 + dnorm(inds.trait2, OPT2, 15.0) * 10.0; + inds.fitnessScaling = effects1 * effects2; +} + +1:1000000 late() { + if (sim.cycle % 5000 == 0) { + inds = p1.individuals; + sim.treeSeqRememberIndividuals(inds); + // keep running until we get close to both optima + if ((abs(mean(inds.trait1) - OPT1) <= 1.0) & (abs(mean(inds.trait2) - OPT2) <= 1.0)) { + sim.treeSeqOutput("phenotypes.trees", metadata=Dictionary("all_mutations", m2.loggedData("values"))); + sim.simulationFinished(); + } + } +} diff --git a/docs/phenotypes2.slim b/docs/phenotypes2.slim new file mode 100644 index 0000000..bc22c48 --- /dev/null +++ b/docs/phenotypes2.slim @@ -0,0 +1,24 @@ +initialize() { + setSeed(123); + initializeTreeSeq(); + initializeTrait("mult", "multiplicative", individualOffsetSD=1.0); + initializeTrait("logistic", "logistic", individualOffsetSD=1.0); + + initializeMutationType("m1", NAN, "n", 0.0, 0.01); + m1.convertToSubstitution = F; // necessary to disentagle from baseline offset + + initializeGenomicElementType("g1", m1, 1.0); + + initializeGenomicElement(g1, 0, 9999999); + initializeRecombinationRate(1e-8); + initializeMutationRate(1e-7); +} + +1 late() { + sim.addSubpop("p1", 100); +} + +1000 late() { + sim.demandPhenotype(NULL, NULL); + sim.treeSeqOutput("phenotypes2.trees"); +} diff --git a/docs/previous_versions.md b/docs/previous_versions.md index ce067fa..c1b3e90 100644 --- a/docs/previous_versions.md +++ b/docs/previous_versions.md @@ -51,7 +51,6 @@ In particular: {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( @@ -74,16 +73,13 @@ In brief, if `mut` is a mutation, then you should replace 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) @@ -92,7 +88,6 @@ for k in mut.derived_state.split(","): 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 diff --git a/docs/python_api.md b/docs/python_api.md index 51e96d2..701800e 100644 --- a/docs/python_api.md +++ b/docs/python_api.md @@ -37,25 +37,25 @@ Here is a quick reference to some of the methods: ```{eval-rst} .. autosummary:: - recapitate - mutation_metadata - annotate - individuals_alive_at - individual_ages - individual_ages_at - remove_vacant - restore_vacant - has_vacant_samples - node_is_vacant - slim_time - next_slim_mutation_id add_mutation_metadata add_mutation_metadata_tables + annotate convert_alleles + default_slim_metadata generate_nucleotides + has_vacant_samples + individual_ages + individual_ages_at + individuals_alive_at + mutation_metadata + next_slim_mutation_id + nodes_vacant population_size + recapitate + remove_vacant + restore_vacant set_slim_state - default_slim_metadata + slim_time update ``` diff --git a/docs/time_units.md b/docs/time_units.md index 419651b..a3518e0 100644 --- a/docs/time_units.md +++ b/docs/time_units.md @@ -26,21 +26,33 @@ ts = tskit.load("example_sim.trees") .. currentmodule:: pyslim ``` +(sec_time_units)= + # Time units There are a number of subtle ways that time units can trip up the user; below are explanations of how to deal with most of these topics. At issue is the fact that tree sequences produced by SLiM have times in units of "ticks", rather than generations. -If we query such a tree sequence for its time units we see -its return value +Sometimes these are the same, and sometimes they are not. + +SLiM sets the `time_unit` property of a tree sequence by default to "ticks": ```{code-cell} ts.time_units ``` - -In general this mean it can be conceptually tricky to make sure that time units +This "time unit" can be set with ``initializeTreeSeq(timeUnit="generations");``, +but you should only do this if in fact you are sure that one tick = one generation +(e.g., in a WF model), +and SLiM does no checking whether this is true. +This tutorial is not about this label, it is about matching up population genetics quantities +(which are often in units of generations) +with the ticks of SLiM time (which is more like calendar time). + +In general it can be conceptually tricky to make sure that time units are consistent across different stages of simulation. +(sec_time_units_warning)= + ## That warning from recapitate about time units If you recapitate a tree sequence then you've probably seen the warning @@ -58,6 +70,7 @@ However, this *only* affects the *label* in metadata (i.e., the output of in the tree sequence, so please only do this if you are running a WF simulation (and, keep reading). +(sec_time_units_mutation_rates)= ## Mutation rates with msprime @@ -202,7 +215,7 @@ and then take the average across all individuals alive at a given time. (This is only one possible choice, and other choices are usually but not always equivalent, but a discussion of the options and distinctions -would make this tutorial lengthy and confusing. +would make this tutorial lengthy and confusing.) Here is a script that computes this. In the simulation, females' fecundity increases with their age, diff --git a/docs/tutorial.md b/docs/tutorial.md index b7ff051..01bf176 100644 --- a/docs/tutorial.md +++ b/docs/tutorial.md @@ -36,12 +36,13 @@ This tutorial covers the most common uses of tree sequences in SLiM/pyslim. ## Recapitation, simplification, and mutation Perhaps the most common pyslim operations involve [](sec_tutorial_recapitation), -[](sec_tutorial_simplification), and/or [](sec_tutorial_adding_neutral_mutations). +[](sec_tutorial_simplification), and/or [](sec_tutorial_adding_neutral_mutations). Below we illustrate all three in the context of running a "hybrid" simulation, combining -both forwards and backwards (coalescent) methods. This hybrid approach is a popular +both forwards and backwards (coalescent) methods. Such hybrid approaches provide a popular application of pyslim because coalescent algorithms, although more limited in the degree of biological realism they can attain, can be much faster than the forwards algorithms implemented in SLiM. +(See more discussion in [Gopalan et al 2025](https://doi.org/10.1101/2025.09.30.679676).) A typical use-case is to take an existing SLiM simulation and endow it with a history derived from a coalescent simulation: this is known as *recapitation*. @@ -49,9 +50,8 @@ For instance, suppose we have a SLiM simulation of a population of 100,000 indiv that we have run for 10,000 generations without neutral mutations. Now, we wish to extract whole-genome genotype data for only 1,000 individuals. Here's one way to do it: - 1. {func}`.recapitate` : - The simulation has likely not reached demographic equilibrium - it has not + The simulation has likely not reached equilibrium - it has not *coalesced* entirely; recapitation uses coalescent simulation to provide a "prior history" for the initial generation of the simulation. @@ -69,7 +69,7 @@ fluctuating around 1000 individuals, for 1000 generations: ``` You can run this in the shell, -setting the random seed so you get exactly the same results +setting the random seed (with `-s 23`) so you get exactly the same results as in the code below: ```{code-cell} :tags: ["hide-output"] @@ -82,6 +82,14 @@ slim -s 23 example_sim.slim ### Recapitation +If some individuals in the final generation of simulation, at some point on the genome, +do not have a MRCA within the tree sequence, +we say that the simulation has not coalesced. +This means, effectively, that the final state of the simulation depends on the genotypes +of the initial generation. If the simulation was begun in an empty state, +we are effectively assuming the initial state had no genetic diversity. +Usually, we'd like instead to assume that the simulation began with some +at least roughly reasonable levels of genetic diversity. ```{figure} _static/pedigree_recapitate.png --- @@ -95,10 +103,13 @@ for a diagram of the previous state.) ``` Although we can initialize a SLiM simulation with the results of a coalescent simulation, -if during the simulation we don't actually use the genotypes for anything, it -can be much more efficient to do this afterwards, hence only doing a coalescent -simulation for the portions of the first-generation ancestors that have -not yet coalesced. (See the SLiM manual for more explanation.) +if SLiM don't actually use the genotypes for anything, it +can be much more efficient to run the coalescent simulation *afterwards*, +hence only doing a coalescent simulation +for the portions of the first-generation ancestors that have +not yet coalesced. +(See the SLiM manual for more explanation, or +[Kelleher et al 2018](https://doi.org/10.1371/journal.pcbi.1006581).) This is depicted in {numref}`figure {number} `: imagine that at some sites, some of the samples don't share a common ancestor within the SLiMulated portion of history (shown in blue). @@ -119,8 +130,19 @@ rts = pyslim.recapitate(orig_ts, recombination_rate=1e-8, ancestral_Ne=200, random_seed=5) ``` -The warning is harmless; it is reminding us to think about generation time -when recapitating a nonWF simulation (a topic we'll deal with later). +(Here and below we set the random seed +so the content of this document does not change; +you should *not* usually explicitly set +the random seed in your scripts.) + +This will produce a warning: + +> TimeUnitsMismatchWarning: The initial_state has time_units=ticks but time is measured +> in generations in msprime. This may lead to significant discrepancies between the +> timescales. + +This is simplfy reminding us to think about generation time +when recapitating a nonWF simulation (see [](sec_time_units)). We can check that this worked as expected, by verifying that after recapitation all trees have only one root: @@ -135,12 +157,31 @@ print(f"Maximum number of roots before recapitation: {orig_max_roots}\n" The {func}`.recapitate` method is just a thin wrapper around {func}`msprime.sim_ancestry`, and you need to set up demography explicitly - for instance, in the example above -we've simulated from an ancestral population of ``Ne=200`` diploids. +we've effectively simulated from an ancestral population of ``Ne=200`` diploids. If you have more than one population, you must set migration rates or else coalescence will never happen (see [](sec_recapitate_with_migration) for an example, and {func}`.recapitate` for more). +What population size (`ancestral_Ne`) should you use for recapitation? +This question is far beyond the scope of this documentation, +but here are a few helpful points: +First, recapitation is (nearly) exactly identical to having initialized +the SLiM simulation with the randomly produced offspring of the endpoint +of a neutral Wright-Fisher simulation that was begun infinitely far in the past. +This Wright-Fisher model is crude: it does not have separate sexes, +for instance, and is in continuous time unless the +{ref}`DTWF model ` is used; +however, a large body of mathematical work on coalescent theory +has shown that this is a good +approximation to a wide class of more realistic models. +Second, this is certainly an approximation to reality, +but then again so is everything else, +and if the start of the SLiM simulation was far enough in the past, +in most cases the precise details won't substantially affect the outcomes. +Third, you can always evaluate the effect of your choices +by changing them a bit and seeing if it affects the results. + #### Recapitation with a nonuniform recombination map @@ -156,7 +197,8 @@ To use the SLiM values for msprime, we need to do three things: The reason why msprime "positions" must start with 0 (step 1) is that in SLiM, a position or "end" indicates the end of a recombination block such that its associated -"rate" applies to everything to the left of that end (see ``initializeRecombinationRate``). +"rate" applies to everything to the left of that end (see ``initializeRecombinationRate``), +while msprime's format indicates the start of that same block. In msprime, we will pass in a {class}`msprime.RateMap`, which requires two things: @@ -186,59 +228,28 @@ This describes recombination rates across a 100Mb genome with higher rates on th (for instance, 3.2 and 2.8 cM/Mb in the first and last 15Mb respectively) and lower rates in the middle (0.25 cM/Mb between 50Mb and 85Mb). The first column gives the starting position, in bp, -for the window whose recombination rate is given in the second column. -(*Note:* this is *not* a standard format for recombination maps - -it is more usual for the *starting* position to be listed!) - -Here is SLiM code to read this file and set the recombination rates: - -``` -lines = readFile("recomb_rates.tsv"); -header = strsplit(lines[0], "\t"); -if (header[0] != "end_position" - | header[1] != "rate(cM/Mb)") { - stop("Unexpected format!"); -} -rates = NULL; -ends = NULL; -nwindows = length(lines) - 1; -for (line in lines[1:nwindows]) { - components = strsplit(line, "\t"); - ends = c(ends, asInteger(components[0])); - rates = c(rates, asFloat(components[1])); -} -initializeRecombinationRate(rates * 1e-8, ends); -``` +for the window whose recombination rate is given in the second column; +the final position is given on the last line with a recombination rate of 0 +(it is a failing of this relatively common file format that there's no good +way to say where the chromosome ends). +To read this file into SLiM, remove the first and last lines +and use the `initializeRecombinationRateFromFile()` function in SLiM, +which by default scales rates by the `1e-8` factor required to convert from +cM/Mb to crossovers per bp. Now, here's code to take the same recombination map used in SLiM, and use it for recapitation in msprime: ```{code-cell} -positions = [] -rates = [] -with open('_static/recomb_rates.tsv', 'r') as file: - header = file.readline().strip().split("\t") - assert(header[0] == "end_position" and header[1] == "rate(cM/Mb)") - for line in file: - components = line.split("\t") - positions.append(float(components[0])) - rates.append(1e-8 * float(components[1])) - -# step 1 -positions.insert(0, 0) -# step 2 -positions[-1] += 1 -assert positions[-1] == orig_ts.sequence_length - -recomb_map = msprime.RateMap(position=positions, rate=rates) +recomb_map = msprime.RateMap.read_hapmap( + "_static/recomb_rates.tsv", + position_col=0, rate_col=1 +) rts = pyslim.recapitate(orig_ts, recombination_rate=recomb_map, ancestral_Ne=200, random_seed=7) assert(max([t.num_roots for t in rts.trees()]) == 1) ``` -(As before, you should *not* usually explicitly set -the random seed in your scripts; we set it here so -the content of this document does not change.) :::{note} Starting from msprime 1.0, the default model of recombination @@ -267,10 +278,11 @@ than you will be lucky enough to have in real life, so at some point you may want to reduce your dataset to a realistic sample size. We can get rid of unneeded samples and any extra information from them by using an operation called *simplification* (this is the same basic approach that SLiM -implements under the hood when outputting a tree sequence, as described in +implements under the hood to keep down memory usage, as described in [the introduction](sec_left_in_tree_sequence)). -Depicted in the figure at the right is the result of applying an explicit call to +Depicted in {numref}`figure {number} ` +is the result of applying an explicit call to {meth}`tskit.TreeSequence.simplify` to our example tree sequence. In the call we asked to keep only 4 genomes (contained in 2 of the individuals in the current generation). This has @@ -289,25 +301,28 @@ only using it if necessary. It is important that simplification - if it happens at all - either (a) comes after recapitation, or (b) is done with the ``keep_input_roots=True`` option (see {meth}`tskit.TreeSequence.simplify`). -This is because simplification removes some of the +This is because simplification will almost certainly remove some of the ancestral genomes in the first generation, which are necessary for recapitation, unless it is asked to "keep the input roots". If we simplify without this option before recapitating, -some of the first-generation blue chromosomes in the figure on the right +some of the first-generation blue chromosomes +in {numref}`figure {number} ` would not be present, so the coalescent simulation would start from a more recent point in time than it really should. As an extreme example, suppose our SLiM simulation has a single diploid who has reproduced by clonal reproduction for 1,000 generations, so that the final tree sequence is just two vertical lines of descent going back to the two chromosomes in the initial individual alive 1,000 generations ago. -Recapitation would produce a shared history for these two chromosomes, +Recapitation would produce a shared history for these two chromosomes that would coalesce some time longer ago than 1,000 generations. However, if we simplified first, then those two branches going back 1,000 generations would be removed, since they don't convey any information about the shape of the tree; and so recapitation might produce a common ancestor more recently than 1,000 generations, which would be inconsistent with the SLiM simulation. +#### How to simplify + After recapitation, simplification to the history of 100 individuals alive today can be done with the {meth}`tskit.TreeSequence.simplify` method: @@ -356,11 +371,13 @@ If you have recorded a tree sequence in SLiM, likely you have not included any n since it is much more efficient to simply add these on afterwards. To add these (in a completely equivalent way to having included them during the simulation), you can use the {func}`msprime.sim_mutations` function, which returns a new tree sequence with additional mutations. -Continuing with the cartoons from above, these are added to each branch of the tree sequence -at the rate per unit time that you request. +These are added to each branch of the tree sequence +at the rate per unit time that you request, +as depicted in {numref}`figure {number} `. We'll add these using the {class}`msprime.SLiMMutationModel`, so that the file can be read back into SLiM, but any of the other mutation models in msprime could be used. -This works as follows: +This works as follows, starting with the tree sequence +produced above by recapitation: ```{code-cell} next_id = pyslim.next_slim_mutation_id(sts) @@ -370,7 +387,8 @@ ts = pyslim.add_mutation_metadata( rate=1e-8, model=msprime.SLiMMutationModel(type=0, next_id=next_id), keep=True, - ) + ), + mutation_type=0, ) print(f"The tree sequence now has {ts.num_mutations} mutations,\n" @@ -380,24 +398,26 @@ print(f"The tree sequence now has {ts.num_mutations} mutations,\n" What's going on here? Let's step through the code. -1. The mutation ``rate = 1e-8``, which adds mutations at a rate of {math}`10^{-8}` per bp. - Unlike previous versions of msprime, this adds mutations using a discrete-sites model, - i.e., only at integer locations (like SLiM). +1. The mutation ``rate = 1e-8`` says mutations are added at a rate of {math}`10^{-8}` per bp. -2. We're passing ``type=0`` to the mutation model. - This is because SLiM mutations need a "mutation type", - and it makes the most sense if we add a type that was unused in the simulation. - In this example we don't have any existing mutation types, so we can safely use ``type=0``. - -3. We also add ``keep = True``, to keep any existing mutations. +2. We specify ``keep = True``, to keep any existing mutations. In this example there aren't any, so this isn't strictly necessary, but this is a good default. -4. If there are existing SLiM mutations on the tree sequence we need to +3. If there are existing SLiM mutations on the tree sequence we need to make sure any newly added mutations have distinct SLiM IDs, so we use {func}`.next_slim_mutation_id` to figure out what the next available ID is, and pass it in. +4. After ``msprime`` adds the mutations, if we want to load this into SLiM, + we need to add information about the mutations using {func}`add_mutation_metadata`. + +5. We're passing ``mutation_type=0`` to {func}`add_mutation_metadata`. + This is because SLiM mutations need a "mutation type", + and it makes the most sense if we add a type that was unused in the simulation. + In this example we don't have any existing mutation types, + so we can safely use ``type=0``, producing mutations of type ``m0`` in SLiM. + (sec_output)= @@ -428,10 +448,10 @@ with open("example_sim.vcf", "w") as vcffile: nts.write_vcf(vcffile, individuals=sample_indivs[:5]) ``` -Here we've just extracted genotypes for the first five individuals; -see below for what's going on in that code and what you probably -actually want to do; -see also {meth}`tskit.TreeSequence.write_vcf` for more options. +Here we've just extracted genotypes for the first five individuals +(see [](sec_extracting_individuals) for more on this), +and have not taken advantage of the options in {meth}`tskit.TreeSequence.write_vcf` +that help make the output more useful. For instance, if you want to use the SLiM pedigree IDs for the names in the VCF file, we could do: @@ -449,6 +469,46 @@ with open("example_sim2.vcf", "w") as vcffile: ``` +(sec_tutorial_mutation_metadata)= + +## Mutation metadata + +Because of mutation stacking (see the SLiM manual), +each "tskit mutation" can represent a superposition of more than one +"SLiM mutation". +This is recorded by setting the derived state of the tskit mutation +to a comma-separated string of SLiM mutation IDs +(or the empty string, to denote "no mutations"). +So, each SLiM mutation can thus appear in more than one tskit mutation, +and so the [metadata](sec_metadata) about these is stored in top-level metadata, +rather than along with the tskit mutations. +[](sec_tutorial_selected_mutations) has a more in-depth example, but here is a quick overview. +To print out the information about each SLiM mutation "carried" +by a given tskit mutation, whose SLiM IDs are stored in the `derived_state` +as a comma-separated string, we'd do: +```{code-cell} +:tags: ["remove-output"] +mut_metadata = pyslim.mutation_metadata(ts) + +mut = ts.mutation(0) +for x in mut.derived_state.split(","): + print(mut_metadata[int(x)]) +``` +```{code-cell} +:tags: ["remove-input"] +mut_metadata = pyslim.mutation_metadata(ts) + +mut = ts.mutation(0) +for x in mut.derived_state.split(","): + util.pp(mut_metadata[int(x)]) +``` +See [](sec_tutorial_selected_mutations) for an example where a tskit mutation +carries more than one (stacked) SLiM mutation. +**Note:** 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! + + (sec_extracting_individuals)= ## Extracting SLiM individuals @@ -458,7 +518,7 @@ individuals from a simulation, for analysis or for outputting their genotypes, for instance. This section demonstrates some basic manipulations of individuals. -### Extracting a sample of individuals +### Extracting a random sample of individuals The first, most common method to extract individuals is simply to get all those that were alive at a particular time, @@ -474,7 +534,7 @@ print(f"There are {len(alive_indivs)} individuals alive in the final generation. ``` Here, ``alive_indivs`` is a vector of *individual* IDs, -so one way to take a sample of living individuals +so one way to take a random sample of living individuals and write their SNPs to a VCF is: ```{code-cell} @@ -485,70 +545,79 @@ with open("example_snps.vcf", "w") as vcffile: ts.write_vcf(vcffile, individuals=keep_indivs) ``` -If you've done nothing else to the output from SLiM, -then this code will work, -but it does requires all alive individuals to be *samples*. -A situation in which this isn't the case is shown in the next section. - - -### Extracting individuals after simplification - -If the tree sequence has been simplified to retain only information -about a set of focal individuals, -then knowing an individual is alive at the end of the simulation -isn't enough to guarantee we have their entire genome sequence: -there are often individuals retained after simplification with -one or more non-sample nodes. -So, to output genotypes after simplification, we need to also check -that the individuals' nodes are also *samples*. -As mentioned earlier, {meth}`tskit.TreeSequence.simplify` takes a list -of nodes as input: +### The non-equivalence of "individual" and "sample" + +A somewhat confusing concept in tskit is that of a +{ref}`sample node `. +If you think of the tree sequence as containing information about some +genetic data, then the sample nodes are those that represent our actual data, +while other nodes represent ancestors. +In practice, many tskit methods apply by default to the samples. + +However, we may have individuals in the tree sequence whose nodes are not samples, +and sometimes it is important to know this. +For instance, by default {meth}`tskit.TreeSequence.write_vcf` +will only output columns for sample nodes, silently omitting any non-sample nodes +among the individuals provided. +For instance, if the tree sequence has been simplified to retain only information +about a set of focal individuals, these nodes are marked as samples, +but other individuals who are alive at the end of the simulation may +still be in the tree sequence (e.g., parents of a pair of focal individuals) +but their entire genomes may not be present +and their nodes may not be marked as samples. + +Here's an example. We'll simplify down to a random sample of the "children" +(those born during the last tick +(as mentioned earlier, {meth}`tskit.TreeSequence.simplify` takes a list +of nodes as input): ```{code-cell} +children = [ind.id for ind in orig_ts.individuals() if ind.metadata['age'] == 0] +keep_indivs = rng.choice(children, 10, replace=False) keep_nodes = [] for i in keep_indivs: keep_nodes.extend(orig_ts.individual(i).nodes) sts = rts.simplify(keep_nodes) ts = msprime.sim_mutations(sts, rate=1e-8, random_seed=1) ``` -Individuals are retained by simplify if any of their nodes are, + +Individuals are retained by simplify if any of their nodes are retained, so we would get an alive individual without sample nodes if, for instance, a parent and two offspring are all alive, and we happen to keep the offspring but not the parent. -For this reason, if at this point we try to extract genotypes for all of the -alive individuals, we encounter a (somewhat confusing) error: +Now, we write out to VCF: ```{code-cell} alive_indivs = pyslim.individuals_alive_at(ts, 0) -try: - with open("example_snps.vcf", "w") as vcffile: - ts.write_vcf(vcffile, individuals=alive_indivs) -except Exception as e: - print ("Error:") - print (e) -``` - -This is just telling us that some of the individuals we're trying -to write to the VCF have nodes that are not samples. -The reference to "missing" is a red herring: -see {ref}`tskit documentation ` -for what it's talking about. -So, instead of writing out genotypes of everyone alive, -we need to get the list of alive individuals *whose nodes are samples*, -using {meth}`is_sample() `: +print(f"There are {len(alive_indivs)} individuals alive.") +with open("example_snps.vcf", "w") as vcffile: + ts.write_vcf(vcffile, individuals=alive_indivs) +``` +We know that this tree sequence has only 10 individuals with sample +(those children, chosen above), but there are more alive individuals. +The resulting VCF has only 10 columns (note some numbers are missed in the +column headers), which could be surprising, +because we passed a longer list to the `individuals=` argument to +{meth}`tskit.TreeSequence.write_vcf`: ```{code-cell} -indivlist = [] -for i in alive_indivs: - ind = ts.individual(i) - if ts.node(ind.nodes[0]).is_sample(): - indivlist.append(i) - # if one node is a sample, the other should be also: - assert ts.node(ind.nodes[1]).is_sample() -with open("example_snps.vcf", "w") as vcffile: - ts.write_vcf(vcffile, individuals=indivlist) +%%bash +head example_snps.vcf ``` +The solution here would be to instead pass a list of the alive individuals who +are samples to `write_vcf`, which we can obtain like so: +```{code-cell} +sample_indivs = [ + i for i in pyslim.individuals_alive_at(ts, 0) + if ts.node(ts.individual(i).nodes[0]).is_sample() +] +len(sample_indivs) +``` +Note that we only check one node of an individual to see if it is a sample; +because we included either all or none of the nodes of each individual, +if one node of an individual is a sample node, +the other is guaranteed to be as well. ### Extracting particular individuals @@ -568,7 +637,12 @@ Let's run it: slim -s 32 migrants.slim ``` -To count up how many individuals are in each population, +In the tree sequence, "populations" are associated with nodes, not individuals, +and so to find the population a given individual `ind` was born in, +we find the list of IDs of that individual's nodes with `ind.nodes`, +and then obtain the actual node object with `ts.node( )`. +So, to count up how many individuals are in each population, +we need to look we could do: ```{code-cell} @@ -603,8 +677,8 @@ it will run forever, unable to coalesce. By default, {func}`.recapitate` *merges* the two populations into a single one of size ``ancestral_Ne``. But, if we'd like them to stay separate, we need to inclue migration between them. -Here's how we set up the demography using msprime's tools: - +Here's how we set up the demography using +{ref}`msprime's tools `: ```{code-cell} demography = msprime.Demography.from_tree_sequence(orig_ts) for pop in demography.populations: @@ -638,7 +712,10 @@ Let's compute genetic diversity within and between each of the two populations often denoted {math}`\pi` and {math}`d_{xy}`). To do this, we need to extract the node IDs from the individuals of the two populations that are alive at the end of the simulation. - +The method {meth}`tskit.TreeSequence.samples` will give us these nodes, +and {meth}`tskit.TreeSequence.diversity` computes genetic diversity for a given list +of node IDs (*not* individual IDs!), +so we can compute as follows: ```{code-cell} pop_nodes = [ts.samples(population=p, time=0) for p in range(ts.num_populations)] diversity = ts.diversity(pop_nodes[1:]) @@ -662,11 +739,11 @@ about an individual in the previous example: ```{code-cell} :tags: ["remove-output"] -ind = ts.individual(0) +ts.individual(0) ``` ```{code-cell} :tags: ["remove-input"] -util.pp(ind) +util.pp(ts.individual(0)) ``` Some information is generic to individuals in tree sequences of any format: @@ -689,19 +766,23 @@ 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, +- the ``tag`` entries contain the correspondingly-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. + (There is always at least one trait: + even models that do not explicitly set up any traits + have a default trait set up by SLiM automatically.) + We can use this metadata in many ways, for example, to create an age distribution by sex: ```{code-cell} import numpy as np max_age = max([ind.metadata["age"] for ind in ts.individuals()]) -age_table = np.zeros((max_age + 1, 2)) +age_table = np.zeros((max_age + 1, 2), dtype='int') age_labels = { pyslim.INDIVIDUAL_TYPE_FEMALE: 'females', pyslim.INDIVIDUAL_TYPE_MALE: 'males' } alive_indivs = pyslim.individuals_alive_at(ts, 0) @@ -732,9 +813,10 @@ iterating over individuals as above. For example, suppose that we want to randomly sample 10 individuals alive and older than 2 time steps from each of the populations at the end of the simulation, and simplify the tree sequence to retain only those individuals. -This can be done using the numpy arrays returned by {func}`.individual_ages` -and `.individuals_population` as follows: - +Since `alive_indivs` (produced with {func}`.individuals_alive_at` above) +and the output of {func}`.individual_ages` and `.individuals_population` +are arrays of length equal to the number of individuals, +this can be done as follows: ```{code-cell} ages = pyslim.individual_ages(ts) adults = alive_indivs[ages[alive_indivs] > 2] @@ -763,6 +845,7 @@ print(f"There are {sub_ts.num_mutations} mutations across {sub_ts.num_trees} dis f"with a mean overall genetic diversity of {sub_ts.diversity()}.") ``` +(sec_tutorial_vacant_nodes)= ## Vacant nodes @@ -775,16 +858,19 @@ For instance, running an msprime simulation backwards from a tree sequence with vacant sample nodes (as in {numref}`figure {number} ` of the Overview) would also simulate ancestry of the vacant nodes. -For this reason, {func}`.recapitate` removes these nodes +For this reason, +you can remove the vacant nodes from the sample with {func}`.remove_vacant`. +Similarly, {func}`.recapitate` removes these nodes from the sample before running msprime, which makes it so their ancestry will not be simulated. Similarly, at present {ref}`statistics in tskit` do not account for missing data, so will return incorrect results if these vacant nodes are not removed from the sample. -To be clear, the vacant nodes will still be present, -just not marked as samples (i.e., with the ``tskit.NODE_IS_SAMPLE`` -flag removed from their node flags). +To be clear, in the output from {func}`.recapitate` or {func}`.remove_vacant` +the vacant nodes will still be present, +just not marked as samples (i.e., the ``tskit.NODE_IS_SAMPLE`` +flag has been removed from their node flags). Once they are not part of the sample, they are essentially invisible to most operations. However, it is helpful to know that they are there. @@ -793,9 +879,9 @@ They are kept because if you wish to read the tree sequence back into SLiM then you'll need them; they can put them back in the sample after being removed with {func}`.restore_vacant`. -If you would like to remove the vacant nodes from the sample for -other reasons, you can use {func}`.remove_vacant`. +To find out which nodes in the tree sequence are vacant nodes, +use {func}`.nodes_vacant`. ## Historical individuals @@ -804,11 +890,10 @@ individuals and the ancestral nodes (genomes) required to reconstruct their gene relationships. But you might want more than that. For example, there may be individuals who are not alive any more, but whose complete ancestry you would like to know. Or perhaps you'd like to know how the final generation relates to particular individuals in -the past. Or it may be that you want to access the spatial location of historical genomes -(which, for technical reasons is linked to individuals, not to genomes). The solution is -to *remember* an individual during the simulation, using the SLiM function -``treeSeqRememberIndividuals()``. Individuals can be Remembered in two ways, as -described below. +the past. Or it may be that you want to access the spatial location of historical genomes. +The solution is to *remember* an individual during the simulation, +using the SLiM function ``treeSeqRememberIndividuals()``. +Individuals can be Remembered in two ways, as described below. @@ -844,11 +929,11 @@ individual in the [introductory example](sec_left_in_tree_sequence) is pictured ### Retaining individuals Alternatively, you may want to avoid treating historical individuals and their genomes as -actual samples, but temporarily *retain* them as long as they are still relevant to -reconstructing the genetic ancestry of the sample nodes. This can save some computational -burden, as not only will nodes and individuals be removed once they are no longer -ancestral, but also the full ancestry of the retained individuals does not need to be -kept. You can retain individuals in this way by using +actual samples, but temporarily *retain* those individuals as long as they are still +relevant to reconstructing the genetic ancestry of the sample nodes. This can save some +computational burden, as not only will nodes and individuals be removed once they are no +longer ancestral, but also the full ancestry of the retained individuals does not need to +be kept. You can retain individuals in this way by using ``treeSeqRememberIndividuals(..., permanent=F)``. Since a retained individual's nodes are not marked as samples, they are subject to the @@ -880,25 +965,28 @@ to show them. Although not needed to reconstruct full genomic history, it is perfectly possible to apply ``treeSeqRememberIndividuals()`` to every individual in every generation of a -simulation (i.e. everyone who has ever lived). If you simply mark everyone for temporary -retention, it should not increase the memory burden of your simulation much: most +simulation (i.e. everyone who has ever lived). If you simply retain everyone +(with `permanent=F`), +it should not increase the memory burden of your simulation much: most individuals will be removed as the simulation progresses, since they will not contain coalescent nodes. However, if you use ``treeSeqInitialize(retainCoalescentOnly=F)``, the number of individuals in the resulting tree sequence is likely to become very large, and the efficiencies provided by tree sequence recording will be substantially reduced. Indeed in this case, retaining will be much the same as permanently remembering everyone -who has ever lived. Nevertheless, if you are willing to sacrifice enough computer memory, +who has ever lived. (It will retain all individuals who are genetic ancestors to the +final generation, which is for a typical-length chromosome a substantial fraction of +everyone who ever lived.) +Nevertheless, if you are willing to sacrifice enough computer memory, either of these is (perhaps surprisingly) possible, even for medium-sized simulations. - (sec_individual_flags)= ### Individual flags We have seen that an individual can appear in the tree sequence because it was -Remembered, Retained, or alive at the end of the simulation (note these -are not mutually exclusive). The ``Individual.flags`` value stores this information. +Remembered or Retained, and/or alive at the end of the simulation. +The ``Individual.flags`` value stores this information. For example, to count up the different individual types, we could do this: :::{todo} @@ -931,7 +1019,9 @@ so these are no longer present, unless you specifically Remember them. ::: -## Generating intial diversity with msprime +(sec_initial_diversity)= + +## Generating initial diversity with msprime Suppose now that we'd like to *start* a SLiM simulation with the result of a coalescent simulation. @@ -956,7 +1046,7 @@ ts = msprime.sim_ancestry( recombination_rate=1e-8, sequence_length=1e6, random_seed=5) -ts = pyslim.annotate(ts, model_type="nonWF", tick=1) +ts = pyslim.annotate(ts, model_type="nonWF", tick=1, stage="early") assert ts.num_individuals == 200 assert ts.num_samples == 400 ``` @@ -967,7 +1057,7 @@ SLiM will set the current time counter to 1. would not execute after loading the tree sequence.) We now have 200 diploids (so, 400 sampled nodes). -Here's individual 199, which hsa SLiM metadata: +Here's individual 199, which has SLiM metadata: ```{code-cell} :tags: ["remove-output"] ind = ts.individual(199) @@ -992,10 +1082,12 @@ ts = pyslim.add_mutation_metadata( ts, rate=1e-8, model=msprime.SLiMMutationModel(type=0), random_seed=9 - ) + ), + mutation_type=0, ) ``` -The resulting mutations are in SLiM format. +The resulting mutations are in SLiM format +because we used {class}`msprime.SLiMMutationModel` 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 @@ -1005,7 +1097,8 @@ 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 +using {func}`.mutation_metadata`, which +as above in [](sec_tutorial_mutation_metadata) provides a dictionary indexed by the SLiM IDs: ```{code-cell} :tags: ["remove-output"] @@ -1018,9 +1111,6 @@ for sid in ds.split(","): 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} @@ -1029,6 +1119,8 @@ ts.dump("initialize_nonWF.trees") Here's a minimal SLiM script that reads in the tree sequence file and runs it for a bit longer. +We load the tree sequence in `early()`, matching the `stage` +passed to {func}`annotate` above. ```{literalinclude} neutral_restart.slim ``` @@ -1108,7 +1200,7 @@ for k in range(3): Here's a script minimally modified from the above to be nucleotide-based: -```{literalinclude} neutral_restart.slim +```{literalinclude} neutral_nucleotide_restart.slim ``` ```{code-cell} @@ -1117,6 +1209,10 @@ Here's a script minimally modified from the above to be nucleotide-based: slim -s 123 neutral_nucleotide_restart.slim ``` +Everything runs reasonably. + + +(sec_tutorial_selected_mutations)= ## Extracting information about selected mutations @@ -1162,8 +1258,12 @@ for x in md: util.pp(x) ``` +The trait value is _not_ the same thing as fitness, because other things can affect fitness too – fitnessEffect() callbacks and individual/subpop fitnessScaling values, in particular. So I think this distinction is important to be clear about. + Since we haven't explicitly defined any traits in this simulation, -the only trait is fitness, and the `effect_size` listed under `per_trait` +the only trait is the default multiplicative trait implicitly defined by SLiM, +which has a direct effect on fitness, +and so 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: @@ -1247,8 +1347,10 @@ 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. The allele frequency spectrum for *all* mutations can be obtained using the -{meth}`tskit.TreeSequence.allele_frequency_spectrum` method, -shown here for a sample of size 10 to make the output easy to see: +{meth}`tskit.TreeSequence.allele_frequency_spectrum` method. +(Note this is the frequency spectrum for the *tskit* mutations, +rather than the SLiM mutations, which may differ because of stacking.) +It is shown here for a sample of size 10 to make the output easy to see: ```{code-cell} samps = np.random.choice(ts.samples(), 10, replace=False) @@ -1380,14 +1482,16 @@ Also known as "gotchas". 1. If you use msprime to simulate a tree sequence, and then use that to initialize a SLiM simulation, you have to specify the same sequence length in both: as in the examples above, - the ``sequence_length`` argument to {func}`msprime.sim_ancestry` should be equal to the SLiM sequence length + the ``sequence_length`` argument to {func}`msprime.sim_ancestry` should be equal to the + `lastPosition` in SLiM *plus 1.0* (e.g., if the base positions in SLiM are 0 to 99, then there are 100 bases in all, so the sequence length should be 100). 2. Make sure to distinguish *individuals* and *nodes*! - ``tskit`` "nodes" correspond to SLiM "genomes". - Individuals in SLiM are diploid, so normally, each has two nodes (but retained - individuals may have nodes removed by simplification: see below). + ``tskit`` "nodes" correspond to SLiM "haplosomes". + Individuals in SLiM normally each have two nodes, but retained + individuals may have nodes removed by simplification: see below, + and some nodes may be [vacant](sec_tutorial_vacant_nodes). 3. As described above, the Individual table contains entries for diff --git a/pyproject.toml b/pyproject.toml index 0a43898..0c3e690 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -64,6 +64,7 @@ docs = [ "matplotlib", "msprime", "pandas", + "plotnine", "sphinx-argparse", "sphinx-issues", ] diff --git a/pyslim/__init__.py b/pyslim/__init__.py index 6f7c8f4..be8064b 100644 --- a/pyslim/__init__.py +++ b/pyslim/__init__.py @@ -36,22 +36,23 @@ #: A value used in individual metadata ("sex") to indicate the individual is a hermaphrodite. INDIVIDUAL_TYPE_HERMAPHRODITE = -1 -#: A value used in individual metadata ("sex") to indicate the individual is a male. +#: A value used in individual metadata ("sex") to indicate the individual is a female. INDIVIDUAL_TYPE_FEMALE = 0 -#: A value used in individual metadata ("sex") to indicate the individual is a female. +#: A value used in individual metadata ("sex") to indicate the individual is a male. INDIVIDUAL_TYPE_MALE = 1 #: An individual flag indicating the individual is a migrant. -INDIVIDUAL_FLAG_MIGRATED = np.uint32(1 << 1) +INDIVIDUAL_FLAG_MIGRATED = np.uint32(1 << 0) -#: This flag exists because SLiM expects certain vacant nodes (=haplosomes) +#: This flag exists because SLiM expects certain vacant nodes #: to be marked as samples (those vacant nodes corresponding to alive individuals), #: but including these as samples causes problems for certain operations in tskit. -#: So, if {meth}`.remove_vacant` is used to remove the 'sample' flags from those +#: So, if :meth:`remove_vacant` is used to remove the 'sample' flags from those #: vacant nodes, this flag is applied so that the sample flag can be easily put back -#: (by {meth}`.restore_vacant`). The flag does *not* mean simply that this is a -#: vacant sample node (indeed, if this flag is set then the `tskit.NODE_IS_SAMPLE` +#: (by :meth:`restore_vacant`). So, this flag means that this is is a vacant node +#: that SLiM will expect to be a sample but is not currently marked as a sample +#: (in particular, if this flag is set then the :data:`tskit.NODE_IS_SAMPLE` #: flag is not expected to be set). NODE_IS_VACANT_SAMPLE = np.uint32(1 << 16) diff --git a/pyslim/methods.py b/pyslim/methods.py index cf84634..bc87b1a 100644 --- a/pyslim/methods.py +++ b/pyslim/methods.py @@ -90,17 +90,19 @@ def _is_chrom_vacant(k, b): return (b >> i & 1) > 0 -def has_vacant_samples(ts, _ts_metadata=None): +def has_vacant_samples(ts, ts_metadata=None): """ Returns whether the tree sequence has vacant sample nodes. - See :meth:`remove_vacant`. + See :func:`remove_vacant`. :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. """ - if _ts_metadata is None: - _ts_metadata = ts.metadata + if ts_metadata is None: + ts_metadata = ts.metadata out = False - k = _chromosome_index(_ts_metadata) + k = _chromosome_index(ts_metadata) for n in ts.samples(): md = ts.node(n).metadata if md is not None: @@ -118,7 +120,7 @@ def nodes_vacant(ts): 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`. + :func:`remove_vacant`. :param tskit.TreeSequence ts: The tree sequence. @@ -153,7 +155,7 @@ def node_is_vacant(ts, node): 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`. + unused node for haploid chromosome types. See :func:`remove_vacant`. :param tskit.TreeSequence ts: The tree sequence. :param tskit.Node node: The node object. @@ -173,7 +175,7 @@ def node_is_vacant(ts, node): def _record_vacant_tables(tables, ts_metadata): """ Sets the NODE_IS_VACANT_SAMPLE flag for all vacant, sample nodes. - See :meth:`remove_vacant`. + See :func:`remove_vacant`. :param tskit.TableCollection tables: The table collection. """ @@ -204,7 +206,7 @@ def _remove_vacant_sample_flags(tables): tables.nodes.set_columns(**dn) -def remove_vacant(ts, _ts_metadata=None): +def remove_vacant(ts, ts_metadata=None): """ Remove sample flags from all vacant nodes. @@ -217,62 +219,74 @@ def remove_vacant(ts, _ts_metadata=None): This method returns a copy of the tree sequence for which all vacant nodes have the sample flag removed; these nodes will thus not affect - :meth:`recapitate`, tree sequence statistics, etc. This also sets + :func:`recapitate`, tree sequence statistics, etc. This also sets the :data:`NODE_IS_VACANT_SAMPLE` flag on these nodes, - so they can be restored with :meth:`restore_vacant`. You probably don't + so they can be restored with :func:`restore_vacant`. You probably don't want to run this method again on the output, since these flags will be - overwritten and :meth:`restore_vacant` will no longer work as expected. + overwritten and :func:`restore_vacant` will no longer work as expected. :param tskit.TreeSequence ts: The tree sequence. + :return tskit.TreeSequence: A copy of the tree sequence with vacant nodes + not 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. """ - if _ts_metadata is None: - _ts_metadata = ts.metadata + if ts_metadata is None: + ts_metadata = ts.metadata tables = ts.dump_tables() - remove_vacant_tables(tables, _ts_metadata) + remove_vacant_tables(tables, ts_metadata) return tables.tree_sequence() -def remove_vacant_tables(tables, _ts_metadata=None): +def remove_vacant_tables(tables, ts_metadata=None): """ - Does the work of :meth:`remove_vacant`, modifying ``tables`` in place. + Does the work of :func:`remove_vacant`, modifying ``tables`` in place. :param tskit.TableCollection tables: The tables underlying a 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. """ - if _ts_metadata is None: - _ts_metadata = tables.metadata - _record_vacant_tables(tables, _ts_metadata) + 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, _ts_metadata=None): +def restore_vacant(ts, ts_metadata=None): """ - The inverse of :meth:`remove_vacant`. + The inverse of :func:`remove_vacant`. This method returns a copy of the tree sequence for which all nodes with the :data:`NODE_IS_VACANT_SAMPLE` flag set have their sample flags - set also. If these nodes are not vacant, an error will be raised. This is - intended to be an inverse of :meth:`remove_vacant`. + set also and the :data:`NODE_IS_VACANT_SAMPLE` removed. If these nodes + are not vacant, an error will be raised. :param tskit.TreeSequence ts: The tree sequence. + :return tskit.TreeSequence: A copy of the tree sequence with vacant nodes + 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. """ - if _ts_metadata is None: - _ts_metadata = ts.metadata + if ts_metadata is None: + ts_metadata = ts.metadata tables = ts.dump_tables() - restore_vacant_tables(tables, _ts_metadata) + restore_vacant_tables(tables, ts_metadata) return tables.tree_sequence() -def restore_vacant_tables(tables, _ts_metadata=None): +def restore_vacant_tables(tables, ts_metadata=None): """ - Does the work of :meth:`restore_vacant`, modifying ``tables`` in place. + Does the work of :func:`restore_vacant`, modifying ``tables`` in place. :param tskit.TableCollection tables: The tables underlying a 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. """ - if _ts_metadata is None: - _ts_metadata = tables.metadata + 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(_ts_metadata) + k = _chromosome_index(ts_metadata) for j in is_vacant: n = tables.nodes[j] if n.metadata is None: @@ -317,10 +331,16 @@ def recapitate(ts, ancestral_Ne=None, *, keep_vacant=False, **kwargs): provided, there will be *no* recombination. Sample flags from vacant nodes will be removed before recapitating: - see :meth:`remove_vacant`. To restore these, use ``keep_vacant=True``. + see :func:`remove_vacant`. To restore these, use ``keep_vacant=True``. You only need to do this if some individuals are not diploid and you will be loading the tree sequence back into SLiM. + Precisely, this function will: (1) :func:`.remove_vacant` nodes, if any; + (2) create a demography in which all populations split from + a new population called "ancestral" of the desired size; + (3) simulate with :func:`msprime.sim_ancestry`; and + (4) :func:`.restore_vacant` nodes, if any. + :param tskit.TreeSequence ts: The tree sequence to transform. :param float ancestral_Ne: If specified, then will simulate from a single ancestral population of this size. It is an error to specify this @@ -328,6 +348,8 @@ def recapitate(ts, ancestral_Ne=None, *, keep_vacant=False, **kwargs): :param bool keep_vacant: Whether to restore the sample flags on any vacant sample nodes. Default: False. :param dict kwargs: Any other arguments to :func:`msprime.sim_ancestry`. + :return tskit.TreeSequence: A copy of the tree sequence with additional ancestral + history added. """ ts_metadata = ts.metadata is_current_version(ts_metadata, _warn=True) @@ -403,7 +425,11 @@ def recapitate(ts, ancestral_Ne=None, *, keep_vacant=False, **kwargs): 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. + for each mutation in the tree sequence that does not already have this information. + To do this, mutations must be in SLiM format, as for instance produced by + :class:`msprime.SLiMMutationModel`. Any information about SLiM mutations already + in top-level metadata will remain unchanged. + 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 @@ -416,6 +442,8 @@ def add_mutation_metadata(ts, mutation_type=0, remove_unused=False): :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. + :return tskit.TreeSequence: A copy of the tree sequence with mutation information in + metadata. """ tables = ts.dump_tables() add_mutation_metadata_tables( @@ -426,8 +454,7 @@ def add_mutation_metadata(ts, mutation_type=0, remove_unused=False): 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`. + Does the work of :func:`.add_mutation_metadata`, modifying ``tables`` in place. :param tskit.TableCollection tables: The table collection to be modified. :param int mutation_type: The numeric ID of the mutation type in SLiM. @@ -486,7 +513,7 @@ def convert_alleles(ts): have "" (the empty string) for the ancestral state at each site; this method will replace this with the corresponding nucleotide from the reference sequence. For mutations, SLiM records the 'derived state' as a SLiM mutation ID; this - method will this with the nucleotide from the mutation's metadata. + method will replace this with the nucleotide from the mutation's metadata. This operation is not reversible: since SLiM mutation IDs are lost, the tree sequence will not be able to be read back into SLiM. @@ -505,6 +532,8 @@ def convert_alleles(ts): generate these, see :func:`.generate_nucleotides`. :param tskit.TreeSequence ts: The tree sequence to transform. + :return tskit.TreeSequence: A copy of the tree sequence with modified + ancestral and derived states. """ tables = ts.dump_tables() has_refseq = ts.has_reference_sequence() and len(ts.reference_sequence.data) > 0 @@ -577,6 +606,7 @@ def generate_nucleotides(ts, reference_sequence=None, keep=True, seed=None): or to randomly generate one. :param bool keep: Whether to leave existing nucleotides in mutations that already have one. :param int seed: The random seed for generating new alleles. + :return tskit.TreeSequence: A copy of the tree sequence with nucleotides. """ rng = np.random.default_rng(seed=seed) if reference_sequence is None: @@ -600,7 +630,7 @@ def generate_nucleotides(ts, reference_sequence=None, keep=True, seed=None): "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) + 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 @@ -648,10 +678,14 @@ def generate_nucleotides(ts, reference_sequence=None, keep=True, seed=None): 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 + Returns the ages of all individuals in the tree sequence, as extracted + from metadata. The result is an array of length equal to the number of individuals, with k-th entry equal to ``ts.individual(k).metadata["age"]``. + These are the ages of the indivdiuals when they were recorded in the tree sequence: + either when the tree sequence was saved (if they are alive) or when they were + last Remembered. See also :func:`.individual_ages_at`. + :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. @@ -687,11 +721,12 @@ def individuals_alive_at( while in nonWF models, birth occurs before "early()", so they are alive for both stages. - In both WF and nonWF models, mortality occurs between + In both the WF and nonWF life cycles, mortality occurs between "early()" and "late()", so that individuals are last alive during the "early()" stage of the time step of their final age, and if individuals are alive during "late()" they will also be alive during "first()" and - "early()" of the next time step. This means it is important to know during + "early()" of the next time step (unless the user calls `killIndividuals()`). + This means it is important to know during which stage individuals were Remembered - for instance, if the call to sim.treeSeqRememberIndividuals() was made during "early()" of a given time step, then those individuals might not have survived until "late()" of that @@ -803,8 +838,9 @@ def individual_ages_at( This is computed as the time ago the individual was born (found by the `time` associated with the the individual's nodes) minus the `time` argument; while "death" is inferred from the individual's ``age``, - recorded in metadata. These values are the same as what would be shown - in SLiM during the corresponding time step and stage. + recorded in metadata (see :func:`.individual_ages`). These values should + be the same as what would be shown in SLiM during the corresponding time + step and stage. Since age increments at the end of each time step, the age is the number of time steps ends the individual has lived @@ -843,8 +879,9 @@ 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 - of ticks since the start of the simulation). Although the latter are - always integers, these will not be if the provided times are not integers. + of ticks since the start of the simulation). Although times in SLiM are + always integers, the returned values will not be integers if the values + in `time` are not. When the tree sequence is written out, SLiM records the current current tick in the metadata: @@ -906,11 +943,17 @@ def _shift_times(ts, dt): def set_slim_state(ts, time=0, individuals=None): """ - Changes the information stored in metadata so that when loaded into SLiM, - the current time will be ``time`` units ago (i.e., at tskit time ``time``) + Returns a new tree sequence for which + the information stored in metadata has been changed so that when loaded into + SLiM, the current time will be ``time`` units ago (i.e., at tskit time ``time``) and the alive individuals will be ``individuals``. The time in SLiM (i.e., the value of the tick counter) will also be ``time`` units earlier. + This is useful, for instance, if you Remember a set of individuals at some + point partway through a SLiM simulation, and would like to load *those* + individuals into SLiM, rather than the final generation. (These could be + used to start a new simulation, for instance.) + Appropriate individuals might be found, for instance, with ``pyslim.individuals_alive_at(ts, time)``. @@ -929,7 +972,8 @@ def set_slim_state(ts, time=0, individuals=None): the original simulation, but in the new simulation they die immediately, then the resulting tree sequence will still be valid but the history as recorded by SLiM in metadata will not make sense. To avoid such issues, one - can Remember and then kill the individuals to be later used in this way. + can in the original SLiM script Remember and then immediately kill the + individuals to be later used in this way. This also subtracts ``time`` from the value of "cycle" in top-level metadata; if this is not desired (or the value in "tick" needs to be @@ -941,6 +985,7 @@ def set_slim_state(ts, time=0, individuals=None): :param numpy.ndarray individuals: An array of the tskit IDs of the individuals that should be marked as alive (all others will be not alive). (Default: leave unchanged.) + :return tskit.TreeSequence: A copy of the tree sequence, modified. """ tables = _shift_times(ts, -time) if time != 0: @@ -1069,7 +1114,15 @@ def has_individual_parents(ts): return _do_individual_parents_stuff(ts, return_parents=False) -def annotate(ts, **kwargs): +def annotate( + ts, + model_type, + tick, + cycle=None, + stage="early", + reference_sequence=None, + annotate_mutations=True, +): """ Takes a tree sequence (as produced by msprime, for instance), and adds in the information necessary for SLiM to use it as an initial state, filling in @@ -1089,7 +1142,15 @@ def annotate(ts, **kwargs): with defaults. (If False, information about mutations is unchanged.) """ tables = ts.dump_tables() - annotate_tables(tables, **kwargs) + annotate_tables( + tables, + model_type=model_type, + tick=tick, + cycle=cycle, + stage=stage, + reference_sequence=reference_sequence, + annotate_mutations=annotate_mutations, + ) return tables.tree_sequence() @@ -1146,7 +1207,7 @@ def annotate_tables( def next_slim_mutation_id(ts): """ - Returns the next SLiM mutation ID for this tree sequence. This is useful + Returns the next unused SLiM mutation ID for this tree sequence. This is useful because if you want to add more mutations to your SLiM tree sequence using :func:`msprime.sim_mutations`, you may need to specify the parameter `next_id` in your :class:`msprime.SLiMMutationModel` to be larger than any diff --git a/pyslim/slim_metadata.py b/pyslim/slim_metadata.py index 970198d..b54e352 100644 --- a/pyslim/slim_metadata.py +++ b/pyslim/slim_metadata.py @@ -711,7 +711,7 @@ def slim_individual_metadata_schema(num_traits=1): def slim_node_metadata_schema(num_chromosomes=1): """ - Unlike other schema, the node metadata schema depends on the number of + The node metadata schema depends on the number of chromosomes in a multichromosome simulation, and {data}`.slim_metadata_schemas` returns the schema for a single-chromosome simulation. This function @@ -761,10 +761,14 @@ def default_slim_metadata(name, num_chromosomes=1, num_traits=1, **kwargs): "tree_sequence", "edge", "site", "mutation", "mutation_list_entry", "node", "individual", or "population". - Additional kwargs are used to update the resulting metadata - (without validity checking). + Additional keyword arguments may be passed in to modify the returned + metadata dictionary: for each such key=value pair the returned dictionary + has `value` (re)assigned to `key`. :param str name: The type of metadata requested. + :param int num_chromosomes: The number of chromosomes (only relevant for "node"). + :param int num_traits: The number of traits + (only relevant for "individual" and "mutation_list_entry"). :rtype dict: """ if name == "tree_sequence": @@ -1803,7 +1807,7 @@ def is_current_version(ts, _warn=False): 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, + :param 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. """ diff --git a/pyslim/slim_tree_sequence.py b/pyslim/slim_tree_sequence.py index f6bc9c0..58840f6 100644 --- a/pyslim/slim_tree_sequence.py +++ b/pyslim/slim_tree_sequence.py @@ -10,7 +10,7 @@ def load(*args, **kwargs): raise RuntimeError("This method has been removed: use tskit.load( ) instead.") -def mutation_metadata(ts, check=True, _ts_metadata=None): +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. @@ -26,14 +26,18 @@ def mutation_metadata(ts, check=True, _ts_metadata=None): can slow down scripts considerably. :param tskit.TreeSequence ts: The tree sequence. + :param bool check: Whether to verify that all mutations are described in top-level + metadata. + :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. :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 + if ts_metadata is None: + ts_metadata = ts.metadata # Note that dictionaries preserve insertion order - ml = _ts_metadata["SLiM_mutation_list"] + 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: diff --git a/pyslim/spatial.py b/pyslim/spatial.py index d4abc4d..f619f23 100644 --- a/pyslim/spatial.py +++ b/pyslim/spatial.py @@ -67,9 +67,9 @@ def _average_time_alive(birth_times, death_times, t0, t1): def population_size(ts, x_bins, y_bins, time_bins, stage="late", remembered_stage=None): """ Calculates the population size in each of the spatial bins defined by grid lines - at ``x_bins`` and ``y_bins``, averaged over each of the time intervals separated by - ``time_bins``. To obtain actual (census) sizes, the tree sequence must contain - all individuals alive, e.g., from a SLiM simulation with all individuals + at ``x_bins`` and ``y_bins``, averaged over each of the ticks in the time intervals + separated by ``time_bins``. To obtain actual (census) sizes, the tree sequence must + contain all individuals alive, e.g., from a SLiM simulation with all individuals permanently remembered. With ``nx``, ``ny`` and ``nt`` the number of bins in the ``x``, ``y`` and time directions diff --git a/tests/__init__.py b/tests/__init__.py index 376b6ce..4b1e6b6 100644 --- a/tests/__init__.py +++ b/tests/__init__.py @@ -20,8 +20,9 @@ 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 + def assert_trait_metadata_equal(self, a, b): + # individual and mutation metadata can have nan's in it, + # thus this function assert isinstance(a, dict) assert type(a) == type(b) assert a.keys() == b.keys() @@ -34,7 +35,7 @@ def assert_indiv_metadata_equal(self, a, b): for x, y in zip(apt, bpt): assert x.keys() == y.keys() for k in x: - if k == "phenotype": + if k == "phenotype" or k == "dominance": assert (np.isnan(x[k]) and np.isnan(y[k])) or (x[k] == y[k]) else: assert x[k] == y[k] @@ -102,7 +103,7 @@ def verify_trees_equal(self, ts1, ts2): assert n1.metadata == n2.metadata i1 = ts1.individual(n1.individual) i2 = ts2.individual(n2.individual) - self.assert_indiv_metadata_equal(i1.metadata, i2.metadata) + self.assert_trait_metadata_equal(i1.metadata, i2.metadata) for _ in range(10): pos = random.uniform(0, ts1.sequence_length) t1 = ts1.at(pos) diff --git a/tests/test_annotation.py b/tests/test_annotation.py index 56ae5b5..531b42a 100644 --- a/tests/test_annotation.py +++ b/tests/test_annotation.py @@ -271,7 +271,7 @@ def verify_remapping(self, ts, rts, subpop_map): md = i.metadata rmd = ri.metadata md["subpopulation"] = fwd_map[md["subpopulation"]] - self.assert_indiv_metadata_equal(md, rmd) + self.assert_trait_metadata_equal(md, rmd) def test_annotate_errors(self, helper_functions): for ts in helper_functions.get_msprime_examples(): diff --git a/tests/test_tree_sequence.py b/tests/test_tree_sequence.py index ed41bd1..2544ac8 100644 --- a/tests/test_tree_sequence.py +++ b/tests/test_tree_sequence.py @@ -59,11 +59,11 @@ 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} + mut_info = pyslim.mutation_metadata(ts) assert len(mut_info) == len(mdl) for mut in ts.mutations(): for j in mut.derived_state.split(","): - assert j in mut_info + assert int(j) in mut_info class TestMutationMetadata(tests.PyslimTestCase): @@ -81,6 +81,16 @@ def test_mutation_metadata(self, recipe): for _, ts in recipe["ts"].items(): verify_mutation_metadata(ts) + @pytest.mark.parametrize("recipe", recipe_eq("traits"), indirect=True) + def test_mutation_metadata_extra_arg(self, recipe): + for _, ts in recipe["ts"].items(): + md1 = pyslim.mutation_metadata(ts) + md2 = pyslim.mutation_metadata(ts, ts_metadata=ts.metadata) + assert len(md1) == len(md2) + for x in md1: + assert x in md2 + self.assert_trait_metadata_equal(md1[x], md2[x]) + @pytest.mark.parametrize("recipe", [next(recipe_eq())], indirect=True) def test_check(self, recipe): for _, ts in recipe["ts"].items(): @@ -329,7 +339,7 @@ def test_unique_names(self): @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 + recomb_rate = 0.1 / ts.sequence_length recap = self.do_recapitate( ts, recombination_rate=recomb_rate, ancestral_Ne=10, random_seed=5 ) @@ -1420,6 +1430,7 @@ def test_has_vacant_samples(self, recipe): chrom_type = ts.metadata["SLiM"]["this_chromosome"]["type"] if chrom_type != "A": assert pyslim.has_vacant_samples(ts) + assert pyslim.has_vacant_samples(ts, ts.metadata) def test_has_vacant_msprime(self): ts = msprime.sim_ancestry( @@ -1472,6 +1483,11 @@ def test_remove_restore_vacant(self, recipe): self.verify_remove_vacant(ts, rts) rrts = pyslim.restore_vacant(rts) self.verify_restore_vacant(ts, rrts) + # with metadata explicitly + rts = pyslim.remove_vacant(ts, ts.metadata) + self.verify_remove_vacant(ts, rts) + rrts = pyslim.restore_vacant(rts, rts.metadata) + self.verify_restore_vacant(ts, rrts) def test_recapitate_keeps_vacant(self, recipe): for chrom, ts in recipe["ts"].items(): diff --git a/uv.lock b/uv.lock index fbf98b3..2a5d8c8 100644 --- a/uv.lock +++ b/uv.lock @@ -2,8 +2,10 @@ version = 1 revision = 3 requires-python = ">=3.11" resolution-markers = [ - "python_full_version >= '3.12'", - "python_full_version < '3.12'", + "python_full_version >= '3.12' and python_full_version < '3.14' and sys_platform == 'emscripten'", + "(python_full_version >= '3.12' and sys_platform != 'emscripten') or (python_full_version >= '3.14' and sys_platform == 'emscripten')", + "python_full_version < '3.12' and sys_platform == 'emscripten'", + "python_full_version < '3.12' and sys_platform != 'emscripten'", ] [[package]] @@ -458,7 +460,7 @@ name = "cryptography" version = "46.0.5" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "cffi", marker = "platform_python_implementation != 'PyPy'" }, + { name = "cffi", marker = "(python_full_version >= '3.14' and platform_python_implementation != 'PyPy') or (platform_python_implementation != 'PyPy' and sys_platform != 'emscripten')" }, ] sdist = { url = "https://files.pythonhosted.org/packages/60/04/ee2a9e8542e4fa2773b81771ff8349ff19cdd56b7258a0cc442639052edb/cryptography-46.0.5.tar.gz", hash = "sha256:abace499247268e3757271b2f1e244b36b06f8515cf27c4d49468fc9eb16e93d", size = 750064, upload-time = "2026-02-10T19:18:38.255Z" } wheels = [ @@ -1274,6 +1276,22 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/b3/38/89ba8ad64ae25be8de66a6d463314cf1eb366222074cfda9ee839c56a4b4/mdurl-0.1.2-py3-none-any.whl", hash = "sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8", size = 9979, upload-time = "2022-08-14T12:40:09.779Z" }, ] +[[package]] +name = "mizani" +version = "0.14.4" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "numpy" }, + { name = "pandas" }, + { name = "scipy", version = "1.17.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.12'" }, + { name = "scipy", version = "1.18.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.12'" }, + { name = "tzdata", marker = "sys_platform == 'emscripten' or sys_platform == 'win32'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/a7/02/43fcf763c70e8aa8edc28ac65713daca2c18d3bc2b998af4647966b5bafb/mizani-0.14.4.tar.gz", hash = "sha256:28934d91516d922d7cb0382c82a6c513692abc0174c42a50294ae571520633f9", size = 772490, upload-time = "2026-01-28T14:42:18.108Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/bd/30/b6617c74a8234ff60265373ef730eb6378ccdda74042f51f9ac936191664/mizani-0.14.4-py3-none-any.whl", hash = "sha256:ed72bf249e2a18b5dcc65cd54c7eaa5444b2cb09c7e18aafa2ab6f05f1b78620", size = 133471, upload-time = "2026-01-28T14:42:16.328Z" }, +] + [[package]] name = "more-itertools" version = "10.8.0" @@ -1570,12 +1588,24 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/b6/61/fae042894f4296ec49e3f193aff5d7c18440da9e48102c3315e1bc4519a7/parso-0.8.6-py2.py3-none-any.whl", hash = "sha256:2c549f800b70a5c4952197248825584cb00f033b29c692671d3bf08bf380baff", size = 106894, upload-time = "2026-02-09T15:45:21.391Z" }, ] +[[package]] +name = "patsy" +version = "1.0.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "numpy" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/be/44/ed13eccdd0519eff265f44b670d46fbb0ec813e2274932dc1c0e48520f7d/patsy-1.0.2.tar.gz", hash = "sha256:cdc995455f6233e90e22de72c37fcadb344e7586fb83f06696f54d92f8ce74c0", size = 399942, upload-time = "2025-10-20T16:17:37.535Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f1/70/ba4b949bdc0490ab78d545459acd7702b211dfccf7eb89bbc1060f52818d/patsy-1.0.2-py2.py3-none-any.whl", hash = "sha256:37bfddbc58fcf0362febb5f54f10743f8b21dd2aa73dec7e7ef59d1b02ae668a", size = 233301, upload-time = "2025-10-20T16:17:36.563Z" }, +] + [[package]] name = "pexpect" version = "4.9.0" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "ptyprocess" }, + { name = "ptyprocess", marker = "python_full_version >= '3.14' or sys_platform != 'emscripten'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/42/92/cc564bf6381ff43ce1f4d06852fc19a2f11d180f23dc32d9588bee2f149d/pexpect-4.9.0.tar.gz", hash = "sha256:ee7d41123f3c9911050ea2c2dac107568dc43b2d3b0c7557a33212c398ead30f", size = 166450, upload-time = "2023-11-25T09:07:26.339Z" } wheels = [ @@ -1678,6 +1708,25 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/48/31/05e764397056194206169869b50cf2fee4dbbbc71b344705b9c0d878d4d8/platformdirs-4.9.2-py3-none-any.whl", hash = "sha256:9170634f126f8efdae22fb58ae8a0eaa86f38365bc57897a6c4f781d1f5875bd", size = 21168, upload-time = "2026-02-16T03:56:08.891Z" }, ] +[[package]] +name = "plotnine" +version = "0.15.8" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "matplotlib" }, + { name = "mizani" }, + { name = "numpy" }, + { name = "pandas" }, + { name = "scipy", version = "1.17.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.12'" }, + { name = "scipy", version = "1.18.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.12'" }, + { name = "statsmodels", version = "0.14.4", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.14' and sys_platform == 'emscripten'" }, + { name = "statsmodels", version = "0.14.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.14' or sys_platform != 'emscripten'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/9f/5d/3471689cf15cf2b6ebb6ebdebc7e33831fb35d85e2323875b5a0e150505d/plotnine-0.15.8.tar.gz", hash = "sha256:d3859997c3abd6edff6dee376d912716e75bbe0b0099b7054176d84fe11e8e20", size = 6863148, upload-time = "2026-08-14T16:27:49.399Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/40/37/7a0275ea8143f0f0a2b27db73656d7ea3459e9cf8f869e292e450b357d1e/plotnine-0.15.8-py3-none-any.whl", hash = "sha256:b8a17fbccdf331f0474ff59ea03186c07d96586eb0ebe70121b03fbf131881a9", size = 1573224, upload-time = "2026-08-14T16:27:47.601Z" }, +] + [[package]] name = "pluggy" version = "1.6.0" @@ -1858,6 +1907,7 @@ dev = [ { name = "matplotlib" }, { name = "msprime" }, { name = "pandas" }, + { name = "plotnine" }, { name = "prek" }, { name = "pytest" }, { name = "pytest-cov" }, @@ -1875,6 +1925,7 @@ docs = [ { name = "matplotlib" }, { name = "msprime" }, { name = "pandas" }, + { name = "plotnine" }, { name = "sphinx-argparse" }, { name = "sphinx-issues" }, ] @@ -1913,6 +1964,7 @@ dev = [ { name = "matplotlib" }, { name = "msprime" }, { name = "pandas" }, + { name = "plotnine" }, { name = "prek", specifier = "==0.3.3" }, { name = "pytest" }, { name = "pytest-cov" }, @@ -1930,6 +1982,7 @@ docs = [ { name = "matplotlib" }, { name = "msprime" }, { name = "pandas" }, + { name = "plotnine" }, { name = "sphinx-argparse" }, { name = "sphinx-issues" }, ] @@ -2356,13 +2409,143 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/2a/07/5bda6a85b220c64c65686bc85bd0bbb23b29c62b3a9f9433fa55f17cda93/ruff-0.15.1-py3-none-win_arm64.whl", hash = "sha256:5ff7d5f0f88567850f45081fac8f4ec212be8d0b963e385c3f7d0d2eb4899416", size = 10874604, upload-time = "2026-02-12T23:09:05.515Z" }, ] +[[package]] +name = "scipy" +version = "1.17.1" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version < '3.12' and sys_platform == 'emscripten'", + "python_full_version < '3.12' and sys_platform != 'emscripten'", +] +dependencies = [ + { name = "numpy", marker = "python_full_version < '3.12'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/7a/97/5a3609c4f8d58b039179648e62dd220f89864f56f7357f5d4f45c29eb2cc/scipy-1.17.1.tar.gz", hash = "sha256:95d8e012d8cb8816c226aef832200b1d45109ed4464303e997c5b13122b297c0", size = 30573822, upload-time = "2026-02-23T00:26:24.851Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/df/75/b4ce781849931fef6fd529afa6b63711d5a733065722d0c3e2724af9e40a/scipy-1.17.1-cp311-cp311-macosx_10_14_x86_64.whl", hash = "sha256:1f95b894f13729334fb990162e911c9e5dc1ab390c58aa6cbecb389c5b5e28ec", size = 31613675, upload-time = "2026-02-23T00:16:00.13Z" }, + { url = "https://files.pythonhosted.org/packages/f7/58/bccc2861b305abdd1b8663d6130c0b3d7cc22e8d86663edbc8401bfd40d4/scipy-1.17.1-cp311-cp311-macosx_12_0_arm64.whl", hash = "sha256:e18f12c6b0bc5a592ed23d3f7b891f68fd7f8241d69b7883769eb5d5dfb52696", size = 28162057, upload-time = "2026-02-23T00:16:09.456Z" }, + { url = "https://files.pythonhosted.org/packages/6d/ee/18146b7757ed4976276b9c9819108adbc73c5aad636e5353e20746b73069/scipy-1.17.1-cp311-cp311-macosx_14_0_arm64.whl", hash = "sha256:a3472cfbca0a54177d0faa68f697d8ba4c80bbdc19908c3465556d9f7efce9ee", size = 20334032, upload-time = "2026-02-23T00:16:17.358Z" }, + { url = "https://files.pythonhosted.org/packages/ec/e6/cef1cf3557f0c54954198554a10016b6a03b2ec9e22a4e1df734936bd99c/scipy-1.17.1-cp311-cp311-macosx_14_0_x86_64.whl", hash = "sha256:766e0dc5a616d026a3a1cffa379af959671729083882f50307e18175797b3dfd", size = 22709533, upload-time = "2026-02-23T00:16:25.791Z" }, + { url = "https://files.pythonhosted.org/packages/4d/60/8804678875fc59362b0fb759ab3ecce1f09c10a735680318ac30da8cd76b/scipy-1.17.1-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:744b2bf3640d907b79f3fd7874efe432d1cf171ee721243e350f55234b4cec4c", size = 33062057, upload-time = "2026-02-23T00:16:36.931Z" }, + { url = "https://files.pythonhosted.org/packages/09/7d/af933f0f6e0767995b4e2d705a0665e454d1c19402aa7e895de3951ebb04/scipy-1.17.1-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:43af8d1f3bea642559019edfe64e9b11192a8978efbd1539d7bc2aaa23d92de4", size = 35349300, upload-time = "2026-02-23T00:16:49.108Z" }, + { url = "https://files.pythonhosted.org/packages/b4/3d/7ccbbdcbb54c8fdc20d3b6930137c782a163fa626f0aef920349873421ba/scipy-1.17.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:cd96a1898c0a47be4520327e01f874acfd61fb48a9420f8aa9f6483412ffa444", size = 35127333, upload-time = "2026-02-23T00:17:01.293Z" }, + { url = "https://files.pythonhosted.org/packages/e8/19/f926cb11c42b15ba08e3a71e376d816ac08614f769b4f47e06c3580c836a/scipy-1.17.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:4eb6c25dd62ee8d5edf68a8e1c171dd71c292fdae95d8aeb3dd7d7de4c364082", size = 37741314, upload-time = "2026-02-23T00:17:12.576Z" }, + { url = "https://files.pythonhosted.org/packages/95/da/0d1df507cf574b3f224ccc3d45244c9a1d732c81dcb26b1e8a766ae271a8/scipy-1.17.1-cp311-cp311-win_amd64.whl", hash = "sha256:d30e57c72013c2a4fe441c2fcb8e77b14e152ad48b5464858e07e2ad9fbfceff", size = 36607512, upload-time = "2026-02-23T00:17:23.424Z" }, + { url = "https://files.pythonhosted.org/packages/68/7f/bdd79ceaad24b671543ffe0ef61ed8e659440eb683b66f033454dcee90eb/scipy-1.17.1-cp311-cp311-win_arm64.whl", hash = "sha256:9ecb4efb1cd6e8c4afea0daa91a87fbddbce1b99d2895d151596716c0b2e859d", size = 24599248, upload-time = "2026-02-23T00:17:34.561Z" }, + { url = "https://files.pythonhosted.org/packages/35/48/b992b488d6f299dbe3f11a20b24d3dda3d46f1a635ede1c46b5b17a7b163/scipy-1.17.1-cp312-cp312-macosx_10_14_x86_64.whl", hash = "sha256:35c3a56d2ef83efc372eaec584314bd0ef2e2f0d2adb21c55e6ad5b344c0dcb8", size = 31610954, upload-time = "2026-02-23T00:17:49.855Z" }, + { url = "https://files.pythonhosted.org/packages/b2/02/cf107b01494c19dc100f1d0b7ac3cc08666e96ba2d64db7626066cee895e/scipy-1.17.1-cp312-cp312-macosx_12_0_arm64.whl", hash = "sha256:fcb310ddb270a06114bb64bbe53c94926b943f5b7f0842194d585c65eb4edd76", size = 28172662, upload-time = "2026-02-23T00:18:01.64Z" }, + { url = "https://files.pythonhosted.org/packages/cf/a9/599c28631bad314d219cf9ffd40e985b24d603fc8a2f4ccc5ae8419a535b/scipy-1.17.1-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:cc90d2e9c7e5c7f1a482c9875007c095c3194b1cfedca3c2f3291cdc2bc7c086", size = 20344366, upload-time = "2026-02-23T00:18:12.015Z" }, + { url = "https://files.pythonhosted.org/packages/35/f5/906eda513271c8deb5af284e5ef0206d17a96239af79f9fa0aebfe0e36b4/scipy-1.17.1-cp312-cp312-macosx_14_0_x86_64.whl", hash = "sha256:c80be5ede8f3f8eded4eff73cc99a25c388ce98e555b17d31da05287015ffa5b", size = 22704017, upload-time = "2026-02-23T00:18:21.502Z" }, + { url = "https://files.pythonhosted.org/packages/da/34/16f10e3042d2f1d6b66e0428308ab52224b6a23049cb2f5c1756f713815f/scipy-1.17.1-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e19ebea31758fac5893a2ac360fedd00116cbb7628e650842a6691ba7ca28a21", size = 32927842, upload-time = "2026-02-23T00:18:35.367Z" }, + { url = "https://files.pythonhosted.org/packages/01/8e/1e35281b8ab6d5d72ebe9911edcdffa3f36b04ed9d51dec6dd140396e220/scipy-1.17.1-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:02ae3b274fde71c5e92ac4d54bc06c42d80e399fec704383dcd99b301df37458", size = 35235890, upload-time = "2026-02-23T00:18:49.188Z" }, + { url = "https://files.pythonhosted.org/packages/c5/5c/9d7f4c88bea6e0d5a4f1bc0506a53a00e9fcb198de372bfe4d3652cef482/scipy-1.17.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:8a604bae87c6195d8b1045eddece0514d041604b14f2727bbc2b3020172045eb", size = 35003557, upload-time = "2026-02-23T00:18:54.74Z" }, + { url = "https://files.pythonhosted.org/packages/65/94/7698add8f276dbab7a9de9fb6b0e02fc13ee61d51c7c3f85ac28b65e1239/scipy-1.17.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:f590cd684941912d10becc07325a3eeb77886fe981415660d9265c4c418d0bea", size = 37625856, upload-time = "2026-02-23T00:19:00.307Z" }, + { url = "https://files.pythonhosted.org/packages/a2/84/dc08d77fbf3d87d3ee27f6a0c6dcce1de5829a64f2eae85a0ecc1f0daa73/scipy-1.17.1-cp312-cp312-win_amd64.whl", hash = "sha256:41b71f4a3a4cab9d366cd9065b288efc4d4f3c0b37a91a8e0947fb5bd7f31d87", size = 36549682, upload-time = "2026-02-23T00:19:07.67Z" }, + { url = "https://files.pythonhosted.org/packages/bc/98/fe9ae9ffb3b54b62559f52dedaebe204b408db8109a8c66fdd04869e6424/scipy-1.17.1-cp312-cp312-win_arm64.whl", hash = "sha256:f4115102802df98b2b0db3cce5cb9b92572633a1197c77b7553e5203f284a5b3", size = 24547340, upload-time = "2026-02-23T00:19:12.024Z" }, + { url = "https://files.pythonhosted.org/packages/76/27/07ee1b57b65e92645f219b37148a7e7928b82e2b5dbeccecb4dff7c64f0b/scipy-1.17.1-cp313-cp313-macosx_10_14_x86_64.whl", hash = "sha256:5e3c5c011904115f88a39308379c17f91546f77c1667cea98739fe0fccea804c", size = 31590199, upload-time = "2026-02-23T00:19:17.192Z" }, + { url = "https://files.pythonhosted.org/packages/ec/ae/db19f8ab842e9b724bf5dbb7db29302a91f1e55bc4d04b1025d6d605a2c5/scipy-1.17.1-cp313-cp313-macosx_12_0_arm64.whl", hash = "sha256:6fac755ca3d2c3edcb22f479fceaa241704111414831ddd3bc6056e18516892f", size = 28154001, upload-time = "2026-02-23T00:19:22.241Z" }, + { url = "https://files.pythonhosted.org/packages/5b/58/3ce96251560107b381cbd6e8413c483bbb1228a6b919fa8652b0d4090e7f/scipy-1.17.1-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:7ff200bf9d24f2e4d5dc6ee8c3ac64d739d3a89e2326ba68aaf6c4a2b838fd7d", size = 20325719, upload-time = "2026-02-23T00:19:26.329Z" }, + { url = "https://files.pythonhosted.org/packages/b2/83/15087d945e0e4d48ce2377498abf5ad171ae013232ae31d06f336e64c999/scipy-1.17.1-cp313-cp313-macosx_14_0_x86_64.whl", hash = "sha256:4b400bdc6f79fa02a4d86640310dde87a21fba0c979efff5248908c6f15fad1b", size = 22683595, upload-time = "2026-02-23T00:19:30.304Z" }, + { url = "https://files.pythonhosted.org/packages/b4/e0/e58fbde4a1a594c8be8114eb4aac1a55bcd6587047efc18a61eb1f5c0d30/scipy-1.17.1-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2b64ca7d4aee0102a97f3ba22124052b4bd2152522355073580bf4845e2550b6", size = 32896429, upload-time = "2026-02-23T00:19:35.536Z" }, + { url = "https://files.pythonhosted.org/packages/f5/5f/f17563f28ff03c7b6799c50d01d5d856a1d55f2676f537ca8d28c7f627cd/scipy-1.17.1-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:581b2264fc0aa555f3f435a5944da7504ea3a065d7029ad60e7c3d1ae09c5464", size = 35203952, upload-time = "2026-02-23T00:19:42.259Z" }, + { url = "https://files.pythonhosted.org/packages/8d/a5/9afd17de24f657fdfe4df9a3f1ea049b39aef7c06000c13db1530d81ccca/scipy-1.17.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:beeda3d4ae615106d7094f7e7cef6218392e4465cc95d25f900bebabfded0950", size = 34979063, upload-time = "2026-02-23T00:19:47.547Z" }, + { url = "https://files.pythonhosted.org/packages/8b/13/88b1d2384b424bf7c924f2038c1c409f8d88bb2a8d49d097861dd64a57b2/scipy-1.17.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:6609bc224e9568f65064cfa72edc0f24ee6655b47575954ec6339534b2798369", size = 37598449, upload-time = "2026-02-23T00:19:53.238Z" }, + { url = "https://files.pythonhosted.org/packages/35/e5/d6d0e51fc888f692a35134336866341c08655d92614f492c6860dc45bb2c/scipy-1.17.1-cp313-cp313-win_amd64.whl", hash = "sha256:37425bc9175607b0268f493d79a292c39f9d001a357bebb6b88fdfaff13f6448", size = 36510943, upload-time = "2026-02-23T00:20:50.89Z" }, + { url = "https://files.pythonhosted.org/packages/2a/fd/3be73c564e2a01e690e19cc618811540ba5354c67c8680dce3281123fb79/scipy-1.17.1-cp313-cp313-win_arm64.whl", hash = "sha256:5cf36e801231b6a2059bf354720274b7558746f3b1a4efb43fcf557ccd484a87", size = 24545621, upload-time = "2026-02-23T00:20:55.871Z" }, + { url = "https://files.pythonhosted.org/packages/6f/6b/17787db8b8114933a66f9dcc479a8272e4b4da75fe03b0c282f7b0ade8cd/scipy-1.17.1-cp313-cp313t-macosx_10_14_x86_64.whl", hash = "sha256:d59c30000a16d8edc7e64152e30220bfbd724c9bbb08368c054e24c651314f0a", size = 31936708, upload-time = "2026-02-23T00:19:58.694Z" }, + { url = "https://files.pythonhosted.org/packages/38/2e/524405c2b6392765ab1e2b722a41d5da33dc5c7b7278184a8ad29b6cb206/scipy-1.17.1-cp313-cp313t-macosx_12_0_arm64.whl", hash = "sha256:010f4333c96c9bb1a4516269e33cb5917b08ef2166d5556ca2fd9f082a9e6ea0", size = 28570135, upload-time = "2026-02-23T00:20:03.934Z" }, + { url = "https://files.pythonhosted.org/packages/fd/c3/5bd7199f4ea8556c0c8e39f04ccb014ac37d1468e6cfa6a95c6b3562b76e/scipy-1.17.1-cp313-cp313t-macosx_14_0_arm64.whl", hash = "sha256:2ceb2d3e01c5f1d83c4189737a42d9cb2fc38a6eeed225e7515eef71ad301dce", size = 20741977, upload-time = "2026-02-23T00:20:07.935Z" }, + { url = "https://files.pythonhosted.org/packages/d9/b8/8ccd9b766ad14c78386599708eb745f6b44f08400a5fd0ade7cf89b6fc93/scipy-1.17.1-cp313-cp313t-macosx_14_0_x86_64.whl", hash = "sha256:844e165636711ef41f80b4103ed234181646b98a53c8f05da12ca5ca289134f6", size = 23029601, upload-time = "2026-02-23T00:20:12.161Z" }, + { url = "https://files.pythonhosted.org/packages/6d/a0/3cb6f4d2fb3e17428ad2880333cac878909ad1a89f678527b5328b93c1d4/scipy-1.17.1-cp313-cp313t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:158dd96d2207e21c966063e1635b1063cd7787b627b6f07305315dd73d9c679e", size = 33019667, upload-time = "2026-02-23T00:20:17.208Z" }, + { url = "https://files.pythonhosted.org/packages/f3/c3/2d834a5ac7bf3a0c806ad1508efc02dda3c8c61472a56132d7894c312dea/scipy-1.17.1-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:74cbb80d93260fe2ffa334efa24cb8f2f0f622a9b9febf8b483c0b865bfb3475", size = 35264159, upload-time = "2026-02-23T00:20:23.087Z" }, + { url = "https://files.pythonhosted.org/packages/4d/77/d3ed4becfdbd217c52062fafe35a72388d1bd82c2d0ba5ca19d6fcc93e11/scipy-1.17.1-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:dbc12c9f3d185f5c737d801da555fb74b3dcfa1a50b66a1a93e09190f41fab50", size = 35102771, upload-time = "2026-02-23T00:20:28.636Z" }, + { url = "https://files.pythonhosted.org/packages/bd/12/d19da97efde68ca1ee5538bb261d5d2c062f0c055575128f11a2730e3ac1/scipy-1.17.1-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:94055a11dfebe37c656e70317e1996dc197e1a15bbcc351bcdd4610e128fe1ca", size = 37665910, upload-time = "2026-02-23T00:20:34.743Z" }, + { url = "https://files.pythonhosted.org/packages/06/1c/1172a88d507a4baaf72c5a09bb6c018fe2ae0ab622e5830b703a46cc9e44/scipy-1.17.1-cp313-cp313t-win_amd64.whl", hash = "sha256:e30bdeaa5deed6bc27b4cc490823cd0347d7dae09119b8803ae576ea0ce52e4c", size = 36562980, upload-time = "2026-02-23T00:20:40.575Z" }, + { url = "https://files.pythonhosted.org/packages/70/b0/eb757336e5a76dfa7911f63252e3b7d1de00935d7705cf772db5b45ec238/scipy-1.17.1-cp313-cp313t-win_arm64.whl", hash = "sha256:a720477885a9d2411f94a93d16f9d89bad0f28ca23c3f8daa521e2dcc3f44d49", size = 24856543, upload-time = "2026-02-23T00:20:45.313Z" }, + { url = "https://files.pythonhosted.org/packages/cf/83/333afb452af6f0fd70414dc04f898647ee1423979ce02efa75c3b0f2c28e/scipy-1.17.1-cp314-cp314-macosx_10_14_x86_64.whl", hash = "sha256:a48a72c77a310327f6a3a920092fa2b8fd03d7deaa60f093038f22d98e096717", size = 31584510, upload-time = "2026-02-23T00:21:01.015Z" }, + { url = "https://files.pythonhosted.org/packages/ed/a6/d05a85fd51daeb2e4ea71d102f15b34fedca8e931af02594193ae4fd25f7/scipy-1.17.1-cp314-cp314-macosx_12_0_arm64.whl", hash = "sha256:45abad819184f07240d8a696117a7aacd39787af9e0b719d00285549ed19a1e9", size = 28170131, upload-time = "2026-02-23T00:21:05.888Z" }, + { url = "https://files.pythonhosted.org/packages/db/7b/8624a203326675d7746a254083a187398090a179335b2e4a20e2ddc46e83/scipy-1.17.1-cp314-cp314-macosx_14_0_arm64.whl", hash = "sha256:3fd1fcdab3ea951b610dc4cef356d416d5802991e7e32b5254828d342f7b7e0b", size = 20342032, upload-time = "2026-02-23T00:21:09.904Z" }, + { url = "https://files.pythonhosted.org/packages/c9/35/2c342897c00775d688d8ff3987aced3426858fd89d5a0e26e020b660b301/scipy-1.17.1-cp314-cp314-macosx_14_0_x86_64.whl", hash = "sha256:7bdf2da170b67fdf10bca777614b1c7d96ae3ca5794fd9587dce41eb2966e866", size = 22678766, upload-time = "2026-02-23T00:21:14.313Z" }, + { url = "https://files.pythonhosted.org/packages/ef/f2/7cdb8eb308a1a6ae1e19f945913c82c23c0c442a462a46480ce487fdc0ac/scipy-1.17.1-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:adb2642e060a6549c343603a3851ba76ef0b74cc8c079a9a58121c7ec9fe2350", size = 32957007, upload-time = "2026-02-23T00:21:19.663Z" }, + { url = "https://files.pythonhosted.org/packages/0b/2e/7eea398450457ecb54e18e9d10110993fa65561c4f3add5e8eccd2b9cd41/scipy-1.17.1-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:eee2cfda04c00a857206a4330f0c5e3e56535494e30ca445eb19ec624ae75118", size = 35221333, upload-time = "2026-02-23T00:21:25.278Z" }, + { url = "https://files.pythonhosted.org/packages/d9/77/5b8509d03b77f093a0d52e606d3c4f79e8b06d1d38c441dacb1e26cacf46/scipy-1.17.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:d2650c1fb97e184d12d8ba010493ee7b322864f7d3d00d3f9bb97d9c21de4068", size = 35042066, upload-time = "2026-02-23T00:21:31.358Z" }, + { url = "https://files.pythonhosted.org/packages/f9/df/18f80fb99df40b4070328d5ae5c596f2f00fffb50167e31439e932f29e7d/scipy-1.17.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:08b900519463543aa604a06bec02461558a6e1cef8fdbb8098f77a48a83c8118", size = 37612763, upload-time = "2026-02-23T00:21:37.247Z" }, + { url = "https://files.pythonhosted.org/packages/4b/39/f0e8ea762a764a9dc52aa7dabcfad51a354819de1f0d4652b6a1122424d6/scipy-1.17.1-cp314-cp314-win_amd64.whl", hash = "sha256:3877ac408e14da24a6196de0ddcace62092bfc12a83823e92e49e40747e52c19", size = 37290984, upload-time = "2026-02-23T00:22:35.023Z" }, + { url = "https://files.pythonhosted.org/packages/7c/56/fe201e3b0f93d1a8bcf75d3379affd228a63d7e2d80ab45467a74b494947/scipy-1.17.1-cp314-cp314-win_arm64.whl", hash = "sha256:f8885db0bc2bffa59d5c1b72fad7a6a92d3e80e7257f967dd81abb553a90d293", size = 25192877, upload-time = "2026-02-23T00:22:39.798Z" }, + { url = "https://files.pythonhosted.org/packages/96/ad/f8c414e121f82e02d76f310f16db9899c4fcde36710329502a6b2a3c0392/scipy-1.17.1-cp314-cp314t-macosx_10_14_x86_64.whl", hash = "sha256:1cc682cea2ae55524432f3cdff9e9a3be743d52a7443d0cba9017c23c87ae2f6", size = 31949750, upload-time = "2026-02-23T00:21:42.289Z" }, + { url = "https://files.pythonhosted.org/packages/7c/b0/c741e8865d61b67c81e255f4f0a832846c064e426636cd7de84e74d209be/scipy-1.17.1-cp314-cp314t-macosx_12_0_arm64.whl", hash = "sha256:2040ad4d1795a0ae89bfc7e8429677f365d45aa9fd5e4587cf1ea737f927b4a1", size = 28585858, upload-time = "2026-02-23T00:21:47.706Z" }, + { url = "https://files.pythonhosted.org/packages/ed/1b/3985219c6177866628fa7c2595bfd23f193ceebbe472c98a08824b9466ff/scipy-1.17.1-cp314-cp314t-macosx_14_0_arm64.whl", hash = "sha256:131f5aaea57602008f9822e2115029b55d4b5f7c070287699fe45c661d051e39", size = 20757723, upload-time = "2026-02-23T00:21:52.039Z" }, + { url = "https://files.pythonhosted.org/packages/c0/19/2a04aa25050d656d6f7b9e7b685cc83d6957fb101665bfd9369ca6534563/scipy-1.17.1-cp314-cp314t-macosx_14_0_x86_64.whl", hash = "sha256:9cdc1a2fcfd5c52cfb3045feb399f7b3ce822abdde3a193a6b9a60b3cb5854ca", size = 23043098, upload-time = "2026-02-23T00:21:56.185Z" }, + { url = "https://files.pythonhosted.org/packages/86/f1/3383beb9b5d0dbddd030335bf8a8b32d4317185efe495374f134d8be6cce/scipy-1.17.1-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6e3dcd57ab780c741fde8dc68619de988b966db759a3c3152e8e9142c26295ad", size = 33030397, upload-time = "2026-02-23T00:22:01.404Z" }, + { url = "https://files.pythonhosted.org/packages/41/68/8f21e8a65a5a03f25a79165ec9d2b28c00e66dc80546cf5eb803aeeff35b/scipy-1.17.1-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a9956e4d4f4a301ebf6cde39850333a6b6110799d470dbbb1e25326ac447f52a", size = 35281163, upload-time = "2026-02-23T00:22:07.024Z" }, + { url = "https://files.pythonhosted.org/packages/84/8d/c8a5e19479554007a5632ed7529e665c315ae7492b4f946b0deb39870e39/scipy-1.17.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:a4328d245944d09fd639771de275701ccadf5f781ba0ff092ad141e017eccda4", size = 35116291, upload-time = "2026-02-23T00:22:12.585Z" }, + { url = "https://files.pythonhosted.org/packages/52/52/e57eceff0e342a1f50e274264ed47497b59e6a4e3118808ee58ddda7b74a/scipy-1.17.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:a77cbd07b940d326d39a1d1b37817e2ee4d79cb30e7338f3d0cddffae70fcaa2", size = 37682317, upload-time = "2026-02-23T00:22:18.513Z" }, + { url = "https://files.pythonhosted.org/packages/11/2f/b29eafe4a3fbc3d6de9662b36e028d5f039e72d345e05c250e121a230dd4/scipy-1.17.1-cp314-cp314t-win_amd64.whl", hash = "sha256:eb092099205ef62cd1782b006658db09e2fed75bffcae7cc0d44052d8aa0f484", size = 37345327, upload-time = "2026-02-23T00:22:24.442Z" }, + { url = "https://files.pythonhosted.org/packages/07/39/338d9219c4e87f3e708f18857ecd24d22a0c3094752393319553096b98af/scipy-1.17.1-cp314-cp314t-win_arm64.whl", hash = "sha256:200e1050faffacc162be6a486a984a0497866ec54149a01270adc8a59b7c7d21", size = 25489165, upload-time = "2026-02-23T00:22:29.563Z" }, +] + +[[package]] +name = "scipy" +version = "1.18.0" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.12' and python_full_version < '3.14' and sys_platform == 'emscripten'", + "(python_full_version >= '3.12' and sys_platform != 'emscripten') or (python_full_version >= '3.14' and sys_platform == 'emscripten')", +] +dependencies = [ + { name = "numpy", marker = "python_full_version >= '3.12'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/a7/25/c2700dfaf6442b4effaa91af24ebce5dc9d31bb4a69706313aae70d72cd0/scipy-1.18.0.tar.gz", hash = "sha256:67b2ad2ad54c72ca6d04975a9b2df8c3638c34ddd5b28738e94fc2b57929d378", size = 30774447, upload-time = "2026-06-19T15:01:43.456Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/6a/19/ca10ead60b0acc80b2b833c2c4a4f2ff753d0f58b811f70d911c7e94a25c/scipy-1.18.0-cp312-cp312-macosx_10_15_x86_64.whl", hash = "sha256:7bd21faaf5a1a3b2eff922d02db5f191b99a6518db9078a8fb23169f6d22259a", size = 31056519, upload-time = "2026-06-19T14:59:45.203Z" }, + { url = "https://files.pythonhosted.org/packages/96/72/1e6442a00cd2924d361aa1b642ab6373ec35c6fabf311a760be9f76e0f13/scipy-1.18.0-cp312-cp312-macosx_12_0_arm64.whl", hash = "sha256:265915e79107de9f946b855e50d7470d5893ec3f54b342e1aa6201cbdcd8bb6b", size = 28681889, upload-time = "2026-06-19T14:59:48.103Z" }, + { url = "https://files.pythonhosted.org/packages/9b/2d/11dd93d21e147a73ba22bd75c0b9208d3a2e0ec76d53170ce7d9029b1015/scipy-1.18.0-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:9ab7b758be6940954a713ee466e2043e9f6e2ed965c1fce5c91039f4be3d90a9", size = 20423580, upload-time = "2026-06-19T14:59:50.665Z" }, + { url = "https://files.pythonhosted.org/packages/9c/01/93552f75e0d2a7dd115a45e59209c51e8d514daff02fc887d2623be06fe1/scipy-1.18.0-cp312-cp312-macosx_14_0_x86_64.whl", hash = "sha256:97b6cddaaee0a779ef6b5ca83c9604b27cc16b2b8fc22c142652df8793319fb8", size = 23054441, upload-time = "2026-06-19T14:59:53.564Z" }, + { url = "https://files.pythonhosted.org/packages/3c/23/21f5e703643d66f21faa6b4c73195bfcad70c55efcb4f1ab327cd7c4101a/scipy-1.18.0-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:52a96e21517c7292375c0e27dd796a811f03fcea5fd4d108fdfea8145dcf17ab", size = 33968720, upload-time = "2026-06-19T14:59:56.415Z" }, + { url = "https://files.pythonhosted.org/packages/dd/aa/1b939f6c67ed68635bb538e6752d3dacc02f66535182e939a89581a44e9c/scipy-1.18.0-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1f55797419e16e7f30cf88ffb3113ce0467f00cfe3f70d5c281730b21769bfc2", size = 35287115, upload-time = "2026-06-19T14:59:59.411Z" }, + { url = "https://files.pythonhosted.org/packages/b6/ff/eec46be7e9234208f801062b53e1983085eddebd693f6c9bfb03b459830d/scipy-1.18.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:ad033410e2e0672ffdc1042110cef20e1c46f8fd0616cee1d44d8d58fad8fc11", size = 35577989, upload-time = "2026-06-19T15:00:02.235Z" }, + { url = "https://files.pythonhosted.org/packages/84/ca/210d4759c7210bb7d269437421959b39a33434e2776b60c5cb8a763bb30a/scipy-1.18.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:4a55985d54c769c872e64b7f4c8a81cc30ef700cc04296abbbf3705439c126de", size = 37421717, upload-time = "2026-06-19T15:00:05.102Z" }, + { url = "https://files.pythonhosted.org/packages/2b/54/9a9edb45345bd6744da5ddfb6628e5d5185920494c6a67ec45b6381004cb/scipy-1.18.0-cp312-cp312-win_amd64.whl", hash = "sha256:71ccc8faa2dd16ac310233203474a8b5cb67f10dedd54a3116d34943f4b19132", size = 36597428, upload-time = "2026-06-19T15:00:08.112Z" }, + { url = "https://files.pythonhosted.org/packages/99/0e/33f32a2a58987e26aec0f7df252cbbad1e90ae77bdbc76f40dd4ed0cf0ea/scipy-1.18.0-cp312-cp312-win_arm64.whl", hash = "sha256:d88363fd9d8fbd3511bd273f1a49efb2a540773ddf92a91d57498ce7dd7f3e76", size = 24351481, upload-time = "2026-06-19T15:00:11.103Z" }, + { url = "https://files.pythonhosted.org/packages/05/52/9c0136c2de7ae0779b7b366447766cec6d9f0702c56bb8ffeb04c8fd3af4/scipy-1.18.0-cp313-cp313-macosx_10_15_x86_64.whl", hash = "sha256:09143f676d157d9f546d663504ef9c1becb819824f1afc018814176411942446", size = 31036107, upload-time = "2026-06-19T15:00:14.03Z" }, + { url = "https://files.pythonhosted.org/packages/02/73/0291a64843270f4efb86cdcf2ee0f2048631b65ec6b405398b2b4dbf11bf/scipy-1.18.0-cp313-cp313-macosx_12_0_arm64.whl", hash = "sha256:5efe260f69417b97ddae455bfb5a95e8359f7f66ad7fa9522a60feb66f169520", size = 28663303, upload-time = "2026-06-19T15:00:16.819Z" }, + { url = "https://files.pythonhosted.org/packages/d3/0f/10ffa0b697a572f4e0d48b92a88895d366422f019f723e7e14a84c050dac/scipy-1.18.0-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:68363b7eaacd8b5dd426df56d782cc156468ac79a127a1b87ca597d6e2e82197", size = 20404960, upload-time = "2026-06-19T15:00:19.635Z" }, + { url = "https://files.pythonhosted.org/packages/7e/d2/e896cea21ba8edd6c81d4c55b1ffcc717e79698dcbebf9641b4cfb4c6622/scipy-1.18.0-cp313-cp313-macosx_14_0_x86_64.whl", hash = "sha256:c5557d8be5da8e41353fcd4d21491fdbab83b062fc579e94dc09a7c8ab4f669b", size = 23034074, upload-time = "2026-06-19T15:00:22.107Z" }, + { url = "https://files.pythonhosted.org/packages/ea/b2/e83ea34279a52c03374477c74006256ec78df65fc877baa4617d6de1d202/scipy-1.18.0-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0d13bca67c096d89fb95ced0d8921807300fce0275643aef9533cc63a0773468", size = 33942038, upload-time = "2026-06-19T15:00:24.964Z" }, + { url = "https://files.pythonhosted.org/packages/f6/af/e8fe5fb136f51e2b01678b92cb4106d10d8cd68ec147ead2e7cb0ac75398/scipy-1.18.0-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a46f9273dbd0eb1cefba61c9b8648b4dfe3cbc14a080176f9a73e44b8336dc7f", size = 35266390, upload-time = "2026-06-19T15:00:28.059Z" }, + { url = "https://files.pythonhosted.org/packages/3a/49/2c5cbb907b56695fc67517811d1db234dfd83381a84814ec220aded2794d/scipy-1.18.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:5aba46108853ddfc77906b6557aac839d2b52e900c1d72a1180adaaab58d265f", size = 35551324, upload-time = "2026-06-19T15:00:31.014Z" }, + { url = "https://files.pythonhosted.org/packages/bb/73/eda39f7a2d306ff0ffc574afd13c0bbb6d10a603d9a413998ee269487a80/scipy-1.18.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:b6f758e35f12757b5d95c00bc6de2438e229c2664b7a92e96f205959d9f2dfa4", size = 37404785, upload-time = "2026-06-19T15:00:34.072Z" }, + { url = "https://files.pythonhosted.org/packages/b7/d2/ae881ee28d014f38e0ccbfd974a06a919ba9af34f1f74bf42b5301891d63/scipy-1.18.0-cp313-cp313-win_amd64.whl", hash = "sha256:1afac4a847207c7ff8efd321734a50b06d0280b3b2a2c0fc2f413101747ad7c7", size = 36554943, upload-time = "2026-06-19T15:00:36.903Z" }, + { url = "https://files.pythonhosted.org/packages/70/3a/21154e2d54eb3639c6bf4dbae2e531c68356bfe95990daa30df33b30d556/scipy-1.18.0-cp313-cp313-win_arm64.whl", hash = "sha256:c5dbddf60e58c2312316d097271a8e73d40eaf2eabfa4d95ed7d3695bbf2ce7b", size = 24350911, upload-time = "2026-06-19T15:00:40.062Z" }, + { url = "https://files.pythonhosted.org/packages/78/b5/915a19b3de2f7430062b509653563db1633ddbb6f021b06731521115d4e2/scipy-1.18.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:4c256ee70c0d1a8a2ace807e199ccd4e3f57037433842abb3fb36bc17eaa9578", size = 31036253, upload-time = "2026-06-19T15:00:43.216Z" }, + { url = "https://files.pythonhosted.org/packages/d7/88/b72def7262e150d16be13fca37a96481138d624e700340bc3362a7588929/scipy-1.18.0-cp314-cp314-macosx_12_0_arm64.whl", hash = "sha256:2ef3abc54a4ffc53765374b0d5728532dfdd2585ed23f6b11c206a1f0b1b9af8", size = 28673758, upload-time = "2026-06-19T15:00:46.663Z" }, + { url = "https://files.pythonhosted.org/packages/91/02/2e636a61a525632c373cf6a9c24442a3ffb79e364d38e98b32042964ac32/scipy-1.18.0-cp314-cp314-macosx_14_0_arm64.whl", hash = "sha256:f2a6af57bd9e4a75d70e4117e78a1bbee84f79ae3fbb6d0111005d6ebcc4cb8d", size = 20415514, upload-time = "2026-06-19T15:00:49.399Z" }, + { url = "https://files.pythonhosted.org/packages/c9/b6/2135974442f6aba159d9d39d774a1c8cb19947016725d69fecc685df45bf/scipy-1.18.0-cp314-cp314-macosx_14_0_x86_64.whl", hash = "sha256:3f1ac564d3bf6c03d861d2cd87a1bea0da2887136f7fb1bf519c05a8971452d6", size = 23034398, upload-time = "2026-06-19T15:00:51.941Z" }, + { url = "https://files.pythonhosted.org/packages/f6/e6/ba89ec5abf6ee9257c0d1ec985573f3ae32742c24bc03e016388a40b1b15/scipy-1.18.0-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:40395a5fcd1abee49a5c7aaa98c29db393eedc835138560a588c47ec16156690", size = 33998032, upload-time = "2026-06-19T15:00:54.838Z" }, + { url = "https://files.pythonhosted.org/packages/7f/c4/bc41eb19b0fd0db868f4132920879019318d80cc522ad8f2bca4611af808/scipy-1.18.0-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8ca01e8ae69f1b18e9a58d91afead31be3cef0dd905a10249dac559ee15460a0", size = 35283333, upload-time = "2026-06-19T15:00:58.152Z" }, + { url = "https://files.pythonhosted.org/packages/53/a4/cbdeef6eb3830a8462a9d4ada814de5fc984345cc9ecf17cbec51a036f1e/scipy-1.18.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:7a7f3b01647384dbc3a711e8c6778e0aabbe93959249fef5c7393396bcac0867", size = 35610216, upload-time = "2026-06-19T15:01:01.155Z" }, + { url = "https://files.pythonhosted.org/packages/80/4d/b2b82502b65f661d1b789c1665dcdf315d5f12194e06fc0b37946294ebae/scipy-1.18.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:6aa94e78ec192a30063a5e72e561c28af769dc311190b24fe91774eff1969709", size = 37418960, upload-time = "2026-06-19T15:01:04.155Z" }, + { url = "https://files.pythonhosted.org/packages/93/3e/902d836831474b0ab5a37d16404f7bc5fafd9efba632890e271ba952635f/scipy-1.18.0-cp314-cp314-win_amd64.whl", hash = "sha256:2d8bbdc6c817f5b4006a54d799d4f5bab6f910193cbb9a1ff310833d4d270f61", size = 37288845, upload-time = "2026-06-19T15:01:07.822Z" }, + { url = "https://files.pythonhosted.org/packages/b6/43/8d73b337a3bdb14daa0314f0434210747c02d79d729ce1777574a817dcf6/scipy-1.18.0-cp314-cp314-win_arm64.whl", hash = "sha256:18e9575f1569b2c54174e6159d32942e03731177f63dce7975f0a0c88d102f5b", size = 24988971, upload-time = "2026-06-19T15:01:11.076Z" }, + { url = "https://files.pythonhosted.org/packages/b4/b4/f11918b0508a2787031a0499a03fbe3546f3bb5ca05d01038c45b278c09a/scipy-1.18.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:f351e0dd702687d12a402b867a1b4146a256923e1c38317cbc472f6372b94707", size = 31399325, upload-time = "2026-06-19T15:01:13.723Z" }, + { url = "https://files.pythonhosted.org/packages/7b/d1/1f287b57c0ff0ee5185dff3946d92c8017d39b0e431f0ae79a3ff1859512/scipy-1.18.0-cp314-cp314t-macosx_12_0_arm64.whl", hash = "sha256:7c7a51b33ce387193c97f228320cf8e87361daa1bba750638677729598b3e677", size = 29092110, upload-time = "2026-06-19T15:01:16.908Z" }, + { url = "https://files.pythonhosted.org/packages/ff/1a/7b74eb6c392fdcb27d414c0e7558a6d0231eb3b6d73571f479bb81ea8794/scipy-1.18.0-cp314-cp314t-macosx_14_0_arm64.whl", hash = "sha256:84031d7b052a54fae2f8632e0ec802073d385476eb9a63079bce6e23ef9283d4", size = 20833811, upload-time = "2026-06-19T15:01:20.488Z" }, + { url = "https://files.pythonhosted.org/packages/7c/ad/f3941716320a7b9cb4d68734a903b45fe16eff5fb7da7e16f2e619304979/scipy-1.18.0-cp314-cp314t-macosx_14_0_x86_64.whl", hash = "sha256:56abf29a7c067dde59be8b9a22d606a4ea1b2f2a4b756d9d903c62818f5dacce", size = 23396644, upload-time = "2026-06-19T15:01:23.364Z" }, + { url = "https://files.pythonhosted.org/packages/22/22/1446b62ffe07f9719b7d9b1b6a4e05a772833ae8f441fe4c22c34c9b250f/scipy-1.18.0-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1ad44305cfa24b1ba5803cbbebf033590ccbac1aa5d612d727b785325ab408b0", size = 34079318, upload-time = "2026-06-19T15:01:26.002Z" }, + { url = "https://files.pythonhosted.org/packages/56/3b/b87da667098bb470fa30c7011b0ba351ee976dd395c78798c66e941665a3/scipy-1.18.0-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:945c1761b93f38d7f99ae81ae80c63e621471608c7eeead563f6df025585cd58", size = 35324320, upload-time = "2026-06-19T15:01:28.881Z" }, + { url = "https://files.pythonhosted.org/packages/f8/a1/c7932f91909759b0267f75fdea34e91309f96b895757534b76a90b6b4344/scipy-1.18.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:1a4441f15d620578772a49e5ab48c0ee1f7a0220e387110283062729136b2553", size = 35699541, upload-time = "2026-06-19T15:01:31.968Z" }, + { url = "https://files.pythonhosted.org/packages/f7/86/5185061a1fcc41d18c5dc2463969b3a3964b31d9ac67b2fb05d4c7ff7670/scipy-1.18.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:9aac6192fac56bf2ca534389d24623f07b39ff83317d58287285e7fbd622ff76", size = 37472480, upload-time = "2026-06-19T15:01:35.136Z" }, + { url = "https://files.pythonhosted.org/packages/31/8e/f04c68e39919a010d34f2ee1367fd705b0a25a02f609d755f0bfbc0a15fc/scipy-1.18.0-cp314-cp314t-win_amd64.whl", hash = "sha256:e40baea28ae7f5475c779741e2d90b1247c78531207b49c7030e698ff81cee3f", size = 37365390, upload-time = "2026-06-19T15:01:38.091Z" }, + { url = "https://files.pythonhosted.org/packages/d5/19/969dc072906c84dd0a3b05dcf57ea750936087d7873549e408b35cfc3f97/scipy-1.18.0-cp314-cp314t-win_arm64.whl", hash = "sha256:368e0a705903c466aa5f08eefb39e6b1b6b2d659e7352a31fd9e2438365be0f8", size = 25279661, upload-time = "2026-06-19T15:01:40.817Z" }, +] + [[package]] name = "secretstorage" version = "3.5.0" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "cryptography" }, - { name = "jeepney" }, + { name = "cryptography", marker = "python_full_version >= '3.14' or sys_platform != 'emscripten'" }, + { name = "jeepney", marker = "python_full_version >= '3.14' or sys_platform != 'emscripten'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/1c/03/e834bcd866f2f8a49a85eaff47340affa3bfa391ee9912a952a1faa68c7b/secretstorage-3.5.0.tar.gz", hash = "sha256:f04b8e4689cbce351744d5537bf6b1329c6fc68f91fa666f60a380edddcd11be", size = 19884, upload-time = "2025-11-23T19:02:53.191Z" } wheels = [ @@ -2709,6 +2892,68 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/f1/7b/ce1eafaf1a76852e2ec9b22edecf1daa58175c090266e9f6c64afcd81d91/stack_data-0.6.3-py3-none-any.whl", hash = "sha256:d5558e0c25a4cb0853cddad3d77da9891a08cb85dd9f9f91b9f8cd66e511e695", size = 24521, upload-time = "2023-09-30T13:58:03.53Z" }, ] +[[package]] +name = "statsmodels" +version = "0.14.4" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.12' and python_full_version < '3.14' and sys_platform == 'emscripten'", + "python_full_version < '3.12' and sys_platform == 'emscripten'", +] +dependencies = [ + { name = "numpy", marker = "python_full_version < '3.14' and sys_platform == 'emscripten'" }, + { name = "packaging", marker = "python_full_version < '3.14' and sys_platform == 'emscripten'" }, + { name = "pandas", marker = "python_full_version < '3.14' and sys_platform == 'emscripten'" }, + { name = "patsy", marker = "python_full_version < '3.14' and sys_platform == 'emscripten'" }, + { name = "scipy", version = "1.17.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.12' and sys_platform == 'emscripten'" }, + { name = "scipy", version = "1.18.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.12' and python_full_version < '3.14' and sys_platform == 'emscripten'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/1f/3b/963a015dd8ea17e10c7b0e2f14d7c4daec903baf60a017e756b57953a4bf/statsmodels-0.14.4.tar.gz", hash = "sha256:5d69e0f39060dc72c067f9bb6e8033b6dccdb0bae101d76a7ef0bcc94e898b67", size = 20354802, upload-time = "2024-10-03T16:15:36.273Z" } + +[[package]] +name = "statsmodels" +version = "0.14.6" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "(python_full_version >= '3.12' and sys_platform != 'emscripten') or (python_full_version >= '3.14' and sys_platform == 'emscripten')", + "python_full_version < '3.12' and sys_platform != 'emscripten'", +] +dependencies = [ + { name = "numpy", marker = "python_full_version >= '3.14' or sys_platform != 'emscripten'" }, + { name = "packaging", marker = "python_full_version >= '3.14' or sys_platform != 'emscripten'" }, + { name = "pandas", marker = "python_full_version >= '3.14' or sys_platform != 'emscripten'" }, + { name = "patsy", marker = "python_full_version >= '3.14' or sys_platform != 'emscripten'" }, + { name = "scipy", version = "1.17.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.12' and sys_platform != 'emscripten'" }, + { name = "scipy", version = "1.18.0", source = { registry = "https://pypi.org/simple" }, marker = "(python_full_version >= '3.12' and sys_platform != 'emscripten') or (python_full_version >= '3.14' and sys_platform == 'emscripten')" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/0d/81/e8d74b34f85285f7335d30c5e3c2d7c0346997af9f3debf9a0a9a63de184/statsmodels-0.14.6.tar.gz", hash = "sha256:4d17873d3e607d398b85126cd4ed7aad89e4e9d89fc744cdab1af3189a996c2a", size = 20689085, upload-time = "2025-12-05T23:08:39.522Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a9/4d/df4dd089b406accfc3bb5ee53ba29bb3bdf5ae61643f86f8f604baa57656/statsmodels-0.14.6-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:6ad5c2810fc6c684254a7792bf1cbaf1606cdee2a253f8bd259c43135d87cfb4", size = 10121514, upload-time = "2025-12-05T19:28:16.521Z" }, + { url = "https://files.pythonhosted.org/packages/82/af/ec48daa7f861f993b91a0dcc791d66e1cf56510a235c5cbd2ab991a31d5c/statsmodels-0.14.6-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:341fa68a7403e10a95c7b6e41134b0da3a7b835ecff1eb266294408535a06eb6", size = 10003346, upload-time = "2025-12-05T19:28:29.568Z" }, + { url = "https://files.pythonhosted.org/packages/a9/2c/c8f7aa24cd729970728f3f98822fb45149adc216f445a9301e441f7ac760/statsmodels-0.14.6-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:bdf1dfe2a3ca56f5529118baf33a13efed2783c528f4a36409b46bbd2d9d48eb", size = 10129872, upload-time = "2025-12-05T23:09:25.724Z" }, + { url = "https://files.pythonhosted.org/packages/40/c6/9ae8e9b0721e9b6eb5f340c3a0ce8cd7cce4f66e03dd81f80d60f111987f/statsmodels-0.14.6-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a3764ba8195c9baf0925a96da0743ff218067a269f01d155ca3558deed2658ca", size = 10381964, upload-time = "2025-12-05T23:09:41.326Z" }, + { url = "https://files.pythonhosted.org/packages/28/8c/cf3d30c8c2da78e2ad1f50ade8b7fabec3ff4cdfc56fbc02e097c4577f90/statsmodels-0.14.6-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:9e8d2e519852adb1b420e018f5ac6e6684b2b877478adf7fda2cfdb58f5acb5d", size = 10409611, upload-time = "2025-12-05T23:09:57.131Z" }, + { url = "https://files.pythonhosted.org/packages/bf/cc/018f14ecb58c6cb89de9d52695740b7d1f5a982aa9ea312483ea3c3d5f77/statsmodels-0.14.6-cp311-cp311-win_amd64.whl", hash = "sha256:2738a00fca51196f5a7d44b06970ace6b8b30289839e4808d656f8a98e35faa7", size = 9580385, upload-time = "2025-12-05T19:28:42.778Z" }, + { url = "https://files.pythonhosted.org/packages/25/ce/308e5e5da57515dd7cab3ec37ea2d5b8ff50bef1fcc8e6d31456f9fae08e/statsmodels-0.14.6-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:fe76140ae7adc5ff0e60a3f0d56f4fffef484efa803c3efebf2fcd734d72ecb5", size = 10091932, upload-time = "2025-12-05T19:28:55.446Z" }, + { url = "https://files.pythonhosted.org/packages/05/30/affbabf3c27fb501ec7b5808230c619d4d1a4525c07301074eb4bda92fa9/statsmodels-0.14.6-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:26d4f0ed3b31f3c86f83a92f5c1f5cbe63fc992cd8915daf28ca49be14463a1c", size = 9997345, upload-time = "2025-12-05T19:29:10.278Z" }, + { url = "https://files.pythonhosted.org/packages/48/f5/3a73b51e6450c31652c53a8e12e24eac64e3824be816c0c2316e7dbdcb7d/statsmodels-0.14.6-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d8c00a42863e4f4733ac9d078bbfad816249c01451740e6f5053ecc7db6d6368", size = 10058649, upload-time = "2025-12-05T23:10:12.775Z" }, + { url = "https://files.pythonhosted.org/packages/81/68/dddd76117df2ef14c943c6bbb6618be5c9401280046f4ddfc9fb4596a1b8/statsmodels-0.14.6-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:19b58cf7474aa9e7e3b0771a66537148b2df9b5884fbf156096c0e6c1ff0469d", size = 10339446, upload-time = "2025-12-05T23:10:28.503Z" }, + { url = "https://files.pythonhosted.org/packages/56/4a/dce451c74c4050535fac1ec0c14b80706d8fc134c9da22db3c8a0ec62c33/statsmodels-0.14.6-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:81e7dcc5e9587f2567e52deaff5220b175bf2f648951549eae5fc9383b62bc37", size = 10368705, upload-time = "2025-12-05T23:10:44.339Z" }, + { url = "https://files.pythonhosted.org/packages/60/15/3daba2df40be8b8a9a027d7f54c8dedf24f0d81b96e54b52293f5f7e3418/statsmodels-0.14.6-cp312-cp312-win_amd64.whl", hash = "sha256:b5eb07acd115aa6208b4058211138393a7e6c2cf12b6f213ede10f658f6a714f", size = 9543991, upload-time = "2025-12-05T23:10:58.536Z" }, + { url = "https://files.pythonhosted.org/packages/81/59/a5aad5b0cc266f5be013db8cde563ac5d2a025e7efc0c328d83b50c72992/statsmodels-0.14.6-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:47ee7af083623d2091954fa71c7549b8443168f41b7c5dce66510274c50fd73e", size = 10072009, upload-time = "2025-12-05T23:11:14.021Z" }, + { url = "https://files.pythonhosted.org/packages/53/dd/d8cfa7922fc6dc3c56fa6c59b348ea7de829a94cd73208c6f8202dd33f17/statsmodels-0.14.6-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:aa60d82e29fcd0a736e86feb63a11d2380322d77a9369a54be8b0965a3985f71", size = 9980018, upload-time = "2025-12-05T23:11:30.907Z" }, + { url = "https://files.pythonhosted.org/packages/ee/77/0ec96803eba444efd75dba32f2ef88765ae3e8f567d276805391ec2c98c6/statsmodels-0.14.6-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:89ee7d595f5939cc20bf946faedcb5137d975f03ae080f300ebb4398f16a5bd4", size = 10060269, upload-time = "2025-12-05T23:11:46.338Z" }, + { url = "https://files.pythonhosted.org/packages/10/b9/fd41f1f6af13a1a1212a06bb377b17762feaa6d656947bf666f76300fc05/statsmodels-0.14.6-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:730f3297b26749b216a06e4327fe0be59b8d05f7d594fb6caff4287b69654589", size = 10324155, upload-time = "2025-12-05T23:12:01.805Z" }, + { url = "https://files.pythonhosted.org/packages/ee/0f/a6900e220abd2c69cd0a07e3ad26c71984be6061415a60e0f17b152ecf08/statsmodels-0.14.6-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:f1c08befa85e93acc992b72a390ddb7bd876190f1360e61d10cf43833463bc9c", size = 10349765, upload-time = "2025-12-05T23:12:18.018Z" }, + { url = "https://files.pythonhosted.org/packages/98/08/b79f0c614f38e566eebbdcff90c0bcacf3c6ba7a5bbb12183c09c29ca400/statsmodels-0.14.6-cp313-cp313-win_amd64.whl", hash = "sha256:8021271a79f35b842c02a1794465a651a9d06ec2080f76ebc3b7adce77d08233", size = 9540043, upload-time = "2025-12-05T23:12:33.887Z" }, + { url = "https://files.pythonhosted.org/packages/71/de/09540e870318e0c7b58316561d417be45eff731263b4234fdd2eee3511a8/statsmodels-0.14.6-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:00781869991f8f02ad3610da6627fd26ebe262210287beb59761982a8fa88cae", size = 10069403, upload-time = "2025-12-05T23:12:48.424Z" }, + { url = "https://files.pythonhosted.org/packages/ab/f0/63c1bfda75dc53cee858006e1f46bd6d6f883853bea1b97949d0087766ca/statsmodels-0.14.6-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:73f305fbf31607b35ce919fae636ab8b80d175328ed38fdc6f354e813b86ee37", size = 9989253, upload-time = "2025-12-05T23:13:05.274Z" }, + { url = "https://files.pythonhosted.org/packages/c1/98/b0dfb4f542b2033a3341aa5f1bdd97024230a4ad3670c5b0839d54e3dcab/statsmodels-0.14.6-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e443e7077a6e2d3faeea72f5a92c9f12c63722686eb80bb40a0f04e4a7e267ad", size = 10090802, upload-time = "2025-12-05T23:13:20.653Z" }, + { url = "https://files.pythonhosted.org/packages/34/0e/2408735aca9e764643196212f9069912100151414dd617d39ffc72d77eee/statsmodels-0.14.6-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:3414e40c073d725007a6603a18247ab7af3467e1af4a5e5a24e4c27bc26673b4", size = 10337587, upload-time = "2025-12-05T23:13:37.597Z" }, + { url = "https://files.pythonhosted.org/packages/0f/36/4d44f7035ab3c0b2b6a4c4ebb98dedf36246ccbc1b3e2f51ebcd7ac83abb/statsmodels-0.14.6-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:a518d3f9889ef920116f9fa56d0338069e110f823926356946dae83bc9e33e19", size = 10363350, upload-time = "2025-12-05T23:13:53.08Z" }, + { url = "https://files.pythonhosted.org/packages/26/33/f1652d0c59fa51de18492ee2345b65372550501ad061daa38f950be390b6/statsmodels-0.14.6-cp314-cp314-win_amd64.whl", hash = "sha256:151b73e29f01fe619dbce7f66d61a356e9d1fe5e906529b78807df9189c37721", size = 9588010, upload-time = "2025-12-05T23:14:07.28Z" }, +] + [[package]] name = "tabulate" version = "0.9.0"