diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 96e84fe..274ebec 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -17,7 +17,7 @@ jobs: strategy: fail-fast: false matrix: - package: [tsinfer-hmm, tskit-hmm] + package: [tsinfer-hmm, tskit-hmm, tsls] defaults: run: working-directory: ${{ matrix.package }} @@ -44,7 +44,7 @@ jobs: strategy: fail-fast: false matrix: - package: [tsinfer-hmm, tskit-hmm] + package: [tsinfer-hmm, tskit-hmm, tsls] python: ["3.11", "3.13"] os: [ubuntu-24.04, macos-latest] defaults: diff --git a/README.md b/README.md index 19e138e..f04677d 100644 --- a/README.md +++ b/README.md @@ -21,6 +21,26 @@ Both build against the tskit C library in the `git-submodules/tskit` submodule, so clone with `--recurse-submodules` or run `git submodule update --init`. +[`tsls/`](tsls) is the merged engine (Python package `tsls`, extension +module `_tsls`). It is based on the tsinfer engine: + +- It copies from every node in the tree at a given position (any node + with a parent or a child); isolated nodes cannot be copied. +- It supports arbitrary topologies (multiple roots, no special root node). +- It provides the Viterbi algorithm (`AncestorMatcher.find_path`, with + the same interface as tsinfer-hmm) and the forward and backward + algorithms (`forward_matrix`, `backward_matrix`), which return + compressed matrices. +- It uses tsinfer's compression (a node's value is dropped when it + equals its parent's), a likelihood threshold and `weight_by_n`. +- It supports multiple mutations per site and any number of alleles. + +Paths match tsinfer-hmm except for ties. To match tsinfer's convention +that its ultimate root node 0 is never copied, remove node 0's edges +before building the `MatcherIndexes`; sc2ts ARGs already have an +isolated node 0. The `benchmarking/` directory compares the engines on +the sc2ts ARG. + The git history records exactly what was copied from where. Each directory was first added verbatim, then changed in separate commits to make it build on its own. @@ -30,11 +50,12 @@ make it build on its own. tsls is distributed under the MIT License. tsinfer is licensed under GPL-3.0-or-later; the tsinfer code copied here was relicensed to MIT by its author. See [tsinfer-hmm/LICENSING.md](tsinfer-hmm/LICENSING.md) for -details. The tskit code is MIT licensed. +details. The merged engine in `tsls/` is derived from tsinfer-hmm and +is covered by the same relicensing. The tskit code is MIT licensed. ## Development -In either `tsinfer-hmm/` or `tskit-hmm/`: +In `tsls/`, `tsinfer-hmm/` or `tskit-hmm/`: ``` # Python tests diff --git a/benchmarking/engines.py b/benchmarking/engines.py index 9276bdc..f105070 100644 --- a/benchmarking/engines.py +++ b/benchmarking/engines.py @@ -30,6 +30,7 @@ import numpy as np import tsinfer_hmm import tskit_hmm +import tsls ALLELES = ("A", "C", "G", "T") MISSING = -1 @@ -202,4 +203,55 @@ def optimum_log_likelihood(self, normalisation_factor): return float(np.sum(np.log(normalisation_factor)) + np.log(n) - m * np.log(stay)) -ENGINES = {engine.name: engine for engine in [TsinferEngine, TskitEngine]} +class TslsEngine(TsinferEngine): + """ + The merged tsls engine, with the same model and options as tsinfer. In + tsls every node in the tree is a state, so we remove the vestigial root + edge 0 -> 1 added by prepare.py to make node 0 isolated, as in sc2ts. + """ + + name = "tsls" + + def __init__(self, ts, *, mu, rho, mismatch_threshold=None): + self.num_sites = ts.num_sites + self.mu = mu + self.rho = rho + self.likelihood_threshold = None + if mismatch_threshold is not None: + self.likelihood_threshold = mu**mismatch_threshold * 0.99 + self.recombination = np.full(ts.num_sites, rho) + self.mismatch = np.full(ts.num_sites, mu) + tables = ts.dump_tables() + tables.edges.keep_rows(tables.edges.parent != 0) + self.matcher_indexes = tsls.MatcherIndexes( + tables.tree_sequence(), alleles=ALLELES + ) + + def match(self, h): + matcher = tsls.AncestorMatcher( + self.matcher_indexes, + self.recombination, + self.mismatch, + likelihood_threshold=self.likelihood_threshold, + weight_by_n=False, + ) + matched = np.zeros(self.num_sites, dtype=np.int8) + left, right, parent = matcher.find_path( + h.astype(np.int8), 0, self.num_sites, matched + ) + stats = { + "total_memory": matcher.total_memory, + "mean_traceback_size": matcher.mean_traceback_size, + } + return ( + left[::-1].astype(np.float64), + right[::-1].astype(np.float64), + parent[::-1].copy(), + matched, + stats, + ) + + +ENGINES = { + engine.name: engine for engine in [TsinferEngine, TskitEngine, TslsEngine] +} diff --git a/benchmarking/pyproject.toml b/benchmarking/pyproject.toml index e6a7909..3a90092 100644 --- a/benchmarking/pyproject.toml +++ b/benchmarking/pyproject.toml @@ -12,6 +12,7 @@ dependencies = [ "tszip", "tsinfer-hmm", "tskit-hmm", + "tsls", ] [dependency-groups] @@ -26,6 +27,7 @@ package = false [tool.uv.sources] tsinfer-hmm = { path = "../tsinfer-hmm", editable = true } tskit-hmm = { path = "../tskit-hmm", editable = true } +tsls = { path = "../tsls", editable = true } [tool.pytest.ini_options] pythonpath = ["."] diff --git a/benchmarking/run.py b/benchmarking/run.py index d8530b0..61b6fb2 100644 --- a/benchmarking/run.py +++ b/benchmarking/run.py @@ -148,7 +148,7 @@ def main( continue before = time.perf_counter() options = {} - if name == "tsinfer": + if name in ("tsinfer", "tsls"): options["mismatch_threshold"] = mismatch_threshold engine = engines.ENGINES[name](ts, mu=mu, rho=rho, **options) setup_time = time.perf_counter() - before diff --git a/benchmarking/uv.lock b/benchmarking/uv.lock index 3698473..e7f104a 100644 --- a/benchmarking/uv.lock +++ b/benchmarking/uv.lock @@ -948,6 +948,25 @@ test = [ { name = "pytest-xdist" }, ] +[[package]] +name = "tsls" +version = "0.0.1.dev0" +source = { editable = "../tsls" } +dependencies = [ + { name = "numpy", version = "2.4.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.12'" }, + { name = "numpy", version = "2.5.4", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.12'" }, + { name = "tskit" }, +] + +[package.metadata] +requires-dist = [ + { name = "numpy", specifier = ">=2" }, + { name = "tskit", specifier = ">=1.0.0" }, +] + +[package.metadata.requires-dev] +test = [{ name = "pytest" }] + [[package]] name = "tsls-benchmarking" version = "0.0.0" @@ -961,6 +980,7 @@ dependencies = [ { name = "tsinfer-hmm" }, { name = "tskit" }, { name = "tskit-hmm" }, + { name = "tsls" }, { name = "tszip" }, ] @@ -979,6 +999,7 @@ requires-dist = [ { name = "tsinfer-hmm", editable = "../tsinfer-hmm" }, { name = "tskit", specifier = ">=1.0.0" }, { name = "tskit-hmm", editable = "../tskit-hmm" }, + { name = "tsls", editable = "../tsls" }, { name = "tszip" }, ] diff --git a/tsls/_tslsmodule.c b/tsls/_tslsmodule.c new file mode 100644 index 0000000..dc622b0 --- /dev/null +++ b/tsls/_tslsmodule.c @@ -0,0 +1,768 @@ + +#define PY_SSIZE_T_CLEAN +#define NPY_NO_DEPRECATED_API NPY_1_7_API_VERSION + +#include +#include +#include +#include +#include + +#include "lib/tsls.h" + +#if PY_MAJOR_VERSION >= 3 +#define IS_PY3K +#endif + +#define MODULE_DOC "Low-level tsls HMM interface." + +static PyObject *TslsLibraryError; +static PyObject *TslsMatchImpossible; + +#include "tskit_lwt_interface.h" + +typedef struct { + PyObject_HEAD + matcher_indexes_t *matcher_indexes; +} MatcherIndexes; + +typedef struct { + PyObject_HEAD + ancestor_matcher_t *ancestor_matcher; + MatcherIndexes *matcher_indexes; +} AncestorMatcher; + +static void +handle_library_error(int err) +{ + if (err == TSLS_ERR_NO_MEMORY) { + PyErr_NoMemory(); + } else if (err == TSLS_ERR_MATCH_IMPOSSIBLE + || err == TSLS_ERR_MATCH_IMPOSSIBLE_EXTREME_MUTATION_PROBA + || err == TSLS_ERR_MATCH_IMPOSSIBLE_ZERO_RECOMB_PRECISION) { + PyErr_Format(TslsMatchImpossible, "%s", tsls_strerror(err)); + } else { + PyErr_Format(TslsLibraryError, "%s", tsls_strerror(err)); + } +} + +static FILE * +make_file(PyObject *fileobj, const char *mode) +{ + FILE *ret = NULL; + FILE *file = NULL; + int fileobj_fd, new_fd; + + fileobj_fd = PyObject_AsFileDescriptor(fileobj); + if (fileobj_fd == -1) { + goto out; + } + new_fd = dup(fileobj_fd); + if (new_fd == -1) { + PyErr_SetFromErrno(PyExc_OSError); + goto out; + } + file = fdopen(new_fd, mode); + if (file == NULL) { + (void) close(new_fd); + PyErr_SetFromErrno(PyExc_OSError); + goto out; + } + ret = file; +out: + return ret; +} + +/*=================================================================== + * MatcherIndexes + *=================================================================== + */ + +static void +MatcherIndexes_dealloc(MatcherIndexes *self) +{ + if (self->matcher_indexes != NULL) { + matcher_indexes_free(self->matcher_indexes); + PyMem_Free(self->matcher_indexes); + self->matcher_indexes = NULL; + } + Py_TYPE(self)->tp_free((PyObject *) self); +} + +static int +MatcherIndexes_init(MatcherIndexes *self, PyObject *args, PyObject *kwds) +{ + int ret = -1; + int err; + LightweightTableCollection *tables; + PyObject *num_alleles_obj = NULL; + PyObject *ancestral_state_obj = NULL; + PyObject *derived_state_obj = NULL; + PyArrayObject *num_alleles_array = NULL; + PyArrayObject *ancestral_state_array = NULL; + PyArrayObject *derived_state_array = NULL; + tsk_size_t *num_alleles_data = NULL; + allele_t *ancestral_state_data = NULL; + allele_t *derived_state_data = NULL; + static char *kwlist[] + = { "tables", "num_alleles", "ancestral_state", "derived_state", NULL }; + + self->matcher_indexes = NULL; + if (!PyArg_ParseTupleAndKeywords(args, kwds, "O!|OOO", kwlist, + &LightweightTableCollectionType, &tables, &num_alleles_obj, + &ancestral_state_obj, &derived_state_obj)) { + goto out; + } + if (LightweightTableCollection_check_state(tables) != 0) { + goto out; + } + if (num_alleles_obj != NULL && num_alleles_obj != Py_None) { + num_alleles_array = (PyArrayObject *) PyArray_FROMANY( + num_alleles_obj, NPY_UINT64, 1, 1, NPY_ARRAY_IN_ARRAY); + if (num_alleles_array == NULL) { + goto out; + } + if (PyArray_DIM(num_alleles_array, 0) + != (npy_intp) tables->tables->sites.num_rows) { + PyErr_SetString(PyExc_ValueError, "num_alleles must have length num_sites"); + goto out; + } + num_alleles_data = PyArray_DATA(num_alleles_array); + } + if (ancestral_state_obj != NULL && ancestral_state_obj != Py_None) { + ancestral_state_array = (PyArrayObject *) PyArray_FROMANY( + ancestral_state_obj, NPY_INT8, 1, 1, NPY_ARRAY_IN_ARRAY); + if (ancestral_state_array == NULL) { + goto out; + } + if (PyArray_DIM(ancestral_state_array, 0) + != (npy_intp) tables->tables->sites.num_rows) { + PyErr_SetString( + PyExc_ValueError, "ancestral_state must have length num_sites"); + goto out; + } + ancestral_state_data = PyArray_DATA(ancestral_state_array); + } + if (derived_state_obj != NULL && derived_state_obj != Py_None) { + derived_state_array = (PyArrayObject *) PyArray_FROMANY( + derived_state_obj, NPY_INT8, 1, 1, NPY_ARRAY_IN_ARRAY); + if (derived_state_array == NULL) { + goto out; + } + if (PyArray_DIM(derived_state_array, 0) + != (npy_intp) tables->tables->mutations.num_rows) { + PyErr_SetString( + PyExc_ValueError, "derived_state must have length num_mutations"); + goto out; + } + derived_state_data = PyArray_DATA(derived_state_array); + } + + self->matcher_indexes = PyMem_Calloc(1, sizeof(*self->matcher_indexes)); + if (self->matcher_indexes == NULL) { + PyErr_NoMemory(); + goto out; + } + err = matcher_indexes_alloc(self->matcher_indexes, tables->tables, + num_alleles_data, ancestral_state_data, derived_state_data, 0); + if (err != 0) { + handle_library_error(err); + goto out; + } + ret = 0; +out: + Py_XDECREF(num_alleles_array); + Py_XDECREF(ancestral_state_array); + Py_XDECREF(derived_state_array); + return ret; +} + +static int +MatcherIndexes_check_state(MatcherIndexes *self) +{ + int ret = 0; + if (self->matcher_indexes == NULL) { + PyErr_SetString(PyExc_SystemError, "MatcherIndexes not initialised"); + ret = -1; + } + return ret; +} + +static PyObject * +MatcherIndexes_print_state(MatcherIndexes *self, PyObject *args) +{ + PyObject *ret = NULL; + PyObject *fileobj; + FILE *file = NULL; + + if (MatcherIndexes_check_state(self) != 0) { + goto out; + } + if (!PyArg_ParseTuple(args, "O", &fileobj)) { + goto out; + } + file = make_file(fileobj, "w"); + if (file == NULL) { + goto out; + } + matcher_indexes_print_state(self->matcher_indexes, file); + ret = Py_BuildValue(""); +out: + if (file != NULL) { + (void) fclose(file); + } + return ret; +} + +static PyMethodDef MatcherIndexes_methods[] = { + { "print_state", (PyCFunction) MatcherIndexes_print_state, METH_VARARGS, + "Low-level debug method" }, + { NULL } /* Sentinel */ +}; + +static PyTypeObject MatcherIndexesType = { + // clang-format off + PyVarObject_HEAD_INIT(NULL, 0) + .tp_name = "_tsls.MatcherIndexes", + .tp_basicsize = sizeof(MatcherIndexes), + .tp_dealloc = (destructor) MatcherIndexes_dealloc, + .tp_flags = Py_TPFLAGS_DEFAULT | Py_TPFLAGS_BASETYPE, + .tp_doc = "MatcherIndexes objects", + .tp_methods = MatcherIndexes_methods, + .tp_init = (initproc) MatcherIndexes_init, + .tp_new = PyType_GenericNew, + // clang-format on +}; + +/*=================================================================== + * AncestorMatcher + *=================================================================== + */ + +static int +AncestorMatcher_check_state(AncestorMatcher *self) +{ + int ret = 0; + if (self->ancestor_matcher == NULL) { + PyErr_SetString(PyExc_SystemError, "AncestorMatcher not initialised"); + ret = -1; + } + return ret; +} + +static void +AncestorMatcher_dealloc(AncestorMatcher *self) +{ + if (self->ancestor_matcher != NULL) { + ancestor_matcher_free(self->ancestor_matcher); + PyMem_Free(self->ancestor_matcher); + self->ancestor_matcher = NULL; + } + Py_XDECREF(self->matcher_indexes); + Py_TYPE(self)->tp_free((PyObject *) self); +} + +static int +AncestorMatcher_init(AncestorMatcher *self, PyObject *args, PyObject *kwds) +{ + int ret = -1; + int err; + int extended_checks = 0; + int weight_by_n = 1; + static char *kwlist[] = { "matcher_indexes", "recombination", "mismatch", + "extended_checks", "likelihood_threshold", "weight_by_n", NULL }; + MatcherIndexes *matcher_indexes = NULL; + PyObject *recombination = NULL; + PyObject *mismatch = NULL; + PyArrayObject *recombination_array = NULL; + PyArrayObject *mismatch_array = NULL; + npy_intp *shape; + double likelihood_threshold = DBL_MIN; + int flags = 0; + + self->ancestor_matcher = NULL; + self->matcher_indexes = NULL; + if (!PyArg_ParseTupleAndKeywords(args, kwds, "O!OO|idi", kwlist, &MatcherIndexesType, + &matcher_indexes, &recombination, &mismatch, &extended_checks, + &likelihood_threshold, &weight_by_n)) { + goto out; + } + self->matcher_indexes = matcher_indexes; + Py_INCREF(self->matcher_indexes); + if (MatcherIndexes_check_state(self->matcher_indexes) != 0) { + goto out; + } + + recombination_array = (PyArrayObject *) PyArray_FromAny(recombination, + PyArray_DescrFromType(NPY_FLOAT64), 1, 1, NPY_ARRAY_IN_ARRAY, NULL); + if (recombination_array == NULL) { + goto out; + } + shape = PyArray_DIMS(recombination_array); + if (shape[0] != (npy_intp) matcher_indexes->matcher_indexes->num_sites) { + PyErr_SetString( + PyExc_ValueError, "Size of recombination array must be num_sites"); + goto out; + } + mismatch_array = (PyArrayObject *) PyArray_FromAny( + mismatch, PyArray_DescrFromType(NPY_FLOAT64), 1, 1, NPY_ARRAY_IN_ARRAY, NULL); + if (mismatch_array == NULL) { + goto out; + } + shape = PyArray_DIMS(mismatch_array); + if (shape[0] != (npy_intp) matcher_indexes->matcher_indexes->num_sites) { + PyErr_SetString(PyExc_ValueError, "Size of mismatch array must be num_sites"); + goto out; + } + + self->ancestor_matcher = PyMem_Malloc(sizeof(ancestor_matcher_t)); + if (self->ancestor_matcher == NULL) { + PyErr_NoMemory(); + goto out; + } + if (extended_checks) { + flags |= TSLS_EXTENDED_CHECKS; + } + if (!weight_by_n) { + flags |= TSLS_DISABLE_WEIGHT_BY_N; + } + err = ancestor_matcher_alloc(self->ancestor_matcher, + self->matcher_indexes->matcher_indexes, PyArray_DATA(recombination_array), + PyArray_DATA(mismatch_array), likelihood_threshold, flags); + if (err != 0) { + handle_library_error(err); + goto out; + } + ret = 0; +out: + Py_XDECREF(recombination_array); + Py_XDECREF(mismatch_array); + return ret; +} + +static PyObject * +AncestorMatcher_find_path(AncestorMatcher *self, PyObject *args, PyObject *kwds) +{ + int err; + PyObject *ret = NULL; + static char *kwlist[] = { "haplotype", "start", "end", NULL }; + PyObject *haplotype = NULL; + PyArrayObject *haplotype_array = NULL; + npy_intp *shape; + size_t num_edges; + int start, end; + PyArrayObject *left = NULL; + PyArrayObject *right = NULL; + PyArrayObject *parent = NULL; + PyArrayObject *match = NULL; + npy_intp dims[1]; + + if (AncestorMatcher_check_state(self) != 0) { + goto out; + } + if (!PyArg_ParseTupleAndKeywords( + args, kwds, "Oii", kwlist, &haplotype, &start, &end)) { + goto out; + } + haplotype_array + = (PyArrayObject *) PyArray_FROM_OTF(haplotype, NPY_INT8, NPY_ARRAY_IN_ARRAY); + if (haplotype_array == NULL) { + goto out; + } + if (PyArray_NDIM(haplotype_array) != 1) { + PyErr_SetString(PyExc_ValueError, "Dim != 1"); + goto out; + } + shape = PyArray_DIMS(haplotype_array); + if (shape[0] != (npy_intp) self->ancestor_matcher->num_sites) { + PyErr_SetString(PyExc_ValueError, "Incorrect size for input haplotype."); + goto out; + } + + dims[0] = self->ancestor_matcher->num_sites; + left = (PyArrayObject *) PyArray_SimpleNew(1, dims, NPY_UINT32); + right = (PyArrayObject *) PyArray_SimpleNew(1, dims, NPY_UINT32); + parent = (PyArrayObject *) PyArray_SimpleNew(1, dims, NPY_INT32); + match = (PyArrayObject *) PyArray_SimpleNew(1, dims, NPY_INT8); + if (left == NULL || right == NULL || parent == NULL || match == NULL) { + goto out; + } + + Py_BEGIN_ALLOW_THREADS + err = ancestor_matcher_find_path(self->ancestor_matcher, (tsk_id_t) start, + (tsk_id_t) end, (allele_t *) PyArray_DATA(haplotype_array), + (allele_t *) PyArray_DATA(match), &num_edges, PyArray_DATA(left), + PyArray_DATA(right), PyArray_DATA(parent)); + Py_END_ALLOW_THREADS + if (err != 0) { + handle_library_error(err); + goto out; + } + ret = Py_BuildValue( + "(kNNNN)", (unsigned long) num_edges, left, right, parent, match); + if (ret == NULL) { + goto out; + } + left = NULL; + right = NULL; + parent = NULL; + match = NULL; +out: + Py_XDECREF(haplotype_array); + Py_XDECREF(match); + Py_XDECREF(left); + Py_XDECREF(right); + Py_XDECREF(parent); + return ret; +} + +/* Run the forward (if forward_norm is NULL) or backward algorithm, and + * return the compressed matrix as a tuple of flat arrays + * (normalisation_factor, offsets, nodes, values), where the values at site + * j are nodes[offsets[j]: offsets[j + 1]] and similarly for values. */ +static PyObject * +AncestorMatcher_run_compressed_matrix( + AncestorMatcher *self, PyObject *haplotype, PyObject *forward_norm) +{ + int err; + PyObject *ret = NULL; + PyArrayObject *haplotype_array = NULL; + PyArrayObject *forward_norm_array = NULL; + PyArrayObject *norm = NULL; + PyArrayObject *offsets = NULL; + PyArrayObject *nodes = NULL; + PyArrayObject *values = NULL; + const size_t num_sites = self->ancestor_matcher->num_sites; + compressed_matrix_t cm; + bool cm_allocated = false; + npy_intp dims[1]; + uint64_t *offsets_data; + tsk_id_t *nodes_data; + double *values_data; + size_t j, k, offset; + + haplotype_array + = (PyArrayObject *) PyArray_FROMANY(haplotype, NPY_INT8, 1, 1, NPY_ARRAY_IN_ARRAY); + if (haplotype_array == NULL) { + goto out; + } + if (PyArray_DIM(haplotype_array, 0) != (npy_intp) num_sites) { + PyErr_SetString(PyExc_ValueError, "Incorrect size for input haplotype."); + goto out; + } + if (forward_norm != NULL) { + forward_norm_array = (PyArrayObject *) PyArray_FROMANY( + forward_norm, NPY_FLOAT64, 1, 1, NPY_ARRAY_IN_ARRAY); + if (forward_norm_array == NULL) { + goto out; + } + if (PyArray_DIM(forward_norm_array, 0) != (npy_intp) num_sites) { + PyErr_SetString( + PyExc_ValueError, "normalisation_factor must have length num_sites"); + goto out; + } + } + err = compressed_matrix_alloc(&cm, num_sites, + 2 * (self->ancestor_matcher->num_nodes + 1) * sizeof(double)); + cm_allocated = true; + if (err != 0) { + handle_library_error(err); + goto out; + } + + Py_BEGIN_ALLOW_THREADS + if (forward_norm_array == NULL) { + err = ancestor_matcher_forward(self->ancestor_matcher, + (allele_t *) PyArray_DATA(haplotype_array), &cm); + } else { + err = ancestor_matcher_backward(self->ancestor_matcher, + (allele_t *) PyArray_DATA(haplotype_array), + (double *) PyArray_DATA(forward_norm_array), &cm); + } + Py_END_ALLOW_THREADS + if (err != 0) { + handle_library_error(err); + goto out; + } + + dims[0] = (npy_intp) num_sites; + norm = (PyArrayObject *) PyArray_SimpleNew(1, dims, NPY_FLOAT64); + dims[0] = (npy_intp) num_sites + 1; + offsets = (PyArrayObject *) PyArray_SimpleNew(1, dims, NPY_UINT64); + dims[0] = (npy_intp) compressed_matrix_get_total_values(&cm); + nodes = (PyArrayObject *) PyArray_SimpleNew(1, dims, NPY_INT32); + values = (PyArrayObject *) PyArray_SimpleNew(1, dims, NPY_FLOAT64); + if (norm == NULL || offsets == NULL || nodes == NULL || values == NULL) { + goto out; + } + memcpy(PyArray_DATA(norm), cm.normalisation_factor, num_sites * sizeof(double)); + offsets_data = PyArray_DATA(offsets); + nodes_data = PyArray_DATA(nodes); + values_data = PyArray_DATA(values); + offset = 0; + for (j = 0; j < num_sites; j++) { + offsets_data[j] = offset; + for (k = 0; k < cm.num_values[j]; k++) { + nodes_data[offset] = cm.nodes[j][k]; + values_data[offset] = cm.values[j][k]; + offset++; + } + } + offsets_data[num_sites] = offset; + + ret = Py_BuildValue("(NNNN)", norm, offsets, nodes, values); + if (ret == NULL) { + goto out; + } + norm = NULL; + offsets = NULL; + nodes = NULL; + values = NULL; +out: + if (cm_allocated) { + compressed_matrix_free(&cm); + } + Py_XDECREF(haplotype_array); + Py_XDECREF(forward_norm_array); + Py_XDECREF(norm); + Py_XDECREF(offsets); + Py_XDECREF(nodes); + Py_XDECREF(values); + return ret; +} + +static PyObject * +AncestorMatcher_forward_matrix(AncestorMatcher *self, PyObject *args, PyObject *kwds) +{ + PyObject *ret = NULL; + static char *kwlist[] = { "haplotype", NULL }; + PyObject *haplotype = NULL; + + if (AncestorMatcher_check_state(self) != 0) { + goto out; + } + if (!PyArg_ParseTupleAndKeywords(args, kwds, "O", kwlist, &haplotype)) { + goto out; + } + ret = AncestorMatcher_run_compressed_matrix(self, haplotype, NULL); +out: + return ret; +} + +static PyObject * +AncestorMatcher_backward_matrix(AncestorMatcher *self, PyObject *args, PyObject *kwds) +{ + PyObject *ret = NULL; + static char *kwlist[] = { "haplotype", "normalisation_factor", NULL }; + PyObject *haplotype = NULL; + PyObject *forward_norm = NULL; + + if (AncestorMatcher_check_state(self) != 0) { + goto out; + } + if (!PyArg_ParseTupleAndKeywords( + args, kwds, "OO", kwlist, &haplotype, &forward_norm)) { + goto out; + } + ret = AncestorMatcher_run_compressed_matrix(self, haplotype, forward_norm); +out: + return ret; +} + +static PyObject * +AncestorMatcher_get_traceback(AncestorMatcher *self, PyObject *args) +{ + PyObject *ret = NULL; + unsigned long site; + node_state_list_t *list; + PyObject *dict = NULL; + PyObject *key = NULL; + PyObject *value = NULL; + int j; + + if (AncestorMatcher_check_state(self) != 0) { + goto out; + } + if (!PyArg_ParseTuple(args, "k", &site)) { + goto out; + } + if (site >= self->ancestor_matcher->num_sites) { + PyErr_SetString(PyExc_ValueError, "site out of range"); + goto out; + } + dict = PyDict_New(); + if (dict == NULL) { + goto out; + } + list = &self->ancestor_matcher->traceback[site]; + for (j = 0; j < list->size; j++) { + key = Py_BuildValue("k", (unsigned long) list->node[j]); + value = Py_BuildValue("i", (int) list->recombination_required[j]); + if (key == NULL || value == NULL) { + goto out; + } + if (PyDict_SetItem(dict, key, value) != 0) { + goto out; + } + Py_DECREF(key); + key = NULL; + Py_DECREF(value); + value = NULL; + } + ret = dict; + dict = NULL; +out: + Py_XDECREF(key); + Py_XDECREF(value); + Py_XDECREF(dict); + return ret; +} + +static PyObject * +AncestorMatcher_get_mean_traceback_size(AncestorMatcher *self, void *closure) +{ + PyObject *ret = NULL; + + if (AncestorMatcher_check_state(self) != 0) { + goto out; + } + ret = Py_BuildValue( + "d", ancestor_matcher_get_mean_traceback_size(self->ancestor_matcher)); +out: + return ret; +} + +static PyObject * +AncestorMatcher_get_total_memory(AncestorMatcher *self, void *closure) +{ + PyObject *ret = NULL; + unsigned long val; + + if (AncestorMatcher_check_state(self) != 0) { + goto out; + } + +#if defined(TSI_NO_ATOMICS) + /* Without atomics, return an obviously wrong value */ + val = (unsigned long) PY_SSIZE_T_MAX; +#else + val = ancestor_matcher_get_total_memory(self->ancestor_matcher); +#endif + ret = Py_BuildValue("k", val); + +out: + return ret; +} + +static PyGetSetDef AncestorMatcher_getsetters[] = { + { "mean_traceback_size", (getter) AncestorMatcher_get_mean_traceback_size, NULL, + "The mean size of the traceback per site." }, + { "total_memory", (getter) AncestorMatcher_get_total_memory, NULL, + "The total amount of memory used by this matcher." }, + { NULL } /* Sentinel */ +}; + +static PyMethodDef AncestorMatcher_methods[] = { + { "find_path", (PyCFunction) AncestorMatcher_find_path, METH_VARARGS | METH_KEYWORDS, + "Returns a best match path for the specified haplotype through the ancestors." }, + { "forward_matrix", (PyCFunction) AncestorMatcher_forward_matrix, + METH_VARARGS | METH_KEYWORDS, + "Runs the forward algorithm and returns the compressed matrix arrays." }, + { "backward_matrix", (PyCFunction) AncestorMatcher_backward_matrix, + METH_VARARGS | METH_KEYWORDS, + "Runs the backward algorithm and returns the compressed matrix arrays." }, + { "get_traceback", (PyCFunction) AncestorMatcher_get_traceback, METH_VARARGS, + "Returns the traceback likelihood dictionary at the specified site." }, + { NULL } /* Sentinel */ +}; + +static PyTypeObject AncestorMatcherType = { + // clang-format off + PyVarObject_HEAD_INIT(NULL, 0) + .tp_name = "_tsls.AncestorMatcher", + .tp_basicsize = sizeof(AncestorMatcher), + .tp_dealloc = (destructor) AncestorMatcher_dealloc, + .tp_flags = Py_TPFLAGS_DEFAULT | Py_TPFLAGS_BASETYPE, + .tp_doc = "AncestorMatcher objects", + .tp_methods = AncestorMatcher_methods, + .tp_getset = AncestorMatcher_getsetters, + .tp_init = (initproc) AncestorMatcher_init, + .tp_new = PyType_GenericNew, + // clang-format on +}; + +/*=================================================================== + * Module level code. + *=================================================================== + */ + +static PyMethodDef tsls_methods[] = { + { NULL } /* Sentinel */ +}; + +/* Initialisation code supports Python 2.x and 3.x. The framework uses the + * recommended structure from http://docs.python.org/howto/cporting.html. + * I've ignored the point about storing state in globals, as the examples + * from the Python documentation still use this idiom. + */ + +#if PY_MAJOR_VERSION >= 3 + +static struct PyModuleDef tslsmodule + = { PyModuleDef_HEAD_INIT, "_tsls", /* name of module */ + MODULE_DOC, /* module documentation, may be NULL */ + -1, tsls_methods, NULL, NULL, NULL, NULL }; + +#define INITERROR return NULL + +PyObject * +PyInit__tsls(void) + +#else +#define INITERROR return + +void +init_tsls(void) +#endif +{ +#if PY_MAJOR_VERSION >= 3 + PyObject *module = PyModule_Create(&tslsmodule); +#else + PyObject *module = Py_InitModule3("_tsls", tsls_methods, MODULE_DOC); +#endif + if (module == NULL) { + INITERROR; + } + /* Initialise numpy */ + import_array(); + + register_lwt_class(module); + + /* MatcherIndexes type */ + MatcherIndexesType.tp_new = PyType_GenericNew; + if (PyType_Ready(&MatcherIndexesType) < 0) { + INITERROR; + } + Py_INCREF(&MatcherIndexesType); + PyModule_AddObject(module, "MatcherIndexes", (PyObject *) &MatcherIndexesType); + + /* AncestorMatcher type */ + AncestorMatcherType.tp_new = PyType_GenericNew; + if (PyType_Ready(&AncestorMatcherType) < 0) { + INITERROR; + } + Py_INCREF(&AncestorMatcherType); + PyModule_AddObject(module, "AncestorMatcher", (PyObject *) &AncestorMatcherType); + + TslsLibraryError = PyErr_NewException("_tsls.LibraryError", NULL, NULL); + Py_INCREF(TslsLibraryError); + PyModule_AddObject(module, "LibraryError", TslsLibraryError); + + TslsMatchImpossible = PyErr_NewException("_tsls.MatchImpossible", NULL, NULL); + Py_INCREF(TslsMatchImpossible); + PyModule_AddObject(module, "MatchImpossible", TslsMatchImpossible); + +#if PY_MAJOR_VERSION >= 3 + return module; +#endif +} diff --git a/tsls/lib/err.c b/tsls/lib/err.c new file mode 100644 index 0000000..5c6b64d --- /dev/null +++ b/tsls/lib/err.c @@ -0,0 +1,59 @@ +#include "err.h" + +const char * +tsls_strerror(int err) +{ + const char *ret = "Unknown error"; + + switch (err) { + case 0: + ret = "Normal exit condition. This is not an error!"; + break; + case TSLS_ERR_GENERIC: + ret = "Generic tsls error - please file a bug report."; + break; + case TSLS_ERR_NO_MEMORY: + ret = "Out of memory"; + break; + case TSLS_ERR_MATCH_IMPOSSIBLE: + ret = "Unexpected failure to find matching haplotype; please open " + "an issue on GitHub"; + break; + case TSLS_ERR_MATCH_IMPOSSIBLE_EXTREME_MUTATION_PROBA: + ret = "Cannot find match: the specified mismatch probability is " + "0 or 1 and no matches are possible with these parameters"; + break; + case TSLS_ERR_MATCH_IMPOSSIBLE_ZERO_RECOMB_PRECISION: + ret = "Cannot find match: the specified recombination probability is " + "zero and no matches could be found"; + break; + case TSLS_ERR_BAD_HAPLOTYPE_ALLELE: + ret = "Haplotype allele must be missing (-1) or >= 0 and < num_alleles"; + break; + case TSLS_ERR_BAD_ANCESTRAL_STATE: + ret = "Ancestral state must be >= 0 and < num_alleles"; + break; + case TSLS_ERR_BAD_MUTATION_DERIVED_STATE: + ret = "Mutation derived state must be >= 0 and < num_alleles"; + break; + case TSLS_ERR_BAD_MUTATION_NODE: + ret = "Bad mutation: node out of bounds"; + break; + case TSLS_ERR_BAD_MUTATION_SITE: + ret = "Bad mutation: site out of bounds"; + break; + case TSLS_ERR_BAD_EDGE_NODE: + ret = "Bad edge: parent or child node out of bounds"; + break; + case TSLS_ERR_BAD_COORDINATE: + ret = "Edge and site coordinates must be integers"; + break; + case TSLS_ERR_BAD_SITE_RANGE: + ret = "Bad site range: must have 0 <= start < end <= num_sites"; + break; + case TSLS_ERR_BAD_NORMALISATION_FACTOR: + ret = "Forward normalisation factors must be > 0"; + break; + } + return ret; +} diff --git a/tsls/lib/err.h b/tsls/lib/err.h new file mode 100644 index 0000000..71f11cc --- /dev/null +++ b/tsls/lib/err.h @@ -0,0 +1,35 @@ +#ifndef __TSLS_ERR_H__ +#define __TSLS_ERR_H__ + +// clang-format off +#define TSLS_ERR_GENERIC -1 +#define TSLS_ERR_NO_MEMORY -2 +#define TSLS_ERR_MATCH_IMPOSSIBLE -3 +#define TSLS_ERR_MATCH_IMPOSSIBLE_EXTREME_MUTATION_PROBA -4 +#define TSLS_ERR_MATCH_IMPOSSIBLE_ZERO_RECOMB_PRECISION -5 +#define TSLS_ERR_BAD_HAPLOTYPE_ALLELE -6 +#define TSLS_ERR_BAD_ANCESTRAL_STATE -7 +#define TSLS_ERR_BAD_MUTATION_DERIVED_STATE -8 +#define TSLS_ERR_BAD_MUTATION_NODE -9 +#define TSLS_ERR_BAD_MUTATION_SITE -10 +#define TSLS_ERR_BAD_EDGE_NODE -11 +#define TSLS_ERR_BAD_COORDINATE -12 +#define TSLS_ERR_BAD_SITE_RANGE -13 +#define TSLS_ERR_BAD_NORMALISATION_FACTOR -14 +// clang-format on + +#ifdef __GNUC__ +#define WARN_UNUSED __attribute__((warn_unused_result)) +#define unlikely(expr) __builtin_expect(!!(expr), 0) +#define likely(expr) __builtin_expect(!!(expr), 1) +#else +/* On windows we don't do any perf related stuff */ +#define WARN_UNUSED +#define restrict +#define unlikely(expr) (expr) +#define likely(expr) (expr) +#endif + +const char *tsls_strerror(int err); + +#endif /*__TSLS_ERR_H__*/ diff --git a/tsls/lib/matcher.c b/tsls/lib/matcher.c new file mode 100644 index 0000000..186c0a7 --- /dev/null +++ b/tsls/lib/matcher.c @@ -0,0 +1,1597 @@ +/* + * The Li and Stephens HMM on a tree sequence. + * + * The states of the HMM at a given position are all the nodes in the tree + * at that position, that is, the nodes with a parent or at least one child. + * Likelihoods are stored on a subset of the nodes: a node with + * NULL_LIKELIHOOD has the same likelihood as its nearest ancestor with a + * value. After processing each site we remove the value from any node + * whose value equals the value it would inherit from its parent. Every root + * always has a value. + * + * The same machinery runs the Viterbi, forward and backward algorithms. + * At each site, the probability of staying on a node is + * p * (1 - rho + rho / n) (Viterbi) or p * (1 - rho) + rho / n * S (forward + * and backward, where S is the total over all states at the previous site), + * and the probability of switching to a given node is rho / n. If weight_by_n is set n is the + * number of nodes in the tree sequence, otherwise n = 1. Values are + * normalised at each site (by the maximum for Viterbi and the sum for + * forward), and then values below likelihood_threshold are set to it. + * + * When a node enters the tree it gets the value 0, and so it can only be + * reached by recombination. When a node leaves the tree, the probability + * of continuing from it is only the probability of recombination. + */ + +#include "tsls.h" + +#include +#include +#include +#include +#include + +#define MODE_VITERBI 0 +#define MODE_FORWARD 1 +#define MODE_BACKWARD 2 + +static inline void +remove_edge(edge_t edge, tsk_id_t *restrict parent, tsk_id_t *restrict left_child, + tsk_id_t *restrict right_child, tsk_id_t *restrict left_sib, + tsk_id_t *restrict right_sib) +{ + const tsk_id_t p = edge.parent; + const tsk_id_t c = edge.child; + const tsk_id_t lsib = left_sib[c]; + const tsk_id_t rsib = right_sib[c]; + + if (lsib == NULL_NODE) { + left_child[p] = rsib; + } else { + right_sib[lsib] = rsib; + } + if (rsib == NULL_NODE) { + right_child[p] = lsib; + } else { + left_sib[rsib] = lsib; + } + parent[c] = NULL_NODE; + left_sib[c] = NULL_NODE; + right_sib[c] = NULL_NODE; +} + +static inline void +insert_edge(edge_t edge, tsk_id_t *restrict parent, tsk_id_t *restrict left_child, + tsk_id_t *restrict right_child, tsk_id_t *restrict left_sib, + tsk_id_t *restrict right_sib) +{ + const tsk_id_t p = edge.parent; + const tsk_id_t c = edge.child; + const tsk_id_t u = right_child[p]; + + parent[c] = p; + if (u == NULL_NODE) { + left_child[p] = c; + left_sib[c] = NULL_NODE; + right_sib[c] = NULL_NODE; + } else { + right_sib[u] = c; + left_sib[c] = u; + right_sib[c] = NULL_NODE; + } + right_child[p] = c; +} + +/* Returns the allelic state of node u, given the allelic_state array in + * which the nodes with mutations at the current site are set. */ +static inline allele_t +get_allelic_state(const allele_t *restrict allelic_state, + const tsk_id_t *restrict parent, tsk_id_t u, allele_t ancestral_state) +{ + while (allelic_state[u] == TSK_NULL) { + if (parent[u] == NULL_NODE) { + return ancestral_state; + } + u = parent[u]; + } + return allelic_state[u]; +} + +static inline bool +is_integer(double x) +{ + return (double) (coordinate_t) x == x; +} + +/*=================================================================== + * Matcher indexes + *=================================================================== + */ + +int +matcher_indexes_print_state(const matcher_indexes_t *self, FILE *out) +{ + size_t j; + mutation_list_node_t *u; + + fprintf(out, "Matcher indexes state\n"); + fprintf(out, "flags = %d\n", (int) self->flags); + fprintf(out, "num_sites = %d\n", (int) self->num_sites); + fprintf(out, "num_nodes = %d\n", (int) self->num_nodes); + fprintf(out, "num_edges = %d\n", (int) self->num_edges); + + fprintf(out, "Sites = \n"); + fprintf(out, "site\tposition\tancestral_state\tnum_alleles\t(node, derived_state)\n"); + for (j = 0; j < self->num_sites; j++) { + fprintf(out, "%d\t%d\t%d\t%d\t", (int) j, (int) self->sites.position[j], + (int) self->sites.ancestral_state[j], (int) self->sites.num_alleles[j]); + for (u = self->sites.mutations[j]; u != NULL; u = u->next) { + fprintf(out, "(%d, %d) ", u->node, u->derived_state); + } + fprintf(out, "\n"); + } + return 0; +} + +static void +matcher_indexes_copy_edge_indexes( + matcher_indexes_t *self, const tsk_table_collection_t *tables) +{ + tsk_size_t j; + tsk_id_t k; + edge_t e; + const tsk_id_t *restrict I = tables->indexes.edge_insertion_order; + const tsk_id_t *restrict O = tables->indexes.edge_removal_order; + const double *restrict edges_right = tables->edges.right; + const double *restrict edges_left = tables->edges.left; + const tsk_id_t *restrict edges_child = tables->edges.child; + const tsk_id_t *restrict edges_parent = tables->edges.parent; + + for (j = 0; j < self->num_edges; j++) { + k = I[j]; + e.left = (coordinate_t) edges_left[k]; + e.right = (coordinate_t) edges_right[k]; + e.parent = edges_parent[k]; + e.child = edges_child[k]; + self->left_index_edges[j] = e; + + k = O[j]; + e.left = (coordinate_t) edges_left[k]; + e.right = (coordinate_t) edges_right[k]; + e.parent = edges_parent[k]; + e.child = edges_child[k]; + self->right_index_edges[j] = e; + } +} + +static int WARN_UNUSED +matcher_indexes_add_mutation( + matcher_indexes_t *self, tsk_id_t site, tsk_id_t node, allele_t derived_state) +{ + int ret = 0; + mutation_list_node_t *list_node, *tail; + + list_node = tsi_blkalloc_get(&self->allocator, sizeof(mutation_list_node_t)); + if (list_node == NULL) { + ret = TSLS_ERR_NO_MEMORY; + goto out; + } + list_node->node = node; + list_node->derived_state = derived_state; + list_node->next = NULL; + if (self->sites.mutations[site] == NULL) { + self->sites.mutations[site] = list_node; + } else { + tail = self->sites.mutations[site]; + while (tail->next != NULL) { + tail = tail->next; + } + tail->next = list_node; + } + self->num_mutations++; +out: + return ret; +} + +/* Copy the site and mutation data into the matcher indexes. The + * ancestral_state (per site) and derived_state (per mutation) arrays + * give the integer-encoded alleles; if NULL, the ancestral state is 0 + * and every mutation has derived state 1. Mutations must be in tskit + * order, so that when two mutations at a site are on the same node the + * later one (the child) takes precedence. */ +static int +matcher_indexes_copy_mutation_data(matcher_indexes_t *self, + const tsk_table_collection_t *tables, const tsk_size_t *num_alleles, + const allele_t *ancestral_state, const allele_t *derived_state) +{ + int ret = 0; + tsk_size_t j; + tsk_id_t site; + allele_t state; + const double *restrict sites_position = tables->sites.position; + const tsk_id_t *restrict mutations_site = tables->mutations.site; + const tsk_id_t *restrict mutations_node = tables->mutations.node; + const tsk_size_t total_mutations = tables->mutations.num_rows; + coordinate_t *restrict converted_position = self->sites.position; + + for (j = 0; j < self->num_sites; j++) { + converted_position[j] = (coordinate_t) sites_position[j]; + self->sites.mutations[j] = NULL; + self->sites.num_alleles[j] = num_alleles != NULL ? num_alleles[j] : 2; + state = ancestral_state != NULL ? ancestral_state[j] : 0; + if (state < 0 || (tsk_size_t) state >= self->sites.num_alleles[j]) { + ret = TSLS_ERR_BAD_ANCESTRAL_STATE; + goto out; + } + self->sites.ancestral_state[j] = state; + } + converted_position[j] = (coordinate_t) tables->sequence_length; + + for (j = 0; j < total_mutations; j++) { + site = mutations_site[j]; + state = derived_state != NULL ? derived_state[j] : 1; + if (state < 0 || (tsk_size_t) state >= self->sites.num_alleles[site]) { + ret = TSLS_ERR_BAD_MUTATION_DERIVED_STATE; + goto out; + } + ret = matcher_indexes_add_mutation(self, site, mutations_node[j], state); + if (ret != 0) { + goto out; + } + } +out: + return ret; +} + +static int +matcher_indexes_validate_tables(const tsk_table_collection_t *tables) +{ + tsk_size_t j; + const tsk_id_t num_nodes = (tsk_id_t) tables->nodes.num_rows; + const tsk_id_t num_sites = (tsk_id_t) tables->sites.num_rows; + + if (!is_integer(tables->sequence_length)) { + return TSLS_ERR_BAD_COORDINATE; + } + for (j = 0; j < tables->edges.num_rows; j++) { + if (tables->edges.parent[j] < 0 || tables->edges.parent[j] >= num_nodes + || tables->edges.child[j] < 0 || tables->edges.child[j] >= num_nodes) { + return TSLS_ERR_BAD_EDGE_NODE; + } + if (!is_integer(tables->edges.left[j]) || !is_integer(tables->edges.right[j])) { + return TSLS_ERR_BAD_COORDINATE; + } + } + for (j = 0; j < tables->sites.num_rows; j++) { + if (!is_integer(tables->sites.position[j])) { + return TSLS_ERR_BAD_COORDINATE; + } + } + for (j = 0; j < tables->mutations.num_rows; j++) { + if (tables->mutations.node[j] < 0 || tables->mutations.node[j] >= num_nodes) { + return TSLS_ERR_BAD_MUTATION_NODE; + } + if (tables->mutations.site[j] < 0 || tables->mutations.site[j] >= num_sites) { + return TSLS_ERR_BAD_MUTATION_SITE; + } + } + return 0; +} + +/* The tables must be sorted and indexed, as in a valid tree sequence. */ +int +matcher_indexes_alloc(matcher_indexes_t *self, const tsk_table_collection_t *tables, + const tsk_size_t *num_alleles, const allele_t *ancestral_state, + const allele_t *derived_state, tsk_flags_t flags) +{ + int ret = 0; + + memset(self, 0, sizeof(*self)); + self->flags = flags; + self->num_edges = tables->edges.num_rows; + self->num_nodes = tables->nodes.num_rows; + self->num_sites = tables->sites.num_rows; + /* num_mutations is incremented as mutations are added */ + self->num_mutations = 0; + + ret = matcher_indexes_validate_tables(tables); + if (ret != 0) { + goto out; + } + + self->left_index_edges = malloc(self->num_edges * sizeof(*self->left_index_edges)); + self->right_index_edges = malloc(self->num_edges * sizeof(*self->right_index_edges)); + self->sites.mutations = malloc(self->num_sites * sizeof(*self->sites.mutations)); + self->sites.num_alleles = malloc(self->num_sites * sizeof(*self->sites.num_alleles)); + self->sites.ancestral_state + = malloc(self->num_sites * sizeof(*self->sites.ancestral_state)); + self->sites.position + = malloc((self->num_sites + 1) * sizeof(*self->sites.position)); + if (self->left_index_edges == NULL || self->right_index_edges == NULL + || self->sites.mutations == NULL || self->sites.position == NULL + || self->sites.num_alleles == NULL || self->sites.ancestral_state == NULL) { + ret = TSLS_ERR_NO_MEMORY; + goto out; + } + if (tsi_blkalloc_init(&self->allocator, 65536) != 0) { + ret = TSLS_ERR_NO_MEMORY; + goto out; + } + matcher_indexes_copy_edge_indexes(self, tables); + ret = matcher_indexes_copy_mutation_data( + self, tables, num_alleles, ancestral_state, derived_state); + if (ret != 0) { + goto out; + } +out: + return ret; +} + +int +matcher_indexes_free(matcher_indexes_t *self) +{ + tsi_safe_free(self->left_index_edges); + tsi_safe_free(self->right_index_edges); + tsi_safe_free(self->sites.mutations); + tsi_safe_free(self->sites.position); + tsi_safe_free(self->sites.num_alleles); + tsi_safe_free(self->sites.ancestral_state); + tsi_blkalloc_free(&self->allocator); + return 0; +} + +/*=================================================================== + * Compressed matrix + *=================================================================== + */ + +int +compressed_matrix_alloc(compressed_matrix_t *self, size_t num_sites, size_t block_size) +{ + int ret = 0; + + memset(self, 0, sizeof(*self)); + self->num_sites = num_sites; + self->normalisation_factor = calloc(num_sites + 1, sizeof(double)); + self->num_values = calloc(num_sites + 1, sizeof(tsk_size_t)); + self->nodes = calloc(num_sites + 1, sizeof(tsk_id_t *)); + self->values = calloc(num_sites + 1, sizeof(double *)); + if (self->normalisation_factor == NULL || self->num_values == NULL + || self->nodes == NULL || self->values == NULL) { + ret = TSLS_ERR_NO_MEMORY; + goto out; + } + if (tsi_blkalloc_init(&self->allocator, TSK_MAX(block_size, 1024)) != 0) { + ret = TSLS_ERR_NO_MEMORY; + goto out; + } +out: + return ret; +} + +int +compressed_matrix_free(compressed_matrix_t *self) +{ + tsi_safe_free(self->normalisation_factor); + tsi_safe_free(self->num_values); + tsi_safe_free(self->nodes); + tsi_safe_free(self->values); + tsi_blkalloc_free(&self->allocator); + return 0; +} + +size_t +compressed_matrix_get_total_values(const compressed_matrix_t *self) +{ + size_t j; + size_t total = 0; + + for (j = 0; j < self->num_sites; j++) { + total += self->num_values[j]; + } + return total; +} + +/* Round up to a multiple of 8 bytes so that the doubles stay aligned. */ +static inline size_t +aligned_size(size_t size) +{ + return (size + 7) & ~((size_t) 7); +} + +static int WARN_UNUSED +compressed_matrix_store_site(compressed_matrix_t *self, tsk_id_t site, + double normalisation_factor, int num_values, const tsk_id_t *restrict nodes, + const double *restrict L) +{ + int ret = 0; + int j; + tsk_id_t *restrict nodes_out; + double *restrict values_out; + + values_out = tsi_blkalloc_get( + &self->allocator, aligned_size((size_t) num_values * sizeof(double))); + nodes_out = tsi_blkalloc_get( + &self->allocator, aligned_size((size_t) num_values * sizeof(tsk_id_t))); + if (values_out == NULL || nodes_out == NULL) { + ret = TSLS_ERR_NO_MEMORY; + goto out; + } + for (j = 0; j < num_values; j++) { + nodes_out[j] = nodes[j]; + values_out[j] = L[nodes[j]]; + } + self->normalisation_factor[site] = normalisation_factor; + self->num_values[site] = (tsk_size_t) num_values; + self->nodes[site] = nodes_out; + self->values[site] = values_out; +out: + return ret; +} + +/*=================================================================== + * Ancestor matcher + *=================================================================== + */ + +static inline bool +ancestor_matcher_in_tree(const ancestor_matcher_t *self, tsk_id_t u) +{ + return self->parent[u] != NULL_NODE || self->left_child[u] != NULL_NODE; +} + +static void +ancestor_matcher_check_state(const ancestor_matcher_t *self) +{ + int num_likelihoods = 0; + int j; + tsk_id_t u; + + for (j = 0; j < self->num_likelihood_nodes; j++) { + u = self->likelihood_nodes[j]; + assert(self->likelihood[u] >= 0); + } + for (u = 0; u < (tsk_id_t) self->num_nodes; u++) { + if (self->likelihood[u] >= 0) { + num_likelihoods++; + } + if (ancestor_matcher_in_tree(self, u)) { + assert(self->likelihood[u] != NOT_IN_TREE); + if (self->parent[u] == NULL_NODE) { + /* Roots always have a likelihood */ + assert(self->likelihood[u] >= 0); + } + } else { + assert(self->likelihood[u] == NOT_IN_TREE); + } + assert(self->allelic_state[u] == TSK_NULL); + assert(self->likelihood_cache[u] == CACHE_UNSET); + } + assert(num_likelihoods == self->num_likelihood_nodes); + (void) num_likelihoods; +} + +int +ancestor_matcher_print_state(ancestor_matcher_t *self, FILE *out) +{ + int j, k; + tsk_id_t u; + + fprintf(out, "Ancestor matcher state\n"); + fprintf(out, "site\trecomb_rate\tmut_rate\n"); + for (j = 0; j < (int) self->num_sites; j++) { + fprintf( + out, "%d\t%f\t%f\n", j, self->recombination_rate[j], self->mismatch_rate[j]); + } + fprintf(out, "tree = \n"); + fprintf(out, "id\tparent\tlchild\trchild\tlsib\trsib\tlikelihood\n"); + for (j = 0; j < (int) self->num_nodes; j++) { + fprintf(out, "%d\t%d\t%d\t%d\t%d\t%d\t%f\n", (int) j, self->parent[j], + self->left_child[j], self->right_child[j], self->left_sib[j], + self->right_sib[j], self->likelihood[j]); + } + fprintf(out, "likelihood nodes\n"); + for (j = 0; j < self->num_likelihood_nodes; j++) { + u = self->likelihood_nodes[j]; + fprintf(out, "\t%d -> %f\n", u, self->likelihood[u]); + } + fprintf(out, "traceback\n"); + for (j = 0; j < (int) self->num_sites; j++) { + fprintf(out, "\t%d:%d (%d)\t", (int) j, self->max_likelihood_node[j], + self->traceback[j].size); + for (k = 0; k < self->traceback[j].size; k++) { + fprintf(out, "(%d, %d)", self->traceback[j].node[k], + self->traceback[j].recombination_required[k]); + } + fprintf(out, "\n"); + } + tsi_blkalloc_print_state(&self->traceback_allocator, out); + return 0; +} + +int +ancestor_matcher_alloc(ancestor_matcher_t *self, + const matcher_indexes_t *matcher_indexes, const double *recombination_rate, + const double *mismatch_rate, double likelihood_threshold, int flags) +{ + int ret = 0; + const size_t N = matcher_indexes->num_nodes; + + memset(self, 0, sizeof(ancestor_matcher_t)); + self->flags = flags; + self->likelihood_threshold = likelihood_threshold; + self->matcher_indexes = matcher_indexes; + self->num_sites = matcher_indexes->num_sites; + self->num_nodes = N; + self->recombination_rate + = malloc((self->num_sites + 1) * sizeof(*self->recombination_rate)); + self->mismatch_rate = malloc((self->num_sites + 1) * sizeof(*self->mismatch_rate)); + self->traceback = calloc(self->num_sites + 1, sizeof(node_state_list_t)); + self->max_likelihood_node = malloc((self->num_sites + 1) * sizeof(tsk_id_t)); + + self->parent = malloc((N + 1) * sizeof(*self->parent)); + self->left_child = malloc((N + 1) * sizeof(*self->left_child)); + self->right_child = malloc((N + 1) * sizeof(*self->right_child)); + self->left_sib = malloc((N + 1) * sizeof(*self->left_sib)); + self->right_sib = malloc((N + 1) * sizeof(*self->right_sib)); + self->num_below = malloc((N + 1) * sizeof(*self->num_below)); + self->num_inheriting = malloc((N + 1) * sizeof(*self->num_inheriting)); + self->likelihood = malloc((N + 1) * sizeof(*self->likelihood)); + self->allelic_state = malloc((N + 1) * sizeof(*self->allelic_state)); + self->recombination_required + = malloc((N + 1) * sizeof(*self->recombination_required)); + self->likelihood_cache = malloc((N + 1) * sizeof(*self->likelihood_cache)); + self->likelihood_nodes = malloc((N + 1) * sizeof(*self->likelihood_nodes)); + self->likelihood_nodes_tmp = malloc((N + 1) * sizeof(*self->likelihood_nodes_tmp)); + + if (self->recombination_rate == NULL || self->mismatch_rate == NULL + || self->traceback == NULL || self->max_likelihood_node == NULL + || self->parent == NULL || self->left_child == NULL || self->right_child == NULL + || self->left_sib == NULL || self->right_sib == NULL || self->num_below == NULL + || self->num_inheriting == NULL || self->recombination_required == NULL + || self->likelihood == NULL || self->likelihood_cache == NULL + || self->likelihood_nodes == NULL || self->likelihood_nodes_tmp == NULL + || self->allelic_state == NULL) { + ret = TSLS_ERR_NO_MEMORY; + goto out; + } + /* Alloc in blocks of at least 64MiB, and large enough to hold the + * traceback for a site in which every node has a likelihood. */ + self->traceback_block_size + = TSK_MAX(64 * 1024 * 1024, aligned_size((N + 1) * sizeof(tsk_id_t)) * 2); + /* If the traceback allocator is using more than 2GiB of RAM free it, so + * that other threads can use the memory */ + self->traceback_realloc_size = 2L * 1024L * 1024L * 1024L; + if (tsi_blkalloc_init(&self->traceback_allocator, self->traceback_block_size) + != 0) { + ret = TSLS_ERR_NO_MEMORY; + goto out; + } + memcpy(self->recombination_rate, recombination_rate, + self->num_sites * sizeof(*self->recombination_rate)); + memcpy(self->mismatch_rate, mismatch_rate, + self->num_sites * sizeof(*self->mismatch_rate)); + memset(self->allelic_state, 0xff, N * sizeof(*self->allelic_state)); + memset(self->max_likelihood_node, 0xff, self->num_sites * sizeof(tsk_id_t)); +out: + return ret; +} + +int +ancestor_matcher_free(ancestor_matcher_t *self) +{ + tsi_safe_free(self->recombination_rate); + tsi_safe_free(self->mismatch_rate); + tsi_safe_free(self->parent); + tsi_safe_free(self->left_child); + tsi_safe_free(self->right_child); + tsi_safe_free(self->left_sib); + tsi_safe_free(self->right_sib); + tsi_safe_free(self->num_below); + tsi_safe_free(self->num_inheriting); + tsi_safe_free(self->recombination_required); + tsi_safe_free(self->likelihood); + tsi_safe_free(self->likelihood_cache); + tsi_safe_free(self->likelihood_nodes); + tsi_safe_free(self->likelihood_nodes_tmp); + tsi_safe_free(self->allelic_state); + tsi_safe_free(self->max_likelihood_node); + tsi_safe_free(self->traceback); + tsi_blkalloc_free(&self->traceback_allocator); + return 0; +} + +static inline void +ancestor_matcher_add_likelihood(ancestor_matcher_t *self, tsk_id_t u, double value) +{ + self->likelihood[u] = value; + self->likelihood_nodes[self->num_likelihood_nodes] = u; + self->num_likelihood_nodes++; +} + +/* Remove the specified node from the list of nodes with likelihoods. */ +static void +ancestor_matcher_delete_likelihood(ancestor_matcher_t *self, const tsk_id_t node) +{ + int j, k; + tsk_id_t *restrict L_nodes = self->likelihood_nodes; + + k = 0; + for (j = 0; j < self->num_likelihood_nodes; j++) { + L_nodes[k] = L_nodes[j]; + if (L_nodes[j] != node) { + k++; + } + } + assert(self->num_likelihood_nodes == k + 1); + self->num_likelihood_nodes = k; +} + +static inline void +ancestor_matcher_update_num_below(ancestor_matcher_t *self, tsk_id_t u, int64_t delta) +{ + const tsk_id_t *restrict parent = self->parent; + int64_t *restrict num_below = self->num_below; + + for (; u != NULL_NODE; u = parent[u]) { + num_below[u] += delta; + } +} + +/* Returns the likelihood inherited by node u, filling in the likelihood + * cache along the path so that later queries are fast. */ +static double +ancestor_matcher_get_inherited_likelihood(ancestor_matcher_t *self, tsk_id_t u) +{ + const double *restrict L = self->likelihood; + double *restrict L_cache = self->likelihood_cache; + const tsk_id_t *restrict parent = self->parent; + double value; + tsk_id_t v; + + v = u; + while (likely(L[v] == NULL_LIKELIHOOD) && likely(L_cache[v] == CACHE_UNSET)) { + v = parent[v]; + assert(v != NULL_NODE); + } + value = L_cache[v]; + if (unlikely(value == CACHE_UNSET)) { + value = L[v]; + } + assert(value >= 0); + v = u; + while (likely(L[v] == NULL_LIKELIHOOD) && likely(L_cache[v] == CACHE_UNSET)) { + L_cache[v] = value; + v = parent[v]; + } + return value; +} + +static void +ancestor_matcher_reset_likelihood_cache(ancestor_matcher_t *self, tsk_id_t u) +{ + double *restrict L_cache = self->likelihood_cache; + const tsk_id_t *restrict parent = self->parent; + + while (u != NULL_NODE && L_cache[u] != CACHE_UNSET) { + L_cache[u] = CACHE_UNSET; + u = parent[u]; + } +} + +/* Remove the specified edge from the tree, and if update_likelihoods is + * true make sure that the child keeps its likelihood. Nodes that are + * isolated after removing the edge are added to the isolated list; they + * only leave the tree if they are still isolated after all the edges at + * this position have been inserted (see ancestor_matcher_next_tree). */ +static void +ancestor_matcher_remove_edge(ancestor_matcher_t *self, edge_t edge, + bool update_likelihoods, tsk_id_t *restrict isolated, int *num_isolated) +{ + double *restrict L = self->likelihood; + const tsk_id_t p = edge.parent; + const tsk_id_t c = edge.child; + + if (self->track_num_below) { + ancestor_matcher_update_num_below(self, p, -self->num_below[c]); + } + if (update_likelihoods && L[c] == NULL_LIKELIHOOD) { + ancestor_matcher_add_likelihood( + self, c, ancestor_matcher_get_inherited_likelihood(self, p)); + } + remove_edge(edge, self->parent, self->left_child, self->right_child, self->left_sib, + self->right_sib); + if (update_likelihoods) { + /* Each node can only become isolated once when removing edges, so + * the isolated list has at most num_nodes entries. */ + if (!ancestor_matcher_in_tree(self, c)) { + isolated[*num_isolated] = c; + (*num_isolated)++; + } + if (!ancestor_matcher_in_tree(self, p)) { + /* p was a root, and so has a likelihood */ + assert(L[p] >= 0); + isolated[*num_isolated] = p; + (*num_isolated)++; + } + } +} + +/* Insert the specified edge into the tree. Nodes entering the tree get + * likelihood 0, and so can only be reached by recombination. */ +static void +ancestor_matcher_insert_edge( + ancestor_matcher_t *self, edge_t edge, bool update_likelihoods) +{ + double *restrict L = self->likelihood; + + if (update_likelihoods) { + if (L[edge.parent] == NOT_IN_TREE) { + ancestor_matcher_add_likelihood(self, edge.parent, 0); + } + if (L[edge.child] == NOT_IN_TREE) { + ancestor_matcher_add_likelihood(self, edge.child, 0); + } + /* Otherwise the child was a root and keeps its likelihood */ + assert(L[edge.child] >= 0); + } + insert_edge(edge, self->parent, self->left_child, self->right_child, self->left_sib, + self->right_sib); + if (self->track_num_below) { + ancestor_matcher_update_num_below(self, edge.parent, self->num_below[edge.child]); + } +} + +/* The current position in the sequence of trees, moving left-to-right + * (direction = 1) or right-to-left (direction = -1). */ +typedef struct { + int direction; + int_fast32_t in_index; + int_fast32_t out_index; + coordinate_t left; + coordinate_t right; +} tree_position_t; + +static void +tree_position_init( + tree_position_t *self, const matcher_indexes_t *matcher_indexes, int direction) +{ + const int_fast32_t M = (int_fast32_t) matcher_indexes->num_edges; + const coordinate_t L = matcher_indexes->sites.position[matcher_indexes->num_sites]; + + self->direction = direction; + if (direction > 0) { + self->in_index = 0; + self->out_index = 0; + self->left = 0; + self->right = 0; + } else { + self->in_index = M - 1; + self->out_index = M - 1; + self->left = L; + self->right = L; + } +} + +/* Move to the next tree in the direction of travel. */ +static void +ancestor_matcher_next_tree( + ancestor_matcher_t *self, tree_position_t *tp, bool update_likelihoods) +{ + const matcher_indexes_t *mi = self->matcher_indexes; + const int_fast32_t M = (int_fast32_t) mi->num_edges; + const coordinate_t sequence_length = mi->sites.position[mi->num_sites]; + const edge_t *restrict left_index = mi->left_index_edges; + const edge_t *restrict right_index = mi->right_index_edges; + tsk_id_t *restrict isolated = self->likelihood_nodes_tmp; + double *restrict L = self->likelihood; + int_fast32_t j, removal_start; + int k, num_isolated = 0; + coordinate_t pos; + tsk_id_t u; + + if (tp->direction > 0) { + pos = tp->right; + removal_start = tp->out_index; + while (tp->out_index < M && right_index[tp->out_index].right == pos) { + ancestor_matcher_remove_edge(self, right_index[tp->out_index], + update_likelihoods, isolated, &num_isolated); + tp->out_index++; + } + if (update_likelihoods) { + for (j = removal_start; j < tp->out_index; j++) { + ancestor_matcher_reset_likelihood_cache(self, right_index[j].parent); + } + } + while (tp->in_index < M && left_index[tp->in_index].left == pos) { + ancestor_matcher_insert_edge( + self, left_index[tp->in_index], update_likelihoods); + tp->in_index++; + } + tp->left = pos; + tp->right = sequence_length; + if (tp->in_index < M) { + tp->right = TSK_MIN(tp->right, left_index[tp->in_index].left); + } + if (tp->out_index < M) { + tp->right = TSK_MIN(tp->right, right_index[tp->out_index].right); + } + } else { + pos = tp->left; + removal_start = tp->out_index; + while (tp->out_index >= 0 && left_index[tp->out_index].left == pos) { + ancestor_matcher_remove_edge(self, left_index[tp->out_index], + update_likelihoods, isolated, &num_isolated); + tp->out_index--; + } + if (update_likelihoods) { + for (j = removal_start; j > tp->out_index; j--) { + ancestor_matcher_reset_likelihood_cache(self, left_index[j].parent); + } + } + while (tp->in_index >= 0 && right_index[tp->in_index].right == pos) { + ancestor_matcher_insert_edge( + self, right_index[tp->in_index], update_likelihoods); + tp->in_index--; + } + tp->right = pos; + tp->left = 0; + if (tp->out_index >= 0) { + tp->left = TSK_MAX(tp->left, left_index[tp->out_index].left); + } + if (tp->in_index >= 0) { + tp->left = TSK_MAX(tp->left, right_index[tp->in_index].right); + } + } + /* Nodes that are still isolated have left the tree. Nodes that were + * isolated and then inserted again keep their likelihoods. */ + for (k = 0; k < num_isolated; k++) { + u = isolated[k]; + if (!ancestor_matcher_in_tree(self, u)) { + ancestor_matcher_delete_likelihood(self, u); + L[u] = NOT_IN_TREE; + } + } +} + +/* Reset the matcher and build the tree containing the specified position, + * giving each root the specified initial likelihood. */ +static void +ancestor_matcher_init_sweep(ancestor_matcher_t *self, tree_position_t *tp, + int direction, coordinate_t position, double initial_value) +{ + const size_t N = self->num_nodes; + tsk_id_t u; + size_t j; + + memset(self->parent, 0xff, N * sizeof(*self->parent)); + memset(self->left_child, 0xff, N * sizeof(*self->left_child)); + memset(self->right_child, 0xff, N * sizeof(*self->right_child)); + memset(self->left_sib, 0xff, N * sizeof(*self->left_sib)); + memset(self->right_sib, 0xff, N * sizeof(*self->right_sib)); + memset(self->recombination_required, 0xff, N * sizeof(*self->recombination_required)); + for (j = 0; j < N; j++) { + self->num_below[j] = 1; + self->likelihood_cache[j] = CACHE_UNSET; + } + tree_position_init(tp, self->matcher_indexes, direction); + do { + ancestor_matcher_next_tree(self, tp, false); + } while (!(tp->left <= position && position < tp->right)); + + self->num_likelihood_nodes = 0; + for (u = 0; u < (tsk_id_t) N; u++) { + if (self->parent[u] != NULL_NODE) { + self->likelihood[u] = NULL_LIKELIHOOD; + } else if (self->left_child[u] != NULL_NODE) { + ancestor_matcher_add_likelihood(self, u, initial_value); + } else { + self->likelihood[u] = NOT_IN_TREE; + } + } +} + +static inline void +ancestor_matcher_set_allelic_state(ancestor_matcher_t *self, const tsk_id_t site) +{ + mutation_list_node_t *mutation; + + for (mutation = self->matcher_indexes->sites.mutations[site]; mutation != NULL; + mutation = mutation->next) { + self->allelic_state[mutation->node] = mutation->derived_state; + } +} + +static inline void +ancestor_matcher_unset_allelic_state(ancestor_matcher_t *self, const tsk_id_t site) +{ + mutation_list_node_t *mutation; + + for (mutation = self->matcher_indexes->sites.mutations[site]; mutation != NULL; + mutation = mutation->next) { + self->allelic_state[mutation->node] = TSK_NULL; + } +} + +/* Make sure that every node with a mutation at this site that is in the + * tree has its own likelihood. */ +static void +ancestor_matcher_insert_mutation_likelihoods( + ancestor_matcher_t *self, const tsk_id_t site) +{ + const double *restrict L = self->likelihood; + const tsk_id_t *restrict parent = self->parent; + mutation_list_node_t *mutation; + tsk_id_t u; + + for (mutation = self->matcher_indexes->sites.mutations[site]; mutation != NULL; + mutation = mutation->next) { + if (L[mutation->node] == NULL_LIKELIHOOD) { + u = mutation->node; + while (L[u] == NULL_LIKELIHOOD) { + u = parent[u]; + assert(u != NULL_NODE); + } + ancestor_matcher_add_likelihood(self, mutation->node, L[u]); + } + } +} + +/* Remove likelihoods that are equal to the likelihood inherited from the + * parent. */ +static void +ancestor_matcher_coalesce_likelihoods(ancestor_matcher_t *self) +{ + double L_p; + tsk_id_t u, p; + double *restrict L = self->likelihood; + const tsk_id_t *restrict parent = self->parent; + tsk_id_t *restrict cached_paths = self->likelihood_nodes_tmp; + const int old_num_likelihood_nodes = self->num_likelihood_nodes; + tsk_id_t *restrict L_nodes = self->likelihood_nodes; + int j, num_cached_paths, num_likelihood_nodes; + + num_cached_paths = 0; + num_likelihood_nodes = 0; + for (j = 0; j < old_num_likelihood_nodes; j++) { + u = L_nodes[j]; + p = parent[u]; + if (p != NULL_NODE) { + cached_paths[num_cached_paths] = p; + num_cached_paths++; + L_p = ancestor_matcher_get_inherited_likelihood(self, p); + if (L[u] == L_p) { + L[u] = NULL_LIKELIHOOD; + } + } + if (L[u] >= 0) { + L_nodes[num_likelihood_nodes] = L_nodes[j]; + num_likelihood_nodes++; + } + } + self->num_likelihood_nodes = num_likelihood_nodes; + for (j = 0; j < num_cached_paths; j++) { + ancestor_matcher_reset_likelihood_cache(self, cached_paths[j]); + } +} + +static inline double +emission_probability(allele_t node_state, allele_t state, double mu, double num_alleles) +{ + if (state == TSLS_MISSING_DATA || node_state == state) { + return 1 - (num_alleles - 1) * mu; + } + return mu; +} + +static inline int +check_haplotype_state(const ancestor_matcher_t *self, tsk_id_t site, allele_t state) +{ + const tsk_size_t num_alleles = self->matcher_indexes->sites.num_alleles[site]; + + if (state < TSLS_MISSING_DATA || (state >= 0 && (tsk_size_t) state >= num_alleles)) { + return TSLS_ERR_BAD_HAPLOTYPE_ALLELE; + } + return 0; +} + +static inline double +ancestor_matcher_get_n(const ancestor_matcher_t *self) +{ + double n = 1.0; + if (!(self->flags & TSLS_DISABLE_WEIGHT_BY_N)) { + n = (double) self->num_nodes; + } + return n; +} + +/* Compute the number of nodes in the tree that inherit the likelihood of + * each likelihood node. */ +static void +ancestor_matcher_compute_num_inheriting(ancestor_matcher_t *self) +{ + const double *restrict L = self->likelihood; + const tsk_id_t *restrict parent = self->parent; + const tsk_id_t *restrict L_nodes = self->likelihood_nodes; + const int64_t *restrict num_below = self->num_below; + int64_t *restrict num_inheriting = self->num_inheriting; + tsk_id_t u, w; + int j; + + for (j = 0; j < self->num_likelihood_nodes; j++) { + u = L_nodes[j]; + num_inheriting[u] = num_below[u]; + } + for (j = 0; j < self->num_likelihood_nodes; j++) { + u = L_nodes[j]; + w = parent[u]; + if (w != NULL_NODE) { + while (L[w] == NULL_LIKELIHOOD) { + w = parent[w]; + } + num_inheriting[w] -= num_below[u]; + } + } +} + +/* Returns the sum of the likelihoods over all the nodes in the tree. */ +static double +ancestor_matcher_total_likelihood(const ancestor_matcher_t *self) +{ + const double *restrict L = self->likelihood; + const tsk_id_t *restrict L_nodes = self->likelihood_nodes; + const int64_t *restrict num_inheriting = self->num_inheriting; + double total = 0; + tsk_id_t u; + int j; + + for (j = 0; j < self->num_likelihood_nodes; j++) { + u = L_nodes[j]; + assert(num_inheriting[u] > 0); + total += (double) num_inheriting[u] * L[u]; + } + return total; +} + +/* Store the recombination_required state in the traceback */ +static int WARN_UNUSED +ancestor_matcher_store_traceback(ancestor_matcher_t *self, const tsk_id_t site_id) +{ + int ret = 0; + tsk_id_t u; + int j; + int8_t *restrict list_R; + tsk_id_t *restrict list_node; + node_state_list_t *restrict list; + node_state_list_t *restrict T = self->traceback; + const tsk_id_t *restrict nodes = self->likelihood_nodes; + const int8_t *restrict R = self->recombination_required; + const int num_likelihood_nodes = self->num_likelihood_nodes; + bool match; + + /* Check to see if the previous site has the same recombination_required. If so, + * we can reuse the same list. */ + match = false; + if (site_id > 0) { + list = &T[site_id - 1]; + if (list->size == num_likelihood_nodes) { + list_node = list->node; + list_R = list->recombination_required; + match = true; + for (j = 0; j < num_likelihood_nodes; j++) { + if (list_node[j] != nodes[j] || list_R[j] != R[nodes[j]]) { + match = false; + break; + } + } + } + } + + if (match) { + T[site_id].size = T[site_id - 1].size; + T[site_id].node = T[site_id - 1].node; + T[site_id].recombination_required = T[site_id - 1].recombination_required; + } else { + list_node = tsi_blkalloc_get(&self->traceback_allocator, + aligned_size((size_t) num_likelihood_nodes * sizeof(tsk_id_t))); + list_R = tsi_blkalloc_get(&self->traceback_allocator, + aligned_size((size_t) num_likelihood_nodes * sizeof(int8_t))); + if (list_node == NULL || list_R == NULL) { + ret = TSLS_ERR_NO_MEMORY; + goto out; + } + T[site_id].node = list_node; + T[site_id].recombination_required = list_R; + T[site_id].size = num_likelihood_nodes; + for (j = 0; j < num_likelihood_nodes; j++) { + u = nodes[j]; + list_node[j] = u; + list_R[j] = R[u]; + } + } + self->total_traceback_size += (size_t) num_likelihood_nodes; +out: + return ret; +} + +static int WARN_UNUSED +ancestor_matcher_process_site_viterbi( + ancestor_matcher_t *self, const tsk_id_t site, const allele_t state) +{ + int ret = 0; + const int num_likelihood_nodes = self->num_likelihood_nodes; + const tsk_id_t *restrict L_nodes = self->likelihood_nodes; + const tsk_id_t *restrict parent = self->parent; + const allele_t *restrict allelic_state = self->allelic_state; + const allele_t ancestral_state = self->matcher_indexes->sites.ancestral_state[site]; + double *restrict L = self->likelihood; + int8_t *restrict recombination_required = self->recombination_required; + const double rho = self->recombination_rate[site]; + const double mu = self->mismatch_rate[site]; + const double n = ancestor_matcher_get_n(self); + const double num_alleles = (double) self->matcher_indexes->sites.num_alleles[site]; + double max_L, p_no_recomb, p_recomb, p_t, p_e; + tsk_id_t u, max_L_node; + int j; + + ancestor_matcher_insert_mutation_likelihoods(self, site); + ancestor_matcher_set_allelic_state(self, site); + max_L = -1; + max_L_node = NULL_NODE; + for (j = 0; j < self->num_likelihood_nodes; j++) { + u = L_nodes[j]; + p_no_recomb = L[u] * (1 - rho + rho / n); + p_recomb = rho / n; + recombination_required[u] = false; + if (p_no_recomb > p_recomb) { + p_t = p_no_recomb; + } else { + p_t = p_recomb; + recombination_required[u] = true; + } + p_e = emission_probability( + get_allelic_state(allelic_state, parent, u, ancestral_state), state, mu, + num_alleles); + L[u] = p_t * p_e; + if (L[u] > max_L) { + max_L = L[u]; + max_L_node = u; + } + } + ancestor_matcher_unset_allelic_state(self, site); + (void) num_likelihood_nodes; + + if (max_L <= 0) { + if (mu <= 0 || mu >= 1) { + ret = TSLS_ERR_MATCH_IMPOSSIBLE_EXTREME_MUTATION_PROBA; + } else if (rho == 0) { + ret = TSLS_ERR_MATCH_IMPOSSIBLE_ZERO_RECOMB_PRECISION; + } else { + ret = TSLS_ERR_MATCH_IMPOSSIBLE; + } + goto out; + } + assert(max_L_node != NULL_NODE); + self->max_likelihood_node[site] = max_L_node; + for (j = 0; j < self->num_likelihood_nodes; j++) { + u = L_nodes[j]; + L[u] = TSK_MAX(L[u] / max_L, self->likelihood_threshold); + } + ret = ancestor_matcher_store_traceback(self, site); + if (ret != 0) { + goto out; + } + ancestor_matcher_coalesce_likelihoods(self); +out: + return ret; +} + +/* Recombination redistributes the total probability over the states at the + * previous site, including nodes that have since left the tree, so we + * compute the total at the end of each site and use it at the next. At the + * first site, previous_total is negative and we use the current total. */ +static int WARN_UNUSED +ancestor_matcher_process_site_forward(ancestor_matcher_t *self, const tsk_id_t site, + const allele_t state, double *previous_total, compressed_matrix_t *output) +{ + int ret = 0; + const tsk_id_t *restrict L_nodes = self->likelihood_nodes; + const tsk_id_t *restrict parent = self->parent; + const allele_t *restrict allelic_state = self->allelic_state; + const allele_t ancestral_state = self->matcher_indexes->sites.ancestral_state[site]; + double *restrict L = self->likelihood; + const double rho = self->recombination_rate[site]; + const double mu = self->mismatch_rate[site]; + const double n = ancestor_matcher_get_n(self); + const double num_alleles = (double) self->matcher_indexes->sites.num_alleles[site]; + double total, normalisation_factor, p_e; + tsk_id_t u; + int j; + + ancestor_matcher_insert_mutation_likelihoods(self, site); + ancestor_matcher_compute_num_inheriting(self); + total = *previous_total; + if (total < 0) { + total = ancestor_matcher_total_likelihood(self); + } + ancestor_matcher_set_allelic_state(self, site); + for (j = 0; j < self->num_likelihood_nodes; j++) { + u = L_nodes[j]; + p_e = emission_probability( + get_allelic_state(allelic_state, parent, u, ancestral_state), state, mu, + num_alleles); + L[u] = (L[u] * (1 - rho) + rho / n * total) * p_e; + } + ancestor_matcher_unset_allelic_state(self, site); + normalisation_factor = ancestor_matcher_total_likelihood(self); + if (!(normalisation_factor > 0)) { + ret = TSLS_ERR_MATCH_IMPOSSIBLE; + goto out; + } + for (j = 0; j < self->num_likelihood_nodes; j++) { + u = L_nodes[j]; + L[u] = TSK_MAX(L[u] / normalisation_factor, self->likelihood_threshold); + } + *previous_total = ancestor_matcher_total_likelihood(self); + ret = compressed_matrix_store_site(output, site, normalisation_factor, + self->num_likelihood_nodes, L_nodes, L); + if (ret != 0) { + goto out; + } + ancestor_matcher_coalesce_likelihoods(self); +out: + return ret; +} + +/* The backward transition from site l to site l - 1 depends on the tree at + * site l - 1 (nodes may have entered or left the tree), so we apply it + * lazily when we reach site l - 1, using these saved values from site l. + */ +typedef struct { + bool pending; + double total; + double rho; + double normalisation_factor; +} backward_transition_t; + +static int WARN_UNUSED +ancestor_matcher_process_site_backward(ancestor_matcher_t *self, const tsk_id_t site, + const allele_t state, const double *forward_normalisation_factor, + backward_transition_t *transition, compressed_matrix_t *output) +{ + int ret = 0; + const tsk_id_t *restrict L_nodes = self->likelihood_nodes; + const tsk_id_t *restrict parent = self->parent; + const allele_t *restrict allelic_state = self->allelic_state; + const allele_t ancestral_state = self->matcher_indexes->sites.ancestral_state[site]; + double *restrict L = self->likelihood; + const double mu = self->mismatch_rate[site]; + const double n = ancestor_matcher_get_n(self); + const double num_alleles = (double) self->matcher_indexes->sites.num_alleles[site]; + double x; + tsk_id_t u; + int j; + + if (transition->pending) { + for (j = 0; j < self->num_likelihood_nodes; j++) { + u = L_nodes[j]; + x = (1 - transition->rho) * L[u] + transition->rho * transition->total / n; + L[u] = TSK_MAX( + x / transition->normalisation_factor, self->likelihood_threshold); + } + } + ancestor_matcher_insert_mutation_likelihoods(self, site); + ret = compressed_matrix_store_site(output, site, forward_normalisation_factor[site], + self->num_likelihood_nodes, L_nodes, L); + if (ret != 0) { + goto out; + } + ancestor_matcher_compute_num_inheriting(self); + ancestor_matcher_set_allelic_state(self, site); + for (j = 0; j < self->num_likelihood_nodes; j++) { + u = L_nodes[j]; + L[u] *= emission_probability( + get_allelic_state(allelic_state, parent, u, ancestral_state), state, mu, + num_alleles); + } + ancestor_matcher_unset_allelic_state(self, site); + transition->pending = true; + transition->total = ancestor_matcher_total_likelihood(self); + transition->rho = self->recombination_rate[site]; + transition->normalisation_factor = forward_normalisation_factor[site]; + ancestor_matcher_coalesce_likelihoods(self); +out: + return ret; +} + +/* Run the specified algorithm over sites [start, end). */ +static int WARN_UNUSED +ancestor_matcher_run(ancestor_matcher_t *self, int mode, tsk_id_t start, tsk_id_t end, + const allele_t *haplotype, const double *forward_normalisation_factor, + compressed_matrix_t *output) +{ + int ret = 0; + tree_position_t tp; + backward_transition_t transition; + const coordinate_t *restrict position = self->matcher_indexes->sites.position; + const int direction = mode == MODE_BACKWARD ? -1 : 1; + tsk_id_t site, first_site; + double previous_total = -1; + + for (site = start; site < end; site++) { + ret = check_haplotype_state(self, site, haplotype[site]); + if (ret != 0) { + goto out; + } + } + self->track_num_below = mode != MODE_VITERBI; + first_site = direction > 0 ? start : end - 1; + ancestor_matcher_init_sweep(self, &tp, direction, position[first_site], 1.0); + transition.pending = false; + + for (site = first_site; start <= site && site < end; site += direction) { + while (!(tp.left <= position[site] && position[site] < tp.right)) { + ancestor_matcher_next_tree(self, &tp, true); + } + if (self->flags & TSLS_EXTENDED_CHECKS) { + ancestor_matcher_check_state(self); + } + if (mode == MODE_VITERBI) { + ret = ancestor_matcher_process_site_viterbi(self, site, haplotype[site]); + } else if (mode == MODE_FORWARD) { + ret = ancestor_matcher_process_site_forward( + self, site, haplotype[site], &previous_total, output); + } else { + ret = ancestor_matcher_process_site_backward(self, site, haplotype[site], + forward_normalisation_factor, &transition, output); + } + if (ret != 0) { + goto out; + } + } +out: + return ret; +} + +/* Resets the recombination_required array from the traceback at the specified site. + */ +static inline void +ancestor_matcher_set_recombination_required( + ancestor_matcher_t *self, tsk_id_t site, int8_t *restrict recombination_required) +{ + int j; + const int8_t *restrict R = self->traceback[site].recombination_required; + const tsk_id_t *restrict node = self->traceback[site].node; + const int size = self->traceback[site].size; + + for (j = 0; j < size; j++) { + recombination_required[node[j]] = R[j]; + } +} + +/* Unsets the likelihood array from the traceback at the specified site. + */ +static inline void +ancestor_matcher_unset_recombination_required( + ancestor_matcher_t *self, tsk_id_t site, int8_t *restrict recombination_required) +{ + int j; + const tsk_id_t *restrict node = self->traceback[site].node; + const int size = self->traceback[site].size; + + for (j = 0; j < size; j++) { + recombination_required[node[j]] = -1; + } +} + +static void +ancestor_matcher_run_traceback(ancestor_matcher_t *self, tsk_id_t start, tsk_id_t end, + allele_t *restrict match, size_t *path_length_out, tsk_id_t *restrict path_left, + tsk_id_t *restrict path_right, tsk_id_t *restrict path_parent) +{ + tsk_id_t site; + edge_t edge; + tsk_id_t u, max_likelihood_node; + coordinate_t left, right, pos, start_pos, end_pos; + tsk_id_t *restrict parent = self->parent; + const allele_t *restrict allelic_state = self->allelic_state; + int8_t *restrict recombination_required = self->recombination_required; + const edge_t *restrict in = self->matcher_indexes->right_index_edges; + const edge_t *restrict out = self->matcher_indexes->left_index_edges; + const coordinate_t *restrict sites_position = self->matcher_indexes->sites.position; + const allele_t *restrict ancestral_state + = self->matcher_indexes->sites.ancestral_state; + const coordinate_t sequence_length = sites_position[self->num_sites]; + int_fast32_t in_index = (int_fast32_t) self->matcher_indexes->num_edges - 1; + int_fast32_t out_index = (int_fast32_t) self->matcher_indexes->num_edges - 1; + size_t path_length = 0; + + start_pos = start == 0 ? 0 : sites_position[start]; + end_pos = sites_position[end]; + + /* Prepare for the traceback and get the memory ready for recording + * the output edges. */ + path_right[path_length] = end_pos; + max_likelihood_node = self->max_likelihood_node[end - 1]; + assert(max_likelihood_node != NULL_NODE); + path_parent[path_length] = max_likelihood_node; + + /* Now go through the trees in reverse and run the traceback */ + memset(parent, 0xff, self->num_nodes * sizeof(*parent)); + memset( + recombination_required, 0xff, self->num_nodes * sizeof(*recombination_required)); + pos = sequence_length; + site = (tsk_id_t) self->num_sites - 1; + + while (pos > start_pos) { + while (out_index >= 0 && out[out_index].left == pos) { + edge = out[out_index]; + out_index--; + parent[edge.child] = NULL_NODE; + } + while (in_index >= 0 && in[in_index].right == pos) { + edge = in[in_index]; + in_index--; + parent[edge.child] = edge.parent; + } + right = pos; + left = 0; + if (out_index >= 0) { + left = TSK_MAX(left, out[out_index].left); + } + if (in_index >= 0) { + left = TSK_MAX(left, in[in_index].right); + } + pos = left; + + /* The tree is ready; perform the traceback at each site in this tree */ + assert(left < right); + for (; site >= 0 && left <= sites_position[site] && sites_position[site] < right; + site--) { + if (start_pos <= sites_position[site] && sites_position[site] < end_pos) { + ancestor_matcher_set_allelic_state(self, site); + u = path_parent[path_length]; + match[site] = get_allelic_state( + allelic_state, parent, u, ancestral_state[site]); + ancestor_matcher_unset_allelic_state(self, site); + + /* Mark the traceback nodes on the tree */ + ancestor_matcher_set_recombination_required( + self, site, recombination_required); + + /* Traverse up the tree from the current node. The first marked + * node that we meet tells us whether we need to recombine. Every + * root has a likelihood, and so is marked. */ + while (recombination_required[u] == -1) { + u = parent[u]; + assert(u != NULL_NODE); + } + if (recombination_required[u] && site > start) { + max_likelihood_node = self->max_likelihood_node[site - 1]; + assert(max_likelihood_node != NULL_NODE); + path_left[path_length] = sites_position[site]; + path_length++; + /* Start the next output edge */ + path_right[path_length] = sites_position[site]; + path_parent[path_length] = max_likelihood_node; + } + /* Unset the values in the tree for the next site. */ + ancestor_matcher_unset_recombination_required( + self, site, recombination_required); + } + } + } + path_left[path_length] = start_pos; + path_length++; + *path_length_out = path_length; +} + +static int +ancestor_matcher_reset_traceback(ancestor_matcher_t *self) +{ + int ret = 0; + + if (self->traceback_allocator.total_size > self->traceback_realloc_size) { + tsi_blkalloc_free(&self->traceback_allocator); + if (tsi_blkalloc_init(&self->traceback_allocator, self->traceback_block_size) + != 0) { + ret = TSLS_ERR_NO_MEMORY; + goto out; + } + } else { + tsi_blkalloc_reset(&self->traceback_allocator); + } + self->total_traceback_size = 0; +out: + return ret; +} + +int +ancestor_matcher_find_path(ancestor_matcher_t *self, tsk_id_t start, tsk_id_t end, + const allele_t *haplotype, allele_t *matched_haplotype, size_t *path_length, + tsk_id_t *path_left, tsk_id_t *path_right, tsk_id_t *path_parent) +{ + int ret = 0; + + if (start < 0 || end > (tsk_id_t) self->num_sites || start >= end) { + ret = TSLS_ERR_BAD_SITE_RANGE; + goto out; + } + ret = ancestor_matcher_reset_traceback(self); + if (ret != 0) { + goto out; + } + ret = ancestor_matcher_run(self, MODE_VITERBI, start, end, haplotype, NULL, NULL); + if (ret != 0) { + goto out; + } + ancestor_matcher_run_traceback(self, start, end, matched_haplotype, path_length, + path_left, path_right, path_parent); +out: + /* Reset some memory for the next call */ + memset(self->traceback, 0, self->num_sites * sizeof(*self->traceback)); + memset(self->max_likelihood_node, 0xff, + self->num_sites * sizeof(*self->max_likelihood_node)); + memset(self->allelic_state, 0xff, self->num_nodes * sizeof(*self->allelic_state)); + return ret; +} + +int +ancestor_matcher_forward( + ancestor_matcher_t *self, const allele_t *haplotype, compressed_matrix_t *output) +{ + int ret = 0; + + if (output->num_sites != self->num_sites) { + ret = TSLS_ERR_BAD_SITE_RANGE; + goto out; + } + if (self->num_sites > 0) { + ret = ancestor_matcher_run(self, MODE_FORWARD, 0, (tsk_id_t) self->num_sites, + haplotype, NULL, output); + } +out: + memset(self->allelic_state, 0xff, self->num_nodes * sizeof(*self->allelic_state)); + return ret; +} + +int +ancestor_matcher_backward(ancestor_matcher_t *self, const allele_t *haplotype, + const double *forward_normalisation_factor, compressed_matrix_t *output) +{ + int ret = 0; + size_t j; + + if (output->num_sites != self->num_sites) { + ret = TSLS_ERR_BAD_SITE_RANGE; + goto out; + } + for (j = 0; j < self->num_sites; j++) { + if (!(forward_normalisation_factor[j] > 0)) { + ret = TSLS_ERR_BAD_NORMALISATION_FACTOR; + goto out; + } + } + if (self->num_sites > 0) { + ret = ancestor_matcher_run(self, MODE_BACKWARD, 0, (tsk_id_t) self->num_sites, + haplotype, forward_normalisation_factor, output); + } +out: + memset(self->allelic_state, 0xff, self->num_nodes * sizeof(*self->allelic_state)); + return ret; +} + +double +ancestor_matcher_get_mean_traceback_size(ancestor_matcher_t *self) +{ + if (self->num_sites == 0) { + return 0; + } + return (double) self->total_traceback_size / ((double) self->num_sites); +} + +size_t +ancestor_matcher_get_total_memory(ancestor_matcher_t *self) +{ + return self->traceback_allocator.total_size; +} diff --git a/tsls/lib/meson.build b/tsls/lib/meson.build new file mode 100644 index 0000000..7e27f16 --- /dev/null +++ b/tsls/lib/meson.build @@ -0,0 +1,29 @@ +project('tsls', 'c', default_options: ['c_std=c11']) + +# lib/subprojects/tskit is a symlink to the tskit C library in the +# git-submodules/tskit submodule at the root of the repository. +tskit_proj = subproject('tskit') +tskit_dep = tskit_proj.get_variable('tskit_dep') + +cc = meson.get_compiler('c') +m_dep = cc.find_library('m', required : false) +cunit_dep = dependency('cunit') + +extra_c_args = [ + '-Wall', '-Wextra', '-Werror', '-Wpedantic', '-W', + '-Wmissing-prototypes', '-Wstrict-prototypes', + '-Wconversion', '-Wshadow', '-Wpointer-arith', '-Wcast-align', + '-Wcast-qual', '-Wwrite-strings', '-Wnested-externs', + '-fshort-enums', '-fno-common'] + +tsls_sources = ['matcher.c', 'err.c', 'tsi_blkalloc.c'] + +tsls_lib = static_library('tsls', + sources: tsls_sources, dependencies: [m_dep, tskit_dep], + c_args: extra_c_args) + + +unit_tests = executable('tests', + sources: ['tests/tests.c'], + link_with: [tsls_lib], dependencies:[cunit_dep, tskit_dep]) +test('Unit tests', unit_tests) diff --git a/tsls/lib/subprojects/tskit b/tsls/lib/subprojects/tskit new file mode 120000 index 0000000..5763827 --- /dev/null +++ b/tsls/lib/subprojects/tskit @@ -0,0 +1 @@ +../../../git-submodules/tskit/c \ No newline at end of file diff --git a/tsls/lib/tests/tests.c b/tsls/lib/tests/tests.c new file mode 100644 index 0000000..20f6168 --- /dev/null +++ b/tsls/lib/tests/tests.c @@ -0,0 +1,317 @@ +/* Lean C tests. The algorithms are tested in detail against a dense + * implementation in the Python tests. */ + +#include "tsls.h" + +#include +#include +#include +#include + +#include + +static FILE *_devnull; + +/* A forest over [0, 10): roots 3 (above 0 and 1) and 4 (above 2) on [0, 5), + * joined by node 6 on [5, 10). Node 5 is isolated. */ +static void +build_forest(tsk_table_collection_t *tables) +{ + int ret; + int j; + double time[] = { 0, 0, 0, 1, 2, 4, 3 }; + + ret = tsk_table_collection_init(tables, 0); + CU_ASSERT_EQUAL_FATAL(ret, 0); + tables->sequence_length = 10; + for (j = 0; j < 7; j++) { + tsk_node_table_add_row(&tables->nodes, time[j] == 0 ? TSK_NODE_IS_SAMPLE : 0, + time[j], TSK_NULL, TSK_NULL, NULL, 0); + } + tsk_edge_table_add_row(&tables->edges, 0, 10, 3, 0, NULL, 0); + tsk_edge_table_add_row(&tables->edges, 0, 10, 3, 1, NULL, 0); + tsk_edge_table_add_row(&tables->edges, 0, 10, 4, 2, NULL, 0); + tsk_edge_table_add_row(&tables->edges, 5, 10, 6, 3, NULL, 0); + tsk_edge_table_add_row(&tables->edges, 5, 10, 6, 4, NULL, 0); + tsk_site_table_add_row(&tables->sites, 1, "A", 1, NULL, 0); + tsk_site_table_add_row(&tables->sites, 3, "C", 1, NULL, 0); + tsk_site_table_add_row(&tables->sites, 6, "G", 1, NULL, 0); + tsk_site_table_add_row(&tables->sites, 8, "A", 1, NULL, 0); + tsk_mutation_table_add_row( + &tables->mutations, 0, 3, TSK_NULL, TSK_UNKNOWN_TIME, "C", 1, NULL, 0); + tsk_mutation_table_add_row( + &tables->mutations, 0, 2, TSK_NULL, TSK_UNKNOWN_TIME, "G", 1, NULL, 0); + tsk_mutation_table_add_row( + &tables->mutations, 1, 4, TSK_NULL, TSK_UNKNOWN_TIME, "A", 1, NULL, 0); + tsk_mutation_table_add_row( + &tables->mutations, 2, 0, TSK_NULL, TSK_UNKNOWN_TIME, "A", 1, NULL, 0); + tsk_mutation_table_add_row( + &tables->mutations, 3, 6, TSK_NULL, TSK_UNKNOWN_TIME, "T", 1, NULL, 0); + tsk_mutation_table_add_row( + &tables->mutations, 3, 1, TSK_NULL, TSK_UNKNOWN_TIME, "A", 1, NULL, 0); + ret = tsk_table_collection_sort(tables, NULL, 0); + CU_ASSERT_EQUAL_FATAL(ret, 0); + ret = tsk_table_collection_build_index(tables, 0); + CU_ASSERT_EQUAL_FATAL(ret, 0); +} + +/* Alleles are A=0, C=1, G=2, T=3 */ +static tsk_size_t num_alleles[] = { 4, 4, 4, 4 }; +static allele_t ancestral_state[] = { 0, 1, 2, 0 }; +static allele_t derived_state[] = { 1, 2, 0, 0, 3, 0 }; + +static void +test_matcher_indexes_errors(void) +{ + int ret; + matcher_indexes_t mi; + tsk_table_collection_t tables; + allele_t bad_ancestral_state[] = { 0, 4, 2, 0 }; + allele_t bad_derived_state[] = { 1, 2, -1, 0, 3, 0 }; + + build_forest(&tables); + + ret = matcher_indexes_alloc(&mi, &tables, num_alleles, bad_ancestral_state, NULL, 0); + CU_ASSERT_EQUAL(ret, TSLS_ERR_BAD_ANCESTRAL_STATE); + matcher_indexes_free(&mi); + + ret = matcher_indexes_alloc(&mi, &tables, num_alleles, NULL, bad_derived_state, 0); + CU_ASSERT_EQUAL(ret, TSLS_ERR_BAD_MUTATION_DERIVED_STATE); + matcher_indexes_free(&mi); + + /* Default of 2 alleles */ + ret = matcher_indexes_alloc(&mi, &tables, NULL, ancestral_state, NULL, 0); + CU_ASSERT_EQUAL(ret, TSLS_ERR_BAD_ANCESTRAL_STATE); + matcher_indexes_free(&mi); + + tables.mutations.node[0] = 100; + ret = matcher_indexes_alloc(&mi, &tables, num_alleles, NULL, NULL, 0); + CU_ASSERT_EQUAL(ret, TSLS_ERR_BAD_MUTATION_NODE); + matcher_indexes_free(&mi); + tables.mutations.node[0] = 0; + + tables.mutations.site[0] = 100; + ret = matcher_indexes_alloc(&mi, &tables, num_alleles, NULL, NULL, 0); + CU_ASSERT_EQUAL(ret, TSLS_ERR_BAD_MUTATION_SITE); + matcher_indexes_free(&mi); + tables.mutations.site[0] = 0; + + tables.edges.child[0] = -1; + ret = matcher_indexes_alloc(&mi, &tables, num_alleles, NULL, NULL, 0); + CU_ASSERT_EQUAL(ret, TSLS_ERR_BAD_EDGE_NODE); + matcher_indexes_free(&mi); + tables.edges.child[0] = 0; + + tables.sites.position[0] = 1.5; + ret = matcher_indexes_alloc(&mi, &tables, num_alleles, NULL, NULL, 0); + CU_ASSERT_EQUAL(ret, TSLS_ERR_BAD_COORDINATE); + matcher_indexes_free(&mi); + + tsk_table_collection_free(&tables); +} + +static void +test_forest_matching(void) +{ + int ret; + tsk_table_collection_t tables; + matcher_indexes_t mi; + ancestor_matcher_t am; + compressed_matrix_t fm, bm; + double rho[] = { 0.01, 0.01, 0.01, 0.01 }; + double mu[] = { 0.01, 0.01, 0.01, 0.01 }; + /* The haplotype of node 1: C (under 3), C (ancestral), G, A (back mutation) */ + allele_t h[] = { 1, 1, 2, 0 }; + allele_t match[4]; + tsk_id_t left[4], right[4], parent[4]; + size_t path_length, j, k, l; + double total; + + build_forest(&tables); + ret = matcher_indexes_alloc(&mi, &tables, num_alleles, ancestral_state, derived_state, 0); + CU_ASSERT_EQUAL_FATAL(ret, 0); + matcher_indexes_print_state(&mi, _devnull); + ret = ancestor_matcher_alloc(&am, &mi, rho, mu, DBL_MIN, TSLS_EXTENDED_CHECKS); + CU_ASSERT_EQUAL_FATAL(ret, 0); + + ret = ancestor_matcher_find_path( + &am, 0, 4, h, match, &path_length, left, right, parent); + CU_ASSERT_EQUAL_FATAL(ret, 0); + CU_ASSERT_EQUAL(path_length, 1); + CU_ASSERT_EQUAL(parent[0], 1); + CU_ASSERT_EQUAL(left[0], 0); + CU_ASSERT_EQUAL(right[0], 10); + for (j = 0; j < 4; j++) { + CU_ASSERT_EQUAL(match[j], h[j]); + } + CU_ASSERT(ancestor_matcher_get_mean_traceback_size(&am) > 0); + CU_ASSERT(ancestor_matcher_get_total_memory(&am) > 0); + ancestor_matcher_print_state(&am, _devnull); + + ret = ancestor_matcher_find_path( + &am, 2, 1, h, match, &path_length, left, right, parent); + CU_ASSERT_EQUAL(ret, TSLS_ERR_BAD_SITE_RANGE); + + ret = compressed_matrix_alloc(&fm, 4, 1024); + CU_ASSERT_EQUAL_FATAL(ret, 0); + ret = compressed_matrix_alloc(&bm, 4, 1024); + CU_ASSERT_EQUAL_FATAL(ret, 0); + ret = ancestor_matcher_forward(&am, h, &fm); + CU_ASSERT_EQUAL_FATAL(ret, 0); + ret = ancestor_matcher_backward(&am, h, fm.normalisation_factor, &bm); + CU_ASSERT_EQUAL_FATAL(ret, 0); + CU_ASSERT(compressed_matrix_get_total_values(&fm) > 0); + + /* The Python tests check the values against a dense implementation; + * here we just check that the outputs are populated. */ + for (j = 0; j < 4; j++) { + CU_ASSERT(fm.normalisation_factor[j] > 0); + total = 0; + for (k = 0; k < fm.num_values[j]; k++) { + for (l = 0; l < bm.num_values[j]; l++) { + if (fm.nodes[j][k] == bm.nodes[j][l]) { + total += fm.values[j][k] * bm.values[j][l]; + } + } + } + CU_ASSERT(total > 0); + } + compressed_matrix_free(&fm); + compressed_matrix_free(&bm); + + ancestor_matcher_free(&am); + matcher_indexes_free(&mi); + tsk_table_collection_free(&tables); +} + +static void +test_bad_inputs(void) +{ + int ret; + tsk_table_collection_t tables; + matcher_indexes_t mi; + ancestor_matcher_t am; + compressed_matrix_t cm; + double rho[] = { 0.01, 0.01, 0.01, 0.01 }; + double mu[] = { 0, 0, 0, 0 }; + double norm[] = { 1, 1, 0, 1 }; + allele_t h[] = { 1, 1, 2, 0 }; + allele_t bad_h[] = { 1, 4, 2, 0 }; + allele_t impossible_h[] = { 3, 3, 3, 3 }; + allele_t match[4]; + tsk_id_t left[4], right[4], parent[4]; + size_t path_length; + + build_forest(&tables); + ret = matcher_indexes_alloc(&mi, &tables, num_alleles, ancestral_state, derived_state, 0); + CU_ASSERT_EQUAL_FATAL(ret, 0); + ret = ancestor_matcher_alloc(&am, &mi, rho, mu, DBL_MIN, 0); + CU_ASSERT_EQUAL_FATAL(ret, 0); + + ret = ancestor_matcher_find_path( + &am, 0, 4, bad_h, match, &path_length, left, right, parent); + CU_ASSERT_EQUAL(ret, TSLS_ERR_BAD_HAPLOTYPE_ALLELE); + ret = ancestor_matcher_find_path( + &am, 0, 4, impossible_h, match, &path_length, left, right, parent); + CU_ASSERT_EQUAL(ret, TSLS_ERR_MATCH_IMPOSSIBLE_EXTREME_MUTATION_PROBA); + + ret = compressed_matrix_alloc(&cm, 4, 1024); + CU_ASSERT_EQUAL_FATAL(ret, 0); + ret = ancestor_matcher_forward(&am, impossible_h, &cm); + CU_ASSERT_EQUAL(ret, TSLS_ERR_MATCH_IMPOSSIBLE); + ret = ancestor_matcher_backward(&am, h, norm, &cm); + CU_ASSERT_EQUAL(ret, TSLS_ERR_BAD_NORMALISATION_FACTOR); + compressed_matrix_free(&cm); + + ret = compressed_matrix_alloc(&cm, 3, 1024); + CU_ASSERT_EQUAL_FATAL(ret, 0); + ret = ancestor_matcher_forward(&am, h, &cm); + CU_ASSERT_EQUAL(ret, TSLS_ERR_BAD_SITE_RANGE); + compressed_matrix_free(&cm); + + ancestor_matcher_free(&am); + matcher_indexes_free(&mi); + tsk_table_collection_free(&tables); +} + +static void +test_strerror(void) +{ + int j; + const char *msg; + + for (j = 0; j < 100; j++) { + msg = tsls_strerror(-j); + CU_ASSERT_FATAL(msg != NULL); + CU_ASSERT(strlen(msg) > 0); + } +} + +static int +tsls_suite_init(void) +{ + _devnull = fopen("/dev/null", "w"); + if (_devnull == NULL) { + return CUE_SINIT_FAILED; + } + return CUE_SUCCESS; +} + +static int +tsls_suite_cleanup(void) +{ + if (_devnull != NULL) { + fclose(_devnull); + } + return CUE_SUCCESS; +} + +static void +handle_cunit_error(void) +{ + fprintf(stderr, "CUnit error occured: %d: %s\n", CU_get_error(), + CU_get_error_msg()); + exit(EXIT_FAILURE); +} + +int +main(int argc, char **argv) +{ + int ret; + CU_pTest test; + CU_pSuite suite; + CU_TestInfo tests[] = { + { "test_matcher_indexes_errors", test_matcher_indexes_errors }, + { "test_forest_matching", test_forest_matching }, + { "test_bad_inputs", test_bad_inputs }, + { "test_strerror", test_strerror }, + { NULL, NULL }, + }; + + if (CUE_SUCCESS != CU_initialize_registry()) { + handle_cunit_error(); + } + suite = CU_add_suite("tsls", tsls_suite_init, tsls_suite_cleanup); + if (NULL == suite) { + handle_cunit_error(); + } + for (ret = 0; tests[ret].pName != NULL; ret++) { + if (NULL == CU_add_test(suite, tests[ret].pName, tests[ret].pTestFunc)) { + handle_cunit_error(); + } + } + CU_basic_set_mode(CU_BRM_VERBOSE); + if (argc == 1) { + CU_basic_run_tests(); + } else { + test = CU_get_test_by_name(argv[1], suite); + if (test == NULL) { + printf("Test '%s' not found\n", argv[1]); + } else { + CU_basic_run_test(suite, test); + } + } + ret = CU_get_number_of_tests_failed() != 0; + CU_cleanup_registry(); + return ret; +} diff --git a/tsls/lib/tsi_blkalloc.c b/tsls/lib/tsi_blkalloc.c new file mode 100644 index 0000000..8f9b7f2 --- /dev/null +++ b/tsls/lib/tsi_blkalloc.c @@ -0,0 +1,112 @@ +/* + * This implementation is largely adapted from tskit's tsk_blkalloc + * (MIT License). It is a near-copy with minor adjustments for use in + * tsls (e.g., tracking total_size and naming). See + * lib/subprojects/tskit for the original sources and licensing. + */ +#include "tsls.h" +#include +#include + +void +tsi_blkalloc_print_state(tsi_blkalloc_t *self, FILE *out) +{ + fprintf(out, "Block allocator%p::\n", (void *) self); + fprintf(out, "\ttop = %lld\n", (long long) self->top); + fprintf(out, "\tchunk_size = %lld\n", (long long) self->chunk_size); + fprintf(out, "\tnum_chunks = %lld\n", (long long) self->num_chunks); + fprintf(out, "\ttotal_allocated = %lld\n", (long long) self->total_allocated); + fprintf(out, "\ttotal_size = %lld\n", (long long) self->total_size); +} + +int WARN_UNUSED +tsi_blkalloc_reset(tsi_blkalloc_t *self) +{ + int ret = 0; + + self->top = 0; + self->current_chunk = 0; + self->total_allocated = 0; + return ret; +} + +int WARN_UNUSED +tsi_blkalloc_init(tsi_blkalloc_t *self, size_t chunk_size) +{ + int ret = 0; + + tsk_memset(self, 0, sizeof(tsi_blkalloc_t)); + if (chunk_size < 1) { + ret = TSK_ERR_BAD_PARAM_VALUE; + goto out; + } + self->chunk_size = chunk_size; + self->top = 0; + self->current_chunk = 0; + self->total_allocated = 0; + self->total_size = 0; + self->num_chunks = 0; + self->mem_chunks = malloc(sizeof(char *)); + if (self->mem_chunks == NULL) { + ret = TSK_ERR_NO_MEMORY; + goto out; + } + self->mem_chunks[0] = malloc(chunk_size); + if (self->mem_chunks[0] == NULL) { + ret = TSK_ERR_NO_MEMORY; + goto out; + } + self->num_chunks = 1; + self->total_size = chunk_size + sizeof(void *); +out: + return ret; +} + +void *WARN_UNUSED +tsi_blkalloc_get(tsi_blkalloc_t *self, size_t size) +{ + void *ret = NULL; + void *p; + + if (size > self->chunk_size) { + goto out; + } + if ((self->top + size) > self->chunk_size) { + if (self->current_chunk == (self->num_chunks - 1)) { + p = realloc(self->mem_chunks, (self->num_chunks + 1) * sizeof(void *)); + if (p == NULL) { + goto out; + } + self->mem_chunks = p; + p = malloc(self->chunk_size); + if (p == NULL) { + goto out; + } + self->mem_chunks[self->num_chunks] = p; + self->num_chunks++; + self->total_size += self->chunk_size + sizeof(void *); + } + self->current_chunk++; + self->top = 0; + } + ret = self->mem_chunks[self->current_chunk] + self->top; + self->top += size; + self->total_allocated += size; +out: + return ret; +} + +void +tsi_blkalloc_free(tsi_blkalloc_t *self) +{ + size_t j; + + for (j = 0; j < self->num_chunks; j++) { + if (self->mem_chunks[j] != NULL) { + free(self->mem_chunks[j]); + } + } + if (self->mem_chunks != NULL) { + free(self->mem_chunks); + } +} diff --git a/tsls/lib/tsls.h b/tsls/lib/tsls.h new file mode 100644 index 0000000..acd666d --- /dev/null +++ b/tsls/lib/tsls.h @@ -0,0 +1,185 @@ +#ifndef __TSLS_H__ +#define __TSLS_H__ + +#include +#include +#include +#include + +/* No atomics on Windows for now */ +#if defined(_WIN32) +#define TSI_NO_ATOMICS 1 +#endif + +#if !defined(TSI_NO_ATOMICS) +#include +#define TSI_ATOMIC(T) _Atomic T +#else +#define TSI_ATOMIC(T) T +#endif + +#include "tskit.h" +#include "err.h" + +#define TSLS_MISSING_DATA (-1) + +/* Special values in the likelihood array. A node with NULL_LIKELIHOOD + * inherits the value of its nearest ancestor with a likelihood, and a node + * that is NOT_IN_TREE is not in the current tree (it has no parent and no + * children), and so is not a state of the HMM. */ +#define NULL_LIKELIHOOD (-1) +#define NOT_IN_TREE (-2) + +#define NULL_NODE (-1) +#define CACHE_UNSET (-1) + +/* Matcher flags */ +#define TSLS_EXTENDED_CHECKS (1 << 0) +#define TSLS_DISABLE_WEIGHT_BY_N (1 << 1) + +typedef int8_t allele_t; +typedef tsk_id_t coordinate_t; + +/* Allow the CPython accessor AncestorMatcher.total_memory to read the + * current allocator usage accurately while other threads may be allocating. + * Only total_size is atomic so it can be observed safely from the getter + * without data races. */ +typedef struct { + size_t chunk_size; /* number of bytes per chunk */ + size_t top; /* the offset of the next available byte in the current chunk */ + size_t current_chunk; /* the index of the chunk currently being used */ + TSI_ATOMIC(size_t) total_size; /* the total number of bytes allocated + overhead. */ + size_t total_allocated; /* the total number of bytes allocated. */ + size_t num_chunks; /* the number of memory chunks. */ + char **mem_chunks; /* the memory chunks */ +} tsi_blkalloc_t; + +extern void tsi_blkalloc_print_state(tsi_blkalloc_t *self, FILE *out); +extern int tsi_blkalloc_reset(tsi_blkalloc_t *self); +extern int tsi_blkalloc_init(tsi_blkalloc_t *self, size_t chunk_size); +extern void *tsi_blkalloc_get(tsi_blkalloc_t *self, size_t size); +extern void tsi_blkalloc_free(tsi_blkalloc_t *self); + +typedef struct { + coordinate_t left; + coordinate_t right; + tsk_id_t parent; + tsk_id_t child; +} edge_t; + +typedef struct _mutation_list_node_t { + tsk_id_t node; + allele_t derived_state; + struct _mutation_list_node_t *next; +} mutation_list_node_t; + +typedef struct { + int32_t size; + tsk_id_t *node; + int8_t *recombination_required; +} node_state_list_t; + +/* The read-only information about a tree sequence needed for matching, + * which can be shared between matchers in different threads. */ +typedef struct { + tsk_flags_t flags; + size_t num_sites; + size_t num_nodes; + size_t num_mutations; + size_t num_edges; + struct { + coordinate_t *position; + mutation_list_node_t **mutations; + tsk_size_t *num_alleles; + allele_t *ancestral_state; + } sites; + edge_t *left_index_edges; + edge_t *right_index_edges; + tsi_blkalloc_t allocator; +} matcher_indexes_t; + +/* The compressed output of the forward and backward algorithms. At each + * site we store a list of (node, value) pairs: the value for a node in the + * tree is the value of its nearest ancestor (including itself) in this list. + */ +typedef struct { + size_t num_sites; + double *normalisation_factor; + tsk_size_t *num_values; + tsk_id_t **nodes; + double **values; + tsi_blkalloc_t allocator; +} compressed_matrix_t; + +typedef struct { + int flags; + const matcher_indexes_t *matcher_indexes; + size_t num_nodes; + size_t num_sites; + /* Input LS model rates */ + double likelihood_threshold; + double *recombination_rate; + double *mismatch_rate; + /* The quintuply linked tree */ + tsk_id_t *parent; + tsk_id_t *left_child; + tsk_id_t *right_child; + tsk_id_t *left_sib; + tsk_id_t *right_sib; + /* The number of nodes in the subtree below each node, including itself. + * Only maintained for the forward and backward algorithms. */ + int64_t *num_below; + bool track_num_below; + /* The number of nodes in the tree that inherit each likelihood value. */ + int64_t *num_inheriting; + double *likelihood; + double *likelihood_cache; + allele_t *allelic_state; + int num_likelihood_nodes; + /* At each site, record a node with the maximum likelihood. */ + tsk_id_t *max_likelihood_node; + /* Used during traceback to map nodes where recombination is required. */ + int8_t *recombination_required; + tsk_id_t *likelihood_nodes_tmp; + tsk_id_t *likelihood_nodes; + node_state_list_t *traceback; + tsi_blkalloc_t traceback_allocator; + size_t total_traceback_size; + size_t traceback_block_size; + size_t traceback_realloc_size; +} ancestor_matcher_t; + +int matcher_indexes_alloc(matcher_indexes_t *self, const tsk_table_collection_t *tables, + const tsk_size_t *num_alleles, const allele_t *ancestral_state, + const allele_t *derived_state, tsk_flags_t options); +int matcher_indexes_print_state(const matcher_indexes_t *self, FILE *out); +int matcher_indexes_free(matcher_indexes_t *self); + +int compressed_matrix_alloc( + compressed_matrix_t *self, size_t num_sites, size_t block_size); +int compressed_matrix_free(compressed_matrix_t *self); +size_t compressed_matrix_get_total_values(const compressed_matrix_t *self); + +int ancestor_matcher_alloc(ancestor_matcher_t *self, + const matcher_indexes_t *matcher_indexes, const double *recombination_rate, + const double *mismatch_rate, double likelihood_threshold, int flags); +int ancestor_matcher_free(ancestor_matcher_t *self); +int ancestor_matcher_find_path(ancestor_matcher_t *self, tsk_id_t start, tsk_id_t end, + const allele_t *haplotype, allele_t *matched_haplotype, size_t *path_length, + tsk_id_t *path_left, tsk_id_t *path_right, tsk_id_t *path_parent); +int ancestor_matcher_forward(ancestor_matcher_t *self, const allele_t *haplotype, + compressed_matrix_t *output); +int ancestor_matcher_backward(ancestor_matcher_t *self, const allele_t *haplotype, + const double *forward_normalisation_factor, compressed_matrix_t *output); +int ancestor_matcher_print_state(ancestor_matcher_t *self, FILE *out); +double ancestor_matcher_get_mean_traceback_size(ancestor_matcher_t *self); +size_t ancestor_matcher_get_total_memory(ancestor_matcher_t *self); + +#define tsi_safe_free(pointer) \ + do { \ + if (pointer != NULL) { \ + free(pointer); \ + } \ + } while (0) + +#endif /* __TSLS_H__ */ diff --git a/tsls/lwt_interface b/tsls/lwt_interface new file mode 120000 index 0000000..f3acb6e --- /dev/null +++ b/tsls/lwt_interface @@ -0,0 +1 @@ +../git-submodules/tskit/python/lwt_interface \ No newline at end of file diff --git a/tsls/pyproject.toml b/tsls/pyproject.toml new file mode 100644 index 0000000..6baf402 --- /dev/null +++ b/tsls/pyproject.toml @@ -0,0 +1,71 @@ +[build-system] +requires = [ + "setuptools>=42", + "wheel", + "numpy>=2" +] +build-backend = "setuptools.build_meta" + +[project] +name = "tsls" +version = "0.0.1.dev0" +authors = [ + {name = "Tskit Developers", email = "admin@tskit.dev"}, +] +description = "The Li and Stephens HMM on a tree sequence." +requires-python = ">=3.11" +license = "MIT" +classifiers = [ + "Programming Language :: C", + "Programming Language :: Python", + "Programming Language :: Python :: 3", + "Programming Language :: Python :: 3 :: Only", + "Development Status :: 2 - Pre-Alpha", + "Intended Audience :: Science/Research", + "Topic :: Scientific/Engineering :: Bio-Informatics", +] +dependencies = [ + "numpy>=2", + "tskit>=1.0.0", +] + +[project.urls] +"Source Code" = "https://github.com/tskit-dev/tsls/" + +[dependency-groups] +test = [ + "pytest", +] + +[tool.setuptools] +packages = ["tsls"] +include-package-data = true + +[tool.pytest.ini_options] +testpaths = ["tests"] +pythonpath = ["tests"] + +[tool.ruff] +target-version = "py311" +line-length = 89 +indent-width = 4 + +[tool.ruff.lint] +select = ["E", "F", "B", "W", "I", "N", "UP", "A", "PT", "PLC0415"] + +ignore = [ + "A001", "A002", "RUF", + "B905", #Don't add strict=False to zips (B905) + "N806", "N802", "N803", # Various nags about uppercase vars + "N818", # Exceptions not called "Error" + "UP032", # Don't upgrade to f strings + "B904", # raise from-issues + "PT011", # pytest.raises(ValueError) too broad + "PT012", # pytest.raises() block should contain a single simple statement + "B028", # No stacklevel for warnings + "B017", # Do not assert blind exception: `BaseException` +] + +[tool.ruff.lint.isort] +known-first-party = ["_tsls", "dense", "tsls"] +known-third-party = ["numpy", "pytest", "tskit"] diff --git a/tsls/setup.py b/tsls/setup.py new file mode 100644 index 0000000..29ae621 --- /dev/null +++ b/tsls/setup.py @@ -0,0 +1,47 @@ +import os +import platform + +import numpy +from setuptools import Extension, setup + +IS_WINDOWS = platform.system() == "Windows" + +libdir = "lib" +tskroot = os.path.join("..", "git-submodules", "tskit", "c") +tskdir = os.path.join(tskroot, "tskit") +kasdir = os.path.join(tskroot, "subprojects", "kastore") +includes = ["lwt_interface", libdir, tskroot, tskdir, kasdir] + +tsls_source_files = [ + "matcher.c", + "err.c", + "tsi_blkalloc.c", +] +# The matcher only needs the tskit core and the table code used by the +# LightweightTableCollection interface. +tsk_source_files = ["core.c", "tables.c"] +kas_source_files = ["kastore.c"] + +sources = ( + ["_tslsmodule.c"] + + [os.path.join(libdir, f) for f in tsls_source_files] + + [os.path.join(tskdir, f) for f in tsk_source_files] + + [os.path.join(kasdir, f) for f in kas_source_files] +) + +libraries = ["Advapi32"] if IS_WINDOWS else [] + +extra_compile_args = [] if IS_WINDOWS else ["-std=c11"] + +_tsls_module = Extension( + "_tsls", + sources=sources, + extra_compile_args=extra_compile_args, + libraries=libraries, + undef_macros=["NDEBUG"], + include_dirs=includes + [numpy.get_include()], +) + +setup( + ext_modules=[_tsls_module], +) diff --git a/tsls/tests/dense.py b/tsls/tests/dense.py new file mode 100644 index 0000000..82a66d3 --- /dev/null +++ b/tsls/tests/dense.py @@ -0,0 +1,129 @@ +""" +A dense implementation of the Li and Stephens HMM over all the nodes of a +tree sequence, used as a reference for the tree-based engine. + +The states at a site are the nodes in the tree at that site (nodes with a +parent or at least one child). Nodes entering the tree have probability +zero, and so can only be reached by recombination. The probability of +leaving a node that leaves the tree is only the probability of +recombination. +""" + +import numpy as np + + +def node_states(ts, alleles=None): + """ + Return the (A, present) tuple of (num_sites, num_nodes) arrays, where + A[j, u] is the allele index of node u at site j and present[j, u] is + True if node u is in the tree at site j. If alleles is None, the ancestral + state is 0 and every derived state is 1. + """ + A = np.zeros((ts.num_sites, ts.num_nodes), dtype=np.int8) + present = np.zeros((ts.num_sites, ts.num_nodes), dtype=bool) + + def encode(state, default): + return default if alleles is None else alleles.index(state) + + for tree in ts.trees(): + in_tree = [ + tree.parent(u) != -1 or tree.num_children(u) > 0 for u in range(ts.num_nodes) + ] + for site in tree.sites(): + state = {} + for mutation in site.mutations: + state[mutation.node] = encode(mutation.derived_state, 1) + ancestral = encode(site.ancestral_state, 0) + for u in range(ts.num_nodes): + v = u + while v != -1 and v not in state: + v = tree.parent(v) + A[site.id, u] = ancestral if v == -1 else state[v] + present[site.id, u] = in_tree[u] + return A, present + + +def emission(A_site, h_site, mu, num_alleles): + match = (A_site == h_site) | (h_site == -1) + return np.where(match, 1 - (num_alleles - 1) * mu, mu) + + +def forward(A, present, h, rho, mu, num_alleles, n, threshold=0): + """ + Return the (F, c) tuple of normalised forward probabilities and + normalisation factors. + """ + m, N = A.shape + F = np.zeros((m, N)) + c = np.zeros(m) + f = present[0].astype(float) + for j in range(m): + # Recombination redistributes the total over all states at the + # previous site, including nodes that are not in the tree at this + # site. + total = np.sum(f) + f = np.where(present[j], f, 0) + f = (f * (1 - rho[j]) + rho[j] / n * total) * emission( + A[j], h[j], mu[j], num_alleles + ) + f = np.where(present[j], f, 0) + c[j] = np.sum(f) + f = np.where(present[j], np.maximum(f / c[j], threshold), 0) + F[j] = f + return F, c + + +def backward(A, present, h, rho, mu, num_alleles, n, c, threshold=0): + """ + Return the backward probabilities, scaled by the forward normalisation + factors c. + """ + m, N = A.shape + B = np.zeros((m, N)) + b = present[m - 1].astype(float) + total = 0 + for j in range(m - 1, -1, -1): + if j < m - 1: + b = np.where(present[j], b, 0) + b = ((1 - rho[j + 1]) * b + rho[j + 1] * total / n) / c[j + 1] + b = np.where(present[j], np.maximum(b, threshold), 0) + B[j] = b + b = np.where(present[j], b * emission(A[j], h[j], mu[j], num_alleles), 0) + total = np.sum(b) + return B + + +def viterbi_log_likelihood(A, present, h, rho, mu, num_alleles, n): + """ + Return the log likelihood of the most likely path, where staying on a + node has probability (1 - rho + rho / n) and switching to a given node + has probability rho / n. + """ + m, N = A.shape + v = present[0].astype(float) + log_likelihood = 0 + for j in range(m): + v = np.where(present[j], v, 0) + v = np.maximum(v * (1 - rho[j] + rho[j] / n), rho[j] / n) + v = np.where(present[j], v * emission(A[j], h[j], mu[j], num_alleles), 0) + max_v = np.max(v) + log_likelihood += np.log(max_v) + v = v / max_v + return log_likelihood + + +def path_log_likelihood(A, present, h, rho, mu, num_alleles, n, path): + """ + Return the log likelihood of the specified path (the node copied at + each site) under the same model as viterbi_log_likelihood. + """ + log_likelihood = 0 + for j, u in enumerate(path): + assert present[j, u] + if j == 0 or path[j - 1] == u: + p_t = 1 - rho[j] + rho[j] / n + else: + p_t = rho[j] / n + e = emission(A[j, u], h[j], mu[j], num_alleles) + log_likelihood += np.log(p_t * e) + return log_likelihood diff --git a/tsls/tests/test_matching.py b/tsls/tests/test_matching.py new file mode 100644 index 0000000..05c6fec --- /dev/null +++ b/tsls/tests/test_matching.py @@ -0,0 +1,493 @@ +""" +Tests for the tsls HMM engine on small hand-built examples, compared with +the dense implementation in dense.py. +""" + +import numpy as np +import pytest +import tskit + +import dense +import tsls + +ACGT = ["A", "C", "G", "T"] + + +def finalise(tables): + tables.sort() + tables.build_index() + tables.compute_mutation_parents() + return tables.tree_sequence() + + +def add_mutations(tables, mutations): + """ + Add sites and mutations from a list of (position, ancestral_state, + [(node, derived_state), ...]) tuples, with mutations on the same node + listed oldest first. + """ + for position, ancestral_state, site_mutations in mutations: + site = tables.sites.add_row(position=position, ancestral_state=ancestral_state) + for node, derived_state in site_mutations: + tables.mutations.add_row(site=site, node=node, derived_state=derived_state) + + +def single_tree_ts(): + # 2.00┊ 6 ┊ + # ┊ ┏━┻━┓ ┊ + # 1.00┊ 4 5 ┊ + # ┊ ┏┻┓ ┏┻┓ ┊ + # 0.00┊ 0 1 2 3 ┊ + # 0 8 + tables = tskit.Tree.generate_balanced(4, span=8).tree_sequence.dump_tables() + add_mutations( + tables, + [ + # Recurrent mutations and a non-zero ancestral state + (1, "G", [(0, "T"), (3, "T")]), + # A back mutation + (3, "A", [(4, "C"), (1, "A")]), + # Three different derived states + (5, "T", [(0, "A"), (2, "C"), (3, "G")]), + # Stacked mutations on the same node, and one on the root + (7, "C", [(6, "G"), (2, "A"), (2, "T")]), + ], + ) + return finalise(tables) + + +def two_tree_ts(): + # Node 4 is only in the left tree and node 7 only in the right tree. + # + # 3.00┊ 6 ┊ 6 ┊ + # ┊ ┏━┻┓ ┊ ┏┻━┓ ┊ + # 2.00┊ 5 ┃ ┊ 5 ┃ ┊ + # ┊ ┏┻┓ ┃ ┊ ┏━┻┓ ┃ ┊ + # 1.50┊ ┃ ┃ ┃ ┊ ┃ 7 ┃ ┊ + # ┊ ┃ ┃ ┃ ┊ ┃ ┏┻┓┃ ┊ + # 1.00┊ 4 ┃ ┃ ┊ ┃ ┃ ┃┃ ┊ + # ┊ ┏┻┓┃ ┃ ┊ ┃ ┃ ┃┃ ┊ + # 0.00┊ 0 1 2 3 ┊ 0 1 2 3 ┊ + # 0 10 20 + tables = tskit.TableCollection(sequence_length=20) + for t in [0, 0, 0, 0, 1, 2, 3, 1.5]: + tables.nodes.add_row(flags=tskit.NODE_IS_SAMPLE if t == 0 else 0, time=t) + for left, right, parent, child in [ + (0, 10, 4, 0), + (0, 10, 4, 1), + (0, 10, 5, 4), + (0, 10, 5, 2), + (0, 20, 6, 5), + (0, 20, 6, 3), + (10, 20, 7, 1), + (10, 20, 7, 2), + (10, 20, 5, 0), + (10, 20, 5, 7), + ]: + tables.edges.add_row(left, right, parent, child) + add_mutations( + tables, + [ + (2, "A", [(4, "C")]), + (5, "A", [(0, "G"), (2, "T")]), + (8, "A", [(5, "C"), (1, "A")]), + (12, "T", [(7, "G")]), + (15, "C", [(0, "T"), (3, "T")]), + (18, "G", [(6, "C")]), + ], + ) + return finalise(tables) + + +def forest_ts(): + # Two roots on [0, 5), joined by node 6 on [5, 10). Node 4 is a unary + # root on [0, 5), node 5 is isolated, and node 6 is only in the right + # tree. + # + # 3.00┊ ┊ 6 ┊ + # ┊ ┊ ┏━┻━┓ ┊ + # 2.00┊ 4 ┊ ┃ 4 ┊ + # ┊ ┃ ┊ ┃ ┃ ┊ + # 1.00┊ 3 ┃ ┊ 3 ┃ ┊ + # ┊ ┏┻┓ ┃ ┊ ┏┻┓ ┃ ┊ + # 0.00┊ 0 1 2 ┊ 0 1 2 ┊ + # 0 5 10 + tables = tskit.TableCollection(sequence_length=10) + for t in [0, 0, 0, 1, 2, 4, 3]: + tables.nodes.add_row(flags=tskit.NODE_IS_SAMPLE if t == 0 else 0, time=t) + for left, right, parent, child in [ + (0, 10, 3, 0), + (0, 10, 3, 1), + (0, 10, 4, 2), + (5, 10, 6, 3), + (5, 10, 6, 4), + ]: + tables.edges.add_row(left, right, parent, child) + add_mutations( + tables, + [ + (1, "A", [(3, "C"), (2, "G")]), + (3, "C", [(4, "A")]), + (4, "T", []), + (6, "G", [(0, "A"), (4, "T")]), + (8, "A", [(6, "T"), (1, "A")]), + ], + ) + return finalise(tables) + + +def isolated_root_ts(): + # The sc2ts convention: node 0 is an isolated vestigial root above the + # root node 1 (in tsinfer, node 0 is joined to node 1 by an edge, which + # is removed before matching). Nodes 1, 2 and 4 are internal samples. + # + # 3.00┊ 1 ┊ 1 ┊ + # ┊ ┏━┻━┓ ┊ ┏━╋━┓ ┊ + # 2.00┊ 2 ┃ ┊ 2 ┃ ┃ ┊ + # ┊ ┏┻┓ ┃ ┊ ┃ ┃ ┃ ┊ + # 1.00┊ 3 4 ┃ ┊ 3 4 ┃ ┊ + # ┊ ┃ ┃ ┊ ┃ ┃ ┊ + # 0.00┊ 5 6 ┊ 5 6 ┊ + # 0 6 12 + tables = tskit.TableCollection(sequence_length=12) + flags = tskit.NODE_IS_SAMPLE + for f, t in [(0, 4), (flags, 3), (flags, 2), (flags, 1), (flags, 1)]: + tables.nodes.add_row(flags=f, time=t) + tables.nodes.add_row(flags=flags, time=0) + tables.nodes.add_row(flags=flags, time=0) + for left, right, parent, child in [ + (0, 12, 1, 2), + (0, 12, 1, 6), + (0, 12, 2, 3), + (0, 6, 2, 4), + (6, 12, 1, 4), + (0, 12, 4, 5), + ]: + tables.edges.add_row(left, right, parent, child) + add_mutations( + tables, + [ + (1, "A", [(2, "C")]), + (3, "G", [(4, "T"), (6, "T")]), + (5, "C", [(3, "A")]), + (7, "T", [(5, "G")]), + (9, "A", [(4, "-")]), + (11, "C", [(1, "G"), (6, "C")]), + ], + ) + return finalise(tables) + + +EXAMPLES = { + "single_tree": single_tree_ts, + "two_trees": two_tree_ts, + "forest": forest_ts, + "isolated_root": isolated_root_ts, +} +ALLELES = ACGT + ["-"] +PARAMS = [(1e-2, 1e-2), (0.2, 0.05), (1e-4, 1e-3)] + + +def queries(ts, A, present): + """ + Return a list of query haplotypes: the haplotypes of all nodes that are + in every tree, plus some random haplotypes, with and without missing + data. + """ + rng = np.random.default_rng(42) + H = [A[:, u] for u in range(ts.num_nodes) if np.all(present[:, u])] + num_alleles = len(ALLELES) + for _ in range(3): + H.append(rng.integers(0, num_alleles, size=ts.num_sites).astype(np.int8)) + for h in list(H[:3]): + h = h.copy() + h[rng.integers(0, ts.num_sites)] = -1 + H.append(h) + h = H[0].copy() + h[0] = -1 + h[-1] = -1 + H.append(h) + return [np.asarray(h, dtype=np.int8) for h in H] + + +def make_matcher(ts, rho, mu, weight_by_n, likelihood_threshold=None): + m = ts.num_sites + mi = tsls.MatcherIndexes(ts, alleles=ALLELES) + am = tsls.AncestorMatcher( + mi, + np.full(m, rho), + np.full(m, mu), + weight_by_n=weight_by_n, + likelihood_threshold=likelihood_threshold, + extended_checks=True, + ) + n = ts.num_nodes if weight_by_n else 1 + return am, n + + +def path_by_site(ts, left, right, parent): + """ + Return the node copied at each site, from a path in any order. + """ + index = np.argsort(left) + left, right, parent = left[index], right[index], parent[index] + assert left[0] == 0 + assert right[-1] == ts.sequence_length + np.testing.assert_array_equal(left[1:], right[:-1]) + segment = np.searchsorted(left, ts.sites_position, side="right") - 1 + return parent[segment] + + +@pytest.fixture(scope="module", params=list(EXAMPLES)) +def example(request): + ts = EXAMPLES[request.param]() + A, present = dense.node_states(ts, ALLELES) + return ts, A, present + + +class TestExamples: + def test_node_states(self, example): + ts, A, present = example + G = ts.genotype_matrix(isolated_as_missing=False, alleles=tuple(ALLELES)) + np.testing.assert_array_equal(A[:, ts.samples()], G) + assert np.any(np.bincount(ts.mutations_site) > 1) + + +@pytest.mark.parametrize("weight_by_n", [True, False]) +@pytest.mark.parametrize(("rho", "mu"), PARAMS) +class TestAgainstDense: + def test_viterbi(self, example, weight_by_n, rho, mu): + ts, A, present = example + am, n = make_matcher(ts, rho, mu, weight_by_n) + m = ts.num_sites + rho_a = np.full(m, rho) + mu_a = np.full(m, mu) + for h in queries(ts, A, present): + match = np.zeros(m, dtype=np.int8) + left, right, parent = am.find_path(h, 0, m, match) + path = path_by_site(ts, left, right, parent) + np.testing.assert_array_equal(match, A[np.arange(m), path]) + assert np.all(present[np.arange(m), path]) + value = dense.path_log_likelihood( + A, present, h, rho_a, mu_a, len(ALLELES), n, path + ) + optimum = dense.viterbi_log_likelihood( + A, present, h, rho_a, mu_a, len(ALLELES), n + ) + assert value == pytest.approx(optimum, rel=1e-9) + + def test_forward_backward(self, example, weight_by_n, rho, mu): + ts, A, present = example + am, n = make_matcher(ts, rho, mu, weight_by_n) + m = ts.num_sites + rho_a = np.full(m, rho) + mu_a = np.full(m, mu) + for h in queries(ts, A, present): + fm = am.forward_matrix(h) + F, c = dense.forward(A, present, h, rho_a, mu_a, len(ALLELES), n) + np.testing.assert_allclose(fm.normalisation_factor, c, rtol=1e-12) + np.testing.assert_allclose(fm.decode(), F, rtol=1e-12, atol=1e-300) + assert fm.log_likelihood == pytest.approx(np.sum(np.log(c))) + + bm = am.backward_matrix(h, fm.normalisation_factor) + B = dense.backward(A, present, h, rho_a, mu_a, len(ALLELES), n, c) + np.testing.assert_allclose(bm.decode(), B, rtol=1e-12, atol=1e-300) + np.testing.assert_allclose(np.sum(F * B, axis=1), 1) + + def test_likelihood_threshold(self, example, weight_by_n, rho, mu): + ts, A, present = example + threshold = 1e-3 + am, n = make_matcher(ts, rho, mu, weight_by_n, likelihood_threshold=threshold) + m = ts.num_sites + rho_a = np.full(m, rho) + mu_a = np.full(m, mu) + for h in queries(ts, A, present): + fm = am.forward_matrix(h) + F, c = dense.forward( + A, present, h, rho_a, mu_a, len(ALLELES), n, threshold=threshold + ) + np.testing.assert_allclose(fm.decode(), F, rtol=1e-12) + bm = am.backward_matrix(h, fm.normalisation_factor) + B = dense.backward( + A, present, h, rho_a, mu_a, len(ALLELES), n, c, threshold=threshold + ) + np.testing.assert_allclose(bm.decode(), B, rtol=1e-12) + match = np.zeros(m, dtype=np.int8) + left, right, parent = am.find_path(h, 0, m, match) + path = path_by_site(ts, left, right, parent) + np.testing.assert_array_equal(match, A[np.arange(m), path]) + + +class TestViterbiPaths: + @pytest.mark.parametrize("name", list(EXAMPLES)) + def test_exact_copy(self, name): + # Each node that is in every tree and has a unique haplotype is + # copied exactly with no recombination. + ts = EXAMPLES[name]() + A, present = dense.node_states(ts, ALLELES) + am, _ = make_matcher(ts, 1e-4, 1e-3, True) + m = ts.num_sites + for u in range(ts.num_nodes): + h = A[:, u] + if not np.all(present[:, u]): + continue + num_same = np.sum(np.all(A == h[:, None], axis=0) & np.all(present, axis=0)) + if num_same > 1: + continue + match = np.zeros(m, dtype=np.int8) + left, right, parent = am.find_path(h, 0, m, match) + assert list(parent) == [u] + assert list(left) == [0] + assert list(right) == [ts.sequence_length] + np.testing.assert_array_equal(match, h) + + def test_two_tree_recombinant(self): + # Copy node 4 (only in the left tree) then node 7 (only in the right) + ts = two_tree_ts() + A, _ = dense.node_states(ts, ALLELES) + h = np.concatenate([A[:3, 4], A[3:, 7]]) + am, _ = make_matcher(ts, 1e-2, 1e-3, True) + match = np.zeros(ts.num_sites, dtype=np.int8) + left, right, parent = am.find_path(h, 0, ts.num_sites, match) + np.testing.assert_array_equal(match, h) + # The path is returned right-to-left + assert list(parent) == [7, 4] + assert list(right) == [20, 12] + assert list(left) == [12, 0] + + def test_isolated_node_never_copied(self): + ts = isolated_root_ts() + A, present = dense.node_states(ts, ALLELES) + assert not np.any(present[:, 0]) + am, _ = make_matcher(ts, 0.2, 0.05, True) + for h in queries(ts, A, present): + match = np.zeros(ts.num_sites, dtype=np.int8) + _, _, parent = am.find_path(h, 0, ts.num_sites, match) + assert 0 not in parent + + def test_start_end(self): + ts = two_tree_ts() + A, _ = dense.node_states(ts, ALLELES) + h = A[:, 0].copy() + h[0] = -1 + h[-1] = -1 + am, _ = make_matcher(ts, 1e-4, 1e-3, True) + match = np.zeros(ts.num_sites, dtype=np.int8) + left, right, parent = am.find_path(h, 1, ts.num_sites - 1, match) + assert list(parent) == [0] + assert list(left) == [ts.sites_position[1]] + assert list(right) == [ts.sites_position[-1]] + np.testing.assert_array_equal(match[1:-1], h[1:-1]) + + +class TestInterface: + def test_bad_alleles(self): + with pytest.raises(ValueError, match="Unknown"): + tsls.MatcherIndexes(single_tree_ts(), alleles=["A", "C"]) + + def test_default_binary_alleles(self): + tables = tskit.Tree.generate_balanced(2, span=4).tree_sequence.dump_tables() + add_mutations(tables, [(1, "0", [(0, "1")])]) + ts = finalise(tables) + mi = tsls.MatcherIndexes(ts) + am = tsls.AncestorMatcher(mi, np.full(1, 1e-3), np.full(1, 1e-3)) + match = np.zeros(1, dtype=np.int8) + _, _, parent = am.find_path(np.array([1], dtype=np.int8), 0, 1, match) + assert list(parent) == [0] + + @pytest.mark.parametrize("state", [5, -2]) + def test_bad_haplotype_allele(self, state): + ts = single_tree_ts() + am, _ = make_matcher(ts, 1e-2, 1e-2, True) + h = np.zeros(ts.num_sites, dtype=np.int8) + h[1] = state + match = np.zeros(ts.num_sites, dtype=np.int8) + with pytest.raises(tsls.LibraryError, match="Haplotype allele"): + am.find_path(h, 0, ts.num_sites, match) + with pytest.raises(tsls.LibraryError, match="Haplotype allele"): + am.forward_matrix(h) + + @pytest.mark.parametrize(("start", "end"), [(0, 0), (-1, 2), (2, 1), (0, 5)]) + def test_bad_site_range(self, start, end): + ts = single_tree_ts() + am, _ = make_matcher(ts, 1e-2, 1e-2, True) + h = np.zeros(ts.num_sites, dtype=np.int8) + match = np.zeros(ts.num_sites, dtype=np.int8) + with pytest.raises(tsls.LibraryError, match="site range"): + am.find_path(h, start, end, match) + + def test_bad_lengths(self): + ts = single_tree_ts() + mi = tsls.MatcherIndexes(ts, alleles=ALLELES) + with pytest.raises(ValueError, match="recombination"): + tsls.AncestorMatcher(mi, np.zeros(1), np.zeros(ts.num_sites)) + with pytest.raises(ValueError, match="mismatch"): + tsls.AncestorMatcher(mi, np.zeros(ts.num_sites), np.zeros(1)) + am, _ = make_matcher(ts, 1e-2, 1e-2, True) + with pytest.raises(ValueError): + am.forward_matrix(np.zeros(1, dtype=np.int8)) + h = np.zeros(ts.num_sites, dtype=np.int8) + with pytest.raises(ValueError, match="normalisation_factor"): + am.backward_matrix(h, np.ones(1)) + + def test_bad_normalisation_factor(self): + ts = single_tree_ts() + am, _ = make_matcher(ts, 1e-2, 1e-2, True) + h = np.zeros(ts.num_sites, dtype=np.int8) + with pytest.raises(tsls.LibraryError, match="normalisation"): + am.backward_matrix(h, np.zeros(ts.num_sites)) + + def test_match_impossible(self): + ts = single_tree_ts() + am, _ = make_matcher(ts, 0, 0, True) + h = np.full(ts.num_sites, 4, dtype=np.int8) + match = np.zeros(ts.num_sites, dtype=np.int8) + with pytest.raises(tsls.MatchImpossible): + am.find_path(h, 0, ts.num_sites, match) + with pytest.raises(tsls.MatchImpossible): + am.forward_matrix(h) + + def test_non_integer_coordinates(self): + tables = tskit.Tree.generate_balanced(2, span=4).tree_sequence.dump_tables() + tables.sites.add_row(position=1.5, ancestral_state="0") + with pytest.raises(tsls.LibraryError, match="integers"): + tsls.MatcherIndexes(finalise(tables)) + + def test_memory_stats(self): + ts = two_tree_ts() + am, _ = make_matcher(ts, 1e-2, 1e-2, True) + assert am.total_memory > 0 + h = np.zeros(ts.num_sites, dtype=np.int8) + match = np.zeros(ts.num_sites, dtype=np.int8) + am.find_path(h, 0, ts.num_sites, match) + assert am.mean_traceback_size > 0 + + def test_reuse_matcher(self): + # Running the algorithms repeatedly on the same matcher gives the + # same results. + ts = two_tree_ts() + A, present = dense.node_states(ts, ALLELES) + am, _ = make_matcher(ts, 0.2, 0.05, True) + h = queries(ts, A, present)[-1] + m = ts.num_sites + results = [] + for _ in range(2): + match = np.zeros(m, dtype=np.int8) + path = am.find_path(h, 0, m, match) + fm = am.forward_matrix(h) + bm = am.backward_matrix(h, fm.normalisation_factor) + results.append((path, match, fm.decode(), bm.decode())) + for x, y in zip(results[0][0], results[1][0]): + np.testing.assert_array_equal(x, y) + for x, y in zip(results[0][1:], results[1][1:]): + np.testing.assert_array_equal(x, y) + + def test_compressed_matrix(self): + ts = single_tree_ts() + am, _ = make_matcher(ts, 1e-2, 1e-2, True) + fm = am.forward_matrix(np.zeros(ts.num_sites, dtype=np.int8)) + assert fm.num_sites == ts.num_sites + nodes, values = fm.get_site(0) + assert len(nodes) == len(values) > 0 + D = fm.decode([0, 6]) + assert D.shape == (ts.num_sites, 2) diff --git a/tsls/tsls/__init__.py b/tsls/tsls/__init__.py new file mode 100644 index 0000000..fd9f665 --- /dev/null +++ b/tsls/tsls/__init__.py @@ -0,0 +1,12 @@ +""" +The Li and Stephens HMM on a tree sequence. +""" + +from _tsls import LibraryError, MatchImpossible # noqa: F401 + +from .matching import ( # noqa: F401 + AncestorMatcher, + CompressedMatrix, + MatcherIndexes, + encode_alleles, +) diff --git a/tsls/tsls/matching.py b/tsls/tsls/matching.py new file mode 100644 index 0000000..1fa0c26 --- /dev/null +++ b/tsls/tsls/matching.py @@ -0,0 +1,185 @@ +""" +The Li and Stephens HMM on a tree sequence. +""" + +import numpy as np + +import _tsls + + +def encode_alleles(ts, alleles): + """ + Return the (ancestral_state, derived_state) arrays for the specified tree + sequence, encoding each state as its index in the specified list of + alleles. Raise a ValueError if any state is not in the list. + """ + alleles = list(alleles) + if len(alleles) > np.iinfo(np.int8).max: + raise ValueError("Too many alleles") + allele_index = {allele: j for j, allele in enumerate(alleles)} + if len(allele_index) != len(alleles): + raise ValueError("Alleles must be unique") + + def encode(states, name): + unique_states, inverse = np.unique(np.asarray(states), return_inverse=True) + code = np.zeros(len(unique_states), dtype=np.int8) + for j, state in enumerate(unique_states): + if state not in allele_index: + raise ValueError(f"Unknown {name}: {state!r}") + code[j] = allele_index[state] + return code[inverse] + + ancestral_state = encode(ts.sites_ancestral_state, "ancestral state") + derived_state = encode(ts.mutations_derived_state, "derived state") + return ancestral_state, derived_state + + +class MatcherIndexes(_tsls.MatcherIndexes): + """ + The information about a tree sequence needed for matching, which can be + shared between matchers (including in different threads). + + The states of the HMM at a given position are the nodes in the tree at + that position: nodes with a parent or at least one child. Isolated + nodes cannot be copied. Coordinates must be integers. + + By default, the ancestral state of every site is 0 and every mutation has + derived state 1. If ``alleles`` is specified, states are instead encoded + as their index in this list, and ``num_alleles`` defaults to its length. + """ + + def __init__(self, ts, *, alleles=None, num_alleles=None): + ancestral_state = None + derived_state = None + if alleles is not None: + ancestral_state, derived_state = encode_alleles(ts, alleles) + if num_alleles is None: + num_alleles = np.full(ts.num_sites, len(alleles), dtype=np.uint64) + tables = ts.dump_tables() + ll_tables = _tsls.LightweightTableCollection(tables.sequence_length) + ll_tables.fromdict(tables.asdict()) + self.ts = ts + self.sequence_length = ts.sequence_length + self.num_sites = ts.num_sites + super().__init__( + ll_tables, + num_alleles=num_alleles, + ancestral_state=ancestral_state, + derived_state=derived_state, + ) + + +class CompressedMatrix: + """ + The output of the forward or backward algorithm. At each site we store a + list of (node, value) pairs, and the value for a node in the tree is the + value of its nearest ancestor (including itself) in this list. + """ + + def __init__(self, ts, normalisation_factor, offsets, nodes, values): + self.ts = ts + self.normalisation_factor = normalisation_factor + self.offsets = offsets + self.nodes = nodes + self.values = values + + @property + def num_sites(self): + return len(self.normalisation_factor) + + @property + def log_likelihood(self): + """ + The log likelihood of the haplotype (for the forward matrix). + """ + return float(np.sum(np.log(self.normalisation_factor))) + + def get_site(self, site): + """ + Return the (nodes, values) arrays stored for the specified site. + """ + start = self.offsets[site] + stop = self.offsets[site + 1] + return self.nodes[start:stop], self.values[start:stop] + + def decode(self, nodes=None): + """ + Return the dense (num_sites, len(nodes)) array of values for the + specified nodes (default: all nodes). Nodes that are not in the tree + at a site have value 0. This is intended for small examples. + """ + if nodes is None: + nodes = np.arange(self.ts.num_nodes) + nodes = np.asarray(nodes, dtype=np.int32) + D = np.zeros((self.num_sites, len(nodes))) + for tree in self.ts.trees(): + for site in tree.sites(): + site_nodes, site_values = self.get_site(site.id) + value = dict(zip(site_nodes.tolist(), site_values.tolist())) + for j, u in enumerate(nodes.tolist()): + if tree.parent(u) == -1 and tree.num_children(u) == 0: + continue + v = u + while v not in value: + v = tree.parent(v) + D[site.id, j] = value[v] + return D + + +class AncestorMatcher(_tsls.AncestorMatcher): + """ + Runs the Viterbi, forward and backward algorithms for haplotypes against + the tree sequence in the specified MatcherIndexes, using the specified + per-site recombination and mismatch probabilities. + """ + + def __init__( + self, + matcher_indexes, + recombination, + mismatch, + *, + likelihood_threshold=None, + weight_by_n=None, + extended_checks=False, + ): + kwargs = {"extended_checks": extended_checks} + if likelihood_threshold is not None: + kwargs["likelihood_threshold"] = likelihood_threshold + if weight_by_n is not None: + kwargs["weight_by_n"] = weight_by_n + self.ts = matcher_indexes.ts + super().__init__( + matcher_indexes, + recombination=recombination, + mismatch=mismatch, + **kwargs, + ) + + def find_path(self, h, start, end, match_out): + """ + Run the Viterbi algorithm on the haplotype h over sites [start, end), + writing the matched haplotype into match_out, and return the + (left, right, parent) arrays of the copying path, in right-to-left + order. + """ + path_len, left, right, parent, matched = super().find_path(h, start, end) + match_out[:] = matched + return left[:path_len], right[:path_len], parent[:path_len] + + def forward_matrix(self, h): + """ + Run the forward algorithm on the haplotype h and return the + CompressedMatrix of normalised forward probabilities. + """ + return CompressedMatrix(self.ts, *super().forward_matrix(h)) + + def backward_matrix(self, h, normalisation_factor): + """ + Run the backward algorithm on the haplotype h, scaled by the + normalisation factors of the forward matrix, and return the + CompressedMatrix of backward probabilities. + """ + return CompressedMatrix( + self.ts, *super().backward_matrix(h, normalisation_factor) + ) diff --git a/tsls/uv.lock b/tsls/uv.lock new file mode 100644 index 0000000..8303151 --- /dev/null +++ b/tsls/uv.lock @@ -0,0 +1,481 @@ +version = 1 +revision = 3 +requires-python = ">=3.11" +resolution-markers = [ + "python_full_version >= '3.12'", + "python_full_version < '3.12'", +] + +[[package]] +name = "attrs" +version = "26.1.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/9a/8e/82a0fe20a541c03148528be8cac2408564a6c9a0cc7e9171802bc1d26985/attrs-26.1.0.tar.gz", hash = "sha256:d03ceb89cb322a8fd706d4fb91940737b6642aa36998fe130a9bc96c985eff32", size = 952055, upload-time = "2026-03-19T14:22:25.026Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/64/b4/17d4b0b2a2dc85a6df63d1157e028ed19f90d4cd97c36717afef2bc2f395/attrs-26.1.0-py3-none-any.whl", hash = "sha256:c647aa4a12dfbad9333ca4e71fe62ddc36f4e63b2d260a37a8b83d2f043ac309", size = 67548, upload-time = "2026-03-19T14:22:23.645Z" }, +] + +[[package]] +name = "colorama" +version = "0.4.6" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", size = 27697, upload-time = "2022-10-25T02:36:22.414Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335, upload-time = "2022-10-25T02:36:20.889Z" }, +] + +[[package]] +name = "iniconfig" +version = "2.3.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/01/e1/2069291243c926a2ff1cd706c7f3eeb9b62144bf60f77c9fb9ff2fb26bd3/iniconfig-2.3.1.tar.gz", hash = "sha256:67f4b9c50da0dedf52af349e7749a80a9057a5031199791b906c3bb3ae878960", size = 21209, upload-time = "2026-10-06T22:48:38.076Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/56/43/4ca9e49d27a1fcf6bece6f6aec0ea46bb9112489b93d4b688fb415457bdb/iniconfig-2.3.1-py3-none-any.whl", hash = "sha256:9121e2c1fdb355232495be3194c8dfe87ccc2d5dee45947b78e68f499790d7a7", size = 7552, upload-time = "2026-10-06T22:48:36.959Z" }, +] + +[[package]] +name = "jsonschema" +version = "4.26.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "attrs" }, + { name = "jsonschema-specifications" }, + { name = "referencing" }, + { name = "rpds-py" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/b3/fc/e067678238fa451312d4c62bf6e6cf5ec56375422aee02f9cb5f909b3047/jsonschema-4.26.0.tar.gz", hash = "sha256:0c26707e2efad8aa1bfc5b7ce170f3fccc2e4918ff85989ba9ffa9facb2be326", size = 366583, upload-time = "2026-01-07T13:41:07.246Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/69/90/f63fb5873511e014207a475e2bb4e8b2e570d655b00ac19a9a0ca0a385ee/jsonschema-4.26.0-py3-none-any.whl", hash = "sha256:d489f15263b8d200f8387e64b4c3a75f06629559fb73deb8fdfb525f2dab50ce", size = 90630, upload-time = "2026-01-07T13:41:05.306Z" }, +] + +[[package]] +name = "jsonschema-specifications" +version = "2025.9.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "referencing" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/19/74/a633ee74eb36c44aa6d1095e7cc5569bebf04342ee146178e2d36600708b/jsonschema_specifications-2025.9.1.tar.gz", hash = "sha256:b540987f239e745613c7a9176f3edb72b832a4ac465cf02712288397832b5e8d", size = 32855, upload-time = "2025-09-08T01:34:59.186Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/41/45/1a4ed80516f02155c51f51e8cedb3c1902296743db0bbc66608a0db2814f/jsonschema_specifications-2025.9.1-py3-none-any.whl", hash = "sha256:98802fee3a11ee76ecaca44429fda8a41bff98b00a0f2838151b113f210cc6fe", size = 18437, upload-time = "2025-09-08T01:34:57.871Z" }, +] + +[[package]] +name = "numpy" +version = "2.4.6" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version < '3.12'", +] +sdist = { url = "https://files.pythonhosted.org/packages/d0/ad/fed0499ce6a338d2a03ebae59cd15093910c8875328855781952abf6c2fe/numpy-2.4.6.tar.gz", hash = "sha256:f3a3570c4a2a16746ac2c31a7c7c7b0c186b95ce902e33db6f28094ed7387dda", size = 20735807, upload-time = "2026-05-18T23:37:14.07Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b3/49/ec46835a70be8fa6446c495126ac84fdb28cb2558e1620ffb87a10c8b64c/numpy-2.4.6-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:0280e0356c0829a18d9de1cb7eee50ec22ca639878d7240307ca0943d73cd2c4", size = 16969194, upload-time = "2026-05-18T23:33:13.503Z" }, + { url = "https://files.pythonhosted.org/packages/0e/0d/f5957185c0ee2f3e12f78715aa9e3b353fd83633316c8532b38faa37e3f6/numpy-2.4.6-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:110f8b71aacb688ec69062bb7f6938a0f8acb01b7c1c4beb453c65b6d234584d", size = 14964111, upload-time = "2026-05-18T23:33:17.795Z" }, + { url = "https://files.pythonhosted.org/packages/ad/40/40a40ee0ddf7ceb782c49af278894b686e586d65d8c1889c8b5da01a3d7d/numpy-2.4.6-cp311-cp311-macosx_14_0_arm64.whl", hash = "sha256:4cfe66903cc32a9921a6733d96b19bb6abf310397581bbad89c228f5abaf0ee8", size = 5469159, upload-time = "2026-05-18T23:33:20.654Z" }, + { url = "https://files.pythonhosted.org/packages/63/13/f9a8046535cb21deae82f8d03de9617e08882d274fad2539630761888228/numpy-2.4.6-cp311-cp311-macosx_14_0_x86_64.whl", hash = "sha256:8155154c7c691289fe18f510b5d4657c68c67989f293f0535a91360392ff6538", size = 6798936, upload-time = "2026-05-18T23:33:22.987Z" }, + { url = "https://files.pythonhosted.org/packages/33/a8/6fa8c1a345a8c85dbb21932c447bee07c30a2c2a3f31e369c0a84b300147/numpy-2.4.6-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0ab0a9c4ffb1a6d95ef519fe4247dba8eb6b18ad93999f76b7f657039acabd47", size = 15966692, upload-time = "2026-05-18T23:33:26.62Z" }, + { url = "https://files.pythonhosted.org/packages/02/03/74fe2a4cb3817d94d86402f2506554130a2f01414e299b5a843e5a8a957f/numpy-2.4.6-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:89cd468399cfd2504718f0ba50e410dca55a170b61a02ad92bb18c8a65186e93", size = 16918164, upload-time = "2026-05-18T23:33:29.955Z" }, + { url = "https://files.pythonhosted.org/packages/c5/80/3615be3313f7e7696609bc194b9f0101da809df79e859bdb84e0cd043f46/numpy-2.4.6-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:c2d37ab77531417474168eb79d6d80b14f821a966818505d03013d0833edb7a8", size = 17322877, upload-time = "2026-05-18T23:33:34.724Z" }, + { url = "https://files.pythonhosted.org/packages/ca/ac/a691e0fe2675e370d0e08ff905adc49a1c8830e8cae03efe4477e92cd55d/numpy-2.4.6-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:f407cb6b8e9d6d8c626bc73c945db1706035af8fd632295547bf1c9e46d092d6", size = 18651487, upload-time = "2026-05-18T23:33:38.217Z" }, + { url = "https://files.pythonhosted.org/packages/15/a7/9bc1cd626d7bf6869bfedf27b91b6ab5dd607758bf8e959d6fa80c6a59cb/numpy-2.4.6-cp311-cp311-win32.whl", hash = "sha256:ddea102b48f9e339f3948bf22040944184627a30fdf7f858667673b9c5f033c8", size = 6233945, upload-time = "2026-05-18T23:33:41.331Z" }, + { url = "https://files.pythonhosted.org/packages/c5/31/7fc6239c12bce7e931463251cca4426c465e1876ba3cc785402ef4dd8f4e/numpy-2.4.6-cp311-cp311-win_amd64.whl", hash = "sha256:1e254a00cdf42b1e4d5b3d68d33af63268d41340d8885df2ab6470f2e1500147", size = 12608406, upload-time = "2026-05-18T23:33:44.131Z" }, + { url = "https://files.pythonhosted.org/packages/27/83/140f85a466595a16382996a1bf06b2b54bcd597488921b0c9daaeeda72af/numpy-2.4.6-cp311-cp311-win_arm64.whl", hash = "sha256:ed9749eef4cbd126da3dc1d6bcb3a57f5eb7ac6a6484146bdbf743f552dfc577", size = 10479528, upload-time = "2026-05-18T23:33:50.725Z" }, + { url = "https://files.pythonhosted.org/packages/95/2a/3d7b5ac8aac24feaf9ad7ed58f45b0bbc06d37e4338ae84c9f2298b570f9/numpy-2.4.6-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:001fbb8e08d942dd57599e781f2472269ee7f2755fae407b4f67b2f0b17da3f1", size = 16689119, upload-time = "2026-05-18T23:33:54.065Z" }, + { url = "https://files.pythonhosted.org/packages/ea/12/92c4c131527599e8288d6918e888d88726f84d805d784b771f32408aeaef/numpy-2.4.6-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:ebfb099f8dcf083deef3ac1ca4c1503f387cf76296fcb3816b66f5ecb5f54fdb", size = 14699246, upload-time = "2026-05-18T23:33:57.621Z" }, + { url = "https://files.pythonhosted.org/packages/ad/fe/c0a6b7b2ca128a8fb228575147073b660656734b8ebe4d76c8fd748dcc79/numpy-2.4.6-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:3213d622a0283a39a93d188f3cf72b26862df52fbb4ca3697f51705016523d41", size = 5204410, upload-time = "2026-05-18T23:34:00.302Z" }, + { url = "https://files.pythonhosted.org/packages/f3/d4/9770d14ba719432bb90a421bfd443872ed0f70f7264b64bec12ea363d5fd/numpy-2.4.6-cp312-cp312-macosx_14_0_x86_64.whl", hash = "sha256:357cc07a6d7b0b182ff02249616a03742827ebb1277546b5c7cd7f7620a45698", size = 6551240, upload-time = "2026-05-18T23:34:02.852Z" }, + { url = "https://files.pythonhosted.org/packages/c9/c6/50a46a6205feba2343f1d6d17438107c5dc491ed1c736e6ea68689fd906b/numpy-2.4.6-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5f9fb9157b4ce2971008323afe46053787b526ef624fea915b261468a8421a0f", size = 15671012, upload-time = "2026-05-18T23:34:05.485Z" }, + { url = "https://files.pythonhosted.org/packages/99/60/14115e6364fa676c5397c2ad3004e527e9aa487abf5d0706ec81bbd08529/numpy-2.4.6-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:90f9849678c75fe7afa2d348ac842c168b0a4d3d61919687216dfc547976d853", size = 16645538, upload-time = "2026-05-18T23:34:09.265Z" }, + { url = "https://files.pythonhosted.org/packages/ae/c5/693cbe59e57db94d2231fa519ca3978dc9e19da5a8f088588f5c6e947ff2/numpy-2.4.6-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:c1a2af6c6ef86344a6b0db6b97834208bf598db514f2b155042439b62605601a", size = 17020706, upload-time = "2026-05-18T23:34:13.053Z" }, + { url = "https://files.pythonhosted.org/packages/ef/fc/85b7c4eff9b4966ade25c2273cf7e7012e92366c032058653934b37de044/numpy-2.4.6-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:e5805d5a22fd19c8ccff10a9561f9df94436b0545619ea579db2d3c35294bce2", size = 18368541, upload-time = "2026-05-18T23:34:17.024Z" }, + { url = "https://files.pythonhosted.org/packages/f6/81/e1b27545deedce7f4a0b348618c6b62d74e36a4dc9ccd42f3eb2f85eee32/numpy-2.4.6-cp312-cp312-win32.whl", hash = "sha256:e3eeb0aabd6bd5ce64faae67e9935203a6991b4bc2a485a767fbafb2c5125f45", size = 5962825, upload-time = "2026-05-18T23:34:20.3Z" }, + { url = "https://files.pythonhosted.org/packages/ab/ca/feab00bd44aa5fe1ad2c18f08b4d3bb92e26484b0b1d1443897809ed528c/numpy-2.4.6-cp312-cp312-win_amd64.whl", hash = "sha256:d8e8286dd7cea7895157318d1b91cdacac64c479f3cbc8dce548331728484751", size = 12321687, upload-time = "2026-05-18T23:34:23.095Z" }, + { url = "https://files.pythonhosted.org/packages/63/cf/5a6d34850a39d1093558564f77ee8e8e0bee5061151b8f05a55711001ec7/numpy-2.4.6-cp312-cp312-win_arm64.whl", hash = "sha256:4081eb135ac24158bd51cdfbef16f1c64df7063b1143f24731387137c092bec8", size = 10221482, upload-time = "2026-05-18T23:34:25.876Z" }, + { url = "https://files.pythonhosted.org/packages/fb/82/bdab26d7438c6791ca31b7c024ca37c1eab8b726ba236129005cd4a06e45/numpy-2.4.6-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:511dbaf848decaaaf4b4ca48032619fb3138710c4bf7da7617765edad1ef96b0", size = 16684648, upload-time = "2026-05-18T23:34:29.41Z" }, + { url = "https://files.pythonhosted.org/packages/1b/30/a80189bcc7f5e4258b3fbc3968d909d1756f54d023299ecc39ad6fdb9ef8/numpy-2.4.6-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:bf162abab1c1a736333192707cef898e735a5ca00f38f27eeedf44b39d9e85eb", size = 14693902, upload-time = "2026-05-18T23:34:33.013Z" }, + { url = "https://files.pythonhosted.org/packages/97/12/70b5d0d7c15e1ebb8a6a84a8caa1d19e181d84fb58bb6d70aca29099dec1/numpy-2.4.6-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:043191bfa8eab18c776647b62723ac9dddece59743b13f49b2016094129c2b3f", size = 5198992, upload-time = "2026-05-18T23:34:36.132Z" }, + { url = "https://files.pythonhosted.org/packages/ba/8c/ebd2a8f8a83541f8d38cc5667e8c2b69cecfd30da6e45693e8158857d44b/numpy-2.4.6-cp313-cp313-macosx_14_0_x86_64.whl", hash = "sha256:6180d8b35af935aed8ece3a85e0a43f87393ae0ac87c8d2c8bd2c993f7270ef3", size = 6546944, upload-time = "2026-05-18T23:34:38.484Z" }, + { url = "https://files.pythonhosted.org/packages/bb/c5/7b863a97a91671a0338f4253bd3b5a3d3852f0692dae91711c9f4a10e787/numpy-2.4.6-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:72fbe16c6fac95aedf5937fa873445cec2110be35d8a4e9433d7501fd98dae6b", size = 15669392, upload-time = "2026-05-18T23:34:41.257Z" }, + { url = "https://files.pythonhosted.org/packages/a5/9d/3584b9984ca4c047aea75214ce1a4c4c73d849bd71b604264b7f5653f8a8/numpy-2.4.6-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a7830bab239b79cda9c08c2da014761cafb48da6150e1da17ac06283f43b6089", size = 16633220, upload-time = "2026-05-18T23:34:45.075Z" }, + { url = "https://files.pythonhosted.org/packages/05/ae/7c67fba23bd98caec7c99261f3a16072ade14813486b0282cb29846de832/numpy-2.4.6-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:ef4aea96ce4d3b074422cb4f2f64e216bf9e213004bb58ecfdf50ea02ea8eb9a", size = 17020800, upload-time = "2026-05-18T23:34:49.065Z" }, + { url = "https://files.pythonhosted.org/packages/d9/5d/3b6725cb31d983c5e66916f5d36f6d7e5521129e4c4404d64f918292a5b6/numpy-2.4.6-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:dfa20cc6ca228e6b155b11da03825975ce66aea520985dbbddf0f2a5a495c605", size = 18357600, upload-time = "2026-05-18T23:34:52.709Z" }, + { url = "https://files.pythonhosted.org/packages/f7/da/2ccc6c2fe8898dee01d90c75c5f5f914a23daf99e3e0f59516a08760c8b5/numpy-2.4.6-cp313-cp313-win32.whl", hash = "sha256:56b39e5e0622a09a25bf5baf62f4bcf0cb8a41ae6e2819cf49bbc5a74c083f91", size = 5961134, upload-time = "2026-05-18T23:34:55.618Z" }, + { url = "https://files.pythonhosted.org/packages/b5/cd/9cc4dc876fb065d5c220aae4d5e14826b2715331bb7618ce1fb07a679d99/numpy-2.4.6-cp313-cp313-win_amd64.whl", hash = "sha256:c4fc99836233ea196540b17ab0983aff60ed07941751930f5f4d05bc3b3b7359", size = 12318598, upload-time = "2026-05-18T23:34:58.928Z" }, + { url = "https://files.pythonhosted.org/packages/39/1e/c0bcba1f8694116485fe28fd1be698c278fcda4141c5b0e53a2aed8b12a8/numpy-2.4.6-cp313-cp313-win_arm64.whl", hash = "sha256:a7c711e21628b52034bb5ab8d1bce291f752fcc5e92accc615778acee1ff4778", size = 10222272, upload-time = "2026-05-18T23:35:02.167Z" }, + { url = "https://files.pythonhosted.org/packages/63/6d/cc5619247c8f4204e507f5883528372e4ac4bb189e579fb859a12e480b1f/numpy-2.4.6-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:112b06a867b235ef466ed3508ddf0238050df9c727cafb5301ac385b899189a1", size = 14821197, upload-time = "2026-05-18T23:35:05.468Z" }, + { url = "https://files.pythonhosted.org/packages/00/58/f1c39161c87d9e9bed660f1ed4bafc0e403d5ec9650b6dd77aead07d489b/numpy-2.4.6-cp313-cp313t-macosx_14_0_arm64.whl", hash = "sha256:eaf7fa2de5c0be8ae6ff8e9bea2ccd725e980541244521d8d4b5f3354a27babe", size = 5326287, upload-time = "2026-05-18T23:35:08.693Z" }, + { url = "https://files.pythonhosted.org/packages/af/57/3917ab0fd97f271a8694513581b8a36c655f111c446852c302f04ccdb6fc/numpy-2.4.6-cp313-cp313t-macosx_14_0_x86_64.whl", hash = "sha256:7265a2f3d436e54ef9f2b52b5c937e6be778781bd97a590319d7348f1c1ca997", size = 6646763, upload-time = "2026-05-18T23:35:11.459Z" }, + { url = "https://files.pythonhosted.org/packages/eb/0f/037e64c494b67581ae18193d770adef354c41f3f2c8ebf865602d949bf8f/numpy-2.4.6-cp313-cp313t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f74a575920ab21fe304421a3fc28793d82e299cae9eccb37084e9fc7f3617c20", size = 15728070, upload-time = "2026-05-18T23:35:14.79Z" }, + { url = "https://files.pythonhosted.org/packages/21/a6/5d2bae9c9542eb4df16dc9c46dc79c186e9bad53805dfa5399a6023c6db0/numpy-2.4.6-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ede83e07a75dd06bc501566c1eca2afc0d61677c1472ac9ad93fdee6e638a48d", size = 16681752, upload-time = "2026-05-18T23:35:18.836Z" }, + { url = "https://files.pythonhosted.org/packages/92/14/23d1dfb410ae362cd59ce53e936b1513d545eb40db3949ced632e19a459e/numpy-2.4.6-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:68bb27509ac1b9a3443094260f6326150663b06abe40b73a2f81160623da5b67", size = 17086024, upload-time = "2026-05-18T23:35:22.52Z" }, + { url = "https://files.pythonhosted.org/packages/4b/6e/23595a2c642cdf3bc567877064bdd7f91c8b0038a4453cf2daf7248eafe9/numpy-2.4.6-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:a0df0043bdb289bde1f62da130d20df23d58b45429f752bc7a8fc5325a225ecd", size = 18403398, upload-time = "2026-05-18T23:35:26.398Z" }, + { url = "https://files.pythonhosted.org/packages/8a/90/0ac3bc947217e66dec77e7cbc6a1979d1af70b6461b82f620d3bccd5e4c8/numpy-2.4.6-cp313-cp313t-win32.whl", hash = "sha256:29a287e0cf63ff528da061de6b9f64a4618da591ca1046aafc54062e40ca7eab", size = 6084971, upload-time = "2026-05-18T23:35:29.387Z" }, + { url = "https://files.pythonhosted.org/packages/77/71/5673e351671a1d2bd6063b91b44f70c0affea7d1516fa7a6572941ba4aa1/numpy-2.4.6-cp313-cp313t-win_amd64.whl", hash = "sha256:25c692919ac5a01f170a3bfcd62d745b24fd095c353d50812637d6fcab442e75", size = 12458532, upload-time = "2026-05-18T23:35:32.175Z" }, + { url = "https://files.pythonhosted.org/packages/3f/88/19d3503c5046e688f049274b27a3ef3d771152fa80d3ba3d01a3dff61abe/numpy-2.4.6-cp313-cp313t-win_arm64.whl", hash = "sha256:1e978ec1e8bd0e0e4de6bb75de9d30cbb74db6b6a2bb727618613703ca0167dd", size = 10291881, upload-time = "2026-05-18T23:35:35.465Z" }, + { url = "https://files.pythonhosted.org/packages/f8/91/3ab2044d05fd16d343c5ac2e69b127f1b2854040dd20b193257c78028bd3/numpy-2.4.6-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:06ca2f61ec4385a07a6977c55ba998a4466c123642b4a32694d3128fce18c079", size = 16683458, upload-time = "2026-05-18T23:35:38.353Z" }, + { url = "https://files.pythonhosted.org/packages/8e/62/764ce66fa4147ae6d73071a3abf804ffe606f174618697c571acdf26a7c9/numpy-2.4.6-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:38efbc8de75c7a0fc1ac190162d892787f3f47b57cc291231aafee36b80982b7", size = 14704559, upload-time = "2026-05-18T23:35:42.14Z" }, + { url = "https://files.pythonhosted.org/packages/60/61/23f27c172f022e04025b7dc2367f4d63c1a398120607ec896228649a6f48/numpy-2.4.6-cp314-cp314-macosx_14_0_arm64.whl", hash = "sha256:d581b735e177fdcdce6fed8e7e8880a3fb6ee4e3653a3ac6af01c6f4c03effc5", size = 5209716, upload-time = "2026-05-18T23:35:45.377Z" }, + { url = "https://files.pythonhosted.org/packages/03/71/21cf70dc6ea3e3acb95fc53a265b2fc248b981f0194ceb5b475271b8809d/numpy-2.4.6-cp314-cp314-macosx_14_0_x86_64.whl", hash = "sha256:0a041d3d761dc3c35cc56ce0351506a02bcbc25f7b169f652435141a17db9096", size = 6543947, upload-time = "2026-05-18T23:35:47.926Z" }, + { url = "https://files.pythonhosted.org/packages/d5/91/64288395ee1799bd2e0b04a305dce9666da90c961e1f3fe982a05ee1c036/numpy-2.4.6-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:40fdc1ae7125e518ea98e53e69a4ebc27e1fd50510c47b7ea130cf21e5e1d42b", size = 15685197, upload-time = "2026-05-18T23:35:50.863Z" }, + { url = "https://files.pythonhosted.org/packages/f3/eb/ebffaa97dc55502df69584a8f0dcf07f69a3e0b3e2323670a2722db9aa39/numpy-2.4.6-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a2c306dea656c12c68f51f4cea133cbe78ca7435eb28c735eac1d3ebe73be6e8", size = 16638245, upload-time = "2026-05-18T23:35:54.752Z" }, + { url = "https://files.pythonhosted.org/packages/b8/0b/54f9da33128d7e350fab89c7455902eeae70349ee52bddb448dc4a576f45/numpy-2.4.6-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:33111801a01c12a8a1e3721f0a9232f8cfc8ae2c6b7098167e6f623c6073f402", size = 17036587, upload-time = "2026-05-18T23:35:58.355Z" }, + { url = "https://files.pythonhosted.org/packages/b6/f0/fdebc1052db1cc37c64beb22072d67cd6d1c71adca1299f53dec2b5e20d3/numpy-2.4.6-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:ae506e6902902557576a26ff33eda8695e7ecb3cb36c3b573a0765dee114ebdb", size = 18363226, upload-time = "2026-05-18T23:36:02.845Z" }, + { url = "https://files.pythonhosted.org/packages/aa/b4/298628d98c72b57e57f7165ae6a481a1deaf6f3c28262a6e4c739c275930/numpy-2.4.6-cp314-cp314-win32.whl", hash = "sha256:aaf159caa35993cb1f56fb9b8e4610d35758e7ca005412eb1daa856a78c9c4b1", size = 6010196, upload-time = "2026-05-18T23:36:05.92Z" }, + { url = "https://files.pythonhosted.org/packages/df/ac/46de6dda46478f7942f839e094970be2d4a861e005c4b3bf07c92e291a09/numpy-2.4.6-cp314-cp314-win_amd64.whl", hash = "sha256:b507f5c4c1d508876d1819b6bf9a49d365b96320b5d4993426b33a23ca4b8261", size = 12450334, upload-time = "2026-05-18T23:36:09.107Z" }, + { url = "https://files.pythonhosted.org/packages/78/92/b8b798ac784102c0da830d2257d59358e3d3d90d1e2b3f2575dad976c5cf/numpy-2.4.6-cp314-cp314-win_arm64.whl", hash = "sha256:6f41ae150c4e32db4f3310cdaf64b1593a03dbabe29eec77fc9b50fe64061df6", size = 10495678, upload-time = "2026-05-18T23:36:12.766Z" }, + { url = "https://files.pythonhosted.org/packages/30/34/ec28d1aa8115971537c01469ab2011ee96827930f0a124de1000cc2a7ed7/numpy-2.4.6-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:ece3d2cfe132e7d51f44a832b303895e6f2d499c5e74dfbdb06ee246147a304a", size = 14823672, upload-time = "2026-05-18T23:36:16.473Z" }, + { url = "https://files.pythonhosted.org/packages/16/bd/f6d1fede4e54e8042a7ff97bb495510f3c220f94bcd9e8b228e87c92cc0d/numpy-2.4.6-cp314-cp314t-macosx_14_0_arm64.whl", hash = "sha256:e3e5193ef5a3dc73bceee50f7fdc2c90dbb76c42df8d8fae3d1067a583df579e", size = 5328731, upload-time = "2026-05-18T23:36:19.767Z" }, + { url = "https://files.pythonhosted.org/packages/f4/f0/e105b9e2fd728a9910103884decd6951d9dd73896b914a98d9a231de02ee/numpy-2.4.6-cp314-cp314t-macosx_14_0_x86_64.whl", hash = "sha256:17f9ade344e7d9b464a084d69bcf18fc691cb1db67c62ed80820bf4926d78f0e", size = 6649805, upload-time = "2026-05-18T23:36:22.266Z" }, + { url = "https://files.pythonhosted.org/packages/82/dd/1206a7ca6ab15e3f02069707ca96222e202af681bb73756da7527f3cb837/numpy-2.4.6-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9cd5ffd25db4e7ba6a375693b3fc0fc1791ec636c17db3720da19bde7180ec43", size = 15730496, upload-time = "2026-05-18T23:36:25.713Z" }, + { url = "https://files.pythonhosted.org/packages/51/e7/38d3ea825dcab85a591734decb2f6c67caa7c8367d374df1a1c3842f9b07/numpy-2.4.6-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7d92c3819208a60205a12a245c91ad70cb0a85336659b19b834205573ac8456e", size = 16679616, upload-time = "2026-05-18T23:36:29.652Z" }, + { url = "https://files.pythonhosted.org/packages/93/b7/caabfdf53edf663e0b4eb74d7d405d83baef09eb5e83bcd32d601d72b93e/numpy-2.4.6-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:e85b752a1e912b70eaad4fafbd4d1238007ab221de2009b9a2f5ae7461239895", size = 17085145, upload-time = "2026-05-18T23:36:33.449Z" }, + { url = "https://files.pythonhosted.org/packages/f9/45/68d7c33a6bcf3e5aa3bdbd57a367e6f615286dfd6482f97e8ffeb734306e/numpy-2.4.6-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:29cb7f67d10b479ff07c17d33e39f78c07f71c40ef30d63c153d340e96cd3fb4", size = 18403813, upload-time = "2026-05-18T23:36:37.369Z" }, + { url = "https://files.pythonhosted.org/packages/9c/50/0753655aa844c99cd9e018aacf76f130f1bd81d881bb74bc0aef5d73a8ba/numpy-2.4.6-cp314-cp314t-win32.whl", hash = "sha256:260a5d70215b61ab4fadf5c7baacd64821842975eea312125ed3c39a6391b063", size = 6156982, upload-time = "2026-05-18T23:36:40.817Z" }, + { url = "https://files.pythonhosted.org/packages/b2/d4/7c67becf668f973cb490cec3e98dfd799d866f9c989a54d355672cfa0db6/numpy-2.4.6-cp314-cp314t-win_amd64.whl", hash = "sha256:81a1cca95ed5bb92aa8b10dd2cdc9a0d3853a50fad926c28b5d7e8ea54389627", size = 12638908, upload-time = "2026-05-18T23:36:43.996Z" }, + { url = "https://files.pythonhosted.org/packages/43/bb/e1c71a4295b1b1d1393d50dbb4f2a36283c6859d9d3892e84f00ec5a91d5/numpy-2.4.6-cp314-cp314t-win_arm64.whl", hash = "sha256:0c9136e14ed34a9e343a31c533d78a9813a69a3148332bce5e9821cb2f996e66", size = 10565867, upload-time = "2026-05-18T23:36:47.114Z" }, + { url = "https://files.pythonhosted.org/packages/de/12/b422cc84439adc0d00de605bf4a308890ae5c26f2c71fbd73e5d08fbb0dd/numpy-2.4.6-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:55cced7c52e981362f708ad635198e97a752dfba412cc03c23bbf3bd8d5cd662", size = 16847511, upload-time = "2026-05-18T23:36:50.673Z" }, + { url = "https://files.pythonhosted.org/packages/44/53/f481bef68011740f8849418d82db07230e825013f31f4eef5ba5b805316a/numpy-2.4.6-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:d6da64deb6b8ed903e7560180a92f2d804ee1ba5eeb849ac2748b8c1aba1f6d7", size = 14889064, upload-time = "2026-05-18T23:36:53.879Z" }, + { url = "https://files.pythonhosted.org/packages/7f/57/42ed575c10ced8af951d426bc4e1f8aff16fd851db33f067036215a7f860/numpy-2.4.6-pp311-pypy311_pp73-macosx_14_0_arm64.whl", hash = "sha256:68a5124b13fa6cc2086764a20005d30bc0548146f7f5322f02fce212ca14317f", size = 5394157, upload-time = "2026-05-18T23:36:57.194Z" }, + { url = "https://files.pythonhosted.org/packages/6a/ef/f66cc724fcc36c1e364c67f51ae9146090b8b584f27d58b97fdae3edd737/numpy-2.4.6-pp311-pypy311_pp73-macosx_14_0_x86_64.whl", hash = "sha256:948424b06129ce883307e8cff868c31396d8dc7630a59c61d70d98dbe70f222c", size = 6708728, upload-time = "2026-05-18T23:36:59.575Z" }, + { url = "https://files.pythonhosted.org/packages/1a/9c/c531f2293b91265d8b48e9b329f54fdd7ffae73cb4134ea10cca4237e9cc/numpy-2.4.6-pp311-pypy311_pp73-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5dbbdb29840ca3d91ee0fece42fc29278886d908280bfec0a5846c6f901a3eb0", size = 15798374, upload-time = "2026-05-18T23:37:02.674Z" }, + { url = "https://files.pythonhosted.org/packages/1a/b0/413077f6b1153ed3cba361401c6783bbad6114804a000cc22eb71c13e190/numpy-2.4.6-pp311-pypy311_pp73-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8ad03c0965fb3c692200e74d458ca28c1dbb4ce96f9a479a8aa041ad5fabca02", size = 16747286, upload-time = "2026-05-18T23:37:06.327Z" }, + { url = "https://files.pythonhosted.org/packages/15/ce/e5ec180bc41812edcd8daeb8639d205622c0e8c02259d8ab25a0201b3c2a/numpy-2.4.6-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:2803abfebfc990042cd494d8ce2d5f82e9d847af6d35ec486923aa19dbad5e73", size = 12504263, upload-time = "2026-05-18T23:37:09.715Z" }, +] + +[[package]] +name = "numpy" +version = "2.5.4" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.12'", +] +sdist = { url = "https://files.pythonhosted.org/packages/95/b0/c7453d0b6e2073c3264468b106ee1563750cecc910965e67357e3698c83e/numpy-2.5.4.tar.gz", hash = "sha256:9a94cf751c9ad8ebaa835bcd3d40dacf8534ad086b88c38029b65123c7999d2a", size = 20866315, upload-time = "2026-10-10T20:05:31.422Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d0/97/ba2074e92b7befea137e77ea8471e768bbd87c339b7e8c9f5a931949f977/numpy-2.5.4-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:c6342f54c67093cae5c0227eb0eb772fdb79f2a2c37a6eb278b9909ee06aa356", size = 17001609, upload-time = "2026-10-10T20:02:40.843Z" }, + { url = "https://files.pythonhosted.org/packages/ff/a9/bac826765e971d8e16e2064e9ac7525fd69b40ac17c905033a7f5442023f/numpy-2.5.4-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:b11e8fda06a7d69f15ebf542660b74466c2e51094800c1fb794f47ad4faeef17", size = 12015718, upload-time = "2026-10-10T20:02:43.45Z" }, + { url = "https://files.pythonhosted.org/packages/31/2f/5ea3570fcb8ccd0882bea99436a513b2c85dad8f774a2057849130a8fb99/numpy-2.5.4-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:9cb18a327b49c5c337f972b03682f6a49855525faaf3c0d3e9c96cd0fd8880a8", size = 5451717, upload-time = "2026-10-10T20:02:46.169Z" }, + { url = "https://files.pythonhosted.org/packages/34/f2/b4fc1bafca03868220b5eaf729d2f21ebd7d7b151c0f9e144fe212bbca35/numpy-2.5.4-cp312-cp312-macosx_14_0_x86_64.whl", hash = "sha256:aec3fc4b32ff82421274f5d205c559c51c840c8df66a78efd7f3612dd005a26a", size = 6789926, upload-time = "2026-10-10T20:02:48.139Z" }, + { url = "https://files.pythonhosted.org/packages/dc/96/8319e2457ae4333c62c815c7006b869a4f60985c1e01024c2f8c6c040fe5/numpy-2.5.4-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:fe4d21ab149f15e4e6043dfb0de87e6e5f34ac176cde83060e9802981fca2ac2", size = 15695312, upload-time = "2026-10-10T20:02:50.115Z" }, + { url = "https://files.pythonhosted.org/packages/43/a3/c799c62e19c337e6d3770b08e475887fb30ce8477d3c09efca6b2f0228a6/numpy-2.5.4-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:fbde6962867ee75b48b0ee29b2b9372ec5d617799dbaf38e82dc0596f2f7738a", size = 16727283, upload-time = "2026-10-10T20:02:53.186Z" }, + { url = "https://files.pythonhosted.org/packages/39/6b/3604e53fb00314d0dc1b94ec9125a1484f649c0a17480b1f0f0c7a9d6250/numpy-2.5.4-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:381a7a3d2e65e64c0ec302795ab9dc12bb1e73f150904699c153716177eebdaf", size = 17047890, upload-time = "2026-10-10T20:02:56.038Z" }, + { url = "https://files.pythonhosted.org/packages/4a/7a/e8b58a5289a0d464c52885de47c35a935cdd70c03a4c3ab94a5126416dd0/numpy-2.5.4-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:b89d0aaae2fe498c648f4c4795c084db535af5bd98ef942b2a3681fb74ce8645", size = 18485839, upload-time = "2026-10-10T20:02:59.018Z" }, + { url = "https://files.pythonhosted.org/packages/6f/c9/47094f597015009f310b8c900def59065ef1ff5a6fe7b51fc65ec58ec2c6/numpy-2.5.4-cp312-cp312-win32.whl", hash = "sha256:9968ab7e49b93ac6e1c3b2239732183152c9150f16308d30b66a372cffe3483c", size = 6138936, upload-time = "2026-10-10T20:03:01.626Z" }, + { url = "https://files.pythonhosted.org/packages/12/33/fefe62073dc8acfd0f2b9ed7c003af2f50aa61555e113e6db02b8f79f145/numpy-2.5.4-cp312-cp312-win_amd64.whl", hash = "sha256:a7b1b6353e36a7e50de2973a38d705c88ee93adcf120673cee7f45a4a3fa223a", size = 12573091, upload-time = "2026-10-10T20:03:04.349Z" }, + { url = "https://files.pythonhosted.org/packages/1a/07/161270b0c2eec56e4c905f6d6d22e1b836887b2cb189d3f5820aa588e9dd/numpy-2.5.4-cp312-cp312-win_arm64.whl", hash = "sha256:aa1cce2ff3f8d953de38b76bf44602caeb69f101430208f64a10067f7cb4b1d3", size = 10521630, upload-time = "2026-10-10T20:03:06.767Z" }, + { url = "https://files.pythonhosted.org/packages/67/14/1c3ee0118a8fce08565a5d8482631608426a33af10a01077fada5dc7c119/numpy-2.5.4-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:2377da2dd3ba2c1200956acbab2a358c83b8e1f8531191672d1cd6ad83250d53", size = 16997729, upload-time = "2026-10-10T20:03:09.291Z" }, + { url = "https://files.pythonhosted.org/packages/83/8c/b0ea9477fb1f0d4484bbc5cba21678cc9969704d8d7f3f158d1db35f8e14/numpy-2.5.4-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:7415db95818b39ec475a5eea54d9e3b6bc83e3912158e46da3438cdce399804d", size = 12009826, upload-time = "2026-10-10T20:03:11.946Z" }, + { url = "https://files.pythonhosted.org/packages/e2/84/6a3d75b3ba3dfe84ac0053450753d1e6d250a8bf80f66474cc46d1fb643f/numpy-2.5.4-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:6d6a71b9d9a97c03633aa12565ef2825ffa036cc1d99cfd50dacf0f128af4fe2", size = 5445803, upload-time = "2026-10-10T20:03:14.329Z" }, + { url = "https://files.pythonhosted.org/packages/61/18/bb993f267ca20b376e07092a16793a5b31ed3138751e9ba480011a14d742/numpy-2.5.4-cp313-cp313-macosx_14_0_x86_64.whl", hash = "sha256:d8200f16437b289a5bb927c6e184eccc3e8389bc0070fea4cd5b9e13c1757959", size = 6786220, upload-time = "2026-10-10T20:03:16.602Z" }, + { url = "https://files.pythonhosted.org/packages/db/b6/135bb0953b61dc21c6cafa14b424ae666944e4899cf140e00c2b322a1a45/numpy-2.5.4-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1c2e71b04c6cad90026e544501bbe0ab9290fa8a4d845e7e8c0d124fb429c988", size = 15689178, upload-time = "2026-10-10T20:03:18.721Z" }, + { url = "https://files.pythonhosted.org/packages/da/24/3bd070f3269dc609d8f26b2643f62ef91bb415841c0b294805aaf7fe06da/numpy-2.5.4-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6ffa07666f8da0eef81d149934a626d0d95fbd6838432a33e66245423a9062c0", size = 16718044, upload-time = "2026-10-10T20:03:21.386Z" }, + { url = "https://files.pythonhosted.org/packages/c7/8e/9d15bd356b0a019c965312b1a3c6a727cac4cae5bc40045fbc12ce4cff9c/numpy-2.5.4-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:2fa3328f784fc8277fc48026f6cad516f5c561c5d8e2e39b3c9e0c8f23223b34", size = 17048364, upload-time = "2026-10-10T20:03:24.468Z" }, + { url = "https://files.pythonhosted.org/packages/dc/fe/9d5b560db964f15871885f2250795d15945f8699e17ef90c0c2ff4c875b2/numpy-2.5.4-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:b86966fbe4ad7de710422175572bcdc75fdedadfb54bc6fab7deabccddd7780b", size = 18474904, upload-time = "2026-10-10T20:03:27.895Z" }, + { url = "https://files.pythonhosted.org/packages/e9/98/d27552990f1bd611ef3e7466adadc78312ea2df63b83aad47fdc3d3ca8df/numpy-2.5.4-cp313-cp313-win32.whl", hash = "sha256:5258bc06526964be5face2fc6f756857a3f24f21ec3e72ca131337a75b165d6c", size = 6134537, upload-time = "2026-10-10T20:03:30.511Z" }, + { url = "https://files.pythonhosted.org/packages/90/8c/140a40398a66b4471211be1affdb6ed24c486d581bd28d07b7f2fcb69540/numpy-2.5.4-cp313-cp313-win_amd64.whl", hash = "sha256:8b4d2fd2d34e5f8c9235ee787de5631a37a28402b15cb80814df973d2be54129", size = 12566113, upload-time = "2026-10-10T20:03:32.612Z" }, + { url = "https://files.pythonhosted.org/packages/34/52/01d205e5e8ccb27b2b0b141e801f22b830198c979111b0fa44771438d9a9/numpy-2.5.4-cp313-cp313-win_arm64.whl", hash = "sha256:bc39ac66a7a9a3fbd6134fda43136b60ffde99c8f4501e64e0d2b24da137babf", size = 10519523, upload-time = "2026-10-10T20:03:35.163Z" }, + { url = "https://files.pythonhosted.org/packages/99/ba/005cb5edd580d2f84d7ca3206b92dc17d4388e56e6f87ffe8f2762f83139/numpy-2.5.4-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:c668b2f0d651605b58892644b0e302c7157f7159544227758c896982ef384b18", size = 17005499, upload-time = "2026-10-10T20:03:37.961Z" }, + { url = "https://files.pythonhosted.org/packages/f3/49/fee7587c33ee35f7977f9051d7f2023d4e7246d62710c80f20c2361ea232/numpy-2.5.4-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:ffa6ce09a1c6a08e9667dd9c97aa0b14184e8d18f2a14b78b2a2328c9147f076", size = 12019666, upload-time = "2026-10-10T20:03:40.606Z" }, + { url = "https://files.pythonhosted.org/packages/d5/b2/c6ce165acffceb15a82c07b9cc77d391f86b3f379ba62911908ae5d34b91/numpy-2.5.4-cp314-cp314-macosx_14_0_arm64.whl", hash = "sha256:956555e0603a4d38019ae6925711cb9dc43195c076a928accf7ea5d50bddfe53", size = 5455617, upload-time = "2026-10-10T20:03:43.138Z" }, + { url = "https://files.pythonhosted.org/packages/77/7f/dd85ce260a669a89be06842cf355d7353a33e6cfbc590fb8ebb947d88dc9/numpy-2.5.4-cp314-cp314-macosx_14_0_x86_64.whl", hash = "sha256:2c2c4afffdeb7920e445028dd71eb932cac3e704792e964bc2a232426d4f1255", size = 6791932, upload-time = "2026-10-10T20:03:44.874Z" }, + { url = "https://files.pythonhosted.org/packages/63/d6/34b0a2b0741386a63025a65a2c09caaaaaad6d0ca95b66cd65c30dd7fcb5/numpy-2.5.4-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4054173604cd8658796053f1f3bc0befb68ec1c0762c57fdad61e199256a8617", size = 15710899, upload-time = "2026-10-10T20:03:46.839Z" }, + { url = "https://files.pythonhosted.org/packages/16/d5/928078d2b28f26829b138b4a6c3980045022fb409f570657a224ae60ef4e/numpy-2.5.4-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d549420b8858885cea8838a727842249218b9c1da24dd517e25c9c7a948310a3", size = 16721710, upload-time = "2026-10-10T20:03:49.489Z" }, + { url = "https://files.pythonhosted.org/packages/f9/cf/673fd1b8f4cd78eb6320e87ec4c90ac19c095644259e3749853a405c70f4/numpy-2.5.4-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:823874a507a84af050493b622affde94b6f7c3a0dc22cb2801381bc03b871c00", size = 17066182, upload-time = "2026-10-10T20:03:52.25Z" }, + { url = "https://files.pythonhosted.org/packages/f3/92/a77b5061b1b3e2643928c37976d79ee173e1b171ed158b7a3c61056b41bc/numpy-2.5.4-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:4e263278bfb5ee6409db8aedbc4cc32973b1b82bc1e8d3c668551d04d83a7e37", size = 18480315, upload-time = "2026-10-10T20:03:55.39Z" }, + { url = "https://files.pythonhosted.org/packages/bb/1d/1486ef3d3fb2279fd93c4c43c1bbbf1ca389a19816696684409f71babaab/numpy-2.5.4-cp314-cp314-win32.whl", hash = "sha256:cfd73180400042a7c532d30c5e287bdd03c59ff9ee1b4c0316af0539e29dfe23", size = 6185739, upload-time = "2026-10-10T20:03:58.186Z" }, + { url = "https://files.pythonhosted.org/packages/52/9a/e1e512ebc948d5b9dd33b08736760f0ebbed2848fd4eda1f553088a6dcee/numpy-2.5.4-cp314-cp314-win_amd64.whl", hash = "sha256:2ca144f15135b6212a5c47b1e2aeca6e412f102f95a2d5d88d8aec77eb255de3", size = 12703552, upload-time = "2026-10-10T20:04:00.28Z" }, + { url = "https://files.pythonhosted.org/packages/2c/05/de709a982d7bbcd688a3fad71f002e9ff80c2db39e03ee726609b610f1d1/numpy-2.5.4-cp314-cp314-win_arm64.whl", hash = "sha256:468397ba3c64427474706e5c9123fe266395496714dc684294eac75cd4930d1e", size = 10803901, upload-time = "2026-10-10T20:04:02.659Z" }, + { url = "https://files.pythonhosted.org/packages/13/34/083570ada3bb2a30fbe5d77c8c6fef9141144a15d33e6f793a67e9749ab8/numpy-2.5.4-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:1ef3aa6d7e29bb13677323114280b05acc57607fa2300e66432d665d5418a162", size = 12138695, upload-time = "2026-10-10T20:04:05.012Z" }, + { url = "https://files.pythonhosted.org/packages/94/06/1f9c24db48eef0c2d1207e3b11fffb0478e39dfd8c1e1be7476936885eed/numpy-2.5.4-cp314-cp314t-macosx_14_0_arm64.whl", hash = "sha256:98b053943e5a0474ec0da309d2cb9d3f18ea57f8a2067c2ab7b5f763d1068380", size = 5574615, upload-time = "2026-10-10T20:04:07.316Z" }, + { url = "https://files.pythonhosted.org/packages/da/0f/593fba2e1560e949123bc7d2fc48b5893d56e58cd4bd5a273d2fbf60b220/numpy-2.5.4-cp314-cp314t-macosx_14_0_x86_64.whl", hash = "sha256:b64a85f40e154983960a4167d4c1d57a50c7f109b3d3264a3a984154e90a8454", size = 6889383, upload-time = "2026-10-10T20:04:09.918Z" }, + { url = "https://files.pythonhosted.org/packages/eb/9f/b799dfdce4e05e80ed4bc815c71ff343a11533b2c0ffc221cae8538cda63/numpy-2.5.4-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a813ed7719bf45463c51779e6a98d0385fe905e48447526938a4b8337333d551", size = 15753763, upload-time = "2026-10-10T20:04:12.278Z" }, + { url = "https://files.pythonhosted.org/packages/34/88/16c5f12f86f5ad2817c4d103205131fc6c8acb3d1878af05a1a4f23ec859/numpy-2.5.4-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c9b80cdf5cedba0e90d93fa5f9a333c4d65bd545cd669b71bb97ce2b703c9d73", size = 16757212, upload-time = "2026-10-10T20:04:14.799Z" }, + { url = "https://files.pythonhosted.org/packages/ff/4f/a1fe40e18a898e6a5089f4f0d891f0a493eb0574d5b34458f0fbe5aa3e5c/numpy-2.5.4-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:2199ed071f460487c8db2c0e5c0b564494190edb4772fe80f9aad88b2604def5", size = 17116471, upload-time = "2026-10-10T20:04:17.58Z" }, + { url = "https://files.pythonhosted.org/packages/aa/46/e923a11c78e65c1722e7aaad817c06bd591324174b9d28ce5d31eee4d432/numpy-2.5.4-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:64f9c9878c1938476365e11ccfb6b770f3b9e5f045ccddc514235041e6959365", size = 18524063, upload-time = "2026-10-10T20:04:20.365Z" }, + { url = "https://files.pythonhosted.org/packages/5a/fa/84ab064514440c1f64a1b21088f2c82756defdd05e07c75ab233899565b2/numpy-2.5.4-cp314-cp314t-win32.whl", hash = "sha256:64d1c8ac28a4077cf987e0a71a7a0ef7e2df70722f07f0baa42dbb7eb6938647", size = 6340926, upload-time = "2026-10-10T20:04:22.865Z" }, + { url = "https://files.pythonhosted.org/packages/7e/7e/6cd886876f435b10685db9b9f7eeb70356f99e052116f4e5f11c5792c714/numpy-2.5.4-cp314-cp314t-win_amd64.whl", hash = "sha256:067374eb538c34c745436365cf7b0112595c1d326f21ce4ff340f61230239fbb", size = 12901584, upload-time = "2026-10-10T20:04:24.99Z" }, + { url = "https://files.pythonhosted.org/packages/38/1b/3c1684f6a06f7307f2335fca6e486cb162847fb97e91d65f8eb5cabad213/numpy-2.5.4-cp314-cp314t-win_arm64.whl", hash = "sha256:e94aef2c639da4a960ad0db8e06471208d8589974953d78b61d345b4eb99e394", size = 10891152, upload-time = "2026-10-10T20:04:27.52Z" }, + { url = "https://files.pythonhosted.org/packages/08/f4/3224deff3af2bef6bc0b175369698d8cb348f3d91d9bb0286cd5c9eae9e0/numpy-2.5.4-cp315-cp315-macosx_10_15_x86_64.whl", hash = "sha256:8dddfbee2e68d26d0d7d7d9cb247b1fd4409241cce32d815a11d97ec2cfde179", size = 17003231, upload-time = "2026-10-10T20:04:30.021Z" }, + { url = "https://files.pythonhosted.org/packages/be/75/fee0b8c6d94b44b2fdfae74f6a4ad5a138739589a8aebaec28ce4e713ed5/numpy-2.5.4-cp315-cp315-macosx_11_0_arm64.whl", hash = "sha256:81e3420b27048b65eb14c3acf0c174a8cb0e023277716110347d2dcb26026dad", size = 12018300, upload-time = "2026-10-10T20:04:32.519Z" }, + { url = "https://files.pythonhosted.org/packages/47/c0/d0b335a499a04b65f532c3f034346ef390f81299060f928492dabc1e0272/numpy-2.5.4-cp315-cp315-macosx_14_0_arm64.whl", hash = "sha256:0b4724a19de67bea8cfc4970798efa78bcbbe2ac2613cfac16721a42d44de2a5", size = 5454250, upload-time = "2026-10-10T20:04:34.943Z" }, + { url = "https://files.pythonhosted.org/packages/5a/0e/461b3783c03d668052e6a21b01b673db6ffcb7831fd32d9aa5368c1cd426/numpy-2.5.4-cp315-cp315-macosx_14_0_x86_64.whl", hash = "sha256:2132418bf8dd124a427ca9e6a1daf9ee1a87185344c95119ceae868b99466da1", size = 6789644, upload-time = "2026-10-10T20:04:37.258Z" }, + { url = "https://files.pythonhosted.org/packages/b3/02/5dad269b02166965a7b4ca14adaddd75dbee0de42435bfecf561b84ba5a6/numpy-2.5.4-cp315-cp315-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:325518d4245b9e331387702aa58c2ce1dc4cdcbb41dfb4ccd5dcbc7e08db1266", size = 15704353, upload-time = "2026-10-10T20:04:39.616Z" }, + { url = "https://files.pythonhosted.org/packages/93/3a/01360c8036822ed9f7aa32189a77d1476567ec1e8e1383522389e4faac45/numpy-2.5.4-cp315-cp315-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:56733449d2544178beaa4545cee357370440cf056c197f9c7bfb19dbfdd0e86d", size = 16718648, upload-time = "2026-10-10T20:04:42.383Z" }, + { url = "https://files.pythonhosted.org/packages/7d/5c/b863a2c093c4d6f21a597fcaf24ead0835c09ab16a8312d5a5a8868af683/numpy-2.5.4-cp315-cp315-musllinux_1_2_aarch64.whl", hash = "sha256:5ec3753760c1a6d8bb91200666e545c3a9728e6269dfb5d6ce02340996698aa3", size = 17059053, upload-time = "2026-10-10T20:04:44.976Z" }, + { url = "https://files.pythonhosted.org/packages/0a/60/ced4f57f9a1258a0af74f17cb0b0c2700b5c67cd6678823c803b263e4df3/numpy-2.5.4-cp315-cp315-musllinux_1_2_x86_64.whl", hash = "sha256:b1185012870173de7ae33d370bd45b1cf5baee747ea4b97036b65f4e93016877", size = 18477406, upload-time = "2026-10-10T20:04:47.863Z" }, + { url = "https://files.pythonhosted.org/packages/f9/bd/0ef22dafaafcc7d4bb3ca26b8d2afbd55dedad8eaba99a8c864e1997456f/numpy-2.5.4-cp315-cp315-win32.whl", hash = "sha256:298eca75243f2cbbfdb460560b9fb2a1792a33cf2ab4286efd43d92e8d3df508", size = 6185133, upload-time = "2026-10-10T20:04:50.467Z" }, + { url = "https://files.pythonhosted.org/packages/50/bc/d2651b155ecc608a77e6f4d15495c11f14f19bb98f8bf0c5b0d38f86dda1/numpy-2.5.4-cp315-cp315-win_amd64.whl", hash = "sha256:332f3378fe077dd850e677ec01bdcc4f22368fb5d50ef10b2c79230b1bf5a592", size = 12703085, upload-time = "2026-10-10T20:04:52.63Z" }, + { url = "https://files.pythonhosted.org/packages/dc/d2/45e404f8abb26fb9eda12b94012936873e827b1be76f2ee7890be128312e/numpy-2.5.4-cp315-cp315-win_arm64.whl", hash = "sha256:d4cccbbc78717966f764cd3af4fb70276fa01fc7a2688af11c78901fa5c04f05", size = 10801451, upload-time = "2026-10-10T20:04:55.677Z" }, + { url = "https://files.pythonhosted.org/packages/c6/c3/2ae14e09cfdb67dc187a342e15308a21c15bf4d2071f8079e6aee5fe56dc/numpy-2.5.4-cp315-cp315t-macosx_10_15_x86_64.whl", hash = "sha256:950ea81d57ef070665581b6e1b5f6a029306423cd1739c5b95fe78aa30db6b9d", size = 17097121, upload-time = "2026-10-10T20:04:58.403Z" }, + { url = "https://files.pythonhosted.org/packages/f5/cf/305ae624ef8a039414317224abe9ec9c2fe7ea3c2e1cf204d43ff6b2ffb9/numpy-2.5.4-cp315-cp315t-macosx_11_0_arm64.whl", hash = "sha256:c05ede731b03fb1b7591faca9389ade3267d2bddf1ad8882bb3f2cc5e101694f", size = 12135439, upload-time = "2026-10-10T20:05:01.65Z" }, + { url = "https://files.pythonhosted.org/packages/a9/a8/f75c63813aef95827bb2c0d13b12803016853056e8792c280058cdbfe783/numpy-2.5.4-cp315-cp315t-macosx_14_0_arm64.whl", hash = "sha256:5fbf7141bbfd63aea22f435c9062a032b9ea0082fe9845dad7f021d3f1234e71", size = 5571451, upload-time = "2026-10-10T20:05:04.135Z" }, + { url = "https://files.pythonhosted.org/packages/6f/0f/f17763f983868b5c49b4101ebd7e00760bd1769478a6bb6a8de6e085bbac/numpy-2.5.4-cp315-cp315t-macosx_14_0_x86_64.whl", hash = "sha256:3573cd22564692a5b899ec344e5d5b9cc4576f2985b96f22af3564ed54f2710f", size = 6883356, upload-time = "2026-10-10T20:05:06.249Z" }, + { url = "https://files.pythonhosted.org/packages/67/a7/8af04c5a79e047996cfa38854dcfbececdd0343a7c933a46fdd03ef6f5da/numpy-2.5.4-cp315-cp315t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6c109eac9cd439193678f69d70733c1108487546ca8eafc107b510ae10c1aecd", size = 15750991, upload-time = "2026-10-10T20:05:08.376Z" }, + { url = "https://files.pythonhosted.org/packages/57/7a/648254290d0c504faa8f2d07aa206660c728802c781a6f3fc68ab7cb5d71/numpy-2.5.4-cp315-cp315t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:80d6ef6e8620eb2c2b4c4caad50b5935d6db3cde2d51581b55dcc79e14016d1d", size = 16757675, upload-time = "2026-10-10T20:05:11.393Z" }, + { url = "https://files.pythonhosted.org/packages/b8/fe/4a8c3cdb0c70400cfe4c5bec42d3099a5673802a95064614b33e07b82aa1/numpy-2.5.4-cp315-cp315t-musllinux_1_2_aarch64.whl", hash = "sha256:77045a4b175bbf5316ec08003880804336c78f92281a1b72222b274ea85ec5ac", size = 17113846, upload-time = "2026-10-10T20:05:14.49Z" }, + { url = "https://files.pythonhosted.org/packages/1b/7e/619692bb67778702c0e9eb2d468568a7573f4e269386ea61aed01ee4e557/numpy-2.5.4-cp315-cp315t-musllinux_1_2_x86_64.whl", hash = "sha256:0f02a46e49cfb6c73bdb7aea1c0d3461dbae9aba613542b65f657cd3d17b9fab", size = 18522915, upload-time = "2026-10-10T20:05:17.33Z" }, + { url = "https://files.pythonhosted.org/packages/b7/b5/4da41c328788f575838f97a098fe8ca691ebc6f6fd73ad4a262ee40b184d/numpy-2.5.4-cp315-cp315t-win32.whl", hash = "sha256:ad62a416ddcf863bf44bba76fbf6b53366ab0692e294f51cae4b5fbe0d246788", size = 6335804, upload-time = "2026-10-10T20:05:19.921Z" }, + { url = "https://files.pythonhosted.org/packages/98/94/6482ddfa3d312490cb9358f375bf2ad56427dbea8769187158e94d653753/numpy-2.5.4-cp315-cp315t-win_amd64.whl", hash = "sha256:38f47be9f74ab870d2633b5456ae519c43758a8d1fd05342f0ce4ecc034396ee", size = 12890095, upload-time = "2026-10-10T20:05:21.875Z" }, + { url = "https://files.pythonhosted.org/packages/48/7f/c2d1b436b6e7cfebac140c2579a298344b85f2991a2ce5c3615cefb29400/numpy-2.5.4-cp315-cp315t-win_arm64.whl", hash = "sha256:7a14a461d9340f1b46b8648578aed9cdb8b3b018a8fac6c1dde2c9192a01a87f", size = 10883718, upload-time = "2026-10-10T20:05:28.547Z" }, +] + +[[package]] +name = "packaging" +version = "26.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/7d/fa/3944b40b07da9ce895c0e6303a5ab7d53da063554f534556b134a54d6093/packaging-26.3.tar.gz", hash = "sha256:94edc256424af38762eb31306eed28beb9f0efc50a8837492c9d6fd6004aed79", size = 313412, upload-time = "2026-08-04T18:15:28.737Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/63/34/ba1c580383c9eada3711951fef0795c80b829a078d72188184bcab9dd527/packaging-26.3-py3-none-any.whl", hash = "sha256:d7193f7c8e4e93f444fde0262bf90af30e16fa0ad0ad44cb553c87339b23cd1c", size = 129956, upload-time = "2026-08-04T18:15:27.159Z" }, +] + +[[package]] +name = "pluggy" +version = "1.7.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/bf/db/7fc19e6f2dc92a966727031389fc2e08b558f0f25eb7403c1119ad4713cd/pluggy-1.7.0.tar.gz", hash = "sha256:d1eaa46ebb595891b860ab086b4d09c8588af65ebd4361b8e8f4bb8920b90ba8", size = 123304, upload-time = "2026-10-15T09:50:58.343Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/40/9e/2b38731e0fc536806f16490e1a12d7f0dc2a1235aa8cc07bcc75416a7daa/pluggy-1.7.0-py3-none-any.whl", hash = "sha256:7dd7b0d8832ba3cb632c306926ded123429211b83641b35dc5c41ad2d34f9bec", size = 27082, upload-time = "2026-10-15T09:50:56.808Z" }, +] + +[[package]] +name = "pygments" +version = "2.21.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/49/2e/ced460408999b33da6b31b0021b0f37d329e202d4169aeb164493778f25b/pygments-2.21.0.tar.gz", hash = "sha256:610ca751c9bc2492b38eb9a38a7fbc93edbbb2d7182edaf34e66ae493dee5c8c", size = 5005329, upload-time = "2026-08-17T08:02:48.824Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/71/46/17f022dd3e953bf20a04a028a21ec746d942f8d2af30fa0f124fa0e6a684/pygments-2.21.0-py3-none-any.whl", hash = "sha256:2363c69b61c4a97c838da3b130dcd6468f4848992b21a82f2a63ec34377137d9", size = 1250147, upload-time = "2026-08-17T08:02:44.912Z" }, +] + +[[package]] +name = "pytest" +version = "9.1.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "colorama", marker = "sys_platform == 'win32'" }, + { name = "iniconfig" }, + { name = "packaging" }, + { name = "pluggy" }, + { name = "pygments" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/e4/47/b9efed96c114afcfa3c9d3fe98a76a1d14c74a9e266d397cf6eb64be5e01/pytest-9.1.1.tar.gz", hash = "sha256:1088fbde8f2b49d95a549a195707afa7a76a3ce9bcadc26b6d71f0ffda5fe313", size = 1636369, upload-time = "2026-06-19T10:58:32.857Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/24/25/1de2678b631f5a49215c6c96fff41ba892b0a34df68d6d80292b1b48aa7f/pytest-9.1.1-py3-none-any.whl", hash = "sha256:37a86b45efb9a47a61a36449063e8e18d0cab3161329fc099eb21783169c4f0c", size = 386536, upload-time = "2026-06-19T10:58:31.347Z" }, +] + +[[package]] +name = "referencing" +version = "0.37.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "attrs" }, + { name = "rpds-py" }, + { name = "typing-extensions", marker = "python_full_version < '3.13'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/22/f5/df4e9027acead3ecc63e50fe1e36aca1523e1719559c499951bb4b53188f/referencing-0.37.0.tar.gz", hash = "sha256:44aefc3142c5b842538163acb373e24cce6632bd54bdb01b21ad5863489f50d8", size = 78036, upload-time = "2025-10-13T15:30:48.871Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2c/58/ca301544e1fa93ed4f80d724bf5b194f6e4b945841c5bfd555878eea9fcb/referencing-0.37.0-py3-none-any.whl", hash = "sha256:381329a9f99628c9069361716891d34ad94af76e461dcb0335825aecc7692231", size = 26766, upload-time = "2025-10-13T15:30:47.625Z" }, +] + +[[package]] +name = "rpds-py" +version = "2026.9.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/42/68/3bd46b8a5e01d3c2ebdf9c5e9497912e3fe0cde02bac21a7130ca866e403/rpds_py-2026.9.1.tar.gz", hash = "sha256:4793ef7f78268b124b73fa933440f01d258bbae01de9fa53e9080c9ab0425a12", size = 63948, upload-time = "2026-10-04T16:32:36.469Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f5/6d/ddb8dfcf41892ba9c672d27988592c038d30db61708230313831b6780a1b/rpds_py-2026.9.1-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:2711d29b653b3bce48a63d18b9c6b53274669e6d6c4094dddeb4d9a0e45128b2", size = 344947, upload-time = "2026-10-04T16:28:55.207Z" }, + { url = "https://files.pythonhosted.org/packages/9e/b2/632c3d034961149eff606e3e357547620a9ab37b849ae5815f67838753fd/rpds_py-2026.9.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:3231c4c0e521dafa5be0c9f114ee2c2ad46650836f2d72caa86801950c3e7044", size = 346770, upload-time = "2026-10-04T16:28:56.884Z" }, + { url = "https://files.pythonhosted.org/packages/6d/45/4df346410bb873d03ec60c3b891d1fceb8cabde1971d29844bbb625cc296/rpds_py-2026.9.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e01b3c878c8641913e688edd1b3f08658c6783d29cf6b826bd3c0d1ae7a1ffaa", size = 377795, upload-time = "2026-10-04T16:28:58.193Z" }, + { url = "https://files.pythonhosted.org/packages/ba/b8/a70f302207e4f29f873c01a986fcac5def49b9c8224328cef1ad7da6b7a9/rpds_py-2026.9.1-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:3e524c7874ac72884d28e16dd5b8d839fd09e0fe76b020d3fbca23212a7b8c52", size = 383137, upload-time = "2026-10-04T16:28:59.411Z" }, + { url = "https://files.pythonhosted.org/packages/13/e3/3aa28a48a01169d0e20728bf0e5ebbea3ae2774285b40853ae49b3ef332a/rpds_py-2026.9.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:761fdae6728ceb99ab182fad2f0cc1e262f610834dc891aea1d1a2a2e634776f", size = 496789, upload-time = "2026-10-04T16:29:01.011Z" }, + { url = "https://files.pythonhosted.org/packages/3e/a7/414e8d088602d66ad6f47c65bf07fe5090992a8574cdd4571c1a83b647ed/rpds_py-2026.9.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:6b723eb406dec5bc9ec516c73ab9c3239a3284e017f7eb89ee2b3258bd504fb7", size = 395929, upload-time = "2026-10-04T16:29:02.465Z" }, + { url = "https://files.pythonhosted.org/packages/2d/5c/aac3b960fd55d37b49f77c0eebbe043eaf36762ebd48e7b46951e59c1c05/rpds_py-2026.9.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:136a1c3fe4402b7008bc81cb62ee538481795b61a7e83df88dff3b3f02b726ff", size = 375426, upload-time = "2026-10-04T16:29:03.944Z" }, + { url = "https://files.pythonhosted.org/packages/d3/80/77987a41c40be7202a78d9aed4a650bfb05792237cfd3b9e22638d990fe5/rpds_py-2026.9.1-cp311-cp311-manylinux_2_31_riscv64.whl", hash = "sha256:839dde845559254f34885267c6878f60d61d5205180226d976fe488d45fa128e", size = 381497, upload-time = "2026-10-04T16:29:05.308Z" }, + { url = "https://files.pythonhosted.org/packages/b8/d0/640b02eb1ecd71a913c1178b6235287b31006266bca9a26862e80e497b54/rpds_py-2026.9.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:56cd8b3f77d7b6812f533b662186a1f28316931166ddc00fb893b1b0db7e9888", size = 405740, upload-time = "2026-10-04T16:29:06.578Z" }, + { url = "https://files.pythonhosted.org/packages/3e/8c/d034195d45d215625a23f2ae85dbbdb86a49a93a51272061c97c80b9d316/rpds_py-2026.9.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:074a4d198bc34d9a8ea425114fc3ded6d11ec01f6a314a8db67454a5152d8834", size = 555850, upload-time = "2026-10-04T16:29:08.115Z" }, + { url = "https://files.pythonhosted.org/packages/de/f8/532f1addfc86207bc3cf23749638bb0d457bce891595012ed8a08d0543b8/rpds_py-2026.9.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:6beb738155fe8ab8091afdfa5a3226b21c2b1593f1e50ebb90eb25b44dbc0391", size = 618661, upload-time = "2026-10-04T16:29:09.554Z" }, + { url = "https://files.pythonhosted.org/packages/50/5b/b0f2c9833a36a5b1f6ac5cb0e635fabacf713266f46ceefba2102da0bf51/rpds_py-2026.9.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:57492a550a1d88d29d003247e5f78dd8cf04a701fac0e4c8db8745a6d2504e0a", size = 583530, upload-time = "2026-10-04T16:29:11.072Z" }, + { url = "https://files.pythonhosted.org/packages/bf/20/464ed4b98d3c8c6a334f3b5e24d3886de04b05658a95fc40a37e0e6e4775/rpds_py-2026.9.1-cp311-cp311-win32.whl", hash = "sha256:d95a354e02393eada6d7351184671aced9d4cce109dabf927cb7aa99624352a1", size = 208125, upload-time = "2026-10-04T16:29:12.408Z" }, + { url = "https://files.pythonhosted.org/packages/df/c9/384a9b62f8cd89c8e3e609b8c9052c140e8c01d807261e0517c904b8bf29/rpds_py-2026.9.1-cp311-cp311-win_amd64.whl", hash = "sha256:ce4d4f52e2a4324396caddbd45a97d8d7be5f42edd25d2355282a9c34f9b2f7f", size = 226045, upload-time = "2026-10-04T16:29:13.751Z" }, + { url = "https://files.pythonhosted.org/packages/4f/3b/0a691f0dcb2301fa9c736346da2d0d4991778373283301c5c12b8f952f3d/rpds_py-2026.9.1-cp311-cp311-win_arm64.whl", hash = "sha256:fdcd198979b4ecffcc1beba366a7fbcf4eb41243691a82fe52ceb0b902f09c12", size = 222636, upload-time = "2026-10-04T16:29:14.989Z" }, + { url = "https://files.pythonhosted.org/packages/5d/34/a828586ea3329fbb50895b50e9cf98ca3d924a9f41a0b26d443bff1b3794/rpds_py-2026.9.1-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:50906f5aea24b5a865cbd0a589698288631d9f3a54c3a937c83aefa95a0d14af", size = 346822, upload-time = "2026-10-04T16:29:16.258Z" }, + { url = "https://files.pythonhosted.org/packages/90/81/ac6a0d064982251856ce009c9e1dd51a34110b3c055aa7d1aad18b4899a3/rpds_py-2026.9.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:e21c1429e205828ea886a2293a4a2c8e01f4c25d9893ca330e97a6cf73f52e7b", size = 341380, upload-time = "2026-10-04T16:29:17.706Z" }, + { url = "https://files.pythonhosted.org/packages/42/ff/bf7d54f362748fd6a49277b9d6531c394074110fedf50ad791ec59133fbb/rpds_py-2026.9.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2693b2728bbcc48d09a981a356954b0c47c53ff25b545856f28a889ea619f69a", size = 373053, upload-time = "2026-10-04T16:29:19.063Z" }, + { url = "https://files.pythonhosted.org/packages/60/de/74b0ccdbbd28687b9b5fdb34c1cabed88352182facaced9d6f5486b8b9ed/rpds_py-2026.9.1-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:8601470267d938bcb7f3ab1a336100af51a4fd5b6ed030ef52461bb3ef5e7e07", size = 377742, upload-time = "2026-10-04T16:29:20.543Z" }, + { url = "https://files.pythonhosted.org/packages/c2/6d/b979775a3057b2a5c26d75ecbff60a20a24ef081db6dd76836fca3f20247/rpds_py-2026.9.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3890a6aa36e6baa53d5258a2a25d3ef8b37ad165a6ab27a892d7c3e3a432cd69", size = 487289, upload-time = "2026-10-04T16:29:21.958Z" }, + { url = "https://files.pythonhosted.org/packages/1d/5d/7c34734ce3ece943d9d6ee0122a74a21bbc75b47acb5b7053c83f6efb1ed/rpds_py-2026.9.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:6b5b393eda5ea42cca1c1a6665f2a4882b4fd5d1777e41ce0545a107fb008c9d", size = 390710, upload-time = "2026-10-04T16:29:23.378Z" }, + { url = "https://files.pythonhosted.org/packages/1d/6d/b26eb1e75395925b3a142ed351cbed2ec8212f5c9d937aee3df32c701baa/rpds_py-2026.9.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:addeda51556dac7c1a2f14cda62db8b621cd12afba3091d03a96c72932387eab", size = 371261, upload-time = "2026-10-04T16:29:25.1Z" }, + { url = "https://files.pythonhosted.org/packages/7c/98/b2fdfe10301a9e27af21e634337fbba7baac0bc17fe44619a17c26bba2cb/rpds_py-2026.9.1-cp312-cp312-manylinux_2_31_riscv64.whl", hash = "sha256:d9edf30457d74eebfd76b045535e36f1cd89062566a128a0db2145ca042d787e", size = 376075, upload-time = "2026-10-04T16:29:26.748Z" }, + { url = "https://files.pythonhosted.org/packages/c5/b1/c4b8d954e49c3c69cf8063c0c9e0919c99f3d2cecc46311606a1cbe835dc/rpds_py-2026.9.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:815d26356930846a40c7bc1366e7b1b0320ab8a063e66c11298a208bed0fd237", size = 399358, upload-time = "2026-10-04T16:29:28.282Z" }, + { url = "https://files.pythonhosted.org/packages/b3/59/9559a7293c97dff0cbb293efffbd36644103883ecc27741af8ef90c84ca8/rpds_py-2026.9.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:3b5a6f40f0a1486b4b36c888123afc67acdbd9f33235927acf5ff295429a0ba3", size = 550267, upload-time = "2026-10-04T16:29:29.851Z" }, + { url = "https://files.pythonhosted.org/packages/6c/9d/6dc60e49511de4c8b00e74f9c1d43aed4d27f712cdb1ade92f2c199cbf64/rpds_py-2026.9.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:b5b8b0753718d258fd454283fbd57e14545d3b40583fa672e27cb4f987626bcc", size = 614018, upload-time = "2026-10-04T16:29:31.54Z" }, + { url = "https://files.pythonhosted.org/packages/fb/bc/92a4ecf27301b886232a413360f6b348a81d55ffef654f2ebda5970ebc3d/rpds_py-2026.9.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:46d80bc76b51a6c24f9944368c28d38b8bcbcea1da4f2f8d3ebc31a67e8c6ec6", size = 578509, upload-time = "2026-10-04T16:29:33.01Z" }, + { url = "https://files.pythonhosted.org/packages/08/53/efb97f2e6589b7ab8394385a73870e0b2ab2a195281c117b21fd6d7d4855/rpds_py-2026.9.1-cp312-cp312-win32.whl", hash = "sha256:befc2d6a953e563f8a7bfd87a42c22ebf8a3e980dcb7b6a4d17b70b0e914e8a3", size = 205401, upload-time = "2026-10-04T16:29:34.451Z" }, + { url = "https://files.pythonhosted.org/packages/1c/84/1700cc748d0eaa747486e4e82882d2575224e6e9a3148df583058850c629/rpds_py-2026.9.1-cp312-cp312-win_amd64.whl", hash = "sha256:5ce8943f79c2210f7abcc28e86367b03b28d95027fd01c46d2472373ae70c86f", size = 222923, upload-time = "2026-10-04T16:29:35.709Z" }, + { url = "https://files.pythonhosted.org/packages/c3/89/0302215373c2f8b4408b0fdfee955368cc378a98ff59508d47a7e0dc2dbf/rpds_py-2026.9.1-cp312-cp312-win_arm64.whl", hash = "sha256:501909f2e4a1e2dee528ef766fe3c469060ebc17e54a8383d404ba07a81a6f02", size = 218415, upload-time = "2026-10-04T16:29:37.263Z" }, + { url = "https://files.pythonhosted.org/packages/83/ea/ee88fd9e756ff93fb6b1182a47ec09504a242620e33ce1d20679efefe841/rpds_py-2026.9.1-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:a36b70596407634ca82d4b989a3729074a008537a0522e4c8046a67c729103e9", size = 346229, upload-time = "2026-10-04T16:29:38.82Z" }, + { url = "https://files.pythonhosted.org/packages/57/71/a097d6552f837500fc36e6b23d09cfb9890c3cc47531f9ca64e149799615/rpds_py-2026.9.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:eba5d173f7d5708b22a93815017a4611873ed54db9f268077c0dd1ed99cfc858", size = 340514, upload-time = "2026-10-04T16:29:40.405Z" }, + { url = "https://files.pythonhosted.org/packages/bd/b7/497e85768bf4e0d8ddbaa096a4cac31d1509251dee2728a8490aa367e0b5/rpds_py-2026.9.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:457866b85daf5034296666168b84a69e0b2e89dc4f1af102b46f6448a60b9063", size = 372611, upload-time = "2026-10-04T16:29:41.778Z" }, + { url = "https://files.pythonhosted.org/packages/52/4b/74ab4108916250b6e198e0d3af05bc6835eb046315f22f7a0ceb49667c5a/rpds_py-2026.9.1-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:a3a52a3ba86436ab3aef510fbe21512abc2ddd1993005dfe50514bd2284ef025", size = 377712, upload-time = "2026-10-04T16:29:43.242Z" }, + { url = "https://files.pythonhosted.org/packages/0c/8e/067e77d9d7b3cc793c9d909b7e97e7aadbbb1fb094093b6876902cc96d38/rpds_py-2026.9.1-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d7841166b7fa64c9c56404617ae4341448847482d45933b13135d26c130519e5", size = 488106, upload-time = "2026-10-04T16:29:44.692Z" }, + { url = "https://files.pythonhosted.org/packages/3c/b4/c5aae6c2dde269bf955f6b7d35065c655a57e47750d9668052ad67e74dda/rpds_py-2026.9.1-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:926bdd3e3b5998ddf70cc64bc8cf57209571f9044542913afb673799fec77dd0", size = 390795, upload-time = "2026-10-04T16:29:46.129Z" }, + { url = "https://files.pythonhosted.org/packages/a0/36/76fab39973ee11e7f9f357c55138197bb01c86f6502cb76487e3b4f42db0/rpds_py-2026.9.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7868b85224291c6cb6759f9b5adb9745f486d226f62b16a614dd5a2a5ab2b35b", size = 370794, upload-time = "2026-10-04T16:29:47.603Z" }, + { url = "https://files.pythonhosted.org/packages/3d/fe/cd2a80e6d7b871937a60e935c5d507aa390d143f4ff3636f640b9733d5df/rpds_py-2026.9.1-cp313-cp313-manylinux_2_31_riscv64.whl", hash = "sha256:3cd182d7291d29b92c521a0069d9c01ba6193628a9a105531d11b40a6d731a33", size = 375673, upload-time = "2026-10-04T16:29:49.223Z" }, + { url = "https://files.pythonhosted.org/packages/6c/18/7464a9953724e55a3b3206062fa0ffdeaa519584c6aabf65d3956d94f131/rpds_py-2026.9.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:e6ea1cda8d8c688278430e4268a42f5e5da3bdd74578dfadc0820c3f1766ce83", size = 398758, upload-time = "2026-10-04T16:29:50.601Z" }, + { url = "https://files.pythonhosted.org/packages/c0/86/1534b436700fd49ff411063b7c4d7e938adfabf90895b6cf1622d5a7d1f6/rpds_py-2026.9.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:5943980471829f6de242a20b109de3111ba6b77e3af0ffc587028ac854b05e6c", size = 550370, upload-time = "2026-10-04T16:29:52.002Z" }, + { url = "https://files.pythonhosted.org/packages/57/1c/e1fa82a8a01e3c5820f3ba98a8b2673f642128eb368fa88871b01dd2c909/rpds_py-2026.9.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:76d3af9732d2dab69f28179b40ba2d87e2f1d5824b4a694780aa787d685e8f36", size = 613106, upload-time = "2026-10-04T16:29:53.655Z" }, + { url = "https://files.pythonhosted.org/packages/29/55/b20b8c4c3dde8755bfcd5b08492a02d0cd2e26929aedf6199ca2a377d42a/rpds_py-2026.9.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:78326f4cb4427a56ba4996c0762b63be45f06b85f086526420d2b3a66e40f84d", size = 578235, upload-time = "2026-10-04T16:29:55.157Z" }, + { url = "https://files.pythonhosted.org/packages/55/42/df3f7bbc3f7ab37a8a9db8d6c2ff2c985422899f1f7926afbf7ec3c0b8b4/rpds_py-2026.9.1-cp313-cp313-win32.whl", hash = "sha256:172e47169583f46ce118cbec68e6795d0da0f4606b488b6434f8276bca0a058c", size = 205293, upload-time = "2026-10-04T16:29:56.669Z" }, + { url = "https://files.pythonhosted.org/packages/31/9c/ba5a9569d719bfdd6ce863df4133ac6a1658cf1b07cc3534c31db729fbbc/rpds_py-2026.9.1-cp313-cp313-win_amd64.whl", hash = "sha256:3e93b2cd69a9830be33e03945cd7cda940a0a8bfcfbff41d6144f0cb0d3d8bd9", size = 222409, upload-time = "2026-10-04T16:29:58.049Z" }, + { url = "https://files.pythonhosted.org/packages/35/72/f28ca566f6c23c35bbf7445f65eb0364577b25a026305995e24f78b83d94/rpds_py-2026.9.1-cp313-cp313-win_arm64.whl", hash = "sha256:d151e148117294133bf8af7eeace085e7e87432db15ab6adf640330298a47f6f", size = 217681, upload-time = "2026-10-04T16:29:59.449Z" }, + { url = "https://files.pythonhosted.org/packages/8a/f2/67b94be1532767803415c1c5a1fd88ea487643d74a673cec1ba140af77bb/rpds_py-2026.9.1-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:c9d1aca01f49170fdcf5c92761b1fafe97f554b721ca4570c5949fff778f0d4b", size = 347178, upload-time = "2026-10-04T16:30:00.865Z" }, + { url = "https://files.pythonhosted.org/packages/04/37/b751de2b59b0197a1d92a5dd491de88e8a5e928c2e6562581974f1e85263/rpds_py-2026.9.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:3f0e9ac28fc067d4d34b88ae43c48e9489455c97fee9633d851f7eeed5a05d35", size = 341878, upload-time = "2026-10-04T16:30:02.564Z" }, + { url = "https://files.pythonhosted.org/packages/72/e2/5873bc4643c250db9e05d48dc0c93763d4aa68bc3b81164cb1af3b45b284/rpds_py-2026.9.1-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:07deecbfce94c78473018bc7d10b337cc651d12df87a1eb2cb3e4024bc9c33d0", size = 373963, upload-time = "2026-10-04T16:30:04.026Z" }, + { url = "https://files.pythonhosted.org/packages/51/03/5acf7632158247f3f6386ff0af3a1ee48167d575037e8d0920594b76d92b/rpds_py-2026.9.1-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:821b2755db9194409254012f429c56643416fb96ef9be090be82ec8826b7f477", size = 377975, upload-time = "2026-10-04T16:30:05.555Z" }, + { url = "https://files.pythonhosted.org/packages/e6/00/63fda451b8bffa5808fc8bb311ee7c073b340974b09f273c7b2a145d3d62/rpds_py-2026.9.1-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3c91c210ae7645626c608400e3519b4a642f837cce09ca830db3beb2e9f274d4", size = 490347, upload-time = "2026-10-04T16:30:07.156Z" }, + { url = "https://files.pythonhosted.org/packages/a1/ae/c093ffd070ba0fb02f76c565d06fecc65ad6e4afdbae78f7031076d3cdac/rpds_py-2026.9.1-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:54ac2158a6f96cfbabff0b2eedaf94b90c5ec7ca8317fcadc61e1c2b2e0ff6ef", size = 392962, upload-time = "2026-10-04T16:30:08.77Z" }, + { url = "https://files.pythonhosted.org/packages/22/9d/d08a1128ab199b2f0cf25bfeb0639bd05119fff4b7c47bec24ef9a8ec23f/rpds_py-2026.9.1-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:eac2f5dbafd585dfe31f86a23ebf0d3ba480a9d49ebc87947267b5608d4ea0cd", size = 371623, upload-time = "2026-10-04T16:30:10.501Z" }, + { url = "https://files.pythonhosted.org/packages/53/c7/4758ddcbb75609414bbccfcb11d612436f9b3ee821bd2f33f0f1604ee648/rpds_py-2026.9.1-cp314-cp314-manylinux_2_31_riscv64.whl", hash = "sha256:8aa5dda18d39b6143eb24809d158f9252c88f402749b6f1b62a506cc7d96cc35", size = 376997, upload-time = "2026-10-04T16:30:12.124Z" }, + { url = "https://files.pythonhosted.org/packages/87/e4/947bd7f608ff60faf46dc9d389c3dffd0e3d767d78a0be19978448ef0ce7/rpds_py-2026.9.1-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:5c90e7fa02e8f5de0d10c17595c568ada48c5302e749462c0ea1a4c362111a86", size = 400169, upload-time = "2026-10-04T16:30:13.804Z" }, + { url = "https://files.pythonhosted.org/packages/4b/35/fe93e020a0543b5670472c18d7e6af3197c08da571240ce1965c84f85c0f/rpds_py-2026.9.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:e6d198bad4e49dd6732fbd636e2fc5c082f45c8cad0b4acb756b00c82c76072e", size = 551615, upload-time = "2026-10-04T16:30:15.332Z" }, + { url = "https://files.pythonhosted.org/packages/0d/4f/5d2a0136bb03b2a56a39dc6ff92d58a6e3e53a2e17079238b86228882f16/rpds_py-2026.9.1-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:96beca19ec79de272e8668585380ff9092c47077c1d7a1e098e00bbd921f4785", size = 614655, upload-time = "2026-10-04T16:30:16.92Z" }, + { url = "https://files.pythonhosted.org/packages/09/1c/3f1025aaf70d9bf7272cc41f8b64ee76b48bf01726248138430e16f23b38/rpds_py-2026.9.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:a5cf77eb04f20b720be95265a3e00eb2a14814074255cc27069c551b2db53118", size = 579602, upload-time = "2026-10-04T16:30:18.555Z" }, + { url = "https://files.pythonhosted.org/packages/53/0d/5c72e6204f76610608706da32b6b7e11ef7e10317558a7bb15a122e008dc/rpds_py-2026.9.1-cp314-cp314-win32.whl", hash = "sha256:a03d57b86d2a51d0a66c92177e2be154ad015f357791d306e714569999cdb4cc", size = 206315, upload-time = "2026-10-04T16:30:20.05Z" }, + { url = "https://files.pythonhosted.org/packages/a4/0b/489d48abbcc7d70cf3fbf662d9d22abf1f4650761c0a9ae05260800800d3/rpds_py-2026.9.1-cp314-cp314-win_amd64.whl", hash = "sha256:837c6b305e26fe0f75b15c92cf3b2ba29e0ae19dc40b1c557b026cb426347d0c", size = 222852, upload-time = "2026-10-04T16:30:21.604Z" }, + { url = "https://files.pythonhosted.org/packages/91/16/bbb05a7e6a10cf79ba639be7f799d770ee15f64175cc61d081b218dd402a/rpds_py-2026.9.1-cp314-cp314-win_arm64.whl", hash = "sha256:fce4b85234a0cbad67bf8e6e1201ee815d172c9aebad75f25645bc4d834f8e31", size = 218709, upload-time = "2026-10-04T16:30:23.036Z" }, + { url = "https://files.pythonhosted.org/packages/22/ac/ac507a0a4ec478ca470440a09583db4be5259ba7670aeba0620822f1e57a/rpds_py-2026.9.1-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:3a72c11530d71abfb66c8d7696a2f86c43e63fca8b948f1a784ac490f4ec688e", size = 349596, upload-time = "2026-10-04T16:30:24.558Z" }, + { url = "https://files.pythonhosted.org/packages/e9/f2/817a46b658d5070f477f722c298ee9a24525b0e4017347964146ef5fdd0e/rpds_py-2026.9.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:068c37bba854ec2fe42f7365c640af11dd9895890ccbf2df5070d0c059bd7f96", size = 343114, upload-time = "2026-10-04T16:30:26.048Z" }, + { url = "https://files.pythonhosted.org/packages/6c/42/6ade976b13ac1b4cb3bf2eb603f1be2fe74df19a29988d4c2b386be59d6f/rpds_py-2026.9.1-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d7fca4eb6df565e2a928f1c7dad92d27db8f9df0f449e76423ed5d7e713ed445", size = 374197, upload-time = "2026-10-04T16:30:27.699Z" }, + { url = "https://files.pythonhosted.org/packages/d9/70/77cdf1d3f1a07faabe936016ae623aec7981f73108a8fe7a203ed2e21998/rpds_py-2026.9.1-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:c933c6678c6f116ff8af47a4c6db0868b8ace74af0343016c0ef00f00272ea69", size = 378025, upload-time = "2026-10-04T16:30:29.451Z" }, + { url = "https://files.pythonhosted.org/packages/3f/6b/18a44a3beaa9b7931acb04af7bd9539836477c630a794452d4826d6185d4/rpds_py-2026.9.1-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:028ad274ea951dac64491b5d1e65712a4aeabfdbdb9fccf797b57bd899b0c495", size = 491969, upload-time = "2026-10-04T16:30:30.995Z" }, + { url = "https://files.pythonhosted.org/packages/73/27/fb39cfd6bddaf741b024f813890374578ff8ac1f1adc473659c048b03b05/rpds_py-2026.9.1-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:740d0a99cf9de0b17a3943388e9294a59becf75e7c43421f387bd3c7a9901f7c", size = 393905, upload-time = "2026-10-04T16:30:32.628Z" }, + { url = "https://files.pythonhosted.org/packages/ed/71/0fa7bb77b57af0d710273964180d11b503f62b8a5c358eb2d8c3f62feff6/rpds_py-2026.9.1-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0da298fb372dc192610a4b9ecbc68a0cd8b675bbbd1fc519d01b41cfd658333e", size = 374569, upload-time = "2026-10-04T16:30:34.257Z" }, + { url = "https://files.pythonhosted.org/packages/54/22/f41cfac269af3b449513ef1bc3d7f32fde52abbbd2d01c7e76b47743acd9/rpds_py-2026.9.1-cp314-cp314t-manylinux_2_31_riscv64.whl", hash = "sha256:eb61be926bb81567c1f48bdc8aa22b9855048dc2efd53871f9f7e6e9a5632346", size = 377482, upload-time = "2026-10-04T16:30:35.997Z" }, + { url = "https://files.pythonhosted.org/packages/b4/fc/312b49006e7f8f9ca5f96647577b8aa6f3df30519c46bd448f5c425af0b2/rpds_py-2026.9.1-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:42e75466f83cd43f6026c81eab74246efb2bdadafb307b85700632d06c68f299", size = 400040, upload-time = "2026-10-04T16:30:37.76Z" }, + { url = "https://files.pythonhosted.org/packages/cf/a6/18cca7a878dc7fa95165a83343fd4d7b65643fd22e54e47340a451121d5c/rpds_py-2026.9.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:617f59cde379b4f648a09797b7f683d04b90a46344cddab85639da5aff0f5531", size = 551849, upload-time = "2026-10-04T16:30:39.443Z" }, + { url = "https://files.pythonhosted.org/packages/e4/6d/1f5685e20f39604691bdc3c05aaa6b8bd2f954e9e996477adf2376768e33/rpds_py-2026.9.1-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:3edae8c5ddfdb6985d49ae9d150516e5076888879022f91a26c2de9276ce0bdb", size = 614842, upload-time = "2026-10-04T16:30:41.231Z" }, + { url = "https://files.pythonhosted.org/packages/c6/25/98652109fd9f7e10268dd4571aa52b81987001b806f37ef1a18260de714a/rpds_py-2026.9.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:0f045bb053c9057720d72c56dffe30dffdc05997b2897a827b9325f0ab6623fa", size = 582606, upload-time = "2026-10-04T16:30:43.345Z" }, + { url = "https://files.pythonhosted.org/packages/19/03/11ca09099bab5f53373a80a334c424ec917c13d050760a59499d2af5171e/rpds_py-2026.9.1-cp314-cp314t-win32.whl", hash = "sha256:bf35d0568abda97233239ce32896d3ad53fccc537832c104e30c94aa5fb93569", size = 203341, upload-time = "2026-10-04T16:30:44.954Z" }, + { url = "https://files.pythonhosted.org/packages/6f/8a/88909e3ffd9f47f5b58211473875d8c3c09079f0058c46fb72c55a702a26/rpds_py-2026.9.1-cp314-cp314t-win_amd64.whl", hash = "sha256:1e8d4d79d828299bf44a55db22a9388ab967b49d17132c88eab0f4360b48da8e", size = 222843, upload-time = "2026-10-04T16:30:46.486Z" }, + { url = "https://files.pythonhosted.org/packages/f6/b7/a662f367d4896dd0a10cef2fc91f10b7f08af1c10858e287e019563f338d/rpds_py-2026.9.1-cp315-cp315-macosx_10_12_x86_64.whl", hash = "sha256:1d77b649e6f7cdf12ca5c2a98dad0ad37f9ea9b6f960408a92f0cb12bb3d04d9", size = 347463, upload-time = "2026-10-04T16:30:48.203Z" }, + { url = "https://files.pythonhosted.org/packages/ae/3f/ad45d03df4f84ebae5439577ee81f3999d182711e82235c8037b6528890e/rpds_py-2026.9.1-cp315-cp315-macosx_11_0_arm64.whl", hash = "sha256:00ba2d8c7dd4ee537978ddf4b3fbd712bef2d8751603f7f3146b3f4287768e25", size = 342051, upload-time = "2026-10-04T16:30:49.872Z" }, + { url = "https://files.pythonhosted.org/packages/7e/31/3dcd68c13d4bcc59c1f7eb33ac8e80698f06f3eb0d8a1e06419836071c20/rpds_py-2026.9.1-cp315-cp315-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ec450527cbf485e13c8d3602a54f428ab0432fdade0ede75efd74b735421c871", size = 374143, upload-time = "2026-10-04T16:30:51.508Z" }, + { url = "https://files.pythonhosted.org/packages/cf/0d/68c1f058a250fbd1380ebda9fc227cbf50117adf8ffe8161ef383ea79f68/rpds_py-2026.9.1-cp315-cp315-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:306ee1850d8105b5baf977e78d45fcadd12c1a54678d614c9baf217708446e91", size = 378604, upload-time = "2026-10-04T16:30:53.206Z" }, + { url = "https://files.pythonhosted.org/packages/d7/d6/2d4c59b85397cb4800594fadf692688ccf5ce556adc930e7a5bf21061a5e/rpds_py-2026.9.1-cp315-cp315-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ef6b65b03247c54692ad4fd9ee97cb772781927db72e3cb05e70b3db6d1ff14f", size = 490790, upload-time = "2026-10-04T16:30:54.925Z" }, + { url = "https://files.pythonhosted.org/packages/da/04/7e05dc3aebaf52f4e026766bd668fdd09a9d0e23f64a14686b36b3501892/rpds_py-2026.9.1-cp315-cp315-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a575404ebc9cf2e91edd32eaf570ec1430eb900d4f56724ba7dd4bc1fc9c176d", size = 393146, upload-time = "2026-10-04T16:30:56.625Z" }, + { url = "https://files.pythonhosted.org/packages/57/ca/e2e9a0a46a74ed51a0498ba1fe10f4ea6b2d9a155f372a2f91e51f18cf10/rpds_py-2026.9.1-cp315-cp315-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2c16ab111bc27c646ba8aa005d0527754edc538ebb636f0b1bf8e244b48d1945", size = 372022, upload-time = "2026-10-04T16:30:58.295Z" }, + { url = "https://files.pythonhosted.org/packages/ba/cb/8f8774df5134e23424372838bcc5c7ed4127d723e1ff52f7bebd4dcb2563/rpds_py-2026.9.1-cp315-cp315-manylinux_2_31_riscv64.whl", hash = "sha256:7664419f27db41d4f1c43a78dccda7dd6e8ef2428df3ee01d0c2a07a6b071297", size = 377092, upload-time = "2026-10-04T16:30:59.984Z" }, + { url = "https://files.pythonhosted.org/packages/90/02/8d7095d73bf9114219be40230baa5df00611e0a82ed9517779ff9c19f82b/rpds_py-2026.9.1-cp315-cp315-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:4b26b03d9d2658ee2fa234f8f4f19f38a09773fe5261028025032e26d4d35af0", size = 400830, upload-time = "2026-10-04T16:31:01.721Z" }, + { url = "https://files.pythonhosted.org/packages/ec/02/8206856f8f363cd042a8315dc86b3912f5dcb6d3c66bbb24b69c2bfb0775/rpds_py-2026.9.1-cp315-cp315-musllinux_1_2_aarch64.whl", hash = "sha256:be3e47e2d91aa3942ff9bf4077a505226005abfc39b6f7554a91c1b9393986b9", size = 551478, upload-time = "2026-10-04T16:31:03.472Z" }, + { url = "https://files.pythonhosted.org/packages/41/6b/36211f1bb1f0b0313f496d92f5905b74ea107f27cb16fa3355a82f04575e/rpds_py-2026.9.1-cp315-cp315-musllinux_1_2_i686.whl", hash = "sha256:6307a0da524939decb8ca4a3933b8ab62525794411d6984fca6726e732804af6", size = 615173, upload-time = "2026-10-04T16:31:05.281Z" }, + { url = "https://files.pythonhosted.org/packages/35/77/cda0c4a6f055446b692f0ed5692f73707cafd3dff82672ede31b1a9b59de/rpds_py-2026.9.1-cp315-cp315-musllinux_1_2_x86_64.whl", hash = "sha256:159a7aab5c5e8b112c8830f54717ce56da1252ebdbb526f5be2df2309280b9e7", size = 579653, upload-time = "2026-10-04T16:31:07.065Z" }, + { url = "https://files.pythonhosted.org/packages/74/ec/d8385f446240aed643b9e92a5055cff3015cc04a13c61f73b2883c478ed5/rpds_py-2026.9.1-cp315-cp315-win32.whl", hash = "sha256:dbc2673f9223d420c91145599b3ba45a8a50c207d1976908e5fb5ddb0c9b9429", size = 206498, upload-time = "2026-10-04T16:31:08.989Z" }, + { url = "https://files.pythonhosted.org/packages/6d/a5/71b5cd00e0521e3b6b81828baea368c62b6b700ebdd9554cd7d41ddf12fa/rpds_py-2026.9.1-cp315-cp315-win_amd64.whl", hash = "sha256:75c38c50ab9aca840225d9a9a3810bf11d04bd5c1f186cabbb8aee56db3e9b15", size = 223121, upload-time = "2026-10-04T16:31:10.84Z" }, + { url = "https://files.pythonhosted.org/packages/33/58/dba857c3bc8221b31b62eb170a3080f4f191de79f047200389b7ed1b06a7/rpds_py-2026.9.1-cp315-cp315-win_arm64.whl", hash = "sha256:a431156bb41865fc14cd5d79bb9d7bbed83110b0159e34e62ae30951f96c0009", size = 218741, upload-time = "2026-10-04T16:31:12.592Z" }, + { url = "https://files.pythonhosted.org/packages/5b/d0/320ab28ccc1415eeb509d68682b0014fb74690cd49f1c2d29a232475af50/rpds_py-2026.9.1-cp315-cp315t-macosx_10_12_x86_64.whl", hash = "sha256:ef0d8c843e2827d6c120ab4687e9423fb1d893db1df27b7c1506615bcb9734a0", size = 349719, upload-time = "2026-10-04T16:31:14.48Z" }, + { url = "https://files.pythonhosted.org/packages/56/88/f5b12f1358f443c08b7ce3cc8391d82d335f4872580e2e097847fd36087a/rpds_py-2026.9.1-cp315-cp315t-macosx_11_0_arm64.whl", hash = "sha256:45bc6bccf78b20fd834237d18db64965d7ee68ba7f60440a26c7ab71e7b8d51a", size = 343267, upload-time = "2026-10-04T16:31:16.827Z" }, + { url = "https://files.pythonhosted.org/packages/f7/0c/c765b0059d532acb3b9c45d781ccc22f15a96dbe443d00903f643ba9df10/rpds_py-2026.9.1-cp315-cp315t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1d55198263bb51f557550c6ed2e6d1cb6a6fed6eb5c9120b741c5926bef8a45d", size = 374555, upload-time = "2026-10-04T16:31:18.931Z" }, + { url = "https://files.pythonhosted.org/packages/6b/8a/cafddfda77564a10cd21184640c3bffda6a2b8d20972fb5dedd5e0166328/rpds_py-2026.9.1-cp315-cp315t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:a8763f20692da7df39b0afdd1ba3042b004c50a45994f76c2d9a25641f7673db", size = 378346, upload-time = "2026-10-04T16:31:20.75Z" }, + { url = "https://files.pythonhosted.org/packages/b9/01/5e626016eff72c183bf6c96539240cace15d402468647a453ec08415b2fc/rpds_py-2026.9.1-cp315-cp315t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e43d4a1f673e8a1cbd8533e809e02b4bf9d4f2280269bb640436556312121250", size = 492373, upload-time = "2026-10-04T16:31:22.614Z" }, + { url = "https://files.pythonhosted.org/packages/3b/9c/15a2469e9389242f46896b3f0a01d68caea8a5a35c011fcb05ef333aae73/rpds_py-2026.9.1-cp315-cp315t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:ea394a937f17a54c51239348bdbe2e3518124c8d4a8951ba04a311d3095bd18f", size = 394371, upload-time = "2026-10-04T16:31:24.768Z" }, + { url = "https://files.pythonhosted.org/packages/63/f5/c100ff77e1e6366e947c75969c258fdfc4b7bc5bbfe7351e62ffbf2a1228/rpds_py-2026.9.1-cp315-cp315t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cdeaa99ce822dca76cfb1b993e9120c5ea212f2eb66d48950ad63c349668a018", size = 375132, upload-time = "2026-10-04T16:31:26.588Z" }, + { url = "https://files.pythonhosted.org/packages/dd/f4/fe0269c9de253e99c81cabc12b8971a5feaa083debdaff1221e06264d9e3/rpds_py-2026.9.1-cp315-cp315t-manylinux_2_31_riscv64.whl", hash = "sha256:b4f062343e7ad3fa94f2c66e5ae667dee47ee74dd41a9057c4fbe163236a123d", size = 377642, upload-time = "2026-10-04T16:31:28.677Z" }, + { url = "https://files.pythonhosted.org/packages/05/65/b34a7b257baccff8f4a24a722933166d4941d5ebdc9c3f4bc4ffcd5ce4f4/rpds_py-2026.9.1-cp315-cp315t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:22ffd29a63d71fb1b81552c21f2c2b734949b7ac751a9be70675a939a900839b", size = 400855, upload-time = "2026-10-04T16:31:30.802Z" }, + { url = "https://files.pythonhosted.org/packages/98/32/844e54176b6071b90b38a564e6940bc6eb8f97b2890dc709c19db9dec0f4/rpds_py-2026.9.1-cp315-cp315t-musllinux_1_2_aarch64.whl", hash = "sha256:08dae4a4095150a7c4545a1fb40b98e1ab1744fbc2770d92c977b9dadaa49ab6", size = 552573, upload-time = "2026-10-04T16:31:32.709Z" }, + { url = "https://files.pythonhosted.org/packages/17/73/6041d20729dffbfdf155c02d65be58bc225a1c1fb548fd87c23ef306138f/rpds_py-2026.9.1-cp315-cp315t-musllinux_1_2_i686.whl", hash = "sha256:9a0460d43603d1fd9ef59c30278531e15d78581721ddb538fa560aa7817ea4ad", size = 615840, upload-time = "2026-10-04T16:31:34.565Z" }, + { url = "https://files.pythonhosted.org/packages/af/9e/418094adaee6b056ce199051b255448ed872829051e341b2294c80da0977/rpds_py-2026.9.1-cp315-cp315t-musllinux_1_2_x86_64.whl", hash = "sha256:1c2d1f6da5128eabf34e963d7163a818846075a52568250d006c4c953b40f903", size = 582764, upload-time = "2026-10-04T16:31:36.665Z" }, + { url = "https://files.pythonhosted.org/packages/3d/9b/1698ebf6b840ddfe8b472198abbed6ace35c6e398faeecd6c47dae742a4e/rpds_py-2026.9.1-cp315-cp315t-win32.whl", hash = "sha256:5c6ee90dee3e85e055ddfd502d611643d9b0fd94c818220bda84ec3dacd9b27b", size = 203795, upload-time = "2026-10-04T16:31:38.53Z" }, + { url = "https://files.pythonhosted.org/packages/8a/e2/91f70d804c61f8eac39a417e82aa1024f655ab9e8bdd7393b196242bc41b/rpds_py-2026.9.1-cp315-cp315t-win_amd64.whl", hash = "sha256:fe5ad0664ec772b02c45859041aa17655709cced7a31005817fbbbd988c25567", size = 223037, upload-time = "2026-10-04T16:31:40.468Z" }, + { url = "https://files.pythonhosted.org/packages/f3/ad/f0eba548e297987964ef8088403db875b333963bf8189f3f563b6936b20e/rpds_py-2026.9.1-pp311-pypy311_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4c0d2cb595a420b34d5086db0add011e26e2c09d6a024afbac4228bf8f863a30", size = 384112, upload-time = "2026-10-04T16:31:42.643Z" }, + { url = "https://files.pythonhosted.org/packages/33/89/891fa4d81f1eae7fe262ecd0aba2c51de94a116c7f3ccfb034fa75f4c537/rpds_py-2026.9.1-pp311-pypy311_pp73-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:f3d6ed6a98cfd19155996605474982cc470d7601746a6439078f1a5a3fa8b050", size = 389662, upload-time = "2026-10-04T16:31:44.642Z" }, + { url = "https://files.pythonhosted.org/packages/4f/46/912cf623b2263f9c9c2d04e8508fc38dbe89983faeba5fb36ca4558ae550/rpds_py-2026.9.1-pp311-pypy311_pp73-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8171b44a054e5c67fd748ada04187f1250bf35b95f85e52ab64bcf3331a923bb", size = 504637, upload-time = "2026-10-04T16:31:46.667Z" }, + { url = "https://files.pythonhosted.org/packages/9b/5c/7f3cfa7c5f01765c9f47c8398210ed0c1eb686c0e99c223cb1c6776d3fb9/rpds_py-2026.9.1-pp311-pypy311_pp73-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:fda1d96e542c37b6c804547dbf489c129fe7c97183a76a5ec275909ba1a063df", size = 402607, upload-time = "2026-10-04T16:31:49.068Z" }, + { url = "https://files.pythonhosted.org/packages/f0/76/2e915d4115ee453c33e6af17bc1df7e22ddc52974e178eeeb92a78c09983/rpds_py-2026.9.1-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:270bdcdaac5d5b6f73c5e22e7e135c7f2a50e789f71d9e241d5be8d90026e19a", size = 382762, upload-time = "2026-10-04T16:31:51.24Z" }, + { url = "https://files.pythonhosted.org/packages/0d/e5/844382925d7b84a4074ad79fc2b9d05eb6231ea243b34f247e68e519b606/rpds_py-2026.9.1-pp311-pypy311_pp73-manylinux_2_31_riscv64.whl", hash = "sha256:84a6ecc0c940169190d2c23bd969debd48c94dbc855acd60188a68d71d421608", size = 389563, upload-time = "2026-10-04T16:31:53.549Z" }, + { url = "https://files.pythonhosted.org/packages/81/82/d6e3951602dc2e24790ef9d299aa24bf5e2493df5043f2c6da00480cc447/rpds_py-2026.9.1-pp311-pypy311_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:ab4b2fda7c2b542f7f9d886cc6a838c5079d2b76f72e6081411faba11adde2c9", size = 416842, upload-time = "2026-10-04T16:31:55.76Z" }, + { url = "https://files.pythonhosted.org/packages/ea/1c/8d248e4ea671d525ab7daa0703ad81cc2175076fa0ccd7ef088facbd2b01/rpds_py-2026.9.1-pp311-pypy311_pp73-musllinux_1_2_aarch64.whl", hash = "sha256:44b32a7c4f0da3d28af31c259e38ddcff096f855e205ed0671d02fcf44f1ea1c", size = 562434, upload-time = "2026-10-04T16:31:57.874Z" }, + { url = "https://files.pythonhosted.org/packages/ad/38/e7ac0db898120d15dea25e3661b1242a40cb3b23d1f4db86c15a01fe0ba1/rpds_py-2026.9.1-pp311-pypy311_pp73-musllinux_1_2_i686.whl", hash = "sha256:a3dbc5ed9514908d5046107d7b1346bde71eea61de6e0e4919c19354f97e769f", size = 627909, upload-time = "2026-10-04T16:31:59.977Z" }, + { url = "https://files.pythonhosted.org/packages/03/c8/37e555514932cb1ee36c75349187aa882a98dee5d5654c761699ed8a0d27/rpds_py-2026.9.1-pp311-pypy311_pp73-musllinux_1_2_x86_64.whl", hash = "sha256:6eae33003518fd4cb4f83a218d5371469dd3001aa3b87128c005b07762f7fe5e", size = 593658, upload-time = "2026-10-04T16:32:02.14Z" }, + { url = "https://files.pythonhosted.org/packages/12/5c/5f69a91add94a8a33acea68a2b034a464946a654ff7296af9d957ef0d6a5/rpds_py-2026.9.1-pp311-pypy311_pp80-macosx_10_12_x86_64.whl", hash = "sha256:0483515261947e4e8b8e1375bf7463e7eb6ccfb3d86e7b554d90cd5285f20f32", size = 351769, upload-time = "2026-10-04T16:32:04.675Z" }, + { url = "https://files.pythonhosted.org/packages/41/22/35ea8bf3b2682b90ab687c74e4828418ff12298a5b0b0a0c033f61c86a30/rpds_py-2026.9.1-pp311-pypy311_pp80-macosx_11_0_arm64.whl", hash = "sha256:4cfaf02209061880210819934de2f4f6aa83dc04dafe6770276acc240a56da31", size = 351719, upload-time = "2026-10-04T16:32:06.834Z" }, + { url = "https://files.pythonhosted.org/packages/a3/90/a9ba81408369d34c1aca73319c15adc91ed45c1d21e9e8ee4832ffb7fe0b/rpds_py-2026.9.1-pp312-pypy312_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6cdc537c8633d7fd92a82e2e0d2ab74320a3f63d5e59fb9cf08711e08fe151c4", size = 381109, upload-time = "2026-10-04T16:32:08.994Z" }, + { url = "https://files.pythonhosted.org/packages/b3/44/5192a0bed94cec86bd2a5e1cc5a1bb8f7eec3aec907eacd2deb13e87e326/rpds_py-2026.9.1-pp312-pypy312_pp73-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:10e208f2425d973938afcd56e28a7c4be32e27b6a60b5d381f49fb9d8acf9759", size = 388222, upload-time = "2026-10-04T16:32:11.321Z" }, + { url = "https://files.pythonhosted.org/packages/92/cd/5356549711448f18b52f7a11ffbe90f1228774996fbf3c6cd1b18d22abaf/rpds_py-2026.9.1-pp312-pypy312_pp73-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:6c0dbbcc19735fe5f8b0a54c07659d154a9e69f47e15d0a6ab7299215daf62cb", size = 500384, upload-time = "2026-10-04T16:32:13.478Z" }, + { url = "https://files.pythonhosted.org/packages/7a/88/ddda9d28adfe33119c75b2e733d4ba7e326f41d7e1b1093951771768ca48/rpds_py-2026.9.1-pp312-pypy312_pp73-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:684fd492fff4fead00587544e059be2bbcb6f93454f21fa2a91b66fc7508be82", size = 401661, upload-time = "2026-10-04T16:32:15.653Z" }, + { url = "https://files.pythonhosted.org/packages/32/c3/bb59ba16a6b4a57995d15b8c04c00f28df3b938c4dcdde48fbe0f73edb0c/rpds_py-2026.9.1-pp312-pypy312_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d1028417bb44037eb3069c1009bd7b7277212876cda22fbe565b0bca9fab6d2c", size = 380748, upload-time = "2026-10-04T16:32:17.727Z" }, + { url = "https://files.pythonhosted.org/packages/f5/b8/0580faa1c5a32ddc160130dd7ae54d2d007272a89a40a6b5db3e50f9f585/rpds_py-2026.9.1-pp312-pypy312_pp73-manylinux_2_31_riscv64.whl", hash = "sha256:492e5e428cbe126221611f47e068f01660352feec4ad18bc0f5ea9b2ae88fb14", size = 385731, upload-time = "2026-10-04T16:32:19.855Z" }, + { url = "https://files.pythonhosted.org/packages/bf/70/f73564642bbe3322c7eeef2c2f258cf040b71c41a430b532e59d04a1b838/rpds_py-2026.9.1-pp312-pypy312_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:88b5268892fde430d5531f95bc560b6efbbd67c929662c586afd729a96e7461c", size = 412131, upload-time = "2026-10-04T16:32:22.285Z" }, + { url = "https://files.pythonhosted.org/packages/8c/5d/17fff2e1f8f68721a2afb5cb48f7e442c751bb6b4883157c5abaf618cd08/rpds_py-2026.9.1-pp312-pypy312_pp73-musllinux_1_2_aarch64.whl", hash = "sha256:01445c8d194aa032a08e944f16567672da1c62dbdbefd8b6d0693032e290cf68", size = 558193, upload-time = "2026-10-04T16:32:24.4Z" }, + { url = "https://files.pythonhosted.org/packages/2a/d0/869ab6fb08531f97aae4a780b4d61b4190d8aef48fe054ee28bcf7114466/rpds_py-2026.9.1-pp312-pypy312_pp73-musllinux_1_2_i686.whl", hash = "sha256:eef6a03b0b6d08d0835ccfa8ec8d1bc70525e3801387567137b50c557695e6da", size = 622448, upload-time = "2026-10-04T16:32:26.844Z" }, + { url = "https://files.pythonhosted.org/packages/ba/a5/9672e532fe02cd3b92c78cfd8743abca939d96853006c63835009a1ecb6f/rpds_py-2026.9.1-pp312-pypy312_pp73-musllinux_1_2_x86_64.whl", hash = "sha256:6b9bf3135b4ad5981df9a73d71a35272d650a2985ae9c2746357b24d59de2448", size = 591126, upload-time = "2026-10-04T16:32:29.274Z" }, + { url = "https://files.pythonhosted.org/packages/80/ef/3a9f8e4279c7920af561deed4cb7ebebed2794a8f608cf404d5eef14a105/rpds_py-2026.9.1-pp312-pypy312_pp80-macosx_10_12_x86_64.whl", hash = "sha256:56c6952a9b15047466d0c2347c446a761d4527f89976156341e68f0ce5cc08b0", size = 357108, upload-time = "2026-10-04T16:32:31.641Z" }, + { url = "https://files.pythonhosted.org/packages/b7/55/4b2fa381a583760aea5c92841e4e928a358e1c6511b129219e9c2826a226/rpds_py-2026.9.1-pp312-pypy312_pp80-macosx_11_0_arm64.whl", hash = "sha256:b242c27c8f836305a4a72df9cdd564386ac57b807bd252a063223331c9316b37", size = 346877, upload-time = "2026-10-04T16:32:34.061Z" }, +] + +[[package]] +name = "tskit" +version = "1.0.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "jsonschema" }, + { name = "numpy", version = "2.4.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.12'" }, + { name = "numpy", version = "2.5.4", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.12'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/22/f5/c23333b3ffd86cbe79520bbee85f050f815c16a625b014dbacbe486fb6c2/tskit-1.0.3.tar.gz", hash = "sha256:8a305b3bcf4145688be17962f6ea2ac20aa65db440ed0c02b49f9b4a3c00836b", size = 933435, upload-time = "2026-05-14T18:20:16.89Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f6/6c/7fdf85e62204319d77da52d2647e1e65c6dc94660a27d7f202fd134f42c9/tskit-1.0.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:a5d5f5e0d0ad53ac554563c76b91c5f2bb721d021b7c761606a9a72fe1e49d74", size = 526356, upload-time = "2026-05-14T18:19:40.482Z" }, + { url = "https://files.pythonhosted.org/packages/3b/ce/4ff9ba98eca28992250fa8eab25db5bc4a3c7b3af4ad46652f7d1b5b22c3/tskit-1.0.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:af978aa415f04fbabd613ad2933526110b4767e8e64f319ec03cefca9e2242bd", size = 496008, upload-time = "2026-05-14T18:19:42.332Z" }, + { url = "https://files.pythonhosted.org/packages/2f/3d/d4d8567a2bca51df46f0c7ad706678b106ce2ba094d8584d8829a24451c0/tskit-1.0.3-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f509b7b271dd80a68ee3f6bfdff614e3b8ac989dcc44dd8d36c25c8181443b79", size = 1395725, upload-time = "2026-05-14T18:19:43.83Z" }, + { url = "https://files.pythonhosted.org/packages/4c/f3/d5ce78eeeda65c4673966d00567f4166b9500843ce953c0dad927a2200c3/tskit-1.0.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:0722a4c1fc9c287c96a2141ae3587df6defed2a049cca7ab90479958c613835f", size = 1377087, upload-time = "2026-05-14T18:19:45.238Z" }, + { url = "https://files.pythonhosted.org/packages/f0/7c/9e53fb9f71bf89eb8327cb2a859d4c9764dc447a42d3407f7ec7c7a25fb8/tskit-1.0.3-cp311-cp311-win32.whl", hash = "sha256:3901c9fc02497e2c7e0ee5ca36ce5b65749f9c630e5ace1daa990963d8f5a63d", size = 449670, upload-time = "2026-05-14T18:19:46.641Z" }, + { url = "https://files.pythonhosted.org/packages/72/88/a80beb0adfb8ceade30b1913010c56e6bd4f837ef2f75aeb76222377d148/tskit-1.0.3-cp311-cp311-win_amd64.whl", hash = "sha256:0d374768d422e941f8ad6ffdba85d6ca1585467532f6f4945b171f18d6613285", size = 496418, upload-time = "2026-05-14T18:19:47.948Z" }, + { url = "https://files.pythonhosted.org/packages/56/1f/d6726034289911b326e74b8b5339c9d1ec6fe46bf203253ae8d90b146a89/tskit-1.0.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:3d035946888c9da5eefb93ca99c2ae3ad2cb7cb64bc6037588fe285f775ee67a", size = 528485, upload-time = "2026-05-14T18:19:49.299Z" }, + { url = "https://files.pythonhosted.org/packages/ad/ed/7fa3f43bc0d7569b75bc6a5c03bc14bd2e9de12a3f6a2f8b3251d505226a/tskit-1.0.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:31aee6a8b6d9a0bdae06f0b186fb7aad54d53f34050c0ee72a6caa560506950f", size = 496860, upload-time = "2026-05-14T18:19:50.716Z" }, + { url = "https://files.pythonhosted.org/packages/28/b1/fb828d0479be0f178e9f33b4483003a421b069e6331dc5f35066dc3029e4/tskit-1.0.3-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:fb07ead500c7e88c69677469b4f489c8997ad944103102b04d724c855b4b980b", size = 1398919, upload-time = "2026-05-14T18:19:52.1Z" }, + { url = "https://files.pythonhosted.org/packages/8b/3e/1c7adaae07a00ce009e692a6d171c7a0882d1ed9753017b78a7db7ae7d72/tskit-1.0.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:241192b12d310e49b21bfae524c76768a15d0ed3f7b950d6ee09dce1528b8973", size = 1378470, upload-time = "2026-05-14T18:19:53.881Z" }, + { url = "https://files.pythonhosted.org/packages/71/a1/10d12e9b7908775cf97ad9e6c3d94b4b970940c74a161cc9aaf540fa83c0/tskit-1.0.3-cp312-cp312-win32.whl", hash = "sha256:3e11c9fe328b27c0e4a3ff64a0fed972d2cd48a743c03c8bc339352664322ee8", size = 450010, upload-time = "2026-05-14T18:19:55.419Z" }, + { url = "https://files.pythonhosted.org/packages/2c/b7/d1842bec89cd4993958165d6da774e85ad79b9426baca571c39457a1b6b5/tskit-1.0.3-cp312-cp312-win_amd64.whl", hash = "sha256:66c83c7f971bc160fd29ca6169e1ee31023a5283ca88936df3eed1e568eee209", size = 496000, upload-time = "2026-05-14T18:19:56.75Z" }, + { url = "https://files.pythonhosted.org/packages/e6/10/aa8cc65060669b911946649cd1802f29640998c5b6ca78c4c845fb3c473d/tskit-1.0.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:d52e2ad0eb79e3b21da8a236d0f5eb888f9b18569e48f4b84a3875523e80ee22", size = 528459, upload-time = "2026-05-14T18:19:58.156Z" }, + { url = "https://files.pythonhosted.org/packages/eb/42/34d981123a335d80e3030e9b0a81a41f654c9cf1c5db0ac630d6b20d8bfd/tskit-1.0.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:5bd1744a020a518e5835b85971fea14ca827d76f778650eb37e3f44c6db0a711", size = 496838, upload-time = "2026-05-14T18:19:59.909Z" }, + { url = "https://files.pythonhosted.org/packages/65/9c/09b072c01b8e58a3137011851d4761daf957f2f86447bc2a2c25e0d5aa4a/tskit-1.0.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:405c20fad5256edd6abd1f8b514c5bc653d808102f509c4512c9b174507dbe84", size = 1399080, upload-time = "2026-05-14T18:20:01.353Z" }, + { url = "https://files.pythonhosted.org/packages/d0/bb/789b91f7a2deca4d50f649b685e2b3dca4b3358e36bf7a5b2b84709bb8d5/tskit-1.0.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:bc1c97f00ce8e79460aa9ff3b36987daf70f2dc267e36a32db77018dbefc822b", size = 1378631, upload-time = "2026-05-14T18:20:03.273Z" }, + { url = "https://files.pythonhosted.org/packages/c7/8b/3b2b19246b2ba3159456e4068887e505d7a2ec857ecd7ae7606d6c129fda/tskit-1.0.3-cp313-cp313-win32.whl", hash = "sha256:53444ef3b2fd7ea9ca863e562c33402e46a55e645887dd4d983686b607e3f788", size = 449996, upload-time = "2026-05-14T18:20:04.788Z" }, + { url = "https://files.pythonhosted.org/packages/23/fc/53ef7ad8b3f2183584af81b0ff046a660c3164530a1f768770dbd4d60cb7/tskit-1.0.3-cp313-cp313-win_amd64.whl", hash = "sha256:721ae7f02730ed91233a6da2a9d4b59632d1512877cd0d6fe948ea5d9ddc17e9", size = 496006, upload-time = "2026-05-14T18:20:06.577Z" }, + { url = "https://files.pythonhosted.org/packages/66/b9/91194216907ebf278d7a58288f3e64881e11a0eedcca7125a49e0bbdafdd/tskit-1.0.3-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:62a45d5e57f5a9181eb113c0dd478783ef3c4829856c0831ea2eba2cdf00a8de", size = 528594, upload-time = "2026-05-14T18:20:08.032Z" }, + { url = "https://files.pythonhosted.org/packages/6e/2c/0a888f4f140922c7a3945dbca5e444fa8052a620c784359673fe9eb1eda7/tskit-1.0.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:8df716837129652fc82d095d80fd4e23a728213a4b18de9233f3aebb073b60b4", size = 496866, upload-time = "2026-05-14T18:20:09.34Z" }, + { url = "https://files.pythonhosted.org/packages/2f/ce/56aa69b768734ede0b8b36c5ea2ce842d50c786b74cfefdfc036a1fd29da/tskit-1.0.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:151eefbbae9a339f3ea64980937fa89d6ac9fb371dba880a9b30791a461753ad", size = 1397616, upload-time = "2026-05-14T18:20:10.762Z" }, + { url = "https://files.pythonhosted.org/packages/6e/5a/b05eab4a4a32bce6a51f991033ed460a836aafc1f406eebc624831558d9b/tskit-1.0.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:2170cacd8ade47fd8f1510aca78549e09e9ba875c25496f0ccfb755e09c0b06b", size = 1377477, upload-time = "2026-05-14T18:20:12.649Z" }, + { url = "https://files.pythonhosted.org/packages/ea/c5/116419617468dc06428e507001d0a7b212116e9e8d336bd8c05a6531b2a0/tskit-1.0.3-cp314-cp314-win32.whl", hash = "sha256:24f8fffc1e1dd4154654e91bb7bac22078b3cadd9837e5c490225aa5c313b819", size = 455586, upload-time = "2026-05-14T18:20:14.165Z" }, + { url = "https://files.pythonhosted.org/packages/6e/7f/61818ed922629178abb07e45439d0af3bb5867b629fe392e9864df58ce5c/tskit-1.0.3-cp314-cp314-win_amd64.whl", hash = "sha256:cbe791757cbd4d060a9c06c270bbcacc932f60ce1d77e98dcea35c931d0b0aa9", size = 505409, upload-time = "2026-05-14T18:20:15.527Z" }, +] + +[[package]] +name = "tsls" +version = "0.0.1.dev0" +source = { editable = "." } +dependencies = [ + { name = "numpy", version = "2.4.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.12'" }, + { name = "numpy", version = "2.5.4", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.12'" }, + { name = "tskit" }, +] + +[package.dev-dependencies] +test = [ + { name = "pytest" }, +] + +[package.metadata] +requires-dist = [ + { name = "numpy", specifier = ">=2" }, + { name = "tskit", specifier = ">=1.0.0" }, +] + +[package.metadata.requires-dev] +test = [{ name = "pytest" }] + +[[package]] +name = "typing-extensions" +version = "4.16.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f6/cc/6253133b5bb138fc3306cebfbda2c520f545d36b5be2c7255cc528bb45d6/typing_extensions-4.16.0.tar.gz", hash = "sha256:dc983d19a509c94dba722ee6abd33940f7c05a89e243c47e907eb4db6f1a43e5", size = 113555, upload-time = "2026-07-02T08:40:05.92Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/49/d3/b8441a820a491ddfc024b0b0cf0393375b75ea13866d9c66727e54c2fc80/typing_extensions-4.16.0-py3-none-any.whl", hash = "sha256:481caa481374e813c1b176ada14e97f1f67a4539ce9cfeb3f350d78d6370c2e8", size = 45571, upload-time = "2026-07-02T08:40:04.659Z" }, +]