From afa04629d84823a15b13227e3b3c76da457d8aa6 Mon Sep 17 00:00:00 2001 From: Laurenz Keller Date: Fri, 22 Sep 2023 14:55:30 +0200 Subject: [PATCH 01/45] Add files for listing children and subtrees --- warmup/children.py | 16 ++++++++++ warmup/subtree.py | 73 ++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 89 insertions(+) create mode 100644 warmup/children.py create mode 100644 warmup/subtree.py diff --git a/warmup/children.py b/warmup/children.py new file mode 100644 index 0000000..6ec7914 --- /dev/null +++ b/warmup/children.py @@ -0,0 +1,16 @@ +from anytree import Node, RenderTree, PreOrderIter + +A = Node("A") +B = Node("B", parent=A) +C = Node("C", parent=A) +D = Node("D", parent=B) +E = Node("E", parent=C) +F = Node("F", parent=C) + +all_nodes = list(PreOrderIter(A)) +print(RenderTree(A)) +children={} +for node in all_nodes: + children[node.name] = [child.name for child in node.children] +print(children) + diff --git a/warmup/subtree.py b/warmup/subtree.py new file mode 100644 index 0000000..0be796e --- /dev/null +++ b/warmup/subtree.py @@ -0,0 +1,73 @@ +from anytree import Node, RenderTree +from itertools import combinations, product +''' +takes a variable number of lists as input and returns a list containing all possible combination of the input lists +in this case: takes a list of lists of subtrees (for each child one list of subtrees) as input, a subtree itself is a list of nodes +outputs all combinations of subtrees +''' +def all_combinations_of_elements(*lists): + n = len(lists) + all_combinations = [] + + for r in range(1, n+1): + for list_combination in combinations(lists, r): + for element_combination in product(*list_combination): + all_combinations.append(list(element_combination)) + + return all_combinations + +'''creates a subtree given a subtree (nodes_list) and the root node''' + +def create_subtree(original_root, nodes_list): + nodes_dict = {} + + for node in [original_root] + list(original_root.descendants): + if node in nodes_list: + parent_node = next((n for n in nodes_list if n is node.parent), None) + nodes_dict[node] = Node(node.name, parent=nodes_dict.get(parent_node)) + + return nodes_dict.get(original_root) + +'''returns a list of all subtrees of a tree, input is the root node +a recursive approach is used: if one knows the subtrees of the children of the root node, +then one can find all combinations of the subtrees of the children and add the root node to each one of these combinations, +this way one obtains all subtrees of the root node''' + +def subtrees(node): + if not node.children: + return [[node]] + + child_subtrees = [subtrees(child) for child in node.children] + + combined_subtrees = all_combinations_of_elements(*child_subtrees) + + result_subtrees = [] + result_subtrees.append([node]) + for combination in combined_subtrees: + subtree_with_root = [node] + [item for sublist in combination for item in sublist] + result_subtrees.append(subtree_with_root) + + return result_subtrees + + +A = Node("0") +B = Node("1", parent=A) +C = Node("3", parent=A) +D = Node("3", parent=B) + +print(RenderTree(A)) +print("\n") +all_node_lists = subtrees(A) +all_node_lists = sorted(all_node_lists, key=len) +print(all_node_lists) +print("\n") +all_subtrees=[] + +for nodes_list in all_node_lists: + subtree=create_subtree(A,nodes_list) + all_subtrees.append(subtree) +i = 1 +for subtree in all_subtrees: + print(f"{i}. ") + print(RenderTree(subtree)) + i += 1 From cc40f5e1aed906d0389926d22f0e8c7f036466d2 Mon Sep 17 00:00:00 2001 From: Laurenz Keller Date: Mon, 25 Sep 2023 13:57:23 +0200 Subject: [PATCH 02/45] added simulation of trees and comparison plots (not correct yet) --- src/pmhn/_trees/_simulate.py | 42 ++++++++++++++++++++++++++-- src/pmhn/_trees/mutation_freq.png | Bin 0 -> 61744 bytes src/pmhn/_trees/trees.png | Bin 0 -> 54230 bytes src/pmhn/_trees/write_csv.py | 45 ++++++++++++++++++++++++++++++ 4 files changed, 84 insertions(+), 3 deletions(-) create mode 100644 src/pmhn/_trees/mutation_freq.png create mode 100644 src/pmhn/_trees/trees.png create mode 100644 src/pmhn/_trees/write_csv.py diff --git a/src/pmhn/_trees/_simulate.py b/src/pmhn/_trees/_simulate.py index 3d47a54..d5f8f44 100644 --- a/src/pmhn/_trees/_simulate.py +++ b/src/pmhn/_trees/_simulate.py @@ -1,9 +1,14 @@ from typing import Union, Sequence - +from anytree import Node import numpy as np from pmhn._trees._interfaces import Tree +def generate_valid_tree(rng, theta: np.ndarray, sampling_time: float, min_tree_size: int = 2, max_tree_size: int = 11): + while True: + tree = _simulate_tree(rng, theta, sampling_time) + if len(tree) >= min_tree_size and len(tree)<=max_tree_size: + return tree def _simulate_tree( rng, @@ -31,7 +36,38 @@ def _simulate_tree( # TODO(Pawel): This is part of https://github.com/cbg-ethz/pMHN/issues/14 # Note that the sampling time is known that our `theta` entries # are log-Theta entries from the paper. - raise NotImplementedError + print("starting ... \n") + theta_size=len(theta) + node_time_map={} + root=Node("0") + node_time_map[root]=0 + U_current=[root] + exit_while=False + while len(U_current)!=0: + U_next=[] + for node in U_current: + path=list(node.path) + old_mutations=[int(node.name) for node in path] + possible_mutations=list(set([i+1 for i in range(theta_size)]).difference(set(old_mutations))) + for j in possible_mutations: + new_node=Node(str(j),parent=node) + l=np.exp(theta[j-1][j-1]) + for anc in [ancestor for ancestor in node.path if ancestor.parent is not None]: + l*=np.exp(theta[j-1][int(anc.name)-1]) + waiting_time=node_time_map[node]+rng.exponential(1.0/l) + if waiting_timeXD}EGnTPf( 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 literal 0 HcmV?d00001 diff --git a/src/pmhn/_trees/trees.png b/src/pmhn/_trees/trees.png new file mode 100644 index 0000000000000000000000000000000000000000..3709f70ed2f948ea89bdb23e48cc2c7990c13ab7 GIT binary patch literal 54230 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 xGkE9v{DvO9vZ?zwu# Date: Tue, 26 Sep 2023 16:49:48 +0200 Subject: [PATCH 03/45] removed plots and warmup directory, added plotting/csv related files & _simulate.py --- src/pmhn/_trees/mutation_freq.png | Bin 61744 -> 0 bytes src/pmhn/_trees/plot_all_mut_freq.py | 33 +++++++++++++++++++++++++++ src/pmhn/_trees/plot_all_trees.py | 32 ++++++++++++++++++++++++++ src/pmhn/_trees/trees.png | Bin 54230 -> 0 bytes 4 files changed, 65 insertions(+) delete mode 100644 src/pmhn/_trees/mutation_freq.png create mode 100644 src/pmhn/_trees/plot_all_mut_freq.py create mode 100644 src/pmhn/_trees/plot_all_trees.py delete mode 100644 src/pmhn/_trees/trees.png diff --git a/src/pmhn/_trees/mutation_freq.png b/src/pmhn/_trees/mutation_freq.png deleted file mode 100644 index f60e65b02b68ae79e7c2dbd43b01a1a97060ae41..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 61744 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 diff --git a/src/pmhn/_trees/plot_all_mut_freq.py b/src/pmhn/_trees/plot_all_mut_freq.py new file mode 100644 index 0000000..525d2e2 --- /dev/null +++ b/src/pmhn/_trees/plot_all_mut_freq.py @@ -0,0 +1,33 @@ +import pandas as pd +import matplotlib.pyplot as plt + +paths = { + 500: ('/home/laukeller/BSc Thesis/TreeMHN/Example/trees_500.csv', '/home/laukeller/BSc Thesis/pMHN/src/pmhn/_trees/trees_500.csv'), + 2000: ('/home/laukeller/BSc Thesis/TreeMHN/Example/trees_2000.csv', '/home/laukeller/BSc Thesis/pMHN/src/pmhn/_trees/trees_2000.csv'), + 5000: ('/home/laukeller/BSc Thesis/TreeMHN/Example/trees_5000.csv', '/home/laukeller/BSc Thesis/pMHN/src/pmhn/_trees/trees_5000.csv'), + 10000: ('/home/laukeller/BSc Thesis/TreeMHN/Example/trees_10000.csv', '/home/laukeller/BSc Thesis/pMHN/src/pmhn/_trees/trees_10000.csv'), +} + +fig, axs = plt.subplots(2, 2, figsize=(10, 10)) # 2x2 grid of subplots +axs = axs.ravel() + +for i, (num_trees, (r_path, python_path)) in enumerate(paths.items()): + r_trees = pd.read_csv(r_path) + python_trees = pd.read_csv(python_path) + + r_mutation_frequencies = r_trees['Mutation_ID'].value_counts().sort_index() + python_mutation_frequencies = python_trees['Mutation_ID'].value_counts().sort_index() + + bar_width = 0.35 + + r_mutation_frequencies.plot(kind='bar', width=bar_width, position=0, align='center', color='b', alpha=0.5, label='R Trees', ax=axs[i]) + python_mutation_frequencies.plot(kind='bar', width=bar_width, position=1, align='center', color='r', alpha=0.5, label='Python Trees', ax=axs[i]) + + axs[i].set_xlabel('Mutation ID') + axs[i].set_ylabel('Frequency') + axs[i].legend(loc='upper right') + axs[i].set_title(f'Mutation Frequency Distribution ({num_trees} trees)') + +plt.tight_layout() +plt.show() + diff --git a/src/pmhn/_trees/plot_all_trees.py b/src/pmhn/_trees/plot_all_trees.py new file mode 100644 index 0000000..119762e --- /dev/null +++ b/src/pmhn/_trees/plot_all_trees.py @@ -0,0 +1,32 @@ +import pandas as pd +import matplotlib.pyplot as plt + +paths = { + 500: ('/home/laukeller/BSc Thesis/TreeMHN/Example/trees_500.csv', '/home/laukeller/BSc Thesis/pMHN/src/pmhn/_trees/trees_500.csv'), + 2000: ('/home/laukeller/BSc Thesis/TreeMHN/Example/trees_2000.csv', '/home/laukeller/BSc Thesis/pMHN/src/pmhn/_trees/trees_2000.csv'), + 5000: ('/home/laukeller/BSc Thesis/TreeMHN/Example/trees_5000.csv', '/home/laukeller/BSc Thesis/pMHN/src/pmhn/_trees/trees_5000.csv'), + 10000: ('/home/laukeller/BSc Thesis/TreeMHN/Example/trees_10000.csv', '/home/laukeller/BSc Thesis/pMHN/src/pmhn/_trees/trees_10000.csv'), +} + +fig, axs = plt.subplots(2, 2, figsize=(10, 10)) # 2x2 grid of subplots +axs = axs.ravel() + +bin_edges = [x - 0.5 for x in range(2, 12)] + [11.5] # [1.5, 2.5, ..., 10.5, 11.5] + +for i, (num_trees, (r_path, python_path)) in enumerate(paths.items()): + r_trees = pd.read_csv(r_path) + python_trees = pd.read_csv(python_path) + + r_tree_sizes = r_trees.groupby('Tree_ID').size() + python_tree_sizes = python_trees.groupby('Tree_ID').size() + + axs[i].hist(r_tree_sizes, bins=bin_edges, alpha=0.5, label='R Trees') + axs[i].hist(python_tree_sizes, bins=bin_edges, alpha=0.5, label='Python Trees') + axs[i].set_xlabel('Tree Size') + axs[i].set_ylabel('Frequency') + axs[i].legend(loc='upper right') + axs[i].set_title(f'Tree Size Distribution ({num_trees} trees)') + +plt.tight_layout() +plt.show() + diff --git a/src/pmhn/_trees/trees.png b/src/pmhn/_trees/trees.png deleted file mode 100644 index 3709f70ed2f948ea89bdb23e48cc2c7990c13ab7..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 54230 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 xGkE9v{DvO9vZ?zwu# Date: Wed, 27 Sep 2023 15:07:02 +0200 Subject: [PATCH 04/45] changed _simulate.py, write_csv.py and created a few unit tests --- src/pmhn/_trees/_simulate.py | 110 +++++++++++++++++------- src/pmhn/_trees/write_csv.py | 6 +- tests/trees/test_simulate.py | 160 +++++++++++++++++++++++++++++++++++ 3 files changed, 245 insertions(+), 31 deletions(-) create mode 100644 tests/trees/test_simulate.py diff --git a/src/pmhn/_trees/_simulate.py b/src/pmhn/_trees/_simulate.py index d5f8f44..a1203b6 100644 --- a/src/pmhn/_trees/_simulate.py +++ b/src/pmhn/_trees/_simulate.py @@ -4,16 +4,58 @@ from pmhn._trees._interfaces import Tree -def generate_valid_tree(rng, theta: np.ndarray, sampling_time: float, min_tree_size: int = 2, max_tree_size: int = 11): +def generate_valid_tree(rng, theta: np.ndarray, sampling_time: float, min_tree_size: int = None, max_tree_size: int = None) -> Tree: + """ + Generates a single valid tree with known sampling time. + + Args: + rng: random number generator + theta: real-valued (i.e., log-theta) matrix, + shape (n_mutations, n_mutations) + sampling_time: known sampling time + min_tree_size: minimum size of the tree + max_tree_size: maximum size of the tree + Returns: + A valid mutation tree that meets the size constraints if specified. + + Note: + The min_tree_size and max_tree_size parameters consider the entire tree, + i.e the root node is included. + To disable the size constraints, leave min_tree_size and max_tree_size as None. + + """ + while True: - tree = _simulate_tree(rng, theta, sampling_time) - if len(tree) >= min_tree_size and len(tree)<=max_tree_size: + tree = _simulate_tree(rng, theta, sampling_time, max_tree_size) + if (min_tree_size is None or len(tree) >= min_tree_size) and (max_tree_size is None or len(tree) <= max_tree_size): return tree + +def _find_possible_mutations(old_mutations: list[int], n_mutations: int) -> list[int]: + """ + Args: + old_mutations: list of ancestor mutations of a given node (including the node itself) + n_mutations: total number of mutations + Returns: + a list of possible mutations that could appear next for a given node + + Note: + We assume that mutations are labeled with a number between 1 and n_mutations, + so each element in old_mutations should be in that range (except for the root node = mutation 0). + If this assumption is violated, the function will produce incorrect results. + + """ + for mutation in old_mutations: + if mutation > n_mutations or mutation < 0: + raise ValueError(f"Invalid mutation {mutation} in old_mutations. It should be 0 <= mutation <= {n_mutations}.") + possible_mutations=list(set([i+1 for i in range(n_mutations)]).difference(set(old_mutations))) + return possible_mutations + def _simulate_tree( rng, theta: np.ndarray, sampling_time: float, + max_tree_size: int = None ) -> Tree: """Simulates a single tree with known sampling time. @@ -22,7 +64,7 @@ def _simulate_tree( theta: real-valued (i.e., log-theta) matrix, shape (n_mutations, n_mutations) sampling_time: known sampling time - + max_tree_size: maximum size of tree Returns: a mutation tree @@ -32,41 +74,47 @@ def _simulate_tree( Appendix A1 to the TreeMHN paper (with the difference that in the paper `Theta_{jl}` is used, which is `Theta_{jl} = exp( theta_{jl} )`. + + If the tree is larger than max_tree_size, the function returns. """ # TODO(Pawel): This is part of https://github.com/cbg-ethz/pMHN/issues/14 # Note that the sampling time is known that our `theta` entries # are log-Theta entries from the paper. - print("starting ... \n") - theta_size=len(theta) - node_time_map={} - root=Node("0") - node_time_map[root]=0 - U_current=[root] - exit_while=False - while len(U_current)!=0: - U_next=[] + + n_mutations = len(theta) + node_time_map = {} + root = Node(0) + node_time_map[root] = 0 + U_current = [root] + exit_while = False + while len(U_current) != 0: + U_next = [] for node in U_current: - path=list(node.path) - old_mutations=[int(node.name) for node in path] - possible_mutations=list(set([i+1 for i in range(theta_size)]).difference(set(old_mutations))) - for j in possible_mutations: - new_node=Node(str(j),parent=node) - l=np.exp(theta[j-1][j-1]) + path = list(node.path) + old_mutations = [node.name for node in path] + possible_mutations = _find_possible_mutations(old_mutations = old_mutations,n_mutations = n_mutations) + for j in possible_mutations: + new_node = Node(j,parent=node) + # Here j lies in the range of 1 to n_mutations inclusive. + # However, Python uses 0-based indexing for arrays. Therefore, we subtract 1 from j when accessing + # elements in the log-theta matrix to correctly map the 1-indexed mutation to the 0-indexed matrix position. + l = theta[j - 1][j - 1] for anc in [ancestor for ancestor in node.path if ancestor.parent is not None]: - l*=np.exp(theta[j-1][int(anc.name)-1]) - waiting_time=node_time_map[node]+rng.exponential(1.0/l) - if waiting_time tuple[np.ndarray, list[Tree]]: """Simulates a data set of trees with known sampling times. @@ -85,7 +135,9 @@ def simulate_trees( mean_sampling_time: the mean sampling time. Can be a float (shared between all data point) or an array of shape (n_points,). - + min_tree_size: minimum tree_size + max_tree_size: maximum tree_size + Returns: sampling times, shape (n_points,) sampled trees, list of length `n_points` @@ -118,7 +170,7 @@ def simulate_trees( sampling_times = rng.exponential(scale=mean_sampling_time, size=n_points) trees = [ - generate_valid_tree(rng, theta=th, sampling_time=t_s) + generate_valid_tree(rng, theta=th, sampling_time=t_s, min_tree_size = min_tree_size, max_tree_size = max_tree_size) for th, t_s in zip(theta, sampling_times) ] diff --git a/src/pmhn/_trees/write_csv.py b/src/pmhn/_trees/write_csv.py index cff0be9..d5e22b9 100644 --- a/src/pmhn/_trees/write_csv.py +++ b/src/pmhn/_trees/write_csv.py @@ -35,11 +35,13 @@ def write_trees_to_csv(trees, output_file_path): theta = mhn_array mean_sampling_time = 1.0 - tree_counts = [500, 2000, 5000, 10000] + tree_counts = [2000, 5000, 10000, 50000] + min_tree_size = 2 + max_tree_size = 12 for n_points in tree_counts: trees_file_path = f"/home/laukeller/BSc Thesis/pMHN/src/pmhn/_trees/trees_{n_points}.csv" - _, trees = _simulate.simulate_trees(rng, n_points, theta, mean_sampling_time) + _, trees = _simulate.simulate_trees(rng, n_points, theta, mean_sampling_time, min_tree_size = min_tree_size, max_tree_size = max_tree_size) write_trees_to_csv(trees, trees_file_path) diff --git a/tests/trees/test_simulate.py b/tests/trees/test_simulate.py new file mode 100644 index 0000000..83865b6 --- /dev/null +++ b/tests/trees/test_simulate.py @@ -0,0 +1,160 @@ +import numpy as np +import pmhn._trees._simulate as simulate +import pytest +def test_generate_valid_tree(): + """ + We want to test if valid trees are generated. Here we test the size requirements + and the waiting times. + """ + rng = np.random.default_rng() + theta = np.array([[-1.41, 0.00, 0.00, 4.91, 1.03, 0.00, -1.91, -0.74, -1.35, 1.48], + [-1.12, -2.26, 0.00, 0.82, 0.00, 0.00, 1.16, 0.00, -1.62, 0.00], + [0.00, -0.86, -2.55, 1.58, 0.00, 0.00, 1.02, -2.70, 0.00, 0.68], + [0.00, 0.00, 0.00, -3.69, 0.00, 0.00, -0.95, 1.42, 0.00, -1.01], + [-3.08, -1.42, -3.14, 0.00, -3.95, 3.90, -1.46, -2.00, 0.00, 2.87], + [-2.24, 0.00, 0.00, 0.00, 0.00, -2.38, -2.13, 1.50, 0.00, 1.35], + [0.00, 0.00, 0.00, 0.00, 1.52, 0.00, -1.79, 0.00, 0.00, 0.00], + [1.69, 0.76, 0.00, 1.29, 1.73, -0.82, -1.38, -4.65, 0.92, 0.00], + [-1.22, 0.00, 0.00, 0.00, 0.65, -1.14, 0.00, 0.00, -3.25, 0.00], + [0.97, 1.75, 0.00, -3.66, -1.28, 0.00, 1.66, 0.00, 0.00, -3.03]] +) + sampling_time = 1.0 + min_tree_size = 2 + max_tree_size = 12 + + for _ in range(10000): + tree = simulate.generate_valid_tree(rng, theta, sampling_time, min_tree_size, max_tree_size) + + + assert min_tree_size <= len(tree) <= max_tree_size + + + for node, time in tree.items(): + assert time < sampling_time + + +def test_generate_tree_no_size_constraints(): + """ + Here we test the case where there are no size requirements. + """ + + rng = np.random.default_rng() + theta = np.array([[-1.41, 0.00, 0.00, 4.91, 1.03, 0.00, -1.91, -0.74, -1.35, 1.48], + [-1.12, -2.26, 0.00, 0.82, 0.00, 0.00, 1.16, 0.00, -1.62, 0.00], + [0.00, -0.86, -2.55, 1.58, 0.00, 0.00, 1.02, -2.70, 0.00, 0.68], + [0.00, 0.00, 0.00, -3.69, 0.00, 0.00, -0.95, 1.42, 0.00, -1.01], + [-3.08, -1.42, -3.14, 0.00, -3.95, 3.90, -1.46, -2.00, 0.00, 2.87], + [-2.24, 0.00, 0.00, 0.00, 0.00, -2.38, -2.13, 1.50, 0.00, 1.35], + [0.00, 0.00, 0.00, 0.00, 1.52, 0.00, -1.79, 0.00, 0.00, 0.00], + [1.69, 0.76, 0.00, 1.29, 1.73, -0.82, -1.38, -4.65, 0.92, 0.00], + [-1.22, 0.00, 0.00, 0.00, 0.65, -1.14, 0.00, 0.00, -3.25, 0.00], + [0.97, 1.75, 0.00, -3.66, -1.28, 0.00, 1.66, 0.00, 0.00, -3.03]] +) + sampling_time = 1.0 + + for _ in range(10000): + tree = simulate.generate_valid_tree(rng, theta, sampling_time) + + for node, time in tree.items(): + assert time < sampling_time + +def test_generate_tree_no_min_size_constraint(): + """ + Here we test the case where min_tree_size is None but max_tree_size is specified. + """ + rng = np.random.default_rng() + theta = np.array([[-1.41, 0.00, 0.00, 4.91, 1.03, 0.00, -1.91, -0.74, -1.35, 1.48], + [-1.12, -2.26, 0.00, 0.82, 0.00, 0.00, 1.16, 0.00, -1.62, 0.00], + [0.00, -0.86, -2.55, 1.58, 0.00, 0.00, 1.02, -2.70, 0.00, 0.68], + [0.00, 0.00, 0.00, -3.69, 0.00, 0.00, -0.95, 1.42, 0.00, -1.01], + [-3.08, -1.42, -3.14, 0.00, -3.95, 3.90, -1.46, -2.00, 0.00, 2.87], + [-2.24, 0.00, 0.00, 0.00, 0.00, -2.38, -2.13, 1.50, 0.00, 1.35], + [0.00, 0.00, 0.00, 0.00, 1.52, 0.00, -1.79, 0.00, 0.00, 0.00], + [1.69, 0.76, 0.00, 1.29, 1.73, -0.82, -1.38, -4.65, 0.92, 0.00], + [-1.22, 0.00, 0.00, 0.00, 0.65, -1.14, 0.00, 0.00, -3.25, 0.00], + [0.97, 1.75, 0.00, -3.66, -1.28, 0.00, 1.66, 0.00, 0.00, -3.03]] +) + sampling_time = 1.0 + max_tree_size = 8 + + for _ in range(10000): + tree = simulate.generate_valid_tree(rng, theta, sampling_time, max_tree_size=max_tree_size) + assert len(tree) <= max_tree_size + + for node, time in tree.items(): + assert time < sampling_time + +def test_generate_tree_no_max_size_constraint(): + """ + Here we test the case where max_tree_size is None but min_tree_size is specified. + """ + rng = np.random.default_rng() + theta = np.array([[-1.41, 0.00, 0.00, 4.91, 1.03, 0.00, -1.91, -0.74, -1.35, 1.48], + [-1.12, -2.26, 0.00, 0.82, 0.00, 0.00, 1.16, 0.00, -1.62, 0.00], + [0.00, -0.86, -2.55, 1.58, 0.00, 0.00, 1.02, -2.70, 0.00, 0.68], + [0.00, 0.00, 0.00, -3.69, 0.00, 0.00, -0.95, 1.42, 0.00, -1.01], + [-3.08, -1.42, -3.14, 0.00, -3.95, 3.90, -1.46, -2.00, 0.00, 2.87], + [-2.24, 0.00, 0.00, 0.00, 0.00, -2.38, -2.13, 1.50, 0.00, 1.35], + [0.00, 0.00, 0.00, 0.00, 1.52, 0.00, -1.79, 0.00, 0.00, 0.00], + [1.69, 0.76, 0.00, 1.29, 1.73, -0.82, -1.38, -4.65, 0.92, 0.00], + [-1.22, 0.00, 0.00, 0.00, 0.65, -1.14, 0.00, 0.00, -3.25, 0.00], + [0.97, 1.75, 0.00, -3.66, -1.28, 0.00, 1.66, 0.00, 0.00, -3.03]] +) + sampling_time = 1.0 + min_tree_size = 3 + + for _ in range(10000): + tree = simulate.generate_valid_tree(rng, theta, sampling_time, min_tree_size=min_tree_size) + + assert len(tree) >= min_tree_size + + for node, time in tree.items(): + assert time < sampling_time + +def test_find_possible_mutations_normal(): + """ + We want to test if the possible_mutations list is correct. + """ + old_mutations = [7, 2, 5, 9] + n_mutations = 10 + possible_mutations = simulate._find_possible_mutations(old_mutations, n_mutations) + + assert possible_mutations == [1, 3, 4, 6, 8, 10] + +def test_find_possible_mutations_edge(): + + """ + We want to test if the possible_mutations list is correct. old_mutations with mutations on the edge. + """ + + old_mutations = [1, 10] + n_mutations = 10 + possible_mutations = simulate._find_possible_mutations(old_mutations, n_mutations) + + assert possible_mutations == [i for i in range(2, 10)] + +def test_find_possible_mutations_except_positive(): + """ + We want to test if an exception is correctly thrown when the old_mutations list is invalid (mutation number too large). + """ + old_mutations = [212, 1, 3, 7] + n_mutations = 10 + with pytest.raises(ValueError) as excinfo: + simulate._find_possible_mutations(old_mutations, n_mutations) + + assert str(excinfo.value) == "Invalid mutation 212 in old_mutations. It should be 0 <= mutation <= 10." + +def test_find_possible_mutations_except_negative(): + """ + We want to test if an exception is correctly thrown when the old_mutations list is invalid (mutation number too small). + """ + old_mutations = [-23, 0, 5] + n_mutations = 10 + with pytest.raises(ValueError) as excinfo: + simulate._find_possible_mutations(old_mutations, n_mutations) + + assert str(excinfo.value) == "Invalid mutation -23 in old_mutations. It should be 0 <= mutation <= 10." + + + + From d154117d367bc5b9f9a190bc4df3303e4bae93f5 Mon Sep 17 00:00:00 2001 From: Laurenz Keller Date: Wed, 27 Sep 2023 21:05:33 +0200 Subject: [PATCH 05/45] changed comment in _simulate.py --- src/pmhn/_trees/_simulate.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pmhn/_trees/_simulate.py b/src/pmhn/_trees/_simulate.py index a1203b6..c837cfa 100644 --- a/src/pmhn/_trees/_simulate.py +++ b/src/pmhn/_trees/_simulate.py @@ -41,7 +41,7 @@ def _find_possible_mutations(old_mutations: list[int], n_mutations: int) -> list Note: We assume that mutations are labeled with a number between 1 and n_mutations, so each element in old_mutations should be in that range (except for the root node = mutation 0). - If this assumption is violated, the function will produce incorrect results. + If this assumption is violated, an exception is raised. """ for mutation in old_mutations: From 39907ab78cb6f9e6b97afd244d5ff2ed92b99b86 Mon Sep 17 00:00:00 2001 From: Laurenz Keller Date: Thu, 28 Sep 2023 00:43:49 +0200 Subject: [PATCH 06/45] minor changes --- src/pmhn/_trees/_simulate.py | 12 ++++++------ src/pmhn/_trees/write_csv.py | 6 +++--- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/src/pmhn/_trees/_simulate.py b/src/pmhn/_trees/_simulate.py index c837cfa..782c4e8 100644 --- a/src/pmhn/_trees/_simulate.py +++ b/src/pmhn/_trees/_simulate.py @@ -64,7 +64,7 @@ def _simulate_tree( theta: real-valued (i.e., log-theta) matrix, shape (n_mutations, n_mutations) sampling_time: known sampling time - max_tree_size: maximum size of tree + max_tree_size: maximum size of the tree Returns: a mutation tree @@ -96,8 +96,8 @@ def _simulate_tree( for j in possible_mutations: new_node = Node(j,parent=node) # Here j lies in the range of 1 to n_mutations inclusive. - # However, Python uses 0-based indexing for arrays. Therefore, we subtract 1 from j when accessing - # elements in the log-theta matrix to correctly map the 1-indexed mutation to the 0-indexed matrix position. + # However, Python uses 0-based indexing for arrays. Therefore, we subtract 1 from j when accessing + # elements in the log-theta matrix to correctly map the 1-indexed mutation to the 0-indexed matrix position. l = theta[j - 1][j - 1] for anc in [ancestor for ancestor in node.path if ancestor.parent is not None]: l += theta[j - 1][anc.name - 1] @@ -106,7 +106,7 @@ def _simulate_tree( if waiting_time < sampling_time: node_time_map[new_node] = waiting_time U_next.append(new_node) - if len(node_time_map) == max_tree_size: + if len(node_time_map) == max_tree_size + 1: exit_while = True break if exit_while: @@ -135,8 +135,8 @@ def simulate_trees( mean_sampling_time: the mean sampling time. Can be a float (shared between all data point) or an array of shape (n_points,). - min_tree_size: minimum tree_size - max_tree_size: maximum tree_size + min_tree_size: minimum size of the trees + max_tree_size: maximum size of the trees Returns: sampling times, shape (n_points,) diff --git a/src/pmhn/_trees/write_csv.py b/src/pmhn/_trees/write_csv.py index d5e22b9..a5b888d 100644 --- a/src/pmhn/_trees/write_csv.py +++ b/src/pmhn/_trees/write_csv.py @@ -22,8 +22,8 @@ def write_trees_to_csv(trees, output_file_path): node_id = 0 for node, _ in tree_dict.items(): node_id += 1 - mutation_id = int(node.name) - parent_id = int(node.parent.name) if node.parent else node_id + mutation_id = node.name + parent_id = node.parent.name if node.parent else node_id writer.writerow([patient_id, tree_id, node_id, mutation_id, parent_id]) if __name__ == "__main__": @@ -38,7 +38,7 @@ def write_trees_to_csv(trees, output_file_path): tree_counts = [2000, 5000, 10000, 50000] min_tree_size = 2 - max_tree_size = 12 + max_tree_size = 11 for n_points in tree_counts: trees_file_path = f"/home/laukeller/BSc Thesis/pMHN/src/pmhn/_trees/trees_{n_points}.csv" From c4f96d3519c03e1a8c3ed48e782f447050774ae7 Mon Sep 17 00:00:00 2001 From: Laurenz Keller Date: Thu, 28 Sep 2023 11:38:27 +0200 Subject: [PATCH 07/45] moved files to warmup dir --- src/pmhn/_trees/_simulate.py | 4 +-- warmup/plot_all_mut_freq.py | 33 +++++++++++++++++++++++++ warmup/plot_all_trees.py | 33 +++++++++++++++++++++++++ warmup/plot_mutation_freq.py | 27 +++++++++++++++++++++ warmup/plot_trees.py | 21 ++++++++++++++++ warmup/write_csv.py | 47 ++++++++++++++++++++++++++++++++++++ 6 files changed, 163 insertions(+), 2 deletions(-) create mode 100644 warmup/plot_all_mut_freq.py create mode 100644 warmup/plot_all_trees.py create mode 100644 warmup/plot_mutation_freq.py create mode 100644 warmup/plot_trees.py create mode 100644 warmup/write_csv.py diff --git a/src/pmhn/_trees/_simulate.py b/src/pmhn/_trees/_simulate.py index 782c4e8..6685ce9 100644 --- a/src/pmhn/_trees/_simulate.py +++ b/src/pmhn/_trees/_simulate.py @@ -96,8 +96,8 @@ def _simulate_tree( for j in possible_mutations: new_node = Node(j,parent=node) # Here j lies in the range of 1 to n_mutations inclusive. - # However, Python uses 0-based indexing for arrays. Therefore, we subtract 1 from j when accessing - # elements in the log-theta matrix to correctly map the 1-indexed mutation to the 0-indexed matrix position. + # However, Python uses 0-based indexing for arrays. Therefore, we subtract 1 from j when accessing + # elements in the log-theta matrix to correctly map the 1-indexed mutation to the 0-indexed matrix position. l = theta[j - 1][j - 1] for anc in [ancestor for ancestor in node.path if ancestor.parent is not None]: l += theta[j - 1][anc.name - 1] diff --git a/warmup/plot_all_mut_freq.py b/warmup/plot_all_mut_freq.py new file mode 100644 index 0000000..b3d14e9 --- /dev/null +++ b/warmup/plot_all_mut_freq.py @@ -0,0 +1,33 @@ +import pandas as pd +import matplotlib.pyplot as plt + +paths = { + 2000: ('/home/laukeller/BSc Thesis/TreeMHN/Example/trees_2000.csv', '/home/laukeller/BSc Thesis/pMHN/src/pmhn/_trees/trees_2000.csv'), + 5000: ('/home/laukeller/BSc Thesis/TreeMHN/Example/trees_5000.csv', '/home/laukeller/BSc Thesis/pMHN/src/pmhn/_trees/trees_5000.csv'), + 10000: ('/home/laukeller/BSc Thesis/TreeMHN/Example/trees_10000.csv', '/home/laukeller/BSc Thesis/pMHN/src/pmhn/_trees/trees_10000.csv'), + 50000: ('/home/laukeller/BSc Thesis/TreeMHN/Example/trees_50000.csv', '/home/laukeller/BSc Thesis/pMHN/src/pmhn/_trees/trees_50000.csv'), +} + +fig, axs = plt.subplots(2, 2, figsize=(10, 10)) # 2x2 grid of subplots +axs = axs.ravel() + +for i, (num_trees, (r_path, python_path)) in enumerate(paths.items()): + r_trees = pd.read_csv(r_path) + python_trees = pd.read_csv(python_path) + + r_mutation_frequencies = r_trees['Mutation_ID'].value_counts().sort_index() + python_mutation_frequencies = python_trees['Mutation_ID'].value_counts().sort_index() + + bar_width = 0.35 + + r_mutation_frequencies.plot(kind='bar', width=bar_width, position=0, align='center', color='b', alpha=0.5, label='R Trees', ax=axs[i]) + python_mutation_frequencies.plot(kind='bar', width=bar_width, position=1, align='center', color='r', alpha=0.5, label='Python Trees', ax=axs[i]) + + axs[i].set_xlabel('Mutation ID') + axs[i].set_ylabel('Frequency') + axs[i].legend(loc='upper right') + axs[i].set_title(f'Mutation Frequency Distribution ({num_trees} trees)') + +plt.tight_layout() +plt.show() + diff --git a/warmup/plot_all_trees.py b/warmup/plot_all_trees.py new file mode 100644 index 0000000..acc932b --- /dev/null +++ b/warmup/plot_all_trees.py @@ -0,0 +1,33 @@ +import pandas as pd +import matplotlib.pyplot as plt + + +paths = { + 2000: ('/home/laukeller/BSc Thesis/TreeMHN/Example/trees_2000.csv', '/home/laukeller/BSc Thesis/pMHN/src/pmhn/_trees/trees_2000.csv'), + 5000: ('/home/laukeller/BSc Thesis/TreeMHN/Example/trees_5000.csv', '/home/laukeller/BSc Thesis/pMHN/src/pmhn/_trees/trees_5000.csv'), + 10000: ('/home/laukeller/BSc Thesis/TreeMHN/Example/trees_10000.csv', '/home/laukeller/BSc Thesis/pMHN/src/pmhn/_trees/trees_10000.csv'), + 50000: ('/home/laukeller/BSc Thesis/TreeMHN/Example/trees_50000.csv', '/home/laukeller/BSc Thesis/pMHN/src/pmhn/_trees/trees_50000.csv'), +} + +fig, axs = plt.subplots(2, 2, figsize=(10, 10)) # 2x2 grid of subplots +axs = axs.ravel() + +bin_edges = [x - 0.5 for x in range(2, 12)] + [11.5] # [1.5, 2.5, ..., 10.5, 11.5] + +for i, (num_trees, (r_path, python_path)) in enumerate(paths.items()): + r_trees = pd.read_csv(r_path) + python_trees = pd.read_csv(python_path) + + r_tree_sizes = r_trees.groupby('Tree_ID').size() + python_tree_sizes = python_trees.groupby('Tree_ID').size() + + axs[i].hist(r_tree_sizes, bins=bin_edges, alpha=0.5, label='R Trees') + axs[i].hist(python_tree_sizes, bins=bin_edges, alpha=0.5, label='Python Trees') + axs[i].set_xlabel('Tree Size') + axs[i].set_ylabel('Frequency') + axs[i].legend(loc='upper right') + axs[i].set_title(f'Tree Size Distribution ({num_trees} trees)') + +plt.tight_layout() +plt.show() + diff --git a/warmup/plot_mutation_freq.py b/warmup/plot_mutation_freq.py new file mode 100644 index 0000000..0bdfc28 --- /dev/null +++ b/warmup/plot_mutation_freq.py @@ -0,0 +1,27 @@ +import pandas as pd +import matplotlib.pyplot as plt + +r_trees_path ='/home/laukeller/BSc Thesis/TreeMHN/Example/trees_10000.csv' +python_trees_path ='/home/laukeller/BSc Thesis/pMHN/src/pmhn/_trees/trees_10000.csv' + +r_trees = pd.read_csv(r_trees_path) +python_trees = pd.read_csv(python_trees_path) + +r_mutation_frequencies = r_trees['Mutation_ID'].value_counts().sort_index() +python_mutation_frequencies = python_trees['Mutation_ID'].value_counts().sort_index() + +fig, ax = plt.subplots(figsize=(10, 6)) + +bar_width = 0.35 + +r_mutation_frequencies.plot(kind='bar', width=bar_width, position=0, align='center', color='b', alpha=0.5, label='R Trees', ax=ax) +python_mutation_frequencies.plot(kind='bar', width=bar_width, position=1, align='center', color='r', alpha=0.5, label='Python Trees', ax=ax) + +ax.set_xlabel('Mutation ID') +ax.set_ylabel('Frequency') +ax.set_title('Mutation Frequencies Comparison') +ax.legend() + +plt.tight_layout() +plt.show() + diff --git a/warmup/plot_trees.py b/warmup/plot_trees.py new file mode 100644 index 0000000..73dd895 --- /dev/null +++ b/warmup/plot_trees.py @@ -0,0 +1,21 @@ +import pandas as pd +import matplotlib.pyplot as plt + +r_trees_path = '/home/laukeller/BSc Thesis/TreeMHN/Example/trees_10000.csv' +python_trees_path = '/home/laukeller/BSc Thesis/pMHN/src/pmhn/_trees/trees_10000.csv' + +r_trees = pd.read_csv(r_trees_path) +python_trees = pd.read_csv(python_trees_path) + +r_tree_sizes = r_trees.groupby('Tree_ID').size() +python_tree_sizes = python_trees.groupby('Tree_ID').size() + +bin_edges = [x - 0.5 for x in range(2, 12)] + [11.5] +plt.hist(r_tree_sizes, bins=bin_edges, alpha=0.5, edgecolor='k', label='R Trees') +plt.hist(python_tree_sizes, bins=bin_edges, alpha=0.5, edgecolor='k', label='Python Trees') +plt.xlabel('Tree Size') +plt.ylabel('Frequency') +plt.legend(loc='upper right') +plt.title('Tree Size Distribution') +plt.show() + diff --git a/warmup/write_csv.py b/warmup/write_csv.py new file mode 100644 index 0000000..a5b888d --- /dev/null +++ b/warmup/write_csv.py @@ -0,0 +1,47 @@ +import csv +import numpy as np +from anytree import Node +import _simulate + +def csv_to_numpy(file_path): + with open(file_path, 'r') as file: + reader = csv.reader(file) + next(reader) + data_list = list(reader) + return np.array(data_list, dtype=float) + +def write_trees_to_csv(trees, output_file_path): + with open(output_file_path, 'w', newline='') as file: + writer = csv.writer(file) + writer.writerow(["Patient_ID", "Tree_ID", "Node_ID", "Mutation_ID", "Parent_ID"]) + + patient_id = 0 + for tree_dict in trees: + patient_id += 1 + tree_id = patient_id + node_id = 0 + for node, _ in tree_dict.items(): + node_id += 1 + mutation_id = node.name + parent_id = node.parent.name if node.parent else node_id + writer.writerow([patient_id, tree_id, node_id, mutation_id, parent_id]) + +if __name__ == "__main__": + mhn_file_path = "/home/laukeller/BSc Thesis/TreeMHN/Example/MHN_Matrix.csv" + mhn_array = csv_to_numpy(mhn_file_path) + print(mhn_array) + + rng = np.random.default_rng() + theta = mhn_array + mean_sampling_time = 1.0 + + tree_counts = [2000, 5000, 10000, 50000] + + min_tree_size = 2 + max_tree_size = 11 + for n_points in tree_counts: + trees_file_path = f"/home/laukeller/BSc Thesis/pMHN/src/pmhn/_trees/trees_{n_points}.csv" + + _, trees = _simulate.simulate_trees(rng, n_points, theta, mean_sampling_time, min_tree_size = min_tree_size, max_tree_size = max_tree_size) + write_trees_to_csv(trees, trees_file_path) + From cb5791775fe1b0e7528f9db66af6f8d994784822 Mon Sep 17 00:00:00 2001 From: Laurenz Keller Date: Thu, 28 Sep 2023 11:45:06 +0200 Subject: [PATCH 08/45] remove not needed files --- poetry.lock | 2300 ++++++++++++++++++++++++++ src/pmhn/_trees/plot_all_mut_freq.py | 33 - src/pmhn/_trees/plot_all_trees.py | 32 - src/pmhn/_trees/write_csv.py | 47 - 4 files changed, 2300 insertions(+), 112 deletions(-) create mode 100644 poetry.lock delete mode 100644 src/pmhn/_trees/plot_all_mut_freq.py delete mode 100644 src/pmhn/_trees/plot_all_trees.py delete mode 100644 src/pmhn/_trees/write_csv.py diff --git a/poetry.lock b/poetry.lock new file mode 100644 index 0000000..fc13778 --- /dev/null +++ b/poetry.lock @@ -0,0 +1,2300 @@ +# This file is automatically @generated by Poetry 1.6.1 and should not be changed by hand. + +[[package]] +name = "anytree" +version = "2.9.0" +description = "Powerful and Lightweight Python Tree Data Structure with various plugins" +optional = false +python-versions = ">=3.7.2,<4" +files = [ + {file = "anytree-2.9.0-py3-none-any.whl", hash = "sha256:7f1ad0f9b225705b780ea0593c8ff52af05df9428e7cc34b9379b879fa462663"}, + {file = "anytree-2.9.0.tar.gz", hash = "sha256:06f7bc294293da2755f4699cc5da5c92d9182a5cfae2842c83fb56f02bd427c8"}, +] + +[package.dependencies] +six = "*" + +[[package]] +name = "arviz" +version = "0.16.1" +description = "Exploratory analysis of Bayesian models" +optional = false +python-versions = ">=3.9" +files = [ + {file = "arviz-0.16.1-py3-none-any.whl", hash = "sha256:872d1d685719f81a31f94c25278cbaef314df7f6cad0671935f26a006182b8d4"}, + {file = "arviz-0.16.1.tar.gz", hash = "sha256:35bab9072f66f5a8204d2a71911d09ce05056c177f1a780de53efa2714c27575"}, +] + +[package.dependencies] +h5netcdf = ">=1.0.2" +matplotlib = ">=3.2" +numpy = ">=1.21.0,<2.0" +packaging = "*" +pandas = ">=1.3.0" +scipy = ">=1.8.0" +setuptools = ">=60.0.0" +typing-extensions = ">=4.1.0" +xarray = ">=0.21.0" +xarray-einstats = ">=0.3" + +[package.extras] +all = ["bokeh (>=1.4.0,<3.0)", "contourpy", "dask[distributed]", "netcdf4", "numba", "ujson", "xarray-datatree", "zarr (>=2.5.0)"] + +[[package]] +name = "babel" +version = "2.12.1" +description = "Internationalization utilities" +optional = false +python-versions = ">=3.7" +files = [ + {file = "Babel-2.12.1-py3-none-any.whl", hash = "sha256:b4246fb7677d3b98f501a39d43396d3cafdc8eadb045f4a31be01863f655c610"}, + {file = "Babel-2.12.1.tar.gz", hash = "sha256:cc2d99999cd01d44420ae725a21c9e3711b3aadc7976d6147f622d8581963455"}, +] + +[[package]] +name = "black" +version = "23.9.1" +description = "The uncompromising code formatter." +optional = false +python-versions = ">=3.8" +files = [ + {file = "black-23.9.1-cp310-cp310-macosx_10_16_arm64.whl", hash = "sha256:d6bc09188020c9ac2555a498949401ab35bb6bf76d4e0f8ee251694664df6301"}, + {file = "black-23.9.1-cp310-cp310-macosx_10_16_universal2.whl", hash = "sha256:13ef033794029b85dfea8032c9d3b92b42b526f1ff4bf13b2182ce4e917f5100"}, + {file = "black-23.9.1-cp310-cp310-macosx_10_16_x86_64.whl", hash = "sha256:75a2dc41b183d4872d3a500d2b9c9016e67ed95738a3624f4751a0cb4818fe71"}, + {file = "black-23.9.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:13a2e4a93bb8ca74a749b6974925c27219bb3df4d42fc45e948a5d9feb5122b7"}, + {file = "black-23.9.1-cp310-cp310-win_amd64.whl", hash = "sha256:adc3e4442eef57f99b5590b245a328aad19c99552e0bdc7f0b04db6656debd80"}, + {file = "black-23.9.1-cp311-cp311-macosx_10_16_arm64.whl", hash = "sha256:8431445bf62d2a914b541da7ab3e2b4f3bc052d2ccbf157ebad18ea126efb91f"}, + {file = "black-23.9.1-cp311-cp311-macosx_10_16_universal2.whl", hash = "sha256:8fc1ddcf83f996247505db6b715294eba56ea9372e107fd54963c7553f2b6dfe"}, + {file = "black-23.9.1-cp311-cp311-macosx_10_16_x86_64.whl", hash = "sha256:7d30ec46de88091e4316b17ae58bbbfc12b2de05e069030f6b747dfc649ad186"}, + {file = "black-23.9.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:031e8c69f3d3b09e1aa471a926a1eeb0b9071f80b17689a655f7885ac9325a6f"}, + {file = "black-23.9.1-cp311-cp311-win_amd64.whl", hash = "sha256:538efb451cd50f43aba394e9ec7ad55a37598faae3348d723b59ea8e91616300"}, + {file = "black-23.9.1-cp38-cp38-macosx_10_16_arm64.whl", hash = "sha256:638619a559280de0c2aa4d76f504891c9860bb8fa214267358f0a20f27c12948"}, + {file = "black-23.9.1-cp38-cp38-macosx_10_16_universal2.whl", hash = "sha256:a732b82747235e0542c03bf352c126052c0fbc458d8a239a94701175b17d4855"}, + {file = "black-23.9.1-cp38-cp38-macosx_10_16_x86_64.whl", hash = "sha256:cf3a4d00e4cdb6734b64bf23cd4341421e8953615cba6b3670453737a72ec204"}, + {file = "black-23.9.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cf99f3de8b3273a8317681d8194ea222f10e0133a24a7548c73ce44ea1679377"}, + {file = "black-23.9.1-cp38-cp38-win_amd64.whl", hash = "sha256:14f04c990259576acd093871e7e9b14918eb28f1866f91968ff5524293f9c573"}, + {file = "black-23.9.1-cp39-cp39-macosx_10_16_arm64.whl", hash = "sha256:c619f063c2d68f19b2d7270f4cf3192cb81c9ec5bc5ba02df91471d0b88c4c5c"}, + {file = "black-23.9.1-cp39-cp39-macosx_10_16_universal2.whl", hash = "sha256:6a3b50e4b93f43b34a9d3ef00d9b6728b4a722c997c99ab09102fd5efdb88325"}, + {file = "black-23.9.1-cp39-cp39-macosx_10_16_x86_64.whl", hash = "sha256:c46767e8df1b7beefb0899c4a95fb43058fa8500b6db144f4ff3ca38eb2f6393"}, + {file = "black-23.9.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:50254ebfa56aa46a9fdd5d651f9637485068a1adf42270148cd101cdf56e0ad9"}, + {file = "black-23.9.1-cp39-cp39-win_amd64.whl", hash = "sha256:403397c033adbc45c2bd41747da1f7fc7eaa44efbee256b53842470d4ac5a70f"}, + {file = "black-23.9.1-py3-none-any.whl", hash = "sha256:6ccd59584cc834b6d127628713e4b6b968e5f79572da66284532525a042549f9"}, + {file = "black-23.9.1.tar.gz", hash = "sha256:24b6b3ff5c6d9ea08a8888f6977eae858e1f340d7260cf56d70a49823236b62d"}, +] + +[package.dependencies] +click = ">=8.0.0" +mypy-extensions = ">=0.4.3" +packaging = ">=22.0" +pathspec = ">=0.9.0" +platformdirs = ">=2" +tomli = {version = ">=1.1.0", markers = "python_version < \"3.11\""} +typing-extensions = {version = ">=4.0.1", markers = "python_version < \"3.11\""} + +[package.extras] +colorama = ["colorama (>=0.4.3)"] +d = ["aiohttp (>=3.7.4)"] +jupyter = ["ipython (>=7.8.0)", "tokenize-rt (>=3.2.0)"] +uvloop = ["uvloop (>=0.15.2)"] + +[[package]] +name = "cachetools" +version = "5.3.1" +description = "Extensible memoizing collections and decorators" +optional = false +python-versions = ">=3.7" +files = [ + {file = "cachetools-5.3.1-py3-none-any.whl", hash = "sha256:95ef631eeaea14ba2e36f06437f36463aac3a096799e876ee55e5cdccb102590"}, + {file = "cachetools-5.3.1.tar.gz", hash = "sha256:dce83f2d9b4e1f732a8cd44af8e8fab2dbe46201467fc98b3ef8f269092bf62b"}, +] + +[[package]] +name = "certifi" +version = "2023.7.22" +description = "Python package for providing Mozilla's CA Bundle." +optional = false +python-versions = ">=3.6" +files = [ + {file = "certifi-2023.7.22-py3-none-any.whl", hash = "sha256:92d6037539857d8206b8f6ae472e8b77db8058fec5937a1ef3f54304089edbb9"}, + {file = "certifi-2023.7.22.tar.gz", hash = "sha256:539cc1d13202e33ca466e88b2807e29f4c13049d6d87031a3c110744495cb082"}, +] + +[[package]] +name = "cfgv" +version = "3.4.0" +description = "Validate configuration and produce human readable error messages." +optional = false +python-versions = ">=3.8" +files = [ + {file = "cfgv-3.4.0-py2.py3-none-any.whl", hash = "sha256:b7265b1f29fd3316bfcd2b330d63d024f2bfd8bcb8b0272f8e19a504856c48f9"}, + {file = "cfgv-3.4.0.tar.gz", hash = "sha256:e52591d4c5f5dead8e0f673fb16db7949d2cfb3f7da4582893288f0ded8fe560"}, +] + +[[package]] +name = "cftime" +version = "1.6.2" +description = "Time-handling functionality from netcdf4-python" +optional = false +python-versions = ">=3.7" +files = [ + {file = "cftime-1.6.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:b4d2a1920f0aad663f25700b30621ff64af373499e52b544da1148dd8c09409a"}, + {file = "cftime-1.6.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:2ba7909a0cd4adcb16797d8d6ab2767e7ddb980b2bf9dbabfc71b3bdd94f072b"}, + {file = "cftime-1.6.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:acb294fdb80e33545ae54b4421df35c4e578708a5ffce1c00408b2294e70ecef"}, + {file = "cftime-1.6.2-cp310-cp310-win_amd64.whl", hash = "sha256:2abdac6ca5b8b6102f319122546739dfc42406b816c16f2a98a8f0cd406d3bf0"}, + {file = "cftime-1.6.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:eb7f8cd0996640b83020133b5ef6b97fc9216c3129eaeeaca361abdff5d82166"}, + {file = "cftime-1.6.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:8d49d69c64cee2c175478eed84c3a57fce083da4ceebce16440f72be561a8489"}, + {file = "cftime-1.6.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:455cec3627e6ca8694b0d9201da6581eb4381b58389f1fbcb51a14fa0e2b3d94"}, + {file = "cftime-1.6.2-cp311-cp311-win_amd64.whl", hash = "sha256:29c18601abea0fd160fbe423e05c7a56fe1d38dd250a6b010de499a132d3fe18"}, + {file = "cftime-1.6.2-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:afb5b38b51b8bc02f1656a9f15c52b0b20a3999adbe1ab9ac57f926e0065b48a"}, + {file = "cftime-1.6.2-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:aedfb7a783d19d7a30cb41951310f3bfe98f9f21fffc723c8af08a11962b0b17"}, + {file = "cftime-1.6.2-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3042048324b4d6a1066c978ec78101effdd84320e8862bfdbf8122d7ad7588ec"}, + {file = "cftime-1.6.2-cp37-none-win_amd64.whl", hash = "sha256:ee70fa069802652cf534de1dd3fc590b7d22d4127447bf96ac9849abcdadadf1"}, + {file = "cftime-1.6.2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:93f00f454329c1f2588ebca2650e8edf7607d6189dbdcc81b5f3be2080155cc4"}, + {file = "cftime-1.6.2-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:e83db2fdda900eb154a9f79dfb665ac6190781c61d2e18151996de5ee7ffd8a2"}, + {file = "cftime-1.6.2-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:56d0242fc4990584b265622622b25bb262a178097711d2d95e53ef52a9d23e7e"}, + {file = "cftime-1.6.2-cp38-cp38-win_amd64.whl", hash = "sha256:055d5d60a756c6c1857cf84d77655bb707057bb6c4a4fbb104a550e76c40aad9"}, + {file = "cftime-1.6.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:0955e1f3e1c09a9e0296b50f135ff9719cb2466f81c8ad4a10ef06fa394de984"}, + {file = "cftime-1.6.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:07fdef2f75a0f0952b0376fa4cd08ef8a1dad3b963976ac07517811d434936b7"}, + {file = "cftime-1.6.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:892d5dc38f8b998c83a2a01f131e63896d020586de473e1878f9e85acc70ad44"}, + {file = "cftime-1.6.2-cp39-cp39-win_amd64.whl", hash = "sha256:86fe550b94525c327578a90b2e13418ca5ba6c636d5efe3edec310e631757eea"}, + {file = "cftime-1.6.2.tar.gz", hash = "sha256:8614c00fb8a5046de304fdd86dbd224f99408185d7b245ac6628d0276596e6d2"}, +] + +[package.dependencies] +numpy = ">1.13.3" + +[[package]] +name = "charset-normalizer" +version = "3.2.0" +description = "The Real First Universal Charset Detector. Open, modern and actively maintained alternative to Chardet." +optional = false +python-versions = ">=3.7.0" +files = [ + {file = "charset-normalizer-3.2.0.tar.gz", hash = "sha256:3bb3d25a8e6c0aedd251753a79ae98a093c7e7b471faa3aa9a93a81431987ace"}, + {file = "charset_normalizer-3.2.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:0b87549028f680ca955556e3bd57013ab47474c3124dc069faa0b6545b6c9710"}, + {file = "charset_normalizer-3.2.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:7c70087bfee18a42b4040bb9ec1ca15a08242cf5867c58726530bdf3945672ed"}, + {file = "charset_normalizer-3.2.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:a103b3a7069b62f5d4890ae1b8f0597618f628b286b03d4bc9195230b154bfa9"}, + {file = "charset_normalizer-3.2.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:94aea8eff76ee6d1cdacb07dd2123a68283cb5569e0250feab1240058f53b623"}, + {file = "charset_normalizer-3.2.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:db901e2ac34c931d73054d9797383d0f8009991e723dab15109740a63e7f902a"}, + {file = "charset_normalizer-3.2.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b0dac0ff919ba34d4df1b6131f59ce95b08b9065233446be7e459f95554c0dc8"}, + {file = "charset_normalizer-3.2.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:193cbc708ea3aca45e7221ae58f0fd63f933753a9bfb498a3b474878f12caaad"}, + {file = "charset_normalizer-3.2.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:09393e1b2a9461950b1c9a45d5fd251dc7c6f228acab64da1c9c0165d9c7765c"}, + {file = "charset_normalizer-3.2.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:baacc6aee0b2ef6f3d308e197b5d7a81c0e70b06beae1f1fcacffdbd124fe0e3"}, + {file = "charset_normalizer-3.2.0-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:bf420121d4c8dce6b889f0e8e4ec0ca34b7f40186203f06a946fa0276ba54029"}, + {file = "charset_normalizer-3.2.0-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:c04a46716adde8d927adb9457bbe39cf473e1e2c2f5d0a16ceb837e5d841ad4f"}, + {file = "charset_normalizer-3.2.0-cp310-cp310-musllinux_1_1_s390x.whl", hash = "sha256:aaf63899c94de41fe3cf934601b0f7ccb6b428c6e4eeb80da72c58eab077b19a"}, + {file = "charset_normalizer-3.2.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:d62e51710986674142526ab9f78663ca2b0726066ae26b78b22e0f5e571238dd"}, + {file = "charset_normalizer-3.2.0-cp310-cp310-win32.whl", hash = "sha256:04e57ab9fbf9607b77f7d057974694b4f6b142da9ed4a199859d9d4d5c63fe96"}, + {file = "charset_normalizer-3.2.0-cp310-cp310-win_amd64.whl", hash = "sha256:48021783bdf96e3d6de03a6e39a1171ed5bd7e8bb93fc84cc649d11490f87cea"}, + {file = "charset_normalizer-3.2.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:4957669ef390f0e6719db3613ab3a7631e68424604a7b448f079bee145da6e09"}, + {file = "charset_normalizer-3.2.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:46fb8c61d794b78ec7134a715a3e564aafc8f6b5e338417cb19fe9f57a5a9bf2"}, + {file = "charset_normalizer-3.2.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:f779d3ad205f108d14e99bb3859aa7dd8e9c68874617c72354d7ecaec2a054ac"}, + {file = "charset_normalizer-3.2.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f25c229a6ba38a35ae6e25ca1264621cc25d4d38dca2942a7fce0b67a4efe918"}, + {file = "charset_normalizer-3.2.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:2efb1bd13885392adfda4614c33d3b68dee4921fd0ac1d3988f8cbb7d589e72a"}, + {file = "charset_normalizer-3.2.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:1f30b48dd7fa1474554b0b0f3fdfdd4c13b5c737a3c6284d3cdc424ec0ffff3a"}, + {file = "charset_normalizer-3.2.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:246de67b99b6851627d945db38147d1b209a899311b1305dd84916f2b88526c6"}, + {file = "charset_normalizer-3.2.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:9bd9b3b31adcb054116447ea22caa61a285d92e94d710aa5ec97992ff5eb7cf3"}, + {file = "charset_normalizer-3.2.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:8c2f5e83493748286002f9369f3e6607c565a6a90425a3a1fef5ae32a36d749d"}, + {file = "charset_normalizer-3.2.0-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:3170c9399da12c9dc66366e9d14da8bf7147e1e9d9ea566067bbce7bb74bd9c2"}, + {file = "charset_normalizer-3.2.0-cp311-cp311-musllinux_1_1_ppc64le.whl", hash = "sha256:7a4826ad2bd6b07ca615c74ab91f32f6c96d08f6fcc3902ceeedaec8cdc3bcd6"}, + {file = "charset_normalizer-3.2.0-cp311-cp311-musllinux_1_1_s390x.whl", hash = "sha256:3b1613dd5aee995ec6d4c69f00378bbd07614702a315a2cf6c1d21461fe17c23"}, + {file = "charset_normalizer-3.2.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:9e608aafdb55eb9f255034709e20d5a83b6d60c054df0802fa9c9883d0a937aa"}, + {file = "charset_normalizer-3.2.0-cp311-cp311-win32.whl", hash = "sha256:f2a1d0fd4242bd8643ce6f98927cf9c04540af6efa92323e9d3124f57727bfc1"}, + {file = "charset_normalizer-3.2.0-cp311-cp311-win_amd64.whl", hash = "sha256:681eb3d7e02e3c3655d1b16059fbfb605ac464c834a0c629048a30fad2b27489"}, + {file = "charset_normalizer-3.2.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:c57921cda3a80d0f2b8aec7e25c8aa14479ea92b5b51b6876d975d925a2ea346"}, + {file = "charset_normalizer-3.2.0-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:41b25eaa7d15909cf3ac4c96088c1f266a9a93ec44f87f1d13d4a0e86c81b982"}, + {file = "charset_normalizer-3.2.0-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f058f6963fd82eb143c692cecdc89e075fa0828db2e5b291070485390b2f1c9c"}, + {file = "charset_normalizer-3.2.0-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a7647ebdfb9682b7bb97e2a5e7cb6ae735b1c25008a70b906aecca294ee96cf4"}, + {file = "charset_normalizer-3.2.0-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:eef9df1eefada2c09a5e7a40991b9fc6ac6ef20b1372abd48d2794a316dc0449"}, + {file = "charset_normalizer-3.2.0-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e03b8895a6990c9ab2cdcd0f2fe44088ca1c65ae592b8f795c3294af00a461c3"}, + {file = "charset_normalizer-3.2.0-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:ee4006268ed33370957f55bf2e6f4d263eaf4dc3cfc473d1d90baff6ed36ce4a"}, + {file = "charset_normalizer-3.2.0-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:c4983bf937209c57240cff65906b18bb35e64ae872da6a0db937d7b4af845dd7"}, + {file = "charset_normalizer-3.2.0-cp37-cp37m-musllinux_1_1_ppc64le.whl", hash = "sha256:3bb7fda7260735efe66d5107fb7e6af6a7c04c7fce9b2514e04b7a74b06bf5dd"}, + {file = "charset_normalizer-3.2.0-cp37-cp37m-musllinux_1_1_s390x.whl", hash = "sha256:72814c01533f51d68702802d74f77ea026b5ec52793c791e2da806a3844a46c3"}, + {file = "charset_normalizer-3.2.0-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:70c610f6cbe4b9fce272c407dd9d07e33e6bf7b4aa1b7ffb6f6ded8e634e3592"}, + {file = "charset_normalizer-3.2.0-cp37-cp37m-win32.whl", hash = "sha256:a401b4598e5d3f4a9a811f3daf42ee2291790c7f9d74b18d75d6e21dda98a1a1"}, + {file = "charset_normalizer-3.2.0-cp37-cp37m-win_amd64.whl", hash = "sha256:c0b21078a4b56965e2b12f247467b234734491897e99c1d51cee628da9786959"}, + {file = "charset_normalizer-3.2.0-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:95eb302ff792e12aba9a8b8f8474ab229a83c103d74a750ec0bd1c1eea32e669"}, + {file = "charset_normalizer-3.2.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:1a100c6d595a7f316f1b6f01d20815d916e75ff98c27a01ae817439ea7726329"}, + {file = "charset_normalizer-3.2.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:6339d047dab2780cc6220f46306628e04d9750f02f983ddb37439ca47ced7149"}, + {file = "charset_normalizer-3.2.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e4b749b9cc6ee664a3300bb3a273c1ca8068c46be705b6c31cf5d276f8628a94"}, + {file = "charset_normalizer-3.2.0-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a38856a971c602f98472050165cea2cdc97709240373041b69030be15047691f"}, + {file = "charset_normalizer-3.2.0-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f87f746ee241d30d6ed93969de31e5ffd09a2961a051e60ae6bddde9ec3583aa"}, + {file = "charset_normalizer-3.2.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:89f1b185a01fe560bc8ae5f619e924407efca2191b56ce749ec84982fc59a32a"}, + {file = "charset_normalizer-3.2.0-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e1c8a2f4c69e08e89632defbfabec2feb8a8d99edc9f89ce33c4b9e36ab63037"}, + {file = "charset_normalizer-3.2.0-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:2f4ac36d8e2b4cc1aa71df3dd84ff8efbe3bfb97ac41242fbcfc053c67434f46"}, + {file = "charset_normalizer-3.2.0-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:a386ebe437176aab38c041de1260cd3ea459c6ce5263594399880bbc398225b2"}, + {file = "charset_normalizer-3.2.0-cp38-cp38-musllinux_1_1_ppc64le.whl", hash = "sha256:ccd16eb18a849fd8dcb23e23380e2f0a354e8daa0c984b8a732d9cfaba3a776d"}, + {file = "charset_normalizer-3.2.0-cp38-cp38-musllinux_1_1_s390x.whl", hash = "sha256:e6a5bf2cba5ae1bb80b154ed68a3cfa2fa00fde979a7f50d6598d3e17d9ac20c"}, + {file = "charset_normalizer-3.2.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:45de3f87179c1823e6d9e32156fb14c1927fcc9aba21433f088fdfb555b77c10"}, + {file = "charset_normalizer-3.2.0-cp38-cp38-win32.whl", hash = "sha256:1000fba1057b92a65daec275aec30586c3de2401ccdcd41f8a5c1e2c87078706"}, + {file = "charset_normalizer-3.2.0-cp38-cp38-win_amd64.whl", hash = "sha256:8b2c760cfc7042b27ebdb4a43a4453bd829a5742503599144d54a032c5dc7e9e"}, + {file = "charset_normalizer-3.2.0-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:855eafa5d5a2034b4621c74925d89c5efef61418570e5ef9b37717d9c796419c"}, + {file = "charset_normalizer-3.2.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:203f0c8871d5a7987be20c72442488a0b8cfd0f43b7973771640fc593f56321f"}, + {file = "charset_normalizer-3.2.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:e857a2232ba53ae940d3456f7533ce6ca98b81917d47adc3c7fd55dad8fab858"}, + {file = "charset_normalizer-3.2.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5e86d77b090dbddbe78867a0275cb4df08ea195e660f1f7f13435a4649e954e5"}, + {file = "charset_normalizer-3.2.0-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c4fb39a81950ec280984b3a44f5bd12819953dc5fa3a7e6fa7a80db5ee853952"}, + {file = "charset_normalizer-3.2.0-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2dee8e57f052ef5353cf608e0b4c871aee320dd1b87d351c28764fc0ca55f9f4"}, + {file = "charset_normalizer-3.2.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8700f06d0ce6f128de3ccdbc1acaea1ee264d2caa9ca05daaf492fde7c2a7200"}, + {file = "charset_normalizer-3.2.0-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1920d4ff15ce893210c1f0c0e9d19bfbecb7983c76b33f046c13a8ffbd570252"}, + {file = "charset_normalizer-3.2.0-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:c1c76a1743432b4b60ab3358c937a3fe1341c828ae6194108a94c69028247f22"}, + {file = "charset_normalizer-3.2.0-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:f7560358a6811e52e9c4d142d497f1a6e10103d3a6881f18d04dbce3729c0e2c"}, + {file = "charset_normalizer-3.2.0-cp39-cp39-musllinux_1_1_ppc64le.whl", hash = "sha256:c8063cf17b19661471ecbdb3df1c84f24ad2e389e326ccaf89e3fb2484d8dd7e"}, + {file = "charset_normalizer-3.2.0-cp39-cp39-musllinux_1_1_s390x.whl", hash = "sha256:cd6dbe0238f7743d0efe563ab46294f54f9bc8f4b9bcf57c3c666cc5bc9d1299"}, + {file = "charset_normalizer-3.2.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:1249cbbf3d3b04902ff081ffbb33ce3377fa6e4c7356f759f3cd076cc138d020"}, + {file = "charset_normalizer-3.2.0-cp39-cp39-win32.whl", hash = "sha256:6c409c0deba34f147f77efaa67b8e4bb83d2f11c8806405f76397ae5b8c0d1c9"}, + {file = "charset_normalizer-3.2.0-cp39-cp39-win_amd64.whl", hash = "sha256:7095f6fbfaa55defb6b733cfeb14efaae7a29f0b59d8cf213be4e7ca0b857b80"}, + {file = "charset_normalizer-3.2.0-py3-none-any.whl", hash = "sha256:8e098148dd37b4ce3baca71fb394c81dc5d9c7728c95df695d2dca218edf40e6"}, +] + +[[package]] +name = "click" +version = "8.1.7" +description = "Composable command line interface toolkit" +optional = false +python-versions = ">=3.7" +files = [ + {file = "click-8.1.7-py3-none-any.whl", hash = "sha256:ae74fb96c20a0277a1d615f1e4d73c8414f5a98db8b799a7931d1582f3390c28"}, + {file = "click-8.1.7.tar.gz", hash = "sha256:ca9853ad459e787e2192211578cc907e7594e294c7ccc834310722b41b9ca6de"}, +] + +[package.dependencies] +colorama = {version = "*", markers = "platform_system == \"Windows\""} + +[[package]] +name = "cloudpickle" +version = "2.2.1" +description = "Extended pickling support for Python objects" +optional = false +python-versions = ">=3.6" +files = [ + {file = "cloudpickle-2.2.1-py3-none-any.whl", hash = "sha256:61f594d1f4c295fa5cd9014ceb3a1fc4a70b0de1164b94fbc2d854ccba056f9f"}, + {file = "cloudpickle-2.2.1.tar.gz", hash = "sha256:d89684b8de9e34a2a43b3460fbca07d09d6e25ce858df4d5a44240403b6178f5"}, +] + +[[package]] +name = "colorama" +version = "0.4.6" +description = "Cross-platform colored terminal text." +optional = false +python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7" +files = [ + {file = "colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6"}, + {file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"}, +] + +[[package]] +name = "cons" +version = "0.4.6" +description = "An implementation of Lisp/Scheme-like cons in Python." +optional = false +python-versions = ">=3.6" +files = [ + {file = "cons-0.4.6.tar.gz", hash = "sha256:669fe9d5ee916d5e42b9cac6acc911df803d04f2e945c1604982a04d27a29b47"}, +] + +[package.dependencies] +logical-unification = ">=0.4.0" + +[[package]] +name = "contourpy" +version = "1.1.0" +description = "Python library for calculating contours of 2D quadrilateral grids" +optional = false +python-versions = ">=3.8" +files = [ + {file = "contourpy-1.1.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:89f06eff3ce2f4b3eb24c1055a26981bffe4e7264acd86f15b97e40530b794bc"}, + {file = "contourpy-1.1.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:dffcc2ddec1782dd2f2ce1ef16f070861af4fb78c69862ce0aab801495dda6a3"}, + {file = "contourpy-1.1.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:25ae46595e22f93592d39a7eac3d638cda552c3e1160255258b695f7b58e5655"}, + {file = "contourpy-1.1.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:17cfaf5ec9862bc93af1ec1f302457371c34e688fbd381f4035a06cd47324f48"}, + {file = "contourpy-1.1.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:18a64814ae7bce73925131381603fff0116e2df25230dfc80d6d690aa6e20b37"}, + {file = "contourpy-1.1.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:90c81f22b4f572f8a2110b0b741bb64e5a6427e0a198b2cdc1fbaf85f352a3aa"}, + {file = "contourpy-1.1.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:53cc3a40635abedbec7f1bde60f8c189c49e84ac180c665f2cd7c162cc454baa"}, + {file = "contourpy-1.1.0-cp310-cp310-win32.whl", hash = "sha256:9b2dd2ca3ac561aceef4c7c13ba654aaa404cf885b187427760d7f7d4c57cff8"}, + {file = "contourpy-1.1.0-cp310-cp310-win_amd64.whl", hash = "sha256:1f795597073b09d631782e7245016a4323cf1cf0b4e06eef7ea6627e06a37ff2"}, + {file = "contourpy-1.1.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:0b7b04ed0961647691cfe5d82115dd072af7ce8846d31a5fac6c142dcce8b882"}, + {file = "contourpy-1.1.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:27bc79200c742f9746d7dd51a734ee326a292d77e7d94c8af6e08d1e6c15d545"}, + {file = "contourpy-1.1.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:052cc634bf903c604ef1a00a5aa093c54f81a2612faedaa43295809ffdde885e"}, + {file = "contourpy-1.1.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9382a1c0bc46230fb881c36229bfa23d8c303b889b788b939365578d762b5c18"}, + {file = "contourpy-1.1.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e5cec36c5090e75a9ac9dbd0ff4a8cf7cecd60f1b6dc23a374c7d980a1cd710e"}, + {file = "contourpy-1.1.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1f0cbd657e9bde94cd0e33aa7df94fb73c1ab7799378d3b3f902eb8eb2e04a3a"}, + {file = "contourpy-1.1.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:181cbace49874f4358e2929aaf7ba84006acb76694102e88dd15af861996c16e"}, + {file = "contourpy-1.1.0-cp311-cp311-win32.whl", hash = "sha256:edb989d31065b1acef3828a3688f88b2abb799a7db891c9e282df5ec7e46221b"}, + {file = "contourpy-1.1.0-cp311-cp311-win_amd64.whl", hash = "sha256:fb3b7d9e6243bfa1efb93ccfe64ec610d85cfe5aec2c25f97fbbd2e58b531256"}, + {file = "contourpy-1.1.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:bcb41692aa09aeb19c7c213411854402f29f6613845ad2453d30bf421fe68fed"}, + {file = "contourpy-1.1.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:5d123a5bc63cd34c27ff9c7ac1cd978909e9c71da12e05be0231c608048bb2ae"}, + {file = "contourpy-1.1.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:62013a2cf68abc80dadfd2307299bfa8f5aa0dcaec5b2954caeb5fa094171103"}, + {file = "contourpy-1.1.0-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:0b6616375d7de55797d7a66ee7d087efe27f03d336c27cf1f32c02b8c1a5ac70"}, + {file = "contourpy-1.1.0-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:317267d915490d1e84577924bd61ba71bf8681a30e0d6c545f577363157e5e94"}, + {file = "contourpy-1.1.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d551f3a442655f3dcc1285723f9acd646ca5858834efeab4598d706206b09c9f"}, + {file = "contourpy-1.1.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:e7a117ce7df5a938fe035cad481b0189049e8d92433b4b33aa7fc609344aafa1"}, + {file = "contourpy-1.1.0-cp38-cp38-win32.whl", hash = "sha256:108dfb5b3e731046a96c60bdc46a1a0ebee0760418951abecbe0fc07b5b93b27"}, + {file = "contourpy-1.1.0-cp38-cp38-win_amd64.whl", hash = "sha256:d4f26b25b4f86087e7d75e63212756c38546e70f2a92d2be44f80114826e1cd4"}, + {file = "contourpy-1.1.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:bc00bb4225d57bff7ebb634646c0ee2a1298402ec10a5fe7af79df9a51c1bfd9"}, + {file = "contourpy-1.1.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:189ceb1525eb0655ab8487a9a9c41f42a73ba52d6789754788d1883fb06b2d8a"}, + {file = "contourpy-1.1.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9f2931ed4741f98f74b410b16e5213f71dcccee67518970c42f64153ea9313b9"}, + {file = "contourpy-1.1.0-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:30f511c05fab7f12e0b1b7730ebdc2ec8deedcfb505bc27eb570ff47c51a8f15"}, + {file = "contourpy-1.1.0-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:143dde50520a9f90e4a2703f367cf8ec96a73042b72e68fcd184e1279962eb6f"}, + {file = "contourpy-1.1.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e94bef2580e25b5fdb183bf98a2faa2adc5b638736b2c0a4da98691da641316a"}, + {file = "contourpy-1.1.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:ed614aea8462735e7d70141374bd7650afd1c3f3cb0c2dbbcbe44e14331bf002"}, + {file = "contourpy-1.1.0-cp39-cp39-win32.whl", hash = "sha256:71551f9520f008b2950bef5f16b0e3587506ef4f23c734b71ffb7b89f8721999"}, + {file = "contourpy-1.1.0-cp39-cp39-win_amd64.whl", hash = "sha256:438ba416d02f82b692e371858143970ed2eb6337d9cdbbede0d8ad9f3d7dd17d"}, + {file = "contourpy-1.1.0-pp38-pypy38_pp73-macosx_10_9_x86_64.whl", hash = "sha256:a698c6a7a432789e587168573a864a7ea374c6be8d4f31f9d87c001d5a843493"}, + {file = "contourpy-1.1.0-pp38-pypy38_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:397b0ac8a12880412da3551a8cb5a187d3298a72802b45a3bd1805e204ad8439"}, + {file = "contourpy-1.1.0-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:a67259c2b493b00e5a4d0f7bfae51fb4b3371395e47d079a4446e9b0f4d70e76"}, + {file = "contourpy-1.1.0-pp39-pypy39_pp73-macosx_10_9_x86_64.whl", hash = "sha256:2b836d22bd2c7bb2700348e4521b25e077255ebb6ab68e351ab5aa91ca27e027"}, + {file = "contourpy-1.1.0-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:084eaa568400cfaf7179b847ac871582199b1b44d5699198e9602ecbbb5f6104"}, + {file = "contourpy-1.1.0-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:911ff4fd53e26b019f898f32db0d4956c9d227d51338fb3b03ec72ff0084ee5f"}, + {file = "contourpy-1.1.0.tar.gz", hash = "sha256:e53046c3863828d21d531cc3b53786e6580eb1ba02477e8681009b6aa0870b21"}, +] + +[package.dependencies] +numpy = ">=1.16" + +[package.extras] +bokeh = ["bokeh", "selenium"] +docs = ["furo", "sphinx-copybutton"] +mypy = ["contourpy[bokeh,docs]", "docutils-stubs", "mypy (==1.2.0)", "types-Pillow"] +test = ["Pillow", "contourpy[test-no-images]", "matplotlib"] +test-no-images = ["pytest", "pytest-cov", "wurlitzer"] + +[[package]] +name = "contourpy" +version = "1.1.1" +description = "Python library for calculating contours of 2D quadrilateral grids" +optional = false +python-versions = ">=3.8" +files = [ + {file = "contourpy-1.1.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:46e24f5412c948d81736509377e255f6040e94216bf1a9b5ea1eaa9d29f6ec1b"}, + {file = "contourpy-1.1.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:0e48694d6a9c5a26ee85b10130c77a011a4fedf50a7279fa0bdaf44bafb4299d"}, + {file = "contourpy-1.1.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a66045af6cf00e19d02191ab578a50cb93b2028c3eefed999793698e9ea768ae"}, + {file = "contourpy-1.1.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4ebf42695f75ee1a952f98ce9775c873e4971732a87334b099dde90b6af6a916"}, + {file = "contourpy-1.1.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f6aec19457617ef468ff091669cca01fa7ea557b12b59a7908b9474bb9674cf0"}, + {file = "contourpy-1.1.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:462c59914dc6d81e0b11f37e560b8a7c2dbab6aca4f38be31519d442d6cde1a1"}, + {file = "contourpy-1.1.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:6d0a8efc258659edc5299f9ef32d8d81de8b53b45d67bf4bfa3067f31366764d"}, + {file = "contourpy-1.1.1-cp310-cp310-win32.whl", hash = "sha256:d6ab42f223e58b7dac1bb0af32194a7b9311065583cc75ff59dcf301afd8a431"}, + {file = "contourpy-1.1.1-cp310-cp310-win_amd64.whl", hash = "sha256:549174b0713d49871c6dee90a4b499d3f12f5e5f69641cd23c50a4542e2ca1eb"}, + {file = "contourpy-1.1.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:407d864db716a067cc696d61fa1ef6637fedf03606e8417fe2aeed20a061e6b2"}, + {file = "contourpy-1.1.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:dfe80c017973e6a4c367e037cb31601044dd55e6bfacd57370674867d15a899b"}, + {file = "contourpy-1.1.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e30aaf2b8a2bac57eb7e1650df1b3a4130e8d0c66fc2f861039d507a11760e1b"}, + {file = "contourpy-1.1.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3de23ca4f381c3770dee6d10ead6fff524d540c0f662e763ad1530bde5112532"}, + {file = "contourpy-1.1.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:566f0e41df06dfef2431defcfaa155f0acfa1ca4acbf8fd80895b1e7e2ada40e"}, + {file = "contourpy-1.1.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b04c2f0adaf255bf756cf08ebef1be132d3c7a06fe6f9877d55640c5e60c72c5"}, + {file = "contourpy-1.1.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:d0c188ae66b772d9d61d43c6030500344c13e3f73a00d1dc241da896f379bb62"}, + {file = "contourpy-1.1.1-cp311-cp311-win32.whl", hash = "sha256:0683e1ae20dc038075d92e0e0148f09ffcefab120e57f6b4c9c0f477ec171f33"}, + {file = "contourpy-1.1.1-cp311-cp311-win_amd64.whl", hash = "sha256:8636cd2fc5da0fb102a2504fa2c4bea3cbc149533b345d72cdf0e7a924decc45"}, + {file = "contourpy-1.1.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:560f1d68a33e89c62da5da4077ba98137a5e4d3a271b29f2f195d0fba2adcb6a"}, + {file = "contourpy-1.1.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:24216552104ae8f3b34120ef84825400b16eb6133af2e27a190fdc13529f023e"}, + {file = "contourpy-1.1.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:56de98a2fb23025882a18b60c7f0ea2d2d70bbbcfcf878f9067234b1c4818442"}, + {file = "contourpy-1.1.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:07d6f11dfaf80a84c97f1a5ba50d129d9303c5b4206f776e94037332e298dda8"}, + {file = "contourpy-1.1.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f1eaac5257a8f8a047248d60e8f9315c6cff58f7803971170d952555ef6344a7"}, + {file = "contourpy-1.1.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:19557fa407e70f20bfaba7d55b4d97b14f9480856c4fb65812e8a05fe1c6f9bf"}, + {file = "contourpy-1.1.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:081f3c0880712e40effc5f4c3b08feca6d064cb8cfbb372ca548105b86fd6c3d"}, + {file = "contourpy-1.1.1-cp312-cp312-win32.whl", hash = "sha256:059c3d2a94b930f4dafe8105bcdc1b21de99b30b51b5bce74c753686de858cb6"}, + {file = "contourpy-1.1.1-cp312-cp312-win_amd64.whl", hash = "sha256:f44d78b61740e4e8c71db1cf1fd56d9050a4747681c59ec1094750a658ceb970"}, + {file = "contourpy-1.1.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:70e5a10f8093d228bb2b552beeb318b8928b8a94763ef03b858ef3612b29395d"}, + {file = "contourpy-1.1.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:8394e652925a18ef0091115e3cc191fef350ab6dc3cc417f06da66bf98071ae9"}, + {file = "contourpy-1.1.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c5bd5680f844c3ff0008523a71949a3ff5e4953eb7701b28760805bc9bcff217"}, + {file = "contourpy-1.1.1-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:66544f853bfa85c0d07a68f6c648b2ec81dafd30f272565c37ab47a33b220684"}, + {file = "contourpy-1.1.1-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e0c02b75acfea5cab07585d25069207e478d12309557f90a61b5a3b4f77f46ce"}, + {file = "contourpy-1.1.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:41339b24471c58dc1499e56783fedc1afa4bb018bcd035cfb0ee2ad2a7501ef8"}, + {file = "contourpy-1.1.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:f29fb0b3f1217dfe9362ec55440d0743fe868497359f2cf93293f4b2701b8251"}, + {file = "contourpy-1.1.1-cp38-cp38-win32.whl", hash = "sha256:f9dc7f933975367251c1b34da882c4f0e0b2e24bb35dc906d2f598a40b72bfc7"}, + {file = "contourpy-1.1.1-cp38-cp38-win_amd64.whl", hash = "sha256:498e53573e8b94b1caeb9e62d7c2d053c263ebb6aa259c81050766beb50ff8d9"}, + {file = "contourpy-1.1.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:ba42e3810999a0ddd0439e6e5dbf6d034055cdc72b7c5c839f37a7c274cb4eba"}, + {file = "contourpy-1.1.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:6c06e4c6e234fcc65435223c7b2a90f286b7f1b2733058bdf1345d218cc59e34"}, + {file = "contourpy-1.1.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ca6fab080484e419528e98624fb5c4282148b847e3602dc8dbe0cb0669469887"}, + {file = "contourpy-1.1.1-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:93df44ab351119d14cd1e6b52a5063d3336f0754b72736cc63db59307dabb718"}, + {file = "contourpy-1.1.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:eafbef886566dc1047d7b3d4b14db0d5b7deb99638d8e1be4e23a7c7ac59ff0f"}, + {file = "contourpy-1.1.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:efe0fab26d598e1ec07d72cf03eaeeba8e42b4ecf6b9ccb5a356fde60ff08b85"}, + {file = "contourpy-1.1.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:f08e469821a5e4751c97fcd34bcb586bc243c39c2e39321822060ba902eac49e"}, + {file = "contourpy-1.1.1-cp39-cp39-win32.whl", hash = "sha256:bfc8a5e9238232a45ebc5cb3bfee71f1167064c8d382cadd6076f0d51cff1da0"}, + {file = "contourpy-1.1.1-cp39-cp39-win_amd64.whl", hash = "sha256:c84fdf3da00c2827d634de4fcf17e3e067490c4aea82833625c4c8e6cdea0887"}, + {file = "contourpy-1.1.1-pp38-pypy38_pp73-macosx_10_9_x86_64.whl", hash = "sha256:229a25f68046c5cf8067d6d6351c8b99e40da11b04d8416bf8d2b1d75922521e"}, + {file = "contourpy-1.1.1-pp38-pypy38_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a10dab5ea1bd4401c9483450b5b0ba5416be799bbd50fc7a6cc5e2a15e03e8a3"}, + {file = "contourpy-1.1.1-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:4f9147051cb8fdb29a51dc2482d792b3b23e50f8f57e3720ca2e3d438b7adf23"}, + {file = "contourpy-1.1.1-pp39-pypy39_pp73-macosx_10_9_x86_64.whl", hash = "sha256:a75cc163a5f4531a256f2c523bd80db509a49fc23721b36dd1ef2f60ff41c3cb"}, + {file = "contourpy-1.1.1-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3b53d5769aa1f2d4ea407c65f2d1d08002952fac1d9e9d307aa2e1023554a163"}, + {file = "contourpy-1.1.1-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:11b836b7dbfb74e049c302bbf74b4b8f6cb9d0b6ca1bf86cfa8ba144aedadd9c"}, + {file = "contourpy-1.1.1.tar.gz", hash = "sha256:96ba37c2e24b7212a77da85004c38e7c4d155d3e72a45eeaf22c1f03f607e8ab"}, +] + +[package.dependencies] +numpy = {version = ">=1.16,<2.0", markers = "python_version <= \"3.11\""} + +[package.extras] +bokeh = ["bokeh", "selenium"] +docs = ["furo", "sphinx (>=7.2)", "sphinx-copybutton"] +mypy = ["contourpy[bokeh,docs]", "docutils-stubs", "mypy (==1.4.1)", "types-Pillow"] +test = ["Pillow", "contourpy[test-no-images]", "matplotlib"] +test-no-images = ["pytest", "pytest-cov", "wurlitzer"] + +[[package]] +name = "coverage" +version = "7.3.1" +description = "Code coverage measurement for Python" +optional = false +python-versions = ">=3.8" +files = [ + {file = "coverage-7.3.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:cd0f7429ecfd1ff597389907045ff209c8fdb5b013d38cfa7c60728cb484b6e3"}, + {file = "coverage-7.3.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:966f10df9b2b2115da87f50f6a248e313c72a668248be1b9060ce935c871f276"}, + {file = "coverage-7.3.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0575c37e207bb9b98b6cf72fdaaa18ac909fb3d153083400c2d48e2e6d28bd8e"}, + {file = "coverage-7.3.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:245c5a99254e83875c7fed8b8b2536f040997a9b76ac4c1da5bff398c06e860f"}, + {file = "coverage-7.3.1-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4c96dd7798d83b960afc6c1feb9e5af537fc4908852ef025600374ff1a017392"}, + {file = "coverage-7.3.1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:de30c1aa80f30af0f6b2058a91505ea6e36d6535d437520067f525f7df123887"}, + {file = "coverage-7.3.1-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:50dd1e2dd13dbbd856ffef69196781edff26c800a74f070d3b3e3389cab2600d"}, + {file = "coverage-7.3.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:b9c0c19f70d30219113b18fe07e372b244fb2a773d4afde29d5a2f7930765136"}, + {file = "coverage-7.3.1-cp310-cp310-win32.whl", hash = "sha256:770f143980cc16eb601ccfd571846e89a5fe4c03b4193f2e485268f224ab602f"}, + {file = "coverage-7.3.1-cp310-cp310-win_amd64.whl", hash = "sha256:cdd088c00c39a27cfa5329349cc763a48761fdc785879220d54eb785c8a38520"}, + {file = "coverage-7.3.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:74bb470399dc1989b535cb41f5ca7ab2af561e40def22d7e188e0a445e7639e3"}, + {file = "coverage-7.3.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:025ded371f1ca280c035d91b43252adbb04d2aea4c7105252d3cbc227f03b375"}, + {file = "coverage-7.3.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a6191b3a6ad3e09b6cfd75b45c6aeeffe7e3b0ad46b268345d159b8df8d835f9"}, + {file = "coverage-7.3.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7eb0b188f30e41ddd659a529e385470aa6782f3b412f860ce22b2491c89b8593"}, + {file = "coverage-7.3.1-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:75c8f0df9dfd8ff745bccff75867d63ef336e57cc22b2908ee725cc552689ec8"}, + {file = "coverage-7.3.1-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:7eb3cd48d54b9bd0e73026dedce44773214064be93611deab0b6a43158c3d5a0"}, + {file = "coverage-7.3.1-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:ac3c5b7e75acac31e490b7851595212ed951889918d398b7afa12736c85e13ce"}, + {file = "coverage-7.3.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:5b4ee7080878077af0afa7238df1b967f00dc10763f6e1b66f5cced4abebb0a3"}, + {file = "coverage-7.3.1-cp311-cp311-win32.whl", hash = "sha256:229c0dd2ccf956bf5aeede7e3131ca48b65beacde2029f0361b54bf93d36f45a"}, + {file = "coverage-7.3.1-cp311-cp311-win_amd64.whl", hash = "sha256:c6f55d38818ca9596dc9019eae19a47410d5322408140d9a0076001a3dcb938c"}, + {file = "coverage-7.3.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:5289490dd1c3bb86de4730a92261ae66ea8d44b79ed3cc26464f4c2cde581fbc"}, + {file = "coverage-7.3.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:ca833941ec701fda15414be400c3259479bfde7ae6d806b69e63b3dc423b1832"}, + {file = "coverage-7.3.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cd694e19c031733e446c8024dedd12a00cda87e1c10bd7b8539a87963685e969"}, + {file = "coverage-7.3.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:aab8e9464c00da5cb9c536150b7fbcd8850d376d1151741dd0d16dfe1ba4fd26"}, + {file = "coverage-7.3.1-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:87d38444efffd5b056fcc026c1e8d862191881143c3aa80bb11fcf9dca9ae204"}, + {file = "coverage-7.3.1-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:8a07b692129b8a14ad7a37941a3029c291254feb7a4237f245cfae2de78de037"}, + {file = "coverage-7.3.1-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:2829c65c8faaf55b868ed7af3c7477b76b1c6ebeee99a28f59a2cb5907a45760"}, + {file = "coverage-7.3.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:1f111a7d85658ea52ffad7084088277135ec5f368457275fc57f11cebb15607f"}, + {file = "coverage-7.3.1-cp312-cp312-win32.whl", hash = "sha256:c397c70cd20f6df7d2a52283857af622d5f23300c4ca8e5bd8c7a543825baa5a"}, + {file = "coverage-7.3.1-cp312-cp312-win_amd64.whl", hash = "sha256:5ae4c6da8b3d123500f9525b50bf0168023313963e0e2e814badf9000dd6ef92"}, + {file = "coverage-7.3.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:ca70466ca3a17460e8fc9cea7123c8cbef5ada4be3140a1ef8f7b63f2f37108f"}, + {file = "coverage-7.3.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:f2781fd3cabc28278dc982a352f50c81c09a1a500cc2086dc4249853ea96b981"}, + {file = "coverage-7.3.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6407424621f40205bbe6325686417e5e552f6b2dba3535dd1f90afc88a61d465"}, + {file = "coverage-7.3.1-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:04312b036580ec505f2b77cbbdfb15137d5efdfade09156961f5277149f5e344"}, + {file = "coverage-7.3.1-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ac9ad38204887349853d7c313f53a7b1c210ce138c73859e925bc4e5d8fc18e7"}, + {file = "coverage-7.3.1-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:53669b79f3d599da95a0afbef039ac0fadbb236532feb042c534fbb81b1a4e40"}, + {file = "coverage-7.3.1-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:614f1f98b84eb256e4f35e726bfe5ca82349f8dfa576faabf8a49ca09e630086"}, + {file = "coverage-7.3.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:f1a317fdf5c122ad642db8a97964733ab7c3cf6009e1a8ae8821089993f175ff"}, + {file = "coverage-7.3.1-cp38-cp38-win32.whl", hash = "sha256:defbbb51121189722420a208957e26e49809feafca6afeef325df66c39c4fdb3"}, + {file = "coverage-7.3.1-cp38-cp38-win_amd64.whl", hash = "sha256:f4f456590eefb6e1b3c9ea6328c1e9fa0f1006e7481179d749b3376fc793478e"}, + {file = "coverage-7.3.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:f12d8b11a54f32688b165fd1a788c408f927b0960984b899be7e4c190ae758f1"}, + {file = "coverage-7.3.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:f09195dda68d94a53123883de75bb97b0e35f5f6f9f3aa5bf6e496da718f0cb6"}, + {file = "coverage-7.3.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c6601a60318f9c3945be6ea0f2a80571f4299b6801716f8a6e4846892737ebe4"}, + {file = "coverage-7.3.1-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:07d156269718670d00a3b06db2288b48527fc5f36859425ff7cec07c6b367745"}, + {file = "coverage-7.3.1-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:636a8ac0b044cfeccae76a36f3b18264edcc810a76a49884b96dd744613ec0b7"}, + {file = "coverage-7.3.1-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:5d991e13ad2ed3aced177f524e4d670f304c8233edad3210e02c465351f785a0"}, + {file = "coverage-7.3.1-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:586649ada7cf139445da386ab6f8ef00e6172f11a939fc3b2b7e7c9082052fa0"}, + {file = "coverage-7.3.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:4aba512a15a3e1e4fdbfed2f5392ec221434a614cc68100ca99dcad7af29f3f8"}, + {file = "coverage-7.3.1-cp39-cp39-win32.whl", hash = "sha256:6bc6f3f4692d806831c136c5acad5ccedd0262aa44c087c46b7101c77e139140"}, + {file = "coverage-7.3.1-cp39-cp39-win_amd64.whl", hash = "sha256:553d7094cb27db58ea91332e8b5681bac107e7242c23f7629ab1316ee73c4981"}, + {file = "coverage-7.3.1-pp38.pp39.pp310-none-any.whl", hash = "sha256:220eb51f5fb38dfdb7e5d54284ca4d0cd70ddac047d750111a68ab1798945194"}, + {file = "coverage-7.3.1.tar.gz", hash = "sha256:6cb7fe1581deb67b782c153136541e20901aa312ceedaf1467dcb35255787952"}, +] + +[package.dependencies] +tomli = {version = "*", optional = true, markers = "python_full_version <= \"3.11.0a6\" and extra == \"toml\""} + +[package.extras] +toml = ["tomli"] + +[[package]] +name = "cycler" +version = "0.11.0" +description = "Composable style cycles" +optional = false +python-versions = ">=3.6" +files = [ + {file = "cycler-0.11.0-py3-none-any.whl", hash = "sha256:3a27e95f763a428a739d2add979fa7494c912a32c17c4c38c4d5f082cad165a3"}, + {file = "cycler-0.11.0.tar.gz", hash = "sha256:9c87405839a19696e837b3b818fed3f5f69f16f1eec1a1ad77e043dcea9c772f"}, +] + +[[package]] +name = "distlib" +version = "0.3.7" +description = "Distribution utilities" +optional = false +python-versions = "*" +files = [ + {file = "distlib-0.3.7-py2.py3-none-any.whl", hash = "sha256:2e24928bc811348f0feb63014e97aaae3037f2cf48712d51ae61df7fd6075057"}, + {file = "distlib-0.3.7.tar.gz", hash = "sha256:9dafe54b34a028eafd95039d5e5d4851a13734540f1331060d31c9916e7147a8"}, +] + +[[package]] +name = "etuples" +version = "0.3.9" +description = "Python S-expression emulation using tuple-like objects." +optional = false +python-versions = ">=3.8" +files = [ + {file = "etuples-0.3.9.tar.gz", hash = "sha256:a474e586683d8ba8d842ba29305005ceed1c08371a4b4b0e0e232527137e5ea3"}, +] + +[package.dependencies] +cons = "*" +multipledispatch = "*" + +[[package]] +name = "exceptiongroup" +version = "1.1.3" +description = "Backport of PEP 654 (exception groups)" +optional = false +python-versions = ">=3.7" +files = [ + {file = "exceptiongroup-1.1.3-py3-none-any.whl", hash = "sha256:343280667a4585d195ca1cf9cef84a4e178c4b6cf2274caef9859782b567d5e3"}, + {file = "exceptiongroup-1.1.3.tar.gz", hash = "sha256:097acd85d473d75af5bb98e41b61ff7fe35efe6675e4f9370ec6ec5126d160e9"}, +] + +[package.extras] +test = ["pytest (>=6)"] + +[[package]] +name = "execnet" +version = "2.0.2" +description = "execnet: rapid multi-Python deployment" +optional = false +python-versions = ">=3.7" +files = [ + {file = "execnet-2.0.2-py3-none-any.whl", hash = "sha256:88256416ae766bc9e8895c76a87928c0012183da3cc4fc18016e6f050e025f41"}, + {file = "execnet-2.0.2.tar.gz", hash = "sha256:cc59bc4423742fd71ad227122eb0dd44db51efb3dc4095b45ac9a08c770096af"}, +] + +[package.extras] +testing = ["hatch", "pre-commit", "pytest", "tox"] + +[[package]] +name = "fastprogress" +version = "1.0.3" +description = "A nested progress with plotting options for fastai" +optional = false +python-versions = ">=3.6" +files = [ + {file = "fastprogress-1.0.3-py3-none-any.whl", hash = "sha256:6dfea88f7a4717b0a8d6ee2048beae5dbed369f932a368c5dd9caff34796f7c5"}, + {file = "fastprogress-1.0.3.tar.gz", hash = "sha256:7a17d2b438890f838c048eefce32c4ded47197ecc8ea042cecc33d3deb8022f5"}, +] + +[[package]] +name = "filelock" +version = "3.12.4" +description = "A platform independent file lock." +optional = false +python-versions = ">=3.8" +files = [ + {file = "filelock-3.12.4-py3-none-any.whl", hash = "sha256:08c21d87ded6e2b9da6728c3dff51baf1dcecf973b768ef35bcbc3447edb9ad4"}, + {file = "filelock-3.12.4.tar.gz", hash = "sha256:2e6f249f1f3654291606e046b09f1fd5eac39b360664c27f5aad072012f8bcbd"}, +] + +[package.extras] +docs = ["furo (>=2023.7.26)", "sphinx (>=7.1.2)", "sphinx-autodoc-typehints (>=1.24)"] +testing = ["covdefaults (>=2.3)", "coverage (>=7.3)", "diff-cover (>=7.7)", "pytest (>=7.4)", "pytest-cov (>=4.1)", "pytest-mock (>=3.11.1)", "pytest-timeout (>=2.1)"] +typing = ["typing-extensions (>=4.7.1)"] + +[[package]] +name = "fonttools" +version = "4.42.1" +description = "Tools to manipulate font files" +optional = false +python-versions = ">=3.8" +files = [ + {file = "fonttools-4.42.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:ed1a13a27f59d1fc1920394a7f596792e9d546c9ca5a044419dca70c37815d7c"}, + {file = "fonttools-4.42.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:c9b1ce7a45978b821a06d375b83763b27a3a5e8a2e4570b3065abad240a18760"}, + {file = "fonttools-4.42.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f720fa82a11c0f9042376fd509b5ed88dab7e3cd602eee63a1af08883b37342b"}, + {file = "fonttools-4.42.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:db55cbaea02a20b49fefbd8e9d62bd481aaabe1f2301dabc575acc6b358874fa"}, + {file = "fonttools-4.42.1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:3a35981d90feebeaef05e46e33e6b9e5b5e618504672ca9cd0ff96b171e4bfff"}, + {file = "fonttools-4.42.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:68a02bbe020dc22ee0540e040117535f06df9358106d3775e8817d826047f3fd"}, + {file = "fonttools-4.42.1-cp310-cp310-win32.whl", hash = "sha256:12a7c247d1b946829bfa2f331107a629ea77dc5391dfd34fdcd78efa61f354ca"}, + {file = "fonttools-4.42.1-cp310-cp310-win_amd64.whl", hash = "sha256:a398bdadb055f8de69f62b0fc70625f7cbdab436bbb31eef5816e28cab083ee8"}, + {file = "fonttools-4.42.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:689508b918332fb40ce117131633647731d098b1b10d092234aa959b4251add5"}, + {file = "fonttools-4.42.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:9e36344e48af3e3bde867a1ca54f97c308735dd8697005c2d24a86054a114a71"}, + {file = "fonttools-4.42.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:19b7db825c8adee96fac0692e6e1ecd858cae9affb3b4812cdb9d934a898b29e"}, + {file = "fonttools-4.42.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:113337c2d29665839b7d90b39f99b3cac731f72a0eda9306165a305c7c31d341"}, + {file = "fonttools-4.42.1-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:37983b6bdab42c501202500a2be3a572f50d4efe3237e0686ee9d5f794d76b35"}, + {file = "fonttools-4.42.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:6ed2662a3d9c832afa36405f8748c250be94ae5dfc5283d668308391f2102861"}, + {file = "fonttools-4.42.1-cp311-cp311-win32.whl", hash = "sha256:179737095eb98332a2744e8f12037b2977f22948cf23ff96656928923ddf560a"}, + {file = "fonttools-4.42.1-cp311-cp311-win_amd64.whl", hash = "sha256:f2b82f46917d8722e6b5eafeefb4fb585d23babd15d8246c664cd88a5bddd19c"}, + {file = "fonttools-4.42.1-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:62f481ac772fd68901573956231aea3e4b1ad87b9b1089a61613a91e2b50bb9b"}, + {file = "fonttools-4.42.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:f2f806990160d1ce42d287aa419df3ffc42dfefe60d473695fb048355fe0c6a0"}, + {file = "fonttools-4.42.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:db372213d39fa33af667c2aa586a0c1235e88e9c850f5dd5c8e1f17515861868"}, + {file = "fonttools-4.42.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5d18fc642fd0ac29236ff88ecfccff229ec0386090a839dd3f1162e9a7944a40"}, + {file = "fonttools-4.42.1-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:8708b98c278012ad267ee8a7433baeb809948855e81922878118464b274c909d"}, + {file = "fonttools-4.42.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:c95b0724a6deea2c8c5d3222191783ced0a2f09bd6d33f93e563f6f1a4b3b3a4"}, + {file = "fonttools-4.42.1-cp38-cp38-win32.whl", hash = "sha256:4aa79366e442dbca6e2c8595645a3a605d9eeabdb7a094d745ed6106816bef5d"}, + {file = "fonttools-4.42.1-cp38-cp38-win_amd64.whl", hash = "sha256:acb47f6f8680de24c1ab65ebde39dd035768e2a9b571a07c7b8da95f6c8815fd"}, + {file = "fonttools-4.42.1-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:5fb289b7a815638a7613d46bcf324c9106804725b2bb8ad913c12b6958ffc4ec"}, + {file = "fonttools-4.42.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:53eb5091ddc8b1199330bb7b4a8a2e7995ad5d43376cadce84523d8223ef3136"}, + {file = "fonttools-4.42.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:46a0ec8adbc6ff13494eb0c9c2e643b6f009ce7320cf640de106fb614e4d4360"}, + {file = "fonttools-4.42.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7cc7d685b8eeca7ae69dc6416833fbfea61660684b7089bca666067cb2937dcf"}, + {file = "fonttools-4.42.1-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:be24fcb80493b2c94eae21df70017351851652a37de514de553435b256b2f249"}, + {file = "fonttools-4.42.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:515607ec756d7865f23070682622c49d922901943697871fc292277cf1e71967"}, + {file = "fonttools-4.42.1-cp39-cp39-win32.whl", hash = "sha256:0eb79a2da5eb6457a6f8ab904838454accc7d4cccdaff1fd2bd3a0679ea33d64"}, + {file = "fonttools-4.42.1-cp39-cp39-win_amd64.whl", hash = "sha256:7286aed4ea271df9eab8d7a9b29e507094b51397812f7ce051ecd77915a6e26b"}, + {file = "fonttools-4.42.1-py3-none-any.whl", hash = "sha256:9398f244e28e0596e2ee6024f808b06060109e33ed38dcc9bded452fd9bbb853"}, + {file = "fonttools-4.42.1.tar.gz", hash = "sha256:c391cd5af88aacaf41dd7cfb96eeedfad297b5899a39e12f4c2c3706d0a3329d"}, +] + +[package.extras] +all = ["brotli (>=1.0.1)", "brotlicffi (>=0.8.0)", "fs (>=2.2.0,<3)", "lxml (>=4.0,<5)", "lz4 (>=1.7.4.2)", "matplotlib", "munkres", "scipy", "skia-pathops (>=0.5.0)", "sympy", "uharfbuzz (>=0.23.0)", "unicodedata2 (>=15.0.0)", "xattr", "zopfli (>=0.1.4)"] +graphite = ["lz4 (>=1.7.4.2)"] +interpolatable = ["munkres", "scipy"] +lxml = ["lxml (>=4.0,<5)"] +pathops = ["skia-pathops (>=0.5.0)"] +plot = ["matplotlib"] +repacker = ["uharfbuzz (>=0.23.0)"] +symfont = ["sympy"] +type1 = ["xattr"] +ufo = ["fs (>=2.2.0,<3)"] +unicode = ["unicodedata2 (>=15.0.0)"] +woff = ["brotli (>=1.0.1)", "brotlicffi (>=0.8.0)", "zopfli (>=0.1.4)"] + +[[package]] +name = "ghp-import" +version = "2.1.0" +description = "Copy your docs directly to the gh-pages branch." +optional = false +python-versions = "*" +files = [ + {file = "ghp-import-2.1.0.tar.gz", hash = "sha256:9c535c4c61193c2df8871222567d7fd7e5014d835f97dc7b7439069e2413d343"}, + {file = "ghp_import-2.1.0-py3-none-any.whl", hash = "sha256:8337dd7b50877f163d4c0289bc1f1c7f127550241988d568c1db512c4324a619"}, +] + +[package.dependencies] +python-dateutil = ">=2.8.1" + +[package.extras] +dev = ["flake8", "markdown", "twine", "wheel"] + +[[package]] +name = "griffe" +version = "0.36.2" +description = "Signatures for entire Python programs. Extract the structure, the frame, the skeleton of your project, to generate API documentation or find breaking changes in your API." +optional = false +python-versions = ">=3.8" +files = [ + {file = "griffe-0.36.2-py3-none-any.whl", hash = "sha256:ba71895a3f5f606b18dcd950e8a1f8e7332a37f90f24caeb002546593f2e0eee"}, + {file = "griffe-0.36.2.tar.gz", hash = "sha256:333ade7932bb9096781d83092602625dfbfe220e87a039d2801259a1bd41d1c2"}, +] + +[package.dependencies] +colorama = ">=0.4" + +[[package]] +name = "h5netcdf" +version = "1.2.0" +description = "netCDF4 via h5py" +optional = false +python-versions = ">=3.9" +files = [ + {file = "h5netcdf-1.2.0-py3-none-any.whl", hash = "sha256:aa53c39b94bcd4595a2e5a2f62f3fb4fb8a723b5ca0a05f2db352f014bcfe72c"}, + {file = "h5netcdf-1.2.0.tar.gz", hash = "sha256:7f6b2733bde06ea2575b79a6450d9bd5c38918ff4cb2a355bf22bbe8c86c6bcf"}, +] + +[package.dependencies] +h5py = "*" +packaging = "*" + +[package.extras] +test = ["netCDF4", "pytest"] + +[[package]] +name = "h5py" +version = "3.9.0" +description = "Read and write HDF5 files from Python" +optional = false +python-versions = ">=3.8" +files = [ + {file = "h5py-3.9.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:eb7bdd5e601dd1739698af383be03f3dad0465fe67184ebd5afca770f50df9d6"}, + {file = "h5py-3.9.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:78e44686334cbbf2dd21d9df15823bc38663f27a3061f6a032c68a3e30c47bf7"}, + {file = "h5py-3.9.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f68b41efd110ce9af1cbe6fa8af9f4dcbadace6db972d30828b911949e28fadd"}, + {file = "h5py-3.9.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:12aa556d540f11a2cae53ea7cfb94017353bd271fb3962e1296b342f6550d1b8"}, + {file = "h5py-3.9.0-cp310-cp310-win_amd64.whl", hash = "sha256:d97409e17915798029e297a84124705c8080da901307ea58f29234e09b073ddc"}, + {file = "h5py-3.9.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:551e358db05a874a0f827b22e95b30092f2303edc4b91bb62ad2f10e0236e1a0"}, + {file = "h5py-3.9.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:6822a814b9d8b8363ff102f76ea8d026f0ca25850bb579d85376029ee3e73b93"}, + {file = "h5py-3.9.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:54f01202cdea754ab4227dd27014bdbd561a4bbe4b631424fd812f7c2ce9c6ac"}, + {file = "h5py-3.9.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:64acceaf6aff92af091a4b83f6dee3cf8d3061f924a6bb3a33eb6c4658a8348b"}, + {file = "h5py-3.9.0-cp311-cp311-win_amd64.whl", hash = "sha256:804c7fb42a34c8ab3a3001901c977a5c24d2e9c586a0f3e7c0a389130b4276fc"}, + {file = "h5py-3.9.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:8d9492391ff5c3c80ec30ae2fe82a3f0efd1e750833739c25b0d090e3be1b095"}, + {file = "h5py-3.9.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:9da9e7e63376c32704e37ad4cea2dceae6964cee0d8515185b3ab9cbd6b947bc"}, + {file = "h5py-3.9.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a4e20897c88759cbcbd38fb45b507adc91af3e0f67722aa302d71f02dd44d286"}, + {file = "h5py-3.9.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dbf5225543ca35ce9f61c950b73899a82be7ba60d58340e76d0bd42bf659235a"}, + {file = "h5py-3.9.0-cp38-cp38-win_amd64.whl", hash = "sha256:36408f8c62f50007d14e000f9f3acf77e103b9e932c114cbe52a3089e50ebf94"}, + {file = "h5py-3.9.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:23e74b878bbe1653ab34ca49b83cac85529cd0b36b9d625516c5830cc5ca2eac"}, + {file = "h5py-3.9.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:3f457089c5d524b7998e3649bc63240679b8fb0a3859ea53bbb06841f3d755f1"}, + {file = "h5py-3.9.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a6284061f3214335e1eec883a6ee497dbe7a79f19e6a57fed2dd1f03acd5a8cb"}, + {file = "h5py-3.9.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:95f7a745efd0d56076999b52e8da5fad5d30823bac98b59c68ae75588d09991a"}, + {file = "h5py-3.9.0-cp39-cp39-win_amd64.whl", hash = "sha256:79bbca34696c6f9eeeb36a91776070c49a060b2879828e2c8fa6c58b8ed10dd1"}, + {file = "h5py-3.9.0.tar.gz", hash = "sha256:e604db6521c1e367c6bd7fad239c847f53cc46646f2d2651372d05ae5e95f817"}, +] + +[package.dependencies] +numpy = ">=1.17.3" + +[[package]] +name = "identify" +version = "2.5.29" +description = "File identification library for Python" +optional = false +python-versions = ">=3.8" +files = [ + {file = "identify-2.5.29-py2.py3-none-any.whl", hash = "sha256:24437fbf6f4d3fe6efd0eb9d67e24dd9106db99af5ceb27996a5f7895f24bf1b"}, + {file = "identify-2.5.29.tar.gz", hash = "sha256:d43d52b86b15918c137e3a74fff5224f60385cd0e9c38e99d07c257f02f151a5"}, +] + +[package.extras] +license = ["ukkonen"] + +[[package]] +name = "idna" +version = "3.4" +description = "Internationalized Domain Names in Applications (IDNA)" +optional = false +python-versions = ">=3.5" +files = [ + {file = "idna-3.4-py3-none-any.whl", hash = "sha256:90b77e79eaa3eba6de819a0c442c0b4ceefc341a7a2ab77d7562bf49f425c5c2"}, + {file = "idna-3.4.tar.gz", hash = "sha256:814f528e8dead7d329833b91c5faa87d60bf71824cd12a7530b5526063d02cb4"}, +] + +[[package]] +name = "iniconfig" +version = "2.0.0" +description = "brain-dead simple config-ini parsing" +optional = false +python-versions = ">=3.7" +files = [ + {file = "iniconfig-2.0.0-py3-none-any.whl", hash = "sha256:b6a85871a79d2e3b22d2d1b94ac2824226a63c6b741c88f7ae975f18b6778374"}, + {file = "iniconfig-2.0.0.tar.gz", hash = "sha256:2d91e135bf72d31a410b17c16da610a82cb55f6b0477d1a902134b24a455b8b3"}, +] + +[[package]] +name = "jinja2" +version = "3.1.2" +description = "A very fast and expressive template engine." +optional = false +python-versions = ">=3.7" +files = [ + {file = "Jinja2-3.1.2-py3-none-any.whl", hash = "sha256:6088930bfe239f0e6710546ab9c19c9ef35e29792895fed6e6e31a023a182a61"}, + {file = "Jinja2-3.1.2.tar.gz", hash = "sha256:31351a702a408a9e7595a8fc6150fc3f43bb6bf7e319770cbc0db9df9437e852"}, +] + +[package.dependencies] +MarkupSafe = ">=2.0" + +[package.extras] +i18n = ["Babel (>=2.7)"] + +[[package]] +name = "joblib" +version = "1.3.2" +description = "Lightweight pipelining with Python functions" +optional = false +python-versions = ">=3.7" +files = [ + {file = "joblib-1.3.2-py3-none-any.whl", hash = "sha256:ef4331c65f239985f3f2220ecc87db222f08fd22097a3dd5698f693875f8cbb9"}, + {file = "joblib-1.3.2.tar.gz", hash = "sha256:92f865e621e17784e7955080b6d042489e3b8e294949cc44c6eac304f59772b1"}, +] + +[[package]] +name = "kiwisolver" +version = "1.4.5" +description = "A fast implementation of the Cassowary constraint solver" +optional = false +python-versions = ">=3.7" +files = [ + {file = "kiwisolver-1.4.5-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:05703cf211d585109fcd72207a31bb170a0f22144d68298dc5e61b3c946518af"}, + {file = "kiwisolver-1.4.5-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:146d14bebb7f1dc4d5fbf74f8a6cb15ac42baadee8912eb84ac0b3b2a3dc6ac3"}, + {file = "kiwisolver-1.4.5-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:6ef7afcd2d281494c0a9101d5c571970708ad911d028137cd558f02b851c08b4"}, + {file = "kiwisolver-1.4.5-cp310-cp310-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:9eaa8b117dc8337728e834b9c6e2611f10c79e38f65157c4c38e9400286f5cb1"}, + {file = "kiwisolver-1.4.5-cp310-cp310-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:ec20916e7b4cbfb1f12380e46486ec4bcbaa91a9c448b97023fde0d5bbf9e4ff"}, + {file = "kiwisolver-1.4.5-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:39b42c68602539407884cf70d6a480a469b93b81b7701378ba5e2328660c847a"}, + {file = "kiwisolver-1.4.5-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:aa12042de0171fad672b6c59df69106d20d5596e4f87b5e8f76df757a7c399aa"}, + {file = "kiwisolver-1.4.5-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2a40773c71d7ccdd3798f6489aaac9eee213d566850a9533f8d26332d626b82c"}, + {file = "kiwisolver-1.4.5-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:19df6e621f6d8b4b9c4d45f40a66839294ff2bb235e64d2178f7522d9170ac5b"}, + {file = "kiwisolver-1.4.5-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:83d78376d0d4fd884e2c114d0621624b73d2aba4e2788182d286309ebdeed770"}, + {file = "kiwisolver-1.4.5-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:e391b1f0a8a5a10ab3b9bb6afcfd74f2175f24f8975fb87ecae700d1503cdee0"}, + {file = "kiwisolver-1.4.5-cp310-cp310-musllinux_1_1_s390x.whl", hash = "sha256:852542f9481f4a62dbb5dd99e8ab7aedfeb8fb6342349a181d4036877410f525"}, + {file = "kiwisolver-1.4.5-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:59edc41b24031bc25108e210c0def6f6c2191210492a972d585a06ff246bb79b"}, + {file = "kiwisolver-1.4.5-cp310-cp310-win32.whl", hash = "sha256:a6aa6315319a052b4ee378aa171959c898a6183f15c1e541821c5c59beaa0238"}, + {file = "kiwisolver-1.4.5-cp310-cp310-win_amd64.whl", hash = "sha256:d0ef46024e6a3d79c01ff13801cb19d0cad7fd859b15037aec74315540acc276"}, + {file = "kiwisolver-1.4.5-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:11863aa14a51fd6ec28688d76f1735f8f69ab1fabf388851a595d0721af042f5"}, + {file = "kiwisolver-1.4.5-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:8ab3919a9997ab7ef2fbbed0cc99bb28d3c13e6d4b1ad36e97e482558a91be90"}, + {file = "kiwisolver-1.4.5-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:fcc700eadbbccbf6bc1bcb9dbe0786b4b1cb91ca0dcda336eef5c2beed37b797"}, + {file = "kiwisolver-1.4.5-cp311-cp311-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:dfdd7c0b105af050eb3d64997809dc21da247cf44e63dc73ff0fd20b96be55a9"}, + {file = "kiwisolver-1.4.5-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:76c6a5964640638cdeaa0c359382e5703e9293030fe730018ca06bc2010c4437"}, + {file = "kiwisolver-1.4.5-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:bbea0db94288e29afcc4c28afbf3a7ccaf2d7e027489c449cf7e8f83c6346eb9"}, + {file = "kiwisolver-1.4.5-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:ceec1a6bc6cab1d6ff5d06592a91a692f90ec7505d6463a88a52cc0eb58545da"}, + {file = "kiwisolver-1.4.5-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:040c1aebeda72197ef477a906782b5ab0d387642e93bda547336b8957c61022e"}, + {file = "kiwisolver-1.4.5-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:f91de7223d4c7b793867797bacd1ee53bfe7359bd70d27b7b58a04efbb9436c8"}, + {file = "kiwisolver-1.4.5-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:faae4860798c31530dd184046a900e652c95513796ef51a12bc086710c2eec4d"}, + {file = "kiwisolver-1.4.5-cp311-cp311-musllinux_1_1_ppc64le.whl", hash = "sha256:b0157420efcb803e71d1b28e2c287518b8808b7cf1ab8af36718fd0a2c453eb0"}, + {file = "kiwisolver-1.4.5-cp311-cp311-musllinux_1_1_s390x.whl", hash = "sha256:06f54715b7737c2fecdbf140d1afb11a33d59508a47bf11bb38ecf21dc9ab79f"}, + {file = "kiwisolver-1.4.5-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:fdb7adb641a0d13bdcd4ef48e062363d8a9ad4a182ac7647ec88f695e719ae9f"}, + {file = "kiwisolver-1.4.5-cp311-cp311-win32.whl", hash = "sha256:bb86433b1cfe686da83ce32a9d3a8dd308e85c76b60896d58f082136f10bffac"}, + {file = "kiwisolver-1.4.5-cp311-cp311-win_amd64.whl", hash = "sha256:6c08e1312a9cf1074d17b17728d3dfce2a5125b2d791527f33ffbe805200a355"}, + {file = "kiwisolver-1.4.5-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:32d5cf40c4f7c7b3ca500f8985eb3fb3a7dfc023215e876f207956b5ea26632a"}, + {file = "kiwisolver-1.4.5-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:f846c260f483d1fd217fe5ed7c173fb109efa6b1fc8381c8b7552c5781756192"}, + {file = "kiwisolver-1.4.5-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:5ff5cf3571589b6d13bfbfd6bcd7a3f659e42f96b5fd1c4830c4cf21d4f5ef45"}, + {file = "kiwisolver-1.4.5-cp312-cp312-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7269d9e5f1084a653d575c7ec012ff57f0c042258bf5db0954bf551c158466e7"}, + {file = "kiwisolver-1.4.5-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:da802a19d6e15dffe4b0c24b38b3af68e6c1a68e6e1d8f30148c83864f3881db"}, + {file = "kiwisolver-1.4.5-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3aba7311af82e335dd1e36ffff68aaca609ca6290c2cb6d821a39aa075d8e3ff"}, + {file = "kiwisolver-1.4.5-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:763773d53f07244148ccac5b084da5adb90bfaee39c197554f01b286cf869228"}, + {file = "kiwisolver-1.4.5-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2270953c0d8cdab5d422bee7d2007f043473f9d2999631c86a223c9db56cbd16"}, + {file = "kiwisolver-1.4.5-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:d099e745a512f7e3bbe7249ca835f4d357c586d78d79ae8f1dcd4d8adeb9bda9"}, + {file = "kiwisolver-1.4.5-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:74db36e14a7d1ce0986fa104f7d5637aea5c82ca6326ed0ec5694280942d1162"}, + {file = "kiwisolver-1.4.5-cp312-cp312-musllinux_1_1_ppc64le.whl", hash = "sha256:7e5bab140c309cb3a6ce373a9e71eb7e4873c70c2dda01df6820474f9889d6d4"}, + {file = "kiwisolver-1.4.5-cp312-cp312-musllinux_1_1_s390x.whl", hash = "sha256:0f114aa76dc1b8f636d077979c0ac22e7cd8f3493abbab152f20eb8d3cda71f3"}, + {file = "kiwisolver-1.4.5-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:88a2df29d4724b9237fc0c6eaf2a1adae0cdc0b3e9f4d8e7dc54b16812d2d81a"}, + {file = "kiwisolver-1.4.5-cp312-cp312-win32.whl", hash = "sha256:72d40b33e834371fd330fb1472ca19d9b8327acb79a5821d4008391db8e29f20"}, + {file = "kiwisolver-1.4.5-cp312-cp312-win_amd64.whl", hash = "sha256:2c5674c4e74d939b9d91dda0fae10597ac7521768fec9e399c70a1f27e2ea2d9"}, + {file = "kiwisolver-1.4.5-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:3a2b053a0ab7a3960c98725cfb0bf5b48ba82f64ec95fe06f1d06c99b552e130"}, + {file = "kiwisolver-1.4.5-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3cd32d6c13807e5c66a7cbb79f90b553642f296ae4518a60d8d76243b0ad2898"}, + {file = "kiwisolver-1.4.5-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:59ec7b7c7e1a61061850d53aaf8e93db63dce0c936db1fda2658b70e4a1be709"}, + {file = "kiwisolver-1.4.5-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:da4cfb373035def307905d05041c1d06d8936452fe89d464743ae7fb8371078b"}, + {file = "kiwisolver-1.4.5-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:2400873bccc260b6ae184b2b8a4fec0e4082d30648eadb7c3d9a13405d861e89"}, + {file = "kiwisolver-1.4.5-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:1b04139c4236a0f3aff534479b58f6f849a8b351e1314826c2d230849ed48985"}, + {file = "kiwisolver-1.4.5-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:4e66e81a5779b65ac21764c295087de82235597a2293d18d943f8e9e32746265"}, + {file = "kiwisolver-1.4.5-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:7931d8f1f67c4be9ba1dd9c451fb0eeca1a25b89e4d3f89e828fe12a519b782a"}, + {file = "kiwisolver-1.4.5-cp37-cp37m-musllinux_1_1_ppc64le.whl", hash = "sha256:b3f7e75f3015df442238cca659f8baa5f42ce2a8582727981cbfa15fee0ee205"}, + {file = "kiwisolver-1.4.5-cp37-cp37m-musllinux_1_1_s390x.whl", hash = "sha256:bbf1d63eef84b2e8c89011b7f2235b1e0bf7dacc11cac9431fc6468e99ac77fb"}, + {file = "kiwisolver-1.4.5-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:4c380469bd3f970ef677bf2bcba2b6b0b4d5c75e7a020fb863ef75084efad66f"}, + {file = "kiwisolver-1.4.5-cp37-cp37m-win32.whl", hash = "sha256:9408acf3270c4b6baad483865191e3e582b638b1654a007c62e3efe96f09a9a3"}, + {file = "kiwisolver-1.4.5-cp37-cp37m-win_amd64.whl", hash = "sha256:5b94529f9b2591b7af5f3e0e730a4e0a41ea174af35a4fd067775f9bdfeee01a"}, + {file = "kiwisolver-1.4.5-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:11c7de8f692fc99816e8ac50d1d1aef4f75126eefc33ac79aac02c099fd3db71"}, + {file = "kiwisolver-1.4.5-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:53abb58632235cd154176ced1ae8f0d29a6657aa1aa9decf50b899b755bc2b93"}, + {file = "kiwisolver-1.4.5-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:88b9f257ca61b838b6f8094a62418421f87ac2a1069f7e896c36a7d86b5d4c29"}, + {file = "kiwisolver-1.4.5-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3195782b26fc03aa9c6913d5bad5aeb864bdc372924c093b0f1cebad603dd712"}, + {file = "kiwisolver-1.4.5-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:fc579bf0f502e54926519451b920e875f433aceb4624a3646b3252b5caa9e0b6"}, + {file = "kiwisolver-1.4.5-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5a580c91d686376f0f7c295357595c5a026e6cbc3d77b7c36e290201e7c11ecb"}, + {file = "kiwisolver-1.4.5-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:cfe6ab8da05c01ba6fbea630377b5da2cd9bcbc6338510116b01c1bc939a2c18"}, + {file = "kiwisolver-1.4.5-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:d2e5a98f0ec99beb3c10e13b387f8db39106d53993f498b295f0c914328b1333"}, + {file = "kiwisolver-1.4.5-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:a51a263952b1429e429ff236d2f5a21c5125437861baeed77f5e1cc2d2c7c6da"}, + {file = "kiwisolver-1.4.5-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:3edd2fa14e68c9be82c5b16689e8d63d89fe927e56debd6e1dbce7a26a17f81b"}, + {file = "kiwisolver-1.4.5-cp38-cp38-musllinux_1_1_ppc64le.whl", hash = "sha256:74d1b44c6cfc897df648cc9fdaa09bc3e7679926e6f96df05775d4fb3946571c"}, + {file = "kiwisolver-1.4.5-cp38-cp38-musllinux_1_1_s390x.whl", hash = "sha256:76d9289ed3f7501012e05abb8358bbb129149dbd173f1f57a1bf1c22d19ab7cc"}, + {file = "kiwisolver-1.4.5-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:92dea1ffe3714fa8eb6a314d2b3c773208d865a0e0d35e713ec54eea08a66250"}, + {file = "kiwisolver-1.4.5-cp38-cp38-win32.whl", hash = "sha256:5c90ae8c8d32e472be041e76f9d2f2dbff4d0b0be8bd4041770eddb18cf49a4e"}, + {file = "kiwisolver-1.4.5-cp38-cp38-win_amd64.whl", hash = "sha256:c7940c1dc63eb37a67721b10d703247552416f719c4188c54e04334321351ced"}, + {file = "kiwisolver-1.4.5-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:9407b6a5f0d675e8a827ad8742e1d6b49d9c1a1da5d952a67d50ef5f4170b18d"}, + {file = "kiwisolver-1.4.5-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:15568384086b6df3c65353820a4473575dbad192e35010f622c6ce3eebd57af9"}, + {file = "kiwisolver-1.4.5-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:0dc9db8e79f0036e8173c466d21ef18e1befc02de8bf8aa8dc0813a6dc8a7046"}, + {file = "kiwisolver-1.4.5-cp39-cp39-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:cdc8a402aaee9a798b50d8b827d7ecf75edc5fb35ea0f91f213ff927c15f4ff0"}, + {file = "kiwisolver-1.4.5-cp39-cp39-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:6c3bd3cde54cafb87d74d8db50b909705c62b17c2099b8f2e25b461882e544ff"}, + {file = "kiwisolver-1.4.5-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:955e8513d07a283056b1396e9a57ceddbd272d9252c14f154d450d227606eb54"}, + {file = "kiwisolver-1.4.5-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:346f5343b9e3f00b8db8ba359350eb124b98c99efd0b408728ac6ebf38173958"}, + {file = "kiwisolver-1.4.5-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b9098e0049e88c6a24ff64545cdfc50807818ba6c1b739cae221bbbcbc58aad3"}, + {file = "kiwisolver-1.4.5-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:00bd361b903dc4bbf4eb165f24d1acbee754fce22ded24c3d56eec268658a5cf"}, + {file = "kiwisolver-1.4.5-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:7b8b454bac16428b22560d0a1cf0a09875339cab69df61d7805bf48919415901"}, + {file = "kiwisolver-1.4.5-cp39-cp39-musllinux_1_1_ppc64le.whl", hash = "sha256:f1d072c2eb0ad60d4c183f3fb44ac6f73fb7a8f16a2694a91f988275cbf352f9"}, + {file = "kiwisolver-1.4.5-cp39-cp39-musllinux_1_1_s390x.whl", hash = "sha256:31a82d498054cac9f6d0b53d02bb85811185bcb477d4b60144f915f3b3126342"}, + {file = "kiwisolver-1.4.5-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:6512cb89e334e4700febbffaaa52761b65b4f5a3cf33f960213d5656cea36a77"}, + {file = "kiwisolver-1.4.5-cp39-cp39-win32.whl", hash = "sha256:9db8ea4c388fdb0f780fe91346fd438657ea602d58348753d9fb265ce1bca67f"}, + {file = "kiwisolver-1.4.5-cp39-cp39-win_amd64.whl", hash = "sha256:59415f46a37f7f2efeec758353dd2eae1b07640d8ca0f0c42548ec4125492635"}, + {file = "kiwisolver-1.4.5-pp37-pypy37_pp73-macosx_10_9_x86_64.whl", hash = "sha256:5c7b3b3a728dc6faf3fc372ef24f21d1e3cee2ac3e9596691d746e5a536de920"}, + {file = "kiwisolver-1.4.5-pp37-pypy37_pp73-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:620ced262a86244e2be10a676b646f29c34537d0d9cc8eb26c08f53d98013390"}, + {file = "kiwisolver-1.4.5-pp37-pypy37_pp73-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:378a214a1e3bbf5ac4a8708304318b4f890da88c9e6a07699c4ae7174c09a68d"}, + {file = "kiwisolver-1.4.5-pp37-pypy37_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:aaf7be1207676ac608a50cd08f102f6742dbfc70e8d60c4db1c6897f62f71523"}, + {file = "kiwisolver-1.4.5-pp37-pypy37_pp73-win_amd64.whl", hash = "sha256:ba55dce0a9b8ff59495ddd050a0225d58bd0983d09f87cfe2b6aec4f2c1234e4"}, + {file = "kiwisolver-1.4.5-pp38-pypy38_pp73-macosx_10_9_x86_64.whl", hash = "sha256:fd32ea360bcbb92d28933fc05ed09bffcb1704ba3fc7942e81db0fd4f81a7892"}, + {file = "kiwisolver-1.4.5-pp38-pypy38_pp73-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:5e7139af55d1688f8b960ee9ad5adafc4ac17c1c473fe07133ac092310d76544"}, + {file = "kiwisolver-1.4.5-pp38-pypy38_pp73-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:dced8146011d2bc2e883f9bd68618b8247387f4bbec46d7392b3c3b032640126"}, + {file = "kiwisolver-1.4.5-pp38-pypy38_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c9bf3325c47b11b2e51bca0824ea217c7cd84491d8ac4eefd1e409705ef092bd"}, + {file = "kiwisolver-1.4.5-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:5794cf59533bc3f1b1c821f7206a3617999db9fbefc345360aafe2e067514929"}, + {file = "kiwisolver-1.4.5-pp39-pypy39_pp73-macosx_10_9_x86_64.whl", hash = "sha256:e368f200bbc2e4f905b8e71eb38b3c04333bddaa6a2464a6355487b02bb7fb09"}, + {file = "kiwisolver-1.4.5-pp39-pypy39_pp73-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e5d706eba36b4c4d5bc6c6377bb6568098765e990cfc21ee16d13963fab7b3e7"}, + {file = "kiwisolver-1.4.5-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:85267bd1aa8880a9c88a8cb71e18d3d64d2751a790e6ca6c27b8ccc724bcd5ad"}, + {file = "kiwisolver-1.4.5-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:210ef2c3a1f03272649aff1ef992df2e724748918c4bc2d5a90352849eb40bea"}, + {file = "kiwisolver-1.4.5-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:11d011a7574eb3b82bcc9c1a1d35c1d7075677fdd15de527d91b46bd35e935ee"}, + {file = "kiwisolver-1.4.5.tar.gz", hash = "sha256:e57e563a57fb22a142da34f38acc2fc1a5c864bc29ca1517a88abc963e60d6ec"}, +] + +[[package]] +name = "logical-unification" +version = "0.4.6" +description = "Logical unification in Python" +optional = false +python-versions = ">=3.6" +files = [ + {file = "logical-unification-0.4.6.tar.gz", hash = "sha256:908435123f8a106fa4dcf9bf1b75c7beb309fa2bbecf277868af8f1c212650a0"}, +] + +[package.dependencies] +multipledispatch = "*" +toolz = "*" + +[[package]] +name = "markdown" +version = "3.4.4" +description = "Python implementation of John Gruber's Markdown." +optional = false +python-versions = ">=3.7" +files = [ + {file = "Markdown-3.4.4-py3-none-any.whl", hash = "sha256:a4c1b65c0957b4bd9e7d86ddc7b3c9868fb9670660f6f99f6d1bca8954d5a941"}, + {file = "Markdown-3.4.4.tar.gz", hash = "sha256:225c6123522495d4119a90b3a3ba31a1e87a70369e03f14799ea9c0d7183a3d6"}, +] + +[package.extras] +docs = ["mdx-gh-links (>=0.2)", "mkdocs (>=1.0)", "mkdocs-nature (>=0.4)"] +testing = ["coverage", "pyyaml"] + +[[package]] +name = "markupsafe" +version = "2.1.3" +description = "Safely add untrusted strings to HTML/XML markup." +optional = false +python-versions = ">=3.7" +files = [ + {file = "MarkupSafe-2.1.3-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:cd0f502fe016460680cd20aaa5a76d241d6f35a1c3350c474bac1273803893fa"}, + {file = "MarkupSafe-2.1.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:e09031c87a1e51556fdcb46e5bd4f59dfb743061cf93c4d6831bf894f125eb57"}, + {file = "MarkupSafe-2.1.3-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:68e78619a61ecf91e76aa3e6e8e33fc4894a2bebe93410754bd28fce0a8a4f9f"}, + {file = "MarkupSafe-2.1.3-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:65c1a9bcdadc6c28eecee2c119465aebff8f7a584dd719facdd9e825ec61ab52"}, + {file = "MarkupSafe-2.1.3-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:525808b8019e36eb524b8c68acdd63a37e75714eac50e988180b169d64480a00"}, + {file = "MarkupSafe-2.1.3-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:962f82a3086483f5e5f64dbad880d31038b698494799b097bc59c2edf392fce6"}, + {file = "MarkupSafe-2.1.3-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:aa7bd130efab1c280bed0f45501b7c8795f9fdbeb02e965371bbef3523627779"}, + {file = "MarkupSafe-2.1.3-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:c9c804664ebe8f83a211cace637506669e7890fec1b4195b505c214e50dd4eb7"}, + {file = "MarkupSafe-2.1.3-cp310-cp310-win32.whl", hash = "sha256:10bbfe99883db80bdbaff2dcf681dfc6533a614f700da1287707e8a5d78a8431"}, + {file = "MarkupSafe-2.1.3-cp310-cp310-win_amd64.whl", hash = "sha256:1577735524cdad32f9f694208aa75e422adba74f1baee7551620e43a3141f559"}, + {file = "MarkupSafe-2.1.3-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:ad9e82fb8f09ade1c3e1b996a6337afac2b8b9e365f926f5a61aacc71adc5b3c"}, + {file = "MarkupSafe-2.1.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:3c0fae6c3be832a0a0473ac912810b2877c8cb9d76ca48de1ed31e1c68386575"}, + {file = "MarkupSafe-2.1.3-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b076b6226fb84157e3f7c971a47ff3a679d837cf338547532ab866c57930dbee"}, + {file = "MarkupSafe-2.1.3-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bfce63a9e7834b12b87c64d6b155fdd9b3b96191b6bd334bf37db7ff1fe457f2"}, + {file = "MarkupSafe-2.1.3-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:338ae27d6b8745585f87218a3f23f1512dbf52c26c28e322dbe54bcede54ccb9"}, + {file = "MarkupSafe-2.1.3-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:e4dd52d80b8c83fdce44e12478ad2e85c64ea965e75d66dbeafb0a3e77308fcc"}, + {file = "MarkupSafe-2.1.3-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:df0be2b576a7abbf737b1575f048c23fb1d769f267ec4358296f31c2479db8f9"}, + {file = "MarkupSafe-2.1.3-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:5bbe06f8eeafd38e5d0a4894ffec89378b6c6a625ff57e3028921f8ff59318ac"}, + {file = "MarkupSafe-2.1.3-cp311-cp311-win32.whl", hash = "sha256:dd15ff04ffd7e05ffcb7fe79f1b98041b8ea30ae9234aed2a9168b5797c3effb"}, + {file = "MarkupSafe-2.1.3-cp311-cp311-win_amd64.whl", hash = "sha256:134da1eca9ec0ae528110ccc9e48041e0828d79f24121a1a146161103c76e686"}, + {file = "MarkupSafe-2.1.3-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:f698de3fd0c4e6972b92290a45bd9b1536bffe8c6759c62471efaa8acb4c37bc"}, + {file = "MarkupSafe-2.1.3-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:aa57bd9cf8ae831a362185ee444e15a93ecb2e344c8e52e4d721ea3ab6ef1823"}, + {file = "MarkupSafe-2.1.3-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ffcc3f7c66b5f5b7931a5aa68fc9cecc51e685ef90282f4a82f0f5e9b704ad11"}, + {file = "MarkupSafe-2.1.3-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:47d4f1c5f80fc62fdd7777d0d40a2e9dda0a05883ab11374334f6c4de38adffd"}, + {file = "MarkupSafe-2.1.3-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1f67c7038d560d92149c060157d623c542173016c4babc0c1913cca0564b9939"}, + {file = "MarkupSafe-2.1.3-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:9aad3c1755095ce347e26488214ef77e0485a3c34a50c5a5e2471dff60b9dd9c"}, + {file = "MarkupSafe-2.1.3-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:14ff806850827afd6b07a5f32bd917fb7f45b046ba40c57abdb636674a8b559c"}, + {file = "MarkupSafe-2.1.3-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:8f9293864fe09b8149f0cc42ce56e3f0e54de883a9de90cd427f191c346eb2e1"}, + {file = "MarkupSafe-2.1.3-cp312-cp312-win32.whl", hash = "sha256:715d3562f79d540f251b99ebd6d8baa547118974341db04f5ad06d5ea3eb8007"}, + {file = "MarkupSafe-2.1.3-cp312-cp312-win_amd64.whl", hash = "sha256:1b8dd8c3fd14349433c79fa8abeb573a55fc0fdd769133baac1f5e07abf54aeb"}, + {file = "MarkupSafe-2.1.3-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:8e254ae696c88d98da6555f5ace2279cf7cd5b3f52be2b5cf97feafe883b58d2"}, + {file = "MarkupSafe-2.1.3-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cb0932dc158471523c9637e807d9bfb93e06a95cbf010f1a38b98623b929ef2b"}, + {file = "MarkupSafe-2.1.3-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9402b03f1a1b4dc4c19845e5c749e3ab82d5078d16a2a4c2cd2df62d57bb0707"}, + {file = "MarkupSafe-2.1.3-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ca379055a47383d02a5400cb0d110cef0a776fc644cda797db0c5696cfd7e18e"}, + {file = "MarkupSafe-2.1.3-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:b7ff0f54cb4ff66dd38bebd335a38e2c22c41a8ee45aa608efc890ac3e3931bc"}, + {file = "MarkupSafe-2.1.3-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:c011a4149cfbcf9f03994ec2edffcb8b1dc2d2aede7ca243746df97a5d41ce48"}, + {file = "MarkupSafe-2.1.3-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:56d9f2ecac662ca1611d183feb03a3fa4406469dafe241673d521dd5ae92a155"}, + {file = "MarkupSafe-2.1.3-cp37-cp37m-win32.whl", hash = "sha256:8758846a7e80910096950b67071243da3e5a20ed2546e6392603c096778d48e0"}, + {file = "MarkupSafe-2.1.3-cp37-cp37m-win_amd64.whl", hash = "sha256:787003c0ddb00500e49a10f2844fac87aa6ce977b90b0feaaf9de23c22508b24"}, + {file = "MarkupSafe-2.1.3-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:2ef12179d3a291be237280175b542c07a36e7f60718296278d8593d21ca937d4"}, + {file = "MarkupSafe-2.1.3-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:2c1b19b3aaacc6e57b7e25710ff571c24d6c3613a45e905b1fde04d691b98ee0"}, + {file = "MarkupSafe-2.1.3-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8afafd99945ead6e075b973fefa56379c5b5c53fd8937dad92c662da5d8fd5ee"}, + {file = "MarkupSafe-2.1.3-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8c41976a29d078bb235fea9b2ecd3da465df42a562910f9022f1a03107bd02be"}, + {file = "MarkupSafe-2.1.3-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d080e0a5eb2529460b30190fcfcc4199bd7f827663f858a226a81bc27beaa97e"}, + {file = "MarkupSafe-2.1.3-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:69c0f17e9f5a7afdf2cc9fb2d1ce6aabdb3bafb7f38017c0b77862bcec2bbad8"}, + {file = "MarkupSafe-2.1.3-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:504b320cd4b7eff6f968eddf81127112db685e81f7e36e75f9f84f0df46041c3"}, + {file = "MarkupSafe-2.1.3-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:42de32b22b6b804f42c5d98be4f7e5e977ecdd9ee9b660fda1a3edf03b11792d"}, + {file = "MarkupSafe-2.1.3-cp38-cp38-win32.whl", hash = "sha256:ceb01949af7121f9fc39f7d27f91be8546f3fb112c608bc4029aef0bab86a2a5"}, + {file = "MarkupSafe-2.1.3-cp38-cp38-win_amd64.whl", hash = "sha256:1b40069d487e7edb2676d3fbdb2b0829ffa2cd63a2ec26c4938b2d34391b4ecc"}, + {file = "MarkupSafe-2.1.3-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:8023faf4e01efadfa183e863fefde0046de576c6f14659e8782065bcece22198"}, + {file = "MarkupSafe-2.1.3-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:6b2b56950d93e41f33b4223ead100ea0fe11f8e6ee5f641eb753ce4b77a7042b"}, + {file = "MarkupSafe-2.1.3-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9dcdfd0eaf283af041973bff14a2e143b8bd64e069f4c383416ecd79a81aab58"}, + {file = "MarkupSafe-2.1.3-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:05fb21170423db021895e1ea1e1f3ab3adb85d1c2333cbc2310f2a26bc77272e"}, + {file = "MarkupSafe-2.1.3-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:282c2cb35b5b673bbcadb33a585408104df04f14b2d9b01d4c345a3b92861c2c"}, + {file = "MarkupSafe-2.1.3-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:ab4a0df41e7c16a1392727727e7998a467472d0ad65f3ad5e6e765015df08636"}, + {file = "MarkupSafe-2.1.3-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:7ef3cb2ebbf91e330e3bb937efada0edd9003683db6b57bb108c4001f37a02ea"}, + {file = "MarkupSafe-2.1.3-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:0a4e4a1aff6c7ac4cd55792abf96c915634c2b97e3cc1c7129578aa68ebd754e"}, + {file = "MarkupSafe-2.1.3-cp39-cp39-win32.whl", hash = "sha256:fec21693218efe39aa7f8599346e90c705afa52c5b31ae019b2e57e8f6542bb2"}, + {file = "MarkupSafe-2.1.3-cp39-cp39-win_amd64.whl", hash = "sha256:3fd4abcb888d15a94f32b75d8fd18ee162ca0c064f35b11134be77050296d6ba"}, + {file = "MarkupSafe-2.1.3.tar.gz", hash = "sha256:af598ed32d6ae86f1b747b82783958b1a4ab8f617b06fe68795c7f026abbdcad"}, +] + +[[package]] +name = "matplotlib" +version = "3.8.0" +description = "Python plotting package" +optional = false +python-versions = ">=3.9" +files = [ + {file = "matplotlib-3.8.0-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:c4940bad88a932ddc69734274f6fb047207e008389489f2b6f77d9ca485f0e7a"}, + {file = "matplotlib-3.8.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:a33bd3045c7452ca1fa65676d88ba940867880e13e2546abb143035fa9072a9d"}, + {file = "matplotlib-3.8.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2ea6886e93401c22e534bbfd39201ce8931b75502895cfb115cbdbbe2d31f287"}, + {file = "matplotlib-3.8.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d670b9348e712ec176de225d425f150dc8e37b13010d85233c539b547da0be39"}, + {file = "matplotlib-3.8.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:7b37b74f00c4cb6af908cb9a00779d97d294e89fd2145ad43f0cdc23f635760c"}, + {file = "matplotlib-3.8.0-cp310-cp310-win_amd64.whl", hash = "sha256:0e723f5b96f3cd4aad99103dc93e9e3cdc4f18afdcc76951f4857b46f8e39d2d"}, + {file = "matplotlib-3.8.0-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:5dc945a9cb2deb7d197ba23eb4c210e591d52d77bf0ba27c35fc82dec9fa78d4"}, + {file = "matplotlib-3.8.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:f8b5a1bf27d078453aa7b5b27f52580e16360d02df6d3dc9504f3d2ce11f6309"}, + {file = "matplotlib-3.8.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6f25ffb6ad972cdffa7df8e5be4b1e3cadd2f8d43fc72085feb1518006178394"}, + {file = "matplotlib-3.8.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:eee482731c8c17d86d9ddb5194d38621f9b0f0d53c99006275a12523ab021732"}, + {file = "matplotlib-3.8.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:36eafe2128772195b373e1242df28d1b7ec6c04c15b090b8d9e335d55a323900"}, + {file = "matplotlib-3.8.0-cp311-cp311-win_amd64.whl", hash = "sha256:061ee58facb3580cd2d046a6d227fb77e9295599c5ec6ad069f06b5821ad1cfc"}, + {file = "matplotlib-3.8.0-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:3cc3776836d0f4f22654a7f2d2ec2004618d5cf86b7185318381f73b80fd8a2d"}, + {file = "matplotlib-3.8.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:6c49a2bd6981264bddcb8c317b6bd25febcece9e2ebfcbc34e7f4c0c867c09dc"}, + {file = "matplotlib-3.8.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:23ed11654fc83cd6cfdf6170b453e437674a050a452133a064d47f2f1371f8d3"}, + {file = "matplotlib-3.8.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dae97fdd6996b3a25da8ee43e3fc734fff502f396801063c6b76c20b56683196"}, + {file = "matplotlib-3.8.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:87df75f528020a6299f76a1d986c0ed4406e3b2bd44bc5e306e46bca7d45e53e"}, + {file = "matplotlib-3.8.0-cp312-cp312-win_amd64.whl", hash = "sha256:90d74a95fe055f73a6cd737beecc1b81c26f2893b7a3751d52b53ff06ca53f36"}, + {file = "matplotlib-3.8.0-cp39-cp39-macosx_10_12_x86_64.whl", hash = "sha256:c3499c312f5def8f362a2bf761d04fa2d452b333f3a9a3f58805273719bf20d9"}, + {file = "matplotlib-3.8.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:31e793c8bd4ea268cc5d3a695c27b30650ec35238626961d73085d5e94b6ab68"}, + {file = "matplotlib-3.8.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0d5ee602ef517a89d1f2c508ca189cfc395dd0b4a08284fb1b97a78eec354644"}, + {file = "matplotlib-3.8.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5de39dc61ca35342cf409e031f70f18219f2c48380d3886c1cf5ad9f17898e06"}, + {file = "matplotlib-3.8.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:dd386c80a98b5f51571b9484bf6c6976de383cd2a8cd972b6a9562d85c6d2087"}, + {file = "matplotlib-3.8.0-cp39-cp39-win_amd64.whl", hash = "sha256:f691b4ef47c7384d0936b2e8ebdeb5d526c81d004ad9403dfb9d4c76b9979a93"}, + {file = "matplotlib-3.8.0-pp39-pypy39_pp73-macosx_10_12_x86_64.whl", hash = "sha256:0b11f354aae62a2aa53ec5bb09946f5f06fc41793e351a04ff60223ea9162955"}, + {file = "matplotlib-3.8.0-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7f54b9fb87ca5acbcdd0f286021bedc162e1425fa5555ebf3b3dfc167b955ad9"}, + {file = "matplotlib-3.8.0-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:60a6e04dfd77c0d3bcfee61c3cd335fff1b917c2f303b32524cd1235e194ef99"}, + {file = "matplotlib-3.8.0.tar.gz", hash = "sha256:df8505e1c19d5c2c26aff3497a7cbd3ccfc2e97043d1e4db3e76afa399164b69"}, +] + +[package.dependencies] +contourpy = ">=1.0.1" +cycler = ">=0.10" +fonttools = ">=4.22.0" +kiwisolver = ">=1.0.1" +numpy = ">=1.21,<2" +packaging = ">=20.0" +pillow = ">=6.2.0" +pyparsing = ">=2.3.1" +python-dateutil = ">=2.7" +setuptools_scm = ">=7" + +[[package]] +name = "mergedeep" +version = "1.3.4" +description = "A deep merge function for 🐍." +optional = false +python-versions = ">=3.6" +files = [ + {file = "mergedeep-1.3.4-py3-none-any.whl", hash = "sha256:70775750742b25c0d8f36c55aed03d24c3384d17c951b3175d898bd778ef0307"}, + {file = "mergedeep-1.3.4.tar.gz", hash = "sha256:0096d52e9dad9939c3d975a774666af186eda617e6ca84df4c94dec30004f2a8"}, +] + +[[package]] +name = "mhn" +version = "0.0.14" +description = "A package to train and work with Mutual Hazard Networks" +optional = false +python-versions = ">=3.8" +files = [ + {file = "mhn-0.0.14.tar.gz", hash = "sha256:0aeaa4dfc4353c8488b5012a7e3f1095f4e7b05b0ddf1c10793cc313fa21c7a9"}, +] + +[package.dependencies] +numpy = ">=1.23.0" +pandas = ">=1.5.3" +scipy = ">=1.8.0" + +[[package]] +name = "minikanren" +version = "1.0.3" +description = "Relational programming in Python" +optional = false +python-versions = ">=3.6" +files = [ + {file = "miniKanren-1.0.3.tar.gz", hash = "sha256:1ec8bdb01144ad5e8752c7c297fb8a122db920f859276d25a72d164e998d7f6e"}, +] + +[package.dependencies] +cons = ">=0.4.0" +etuples = ">=0.3.1" +logical-unification = ">=0.4.1" +multipledispatch = "*" +toolz = "*" + +[[package]] +name = "mkdocs" +version = "1.5.3" +description = "Project documentation with Markdown." +optional = false +python-versions = ">=3.7" +files = [ + {file = "mkdocs-1.5.3-py3-none-any.whl", hash = "sha256:3b3a78e736b31158d64dbb2f8ba29bd46a379d0c6e324c2246c3bc3d2189cfc1"}, + {file = "mkdocs-1.5.3.tar.gz", hash = "sha256:eb7c99214dcb945313ba30426c2451b735992c73c2e10838f76d09e39ff4d0e2"}, +] + +[package.dependencies] +click = ">=7.0" +colorama = {version = ">=0.4", markers = "platform_system == \"Windows\""} +ghp-import = ">=1.0" +jinja2 = ">=2.11.1" +markdown = ">=3.2.1" +markupsafe = ">=2.0.1" +mergedeep = ">=1.3.4" +packaging = ">=20.5" +pathspec = ">=0.11.1" +platformdirs = ">=2.2.0" +pyyaml = ">=5.1" +pyyaml-env-tag = ">=0.1" +watchdog = ">=2.0" + +[package.extras] +i18n = ["babel (>=2.9.0)"] +min-versions = ["babel (==2.9.0)", "click (==7.0)", "colorama (==0.4)", "ghp-import (==1.0)", "importlib-metadata (==4.3)", "jinja2 (==2.11.1)", "markdown (==3.2.1)", "markupsafe (==2.0.1)", "mergedeep (==1.3.4)", "packaging (==20.5)", "pathspec (==0.11.1)", "platformdirs (==2.2.0)", "pyyaml (==5.1)", "pyyaml-env-tag (==0.1)", "typing-extensions (==3.10)", "watchdog (==2.0)"] + +[[package]] +name = "mkdocs-autorefs" +version = "0.5.0" +description = "Automatically link across pages in MkDocs." +optional = false +python-versions = ">=3.8" +files = [ + {file = "mkdocs_autorefs-0.5.0-py3-none-any.whl", hash = "sha256:7930fcb8ac1249f10e683967aeaddc0af49d90702af111a5e390e8b20b3d97ff"}, + {file = "mkdocs_autorefs-0.5.0.tar.gz", hash = "sha256:9a5054a94c08d28855cfab967ada10ed5be76e2bfad642302a610b252c3274c0"}, +] + +[package.dependencies] +Markdown = ">=3.3" +mkdocs = ">=1.1" + +[[package]] +name = "mkdocs-material" +version = "9.3.2" +description = "Documentation that simply works" +optional = false +python-versions = ">=3.8" +files = [ + {file = "mkdocs_material-9.3.2-py3-none-any.whl", hash = "sha256:f2fd5cef6f0266b4caad6414f31c6a51e3183dbdd341995ad8fa7f33bc998c3d"}, + {file = "mkdocs_material-9.3.2.tar.gz", hash = "sha256:7b3a35a7731af02d70d120224fcec053ce09bebbf83dff3366ab72abc4d5fc89"}, +] + +[package.dependencies] +babel = ">=2.10,<3.0" +colorama = ">=0.4,<1.0" +jinja2 = ">=3.0,<4.0" +markdown = ">=3.2,<4.0" +mkdocs = ">=1.5,<2.0" +mkdocs-material-extensions = ">=1.1,<2.0" +paginate = ">=0.5,<1.0" +pygments = ">=2.16,<3.0" +pymdown-extensions = ">=10.2,<11.0" +regex = ">=2022.4,<2023.0" +requests = ">=2.26,<3.0" + +[package.extras] +git = ["mkdocs-git-committers-plugin-2 (>=1.1,<2.0)", "mkdocs-git-revision-date-localized-plugin (>=1.2,<2.0)"] +imaging = ["cairosvg (>=2.6,<3.0)", "pillow (>=9.4,<10.0)"] +recommended = ["mkdocs-minify-plugin (>=0.7,<1.0)", "mkdocs-redirects (>=1.2,<2.0)", "mkdocs-rss-plugin (>=1.6,<2.0)"] + +[[package]] +name = "mkdocs-material-extensions" +version = "1.1.1" +description = "Extension pack for Python Markdown and MkDocs Material." +optional = false +python-versions = ">=3.7" +files = [ + {file = "mkdocs_material_extensions-1.1.1-py3-none-any.whl", hash = "sha256:e41d9f38e4798b6617ad98ca8f7f1157b1e4385ac1459ca1e4ea219b556df945"}, + {file = "mkdocs_material_extensions-1.1.1.tar.gz", hash = "sha256:9c003da71e2cc2493d910237448c672e00cefc800d3d6ae93d2fc69979e3bd93"}, +] + +[[package]] +name = "mkdocstrings" +version = "0.22.0" +description = "Automatic documentation from sources, for MkDocs." +optional = false +python-versions = ">=3.7" +files = [ + {file = "mkdocstrings-0.22.0-py3-none-any.whl", hash = "sha256:2d4095d461554ff6a778fdabdca3c00c468c2f1459d469f7a7f622a2b23212ba"}, + {file = "mkdocstrings-0.22.0.tar.gz", hash = "sha256:82a33b94150ebb3d4b5c73bab4598c3e21468c79ec072eff6931c8f3bfc38256"}, +] + +[package.dependencies] +Jinja2 = ">=2.11.1" +Markdown = ">=3.3" +MarkupSafe = ">=1.1" +mkdocs = ">=1.2" +mkdocs-autorefs = ">=0.3.1" +mkdocstrings-python = {version = ">=0.5.2", optional = true, markers = "extra == \"python\""} +pymdown-extensions = ">=6.3" + +[package.extras] +crystal = ["mkdocstrings-crystal (>=0.3.4)"] +python = ["mkdocstrings-python (>=0.5.2)"] +python-legacy = ["mkdocstrings-python-legacy (>=0.2.1)"] + +[[package]] +name = "mkdocstrings-python" +version = "1.7.0" +description = "A Python handler for mkdocstrings." +optional = false +python-versions = ">=3.8" +files = [ + {file = "mkdocstrings_python-1.7.0-py3-none-any.whl", hash = "sha256:85c5f009a5a0ebb6076b7818c82a2bb0eebd0b54662628fa8b25ee14a6207951"}, + {file = "mkdocstrings_python-1.7.0.tar.gz", hash = "sha256:5dac2712bd38a3ff0812b8650a68b232601d1474091b380a8b5bc102c8c0d80a"}, +] + +[package.dependencies] +griffe = ">=0.35" +mkdocstrings = ">=0.20" + +[[package]] +name = "multipledispatch" +version = "1.0.0" +description = "Multiple dispatch" +optional = false +python-versions = "*" +files = [ + {file = "multipledispatch-1.0.0-py3-none-any.whl", hash = "sha256:0c53cd8b077546da4e48869f49b13164bebafd0c2a5afceb6bb6a316e7fb46e4"}, + {file = "multipledispatch-1.0.0.tar.gz", hash = "sha256:5c839915465c68206c3e9c473357908216c28383b425361e5d144594bf85a7e0"}, +] + +[[package]] +name = "mypy-extensions" +version = "1.0.0" +description = "Type system extensions for programs checked with the mypy type checker." +optional = false +python-versions = ">=3.5" +files = [ + {file = "mypy_extensions-1.0.0-py3-none-any.whl", hash = "sha256:4392f6c0eb8a5668a69e23d168ffa70f0be9ccfd32b5cc2d26a34ae5b844552d"}, + {file = "mypy_extensions-1.0.0.tar.gz", hash = "sha256:75dbf8955dc00442a438fc4d0666508a9a97b6bd41aa2f0ffe9d2f2725af0782"}, +] + +[[package]] +name = "netcdf4" +version = "1.6.4" +description = "Provides an object-oriented python interface to the netCDF version 4 library" +optional = false +python-versions = ">=3.7" +files = [ + {file = "netCDF4-1.6.4-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:59eba11eac34f9c7bd209121b26954c71ff69a2eb2838d2ac339b39ea60a95c0"}, + {file = "netCDF4-1.6.4-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:541286ce7318033b326da2e4325f08037b561887d5fd25d12c2b0cad74eb1edc"}, + {file = "netCDF4-1.6.4-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6620f7962daff02df2fbcbc8f7af53edf21512bc7f5697d846d424a094f96345"}, + {file = "netCDF4-1.6.4-cp310-cp310-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:2dcf74f7b15ae6105c09df93af22ad61366a288fbb329fdbe3b9606c56c788f5"}, + {file = "netCDF4-1.6.4-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b368fc185cac7d8890a8cf1016488cd252a144008144d06a615925b09bf8d67e"}, + {file = "netCDF4-1.6.4-cp310-cp310-win_amd64.whl", hash = "sha256:eb19c51fa090b562be4f87ff736b629ffafc03d7bf1fa10f623f957d49417b4f"}, + {file = "netCDF4-1.6.4-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:08a7efab50d5a4da6227244e544a74f676b732ccdff2b8a4678174c15a3f8dff"}, + {file = "netCDF4-1.6.4-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:f9b7ad3f66d50970fa1bd5509c52bb8a99740c8a79fafa83bfd0bc7348b6ab5a"}, + {file = "netCDF4-1.6.4-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2c5cad50d4e6e5a3b35c76862c58393ddf93baa44de98b6e040ac21896145881"}, + {file = "netCDF4-1.6.4-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1c2395c53a37717b1047650e1c9de68a3d69542bb25df5c594e1e14e9480bb18"}, + {file = "netCDF4-1.6.4-cp311-cp311-win_amd64.whl", hash = "sha256:c85f77599c61a88d512d6536e181ff1c01fd16f4740367e4f8e5cacb36500293"}, + {file = "netCDF4-1.6.4-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:3e3efefdb9fc936503e89ff5b40b33f2c96527bae1e9672a2d369c5f7cb30bbd"}, + {file = "netCDF4-1.6.4-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:74a3e720a9764d13d669763c1f3c73f3f719ef181dccecad062440eea03f069e"}, + {file = "netCDF4-1.6.4-cp37-cp37m-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c67bc7fafced3f9370fdef6ce7abe235a771bed7ebe534f4ba3c84d9689dae23"}, + {file = "netCDF4-1.6.4-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b599a3b5c0d0e3affa70d7954c2b0c4ab7d7bdb52b0e413c811da9725982de33"}, + {file = "netCDF4-1.6.4-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:b21d47c23edd02ff83160c8ccc1e4d4946a91d454b246e7f63d7a6d63901707c"}, + {file = "netCDF4-1.6.4-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:8eff69acdf250ebaf817cc9bbfb457d241f92f989c7f9cca33eb29985c0f9797"}, + {file = "netCDF4-1.6.4-cp38-cp38-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4d6bcdf27fd611eaec891f4281caf5c56bbad6f5fc245ce3e6dd2dc4fc3fad56"}, + {file = "netCDF4-1.6.4-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c99a3d476242d489da26f71e318ddd314723ca766f4db11270863a764b107bc3"}, + {file = "netCDF4-1.6.4-cp38-cp38-win_amd64.whl", hash = "sha256:422077c7303cb17cbe8b9ace965cafda1f9d4c9b035809fc5c87091e7ff4d606"}, + {file = "netCDF4-1.6.4-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:0050e49889c357ae0a73f96e2b9988f250396689b78142de88a39ac8b1e702aa"}, + {file = "netCDF4-1.6.4-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:0a2cd2e2a9da98f1838e1edfeb011b840a434e04d156052b936ffe49f5298126"}, + {file = "netCDF4-1.6.4-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:695cd0a40df49b4218350686c44e13b3a4671e53ee33faf7439a89940cbccc68"}, + {file = "netCDF4-1.6.4-cp39-cp39-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e582e6e41b27fc3a3296239afe065941bda60118d585df0ad41603f6f962888e"}, + {file = "netCDF4-1.6.4-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f7b4ab3f0cd28a6685031e8b9be00c3047b4256da576c9de540440d013a31cea"}, + {file = "netCDF4-1.6.4-cp39-cp39-win_amd64.whl", hash = "sha256:1a950b3d8fcffca05e371a4cb64dc76580375588d1fca43b788192f409108ebe"}, + {file = "netCDF4-1.6.4.tar.gz", hash = "sha256:66da6542cbc7a6045cd1d979397dfd5a3f6c880c76d52b8f98bb108c82ee8c6e"}, +] + +[package.dependencies] +certifi = "*" +cftime = "*" +numpy = "*" + +[[package]] +name = "nodeenv" +version = "1.8.0" +description = "Node.js virtual environment builder" +optional = false +python-versions = ">=2.7,!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*" +files = [ + {file = "nodeenv-1.8.0-py2.py3-none-any.whl", hash = "sha256:df865724bb3c3adc86b3876fa209771517b0cfe596beff01a92700e0e8be4cec"}, + {file = "nodeenv-1.8.0.tar.gz", hash = "sha256:d51e0c37e64fbf47d017feac3145cdbb58836d7eee8c6f6d3b6880c5456227d2"}, +] + +[package.dependencies] +setuptools = "*" + +[[package]] +name = "numpy" +version = "1.25.2" +description = "Fundamental package for array computing in Python" +optional = false +python-versions = ">=3.9" +files = [ + {file = "numpy-1.25.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:db3ccc4e37a6873045580d413fe79b68e47a681af8db2e046f1dacfa11f86eb3"}, + {file = "numpy-1.25.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:90319e4f002795ccfc9050110bbbaa16c944b1c37c0baeea43c5fb881693ae1f"}, + {file = "numpy-1.25.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:dfe4a913e29b418d096e696ddd422d8a5d13ffba4ea91f9f60440a3b759b0187"}, + {file = "numpy-1.25.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f08f2e037bba04e707eebf4bc934f1972a315c883a9e0ebfa8a7756eabf9e357"}, + {file = "numpy-1.25.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:bec1e7213c7cb00d67093247f8c4db156fd03075f49876957dca4711306d39c9"}, + {file = "numpy-1.25.2-cp310-cp310-win32.whl", hash = "sha256:7dc869c0c75988e1c693d0e2d5b26034644399dd929bc049db55395b1379e044"}, + {file = "numpy-1.25.2-cp310-cp310-win_amd64.whl", hash = "sha256:834b386f2b8210dca38c71a6e0f4fd6922f7d3fcff935dbe3a570945acb1b545"}, + {file = "numpy-1.25.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:c5462d19336db4560041517dbb7759c21d181a67cb01b36ca109b2ae37d32418"}, + {file = "numpy-1.25.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:c5652ea24d33585ea39eb6a6a15dac87a1206a692719ff45d53c5282e66d4a8f"}, + {file = "numpy-1.25.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0d60fbae8e0019865fc4784745814cff1c421df5afee233db6d88ab4f14655a2"}, + {file = "numpy-1.25.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:60e7f0f7f6d0eee8364b9a6304c2845b9c491ac706048c7e8cf47b83123b8dbf"}, + {file = "numpy-1.25.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:bb33d5a1cf360304754913a350edda36d5b8c5331a8237268c48f91253c3a364"}, + {file = "numpy-1.25.2-cp311-cp311-win32.whl", hash = "sha256:5883c06bb92f2e6c8181df7b39971a5fb436288db58b5a1c3967702d4278691d"}, + {file = "numpy-1.25.2-cp311-cp311-win_amd64.whl", hash = "sha256:5c97325a0ba6f9d041feb9390924614b60b99209a71a69c876f71052521d42a4"}, + {file = "numpy-1.25.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:b79e513d7aac42ae918db3ad1341a015488530d0bb2a6abcbdd10a3a829ccfd3"}, + {file = "numpy-1.25.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:eb942bfb6f84df5ce05dbf4b46673ffed0d3da59f13635ea9b926af3deb76926"}, + {file = "numpy-1.25.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3e0746410e73384e70d286f93abf2520035250aad8c5714240b0492a7302fdca"}, + {file = "numpy-1.25.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d7806500e4f5bdd04095e849265e55de20d8cc4b661b038957354327f6d9b295"}, + {file = "numpy-1.25.2-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:8b77775f4b7df768967a7c8b3567e309f617dd5e99aeb886fa14dc1a0791141f"}, + {file = "numpy-1.25.2-cp39-cp39-win32.whl", hash = "sha256:2792d23d62ec51e50ce4d4b7d73de8f67a2fd3ea710dcbc8563a51a03fb07b01"}, + {file = "numpy-1.25.2-cp39-cp39-win_amd64.whl", hash = "sha256:76b4115d42a7dfc5d485d358728cdd8719be33cc5ec6ec08632a5d6fca2ed380"}, + {file = "numpy-1.25.2-pp39-pypy39_pp73-macosx_10_9_x86_64.whl", hash = "sha256:1a1329e26f46230bf77b02cc19e900db9b52f398d6722ca853349a782d4cff55"}, + {file = "numpy-1.25.2-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4c3abc71e8b6edba80a01a52e66d83c5d14433cbcd26a40c329ec7ed09f37901"}, + {file = "numpy-1.25.2-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:1b9735c27cea5d995496f46a8b1cd7b408b3f34b6d50459d9ac8fe3a20cc17bf"}, + {file = "numpy-1.25.2.tar.gz", hash = "sha256:fd608e19c8d7c55021dffd43bfe5492fab8cc105cc8986f813f8c3c048b38760"}, +] + +[[package]] +name = "packaging" +version = "23.1" +description = "Core utilities for Python packages" +optional = false +python-versions = ">=3.7" +files = [ + {file = "packaging-23.1-py3-none-any.whl", hash = "sha256:994793af429502c4ea2ebf6bf664629d07c1a9fe974af92966e4b8d2df7edc61"}, + {file = "packaging-23.1.tar.gz", hash = "sha256:a392980d2b6cffa644431898be54b0045151319d1e7ec34f0cfed48767dd334f"}, +] + +[[package]] +name = "paginate" +version = "0.5.6" +description = "Divides large result sets into pages for easier browsing" +optional = false +python-versions = "*" +files = [ + {file = "paginate-0.5.6.tar.gz", hash = "sha256:5e6007b6a9398177a7e1648d04fdd9f8c9766a1a945bceac82f1929e8c78af2d"}, +] + +[[package]] +name = "pandas" +version = "2.1.0" +description = "Powerful data structures for data analysis, time series, and statistics" +optional = false +python-versions = ">=3.9" +files = [ + {file = "pandas-2.1.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:40dd20439ff94f1b2ed55b393ecee9cb6f3b08104c2c40b0cb7186a2f0046242"}, + {file = "pandas-2.1.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:d4f38e4fedeba580285eaac7ede4f686c6701a9e618d8a857b138a126d067f2f"}, + {file = "pandas-2.1.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6e6a0fe052cf27ceb29be9429428b4918f3740e37ff185658f40d8702f0b3e09"}, + {file = "pandas-2.1.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9d81e1813191070440d4c7a413cb673052b3b4a984ffd86b8dd468c45742d3cc"}, + {file = "pandas-2.1.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:eb20252720b1cc1b7d0b2879ffc7e0542dd568f24d7c4b2347cb035206936421"}, + {file = "pandas-2.1.0-cp310-cp310-win_amd64.whl", hash = "sha256:38f74ef7ebc0ffb43b3d633e23d74882bce7e27bfa09607f3c5d3e03ffd9a4a5"}, + {file = "pandas-2.1.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:cda72cc8c4761c8f1d97b169661f23a86b16fdb240bdc341173aee17e4d6cedd"}, + {file = "pandas-2.1.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:d97daeac0db8c993420b10da4f5f5b39b01fc9ca689a17844e07c0a35ac96b4b"}, + {file = "pandas-2.1.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d8c58b1113892e0c8078f006a167cc210a92bdae23322bb4614f2f0b7a4b510f"}, + {file = "pandas-2.1.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:629124923bcf798965b054a540f9ccdfd60f71361255c81fa1ecd94a904b9dd3"}, + {file = "pandas-2.1.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:70cf866af3ab346a10debba8ea78077cf3a8cd14bd5e4bed3d41555a3280041c"}, + {file = "pandas-2.1.0-cp311-cp311-win_amd64.whl", hash = "sha256:d53c8c1001f6a192ff1de1efe03b31a423d0eee2e9e855e69d004308e046e694"}, + {file = "pandas-2.1.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:86f100b3876b8c6d1a2c66207288ead435dc71041ee4aea789e55ef0e06408cb"}, + {file = "pandas-2.1.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:28f330845ad21c11db51e02d8d69acc9035edfd1116926ff7245c7215db57957"}, + {file = "pandas-2.1.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b9a6ccf0963db88f9b12df6720e55f337447aea217f426a22d71f4213a3099a6"}, + {file = "pandas-2.1.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d99e678180bc59b0c9443314297bddce4ad35727a1a2656dbe585fd78710b3b9"}, + {file = "pandas-2.1.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:b31da36d376d50a1a492efb18097b9101bdbd8b3fbb3f49006e02d4495d4c644"}, + {file = "pandas-2.1.0-cp39-cp39-win_amd64.whl", hash = "sha256:0164b85937707ec7f70b34a6c3a578dbf0f50787f910f21ca3b26a7fd3363437"}, + {file = "pandas-2.1.0.tar.gz", hash = "sha256:62c24c7fc59e42b775ce0679cfa7b14a5f9bfb7643cfbe708c960699e05fb918"}, +] + +[package.dependencies] +numpy = [ + {version = ">=1.22.4", markers = "python_version < \"3.11\""}, + {version = ">=1.23.2", markers = "python_version >= \"3.11\""}, +] +python-dateutil = ">=2.8.2" +pytz = ">=2020.1" +tzdata = ">=2022.1" + +[package.extras] +all = ["PyQt5 (>=5.15.6)", "SQLAlchemy (>=1.4.36)", "beautifulsoup4 (>=4.11.1)", "bottleneck (>=1.3.4)", "dataframe-api-compat (>=0.1.7)", "fastparquet (>=0.8.1)", "fsspec (>=2022.05.0)", "gcsfs (>=2022.05.0)", "html5lib (>=1.1)", "hypothesis (>=6.46.1)", "jinja2 (>=3.1.2)", "lxml (>=4.8.0)", "matplotlib (>=3.6.1)", "numba (>=0.55.2)", "numexpr (>=2.8.0)", "odfpy (>=1.4.1)", "openpyxl (>=3.0.10)", "pandas-gbq (>=0.17.5)", "psycopg2 (>=2.9.3)", "pyarrow (>=7.0.0)", "pymysql (>=1.0.2)", "pyreadstat (>=1.1.5)", "pytest (>=7.3.2)", "pytest-asyncio (>=0.17.0)", "pytest-xdist (>=2.2.0)", "pyxlsb (>=1.0.9)", "qtpy (>=2.2.0)", "s3fs (>=2022.05.0)", "scipy (>=1.8.1)", "tables (>=3.7.0)", "tabulate (>=0.8.10)", "xarray (>=2022.03.0)", "xlrd (>=2.0.1)", "xlsxwriter (>=3.0.3)", "zstandard (>=0.17.0)"] +aws = ["s3fs (>=2022.05.0)"] +clipboard = ["PyQt5 (>=5.15.6)", "qtpy (>=2.2.0)"] +compression = ["zstandard (>=0.17.0)"] +computation = ["scipy (>=1.8.1)", "xarray (>=2022.03.0)"] +consortium-standard = ["dataframe-api-compat (>=0.1.7)"] +excel = ["odfpy (>=1.4.1)", "openpyxl (>=3.0.10)", "pyxlsb (>=1.0.9)", "xlrd (>=2.0.1)", "xlsxwriter (>=3.0.3)"] +feather = ["pyarrow (>=7.0.0)"] +fss = ["fsspec (>=2022.05.0)"] +gcp = ["gcsfs (>=2022.05.0)", "pandas-gbq (>=0.17.5)"] +hdf5 = ["tables (>=3.7.0)"] +html = ["beautifulsoup4 (>=4.11.1)", "html5lib (>=1.1)", "lxml (>=4.8.0)"] +mysql = ["SQLAlchemy (>=1.4.36)", "pymysql (>=1.0.2)"] +output-formatting = ["jinja2 (>=3.1.2)", "tabulate (>=0.8.10)"] +parquet = ["pyarrow (>=7.0.0)"] +performance = ["bottleneck (>=1.3.4)", "numba (>=0.55.2)", "numexpr (>=2.8.0)"] +plot = ["matplotlib (>=3.6.1)"] +postgresql = ["SQLAlchemy (>=1.4.36)", "psycopg2 (>=2.9.3)"] +spss = ["pyreadstat (>=1.1.5)"] +sql-other = ["SQLAlchemy (>=1.4.36)"] +test = ["hypothesis (>=6.46.1)", "pytest (>=7.3.2)", "pytest-asyncio (>=0.17.0)", "pytest-xdist (>=2.2.0)"] +xml = ["lxml (>=4.8.0)"] + +[[package]] +name = "pathspec" +version = "0.11.2" +description = "Utility library for gitignore style pattern matching of file paths." +optional = false +python-versions = ">=3.7" +files = [ + {file = "pathspec-0.11.2-py3-none-any.whl", hash = "sha256:1d6ed233af05e679efb96b1851550ea95bbb64b7c490b0f5aa52996c11e92a20"}, + {file = "pathspec-0.11.2.tar.gz", hash = "sha256:e0d8d0ac2f12da61956eb2306b69f9469b42f4deb0f3cb6ed47b9cce9996ced3"}, +] + +[[package]] +name = "pillow" +version = "10.0.1" +description = "Python Imaging Library (Fork)" +optional = false +python-versions = ">=3.8" +files = [ + {file = "Pillow-10.0.1-cp310-cp310-macosx_10_10_x86_64.whl", hash = "sha256:8f06be50669087250f319b706decf69ca71fdecd829091a37cc89398ca4dc17a"}, + {file = "Pillow-10.0.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:50bd5f1ebafe9362ad622072a1d2f5850ecfa44303531ff14353a4059113b12d"}, + {file = "Pillow-10.0.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e6a90167bcca1216606223a05e2cf991bb25b14695c518bc65639463d7db722d"}, + {file = "Pillow-10.0.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f11c9102c56ffb9ca87134bd025a43d2aba3f1155f508eff88f694b33a9c6d19"}, + {file = "Pillow-10.0.1-cp310-cp310-manylinux_2_28_aarch64.whl", hash = "sha256:186f7e04248103482ea6354af6d5bcedb62941ee08f7f788a1c7707bc720c66f"}, + {file = "Pillow-10.0.1-cp310-cp310-manylinux_2_28_x86_64.whl", hash = "sha256:0462b1496505a3462d0f35dc1c4d7b54069747d65d00ef48e736acda2c8cbdff"}, + {file = "Pillow-10.0.1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:d889b53ae2f030f756e61a7bff13684dcd77e9af8b10c6048fb2c559d6ed6eaf"}, + {file = "Pillow-10.0.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:552912dbca585b74d75279a7570dd29fa43b6d93594abb494ebb31ac19ace6bd"}, + {file = "Pillow-10.0.1-cp310-cp310-win_amd64.whl", hash = "sha256:787bb0169d2385a798888e1122c980c6eff26bf941a8ea79747d35d8f9210ca0"}, + {file = "Pillow-10.0.1-cp311-cp311-macosx_10_10_x86_64.whl", hash = "sha256:fd2a5403a75b54661182b75ec6132437a181209b901446ee5724b589af8edef1"}, + {file = "Pillow-10.0.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:2d7e91b4379f7a76b31c2dda84ab9e20c6220488e50f7822e59dac36b0cd92b1"}, + {file = "Pillow-10.0.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:19e9adb3f22d4c416e7cd79b01375b17159d6990003633ff1d8377e21b7f1b21"}, + {file = "Pillow-10.0.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:93139acd8109edcdeffd85e3af8ae7d88b258b3a1e13a038f542b79b6d255c54"}, + {file = "Pillow-10.0.1-cp311-cp311-manylinux_2_28_aarch64.whl", hash = "sha256:92a23b0431941a33242b1f0ce6c88a952e09feeea9af4e8be48236a68ffe2205"}, + {file = "Pillow-10.0.1-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:cbe68deb8580462ca0d9eb56a81912f59eb4542e1ef8f987405e35a0179f4ea2"}, + {file = "Pillow-10.0.1-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:522ff4ac3aaf839242c6f4e5b406634bfea002469656ae8358644fc6c4856a3b"}, + {file = "Pillow-10.0.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:84efb46e8d881bb06b35d1d541aa87f574b58e87f781cbba8d200daa835b42e1"}, + {file = "Pillow-10.0.1-cp311-cp311-win_amd64.whl", hash = "sha256:898f1d306298ff40dc1b9ca24824f0488f6f039bc0e25cfb549d3195ffa17088"}, + {file = "Pillow-10.0.1-cp312-cp312-macosx_10_10_x86_64.whl", hash = "sha256:bcf1207e2f2385a576832af02702de104be71301c2696d0012b1b93fe34aaa5b"}, + {file = "Pillow-10.0.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:5d6c9049c6274c1bb565021367431ad04481ebb54872edecfcd6088d27edd6ed"}, + {file = "Pillow-10.0.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:28444cb6ad49726127d6b340217f0627abc8732f1194fd5352dec5e6a0105635"}, + {file = "Pillow-10.0.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:de596695a75496deb3b499c8c4f8e60376e0516e1a774e7bc046f0f48cd620ad"}, + {file = "Pillow-10.0.1-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:2872f2d7846cf39b3dbff64bc1104cc48c76145854256451d33c5faa55c04d1a"}, + {file = "Pillow-10.0.1-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:4ce90f8a24e1c15465048959f1e94309dfef93af272633e8f37361b824532e91"}, + {file = "Pillow-10.0.1-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:ee7810cf7c83fa227ba9125de6084e5e8b08c59038a7b2c9045ef4dde61663b4"}, + {file = "Pillow-10.0.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:b1be1c872b9b5fcc229adeadbeb51422a9633abd847c0ff87dc4ef9bb184ae08"}, + {file = "Pillow-10.0.1-cp312-cp312-win_amd64.whl", hash = "sha256:98533fd7fa764e5f85eebe56c8e4094db912ccbe6fbf3a58778d543cadd0db08"}, + {file = "Pillow-10.0.1-cp38-cp38-macosx_10_10_x86_64.whl", hash = "sha256:764d2c0daf9c4d40ad12fbc0abd5da3af7f8aa11daf87e4fa1b834000f4b6b0a"}, + {file = "Pillow-10.0.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:fcb59711009b0168d6ee0bd8fb5eb259c4ab1717b2f538bbf36bacf207ef7a68"}, + {file = "Pillow-10.0.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:697a06bdcedd473b35e50a7e7506b1d8ceb832dc238a336bd6f4f5aa91a4b500"}, + {file = "Pillow-10.0.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9f665d1e6474af9f9da5e86c2a3a2d2d6204e04d5af9c06b9d42afa6ebde3f21"}, + {file = "Pillow-10.0.1-cp38-cp38-manylinux_2_28_aarch64.whl", hash = "sha256:2fa6dd2661838c66f1a5473f3b49ab610c98a128fc08afbe81b91a1f0bf8c51d"}, + {file = "Pillow-10.0.1-cp38-cp38-manylinux_2_28_x86_64.whl", hash = "sha256:3a04359f308ebee571a3127fdb1bd01f88ba6f6fb6d087f8dd2e0d9bff43f2a7"}, + {file = "Pillow-10.0.1-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:723bd25051454cea9990203405fa6b74e043ea76d4968166dfd2569b0210886a"}, + {file = "Pillow-10.0.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:71671503e3015da1b50bd18951e2f9daf5b6ffe36d16f1eb2c45711a301521a7"}, + {file = "Pillow-10.0.1-cp38-cp38-win_amd64.whl", hash = "sha256:44e7e4587392953e5e251190a964675f61e4dae88d1e6edbe9f36d6243547ff3"}, + {file = "Pillow-10.0.1-cp39-cp39-macosx_10_10_x86_64.whl", hash = "sha256:3855447d98cced8670aaa63683808df905e956f00348732448b5a6df67ee5849"}, + {file = "Pillow-10.0.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:ed2d9c0704f2dc4fa980b99d565c0c9a543fe5101c25b3d60488b8ba80f0cce1"}, + {file = "Pillow-10.0.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f5bb289bb835f9fe1a1e9300d011eef4d69661bb9b34d5e196e5e82c4cb09b37"}, + {file = "Pillow-10.0.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3a0d3e54ab1df9df51b914b2233cf779a5a10dfd1ce339d0421748232cea9876"}, + {file = "Pillow-10.0.1-cp39-cp39-manylinux_2_28_aarch64.whl", hash = "sha256:2cc6b86ece42a11f16f55fe8903595eff2b25e0358dec635d0a701ac9586588f"}, + {file = "Pillow-10.0.1-cp39-cp39-manylinux_2_28_x86_64.whl", hash = "sha256:ca26ba5767888c84bf5a0c1a32f069e8204ce8c21d00a49c90dabeba00ce0145"}, + {file = "Pillow-10.0.1-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:f0b4b06da13275bc02adfeb82643c4a6385bd08d26f03068c2796f60d125f6f2"}, + {file = "Pillow-10.0.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:bc2e3069569ea9dbe88d6b8ea38f439a6aad8f6e7a6283a38edf61ddefb3a9bf"}, + {file = "Pillow-10.0.1-cp39-cp39-win_amd64.whl", hash = "sha256:8b451d6ead6e3500b6ce5c7916a43d8d8d25ad74b9102a629baccc0808c54971"}, + {file = "Pillow-10.0.1-pp310-pypy310_pp73-macosx_10_10_x86_64.whl", hash = "sha256:32bec7423cdf25c9038fef614a853c9d25c07590e1a870ed471f47fb80b244db"}, + {file = "Pillow-10.0.1-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b7cf63d2c6928b51d35dfdbda6f2c1fddbe51a6bc4a9d4ee6ea0e11670dd981e"}, + {file = "Pillow-10.0.1-pp310-pypy310_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:f6d3d4c905e26354e8f9d82548475c46d8e0889538cb0657aa9c6f0872a37aa4"}, + {file = "Pillow-10.0.1-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:847e8d1017c741c735d3cd1883fa7b03ded4f825a6e5fcb9378fd813edee995f"}, + {file = "Pillow-10.0.1-pp39-pypy39_pp73-macosx_10_10_x86_64.whl", hash = "sha256:7f771e7219ff04b79e231d099c0a28ed83aa82af91fd5fa9fdb28f5b8d5addaf"}, + {file = "Pillow-10.0.1-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:459307cacdd4138edee3875bbe22a2492519e060660eaf378ba3b405d1c66317"}, + {file = "Pillow-10.0.1-pp39-pypy39_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:b059ac2c4c7a97daafa7dc850b43b2d3667def858a4f112d1aa082e5c3d6cf7d"}, + {file = "Pillow-10.0.1-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:d6caf3cd38449ec3cd8a68b375e0c6fe4b6fd04edb6c9766b55ef84a6e8ddf2d"}, + {file = "Pillow-10.0.1.tar.gz", hash = "sha256:d72967b06be9300fed5cfbc8b5bafceec48bf7cdc7dab66b1d2549035287191d"}, +] + +[package.extras] +docs = ["furo", "olefile", "sphinx (>=2.4)", "sphinx-copybutton", "sphinx-inline-tabs", "sphinx-removed-in", "sphinxext-opengraph"] +tests = ["check-manifest", "coverage", "defusedxml", "markdown2", "olefile", "packaging", "pyroma", "pytest", "pytest-cov", "pytest-timeout"] + +[[package]] +name = "platformdirs" +version = "3.10.0" +description = "A small Python package for determining appropriate platform-specific dirs, e.g. a \"user data dir\"." +optional = false +python-versions = ">=3.7" +files = [ + {file = "platformdirs-3.10.0-py3-none-any.whl", hash = "sha256:d7c24979f292f916dc9cbf8648319032f551ea8c49a4c9bf2fb556a02070ec1d"}, + {file = "platformdirs-3.10.0.tar.gz", hash = "sha256:b45696dab2d7cc691a3226759c0d3b00c47c8b6e293d96f6436f733303f77f6d"}, +] + +[package.extras] +docs = ["furo (>=2023.7.26)", "proselint (>=0.13)", "sphinx (>=7.1.1)", "sphinx-autodoc-typehints (>=1.24)"] +test = ["appdirs (==1.4.4)", "covdefaults (>=2.3)", "pytest (>=7.4)", "pytest-cov (>=4.1)", "pytest-mock (>=3.11.1)"] + +[[package]] +name = "pluggy" +version = "1.3.0" +description = "plugin and hook calling mechanisms for python" +optional = false +python-versions = ">=3.8" +files = [ + {file = "pluggy-1.3.0-py3-none-any.whl", hash = "sha256:d89c696a773f8bd377d18e5ecda92b7a3793cbe66c87060a6fb58c7b6e1061f7"}, + {file = "pluggy-1.3.0.tar.gz", hash = "sha256:cf61ae8f126ac6f7c451172cf30e3e43d3ca77615509771b3a984a0730651e12"}, +] + +[package.extras] +dev = ["pre-commit", "tox"] +testing = ["pytest", "pytest-benchmark"] + +[[package]] +name = "pre-commit" +version = "3.4.0" +description = "A framework for managing and maintaining multi-language pre-commit hooks." +optional = false +python-versions = ">=3.8" +files = [ + {file = "pre_commit-3.4.0-py2.py3-none-any.whl", hash = "sha256:96d529a951f8b677f730a7212442027e8ba53f9b04d217c4c67dc56c393ad945"}, + {file = "pre_commit-3.4.0.tar.gz", hash = "sha256:6bbd5129a64cad4c0dfaeeb12cd8f7ea7e15b77028d985341478c8af3c759522"}, +] + +[package.dependencies] +cfgv = ">=2.0.0" +identify = ">=1.0.0" +nodeenv = ">=0.11.1" +pyyaml = ">=5.1" +virtualenv = ">=20.10.0" + +[[package]] +name = "pydantic" +version = "1.10.12" +description = "Data validation and settings management using python type hints" +optional = false +python-versions = ">=3.7" +files = [ + {file = "pydantic-1.10.12-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:a1fcb59f2f355ec350073af41d927bf83a63b50e640f4dbaa01053a28b7a7718"}, + {file = "pydantic-1.10.12-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:b7ccf02d7eb340b216ec33e53a3a629856afe1c6e0ef91d84a4e6f2fb2ca70fe"}, + {file = "pydantic-1.10.12-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8fb2aa3ab3728d950bcc885a2e9eff6c8fc40bc0b7bb434e555c215491bcf48b"}, + {file = "pydantic-1.10.12-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:771735dc43cf8383959dc9b90aa281f0b6092321ca98677c5fb6125a6f56d58d"}, + {file = "pydantic-1.10.12-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:ca48477862372ac3770969b9d75f1bf66131d386dba79506c46d75e6b48c1e09"}, + {file = "pydantic-1.10.12-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:a5e7add47a5b5a40c49b3036d464e3c7802f8ae0d1e66035ea16aa5b7a3923ed"}, + {file = "pydantic-1.10.12-cp310-cp310-win_amd64.whl", hash = "sha256:e4129b528c6baa99a429f97ce733fff478ec955513630e61b49804b6cf9b224a"}, + {file = "pydantic-1.10.12-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:b0d191db0f92dfcb1dec210ca244fdae5cbe918c6050b342d619c09d31eea0cc"}, + {file = "pydantic-1.10.12-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:795e34e6cc065f8f498c89b894a3c6da294a936ee71e644e4bd44de048af1405"}, + {file = "pydantic-1.10.12-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:69328e15cfda2c392da4e713443c7dbffa1505bc9d566e71e55abe14c97ddc62"}, + {file = "pydantic-1.10.12-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:2031de0967c279df0d8a1c72b4ffc411ecd06bac607a212892757db7462fc494"}, + {file = "pydantic-1.10.12-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:ba5b2e6fe6ca2b7e013398bc7d7b170e21cce322d266ffcd57cca313e54fb246"}, + {file = "pydantic-1.10.12-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:2a7bac939fa326db1ab741c9d7f44c565a1d1e80908b3797f7f81a4f86bc8d33"}, + {file = "pydantic-1.10.12-cp311-cp311-win_amd64.whl", hash = "sha256:87afda5539d5140cb8ba9e8b8c8865cb5b1463924d38490d73d3ccfd80896b3f"}, + {file = "pydantic-1.10.12-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:549a8e3d81df0a85226963611950b12d2d334f214436a19537b2efed61b7639a"}, + {file = "pydantic-1.10.12-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:598da88dfa127b666852bef6d0d796573a8cf5009ffd62104094a4fe39599565"}, + {file = "pydantic-1.10.12-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ba5c4a8552bff16c61882db58544116d021d0b31ee7c66958d14cf386a5b5350"}, + {file = "pydantic-1.10.12-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:c79e6a11a07da7374f46970410b41d5e266f7f38f6a17a9c4823db80dadf4303"}, + {file = "pydantic-1.10.12-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:ab26038b8375581dc832a63c948f261ae0aa21f1d34c1293469f135fa92972a5"}, + {file = "pydantic-1.10.12-cp37-cp37m-win_amd64.whl", hash = "sha256:e0a16d274b588767602b7646fa05af2782576a6cf1022f4ba74cbb4db66f6ca8"}, + {file = "pydantic-1.10.12-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:6a9dfa722316f4acf4460afdf5d41d5246a80e249c7ff475c43a3a1e9d75cf62"}, + {file = "pydantic-1.10.12-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:a73f489aebd0c2121ed974054cb2759af8a9f747de120acd2c3394cf84176ccb"}, + {file = "pydantic-1.10.12-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6b30bcb8cbfccfcf02acb8f1a261143fab622831d9c0989707e0e659f77a18e0"}, + {file = "pydantic-1.10.12-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:2fcfb5296d7877af406ba1547dfde9943b1256d8928732267e2653c26938cd9c"}, + {file = "pydantic-1.10.12-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:2f9a6fab5f82ada41d56b0602606a5506aab165ca54e52bc4545028382ef1c5d"}, + {file = "pydantic-1.10.12-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:dea7adcc33d5d105896401a1f37d56b47d443a2b2605ff8a969a0ed5543f7e33"}, + {file = "pydantic-1.10.12-cp38-cp38-win_amd64.whl", hash = "sha256:1eb2085c13bce1612da8537b2d90f549c8cbb05c67e8f22854e201bde5d98a47"}, + {file = "pydantic-1.10.12-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:ef6c96b2baa2100ec91a4b428f80d8f28a3c9e53568219b6c298c1125572ebc6"}, + {file = "pydantic-1.10.12-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:6c076be61cd0177a8433c0adcb03475baf4ee91edf5a4e550161ad57fc90f523"}, + {file = "pydantic-1.10.12-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2d5a58feb9a39f481eda4d5ca220aa8b9d4f21a41274760b9bc66bfd72595b86"}, + {file = "pydantic-1.10.12-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e5f805d2d5d0a41633651a73fa4ecdd0b3d7a49de4ec3fadf062fe16501ddbf1"}, + {file = "pydantic-1.10.12-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:1289c180abd4bd4555bb927c42ee42abc3aee02b0fb2d1223fb7c6e5bef87dbe"}, + {file = "pydantic-1.10.12-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:5d1197e462e0364906cbc19681605cb7c036f2475c899b6f296104ad42b9f5fb"}, + {file = "pydantic-1.10.12-cp39-cp39-win_amd64.whl", hash = "sha256:fdbdd1d630195689f325c9ef1a12900524dceb503b00a987663ff4f58669b93d"}, + {file = "pydantic-1.10.12-py3-none-any.whl", hash = "sha256:b749a43aa51e32839c9d71dc67eb1e4221bb04af1033a32e3923d46f9effa942"}, + {file = "pydantic-1.10.12.tar.gz", hash = "sha256:0fe8a415cea8f340e7a9af9c54fc71a649b43e8ca3cc732986116b3cb135d303"}, +] + +[package.dependencies] +typing-extensions = ">=4.2.0" + +[package.extras] +dotenv = ["python-dotenv (>=0.10.4)"] +email = ["email-validator (>=1.0.3)"] + +[[package]] +name = "pygments" +version = "2.16.1" +description = "Pygments is a syntax highlighting package written in Python." +optional = false +python-versions = ">=3.7" +files = [ + {file = "Pygments-2.16.1-py3-none-any.whl", hash = "sha256:13fc09fa63bc8d8671a6d247e1eb303c4b343eaee81d861f3404db2935653692"}, + {file = "Pygments-2.16.1.tar.gz", hash = "sha256:1daff0494820c69bc8941e407aa20f577374ee88364ee10a98fdbe0aece96e29"}, +] + +[package.extras] +plugins = ["importlib-metadata"] + +[[package]] +name = "pymc" +version = "5.8.1" +description = "Probabilistic Programming in Python: Bayesian Modeling and Probabilistic Machine Learning with PyTensor" +optional = false +python-versions = ">=3.9" +files = [ + {file = "pymc-5.8.1-py3-none-any.whl", hash = "sha256:d04b4e142103ffca32c593e2147a8f7d88dd3eb3cbc25fac26e8fd72af9f4426"}, + {file = "pymc-5.8.1.tar.gz", hash = "sha256:71ab05caa6aa1ec6b33242e027e04ac200bad8db516f7e7faaaa5d415c6182c1"}, +] + +[package.dependencies] +arviz = ">=0.13.0" +cachetools = ">=4.2.1" +cloudpickle = "*" +fastprogress = ">=0.2.0" +numpy = ">=1.15.0" +pandas = ">=0.24.0" +pytensor = ">=2.16.1,<2.17" +scipy = ">=1.4.1" +typing-extensions = ">=3.7.4" + +[[package]] +name = "pymdown-extensions" +version = "10.3" +description = "Extension pack for Python Markdown." +optional = false +python-versions = ">=3.8" +files = [ + {file = "pymdown_extensions-10.3-py3-none-any.whl", hash = "sha256:77a82c621c58a83efc49a389159181d570e370fff9f810d3a4766a75fc678b66"}, + {file = "pymdown_extensions-10.3.tar.gz", hash = "sha256:94a0d8a03246712b64698af223848fd80aaf1ae4c4be29c8c61939b0467b5722"}, +] + +[package.dependencies] +markdown = ">=3.2" +pyyaml = "*" + +[package.extras] +extra = ["pygments (>=2.12)"] + +[[package]] +name = "pyparsing" +version = "3.1.1" +description = "pyparsing module - Classes and methods to define and execute parsing grammars" +optional = false +python-versions = ">=3.6.8" +files = [ + {file = "pyparsing-3.1.1-py3-none-any.whl", hash = "sha256:32c7c0b711493c72ff18a981d24f28aaf9c1fb7ed5e9667c9e84e3db623bdbfb"}, + {file = "pyparsing-3.1.1.tar.gz", hash = "sha256:ede28a1a32462f5a9705e07aea48001a08f7cf81a021585011deba701581a0db"}, +] + +[package.extras] +diagrams = ["jinja2", "railroad-diagrams"] + +[[package]] +name = "pyright" +version = "1.1.327" +description = "Command line wrapper for pyright" +optional = false +python-versions = ">=3.7" +files = [ + {file = "pyright-1.1.327-py3-none-any.whl", hash = "sha256:3462cda239e9140276238bbdbd0b59d77406f1c2e14d8cb8c20c8e25639c6b3c"}, + {file = "pyright-1.1.327.tar.gz", hash = "sha256:ba74148ad64f22020dbbed6781c4bdb38ecb8a7ca90dc3c87a4f08d1c0e11592"}, +] + +[package.dependencies] +nodeenv = ">=1.6.0" + +[package.extras] +all = ["twine (>=3.4.1)"] +dev = ["twine (>=3.4.1)"] + +[[package]] +name = "pytensor" +version = "2.16.1" +description = "Optimizing compiler for evaluating mathematical expressions on CPUs and GPUs." +optional = false +python-versions = ">=3.9" +files = [ + {file = "pytensor-2.16.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:8d521e9fa556ee085c837098310ca6561772e6012cb310ff66eed1306d6c68d4"}, + {file = "pytensor-2.16.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2b9629ee883cd7881c0e06e17b93da3a672028d4fb0198dbf3cbd959b55e79b8"}, + {file = "pytensor-2.16.1-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:e6b2d78b84e9b22ac1c817409aeb629de49886c01c1f0e9607cf968416c8956a"}, + {file = "pytensor-2.16.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:0fa2956f5219711673b71825f6316cfbb8dcbf4d398b13d9d3e2e73d7578c157"}, + {file = "pytensor-2.16.1-cp310-cp310-win_amd64.whl", hash = "sha256:67f25d3c794a25430efad499f356dbe389dd6700387d245fa293fa474adcbcaf"}, + {file = "pytensor-2.16.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:5ff70f49c66059a894b408d340f9d83c6cc5850bb096a035f1fbf8c609aedfab"}, + {file = "pytensor-2.16.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a8eb0dd68fa383d635e5fd0b89d22d3632c7cf8e90df3ccc37ea459bbf3e6c3f"}, + {file = "pytensor-2.16.1-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:5f89154c13d51e1d3b4f29d027d60f1dbce21e8bcf5a71d6fc9cd6c650b72353"}, + {file = "pytensor-2.16.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:37b787e5f864d8ecf6a285a3cfbf179df422e642f616a208da2f24f574d839a3"}, + {file = "pytensor-2.16.1-cp311-cp311-win_amd64.whl", hash = "sha256:78998d5f4a0501d30e9873232c6209165305ed236a50bee250c3743c258491c1"}, + {file = "pytensor-2.16.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:208b2fa548465f323ef5fd1779e37c7d4a14980c6263650e4a9f4befbe7c002e"}, + {file = "pytensor-2.16.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:096c4cbd4c22c50d01f3e3fe156d8182337011accc634cfec56043f8e93f39db"}, + {file = "pytensor-2.16.1-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:fa29fe4f15785e326d4af91b8882442820f94db8ce4293d97b4a83bd3b0e895a"}, + {file = "pytensor-2.16.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:47eca1d1fc0acd86e5b5867d40be2e361b399f6c4137510b158642cd66c76d75"}, + {file = "pytensor-2.16.1-cp39-cp39-win_amd64.whl", hash = "sha256:3191208f2e9816d205995acd94269f97db5a050f9e16f836fb7e0d8cbe824b67"}, + {file = "pytensor-2.16.1.tar.gz", hash = "sha256:dc586d5aea09e29dcc6927c3b99b13af86ea07736d1555be03e2d11282da821a"}, +] + +[package.dependencies] +cons = "*" +etuples = "*" +filelock = "*" +logical-unification = "*" +miniKanren = "*" +numpy = ">=1.17.0" +scipy = ">=0.14" +setuptools = ">=48.0.0" +typing-extensions = "*" + +[package.extras] +complete = ["pytensor[jax]", "pytensor[numba]"] +development = ["pytensor[complete]", "pytensor[rtd]", "pytensor[tests]"] +jax = ["jax", "jaxlib"] +numba = ["numba (>=0.55)", "numba-scipy (>=0.3.0)"] +rtd = ["pydot", "pydot-ng", "pydot2", "pygments", "sphinx (>=5.1.0,<6)"] +tests = ["coverage (>=5.1)", "pre-commit", "pytest", "pytest-benchmark", "pytest-cov (>=2.6.1)", "pytest-mock"] + +[[package]] +name = "pytest" +version = "7.4.2" +description = "pytest: simple powerful testing with Python" +optional = false +python-versions = ">=3.7" +files = [ + {file = "pytest-7.4.2-py3-none-any.whl", hash = "sha256:1d881c6124e08ff0a1bb75ba3ec0bfd8b5354a01c194ddd5a0a870a48d99b002"}, + {file = "pytest-7.4.2.tar.gz", hash = "sha256:a766259cfab564a2ad52cb1aae1b881a75c3eb7e34ca3779697c23ed47c47069"}, +] + +[package.dependencies] +colorama = {version = "*", markers = "sys_platform == \"win32\""} +exceptiongroup = {version = ">=1.0.0rc8", markers = "python_version < \"3.11\""} +iniconfig = "*" +packaging = "*" +pluggy = ">=0.12,<2.0" +tomli = {version = ">=1.0.0", markers = "python_version < \"3.11\""} + +[package.extras] +testing = ["argcomplete", "attrs (>=19.2.0)", "hypothesis (>=3.56)", "mock", "nose", "pygments (>=2.7.2)", "requests", "setuptools", "xmlschema"] + +[[package]] +name = "pytest-cov" +version = "4.1.0" +description = "Pytest plugin for measuring coverage." +optional = false +python-versions = ">=3.7" +files = [ + {file = "pytest-cov-4.1.0.tar.gz", hash = "sha256:3904b13dfbfec47f003b8e77fd5b589cd11904a21ddf1ab38a64f204d6a10ef6"}, + {file = "pytest_cov-4.1.0-py3-none-any.whl", hash = "sha256:6ba70b9e97e69fcc3fb45bfeab2d0a138fb65c4d0d6a41ef33983ad114be8c3a"}, +] + +[package.dependencies] +coverage = {version = ">=5.2.1", extras = ["toml"]} +pytest = ">=4.6" + +[package.extras] +testing = ["fields", "hunter", "process-tests", "pytest-xdist", "six", "virtualenv"] + +[[package]] +name = "pytest-xdist" +version = "3.3.1" +description = "pytest xdist plugin for distributed testing, most importantly across multiple CPUs" +optional = false +python-versions = ">=3.7" +files = [ + {file = "pytest-xdist-3.3.1.tar.gz", hash = "sha256:d5ee0520eb1b7bcca50a60a518ab7a7707992812c578198f8b44fdfac78e8c93"}, + {file = "pytest_xdist-3.3.1-py3-none-any.whl", hash = "sha256:ff9daa7793569e6a68544850fd3927cd257cc03a7ef76c95e86915355e82b5f2"}, +] + +[package.dependencies] +execnet = ">=1.1" +pytest = ">=6.2.0" + +[package.extras] +psutil = ["psutil (>=3.0)"] +setproctitle = ["setproctitle"] +testing = ["filelock"] + +[[package]] +name = "python-dateutil" +version = "2.8.2" +description = "Extensions to the standard Python datetime module" +optional = false +python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,>=2.7" +files = [ + {file = "python-dateutil-2.8.2.tar.gz", hash = "sha256:0123cacc1627ae19ddf3c27a5de5bd67ee4586fbdd6440d9748f8abb483d3e86"}, + {file = "python_dateutil-2.8.2-py2.py3-none-any.whl", hash = "sha256:961d03dc3453ebbc59dbdea9e4e11c5651520a876d0f4db161e8674aae935da9"}, +] + +[package.dependencies] +six = ">=1.5" + +[[package]] +name = "pytz" +version = "2023.3.post1" +description = "World timezone definitions, modern and historical" +optional = false +python-versions = "*" +files = [ + {file = "pytz-2023.3.post1-py2.py3-none-any.whl", hash = "sha256:ce42d816b81b68506614c11e8937d3aa9e41007ceb50bfdcb0749b921bf646c7"}, + {file = "pytz-2023.3.post1.tar.gz", hash = "sha256:7b4fddbeb94a1eba4b557da24f19fdf9db575192544270a9101d8509f9f43d7b"}, +] + +[[package]] +name = "pyyaml" +version = "6.0.1" +description = "YAML parser and emitter for Python" +optional = false +python-versions = ">=3.6" +files = [ + {file = "PyYAML-6.0.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:d858aa552c999bc8a8d57426ed01e40bef403cd8ccdd0fc5f6f04a00414cac2a"}, + {file = "PyYAML-6.0.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:fd66fc5d0da6d9815ba2cebeb4205f95818ff4b79c3ebe268e75d961704af52f"}, + {file = "PyYAML-6.0.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:69b023b2b4daa7548bcfbd4aa3da05b3a74b772db9e23b982788168117739938"}, + {file = "PyYAML-6.0.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:81e0b275a9ecc9c0c0c07b4b90ba548307583c125f54d5b6946cfee6360c733d"}, + {file = "PyYAML-6.0.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ba336e390cd8e4d1739f42dfe9bb83a3cc2e80f567d8805e11b46f4a943f5515"}, + {file = "PyYAML-6.0.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:326c013efe8048858a6d312ddd31d56e468118ad4cdeda36c719bf5bb6192290"}, + {file = "PyYAML-6.0.1-cp310-cp310-win32.whl", hash = "sha256:bd4af7373a854424dabd882decdc5579653d7868b8fb26dc7d0e99f823aa5924"}, + {file = "PyYAML-6.0.1-cp310-cp310-win_amd64.whl", hash = "sha256:fd1592b3fdf65fff2ad0004b5e363300ef59ced41c2e6b3a99d4089fa8c5435d"}, + {file = "PyYAML-6.0.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:6965a7bc3cf88e5a1c3bd2e0b5c22f8d677dc88a455344035f03399034eb3007"}, + {file = "PyYAML-6.0.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:f003ed9ad21d6a4713f0a9b5a7a0a79e08dd0f221aff4525a2be4c346ee60aab"}, + {file = "PyYAML-6.0.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:42f8152b8dbc4fe7d96729ec2b99c7097d656dc1213a3229ca5383f973a5ed6d"}, + {file = "PyYAML-6.0.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:062582fca9fabdd2c8b54a3ef1c978d786e0f6b3a1510e0ac93ef59e0ddae2bc"}, + {file = "PyYAML-6.0.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d2b04aac4d386b172d5b9692e2d2da8de7bfb6c387fa4f801fbf6fb2e6ba4673"}, + {file = "PyYAML-6.0.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:e7d73685e87afe9f3b36c799222440d6cf362062f78be1013661b00c5c6f678b"}, + {file = "PyYAML-6.0.1-cp311-cp311-win32.whl", hash = "sha256:1635fd110e8d85d55237ab316b5b011de701ea0f29d07611174a1b42f1444741"}, + {file = "PyYAML-6.0.1-cp311-cp311-win_amd64.whl", hash = "sha256:bf07ee2fef7014951eeb99f56f39c9bb4af143d8aa3c21b1677805985307da34"}, + {file = "PyYAML-6.0.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:855fb52b0dc35af121542a76b9a84f8d1cd886ea97c84703eaa6d88e37a2ad28"}, + {file = "PyYAML-6.0.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:40df9b996c2b73138957fe23a16a4f0ba614f4c0efce1e9406a184b6d07fa3a9"}, + {file = "PyYAML-6.0.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6c22bec3fbe2524cde73d7ada88f6566758a8f7227bfbf93a408a9d86bcc12a0"}, + {file = "PyYAML-6.0.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:8d4e9c88387b0f5c7d5f281e55304de64cf7f9c0021a3525bd3b1c542da3b0e4"}, + {file = "PyYAML-6.0.1-cp312-cp312-win32.whl", hash = "sha256:d483d2cdf104e7c9fa60c544d92981f12ad66a457afae824d146093b8c294c54"}, + {file = "PyYAML-6.0.1-cp312-cp312-win_amd64.whl", hash = "sha256:0d3304d8c0adc42be59c5f8a4d9e3d7379e6955ad754aa9d6ab7a398b59dd1df"}, + {file = "PyYAML-6.0.1-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:50550eb667afee136e9a77d6dc71ae76a44df8b3e51e41b77f6de2932bfe0f47"}, + {file = "PyYAML-6.0.1-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1fe35611261b29bd1de0070f0b2f47cb6ff71fa6595c077e42bd0c419fa27b98"}, + {file = "PyYAML-6.0.1-cp36-cp36m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:704219a11b772aea0d8ecd7058d0082713c3562b4e271b849ad7dc4a5c90c13c"}, + {file = "PyYAML-6.0.1-cp36-cp36m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:afd7e57eddb1a54f0f1a974bc4391af8bcce0b444685d936840f125cf046d5bd"}, + {file = "PyYAML-6.0.1-cp36-cp36m-win32.whl", hash = "sha256:fca0e3a251908a499833aa292323f32437106001d436eca0e6e7833256674585"}, + {file = "PyYAML-6.0.1-cp36-cp36m-win_amd64.whl", hash = "sha256:f22ac1c3cac4dbc50079e965eba2c1058622631e526bd9afd45fedd49ba781fa"}, + {file = "PyYAML-6.0.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:b1275ad35a5d18c62a7220633c913e1b42d44b46ee12554e5fd39c70a243d6a3"}, + {file = "PyYAML-6.0.1-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:18aeb1bf9a78867dc38b259769503436b7c72f7a1f1f4c93ff9a17de54319b27"}, + {file = "PyYAML-6.0.1-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:596106435fa6ad000c2991a98fa58eeb8656ef2325d7e158344fb33864ed87e3"}, + {file = "PyYAML-6.0.1-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:baa90d3f661d43131ca170712d903e6295d1f7a0f595074f151c0aed377c9b9c"}, + {file = "PyYAML-6.0.1-cp37-cp37m-win32.whl", hash = "sha256:9046c58c4395dff28dd494285c82ba00b546adfc7ef001486fbf0324bc174fba"}, + {file = "PyYAML-6.0.1-cp37-cp37m-win_amd64.whl", hash = "sha256:4fb147e7a67ef577a588a0e2c17b6db51dda102c71de36f8549b6816a96e1867"}, + {file = "PyYAML-6.0.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:1d4c7e777c441b20e32f52bd377e0c409713e8bb1386e1099c2415f26e479595"}, + {file = "PyYAML-6.0.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a0cd17c15d3bb3fa06978b4e8958dcdc6e0174ccea823003a106c7d4d7899ac5"}, + {file = "PyYAML-6.0.1-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:28c119d996beec18c05208a8bd78cbe4007878c6dd15091efb73a30e90539696"}, + {file = "PyYAML-6.0.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7e07cbde391ba96ab58e532ff4803f79c4129397514e1413a7dc761ccd755735"}, + {file = "PyYAML-6.0.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:49a183be227561de579b4a36efbb21b3eab9651dd81b1858589f796549873dd6"}, + {file = "PyYAML-6.0.1-cp38-cp38-win32.whl", hash = "sha256:184c5108a2aca3c5b3d3bf9395d50893a7ab82a38004c8f61c258d4428e80206"}, + {file = "PyYAML-6.0.1-cp38-cp38-win_amd64.whl", hash = "sha256:1e2722cc9fbb45d9b87631ac70924c11d3a401b2d7f410cc0e3bbf249f2dca62"}, + {file = "PyYAML-6.0.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:9eb6caa9a297fc2c2fb8862bc5370d0303ddba53ba97e71f08023b6cd73d16a8"}, + {file = "PyYAML-6.0.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:c8098ddcc2a85b61647b2590f825f3db38891662cfc2fc776415143f599bb859"}, + {file = "PyYAML-6.0.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5773183b6446b2c99bb77e77595dd486303b4faab2b086e7b17bc6bef28865f6"}, + {file = "PyYAML-6.0.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b786eecbdf8499b9ca1d697215862083bd6d2a99965554781d0d8d1ad31e13a0"}, + {file = "PyYAML-6.0.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bc1bf2925a1ecd43da378f4db9e4f799775d6367bdb94671027b73b393a7c42c"}, + {file = "PyYAML-6.0.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:04ac92ad1925b2cff1db0cfebffb6ffc43457495c9b3c39d3fcae417d7125dc5"}, + {file = "PyYAML-6.0.1-cp39-cp39-win32.whl", hash = "sha256:faca3bdcf85b2fc05d06ff3fbc1f83e1391b3e724afa3feba7d13eeab355484c"}, + {file = "PyYAML-6.0.1-cp39-cp39-win_amd64.whl", hash = "sha256:510c9deebc5c0225e8c96813043e62b680ba2f9c50a08d3724c7f28a747d1486"}, + {file = "PyYAML-6.0.1.tar.gz", hash = "sha256:bfdf460b1736c775f2ba9f6a92bca30bc2095067b8a9d77876d1fad6cc3b4a43"}, +] + +[[package]] +name = "pyyaml-env-tag" +version = "0.1" +description = "A custom YAML tag for referencing environment variables in YAML files. " +optional = false +python-versions = ">=3.6" +files = [ + {file = "pyyaml_env_tag-0.1-py3-none-any.whl", hash = "sha256:af31106dec8a4d68c60207c1886031cbf839b68aa7abccdb19868200532c2069"}, + {file = "pyyaml_env_tag-0.1.tar.gz", hash = "sha256:70092675bda14fdec33b31ba77e7543de9ddc88f2e5b99160396572d11525bdb"}, +] + +[package.dependencies] +pyyaml = "*" + +[[package]] +name = "regex" +version = "2022.10.31" +description = "Alternative regular expression module, to replace re." +optional = false +python-versions = ">=3.6" +files = [ + {file = "regex-2022.10.31-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:a8ff454ef0bb061e37df03557afda9d785c905dab15584860f982e88be73015f"}, + {file = "regex-2022.10.31-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:1eba476b1b242620c266edf6325b443a2e22b633217a9835a52d8da2b5c051f9"}, + {file = "regex-2022.10.31-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d0e5af9a9effb88535a472e19169e09ce750c3d442fb222254a276d77808620b"}, + {file = "regex-2022.10.31-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d03fe67b2325cb3f09be029fd5da8df9e6974f0cde2c2ac6a79d2634e791dd57"}, + {file = "regex-2022.10.31-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a9d0b68ac1743964755ae2d89772c7e6fb0118acd4d0b7464eaf3921c6b49dd4"}, + {file = "regex-2022.10.31-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8a45b6514861916c429e6059a55cf7db74670eaed2052a648e3e4d04f070e001"}, + {file = "regex-2022.10.31-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:8b0886885f7323beea6f552c28bff62cbe0983b9fbb94126531693ea6c5ebb90"}, + {file = "regex-2022.10.31-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:5aefb84a301327ad115e9d346c8e2760009131d9d4b4c6b213648d02e2abe144"}, + {file = "regex-2022.10.31-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:702d8fc6f25bbf412ee706bd73019da5e44a8400861dfff7ff31eb5b4a1276dc"}, + {file = "regex-2022.10.31-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:a3c1ebd4ed8e76e886507c9eddb1a891673686c813adf889b864a17fafcf6d66"}, + {file = "regex-2022.10.31-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:50921c140561d3db2ab9f5b11c5184846cde686bb5a9dc64cae442926e86f3af"}, + {file = "regex-2022.10.31-cp310-cp310-musllinux_1_1_s390x.whl", hash = "sha256:7db345956ecce0c99b97b042b4ca7326feeec6b75facd8390af73b18e2650ffc"}, + {file = "regex-2022.10.31-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:763b64853b0a8f4f9cfb41a76a4a85a9bcda7fdda5cb057016e7706fde928e66"}, + {file = "regex-2022.10.31-cp310-cp310-win32.whl", hash = "sha256:44136355e2f5e06bf6b23d337a75386371ba742ffa771440b85bed367c1318d1"}, + {file = "regex-2022.10.31-cp310-cp310-win_amd64.whl", hash = "sha256:bfff48c7bd23c6e2aec6454aaf6edc44444b229e94743b34bdcdda2e35126cf5"}, + {file = "regex-2022.10.31-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:4b4b1fe58cd102d75ef0552cf17242705ce0759f9695334a56644ad2d83903fe"}, + {file = "regex-2022.10.31-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:542e3e306d1669b25936b64917285cdffcd4f5c6f0247636fec037187bd93542"}, + {file = "regex-2022.10.31-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c27cc1e4b197092e50ddbf0118c788d9977f3f8f35bfbbd3e76c1846a3443df7"}, + {file = "regex-2022.10.31-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b8e38472739028e5f2c3a4aded0ab7eadc447f0d84f310c7a8bb697ec417229e"}, + {file = "regex-2022.10.31-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:76c598ca73ec73a2f568e2a72ba46c3b6c8690ad9a07092b18e48ceb936e9f0c"}, + {file = "regex-2022.10.31-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c28d3309ebd6d6b2cf82969b5179bed5fefe6142c70f354ece94324fa11bf6a1"}, + {file = "regex-2022.10.31-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:9af69f6746120998cd9c355e9c3c6aec7dff70d47247188feb4f829502be8ab4"}, + {file = "regex-2022.10.31-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:a5f9505efd574d1e5b4a76ac9dd92a12acb2b309551e9aa874c13c11caefbe4f"}, + {file = "regex-2022.10.31-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:5ff525698de226c0ca743bfa71fc6b378cda2ddcf0d22d7c37b1cc925c9650a5"}, + {file = "regex-2022.10.31-cp311-cp311-musllinux_1_1_ppc64le.whl", hash = "sha256:4fe7fda2fe7c8890d454f2cbc91d6c01baf206fbc96d89a80241a02985118c0c"}, + {file = "regex-2022.10.31-cp311-cp311-musllinux_1_1_s390x.whl", hash = "sha256:2cdc55ca07b4e70dda898d2ab7150ecf17c990076d3acd7a5f3b25cb23a69f1c"}, + {file = "regex-2022.10.31-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:44a6c2f6374e0033873e9ed577a54a3602b4f609867794c1a3ebba65e4c93ee7"}, + {file = "regex-2022.10.31-cp311-cp311-win32.whl", hash = "sha256:d8716f82502997b3d0895d1c64c3b834181b1eaca28f3f6336a71777e437c2af"}, + {file = "regex-2022.10.31-cp311-cp311-win_amd64.whl", hash = "sha256:61edbca89aa3f5ef7ecac8c23d975fe7261c12665f1d90a6b1af527bba86ce61"}, + {file = "regex-2022.10.31-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:0a069c8483466806ab94ea9068c34b200b8bfc66b6762f45a831c4baaa9e8cdd"}, + {file = "regex-2022.10.31-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d26166acf62f731f50bdd885b04b38828436d74e8e362bfcb8df221d868b5d9b"}, + {file = "regex-2022.10.31-cp36-cp36m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ac741bf78b9bb432e2d314439275235f41656e189856b11fb4e774d9f7246d81"}, + {file = "regex-2022.10.31-cp36-cp36m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:75f591b2055523fc02a4bbe598aa867df9e953255f0b7f7715d2a36a9c30065c"}, + {file = "regex-2022.10.31-cp36-cp36m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6b30bddd61d2a3261f025ad0f9ee2586988c6a00c780a2fb0a92cea2aa702c54"}, + {file = "regex-2022.10.31-cp36-cp36m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ef4163770525257876f10e8ece1cf25b71468316f61451ded1a6f44273eedeb5"}, + {file = "regex-2022.10.31-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:7b280948d00bd3973c1998f92e22aa3ecb76682e3a4255f33e1020bd32adf443"}, + {file = "regex-2022.10.31-cp36-cp36m-musllinux_1_1_aarch64.whl", hash = "sha256:d0213671691e341f6849bf33cd9fad21f7b1cb88b89e024f33370733fec58742"}, + {file = "regex-2022.10.31-cp36-cp36m-musllinux_1_1_i686.whl", hash = "sha256:22e7ebc231d28393dfdc19b185d97e14a0f178bedd78e85aad660e93b646604e"}, + {file = "regex-2022.10.31-cp36-cp36m-musllinux_1_1_ppc64le.whl", hash = "sha256:8ad241da7fac963d7573cc67a064c57c58766b62a9a20c452ca1f21050868dfa"}, + {file = "regex-2022.10.31-cp36-cp36m-musllinux_1_1_s390x.whl", hash = "sha256:586b36ebda81e6c1a9c5a5d0bfdc236399ba6595e1397842fd4a45648c30f35e"}, + {file = "regex-2022.10.31-cp36-cp36m-musllinux_1_1_x86_64.whl", hash = "sha256:0653d012b3bf45f194e5e6a41df9258811ac8fc395579fa82958a8b76286bea4"}, + {file = "regex-2022.10.31-cp36-cp36m-win32.whl", hash = "sha256:144486e029793a733e43b2e37df16a16df4ceb62102636ff3db6033994711066"}, + {file = "regex-2022.10.31-cp36-cp36m-win_amd64.whl", hash = "sha256:c14b63c9d7bab795d17392c7c1f9aaabbffd4cf4387725a0ac69109fb3b550c6"}, + {file = "regex-2022.10.31-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:4cac3405d8dda8bc6ed499557625585544dd5cbf32072dcc72b5a176cb1271c8"}, + {file = "regex-2022.10.31-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:23cbb932cc53a86ebde0fb72e7e645f9a5eec1a5af7aa9ce333e46286caef783"}, + {file = "regex-2022.10.31-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:74bcab50a13960f2a610cdcd066e25f1fd59e23b69637c92ad470784a51b1347"}, + {file = "regex-2022.10.31-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:78d680ef3e4d405f36f0d6d1ea54e740366f061645930072d39bca16a10d8c93"}, + {file = "regex-2022.10.31-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ce6910b56b700bea7be82c54ddf2e0ed792a577dfaa4a76b9af07d550af435c6"}, + {file = "regex-2022.10.31-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:659175b2144d199560d99a8d13b2228b85e6019b6e09e556209dfb8c37b78a11"}, + {file = "regex-2022.10.31-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:1ddf14031a3882f684b8642cb74eea3af93a2be68893901b2b387c5fd92a03ec"}, + {file = "regex-2022.10.31-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:b683e5fd7f74fb66e89a1ed16076dbab3f8e9f34c18b1979ded614fe10cdc4d9"}, + {file = "regex-2022.10.31-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:2bde29cc44fa81c0a0c8686992c3080b37c488df167a371500b2a43ce9f026d1"}, + {file = "regex-2022.10.31-cp37-cp37m-musllinux_1_1_ppc64le.whl", hash = "sha256:4919899577ba37f505aaebdf6e7dc812d55e8f097331312db7f1aab18767cce8"}, + {file = "regex-2022.10.31-cp37-cp37m-musllinux_1_1_s390x.whl", hash = "sha256:9c94f7cc91ab16b36ba5ce476f1904c91d6c92441f01cd61a8e2729442d6fcf5"}, + {file = "regex-2022.10.31-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:ae1e96785696b543394a4e3f15f3f225d44f3c55dafe3f206493031419fedf95"}, + {file = "regex-2022.10.31-cp37-cp37m-win32.whl", hash = "sha256:c670f4773f2f6f1957ff8a3962c7dd12e4be54d05839b216cb7fd70b5a1df394"}, + {file = "regex-2022.10.31-cp37-cp37m-win_amd64.whl", hash = "sha256:8e0caeff18b96ea90fc0eb6e3bdb2b10ab5b01a95128dfeccb64a7238decf5f0"}, + {file = "regex-2022.10.31-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:131d4be09bea7ce2577f9623e415cab287a3c8e0624f778c1d955ec7c281bd4d"}, + {file = "regex-2022.10.31-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:e613a98ead2005c4ce037c7b061f2409a1a4e45099edb0ef3200ee26ed2a69a8"}, + {file = "regex-2022.10.31-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:052b670fafbe30966bbe5d025e90b2a491f85dfe5b2583a163b5e60a85a321ad"}, + {file = "regex-2022.10.31-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:aa62a07ac93b7cb6b7d0389d8ef57ffc321d78f60c037b19dfa78d6b17c928ee"}, + {file = "regex-2022.10.31-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5352bea8a8f84b89d45ccc503f390a6be77917932b1c98c4cdc3565137acc714"}, + {file = "regex-2022.10.31-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:20f61c9944f0be2dc2b75689ba409938c14876c19d02f7585af4460b6a21403e"}, + {file = "regex-2022.10.31-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:29c04741b9ae13d1e94cf93fca257730b97ce6ea64cfe1eba11cf9ac4e85afb6"}, + {file = "regex-2022.10.31-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:543883e3496c8b6d58bd036c99486c3c8387c2fc01f7a342b760c1ea3158a318"}, + {file = "regex-2022.10.31-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:b7a8b43ee64ca8f4befa2bea4083f7c52c92864d8518244bfa6e88c751fa8fff"}, + {file = "regex-2022.10.31-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:6a9a19bea8495bb419dc5d38c4519567781cd8d571c72efc6aa959473d10221a"}, + {file = "regex-2022.10.31-cp38-cp38-musllinux_1_1_ppc64le.whl", hash = "sha256:6ffd55b5aedc6f25fd8d9f905c9376ca44fcf768673ffb9d160dd6f409bfda73"}, + {file = "regex-2022.10.31-cp38-cp38-musllinux_1_1_s390x.whl", hash = "sha256:4bdd56ee719a8f751cf5a593476a441c4e56c9b64dc1f0f30902858c4ef8771d"}, + {file = "regex-2022.10.31-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:8ca88da1bd78990b536c4a7765f719803eb4f8f9971cc22d6ca965c10a7f2c4c"}, + {file = "regex-2022.10.31-cp38-cp38-win32.whl", hash = "sha256:5a260758454580f11dd8743fa98319bb046037dfab4f7828008909d0aa5292bc"}, + {file = "regex-2022.10.31-cp38-cp38-win_amd64.whl", hash = "sha256:5e6a5567078b3eaed93558842346c9d678e116ab0135e22eb72db8325e90b453"}, + {file = "regex-2022.10.31-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:5217c25229b6a85049416a5c1e6451e9060a1edcf988641e309dbe3ab26d3e49"}, + {file = "regex-2022.10.31-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:4bf41b8b0a80708f7e0384519795e80dcb44d7199a35d52c15cc674d10b3081b"}, + {file = "regex-2022.10.31-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0cf0da36a212978be2c2e2e2d04bdff46f850108fccc1851332bcae51c8907cc"}, + {file = "regex-2022.10.31-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d403d781b0e06d2922435ce3b8d2376579f0c217ae491e273bab8d092727d244"}, + {file = "regex-2022.10.31-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a37d51fa9a00d265cf73f3de3930fa9c41548177ba4f0faf76e61d512c774690"}, + {file = "regex-2022.10.31-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e4f781ffedd17b0b834c8731b75cce2639d5a8afe961c1e58ee7f1f20b3af185"}, + {file = "regex-2022.10.31-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d243b36fbf3d73c25e48014961e83c19c9cc92530516ce3c43050ea6276a2ab7"}, + {file = "regex-2022.10.31-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:370f6e97d02bf2dd20d7468ce4f38e173a124e769762d00beadec3bc2f4b3bc4"}, + {file = "regex-2022.10.31-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:597f899f4ed42a38df7b0e46714880fb4e19a25c2f66e5c908805466721760f5"}, + {file = "regex-2022.10.31-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:7dbdce0c534bbf52274b94768b3498abdf675a691fec5f751b6057b3030f34c1"}, + {file = "regex-2022.10.31-cp39-cp39-musllinux_1_1_ppc64le.whl", hash = "sha256:22960019a842777a9fa5134c2364efaed5fbf9610ddc5c904bd3a400973b0eb8"}, + {file = "regex-2022.10.31-cp39-cp39-musllinux_1_1_s390x.whl", hash = "sha256:7f5a3ffc731494f1a57bd91c47dc483a1e10048131ffb52d901bfe2beb6102e8"}, + {file = "regex-2022.10.31-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:7ef6b5942e6bfc5706301a18a62300c60db9af7f6368042227ccb7eeb22d0892"}, + {file = "regex-2022.10.31-cp39-cp39-win32.whl", hash = "sha256:395161bbdbd04a8333b9ff9763a05e9ceb4fe210e3c7690f5e68cedd3d65d8e1"}, + {file = "regex-2022.10.31-cp39-cp39-win_amd64.whl", hash = "sha256:957403a978e10fb3ca42572a23e6f7badff39aa1ce2f4ade68ee452dc6807692"}, + {file = "regex-2022.10.31.tar.gz", hash = "sha256:a3a98921da9a1bf8457aeee6a551948a83601689e5ecdd736894ea9bbec77e83"}, +] + +[[package]] +name = "requests" +version = "2.31.0" +description = "Python HTTP for Humans." +optional = false +python-versions = ">=3.7" +files = [ + {file = "requests-2.31.0-py3-none-any.whl", hash = "sha256:58cd2187c01e70e6e26505bca751777aa9f2ee0b7f4300988b709f44e013003f"}, + {file = "requests-2.31.0.tar.gz", hash = "sha256:942c5a758f98d790eaed1a29cb6eefc7ffb0d1cf7af05c3d2791656dbd6ad1e1"}, +] + +[package.dependencies] +certifi = ">=2017.4.17" +charset-normalizer = ">=2,<4" +idna = ">=2.5,<4" +urllib3 = ">=1.21.1,<3" + +[package.extras] +socks = ["PySocks (>=1.5.6,!=1.5.7)"] +use-chardet-on-py3 = ["chardet (>=3.0.2,<6)"] + +[[package]] +name = "ruff" +version = "0.0.253" +description = "An extremely fast Python linter, written in Rust." +optional = false +python-versions = ">=3.7" +files = [ + {file = "ruff-0.0.253-py3-none-macosx_10_7_x86_64.whl", hash = "sha256:69126b80d4da50a394cfe9da947377841cc6c83b0e05cfe9933672ce5c61bfcf"}, + {file = "ruff-0.0.253-py3-none-macosx_10_9_x86_64.macosx_11_0_arm64.macosx_10_9_universal2.whl", hash = "sha256:0f44caf5bbdaeacc3cba4ee3369638e4f6e4e71c9ca773d2f3fc3f65e4bfb434"}, + {file = "ruff-0.0.253-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8144a2fd6533e7a0dbaaf9a3dde44b8414eebf5a86a1fe21e0471d052a3e9c14"}, + {file = "ruff-0.0.253-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:07603b362f0dad56e30e7ef2f37bf480732ff8bcf52fe4fd6c9445eb42259f42"}, + {file = "ruff-0.0.253-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:68f9a50f48510a443ec57bcf51656bbef47e5972290c450398108ac2a53dfd32"}, + {file = "ruff-0.0.253-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:c6ed42010c379d42b81b537957b413cf8531a00d0a6270913e8527d9d73c7e0c"}, + {file = "ruff-0.0.253-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ba4b3921fa9c59855b66e1a5ef140d0d872f15a83282bff5b5e3e8db89a45aa2"}, + {file = "ruff-0.0.253-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:60bda6fd99f9d3919df4362b671a12c83ef83279fc7bc1dc0e1aa689dfd91a71"}, + {file = "ruff-0.0.253-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:19061d9b5809a0505a233580b48b59b847823ab90e266f8ae40cb31d3708bacf"}, + {file = "ruff-0.0.253-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:6ee92a7688f327c664891567aa24e4a8cae8635934df95e0dbe65b0e991fcc6e"}, + {file = "ruff-0.0.253-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:f0ff811ea61684c6e9284afa701b8388818ab5ef8ebd6144c15c9ba64f459f1e"}, + {file = "ruff-0.0.253-py3-none-musllinux_1_2_i686.whl", hash = "sha256:4548734b2671b80ee4c20aa410d7d2a5b32f087f8759d4f5991c74b8cfa51d7b"}, + {file = "ruff-0.0.253-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:e2485f728f04bf3bd6142e55dd2869c769299b73a4bdbe1a795e98332df75561"}, + {file = "ruff-0.0.253-py3-none-win32.whl", hash = "sha256:a66109185382375246d7b0dae2f594801fd8ceb5f8206159c55791aaec9aa4bb"}, + {file = "ruff-0.0.253-py3-none-win_amd64.whl", hash = "sha256:a64e9f97a6b0bfce924e65fa845f669c969d42c30fb61e1e4d87b2c70d835cb9"}, + {file = "ruff-0.0.253-py3-none-win_arm64.whl", hash = "sha256:506987ac3bc212cd74bf1ca032756e67ada93c4add3b7541e3549bbad5e0fc40"}, + {file = "ruff-0.0.253.tar.gz", hash = "sha256:ab746c843a9673d2637bcbcb45da12ed4d44c0c90f0823484d6dcb660118b539"}, +] + +[[package]] +name = "scipy" +version = "1.9.3" +description = "Fundamental algorithms for scientific computing in Python" +optional = false +python-versions = ">=3.8" +files = [ + {file = "scipy-1.9.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:1884b66a54887e21addf9c16fb588720a8309a57b2e258ae1c7986d4444d3bc0"}, + {file = "scipy-1.9.3-cp310-cp310-macosx_12_0_arm64.whl", hash = "sha256:83b89e9586c62e787f5012e8475fbb12185bafb996a03257e9675cd73d3736dd"}, + {file = "scipy-1.9.3-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1a72d885fa44247f92743fc20732ae55564ff2a519e8302fb7e18717c5355a8b"}, + {file = "scipy-1.9.3-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d01e1dd7b15bd2449c8bfc6b7cc67d630700ed655654f0dfcf121600bad205c9"}, + {file = "scipy-1.9.3-cp310-cp310-win_amd64.whl", hash = "sha256:68239b6aa6f9c593da8be1509a05cb7f9efe98b80f43a5861cd24c7557e98523"}, + {file = "scipy-1.9.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:b41bc822679ad1c9a5f023bc93f6d0543129ca0f37c1ce294dd9d386f0a21096"}, + {file = "scipy-1.9.3-cp311-cp311-macosx_12_0_arm64.whl", hash = "sha256:90453d2b93ea82a9f434e4e1cba043e779ff67b92f7a0e85d05d286a3625df3c"}, + {file = "scipy-1.9.3-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:83c06e62a390a9167da60bedd4575a14c1f58ca9dfde59830fc42e5197283dab"}, + {file = "scipy-1.9.3-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:abaf921531b5aeaafced90157db505e10345e45038c39e5d9b6c7922d68085cb"}, + {file = "scipy-1.9.3-cp311-cp311-win_amd64.whl", hash = "sha256:06d2e1b4c491dc7d8eacea139a1b0b295f74e1a1a0f704c375028f8320d16e31"}, + {file = "scipy-1.9.3-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:5a04cd7d0d3eff6ea4719371cbc44df31411862b9646db617c99718ff68d4840"}, + {file = "scipy-1.9.3-cp38-cp38-macosx_12_0_arm64.whl", hash = "sha256:545c83ffb518094d8c9d83cce216c0c32f8c04aaf28b92cc8283eda0685162d5"}, + {file = "scipy-1.9.3-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0d54222d7a3ba6022fdf5773931b5d7c56efe41ede7f7128c7b1637700409108"}, + {file = "scipy-1.9.3-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cff3a5295234037e39500d35316a4c5794739433528310e117b8a9a0c76d20fc"}, + {file = "scipy-1.9.3-cp38-cp38-win_amd64.whl", hash = "sha256:2318bef588acc7a574f5bfdff9c172d0b1bf2c8143d9582e05f878e580a3781e"}, + {file = "scipy-1.9.3-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:d644a64e174c16cb4b2e41dfea6af722053e83d066da7343f333a54dae9bc31c"}, + {file = "scipy-1.9.3-cp39-cp39-macosx_12_0_arm64.whl", hash = "sha256:da8245491d73ed0a994ed9c2e380fd058ce2fa8a18da204681f2fe1f57f98f95"}, + {file = "scipy-1.9.3-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4db5b30849606a95dcf519763dd3ab6fe9bd91df49eba517359e450a7d80ce2e"}, + {file = "scipy-1.9.3-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c68db6b290cbd4049012990d7fe71a2abd9ffbe82c0056ebe0f01df8be5436b0"}, + {file = "scipy-1.9.3-cp39-cp39-win_amd64.whl", hash = "sha256:5b88e6d91ad9d59478fafe92a7c757d00c59e3bdc3331be8ada76a4f8d683f58"}, + {file = "scipy-1.9.3.tar.gz", hash = "sha256:fbc5c05c85c1a02be77b1ff591087c83bc44579c6d2bd9fb798bb64ea5e1a027"}, +] + +[package.dependencies] +numpy = ">=1.18.5,<1.26.0" + +[package.extras] +dev = ["flake8", "mypy", "pycodestyle", "typing_extensions"] +doc = ["matplotlib (>2)", "numpydoc", "pydata-sphinx-theme (==0.9.0)", "sphinx (!=4.1.0)", "sphinx-panels (>=0.5.2)", "sphinx-tabs"] +test = ["asv", "gmpy2", "mpmath", "pytest", "pytest-cov", "pytest-xdist", "scikit-umfpack", "threadpoolctl"] + +[[package]] +name = "seaborn" +version = "0.12.2" +description = "Statistical data visualization" +optional = false +python-versions = ">=3.7" +files = [ + {file = "seaborn-0.12.2-py3-none-any.whl", hash = "sha256:ebf15355a4dba46037dfd65b7350f014ceb1f13c05e814eda2c9f5fd731afc08"}, + {file = "seaborn-0.12.2.tar.gz", hash = "sha256:374645f36509d0dcab895cba5b47daf0586f77bfe3b36c97c607db7da5be0139"}, +] + +[package.dependencies] +matplotlib = ">=3.1,<3.6.1 || >3.6.1" +numpy = ">=1.17,<1.24.0 || >1.24.0" +pandas = ">=0.25" + +[package.extras] +dev = ["flake8", "flit", "mypy", "pandas-stubs", "pre-commit", "pytest", "pytest-cov", "pytest-xdist"] +docs = ["ipykernel", "nbconvert", "numpydoc", "pydata_sphinx_theme (==0.10.0rc2)", "pyyaml", "sphinx-copybutton", "sphinx-design", "sphinx-issues"] +stats = ["scipy (>=1.3)", "statsmodels (>=0.10)"] + +[[package]] +name = "setuptools" +version = "68.2.2" +description = "Easily download, build, install, upgrade, and uninstall Python packages" +optional = false +python-versions = ">=3.8" +files = [ + {file = "setuptools-68.2.2-py3-none-any.whl", hash = "sha256:b454a35605876da60632df1a60f736524eb73cc47bbc9f3f1ef1b644de74fd2a"}, + {file = "setuptools-68.2.2.tar.gz", hash = "sha256:4ac1475276d2f1c48684874089fefcd83bd7162ddaafb81fac866ba0db282a87"}, +] + +[package.extras] +docs = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "pygments-github-lexers (==0.0.5)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-favicon", "sphinx-hoverxref (<2)", "sphinx-inline-tabs", "sphinx-lint", "sphinx-notfound-page (>=1,<2)", "sphinx-reredirects", "sphinxcontrib-towncrier"] +testing = ["build[virtualenv]", "filelock (>=3.4.0)", "flake8-2020", "ini2toml[lite] (>=0.9)", "jaraco.develop (>=7.21)", "jaraco.envs (>=2.2)", "jaraco.path (>=3.2.0)", "pip (>=19.1)", "pytest (>=6)", "pytest-black (>=0.3.7)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=2.2)", "pytest-mypy (>=0.9.1)", "pytest-perf", "pytest-ruff", "pytest-timeout", "pytest-xdist", "tomli-w (>=1.0.0)", "virtualenv (>=13.0.0)", "wheel"] +testing-integration = ["build[virtualenv] (>=1.0.3)", "filelock (>=3.4.0)", "jaraco.envs (>=2.2)", "jaraco.path (>=3.2.0)", "packaging (>=23.1)", "pytest", "pytest-enabler", "pytest-xdist", "tomli", "virtualenv (>=13.0.0)", "wheel"] + +[[package]] +name = "setuptools-scm" +version = "7.1.0" +description = "the blessed package to manage your versions by scm tags" +optional = false +python-versions = ">=3.7" +files = [ + {file = "setuptools_scm-7.1.0-py3-none-any.whl", hash = "sha256:73988b6d848709e2af142aa48c986ea29592bbcfca5375678064708205253d8e"}, + {file = "setuptools_scm-7.1.0.tar.gz", hash = "sha256:6c508345a771aad7d56ebff0e70628bf2b0ec7573762be9960214730de278f27"}, +] + +[package.dependencies] +packaging = ">=20.0" +setuptools = "*" +tomli = {version = ">=1.0.0", markers = "python_version < \"3.11\""} +typing-extensions = "*" + +[package.extras] +test = ["pytest (>=6.2)", "virtualenv (>20)"] +toml = ["setuptools (>=42)"] + +[[package]] +name = "six" +version = "1.16.0" +description = "Python 2 and 3 compatibility utilities" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*" +files = [ + {file = "six-1.16.0-py2.py3-none-any.whl", hash = "sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254"}, + {file = "six-1.16.0.tar.gz", hash = "sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926"}, +] + +[[package]] +name = "tomli" +version = "2.0.1" +description = "A lil' TOML parser" +optional = false +python-versions = ">=3.7" +files = [ + {file = "tomli-2.0.1-py3-none-any.whl", hash = "sha256:939de3e7a6161af0c887ef91b7d41a53e7c5a1ca976325f429cb46ea9bc30ecc"}, + {file = "tomli-2.0.1.tar.gz", hash = "sha256:de526c12914f0c550d15924c62d72abc48d6fe7364aa87328337a31007fe8a4f"}, +] + +[[package]] +name = "toolz" +version = "0.12.0" +description = "List processing tools and functional utilities" +optional = false +python-versions = ">=3.5" +files = [ + {file = "toolz-0.12.0-py3-none-any.whl", hash = "sha256:2059bd4148deb1884bb0eb770a3cde70e7f954cfbbdc2285f1f2de01fd21eb6f"}, + {file = "toolz-0.12.0.tar.gz", hash = "sha256:88c570861c440ee3f2f6037c4654613228ff40c93a6c25e0eba70d17282c6194"}, +] + +[[package]] +name = "typing-extensions" +version = "4.8.0" +description = "Backported and Experimental Type Hints for Python 3.8+" +optional = false +python-versions = ">=3.8" +files = [ + {file = "typing_extensions-4.8.0-py3-none-any.whl", hash = "sha256:8f92fc8806f9a6b641eaa5318da32b44d401efaac0f6678c9bc448ba3605faa0"}, + {file = "typing_extensions-4.8.0.tar.gz", hash = "sha256:df8e4339e9cb77357558cbdbceca33c303714cf861d1eef15e1070055ae8b7ef"}, +] + +[[package]] +name = "tzdata" +version = "2023.3" +description = "Provider of IANA time zone data" +optional = false +python-versions = ">=2" +files = [ + {file = "tzdata-2023.3-py2.py3-none-any.whl", hash = "sha256:7e65763eef3120314099b6939b5546db7adce1e7d6f2e179e3df563c70511eda"}, + {file = "tzdata-2023.3.tar.gz", hash = "sha256:11ef1e08e54acb0d4f95bdb1be05da659673de4acbd21bf9c69e94cc5e907a3a"}, +] + +[[package]] +name = "urllib3" +version = "2.0.4" +description = "HTTP library with thread-safe connection pooling, file post, and more." +optional = false +python-versions = ">=3.7" +files = [ + {file = "urllib3-2.0.4-py3-none-any.whl", hash = "sha256:de7df1803967d2c2a98e4b11bb7d6bd9210474c46e8a0401514e3a42a75ebde4"}, + {file = "urllib3-2.0.4.tar.gz", hash = "sha256:8d22f86aae8ef5e410d4f539fde9ce6b2113a001bb4d189e0aed70642d602b11"}, +] + +[package.extras] +brotli = ["brotli (>=1.0.9)", "brotlicffi (>=0.8.0)"] +secure = ["certifi", "cryptography (>=1.9)", "idna (>=2.0.0)", "pyopenssl (>=17.1.0)", "urllib3-secure-extra"] +socks = ["pysocks (>=1.5.6,!=1.5.7,<2.0)"] +zstd = ["zstandard (>=0.18.0)"] + +[[package]] +name = "virtualenv" +version = "20.24.5" +description = "Virtual Python Environment builder" +optional = false +python-versions = ">=3.7" +files = [ + {file = "virtualenv-20.24.5-py3-none-any.whl", hash = "sha256:b80039f280f4919c77b30f1c23294ae357c4c8701042086e3fc005963e4e537b"}, + {file = "virtualenv-20.24.5.tar.gz", hash = "sha256:e8361967f6da6fbdf1426483bfe9fca8287c242ac0bc30429905721cefbff752"}, +] + +[package.dependencies] +distlib = ">=0.3.7,<1" +filelock = ">=3.12.2,<4" +platformdirs = ">=3.9.1,<4" + +[package.extras] +docs = ["furo (>=2023.7.26)", "proselint (>=0.13)", "sphinx (>=7.1.2)", "sphinx-argparse (>=0.4)", "sphinxcontrib-towncrier (>=0.2.1a0)", "towncrier (>=23.6)"] +test = ["covdefaults (>=2.3)", "coverage (>=7.2.7)", "coverage-enable-subprocess (>=1)", "flaky (>=3.7)", "packaging (>=23.1)", "pytest (>=7.4)", "pytest-env (>=0.8.2)", "pytest-freezer (>=0.4.8)", "pytest-mock (>=3.11.1)", "pytest-randomly (>=3.12)", "pytest-timeout (>=2.1)", "setuptools (>=68)", "time-machine (>=2.10)"] + +[[package]] +name = "watchdog" +version = "3.0.0" +description = "Filesystem events monitoring" +optional = false +python-versions = ">=3.7" +files = [ + {file = "watchdog-3.0.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:336adfc6f5cc4e037d52db31194f7581ff744b67382eb6021c868322e32eef41"}, + {file = "watchdog-3.0.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:a70a8dcde91be523c35b2bf96196edc5730edb347e374c7de7cd20c43ed95397"}, + {file = "watchdog-3.0.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:adfdeab2da79ea2f76f87eb42a3ab1966a5313e5a69a0213a3cc06ef692b0e96"}, + {file = "watchdog-3.0.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:2b57a1e730af3156d13b7fdddfc23dea6487fceca29fc75c5a868beed29177ae"}, + {file = "watchdog-3.0.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:7ade88d0d778b1b222adebcc0927428f883db07017618a5e684fd03b83342bd9"}, + {file = "watchdog-3.0.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:7e447d172af52ad204d19982739aa2346245cc5ba6f579d16dac4bfec226d2e7"}, + {file = "watchdog-3.0.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:9fac43a7466eb73e64a9940ac9ed6369baa39b3bf221ae23493a9ec4d0022674"}, + {file = "watchdog-3.0.0-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:8ae9cda41fa114e28faf86cb137d751a17ffd0316d1c34ccf2235e8a84365c7f"}, + {file = "watchdog-3.0.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:25f70b4aa53bd743729c7475d7ec41093a580528b100e9a8c5b5efe8899592fc"}, + {file = "watchdog-3.0.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:4f94069eb16657d2c6faada4624c39464f65c05606af50bb7902e036e3219be3"}, + {file = "watchdog-3.0.0-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:7c5f84b5194c24dd573fa6472685b2a27cc5a17fe5f7b6fd40345378ca6812e3"}, + {file = "watchdog-3.0.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:3aa7f6a12e831ddfe78cdd4f8996af9cf334fd6346531b16cec61c3b3c0d8da0"}, + {file = "watchdog-3.0.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:233b5817932685d39a7896b1090353fc8efc1ef99c9c054e46c8002561252fb8"}, + {file = "watchdog-3.0.0-pp37-pypy37_pp73-macosx_10_9_x86_64.whl", hash = "sha256:13bbbb462ee42ec3c5723e1205be8ced776f05b100e4737518c67c8325cf6100"}, + {file = "watchdog-3.0.0-pp38-pypy38_pp73-macosx_10_9_x86_64.whl", hash = "sha256:8f3ceecd20d71067c7fd4c9e832d4e22584318983cabc013dbf3f70ea95de346"}, + {file = "watchdog-3.0.0-pp39-pypy39_pp73-macosx_10_9_x86_64.whl", hash = "sha256:c9d8c8ec7efb887333cf71e328e39cffbf771d8f8f95d308ea4125bf5f90ba64"}, + {file = "watchdog-3.0.0-py3-none-manylinux2014_aarch64.whl", hash = "sha256:0e06ab8858a76e1219e68c7573dfeba9dd1c0219476c5a44d5333b01d7e1743a"}, + {file = "watchdog-3.0.0-py3-none-manylinux2014_armv7l.whl", hash = "sha256:d00e6be486affb5781468457b21a6cbe848c33ef43f9ea4a73b4882e5f188a44"}, + {file = "watchdog-3.0.0-py3-none-manylinux2014_i686.whl", hash = "sha256:c07253088265c363d1ddf4b3cdb808d59a0468ecd017770ed716991620b8f77a"}, + {file = "watchdog-3.0.0-py3-none-manylinux2014_ppc64.whl", hash = "sha256:5113334cf8cf0ac8cd45e1f8309a603291b614191c9add34d33075727a967709"}, + {file = "watchdog-3.0.0-py3-none-manylinux2014_ppc64le.whl", hash = "sha256:51f90f73b4697bac9c9a78394c3acbbd331ccd3655c11be1a15ae6fe289a8c83"}, + {file = "watchdog-3.0.0-py3-none-manylinux2014_s390x.whl", hash = "sha256:ba07e92756c97e3aca0912b5cbc4e5ad802f4557212788e72a72a47ff376950d"}, + {file = "watchdog-3.0.0-py3-none-manylinux2014_x86_64.whl", hash = "sha256:d429c2430c93b7903914e4db9a966c7f2b068dd2ebdd2fa9b9ce094c7d459f33"}, + {file = "watchdog-3.0.0-py3-none-win32.whl", hash = "sha256:3ed7c71a9dccfe838c2f0b6314ed0d9b22e77d268c67e015450a29036a81f60f"}, + {file = "watchdog-3.0.0-py3-none-win_amd64.whl", hash = "sha256:4c9956d27be0bb08fc5f30d9d0179a855436e655f046d288e2bcc11adfae893c"}, + {file = "watchdog-3.0.0-py3-none-win_ia64.whl", hash = "sha256:5d9f3a10e02d7371cd929b5d8f11e87d4bad890212ed3901f9b4d68767bee759"}, + {file = "watchdog-3.0.0.tar.gz", hash = "sha256:4d98a320595da7a7c5a18fc48cb633c2e73cda78f93cac2ef42d42bf609a33f9"}, +] + +[package.extras] +watchmedo = ["PyYAML (>=3.10)"] + +[[package]] +name = "xarray" +version = "2023.8.0" +description = "N-D labeled arrays and datasets in Python" +optional = false +python-versions = ">=3.9" +files = [ + {file = "xarray-2023.8.0-py3-none-any.whl", hash = "sha256:eb42b56aea2c7d5db2a7d0c33fb005b78eb5c4421eb747f2ced138c70b5c204e"}, + {file = "xarray-2023.8.0.tar.gz", hash = "sha256:825c6d64202a731a4e49321edd1e9dfabf4be06802f1b8c8a3c00a3ebfc8cedf"}, +] + +[package.dependencies] +numpy = ">=1.21" +packaging = ">=21.3" +pandas = ">=1.4" + +[package.extras] +accel = ["bottleneck", "flox", "numbagg", "scipy"] +complete = ["bottleneck", "cftime", "dask[complete]", "flox", "fsspec", "h5netcdf", "matplotlib", "nc-time-axis", "netCDF4", "numbagg", "pooch", "pydap", "scipy", "seaborn", "zarr"] +docs = ["bottleneck", "cftime", "dask[complete]", "flox", "fsspec", "h5netcdf", "ipykernel", "ipython", "jupyter-client", "matplotlib", "nbsphinx", "nc-time-axis", "netCDF4", "numbagg", "pooch", "pydap", "scanpydoc", "scipy", "seaborn", "sphinx-autosummary-accessors", "sphinx-rtd-theme", "zarr"] +io = ["cftime", "fsspec", "h5netcdf", "netCDF4", "pooch", "pydap", "scipy", "zarr"] +parallel = ["dask[complete]"] +viz = ["matplotlib", "nc-time-axis", "seaborn"] + +[[package]] +name = "xarray-einstats" +version = "0.6.0" +description = "Stats, linear algebra and einops for xarray" +optional = false +python-versions = ">=3.9" +files = [ + {file = "xarray_einstats-0.6.0-py3-none-any.whl", hash = "sha256:4c6f556a9d8603245545cb88583c04398b10a70c572936a2f48678330545883a"}, + {file = "xarray_einstats-0.6.0.tar.gz", hash = "sha256:ace90601505cfbe2d374762e674557ed14e1725b024823372f7ef9fd237effad"}, +] + +[package.dependencies] +numpy = ">=1.21" +scipy = ">=1.7" +xarray = ">=2022.09.0" + +[package.extras] +doc = ["furo", "jupyter-sphinx", "matplotlib", "myst-nb", "myst-parser[linkify]", "numpydoc", "sphinx (>=4)", "sphinx-copybutton", "sphinx-design", "sphinx-togglebutton", "watermark"] +einops = ["einops"] +numba = ["numba (>=0.55)"] +test = ["hypothesis", "packaging", "pytest", "pytest-cov"] + +[metadata] +lock-version = "2.0" +python-versions = "^3.10" +content-hash = "1ad690eaead0f61cf7b27e373629283d6dcb895564dae20f854f51ec517f450f" diff --git a/src/pmhn/_trees/plot_all_mut_freq.py b/src/pmhn/_trees/plot_all_mut_freq.py deleted file mode 100644 index 525d2e2..0000000 --- a/src/pmhn/_trees/plot_all_mut_freq.py +++ /dev/null @@ -1,33 +0,0 @@ -import pandas as pd -import matplotlib.pyplot as plt - -paths = { - 500: ('/home/laukeller/BSc Thesis/TreeMHN/Example/trees_500.csv', '/home/laukeller/BSc Thesis/pMHN/src/pmhn/_trees/trees_500.csv'), - 2000: ('/home/laukeller/BSc Thesis/TreeMHN/Example/trees_2000.csv', '/home/laukeller/BSc Thesis/pMHN/src/pmhn/_trees/trees_2000.csv'), - 5000: ('/home/laukeller/BSc Thesis/TreeMHN/Example/trees_5000.csv', '/home/laukeller/BSc Thesis/pMHN/src/pmhn/_trees/trees_5000.csv'), - 10000: ('/home/laukeller/BSc Thesis/TreeMHN/Example/trees_10000.csv', '/home/laukeller/BSc Thesis/pMHN/src/pmhn/_trees/trees_10000.csv'), -} - -fig, axs = plt.subplots(2, 2, figsize=(10, 10)) # 2x2 grid of subplots -axs = axs.ravel() - -for i, (num_trees, (r_path, python_path)) in enumerate(paths.items()): - r_trees = pd.read_csv(r_path) - python_trees = pd.read_csv(python_path) - - r_mutation_frequencies = r_trees['Mutation_ID'].value_counts().sort_index() - python_mutation_frequencies = python_trees['Mutation_ID'].value_counts().sort_index() - - bar_width = 0.35 - - r_mutation_frequencies.plot(kind='bar', width=bar_width, position=0, align='center', color='b', alpha=0.5, label='R Trees', ax=axs[i]) - python_mutation_frequencies.plot(kind='bar', width=bar_width, position=1, align='center', color='r', alpha=0.5, label='Python Trees', ax=axs[i]) - - axs[i].set_xlabel('Mutation ID') - axs[i].set_ylabel('Frequency') - axs[i].legend(loc='upper right') - axs[i].set_title(f'Mutation Frequency Distribution ({num_trees} trees)') - -plt.tight_layout() -plt.show() - diff --git a/src/pmhn/_trees/plot_all_trees.py b/src/pmhn/_trees/plot_all_trees.py deleted file mode 100644 index 119762e..0000000 --- a/src/pmhn/_trees/plot_all_trees.py +++ /dev/null @@ -1,32 +0,0 @@ -import pandas as pd -import matplotlib.pyplot as plt - -paths = { - 500: ('/home/laukeller/BSc Thesis/TreeMHN/Example/trees_500.csv', '/home/laukeller/BSc Thesis/pMHN/src/pmhn/_trees/trees_500.csv'), - 2000: ('/home/laukeller/BSc Thesis/TreeMHN/Example/trees_2000.csv', '/home/laukeller/BSc Thesis/pMHN/src/pmhn/_trees/trees_2000.csv'), - 5000: ('/home/laukeller/BSc Thesis/TreeMHN/Example/trees_5000.csv', '/home/laukeller/BSc Thesis/pMHN/src/pmhn/_trees/trees_5000.csv'), - 10000: ('/home/laukeller/BSc Thesis/TreeMHN/Example/trees_10000.csv', '/home/laukeller/BSc Thesis/pMHN/src/pmhn/_trees/trees_10000.csv'), -} - -fig, axs = plt.subplots(2, 2, figsize=(10, 10)) # 2x2 grid of subplots -axs = axs.ravel() - -bin_edges = [x - 0.5 for x in range(2, 12)] + [11.5] # [1.5, 2.5, ..., 10.5, 11.5] - -for i, (num_trees, (r_path, python_path)) in enumerate(paths.items()): - r_trees = pd.read_csv(r_path) - python_trees = pd.read_csv(python_path) - - r_tree_sizes = r_trees.groupby('Tree_ID').size() - python_tree_sizes = python_trees.groupby('Tree_ID').size() - - axs[i].hist(r_tree_sizes, bins=bin_edges, alpha=0.5, label='R Trees') - axs[i].hist(python_tree_sizes, bins=bin_edges, alpha=0.5, label='Python Trees') - axs[i].set_xlabel('Tree Size') - axs[i].set_ylabel('Frequency') - axs[i].legend(loc='upper right') - axs[i].set_title(f'Tree Size Distribution ({num_trees} trees)') - -plt.tight_layout() -plt.show() - diff --git a/src/pmhn/_trees/write_csv.py b/src/pmhn/_trees/write_csv.py deleted file mode 100644 index a5b888d..0000000 --- a/src/pmhn/_trees/write_csv.py +++ /dev/null @@ -1,47 +0,0 @@ -import csv -import numpy as np -from anytree import Node -import _simulate - -def csv_to_numpy(file_path): - with open(file_path, 'r') as file: - reader = csv.reader(file) - next(reader) - data_list = list(reader) - return np.array(data_list, dtype=float) - -def write_trees_to_csv(trees, output_file_path): - with open(output_file_path, 'w', newline='') as file: - writer = csv.writer(file) - writer.writerow(["Patient_ID", "Tree_ID", "Node_ID", "Mutation_ID", "Parent_ID"]) - - patient_id = 0 - for tree_dict in trees: - patient_id += 1 - tree_id = patient_id - node_id = 0 - for node, _ in tree_dict.items(): - node_id += 1 - mutation_id = node.name - parent_id = node.parent.name if node.parent else node_id - writer.writerow([patient_id, tree_id, node_id, mutation_id, parent_id]) - -if __name__ == "__main__": - mhn_file_path = "/home/laukeller/BSc Thesis/TreeMHN/Example/MHN_Matrix.csv" - mhn_array = csv_to_numpy(mhn_file_path) - print(mhn_array) - - rng = np.random.default_rng() - theta = mhn_array - mean_sampling_time = 1.0 - - tree_counts = [2000, 5000, 10000, 50000] - - min_tree_size = 2 - max_tree_size = 11 - for n_points in tree_counts: - trees_file_path = f"/home/laukeller/BSc Thesis/pMHN/src/pmhn/_trees/trees_{n_points}.csv" - - _, trees = _simulate.simulate_trees(rng, n_points, theta, mean_sampling_time, min_tree_size = min_tree_size, max_tree_size = max_tree_size) - write_trees_to_csv(trees, trees_file_path) - From e6ea6bea3de1c22fd7ab16ccee3d64724bc42fc7 Mon Sep 17 00:00:00 2001 From: Laurenz Keller Date: Thu, 28 Sep 2023 14:12:30 +0200 Subject: [PATCH 09/45] change: draw new sampling time if tree is discarded, mean_sampling_time now passed as an argument --- src/pmhn/_trees/_simulate.py | 23 +++++++++++++---------- 1 file changed, 13 insertions(+), 10 deletions(-) diff --git a/src/pmhn/_trees/_simulate.py b/src/pmhn/_trees/_simulate.py index 6685ce9..54c76d5 100644 --- a/src/pmhn/_trees/_simulate.py +++ b/src/pmhn/_trees/_simulate.py @@ -4,7 +4,7 @@ from pmhn._trees._interfaces import Tree -def generate_valid_tree(rng, theta: np.ndarray, sampling_time: float, min_tree_size: int = None, max_tree_size: int = None) -> Tree: +def generate_valid_tree(rng, theta: np.ndarray, sampling_time: float, mean_sampling_time: float, min_tree_size: int = None, max_tree_size: int = None) -> Tree: """ Generates a single valid tree with known sampling time. @@ -15,6 +15,7 @@ def generate_valid_tree(rng, theta: np.ndarray, sampling_time: float, min_tree_s sampling_time: known sampling time min_tree_size: minimum size of the tree max_tree_size: maximum size of the tree + mean_sampling_time: parameter for rng Returns: A valid mutation tree that meets the size constraints if specified. @@ -22,13 +23,15 @@ def generate_valid_tree(rng, theta: np.ndarray, sampling_time: float, min_tree_s The min_tree_size and max_tree_size parameters consider the entire tree, i.e the root node is included. To disable the size constraints, leave min_tree_size and max_tree_size as None. - + If a tree is discarded, a new sampling time is drawn. """ while True: tree = _simulate_tree(rng, theta, sampling_time, max_tree_size) if (min_tree_size is None or len(tree) >= min_tree_size) and (max_tree_size is None or len(tree) <= max_tree_size): return tree + else: + sampling_time = rng.exponential(scale = mean_sampling_time) def _find_possible_mutations(old_mutations: list[int], n_mutations: int) -> list[int]: """ @@ -39,9 +42,9 @@ def _find_possible_mutations(old_mutations: list[int], n_mutations: int) -> list a list of possible mutations that could appear next for a given node Note: - We assume that mutations are labeled with a number between 1 and n_mutations, - so each element in old_mutations should be in that range (except for the root node = mutation 0). - If this assumption is violated, an exception is raised. + We assume that mutations are labeled with a number between 1 and n_mutations, + so each element in old_mutations should be in that range (except for the root node = mutation 0). + If this assumption is violated, an exception is raised. """ for mutation in old_mutations: @@ -75,7 +78,7 @@ def _simulate_tree( (with the difference that in the paper `Theta_{jl}` is used, which is `Theta_{jl} = exp( theta_{jl} )`. - If the tree is larger than max_tree_size, the function returns. + If the tree is larger than max_tree_size, the function returns immediately. """ # TODO(Pawel): This is part of https://github.com/cbg-ethz/pMHN/issues/14 # Note that the sampling time is known that our `theta` entries @@ -135,8 +138,8 @@ def simulate_trees( mean_sampling_time: the mean sampling time. Can be a float (shared between all data point) or an array of shape (n_points,). - min_tree_size: minimum size of the trees - max_tree_size: maximum size of the trees + min_tree_size: minimum size of the trees + max_tree_size: maximum size of the trees Returns: sampling times, shape (n_points,) @@ -170,8 +173,8 @@ def simulate_trees( sampling_times = rng.exponential(scale=mean_sampling_time, size=n_points) trees = [ - generate_valid_tree(rng, theta=th, sampling_time=t_s, min_tree_size = min_tree_size, max_tree_size = max_tree_size) - for th, t_s in zip(theta, sampling_times) + generate_valid_tree(rng, theta=th, sampling_time=t_s, mean_sampling_time=ms, min_tree_size = min_tree_size, max_tree_size = max_tree_size) + for th, t_s, ms in zip(theta, sampling_times, mean_sampling_time) ] return sampling_times, trees From 3131c242420ea77ce5d03c8559b68c39123c10a2 Mon Sep 17 00:00:00 2001 From: Laurenz Keller Date: Thu, 28 Sep 2023 14:12:53 +0200 Subject: [PATCH 10/45] reformatted files with black --- reformatted, 28 files left unchanged. | 905 ++++++++++++++++++++++++++ src/pmhn/_trees/._simulate.py.swp | Bin 0 -> 20480 bytes tests/trees/test_simulate.py | 183 +++--- warmup/children.py | 5 +- warmup/plot_all_mut_freq.py | 71 +- warmup/plot_all_trees.py | 46 +- warmup/plot_mutation_freq.py | 39 +- warmup/plot_trees.py | 25 +- warmup/subtree.py | 45 +- warmup/write_csv.py | 41 +- 10 files changed, 1185 insertions(+), 175 deletions(-) create mode 100644 reformatted, 28 files left unchanged. create mode 100644 src/pmhn/_trees/._simulate.py.swp diff --git a/reformatted, 28 files left unchanged. b/reformatted, 28 files left unchanged. new file mode 100644 index 0000000..4f6792a --- /dev/null +++ b/reformatted, 28 files left unchanged. @@ -0,0 +1,905 @@ +diff --git a/src/pmhn/_trees/_simulate.py b/src/pmhn/_trees/_simulate.py +index 6685ce9..f9e9cda 100644 +--- a/src/pmhn/_trees/_simulate.py ++++ b/src/pmhn/_trees/_simulate.py +@@ -4,10 +4,17 @@ import numpy as np +  + from pmhn._trees._interfaces import Tree +  +-def generate_valid_tree(rng, theta: np.ndarray, sampling_time: float, min_tree_size: int = None, max_tree_size: int = None) -> Tree: ++ ++def generate_valid_tree( ++ rng, ++ theta: np.ndarray, ++ mean_sampling_time: float, ++ min_tree_size: int = None, ++ max_tree_size: int = None, ++) -> Tree: + """ + Generates a single valid tree with known sampling time. +-  ++ + Args: + rng: random number generator + theta: real-valued (i.e., log-theta) matrix, +@@ -18,44 +25,50 @@ def generate_valid_tree(rng, theta: np.ndarray, sampling_time: float, min_tree_s + Returns: + A valid mutation tree that meets the size constraints if specified. +  +- Note:  ++ Note: + The min_tree_size and max_tree_size parameters consider the entire tree, + i.e the root node is included. + To disable the size constraints, leave min_tree_size and max_tree_size as None. +  + """ +  +- while True:  ++ while True: ++ sampling_time = rng.exponential(scale=mean_sampling_time) + tree = _simulate_tree(rng, theta, sampling_time, max_tree_size) +- if (min_tree_size is None or len(tree) >= min_tree_size) and (max_tree_size is None or len(tree) <= max_tree_size): +- return tree  +-  ++ if (min_tree_size is None or len(tree) >= min_tree_size) and ( ++ max_tree_size is None or len(tree) <= max_tree_size ++ ): ++ return tree ++ ++ + def _find_possible_mutations(old_mutations: list[int], n_mutations: int) -> list[int]: + """ + Args: + old_mutations: list of ancestor mutations of a given node (including the node itself) + n_mutations: total number of mutations + Returns: +- a list of possible mutations that could appear next for a given node  +-  ++ a list of possible mutations that could appear next for a given node ++ + Note: +- We assume that mutations are labeled with a number between 1 and n_mutations, +- so each element in old_mutations should be in that range (except for the root node = mutation 0).  ++ We assume that mutations are labeled with a number between 1 and n_mutations, ++ so each element in old_mutations should be in that range (except for the root node = mutation 0). + If this assumption is violated, an exception is raised. +-  ++ + """ + for mutation in old_mutations: + if mutation > n_mutations or mutation < 0: +- raise ValueError(f"Invalid mutation {mutation} in old_mutations. It should be 0 <= mutation <= {n_mutations}.") ++ raise ValueError( ++ f"Invalid mutation {mutation} in old_mutations. It should be 0 <= mutation <= {n_mutations}." ++ ) ++ ++ possible_mutations = list( ++ set([i + 1 for i in range(n_mutations)]).difference(set(old_mutations)) ++ ) ++ return possible_mutations ++ +  +- possible_mutations=list(set([i+1 for i in range(n_mutations)]).difference(set(old_mutations))) +- return possible_mutations  +-  + def _simulate_tree( +- rng, +- theta: np.ndarray, +- sampling_time: float, +- max_tree_size: int = None ++ rng, theta: np.ndarray, sampling_time: float, max_tree_size: int = None + ) -> Tree: + """Simulates a single tree with known sampling time. +  +@@ -64,7 +77,7 @@ def _simulate_tree( + theta: real-valued (i.e., log-theta) matrix, + shape (n_mutations, n_mutations) + sampling_time: known sampling time +- max_tree_size: maximum size of the tree ++ max_tree_size: maximum size of the tree + Returns: + a mutation tree +  +@@ -74,7 +87,7 @@ def _simulate_tree( + Appendix A1 to the TreeMHN paper + (with the difference that in the paper `Theta_{jl}` + is used, which is `Theta_{jl} = exp( theta_{jl} )`. +-  ++ + If the tree is larger than max_tree_size, the function returns. + """ + # TODO(Pawel): This is part of https://github.com/cbg-ethz/pMHN/issues/14 +@@ -92,17 +105,23 @@ def _simulate_tree( + for node in U_current: + path = list(node.path) + old_mutations = [node.name for node in path] +- possible_mutations = _find_possible_mutations(old_mutations = old_mutations,n_mutations = n_mutations) +- for j in possible_mutations:  +- new_node = Node(j,parent=node) ++ possible_mutations = _find_possible_mutations( ++ old_mutations=old_mutations, n_mutations=n_mutations ++ ) ++ for j in possible_mutations: ++ new_node = Node(j, parent=node) + # Here j lies in the range of 1 to n_mutations inclusive. +- # However, Python uses 0-based indexing for arrays. Therefore, we subtract 1 from j when accessing +- # elements in the log-theta matrix to correctly map the 1-indexed mutation to the 0-indexed matrix position.  ++ # However, Python uses 0-based indexing for arrays. Therefore, we subtract 1 from j when accessing ++ # elements in the log-theta matrix to correctly map the 1-indexed mutation to the 0-indexed matrix position. + l = theta[j - 1][j - 1] +- for anc in [ancestor for ancestor in node.path if ancestor.parent is not None]: ++ for anc in [ ++ ancestor for ancestor in node.path if ancestor.parent is not None ++ ]: + l += theta[j - 1][anc.name - 1] + l = np.exp(l) + waiting_time = node_time_map[node] + rng.exponential(1.0 / l) ++ print("waiting_time shape:", np.shape(waiting_time)) ++ print("sampling_time shape:", np.shape(sampling_time)) + if waiting_time < sampling_time: + node_time_map[new_node] = waiting_time + U_next.append(new_node) +@@ -124,7 +143,7 @@ def simulate_trees( + theta: np.ndarray, + mean_sampling_time: Union[np.ndarray, float, Sequence[float]], + min_tree_size: int = None, +- max_tree_size: int = None ++ max_tree_size: int = None, + ) -> tuple[np.ndarray, list[Tree]]: + """Simulates a data set of trees with known sampling times. +  +@@ -135,9 +154,9 @@ def simulate_trees( + mean_sampling_time: the mean sampling time. + Can be a float (shared between all data point) + or an array of shape (n_points,). +- min_tree_size: minimum size of the trees +- max_tree_size: maximum size of the trees +-  ++ min_tree_size: minimum size of the trees ++ max_tree_size: maximum size of the trees ++ + Returns: + sampling times, shape (n_points,) + sampled trees, list of length `n_points` +@@ -150,16 +169,6 @@ def simulate_trees( + 3, + }, "Theta should have shape (m, m) or (n_points, m, m)." +  +- # Make sure mean_sampling_time is an array of shape (n_points,) +- if isinstance(mean_sampling_time, float): +- mean_sampling_time = np.full(n_points, fill_value=mean_sampling_time) +- else: +- mean_sampling_time = np.asarray(mean_sampling_time) +- +- assert ( +- len(mean_sampling_time) == n_points +- ), "mean_sampling_time should have length n_points." +- + # Make sure theta has shape (n_points, n, n) + if len(theta.shape) == 2: + theta = np.asarray([theta for _ in range(n_points)]) +@@ -167,11 +176,15 @@ def simulate_trees( + assert theta.shape[0] == n_points, "Theta should have shape (n_points, n, n)." + assert theta.shape[1] == theta.shape[2], "Each theta should be square." +  +- sampling_times = rng.exponential(scale=mean_sampling_time, size=n_points) +- + trees = [ +- generate_valid_tree(rng, theta=th, sampling_time=t_s, min_tree_size = min_tree_size, max_tree_size = max_tree_size) +- for th, t_s in zip(theta, sampling_times) ++ generate_valid_tree( ++ rng, ++ theta=th, ++ mean_sampling_time=mean_sampling_time, ++ min_tree_size=min_tree_size, ++ max_tree_size=max_tree_size, ++ ) ++ for th in theta + ] +  +- return sampling_times, trees ++ return trees +diff --git a/tests/trees/test_simulate.py b/tests/trees/test_simulate.py +index 83865b6..4c8f8df 100644 +--- a/tests/trees/test_simulate.py ++++ b/tests/trees/test_simulate.py +@@ -1,34 +1,39 @@ + import numpy as np + import pmhn._trees._simulate as simulate + import pytest ++ ++ + def test_generate_valid_tree(): + """ + We want to test if valid trees are generated. Here we test the size requirements + and the waiting times. + """ + rng = np.random.default_rng() +- theta = np.array([[-1.41, 0.00, 0.00, 4.91, 1.03, 0.00, -1.91, -0.74, -1.35, 1.48], +- [-1.12, -2.26, 0.00, 0.82, 0.00, 0.00, 1.16, 0.00, -1.62, 0.00], +- [0.00, -0.86, -2.55, 1.58, 0.00, 0.00, 1.02, -2.70, 0.00, 0.68], +- [0.00, 0.00, 0.00, -3.69, 0.00, 0.00, -0.95, 1.42, 0.00, -1.01], +- [-3.08, -1.42, -3.14, 0.00, -3.95, 3.90, -1.46, -2.00, 0.00, 2.87], +- [-2.24, 0.00, 0.00, 0.00, 0.00, -2.38, -2.13, 1.50, 0.00, 1.35], +- [0.00, 0.00, 0.00, 0.00, 1.52, 0.00, -1.79, 0.00, 0.00, 0.00], +- [1.69, 0.76, 0.00, 1.29, 1.73, -0.82, -1.38, -4.65, 0.92, 0.00], +- [-1.22, 0.00, 0.00, 0.00, 0.65, -1.14, 0.00, 0.00, -3.25, 0.00], +- [0.97, 1.75, 0.00, -3.66, -1.28, 0.00, 1.66, 0.00, 0.00, -3.03]] +-) ++ theta = np.array( ++ [ ++ [-1.41, 0.00, 0.00, 4.91, 1.03, 0.00, -1.91, -0.74, -1.35, 1.48], ++ [-1.12, -2.26, 0.00, 0.82, 0.00, 0.00, 1.16, 0.00, -1.62, 0.00], ++ [0.00, -0.86, -2.55, 1.58, 0.00, 0.00, 1.02, -2.70, 0.00, 0.68], ++ [0.00, 0.00, 0.00, -3.69, 0.00, 0.00, -0.95, 1.42, 0.00, -1.01], ++ [-3.08, -1.42, -3.14, 0.00, -3.95, 3.90, -1.46, -2.00, 0.00, 2.87], ++ [-2.24, 0.00, 0.00, 0.00, 0.00, -2.38, -2.13, 1.50, 0.00, 1.35], ++ [0.00, 0.00, 0.00, 0.00, 1.52, 0.00, -1.79, 0.00, 0.00, 0.00], ++ [1.69, 0.76, 0.00, 1.29, 1.73, -0.82, -1.38, -4.65, 0.92, 0.00], ++ [-1.22, 0.00, 0.00, 0.00, 0.65, -1.14, 0.00, 0.00, -3.25, 0.00], ++ [0.97, 1.75, 0.00, -3.66, -1.28, 0.00, 1.66, 0.00, 0.00, -3.03], ++ ] ++ ) + sampling_time = 1.0 + min_tree_size = 2 + max_tree_size = 12 +  + for _ in range(10000): +- tree = simulate.generate_valid_tree(rng, theta, sampling_time, min_tree_size, max_tree_size) +-  +-  ++ tree = simulate.generate_valid_tree( ++ rng, theta, sampling_time, min_tree_size, max_tree_size ++ ) ++ + assert min_tree_size <= len(tree) <= max_tree_size +-  +-  ++ + for node, time in tree.items(): + assert time < sampling_time +  +@@ -39,90 +44,106 @@ def test_generate_tree_no_size_constraints(): + """ +  + rng = np.random.default_rng() +- theta = np.array([[-1.41, 0.00, 0.00, 4.91, 1.03, 0.00, -1.91, -0.74, -1.35, 1.48], +- [-1.12, -2.26, 0.00, 0.82, 0.00, 0.00, 1.16, 0.00, -1.62, 0.00], +- [0.00, -0.86, -2.55, 1.58, 0.00, 0.00, 1.02, -2.70, 0.00, 0.68], +- [0.00, 0.00, 0.00, -3.69, 0.00, 0.00, -0.95, 1.42, 0.00, -1.01], +- [-3.08, -1.42, -3.14, 0.00, -3.95, 3.90, -1.46, -2.00, 0.00, 2.87], +- [-2.24, 0.00, 0.00, 0.00, 0.00, -2.38, -2.13, 1.50, 0.00, 1.35], +- [0.00, 0.00, 0.00, 0.00, 1.52, 0.00, -1.79, 0.00, 0.00, 0.00], +- [1.69, 0.76, 0.00, 1.29, 1.73, -0.82, -1.38, -4.65, 0.92, 0.00], +- [-1.22, 0.00, 0.00, 0.00, 0.65, -1.14, 0.00, 0.00, -3.25, 0.00], +- [0.97, 1.75, 0.00, -3.66, -1.28, 0.00, 1.66, 0.00, 0.00, -3.03]] +-) ++ theta = np.array( ++ [ ++ [-1.41, 0.00, 0.00, 4.91, 1.03, 0.00, -1.91, -0.74, -1.35, 1.48], ++ [-1.12, -2.26, 0.00, 0.82, 0.00, 0.00, 1.16, 0.00, -1.62, 0.00], ++ [0.00, -0.86, -2.55, 1.58, 0.00, 0.00, 1.02, -2.70, 0.00, 0.68], ++ [0.00, 0.00, 0.00, -3.69, 0.00, 0.00, -0.95, 1.42, 0.00, -1.01], ++ [-3.08, -1.42, -3.14, 0.00, -3.95, 3.90, -1.46, -2.00, 0.00, 2.87], ++ [-2.24, 0.00, 0.00, 0.00, 0.00, -2.38, -2.13, 1.50, 0.00, 1.35], ++ [0.00, 0.00, 0.00, 0.00, 1.52, 0.00, -1.79, 0.00, 0.00, 0.00], ++ [1.69, 0.76, 0.00, 1.29, 1.73, -0.82, -1.38, -4.65, 0.92, 0.00], ++ [-1.22, 0.00, 0.00, 0.00, 0.65, -1.14, 0.00, 0.00, -3.25, 0.00], ++ [0.97, 1.75, 0.00, -3.66, -1.28, 0.00, 1.66, 0.00, 0.00, -3.03], ++ ] ++ ) + sampling_time = 1.0 +-  ++ + for _ in range(10000): + tree = simulate.generate_valid_tree(rng, theta, sampling_time) +-  ++ + for node, time in tree.items(): + assert time < sampling_time +  ++ + def test_generate_tree_no_min_size_constraint(): + """ + Here we test the case where min_tree_size is None but max_tree_size is specified. + """ + rng = np.random.default_rng() +- theta = np.array([[-1.41, 0.00, 0.00, 4.91, 1.03, 0.00, -1.91, -0.74, -1.35, 1.48], +- [-1.12, -2.26, 0.00, 0.82, 0.00, 0.00, 1.16, 0.00, -1.62, 0.00], +- [0.00, -0.86, -2.55, 1.58, 0.00, 0.00, 1.02, -2.70, 0.00, 0.68], +- [0.00, 0.00, 0.00, -3.69, 0.00, 0.00, -0.95, 1.42, 0.00, -1.01], +- [-3.08, -1.42, -3.14, 0.00, -3.95, 3.90, -1.46, -2.00, 0.00, 2.87], +- [-2.24, 0.00, 0.00, 0.00, 0.00, -2.38, -2.13, 1.50, 0.00, 1.35], +- [0.00, 0.00, 0.00, 0.00, 1.52, 0.00, -1.79, 0.00, 0.00, 0.00], +- [1.69, 0.76, 0.00, 1.29, 1.73, -0.82, -1.38, -4.65, 0.92, 0.00], +- [-1.22, 0.00, 0.00, 0.00, 0.65, -1.14, 0.00, 0.00, -3.25, 0.00], +- [0.97, 1.75, 0.00, -3.66, -1.28, 0.00, 1.66, 0.00, 0.00, -3.03]] +-) ++ theta = np.array( ++ [ ++ [-1.41, 0.00, 0.00, 4.91, 1.03, 0.00, -1.91, -0.74, -1.35, 1.48], ++ [-1.12, -2.26, 0.00, 0.82, 0.00, 0.00, 1.16, 0.00, -1.62, 0.00], ++ [0.00, -0.86, -2.55, 1.58, 0.00, 0.00, 1.02, -2.70, 0.00, 0.68], ++ [0.00, 0.00, 0.00, -3.69, 0.00, 0.00, -0.95, 1.42, 0.00, -1.01], ++ [-3.08, -1.42, -3.14, 0.00, -3.95, 3.90, -1.46, -2.00, 0.00, 2.87], ++ [-2.24, 0.00, 0.00, 0.00, 0.00, -2.38, -2.13, 1.50, 0.00, 1.35], ++ [0.00, 0.00, 0.00, 0.00, 1.52, 0.00, -1.79, 0.00, 0.00, 0.00], ++ [1.69, 0.76, 0.00, 1.29, 1.73, -0.82, -1.38, -4.65, 0.92, 0.00], ++ [-1.22, 0.00, 0.00, 0.00, 0.65, -1.14, 0.00, 0.00, -3.25, 0.00], ++ [0.97, 1.75, 0.00, -3.66, -1.28, 0.00, 1.66, 0.00, 0.00, -3.03], ++ ] ++ ) + sampling_time = 1.0 + max_tree_size = 8 +-  ++ + for _ in range(10000): +- tree = simulate.generate_valid_tree(rng, theta, sampling_time, max_tree_size=max_tree_size) ++ tree = simulate.generate_valid_tree( ++ rng, theta, sampling_time, max_tree_size=max_tree_size ++ ) + assert len(tree) <= max_tree_size +-  ++ + for node, time in tree.items(): + assert time < sampling_time +  ++ + def test_generate_tree_no_max_size_constraint(): + """ + Here we test the case where max_tree_size is None but min_tree_size is specified. + """ + rng = np.random.default_rng() +- theta = np.array([[-1.41, 0.00, 0.00, 4.91, 1.03, 0.00, -1.91, -0.74, -1.35, 1.48], +- [-1.12, -2.26, 0.00, 0.82, 0.00, 0.00, 1.16, 0.00, -1.62, 0.00], +- [0.00, -0.86, -2.55, 1.58, 0.00, 0.00, 1.02, -2.70, 0.00, 0.68], +- [0.00, 0.00, 0.00, -3.69, 0.00, 0.00, -0.95, 1.42, 0.00, -1.01], +- [-3.08, -1.42, -3.14, 0.00, -3.95, 3.90, -1.46, -2.00, 0.00, 2.87], +- [-2.24, 0.00, 0.00, 0.00, 0.00, -2.38, -2.13, 1.50, 0.00, 1.35], +- [0.00, 0.00, 0.00, 0.00, 1.52, 0.00, -1.79, 0.00, 0.00, 0.00], +- [1.69, 0.76, 0.00, 1.29, 1.73, -0.82, -1.38, -4.65, 0.92, 0.00], +- [-1.22, 0.00, 0.00, 0.00, 0.65, -1.14, 0.00, 0.00, -3.25, 0.00], +- [0.97, 1.75, 0.00, -3.66, -1.28, 0.00, 1.66, 0.00, 0.00, -3.03]] +-) ++ theta = np.array( ++ [ ++ [-1.41, 0.00, 0.00, 4.91, 1.03, 0.00, -1.91, -0.74, -1.35, 1.48], ++ [-1.12, -2.26, 0.00, 0.82, 0.00, 0.00, 1.16, 0.00, -1.62, 0.00], ++ [0.00, -0.86, -2.55, 1.58, 0.00, 0.00, 1.02, -2.70, 0.00, 0.68], ++ [0.00, 0.00, 0.00, -3.69, 0.00, 0.00, -0.95, 1.42, 0.00, -1.01], ++ [-3.08, -1.42, -3.14, 0.00, -3.95, 3.90, -1.46, -2.00, 0.00, 2.87], ++ [-2.24, 0.00, 0.00, 0.00, 0.00, -2.38, -2.13, 1.50, 0.00, 1.35], ++ [0.00, 0.00, 0.00, 0.00, 1.52, 0.00, -1.79, 0.00, 0.00, 0.00], ++ [1.69, 0.76, 0.00, 1.29, 1.73, -0.82, -1.38, -4.65, 0.92, 0.00], ++ [-1.22, 0.00, 0.00, 0.00, 0.65, -1.14, 0.00, 0.00, -3.25, 0.00], ++ [0.97, 1.75, 0.00, -3.66, -1.28, 0.00, 1.66, 0.00, 0.00, -3.03], ++ ] ++ ) + sampling_time = 1.0 + min_tree_size = 3 +-  ++ + for _ in range(10000): +- tree = simulate.generate_valid_tree(rng, theta, sampling_time, min_tree_size=min_tree_size) +-  ++ tree = simulate.generate_valid_tree( ++ rng, theta, sampling_time, min_tree_size=min_tree_size ++ ) ++ + assert len(tree) >= min_tree_size +-  ++ + for node, time in tree.items(): + assert time < sampling_time +-  ++ ++ + def test_find_possible_mutations_normal(): + """ +- We want to test if the possible_mutations list is correct.  ++ We want to test if the possible_mutations list is correct. + """ + old_mutations = [7, 2, 5, 9] + n_mutations = 10 + possible_mutations = simulate._find_possible_mutations(old_mutations, n_mutations) +-  ++ + assert possible_mutations == [1, 3, 4, 6, 8, 10] +-  +-def test_find_possible_mutations_edge():  +  ++ ++def test_find_possible_mutations_edge(): + """ + We want to test if the possible_mutations list is correct. old_mutations with mutations on the edge. + """ +@@ -130,31 +151,35 @@ def test_find_possible_mutations_edge(): + old_mutations = [1, 10] + n_mutations = 10 + possible_mutations = simulate._find_possible_mutations(old_mutations, n_mutations) +-  ++ + assert possible_mutations == [i for i in range(2, 10)] +-  +-def test_find_possible_mutations_except_positive():  ++ ++ ++def test_find_possible_mutations_except_positive(): + """ + We want to test if an exception is correctly thrown when the old_mutations list is invalid (mutation number too large). +- """  ++ """ + old_mutations = [212, 1, 3, 7] + n_mutations = 10 + with pytest.raises(ValueError) as excinfo: +- simulate._find_possible_mutations(old_mutations, n_mutations) +-  +- assert str(excinfo.value) == "Invalid mutation 212 in old_mutations. It should be 0 <= mutation <= 10." +-  +-def test_find_possible_mutations_except_negative():  ++ simulate._find_possible_mutations(old_mutations, n_mutations) ++ ++ assert ( ++ str(excinfo.value) ++ == "Invalid mutation 212 in old_mutations. It should be 0 <= mutation <= 10." ++ ) ++ ++ ++def test_find_possible_mutations_except_negative(): + """ + We want to test if an exception is correctly thrown when the old_mutations list is invalid (mutation number too small). +- """  ++ """ + old_mutations = [-23, 0, 5] + n_mutations = 10 + with pytest.raises(ValueError) as excinfo: +- simulate._find_possible_mutations(old_mutations, n_mutations) +-  +- assert str(excinfo.value) == "Invalid mutation -23 in old_mutations. It should be 0 <= mutation <= 10." +-  +-  +-  ++ simulate._find_possible_mutations(old_mutations, n_mutations) +  ++ assert ( ++ str(excinfo.value) ++ == "Invalid mutation -23 in old_mutations. It should be 0 <= mutation <= 10." ++ ) +diff --git a/warmup/children.py b/warmup/children.py +index 6ec7914..d22c176 100644 +--- a/warmup/children.py ++++ b/warmup/children.py +@@ -9,8 +9,7 @@ F = Node("F", parent=C) +  + all_nodes = list(PreOrderIter(A)) + print(RenderTree(A)) +-children={} ++children = {} + for node in all_nodes: + children[node.name] = [child.name for child in node.children] +-print(children)  +- ++print(children) +diff --git a/warmup/plot_all_mut_freq.py b/warmup/plot_all_mut_freq.py +index b3d14e9..15b05fb 100644 +--- a/warmup/plot_all_mut_freq.py ++++ b/warmup/plot_all_mut_freq.py +@@ -2,32 +2,63 @@ import pandas as pd + import matplotlib.pyplot as plt +  + paths = { +- 2000: ('/home/laukeller/BSc Thesis/TreeMHN/Example/trees_2000.csv', '/home/laukeller/BSc Thesis/pMHN/src/pmhn/_trees/trees_2000.csv'), +- 5000: ('/home/laukeller/BSc Thesis/TreeMHN/Example/trees_5000.csv', '/home/laukeller/BSc Thesis/pMHN/src/pmhn/_trees/trees_5000.csv'), +- 10000: ('/home/laukeller/BSc Thesis/TreeMHN/Example/trees_10000.csv', '/home/laukeller/BSc Thesis/pMHN/src/pmhn/_trees/trees_10000.csv'), +- 50000: ('/home/laukeller/BSc Thesis/TreeMHN/Example/trees_50000.csv', '/home/laukeller/BSc Thesis/pMHN/src/pmhn/_trees/trees_50000.csv'), ++ 500: ( ++ "/home/laukeller/BSc Thesis/TreeMHN/Example/trees_500.csv", ++ "/home/laukeller/BSc Thesis/pMHN/warmup/trees_500.csv", ++ ), ++ 5000: ( ++ "/home/laukeller/BSc Thesis/TreeMHN/Example/trees_5000.csv", ++ "/home/laukeller/BSc Thesis/pMHN/warmup/trees_5000.csv", ++ ), ++ 10000: ( ++ "/home/laukeller/BSc Thesis/TreeMHN/Example/trees_10000.csv", ++ "/home/laukeller/BSc Thesis/pMHN/warmup/trees_10000.csv", ++ ), ++ 50000: ( ++ "/home/laukeller/BSc Thesis/TreeMHN/Example/trees_50000.csv", ++ "/home/laukeller/BSc Thesis/pMHN/warmup/trees_50000.csv", ++ ), + } +  + fig, axs = plt.subplots(2, 2, figsize=(10, 10)) # 2x2 grid of subplots +-axs = axs.ravel()  ++axs = axs.ravel() +  + for i, (num_trees, (r_path, python_path)) in enumerate(paths.items()): + r_trees = pd.read_csv(r_path) + python_trees = pd.read_csv(python_path) +-  +- r_mutation_frequencies = r_trees['Mutation_ID'].value_counts().sort_index() +- python_mutation_frequencies = python_trees['Mutation_ID'].value_counts().sort_index() +-  ++ ++ r_mutation_frequencies = r_trees["Mutation_ID"].value_counts().sort_index() ++ python_mutation_frequencies = ( ++ python_trees["Mutation_ID"].value_counts().sort_index() ++ ) ++ + bar_width = 0.35 +-  +- r_mutation_frequencies.plot(kind='bar', width=bar_width, position=0, align='center', color='b', alpha=0.5, label='R Trees', ax=axs[i]) +- python_mutation_frequencies.plot(kind='bar', width=bar_width, position=1, align='center', color='r', alpha=0.5, label='Python Trees', ax=axs[i]) +-  +- axs[i].set_xlabel('Mutation ID') +- axs[i].set_ylabel('Frequency') +- axs[i].legend(loc='upper right') +- axs[i].set_title(f'Mutation Frequency Distribution ({num_trees} trees)') +- +-plt.tight_layout()  +-plt.show() +  ++ r_mutation_frequencies.plot( ++ kind="bar", ++ width=bar_width, ++ position=0, ++ align="center", ++ color="b", ++ alpha=0.5, ++ label="R Trees", ++ ax=axs[i], ++ ) ++ python_mutation_frequencies.plot( ++ kind="bar", ++ width=bar_width, ++ position=1, ++ align="center", ++ color="r", ++ alpha=0.5, ++ label="Python Trees", ++ ax=axs[i], ++ ) ++ ++ axs[i].set_xlabel("Mutation ID") ++ axs[i].set_ylabel("Frequency") ++ axs[i].legend(loc="upper right") ++ axs[i].set_title(f"Mutation Frequency Distribution ({num_trees} trees)") ++ ++plt.tight_layout() ++plt.show() +diff --git a/warmup/plot_all_trees.py b/warmup/plot_all_trees.py +index acc932b..1120bb5 100644 +--- a/warmup/plot_all_trees.py ++++ b/warmup/plot_all_trees.py +@@ -3,12 +3,25 @@ import matplotlib.pyplot as plt +  +  + paths = { +- 2000: ('/home/laukeller/BSc Thesis/TreeMHN/Example/trees_2000.csv', '/home/laukeller/BSc Thesis/pMHN/src/pmhn/_trees/trees_2000.csv'), +- 5000: ('/home/laukeller/BSc Thesis/TreeMHN/Example/trees_5000.csv', '/home/laukeller/BSc Thesis/pMHN/src/pmhn/_trees/trees_5000.csv'), +- 10000: ('/home/laukeller/BSc Thesis/TreeMHN/Example/trees_10000.csv', '/home/laukeller/BSc Thesis/pMHN/src/pmhn/_trees/trees_10000.csv'), +- 50000: ('/home/laukeller/BSc Thesis/TreeMHN/Example/trees_50000.csv', '/home/laukeller/BSc Thesis/pMHN/src/pmhn/_trees/trees_50000.csv'), ++ 500: ( ++ "/home/laukeller/BSc Thesis/TreeMHN/Example/trees_500.csv", ++ "/home/laukeller/BSc Thesis/pMHN/warmup/trees_500.csv", ++ ), ++ 5000: ( ++ "/home/laukeller/BSc Thesis/TreeMHN/Example/trees_5000.csv", ++ "/home/laukeller/BSc Thesis/pMHN/warmup/trees_5000.csv", ++ ), ++ 10000: ( ++ "/home/laukeller/BSc Thesis/TreeMHN/Example/trees_10000.csv", ++ "/home/laukeller/BSc Thesis/pMHN/warmup/trees_10000.csv", ++ ), ++ 50000: ( ++ "/home/laukeller/BSc Thesis/TreeMHN/Example/trees_50000.csv", ++ "/home/laukeller/BSc Thesis/pMHN/warmup/trees_50000.csv", ++ ), + } +  ++ + fig, axs = plt.subplots(2, 2, figsize=(10, 10)) # 2x2 grid of subplots + axs = axs.ravel() +  +@@ -17,17 +30,16 @@ bin_edges = [x - 0.5 for x in range(2, 12)] + [11.5] # [1.5, 2.5, ..., 10.5, 11 + for i, (num_trees, (r_path, python_path)) in enumerate(paths.items()): + r_trees = pd.read_csv(r_path) + python_trees = pd.read_csv(python_path) +-  +- r_tree_sizes = r_trees.groupby('Tree_ID').size() +- python_tree_sizes = python_trees.groupby('Tree_ID').size() +-  +- axs[i].hist(r_tree_sizes, bins=bin_edges, alpha=0.5, label='R Trees') +- axs[i].hist(python_tree_sizes, bins=bin_edges, alpha=0.5, label='Python Trees') +- axs[i].set_xlabel('Tree Size') +- axs[i].set_ylabel('Frequency') +- axs[i].legend(loc='upper right') +- axs[i].set_title(f'Tree Size Distribution ({num_trees} trees)') +- +-plt.tight_layout()  +-plt.show() +  ++ r_tree_sizes = r_trees.groupby("Tree_ID").size() ++ python_tree_sizes = python_trees.groupby("Tree_ID").size() ++ ++ axs[i].hist(r_tree_sizes, bins=bin_edges, alpha=0.5, label="R Trees") ++ axs[i].hist(python_tree_sizes, bins=bin_edges, alpha=0.5, label="Python Trees") ++ axs[i].set_xlabel("Tree Size") ++ axs[i].set_ylabel("Frequency") ++ axs[i].legend(loc="upper right") ++ axs[i].set_title(f"Tree Size Distribution ({num_trees} trees)") ++ ++plt.tight_layout() ++plt.show() +diff --git a/warmup/plot_mutation_freq.py b/warmup/plot_mutation_freq.py +index 0bdfc28..52962b0 100644 +--- a/warmup/plot_mutation_freq.py ++++ b/warmup/plot_mutation_freq.py +@@ -1,27 +1,44 @@ + import pandas as pd + import matplotlib.pyplot as plt +  +-r_trees_path ='/home/laukeller/BSc Thesis/TreeMHN/Example/trees_10000.csv' +-python_trees_path ='/home/laukeller/BSc Thesis/pMHN/src/pmhn/_trees/trees_10000.csv' ++r_trees_path = "/home/laukeller/BSc Thesis/TreeMHN/Example/trees_10000.csv" ++python_trees_path = "/home/laukeller/BSc Thesis/pMHN/src/pmhn/_trees/trees_10000.csv" +  + r_trees = pd.read_csv(r_trees_path) + python_trees = pd.read_csv(python_trees_path) +  +-r_mutation_frequencies = r_trees['Mutation_ID'].value_counts().sort_index() +-python_mutation_frequencies = python_trees['Mutation_ID'].value_counts().sort_index() ++r_mutation_frequencies = r_trees["Mutation_ID"].value_counts().sort_index() ++python_mutation_frequencies = python_trees["Mutation_ID"].value_counts().sort_index() +  + fig, ax = plt.subplots(figsize=(10, 6)) +  + bar_width = 0.35 +  +-r_mutation_frequencies.plot(kind='bar', width=bar_width, position=0, align='center', color='b', alpha=0.5, label='R Trees', ax=ax) +-python_mutation_frequencies.plot(kind='bar', width=bar_width, position=1, align='center', color='r', alpha=0.5, label='Python Trees', ax=ax) +- +-ax.set_xlabel('Mutation ID') +-ax.set_ylabel('Frequency') +-ax.set_title('Mutation Frequencies Comparison') ++r_mutation_frequencies.plot( ++ kind="bar", ++ width=bar_width, ++ position=0, ++ align="center", ++ color="b", ++ alpha=0.5, ++ label="R Trees", ++ ax=ax, ++) ++python_mutation_frequencies.plot( ++ kind="bar", ++ width=bar_width, ++ position=1, ++ align="center", ++ color="r", ++ alpha=0.5, ++ label="Python Trees", ++ ax=ax, ++) ++ ++ax.set_xlabel("Mutation ID") ++ax.set_ylabel("Frequency") ++ax.set_title("Mutation Frequencies Comparison") + ax.legend() +  + plt.tight_layout() + plt.show() +- +diff --git a/warmup/plot_trees.py b/warmup/plot_trees.py +index 73dd895..d1400e9 100644 +--- a/warmup/plot_trees.py ++++ b/warmup/plot_trees.py +@@ -1,21 +1,22 @@ + import pandas as pd + import matplotlib.pyplot as plt +  +-r_trees_path = '/home/laukeller/BSc Thesis/TreeMHN/Example/trees_10000.csv' +-python_trees_path = '/home/laukeller/BSc Thesis/pMHN/src/pmhn/_trees/trees_10000.csv' ++r_trees_path = "/home/laukeller/BSc Thesis/TreeMHN/Example/trees_10000.csv" ++python_trees_path = "/home/laukeller/BSc Thesis/pMHN/src/pmhn/_trees/trees_10000.csv" +  + r_trees = pd.read_csv(r_trees_path) + python_trees = pd.read_csv(python_trees_path) +  +-r_tree_sizes = r_trees.groupby('Tree_ID').size() +-python_tree_sizes = python_trees.groupby('Tree_ID').size() ++r_tree_sizes = r_trees.groupby("Tree_ID").size() ++python_tree_sizes = python_trees.groupby("Tree_ID").size() +  +-bin_edges = [x - 0.5 for x in range(2, 12)] + [11.5]  +-plt.hist(r_tree_sizes, bins=bin_edges, alpha=0.5, edgecolor='k', label='R Trees') +-plt.hist(python_tree_sizes, bins=bin_edges, alpha=0.5, edgecolor='k', label='Python Trees') +-plt.xlabel('Tree Size') +-plt.ylabel('Frequency') +-plt.legend(loc='upper right') +-plt.title('Tree Size Distribution') ++bin_edges = [x - 0.5 for x in range(2, 12)] + [11.5] ++plt.hist(r_tree_sizes, bins=bin_edges, alpha=0.5, edgecolor="k", label="R Trees") ++plt.hist( ++ python_tree_sizes, bins=bin_edges, alpha=0.5, edgecolor="k", label="Python Trees" ++) ++plt.xlabel("Tree Size") ++plt.ylabel("Frequency") ++plt.legend(loc="upper right") ++plt.title("Tree Size Distribution") + plt.show() +- +diff --git a/warmup/subtree.py b/warmup/subtree.py +index 0be796e..14ed810 100644 +--- a/warmup/subtree.py ++++ b/warmup/subtree.py +@@ -1,52 +1,61 @@ +-from anytree import Node, RenderTree  ++from anytree import Node, RenderTree + from itertools import combinations, product +-''' ++ ++""" + takes a variable number of lists as input and returns a list containing all possible combination of the input lists + in this case: takes a list of lists of subtrees (for each child one list of subtrees) as input, a subtree itself is a list of nodes + outputs all combinations of subtrees  +-''' ++""" ++ ++ + def all_combinations_of_elements(*lists): + n = len(lists) + all_combinations = [] +  +- for r in range(1, n+1): ++ for r in range(1, n + 1): + for list_combination in combinations(lists, r): + for element_combination in product(*list_combination): +- all_combinations.append(list(element_combination))  ++ all_combinations.append(list(element_combination)) +  + return all_combinations +  +-'''creates a subtree given a subtree (nodes_list) and the root node''' ++ ++"""creates a subtree given a subtree (nodes_list) and the root node""" ++ +  + def create_subtree(original_root, nodes_list): + nodes_dict = {} +-  ++ + for node in [original_root] + list(original_root.descendants): + if node in nodes_list: + parent_node = next((n for n in nodes_list if n is node.parent), None) + nodes_dict[node] = Node(node.name, parent=nodes_dict.get(parent_node)) +-  ++ + return nodes_dict.get(original_root) +  +-'''returns a list of all subtrees of a tree, input is the root node ++ ++"""returns a list of all subtrees of a tree, input is the root node + a recursive approach is used: if one knows the subtrees of the children of the root node, + then one can find all combinations of the subtrees of the children and add the root node to each one of these combinations, +-this way one obtains all subtrees of the root node''' ++this way one obtains all subtrees of the root node""" ++ +  + def subtrees(node): + if not node.children: + return [[node]] +-  ++ + child_subtrees = [subtrees(child) for child in node.children] +-  ++ + combined_subtrees = all_combinations_of_elements(*child_subtrees) +-  ++ + result_subtrees = [] + result_subtrees.append([node]) + for combination in combined_subtrees: +- subtree_with_root = [node] + [item for sublist in combination for item in sublist]  ++ subtree_with_root = [node] + [ ++ item for sublist in combination for item in sublist ++ ] + result_subtrees.append(subtree_with_root) +-  ++ + return result_subtrees +  +  +@@ -61,13 +70,13 @@ all_node_lists = subtrees(A) + all_node_lists = sorted(all_node_lists, key=len) + print(all_node_lists) + print("\n") +-all_subtrees=[] ++all_subtrees = [] +  + for nodes_list in all_node_lists: +- subtree=create_subtree(A,nodes_list) ++ subtree = create_subtree(A, nodes_list) + all_subtrees.append(subtree) + i = 1 + for subtree in all_subtrees: + print(f"{i}. ") + print(RenderTree(subtree)) +- i += 1  ++ i += 1 +diff --git a/warmup/write_csv.py b/warmup/write_csv.py +index a5b888d..a06c8f3 100644 +--- a/warmup/write_csv.py ++++ b/warmup/write_csv.py +@@ -1,20 +1,24 @@ + import csv + import numpy as np + from anytree import Node +-import _simulate  ++import pmhn._trees._simulate as _simulate ++ +  + def csv_to_numpy(file_path): +- with open(file_path, 'r') as file: ++ with open(file_path, "r") as file: + reader = csv.reader(file) + next(reader) + data_list = list(reader) + return np.array(data_list, dtype=float) +  ++ + def write_trees_to_csv(trees, output_file_path): +- with open(output_file_path, 'w', newline='') as file: ++ with open(output_file_path, "w", newline="") as file: + writer = csv.writer(file) +- writer.writerow(["Patient_ID", "Tree_ID", "Node_ID", "Mutation_ID", "Parent_ID"]) +-  ++ writer.writerow( ++ ["Patient_ID", "Tree_ID", "Node_ID", "Mutation_ID", "Parent_ID"] ++ ) ++ + patient_id = 0 + for tree_dict in trees: + patient_id += 1 +@@ -26,22 +30,29 @@ def write_trees_to_csv(trees, output_file_path): + parent_id = node.parent.name if node.parent else node_id + writer.writerow([patient_id, tree_id, node_id, mutation_id, parent_id]) +  ++ + if __name__ == "__main__": + mhn_file_path = "/home/laukeller/BSc Thesis/TreeMHN/Example/MHN_Matrix.csv" + mhn_array = csv_to_numpy(mhn_file_path) + print(mhn_array) +-  ++ + rng = np.random.default_rng() +- theta = mhn_array  +- mean_sampling_time = 1.0  +-  +- tree_counts = [2000, 5000, 10000, 50000] +-  ++ theta = mhn_array ++ mean_sampling_time = 1.0 ++ ++ tree_counts = [500, 5000, 10000, 50000] ++ + min_tree_size = 2 + max_tree_size = 11 + for n_points in tree_counts: +- trees_file_path = f"/home/laukeller/BSc Thesis/pMHN/src/pmhn/_trees/trees_{n_points}.csv" +-  +- _, trees = _simulate.simulate_trees(rng, n_points, theta, mean_sampling_time, min_tree_size = min_tree_size, max_tree_size = max_tree_size) +- write_trees_to_csv(trees, trees_file_path) ++ trees_file_path = f"/home/laukeller/BSc Thesis/pMHN/warmup/trees_{n_points}.csv" +  ++ trees = _simulate.simulate_trees( ++ rng, ++ n_points, ++ theta, ++ mean_sampling_time, ++ min_tree_size=min_tree_size, ++ max_tree_size=max_tree_size, ++ ) ++ write_trees_to_csv(trees, trees_file_path) diff --git a/src/pmhn/_trees/._simulate.py.swp b/src/pmhn/_trees/._simulate.py.swp new file mode 100644 index 0000000000000000000000000000000000000000..3e2af75ae2b90be8839cb215454d57392ef12adf GIT binary patch literal 20480 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 min_tree_size - + for node, time in tree.items(): assert time < sampling_time - + + def test_find_possible_mutations_normal(): """ - We want to test if the possible_mutations list is correct. + We want to test if the possible_mutations list is correct. """ old_mutations = [7, 2, 5, 9] n_mutations = 10 possible_mutations = simulate._find_possible_mutations(old_mutations, n_mutations) - + assert possible_mutations == [1, 3, 4, 6, 8, 10] - -def test_find_possible_mutations_edge(): + +def test_find_possible_mutations_edge(): """ We want to test if the possible_mutations list is correct. old_mutations with mutations on the edge. """ @@ -130,31 +151,35 @@ def test_find_possible_mutations_edge(): old_mutations = [1, 10] n_mutations = 10 possible_mutations = simulate._find_possible_mutations(old_mutations, n_mutations) - + assert possible_mutations == [i for i in range(2, 10)] - -def test_find_possible_mutations_except_positive(): + + +def test_find_possible_mutations_except_positive(): """ We want to test if an exception is correctly thrown when the old_mutations list is invalid (mutation number too large). - """ + """ old_mutations = [212, 1, 3, 7] n_mutations = 10 with pytest.raises(ValueError) as excinfo: - simulate._find_possible_mutations(old_mutations, n_mutations) - - assert str(excinfo.value) == "Invalid mutation 212 in old_mutations. It should be 0 <= mutation <= 10." - -def test_find_possible_mutations_except_negative(): + simulate._find_possible_mutations(old_mutations, n_mutations) + + assert ( + str(excinfo.value) + == "Invalid mutation 212 in old_mutations. It should be 0 <= mutation <= 10." + ) + + +def test_find_possible_mutations_except_negative(): """ We want to test if an exception is correctly thrown when the old_mutations list is invalid (mutation number too small). - """ + """ old_mutations = [-23, 0, 5] n_mutations = 10 with pytest.raises(ValueError) as excinfo: - simulate._find_possible_mutations(old_mutations, n_mutations) - - assert str(excinfo.value) == "Invalid mutation -23 in old_mutations. It should be 0 <= mutation <= 10." - - - + simulate._find_possible_mutations(old_mutations, n_mutations) + assert ( + str(excinfo.value) + == "Invalid mutation -23 in old_mutations. It should be 0 <= mutation <= 10." + ) diff --git a/warmup/children.py b/warmup/children.py index 6ec7914..d22c176 100644 --- a/warmup/children.py +++ b/warmup/children.py @@ -9,8 +9,7 @@ all_nodes = list(PreOrderIter(A)) print(RenderTree(A)) -children={} +children = {} for node in all_nodes: children[node.name] = [child.name for child in node.children] -print(children) - +print(children) diff --git a/warmup/plot_all_mut_freq.py b/warmup/plot_all_mut_freq.py index b3d14e9..15b05fb 100644 --- a/warmup/plot_all_mut_freq.py +++ b/warmup/plot_all_mut_freq.py @@ -2,32 +2,63 @@ import matplotlib.pyplot as plt paths = { - 2000: ('/home/laukeller/BSc Thesis/TreeMHN/Example/trees_2000.csv', '/home/laukeller/BSc Thesis/pMHN/src/pmhn/_trees/trees_2000.csv'), - 5000: ('/home/laukeller/BSc Thesis/TreeMHN/Example/trees_5000.csv', '/home/laukeller/BSc Thesis/pMHN/src/pmhn/_trees/trees_5000.csv'), - 10000: ('/home/laukeller/BSc Thesis/TreeMHN/Example/trees_10000.csv', '/home/laukeller/BSc Thesis/pMHN/src/pmhn/_trees/trees_10000.csv'), - 50000: ('/home/laukeller/BSc Thesis/TreeMHN/Example/trees_50000.csv', '/home/laukeller/BSc Thesis/pMHN/src/pmhn/_trees/trees_50000.csv'), + 500: ( + "/home/laukeller/BSc Thesis/TreeMHN/Example/trees_500.csv", + "/home/laukeller/BSc Thesis/pMHN/warmup/trees_500.csv", + ), + 5000: ( + "/home/laukeller/BSc Thesis/TreeMHN/Example/trees_5000.csv", + "/home/laukeller/BSc Thesis/pMHN/warmup/trees_5000.csv", + ), + 10000: ( + "/home/laukeller/BSc Thesis/TreeMHN/Example/trees_10000.csv", + "/home/laukeller/BSc Thesis/pMHN/warmup/trees_10000.csv", + ), + 50000: ( + "/home/laukeller/BSc Thesis/TreeMHN/Example/trees_50000.csv", + "/home/laukeller/BSc Thesis/pMHN/warmup/trees_50000.csv", + ), } fig, axs = plt.subplots(2, 2, figsize=(10, 10)) # 2x2 grid of subplots -axs = axs.ravel() +axs = axs.ravel() for i, (num_trees, (r_path, python_path)) in enumerate(paths.items()): r_trees = pd.read_csv(r_path) python_trees = pd.read_csv(python_path) - - r_mutation_frequencies = r_trees['Mutation_ID'].value_counts().sort_index() - python_mutation_frequencies = python_trees['Mutation_ID'].value_counts().sort_index() - + + r_mutation_frequencies = r_trees["Mutation_ID"].value_counts().sort_index() + python_mutation_frequencies = ( + python_trees["Mutation_ID"].value_counts().sort_index() + ) + bar_width = 0.35 - - r_mutation_frequencies.plot(kind='bar', width=bar_width, position=0, align='center', color='b', alpha=0.5, label='R Trees', ax=axs[i]) - python_mutation_frequencies.plot(kind='bar', width=bar_width, position=1, align='center', color='r', alpha=0.5, label='Python Trees', ax=axs[i]) - - axs[i].set_xlabel('Mutation ID') - axs[i].set_ylabel('Frequency') - axs[i].legend(loc='upper right') - axs[i].set_title(f'Mutation Frequency Distribution ({num_trees} trees)') - -plt.tight_layout() -plt.show() + r_mutation_frequencies.plot( + kind="bar", + width=bar_width, + position=0, + align="center", + color="b", + alpha=0.5, + label="R Trees", + ax=axs[i], + ) + python_mutation_frequencies.plot( + kind="bar", + width=bar_width, + position=1, + align="center", + color="r", + alpha=0.5, + label="Python Trees", + ax=axs[i], + ) + + axs[i].set_xlabel("Mutation ID") + axs[i].set_ylabel("Frequency") + axs[i].legend(loc="upper right") + axs[i].set_title(f"Mutation Frequency Distribution ({num_trees} trees)") + +plt.tight_layout() +plt.show() diff --git a/warmup/plot_all_trees.py b/warmup/plot_all_trees.py index acc932b..1120bb5 100644 --- a/warmup/plot_all_trees.py +++ b/warmup/plot_all_trees.py @@ -3,12 +3,25 @@ paths = { - 2000: ('/home/laukeller/BSc Thesis/TreeMHN/Example/trees_2000.csv', '/home/laukeller/BSc Thesis/pMHN/src/pmhn/_trees/trees_2000.csv'), - 5000: ('/home/laukeller/BSc Thesis/TreeMHN/Example/trees_5000.csv', '/home/laukeller/BSc Thesis/pMHN/src/pmhn/_trees/trees_5000.csv'), - 10000: ('/home/laukeller/BSc Thesis/TreeMHN/Example/trees_10000.csv', '/home/laukeller/BSc Thesis/pMHN/src/pmhn/_trees/trees_10000.csv'), - 50000: ('/home/laukeller/BSc Thesis/TreeMHN/Example/trees_50000.csv', '/home/laukeller/BSc Thesis/pMHN/src/pmhn/_trees/trees_50000.csv'), + 500: ( + "/home/laukeller/BSc Thesis/TreeMHN/Example/trees_500.csv", + "/home/laukeller/BSc Thesis/pMHN/warmup/trees_500.csv", + ), + 5000: ( + "/home/laukeller/BSc Thesis/TreeMHN/Example/trees_5000.csv", + "/home/laukeller/BSc Thesis/pMHN/warmup/trees_5000.csv", + ), + 10000: ( + "/home/laukeller/BSc Thesis/TreeMHN/Example/trees_10000.csv", + "/home/laukeller/BSc Thesis/pMHN/warmup/trees_10000.csv", + ), + 50000: ( + "/home/laukeller/BSc Thesis/TreeMHN/Example/trees_50000.csv", + "/home/laukeller/BSc Thesis/pMHN/warmup/trees_50000.csv", + ), } + fig, axs = plt.subplots(2, 2, figsize=(10, 10)) # 2x2 grid of subplots axs = axs.ravel() @@ -17,17 +30,16 @@ for i, (num_trees, (r_path, python_path)) in enumerate(paths.items()): r_trees = pd.read_csv(r_path) python_trees = pd.read_csv(python_path) - - r_tree_sizes = r_trees.groupby('Tree_ID').size() - python_tree_sizes = python_trees.groupby('Tree_ID').size() - - axs[i].hist(r_tree_sizes, bins=bin_edges, alpha=0.5, label='R Trees') - axs[i].hist(python_tree_sizes, bins=bin_edges, alpha=0.5, label='Python Trees') - axs[i].set_xlabel('Tree Size') - axs[i].set_ylabel('Frequency') - axs[i].legend(loc='upper right') - axs[i].set_title(f'Tree Size Distribution ({num_trees} trees)') - -plt.tight_layout() -plt.show() + r_tree_sizes = r_trees.groupby("Tree_ID").size() + python_tree_sizes = python_trees.groupby("Tree_ID").size() + + axs[i].hist(r_tree_sizes, bins=bin_edges, alpha=0.5, label="R Trees") + axs[i].hist(python_tree_sizes, bins=bin_edges, alpha=0.5, label="Python Trees") + axs[i].set_xlabel("Tree Size") + axs[i].set_ylabel("Frequency") + axs[i].legend(loc="upper right") + axs[i].set_title(f"Tree Size Distribution ({num_trees} trees)") + +plt.tight_layout() +plt.show() diff --git a/warmup/plot_mutation_freq.py b/warmup/plot_mutation_freq.py index 0bdfc28..52962b0 100644 --- a/warmup/plot_mutation_freq.py +++ b/warmup/plot_mutation_freq.py @@ -1,27 +1,44 @@ import pandas as pd import matplotlib.pyplot as plt -r_trees_path ='/home/laukeller/BSc Thesis/TreeMHN/Example/trees_10000.csv' -python_trees_path ='/home/laukeller/BSc Thesis/pMHN/src/pmhn/_trees/trees_10000.csv' +r_trees_path = "/home/laukeller/BSc Thesis/TreeMHN/Example/trees_10000.csv" +python_trees_path = "/home/laukeller/BSc Thesis/pMHN/src/pmhn/_trees/trees_10000.csv" r_trees = pd.read_csv(r_trees_path) python_trees = pd.read_csv(python_trees_path) -r_mutation_frequencies = r_trees['Mutation_ID'].value_counts().sort_index() -python_mutation_frequencies = python_trees['Mutation_ID'].value_counts().sort_index() +r_mutation_frequencies = r_trees["Mutation_ID"].value_counts().sort_index() +python_mutation_frequencies = python_trees["Mutation_ID"].value_counts().sort_index() fig, ax = plt.subplots(figsize=(10, 6)) bar_width = 0.35 -r_mutation_frequencies.plot(kind='bar', width=bar_width, position=0, align='center', color='b', alpha=0.5, label='R Trees', ax=ax) -python_mutation_frequencies.plot(kind='bar', width=bar_width, position=1, align='center', color='r', alpha=0.5, label='Python Trees', ax=ax) - -ax.set_xlabel('Mutation ID') -ax.set_ylabel('Frequency') -ax.set_title('Mutation Frequencies Comparison') +r_mutation_frequencies.plot( + kind="bar", + width=bar_width, + position=0, + align="center", + color="b", + alpha=0.5, + label="R Trees", + ax=ax, +) +python_mutation_frequencies.plot( + kind="bar", + width=bar_width, + position=1, + align="center", + color="r", + alpha=0.5, + label="Python Trees", + ax=ax, +) + +ax.set_xlabel("Mutation ID") +ax.set_ylabel("Frequency") +ax.set_title("Mutation Frequencies Comparison") ax.legend() plt.tight_layout() plt.show() - diff --git a/warmup/plot_trees.py b/warmup/plot_trees.py index 73dd895..d1400e9 100644 --- a/warmup/plot_trees.py +++ b/warmup/plot_trees.py @@ -1,21 +1,22 @@ import pandas as pd import matplotlib.pyplot as plt -r_trees_path = '/home/laukeller/BSc Thesis/TreeMHN/Example/trees_10000.csv' -python_trees_path = '/home/laukeller/BSc Thesis/pMHN/src/pmhn/_trees/trees_10000.csv' +r_trees_path = "/home/laukeller/BSc Thesis/TreeMHN/Example/trees_10000.csv" +python_trees_path = "/home/laukeller/BSc Thesis/pMHN/src/pmhn/_trees/trees_10000.csv" r_trees = pd.read_csv(r_trees_path) python_trees = pd.read_csv(python_trees_path) -r_tree_sizes = r_trees.groupby('Tree_ID').size() -python_tree_sizes = python_trees.groupby('Tree_ID').size() +r_tree_sizes = r_trees.groupby("Tree_ID").size() +python_tree_sizes = python_trees.groupby("Tree_ID").size() -bin_edges = [x - 0.5 for x in range(2, 12)] + [11.5] -plt.hist(r_tree_sizes, bins=bin_edges, alpha=0.5, edgecolor='k', label='R Trees') -plt.hist(python_tree_sizes, bins=bin_edges, alpha=0.5, edgecolor='k', label='Python Trees') -plt.xlabel('Tree Size') -plt.ylabel('Frequency') -plt.legend(loc='upper right') -plt.title('Tree Size Distribution') +bin_edges = [x - 0.5 for x in range(2, 12)] + [11.5] +plt.hist(r_tree_sizes, bins=bin_edges, alpha=0.5, edgecolor="k", label="R Trees") +plt.hist( + python_tree_sizes, bins=bin_edges, alpha=0.5, edgecolor="k", label="Python Trees" +) +plt.xlabel("Tree Size") +plt.ylabel("Frequency") +plt.legend(loc="upper right") +plt.title("Tree Size Distribution") plt.show() - diff --git a/warmup/subtree.py b/warmup/subtree.py index 0be796e..14ed810 100644 --- a/warmup/subtree.py +++ b/warmup/subtree.py @@ -1,52 +1,61 @@ -from anytree import Node, RenderTree +from anytree import Node, RenderTree from itertools import combinations, product -''' + +""" takes a variable number of lists as input and returns a list containing all possible combination of the input lists in this case: takes a list of lists of subtrees (for each child one list of subtrees) as input, a subtree itself is a list of nodes outputs all combinations of subtrees -''' +""" + + def all_combinations_of_elements(*lists): n = len(lists) all_combinations = [] - for r in range(1, n+1): + for r in range(1, n + 1): for list_combination in combinations(lists, r): for element_combination in product(*list_combination): - all_combinations.append(list(element_combination)) + all_combinations.append(list(element_combination)) return all_combinations -'''creates a subtree given a subtree (nodes_list) and the root node''' + +"""creates a subtree given a subtree (nodes_list) and the root node""" + def create_subtree(original_root, nodes_list): nodes_dict = {} - + for node in [original_root] + list(original_root.descendants): if node in nodes_list: parent_node = next((n for n in nodes_list if n is node.parent), None) nodes_dict[node] = Node(node.name, parent=nodes_dict.get(parent_node)) - + return nodes_dict.get(original_root) -'''returns a list of all subtrees of a tree, input is the root node + +"""returns a list of all subtrees of a tree, input is the root node a recursive approach is used: if one knows the subtrees of the children of the root node, then one can find all combinations of the subtrees of the children and add the root node to each one of these combinations, -this way one obtains all subtrees of the root node''' +this way one obtains all subtrees of the root node""" + def subtrees(node): if not node.children: return [[node]] - + child_subtrees = [subtrees(child) for child in node.children] - + combined_subtrees = all_combinations_of_elements(*child_subtrees) - + result_subtrees = [] result_subtrees.append([node]) for combination in combined_subtrees: - subtree_with_root = [node] + [item for sublist in combination for item in sublist] + subtree_with_root = [node] + [ + item for sublist in combination for item in sublist + ] result_subtrees.append(subtree_with_root) - + return result_subtrees @@ -61,13 +70,13 @@ def subtrees(node): all_node_lists = sorted(all_node_lists, key=len) print(all_node_lists) print("\n") -all_subtrees=[] +all_subtrees = [] for nodes_list in all_node_lists: - subtree=create_subtree(A,nodes_list) + subtree = create_subtree(A, nodes_list) all_subtrees.append(subtree) i = 1 for subtree in all_subtrees: print(f"{i}. ") print(RenderTree(subtree)) - i += 1 + i += 1 diff --git a/warmup/write_csv.py b/warmup/write_csv.py index a5b888d..168bab3 100644 --- a/warmup/write_csv.py +++ b/warmup/write_csv.py @@ -1,20 +1,24 @@ import csv import numpy as np from anytree import Node -import _simulate +import pmhn._trees._simulate as _simulate + def csv_to_numpy(file_path): - with open(file_path, 'r') as file: + with open(file_path, "r") as file: reader = csv.reader(file) next(reader) data_list = list(reader) return np.array(data_list, dtype=float) + def write_trees_to_csv(trees, output_file_path): - with open(output_file_path, 'w', newline='') as file: + with open(output_file_path, "w", newline="") as file: writer = csv.writer(file) - writer.writerow(["Patient_ID", "Tree_ID", "Node_ID", "Mutation_ID", "Parent_ID"]) - + writer.writerow( + ["Patient_ID", "Tree_ID", "Node_ID", "Mutation_ID", "Parent_ID"] + ) + patient_id = 0 for tree_dict in trees: patient_id += 1 @@ -26,22 +30,29 @@ def write_trees_to_csv(trees, output_file_path): parent_id = node.parent.name if node.parent else node_id writer.writerow([patient_id, tree_id, node_id, mutation_id, parent_id]) + if __name__ == "__main__": mhn_file_path = "/home/laukeller/BSc Thesis/TreeMHN/Example/MHN_Matrix.csv" mhn_array = csv_to_numpy(mhn_file_path) print(mhn_array) - + rng = np.random.default_rng() - theta = mhn_array - mean_sampling_time = 1.0 - - tree_counts = [2000, 5000, 10000, 50000] - + theta = mhn_array + mean_sampling_time = 1.0 + + tree_counts = [500, 5000, 10000, 50000] + min_tree_size = 2 max_tree_size = 11 for n_points in tree_counts: - trees_file_path = f"/home/laukeller/BSc Thesis/pMHN/src/pmhn/_trees/trees_{n_points}.csv" - - _, trees = _simulate.simulate_trees(rng, n_points, theta, mean_sampling_time, min_tree_size = min_tree_size, max_tree_size = max_tree_size) - write_trees_to_csv(trees, trees_file_path) + trees_file_path = f"/home/laukeller/BSc Thesis/pMHN/warmup/trees_{n_points}.csv" + _, trees = _simulate.simulate_trees( + rng, + n_points, + theta, + mean_sampling_time, + min_tree_size=min_tree_size, + max_tree_size=max_tree_size, + ) + write_trees_to_csv(trees, trees_file_path) From ea7cd53dcefbc9a826ee86c286db4002799e2000 Mon Sep 17 00:00:00 2001 From: Laurenz Keller Date: Thu, 28 Sep 2023 14:52:57 +0200 Subject: [PATCH 11/45] changed unit tests --- src/pmhn/_trees/._simulate.py.swp | Bin 20480 -> 0 bytes src/pmhn/_trees/_simulate.py | 20 +++++++++++++------ tests/trees/test_simulate.py | 31 +++++++++++++++++------------- 3 files changed, 32 insertions(+), 19 deletions(-) delete mode 100644 src/pmhn/_trees/._simulate.py.swp diff --git a/src/pmhn/_trees/._simulate.py.swp b/src/pmhn/_trees/._simulate.py.swp deleted file mode 100644 index 3e2af75ae2b90be8839cb215454d57392ef12adf..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 20480 zcmeHOZ;T{G6>kKDML_;th<++|`7p!H^zI%g=i~-+!0Bz`{n6XQGwWuycBW>h?|Qn2 z?w;G-I}eS5ibg_$CdOch5G8!TfFvf8=m%nmA5cT0CZLCj5QP{FXb8%m=OV8P z$0a5bp*#EAp6;%C_3BquuikspvpjRt+#a#r-R9u9$Z=--ThuMLKkl@yavWa{k0{?) z5j{J_k(R5^_q^c9e%YxXnyud;qvbHDe_RFi?Yi>N(M>>N(M>>N(M>>N(M>>-Wd!egEO7aqbp~c?o67qZ_a0!^I7J6y*b-* z_W66v{r%?LHfQ_XT$ZPjfs%oefs%oefs%oefs%oefs%oefs%oefs%o@fdScZoLdmQ zKTQb$eE)Cb_aDFCapr(0FLay-fH`0b@ahG)2G)Qka53=Y`Hpixa61qHhk%{HG;kSk zDe&6+9OqxaKY{0f-vhq@9tHwnA217C0Q~Yi$9V#H47dx}1zZIDVZw2~2gJZG;BsIK za4zu7xsLN7@O|J;;4{D>;98&tTmd|Hj^jKF+ymSUOaUJNPQDkifnNjn0fzw(*baOU zIC(an1GfWBUmcn|OfID8#oF5iZ+ESoGD$QTefB9#oIK*X{?@V#KA zm3Vy>*95;~<2Y1ek^j@DZWxKAhleec=*Pkf#4&GBNqQ=gwJ{NWC4*K$ZZ(y(q5>7+ z-JRA^>3bc1qo)!DD-cbxn$$?4W>YsgCb8KkO}v05;EkYTN;Hetcr4e7kMq}BCGIV1 zF6bT?nvQWFs<~=y5C$qpJn2{Bw)9nVyo1mhDl~)EAoPMHu4-{Ij#ZTCuDNkf4%EWz zqG&b?ceX9o#N;m7?&-TC?uA3YBbJqjZyw4>xsy!Z2C`;YmTePK@YYl!t$?093K7(( zsAOe=nh2^8nGOi^j-CbsSH?1mY}F3>k3~5dJgE$RjF&+ z8Z3HB?2$*%{xDLeUp*ObqK;OkiVlnuTj&kiTSI@2AYwMA!3un)$03ab#j(H-IJBiw%S|DC}4b7+3pLFPn`< z<6r<<3vIFisEGsW=AnX-Sm5i$MJ>YcoYRpT&Im)Bx+HD8PDjBk7*6#2wXls`!}te5 z*iq~owEA+8E!~5y_ArXDE24J?TY*|jxRNl(a-`&u^bWQPwdN(Q)t=|)jONCL>F!%p z*u0{BLg%bW=r|t@^$6rk&}Xxa6}rj$MqwbuR%>fU>+88O9Z19xY^u<==(<~eobuqiRfX&MdWMA=>WA(CU@+?q-d9DZ{VZ!UmtUD{4&ZIG~1FMeK%9&teOeLm-@>L(xmwj2X)?}2p zjxkGQNZKK$csudeac{t8aklOS9km9-YAqC%qxv)16EH|nFD@1Z3SOWLwaM@Dm&^v_vr6lnXy*gL9RhvNU25kG$m@ioQ& z_Iv%4i2Z*KJPbSpdpu-V1yC;F3&2^xU!luMfOMkk*KocY=mSf@CBS)r13U=bZv@T(&H~;* zoc|Q?1K=v43OtS&|9;?J;11w6AOPlp4+C3)za!3n8TbqE81M~X8Mqd>5I6&P0dfEH zz%#%Dz)@fia53;(#Q#qKw*sF7T;O@+0v-i^0^AMU21LM3z(;`!Faf-R{LAlvM}T9% zIlxK0IPt@=mnZULPM@uL!CKghrBpdw(`%_KWy=IE=&+3MF#MN@b0v#QeS+_f_`GRpnP3 zuw1ByKTJG!vU*7}h#S+>D{!-h%WgaDPq&v>>MH3S(?9j`;&_N(_{?6z((-QoT;@}g zL|;ZL@cN)kK)!bt=5r!JbcaEkY_Cajzb>UiBQtSj~~(j~Fxm5I#@TuR&B$&P}zCU&yvB_ZTKl0VF~ z(t`W&C|X5;L|uY<_@lx*{gp5Re|@_FctKj_)A0YnAhumrrZ|FEB9n3@N#SwSouLso zzA^D4bGsUJV!2u1Q{Z{?kSEo~Hm{wOjoj+AD55Z%sIdF$NFb3G1qX?2U|^+R=SaFE zDxRxcxZ-vPubM4fySV}fd>vGgK?fVA2^dTwDvVh2=0O7OslE>8#Ae}vi4JHSPbUZ% zAx69NtcK~_sPmqB>giA4c*0k>4*xUG`nV0&=6n2plLF%kf}hHQw-v4gEU8Dc*XDZC zVL-aa&1N~*C|Klb)z}+hjSy*sLe9!0W06K4No`-8>9>ubQa1y@JC zkbZ~=D*`*D*4iq4!Yp)nl}(5^6p9jj#^)UCbFRP8gx9Wi&?;J{$-lyFqrHK40~n}O z`I)LKSj!19VgPIrs7)JzT!sQ2J&UDX2R1=4i!OBXwCUBOErc0b$DCm(Y+l9AtW+Jc zQZR&VPGVA2B(X;$Jk&0&Sb=A$eS1x8YF@19-0&h>5GK;Mn!;P{4{b3%7ZwsI84YPT zTT4`prH?Z=pqR%av1Tn%M!*a{Zen&-3c)*BQVQ#7cJbDW;#G0oND{irtX2x9bK{yc z&GQ?XN$`S1{LsCa}SE-vpuR0!hi#j~3$%`EK z1Uivi4YKWUKBPJlek1QtoQ!WH6@)m2lHst-igM9K`e?QlI*g^ogl2MK)>&XS>!BUT zCr~acHjiELtz^RkK{T4ED8|eTVz0x_gCgg}qYO0$FE`%}L&VZfF=D!SFnEghl|uMP zd-7O^L=bDP*i)noU4`;AW%PAP>$}(7I zGR$)dE%bwAeL&d;bDuMfSzeLj|FaRlRuSJ){C_=8bo>Lc{@uVgfv*8I;7!E$F98($ ze+Re|XaK)LjDH94b%5gjC7=#Gj2QnRU=QE|RbUHnKJW_SdU^-or@)VZbzlvkcLC;r z%YYXU-~SAtJiu3h0T2QTm;v5Iy#E{E+rVdm1Hiu#@4pH>0JMP*0oMSR0}k*aV*Mw9 zM}hl*TY&-aNx%hO0S{jU4gt&wvr+yl87LVj8F&{nKwD}4C{vF}Mk4Ou7@NX1{vIQX zIm*=IyC?Ms=c=zd9(G)-#XU&y6Nk= min_tree_size) and (max_tree_size is None or len(tree) <= max_tree_size): - return tree + return tree, sampling_time else: sampling_time = rng.exponential(scale = mean_sampling_time) @@ -109,7 +109,7 @@ def _simulate_tree( if waiting_time < sampling_time: node_time_map[new_node] = waiting_time U_next.append(new_node) - if len(node_time_map) == max_tree_size + 1: + if max_tree_size is not None and len(node_time_map) == max_tree_size + 1: exit_while = True break if exit_while: @@ -172,9 +172,17 @@ def simulate_trees( sampling_times = rng.exponential(scale=mean_sampling_time, size=n_points) - trees = [ - generate_valid_tree(rng, theta=th, sampling_time=t_s, mean_sampling_time=ms, min_tree_size = min_tree_size, max_tree_size = max_tree_size) - for th, t_s, ms in zip(theta, sampling_times, mean_sampling_time) - ] + + trees, sampling_times = zip(*[ + generate_valid_tree( + rng, theta=th, sampling_time=t_s, mean_sampling_time=ms, + min_tree_size=min_tree_size, max_tree_size=max_tree_size + ) + for th, t_s, ms in zip(theta, sampling_times, mean_sampling_time) +]) + + + trees = list(trees) + sampling_times = list(sampling_times) return sampling_times, trees diff --git a/tests/trees/test_simulate.py b/tests/trees/test_simulate.py index 4c8f8df..9d5295c 100644 --- a/tests/trees/test_simulate.py +++ b/tests/trees/test_simulate.py @@ -23,14 +23,13 @@ def test_generate_valid_tree(): [0.97, 1.75, 0.00, -3.66, -1.28, 0.00, 1.66, 0.00, 0.00, -3.03], ] ) - sampling_time = 1.0 + mean_sampling_time = 1.0 + sampling_time = rng.exponential(scale = mean_sampling_time) min_tree_size = 2 - max_tree_size = 12 + max_tree_size = 11 for _ in range(10000): - tree = simulate.generate_valid_tree( - rng, theta, sampling_time, min_tree_size, max_tree_size - ) + tree, sampling_time = simulate.generate_valid_tree(rng, theta, sampling_time,mean_sampling_time, min_tree_size, max_tree_size) assert min_tree_size <= len(tree) <= max_tree_size @@ -58,10 +57,12 @@ def test_generate_tree_no_size_constraints(): [0.97, 1.75, 0.00, -3.66, -1.28, 0.00, 1.66, 0.00, 0.00, -3.03], ] ) - sampling_time = 1.0 + mean_sampling_time = 1.0 + sampling_time = rng.exponential(scale = mean_sampling_time) + for _ in range(10000): - tree = simulate.generate_valid_tree(rng, theta, sampling_time) + tree, sampling_time = simulate.generate_valid_tree(rng, theta, sampling_time, mean_sampling_time=mean_sampling_time) for node, time in tree.items(): assert time < sampling_time @@ -86,12 +87,14 @@ def test_generate_tree_no_min_size_constraint(): [0.97, 1.75, 0.00, -3.66, -1.28, 0.00, 1.66, 0.00, 0.00, -3.03], ] ) - sampling_time = 1.0 + mean_sampling_time = 1.0 + sampling_time = rng.exponential(scale = mean_sampling_time) + max_tree_size = 8 for _ in range(10000): - tree = simulate.generate_valid_tree( - rng, theta, sampling_time, max_tree_size=max_tree_size + tree, sampling_time = simulate.generate_valid_tree( + rng, theta, sampling_time, mean_sampling_time=mean_sampling_time, max_tree_size=max_tree_size ) assert len(tree) <= max_tree_size @@ -118,12 +121,14 @@ def test_generate_tree_no_max_size_constraint(): [0.97, 1.75, 0.00, -3.66, -1.28, 0.00, 1.66, 0.00, 0.00, -3.03], ] ) - sampling_time = 1.0 + mean_sampling_time = 1.0 + sampling_time = rng.exponential(scale = mean_sampling_time) + min_tree_size = 3 for _ in range(10000): - tree = simulate.generate_valid_tree( - rng, theta, sampling_time, min_tree_size=min_tree_size + tree, sampling_time = simulate.generate_valid_tree( + rng, theta, sampling_time, mean_sampling_time=mean_sampling_time, min_tree_size=min_tree_size ) assert len(tree) >= min_tree_size From a206230b87d5f22cdb5e9eee0f73dfd2596fb7f8 Mon Sep 17 00:00:00 2001 From: Laurenz Keller Date: Fri, 29 Sep 2023 13:32:34 +0200 Subject: [PATCH 12/45] reformat with black --- reformatted, 28 files left unchanged. | 905 -------------------------- src/pmhn/_trees/_simulate.py | 108 +-- tests/trees/test_simulate.py | 29 +- 3 files changed, 86 insertions(+), 956 deletions(-) delete mode 100644 reformatted, 28 files left unchanged. diff --git a/reformatted, 28 files left unchanged. b/reformatted, 28 files left unchanged. deleted file mode 100644 index 4f6792a..0000000 --- a/reformatted, 28 files left unchanged. +++ /dev/null @@ -1,905 +0,0 @@ -diff --git a/src/pmhn/_trees/_simulate.py b/src/pmhn/_trees/_simulate.py -index 6685ce9..f9e9cda 100644 ---- a/src/pmhn/_trees/_simulate.py -+++ b/src/pmhn/_trees/_simulate.py -@@ -4,10 +4,17 @@ import numpy as np -  - from pmhn._trees._interfaces import Tree -  --def generate_valid_tree(rng, theta: np.ndarray, sampling_time: float, min_tree_size: int = None, max_tree_size: int = None) -> Tree: -+ -+def generate_valid_tree( -+ rng, -+ theta: np.ndarray, -+ mean_sampling_time: float, -+ min_tree_size: int = None, -+ max_tree_size: int = None, -+) -> Tree: - """ - Generates a single valid tree with known sampling time. --  -+ - Args: - rng: random number generator - theta: real-valued (i.e., log-theta) matrix, -@@ -18,44 +25,50 @@ def generate_valid_tree(rng, theta: np.ndarray, sampling_time: float, min_tree_s - Returns: - A valid mutation tree that meets the size constraints if specified. -  -- Note:  -+ Note: - The min_tree_size and max_tree_size parameters consider the entire tree, - i.e the root node is included. - To disable the size constraints, leave min_tree_size and max_tree_size as None. -  - """ -  -- while True:  -+ while True: -+ sampling_time = rng.exponential(scale=mean_sampling_time) - tree = _simulate_tree(rng, theta, sampling_time, max_tree_size) -- if (min_tree_size is None or len(tree) >= min_tree_size) and (max_tree_size is None or len(tree) <= max_tree_size): -- return tree  --  -+ if (min_tree_size is None or len(tree) >= min_tree_size) and ( -+ max_tree_size is None or len(tree) <= max_tree_size -+ ): -+ return tree -+ -+ - def _find_possible_mutations(old_mutations: list[int], n_mutations: int) -> list[int]: - """ - Args: - old_mutations: list of ancestor mutations of a given node (including the node itself) - n_mutations: total number of mutations - Returns: -- a list of possible mutations that could appear next for a given node  --  -+ a list of possible mutations that could appear next for a given node -+ - Note: -- We assume that mutations are labeled with a number between 1 and n_mutations, -- so each element in old_mutations should be in that range (except for the root node = mutation 0).  -+ We assume that mutations are labeled with a number between 1 and n_mutations, -+ so each element in old_mutations should be in that range (except for the root node = mutation 0). - If this assumption is violated, an exception is raised. --  -+ - """ - for mutation in old_mutations: - if mutation > n_mutations or mutation < 0: -- raise ValueError(f"Invalid mutation {mutation} in old_mutations. It should be 0 <= mutation <= {n_mutations}.") -+ raise ValueError( -+ f"Invalid mutation {mutation} in old_mutations. It should be 0 <= mutation <= {n_mutations}." -+ ) -+ -+ possible_mutations = list( -+ set([i + 1 for i in range(n_mutations)]).difference(set(old_mutations)) -+ ) -+ return possible_mutations -+ -  -- possible_mutations=list(set([i+1 for i in range(n_mutations)]).difference(set(old_mutations))) -- return possible_mutations  --  - def _simulate_tree( -- rng, -- theta: np.ndarray, -- sampling_time: float, -- max_tree_size: int = None -+ rng, theta: np.ndarray, sampling_time: float, max_tree_size: int = None - ) -> Tree: - """Simulates a single tree with known sampling time. -  -@@ -64,7 +77,7 @@ def _simulate_tree( - theta: real-valued (i.e., log-theta) matrix, - shape (n_mutations, n_mutations) - sampling_time: known sampling time -- max_tree_size: maximum size of the tree -+ max_tree_size: maximum size of the tree - Returns: - a mutation tree -  -@@ -74,7 +87,7 @@ def _simulate_tree( - Appendix A1 to the TreeMHN paper - (with the difference that in the paper `Theta_{jl}` - is used, which is `Theta_{jl} = exp( theta_{jl} )`. --  -+ - If the tree is larger than max_tree_size, the function returns. - """ - # TODO(Pawel): This is part of https://github.com/cbg-ethz/pMHN/issues/14 -@@ -92,17 +105,23 @@ def _simulate_tree( - for node in U_current: - path = list(node.path) - old_mutations = [node.name for node in path] -- possible_mutations = _find_possible_mutations(old_mutations = old_mutations,n_mutations = n_mutations) -- for j in possible_mutations:  -- new_node = Node(j,parent=node) -+ possible_mutations = _find_possible_mutations( -+ old_mutations=old_mutations, n_mutations=n_mutations -+ ) -+ for j in possible_mutations: -+ new_node = Node(j, parent=node) - # Here j lies in the range of 1 to n_mutations inclusive. -- # However, Python uses 0-based indexing for arrays. Therefore, we subtract 1 from j when accessing -- # elements in the log-theta matrix to correctly map the 1-indexed mutation to the 0-indexed matrix position.  -+ # However, Python uses 0-based indexing for arrays. Therefore, we subtract 1 from j when accessing -+ # elements in the log-theta matrix to correctly map the 1-indexed mutation to the 0-indexed matrix position. - l = theta[j - 1][j - 1] -- for anc in [ancestor for ancestor in node.path if ancestor.parent is not None]: -+ for anc in [ -+ ancestor for ancestor in node.path if ancestor.parent is not None -+ ]: - l += theta[j - 1][anc.name - 1] - l = np.exp(l) - waiting_time = node_time_map[node] + rng.exponential(1.0 / l) -+ print("waiting_time shape:", np.shape(waiting_time)) -+ print("sampling_time shape:", np.shape(sampling_time)) - if waiting_time < sampling_time: - node_time_map[new_node] = waiting_time - U_next.append(new_node) -@@ -124,7 +143,7 @@ def simulate_trees( - theta: np.ndarray, - mean_sampling_time: Union[np.ndarray, float, Sequence[float]], - min_tree_size: int = None, -- max_tree_size: int = None -+ max_tree_size: int = None, - ) -> tuple[np.ndarray, list[Tree]]: - """Simulates a data set of trees with known sampling times. -  -@@ -135,9 +154,9 @@ def simulate_trees( - mean_sampling_time: the mean sampling time. - Can be a float (shared between all data point) - or an array of shape (n_points,). -- min_tree_size: minimum size of the trees -- max_tree_size: maximum size of the trees --  -+ min_tree_size: minimum size of the trees -+ max_tree_size: maximum size of the trees -+ - Returns: - sampling times, shape (n_points,) - sampled trees, list of length `n_points` -@@ -150,16 +169,6 @@ def simulate_trees( - 3, - }, "Theta should have shape (m, m) or (n_points, m, m)." -  -- # Make sure mean_sampling_time is an array of shape (n_points,) -- if isinstance(mean_sampling_time, float): -- mean_sampling_time = np.full(n_points, fill_value=mean_sampling_time) -- else: -- mean_sampling_time = np.asarray(mean_sampling_time) -- -- assert ( -- len(mean_sampling_time) == n_points -- ), "mean_sampling_time should have length n_points." -- - # Make sure theta has shape (n_points, n, n) - if len(theta.shape) == 2: - theta = np.asarray([theta for _ in range(n_points)]) -@@ -167,11 +176,15 @@ def simulate_trees( - assert theta.shape[0] == n_points, "Theta should have shape (n_points, n, n)." - assert theta.shape[1] == theta.shape[2], "Each theta should be square." -  -- sampling_times = rng.exponential(scale=mean_sampling_time, size=n_points) -- - trees = [ -- generate_valid_tree(rng, theta=th, sampling_time=t_s, min_tree_size = min_tree_size, max_tree_size = max_tree_size) -- for th, t_s in zip(theta, sampling_times) -+ generate_valid_tree( -+ rng, -+ theta=th, -+ mean_sampling_time=mean_sampling_time, -+ min_tree_size=min_tree_size, -+ max_tree_size=max_tree_size, -+ ) -+ for th in theta - ] -  -- return sampling_times, trees -+ return trees -diff --git a/tests/trees/test_simulate.py b/tests/trees/test_simulate.py -index 83865b6..4c8f8df 100644 ---- a/tests/trees/test_simulate.py -+++ b/tests/trees/test_simulate.py -@@ -1,34 +1,39 @@ - import numpy as np - import pmhn._trees._simulate as simulate - import pytest -+ -+ - def test_generate_valid_tree(): - """ - We want to test if valid trees are generated. Here we test the size requirements - and the waiting times. - """ - rng = np.random.default_rng() -- theta = np.array([[-1.41, 0.00, 0.00, 4.91, 1.03, 0.00, -1.91, -0.74, -1.35, 1.48], -- [-1.12, -2.26, 0.00, 0.82, 0.00, 0.00, 1.16, 0.00, -1.62, 0.00], -- [0.00, -0.86, -2.55, 1.58, 0.00, 0.00, 1.02, -2.70, 0.00, 0.68], -- [0.00, 0.00, 0.00, -3.69, 0.00, 0.00, -0.95, 1.42, 0.00, -1.01], -- [-3.08, -1.42, -3.14, 0.00, -3.95, 3.90, -1.46, -2.00, 0.00, 2.87], -- [-2.24, 0.00, 0.00, 0.00, 0.00, -2.38, -2.13, 1.50, 0.00, 1.35], -- [0.00, 0.00, 0.00, 0.00, 1.52, 0.00, -1.79, 0.00, 0.00, 0.00], -- [1.69, 0.76, 0.00, 1.29, 1.73, -0.82, -1.38, -4.65, 0.92, 0.00], -- [-1.22, 0.00, 0.00, 0.00, 0.65, -1.14, 0.00, 0.00, -3.25, 0.00], -- [0.97, 1.75, 0.00, -3.66, -1.28, 0.00, 1.66, 0.00, 0.00, -3.03]] --) -+ theta = np.array( -+ [ -+ [-1.41, 0.00, 0.00, 4.91, 1.03, 0.00, -1.91, -0.74, -1.35, 1.48], -+ [-1.12, -2.26, 0.00, 0.82, 0.00, 0.00, 1.16, 0.00, -1.62, 0.00], -+ [0.00, -0.86, -2.55, 1.58, 0.00, 0.00, 1.02, -2.70, 0.00, 0.68], -+ [0.00, 0.00, 0.00, -3.69, 0.00, 0.00, -0.95, 1.42, 0.00, -1.01], -+ [-3.08, -1.42, -3.14, 0.00, -3.95, 3.90, -1.46, -2.00, 0.00, 2.87], -+ [-2.24, 0.00, 0.00, 0.00, 0.00, -2.38, -2.13, 1.50, 0.00, 1.35], -+ [0.00, 0.00, 0.00, 0.00, 1.52, 0.00, -1.79, 0.00, 0.00, 0.00], -+ [1.69, 0.76, 0.00, 1.29, 1.73, -0.82, -1.38, -4.65, 0.92, 0.00], -+ [-1.22, 0.00, 0.00, 0.00, 0.65, -1.14, 0.00, 0.00, -3.25, 0.00], -+ [0.97, 1.75, 0.00, -3.66, -1.28, 0.00, 1.66, 0.00, 0.00, -3.03], -+ ] -+ ) - sampling_time = 1.0 - min_tree_size = 2 - max_tree_size = 12 -  - for _ in range(10000): -- tree = simulate.generate_valid_tree(rng, theta, sampling_time, min_tree_size, max_tree_size) --  --  -+ tree = simulate.generate_valid_tree( -+ rng, theta, sampling_time, min_tree_size, max_tree_size -+ ) -+ - assert min_tree_size <= len(tree) <= max_tree_size --  --  -+ - for node, time in tree.items(): - assert time < sampling_time -  -@@ -39,90 +44,106 @@ def test_generate_tree_no_size_constraints(): - """ -  - rng = np.random.default_rng() -- theta = np.array([[-1.41, 0.00, 0.00, 4.91, 1.03, 0.00, -1.91, -0.74, -1.35, 1.48], -- [-1.12, -2.26, 0.00, 0.82, 0.00, 0.00, 1.16, 0.00, -1.62, 0.00], -- [0.00, -0.86, -2.55, 1.58, 0.00, 0.00, 1.02, -2.70, 0.00, 0.68], -- [0.00, 0.00, 0.00, -3.69, 0.00, 0.00, -0.95, 1.42, 0.00, -1.01], -- [-3.08, -1.42, -3.14, 0.00, -3.95, 3.90, -1.46, -2.00, 0.00, 2.87], -- [-2.24, 0.00, 0.00, 0.00, 0.00, -2.38, -2.13, 1.50, 0.00, 1.35], -- [0.00, 0.00, 0.00, 0.00, 1.52, 0.00, -1.79, 0.00, 0.00, 0.00], -- [1.69, 0.76, 0.00, 1.29, 1.73, -0.82, -1.38, -4.65, 0.92, 0.00], -- [-1.22, 0.00, 0.00, 0.00, 0.65, -1.14, 0.00, 0.00, -3.25, 0.00], -- [0.97, 1.75, 0.00, -3.66, -1.28, 0.00, 1.66, 0.00, 0.00, -3.03]] --) -+ theta = np.array( -+ [ -+ [-1.41, 0.00, 0.00, 4.91, 1.03, 0.00, -1.91, -0.74, -1.35, 1.48], -+ [-1.12, -2.26, 0.00, 0.82, 0.00, 0.00, 1.16, 0.00, -1.62, 0.00], -+ [0.00, -0.86, -2.55, 1.58, 0.00, 0.00, 1.02, -2.70, 0.00, 0.68], -+ [0.00, 0.00, 0.00, -3.69, 0.00, 0.00, -0.95, 1.42, 0.00, -1.01], -+ [-3.08, -1.42, -3.14, 0.00, -3.95, 3.90, -1.46, -2.00, 0.00, 2.87], -+ [-2.24, 0.00, 0.00, 0.00, 0.00, -2.38, -2.13, 1.50, 0.00, 1.35], -+ [0.00, 0.00, 0.00, 0.00, 1.52, 0.00, -1.79, 0.00, 0.00, 0.00], -+ [1.69, 0.76, 0.00, 1.29, 1.73, -0.82, -1.38, -4.65, 0.92, 0.00], -+ [-1.22, 0.00, 0.00, 0.00, 0.65, -1.14, 0.00, 0.00, -3.25, 0.00], -+ [0.97, 1.75, 0.00, -3.66, -1.28, 0.00, 1.66, 0.00, 0.00, -3.03], -+ ] -+ ) - sampling_time = 1.0 --  -+ - for _ in range(10000): - tree = simulate.generate_valid_tree(rng, theta, sampling_time) --  -+ - for node, time in tree.items(): - assert time < sampling_time -  -+ - def test_generate_tree_no_min_size_constraint(): - """ - Here we test the case where min_tree_size is None but max_tree_size is specified. - """ - rng = np.random.default_rng() -- theta = np.array([[-1.41, 0.00, 0.00, 4.91, 1.03, 0.00, -1.91, -0.74, -1.35, 1.48], -- [-1.12, -2.26, 0.00, 0.82, 0.00, 0.00, 1.16, 0.00, -1.62, 0.00], -- [0.00, -0.86, -2.55, 1.58, 0.00, 0.00, 1.02, -2.70, 0.00, 0.68], -- [0.00, 0.00, 0.00, -3.69, 0.00, 0.00, -0.95, 1.42, 0.00, -1.01], -- [-3.08, -1.42, -3.14, 0.00, -3.95, 3.90, -1.46, -2.00, 0.00, 2.87], -- [-2.24, 0.00, 0.00, 0.00, 0.00, -2.38, -2.13, 1.50, 0.00, 1.35], -- [0.00, 0.00, 0.00, 0.00, 1.52, 0.00, -1.79, 0.00, 0.00, 0.00], -- [1.69, 0.76, 0.00, 1.29, 1.73, -0.82, -1.38, -4.65, 0.92, 0.00], -- [-1.22, 0.00, 0.00, 0.00, 0.65, -1.14, 0.00, 0.00, -3.25, 0.00], -- [0.97, 1.75, 0.00, -3.66, -1.28, 0.00, 1.66, 0.00, 0.00, -3.03]] --) -+ theta = np.array( -+ [ -+ [-1.41, 0.00, 0.00, 4.91, 1.03, 0.00, -1.91, -0.74, -1.35, 1.48], -+ [-1.12, -2.26, 0.00, 0.82, 0.00, 0.00, 1.16, 0.00, -1.62, 0.00], -+ [0.00, -0.86, -2.55, 1.58, 0.00, 0.00, 1.02, -2.70, 0.00, 0.68], -+ [0.00, 0.00, 0.00, -3.69, 0.00, 0.00, -0.95, 1.42, 0.00, -1.01], -+ [-3.08, -1.42, -3.14, 0.00, -3.95, 3.90, -1.46, -2.00, 0.00, 2.87], -+ [-2.24, 0.00, 0.00, 0.00, 0.00, -2.38, -2.13, 1.50, 0.00, 1.35], -+ [0.00, 0.00, 0.00, 0.00, 1.52, 0.00, -1.79, 0.00, 0.00, 0.00], -+ [1.69, 0.76, 0.00, 1.29, 1.73, -0.82, -1.38, -4.65, 0.92, 0.00], -+ [-1.22, 0.00, 0.00, 0.00, 0.65, -1.14, 0.00, 0.00, -3.25, 0.00], -+ [0.97, 1.75, 0.00, -3.66, -1.28, 0.00, 1.66, 0.00, 0.00, -3.03], -+ ] -+ ) - sampling_time = 1.0 - max_tree_size = 8 --  -+ - for _ in range(10000): -- tree = simulate.generate_valid_tree(rng, theta, sampling_time, max_tree_size=max_tree_size) -+ tree = simulate.generate_valid_tree( -+ rng, theta, sampling_time, max_tree_size=max_tree_size -+ ) - assert len(tree) <= max_tree_size --  -+ - for node, time in tree.items(): - assert time < sampling_time -  -+ - def test_generate_tree_no_max_size_constraint(): - """ - Here we test the case where max_tree_size is None but min_tree_size is specified. - """ - rng = np.random.default_rng() -- theta = np.array([[-1.41, 0.00, 0.00, 4.91, 1.03, 0.00, -1.91, -0.74, -1.35, 1.48], -- [-1.12, -2.26, 0.00, 0.82, 0.00, 0.00, 1.16, 0.00, -1.62, 0.00], -- [0.00, -0.86, -2.55, 1.58, 0.00, 0.00, 1.02, -2.70, 0.00, 0.68], -- [0.00, 0.00, 0.00, -3.69, 0.00, 0.00, -0.95, 1.42, 0.00, -1.01], -- [-3.08, -1.42, -3.14, 0.00, -3.95, 3.90, -1.46, -2.00, 0.00, 2.87], -- [-2.24, 0.00, 0.00, 0.00, 0.00, -2.38, -2.13, 1.50, 0.00, 1.35], -- [0.00, 0.00, 0.00, 0.00, 1.52, 0.00, -1.79, 0.00, 0.00, 0.00], -- [1.69, 0.76, 0.00, 1.29, 1.73, -0.82, -1.38, -4.65, 0.92, 0.00], -- [-1.22, 0.00, 0.00, 0.00, 0.65, -1.14, 0.00, 0.00, -3.25, 0.00], -- [0.97, 1.75, 0.00, -3.66, -1.28, 0.00, 1.66, 0.00, 0.00, -3.03]] --) -+ theta = np.array( -+ [ -+ [-1.41, 0.00, 0.00, 4.91, 1.03, 0.00, -1.91, -0.74, -1.35, 1.48], -+ [-1.12, -2.26, 0.00, 0.82, 0.00, 0.00, 1.16, 0.00, -1.62, 0.00], -+ [0.00, -0.86, -2.55, 1.58, 0.00, 0.00, 1.02, -2.70, 0.00, 0.68], -+ [0.00, 0.00, 0.00, -3.69, 0.00, 0.00, -0.95, 1.42, 0.00, -1.01], -+ [-3.08, -1.42, -3.14, 0.00, -3.95, 3.90, -1.46, -2.00, 0.00, 2.87], -+ [-2.24, 0.00, 0.00, 0.00, 0.00, -2.38, -2.13, 1.50, 0.00, 1.35], -+ [0.00, 0.00, 0.00, 0.00, 1.52, 0.00, -1.79, 0.00, 0.00, 0.00], -+ [1.69, 0.76, 0.00, 1.29, 1.73, -0.82, -1.38, -4.65, 0.92, 0.00], -+ [-1.22, 0.00, 0.00, 0.00, 0.65, -1.14, 0.00, 0.00, -3.25, 0.00], -+ [0.97, 1.75, 0.00, -3.66, -1.28, 0.00, 1.66, 0.00, 0.00, -3.03], -+ ] -+ ) - sampling_time = 1.0 - min_tree_size = 3 --  -+ - for _ in range(10000): -- tree = simulate.generate_valid_tree(rng, theta, sampling_time, min_tree_size=min_tree_size) --  -+ tree = simulate.generate_valid_tree( -+ rng, theta, sampling_time, min_tree_size=min_tree_size -+ ) -+ - assert len(tree) >= min_tree_size --  -+ - for node, time in tree.items(): - assert time < sampling_time --  -+ -+ - def test_find_possible_mutations_normal(): - """ -- We want to test if the possible_mutations list is correct.  -+ We want to test if the possible_mutations list is correct. - """ - old_mutations = [7, 2, 5, 9] - n_mutations = 10 - possible_mutations = simulate._find_possible_mutations(old_mutations, n_mutations) --  -+ - assert possible_mutations == [1, 3, 4, 6, 8, 10] --  --def test_find_possible_mutations_edge():  -  -+ -+def test_find_possible_mutations_edge(): - """ - We want to test if the possible_mutations list is correct. old_mutations with mutations on the edge. - """ -@@ -130,31 +151,35 @@ def test_find_possible_mutations_edge(): - old_mutations = [1, 10] - n_mutations = 10 - possible_mutations = simulate._find_possible_mutations(old_mutations, n_mutations) --  -+ - assert possible_mutations == [i for i in range(2, 10)] --  --def test_find_possible_mutations_except_positive():  -+ -+ -+def test_find_possible_mutations_except_positive(): - """ - We want to test if an exception is correctly thrown when the old_mutations list is invalid (mutation number too large). -- """  -+ """ - old_mutations = [212, 1, 3, 7] - n_mutations = 10 - with pytest.raises(ValueError) as excinfo: -- simulate._find_possible_mutations(old_mutations, n_mutations) --  -- assert str(excinfo.value) == "Invalid mutation 212 in old_mutations. It should be 0 <= mutation <= 10." --  --def test_find_possible_mutations_except_negative():  -+ simulate._find_possible_mutations(old_mutations, n_mutations) -+ -+ assert ( -+ str(excinfo.value) -+ == "Invalid mutation 212 in old_mutations. It should be 0 <= mutation <= 10." -+ ) -+ -+ -+def test_find_possible_mutations_except_negative(): - """ - We want to test if an exception is correctly thrown when the old_mutations list is invalid (mutation number too small). -- """  -+ """ - old_mutations = [-23, 0, 5] - n_mutations = 10 - with pytest.raises(ValueError) as excinfo: -- simulate._find_possible_mutations(old_mutations, n_mutations) --  -- assert str(excinfo.value) == "Invalid mutation -23 in old_mutations. It should be 0 <= mutation <= 10." --  --  --  -+ simulate._find_possible_mutations(old_mutations, n_mutations) -  -+ assert ( -+ str(excinfo.value) -+ == "Invalid mutation -23 in old_mutations. It should be 0 <= mutation <= 10." -+ ) -diff --git a/warmup/children.py b/warmup/children.py -index 6ec7914..d22c176 100644 ---- a/warmup/children.py -+++ b/warmup/children.py -@@ -9,8 +9,7 @@ F = Node("F", parent=C) -  - all_nodes = list(PreOrderIter(A)) - print(RenderTree(A)) --children={} -+children = {} - for node in all_nodes: - children[node.name] = [child.name for child in node.children] --print(children)  -- -+print(children) -diff --git a/warmup/plot_all_mut_freq.py b/warmup/plot_all_mut_freq.py -index b3d14e9..15b05fb 100644 ---- a/warmup/plot_all_mut_freq.py -+++ b/warmup/plot_all_mut_freq.py -@@ -2,32 +2,63 @@ import pandas as pd - import matplotlib.pyplot as plt -  - paths = { -- 2000: ('/home/laukeller/BSc Thesis/TreeMHN/Example/trees_2000.csv', '/home/laukeller/BSc Thesis/pMHN/src/pmhn/_trees/trees_2000.csv'), -- 5000: ('/home/laukeller/BSc Thesis/TreeMHN/Example/trees_5000.csv', '/home/laukeller/BSc Thesis/pMHN/src/pmhn/_trees/trees_5000.csv'), -- 10000: ('/home/laukeller/BSc Thesis/TreeMHN/Example/trees_10000.csv', '/home/laukeller/BSc Thesis/pMHN/src/pmhn/_trees/trees_10000.csv'), -- 50000: ('/home/laukeller/BSc Thesis/TreeMHN/Example/trees_50000.csv', '/home/laukeller/BSc Thesis/pMHN/src/pmhn/_trees/trees_50000.csv'), -+ 500: ( -+ "/home/laukeller/BSc Thesis/TreeMHN/Example/trees_500.csv", -+ "/home/laukeller/BSc Thesis/pMHN/warmup/trees_500.csv", -+ ), -+ 5000: ( -+ "/home/laukeller/BSc Thesis/TreeMHN/Example/trees_5000.csv", -+ "/home/laukeller/BSc Thesis/pMHN/warmup/trees_5000.csv", -+ ), -+ 10000: ( -+ "/home/laukeller/BSc Thesis/TreeMHN/Example/trees_10000.csv", -+ "/home/laukeller/BSc Thesis/pMHN/warmup/trees_10000.csv", -+ ), -+ 50000: ( -+ "/home/laukeller/BSc Thesis/TreeMHN/Example/trees_50000.csv", -+ "/home/laukeller/BSc Thesis/pMHN/warmup/trees_50000.csv", -+ ), - } -  - fig, axs = plt.subplots(2, 2, figsize=(10, 10)) # 2x2 grid of subplots --axs = axs.ravel()  -+axs = axs.ravel() -  - for i, (num_trees, (r_path, python_path)) in enumerate(paths.items()): - r_trees = pd.read_csv(r_path) - python_trees = pd.read_csv(python_path) --  -- r_mutation_frequencies = r_trees['Mutation_ID'].value_counts().sort_index() -- python_mutation_frequencies = python_trees['Mutation_ID'].value_counts().sort_index() --  -+ -+ r_mutation_frequencies = r_trees["Mutation_ID"].value_counts().sort_index() -+ python_mutation_frequencies = ( -+ python_trees["Mutation_ID"].value_counts().sort_index() -+ ) -+ - bar_width = 0.35 --  -- r_mutation_frequencies.plot(kind='bar', width=bar_width, position=0, align='center', color='b', alpha=0.5, label='R Trees', ax=axs[i]) -- python_mutation_frequencies.plot(kind='bar', width=bar_width, position=1, align='center', color='r', alpha=0.5, label='Python Trees', ax=axs[i]) --  -- axs[i].set_xlabel('Mutation ID') -- axs[i].set_ylabel('Frequency') -- axs[i].legend(loc='upper right') -- axs[i].set_title(f'Mutation Frequency Distribution ({num_trees} trees)') -- --plt.tight_layout()  --plt.show() -  -+ r_mutation_frequencies.plot( -+ kind="bar", -+ width=bar_width, -+ position=0, -+ align="center", -+ color="b", -+ alpha=0.5, -+ label="R Trees", -+ ax=axs[i], -+ ) -+ python_mutation_frequencies.plot( -+ kind="bar", -+ width=bar_width, -+ position=1, -+ align="center", -+ color="r", -+ alpha=0.5, -+ label="Python Trees", -+ ax=axs[i], -+ ) -+ -+ axs[i].set_xlabel("Mutation ID") -+ axs[i].set_ylabel("Frequency") -+ axs[i].legend(loc="upper right") -+ axs[i].set_title(f"Mutation Frequency Distribution ({num_trees} trees)") -+ -+plt.tight_layout() -+plt.show() -diff --git a/warmup/plot_all_trees.py b/warmup/plot_all_trees.py -index acc932b..1120bb5 100644 ---- a/warmup/plot_all_trees.py -+++ b/warmup/plot_all_trees.py -@@ -3,12 +3,25 @@ import matplotlib.pyplot as plt -  -  - paths = { -- 2000: ('/home/laukeller/BSc Thesis/TreeMHN/Example/trees_2000.csv', '/home/laukeller/BSc Thesis/pMHN/src/pmhn/_trees/trees_2000.csv'), -- 5000: ('/home/laukeller/BSc Thesis/TreeMHN/Example/trees_5000.csv', '/home/laukeller/BSc Thesis/pMHN/src/pmhn/_trees/trees_5000.csv'), -- 10000: ('/home/laukeller/BSc Thesis/TreeMHN/Example/trees_10000.csv', '/home/laukeller/BSc Thesis/pMHN/src/pmhn/_trees/trees_10000.csv'), -- 50000: ('/home/laukeller/BSc Thesis/TreeMHN/Example/trees_50000.csv', '/home/laukeller/BSc Thesis/pMHN/src/pmhn/_trees/trees_50000.csv'), -+ 500: ( -+ "/home/laukeller/BSc Thesis/TreeMHN/Example/trees_500.csv", -+ "/home/laukeller/BSc Thesis/pMHN/warmup/trees_500.csv", -+ ), -+ 5000: ( -+ "/home/laukeller/BSc Thesis/TreeMHN/Example/trees_5000.csv", -+ "/home/laukeller/BSc Thesis/pMHN/warmup/trees_5000.csv", -+ ), -+ 10000: ( -+ "/home/laukeller/BSc Thesis/TreeMHN/Example/trees_10000.csv", -+ "/home/laukeller/BSc Thesis/pMHN/warmup/trees_10000.csv", -+ ), -+ 50000: ( -+ "/home/laukeller/BSc Thesis/TreeMHN/Example/trees_50000.csv", -+ "/home/laukeller/BSc Thesis/pMHN/warmup/trees_50000.csv", -+ ), - } -  -+ - fig, axs = plt.subplots(2, 2, figsize=(10, 10)) # 2x2 grid of subplots - axs = axs.ravel() -  -@@ -17,17 +30,16 @@ bin_edges = [x - 0.5 for x in range(2, 12)] + [11.5] # [1.5, 2.5, ..., 10.5, 11 - for i, (num_trees, (r_path, python_path)) in enumerate(paths.items()): - r_trees = pd.read_csv(r_path) - python_trees = pd.read_csv(python_path) --  -- r_tree_sizes = r_trees.groupby('Tree_ID').size() -- python_tree_sizes = python_trees.groupby('Tree_ID').size() --  -- axs[i].hist(r_tree_sizes, bins=bin_edges, alpha=0.5, label='R Trees') -- axs[i].hist(python_tree_sizes, bins=bin_edges, alpha=0.5, label='Python Trees') -- axs[i].set_xlabel('Tree Size') -- axs[i].set_ylabel('Frequency') -- axs[i].legend(loc='upper right') -- axs[i].set_title(f'Tree Size Distribution ({num_trees} trees)') -- --plt.tight_layout()  --plt.show() -  -+ r_tree_sizes = r_trees.groupby("Tree_ID").size() -+ python_tree_sizes = python_trees.groupby("Tree_ID").size() -+ -+ axs[i].hist(r_tree_sizes, bins=bin_edges, alpha=0.5, label="R Trees") -+ axs[i].hist(python_tree_sizes, bins=bin_edges, alpha=0.5, label="Python Trees") -+ axs[i].set_xlabel("Tree Size") -+ axs[i].set_ylabel("Frequency") -+ axs[i].legend(loc="upper right") -+ axs[i].set_title(f"Tree Size Distribution ({num_trees} trees)") -+ -+plt.tight_layout() -+plt.show() -diff --git a/warmup/plot_mutation_freq.py b/warmup/plot_mutation_freq.py -index 0bdfc28..52962b0 100644 ---- a/warmup/plot_mutation_freq.py -+++ b/warmup/plot_mutation_freq.py -@@ -1,27 +1,44 @@ - import pandas as pd - import matplotlib.pyplot as plt -  --r_trees_path ='/home/laukeller/BSc Thesis/TreeMHN/Example/trees_10000.csv' --python_trees_path ='/home/laukeller/BSc Thesis/pMHN/src/pmhn/_trees/trees_10000.csv' -+r_trees_path = "/home/laukeller/BSc Thesis/TreeMHN/Example/trees_10000.csv" -+python_trees_path = "/home/laukeller/BSc Thesis/pMHN/src/pmhn/_trees/trees_10000.csv" -  - r_trees = pd.read_csv(r_trees_path) - python_trees = pd.read_csv(python_trees_path) -  --r_mutation_frequencies = r_trees['Mutation_ID'].value_counts().sort_index() --python_mutation_frequencies = python_trees['Mutation_ID'].value_counts().sort_index() -+r_mutation_frequencies = r_trees["Mutation_ID"].value_counts().sort_index() -+python_mutation_frequencies = python_trees["Mutation_ID"].value_counts().sort_index() -  - fig, ax = plt.subplots(figsize=(10, 6)) -  - bar_width = 0.35 -  --r_mutation_frequencies.plot(kind='bar', width=bar_width, position=0, align='center', color='b', alpha=0.5, label='R Trees', ax=ax) --python_mutation_frequencies.plot(kind='bar', width=bar_width, position=1, align='center', color='r', alpha=0.5, label='Python Trees', ax=ax) -- --ax.set_xlabel('Mutation ID') --ax.set_ylabel('Frequency') --ax.set_title('Mutation Frequencies Comparison') -+r_mutation_frequencies.plot( -+ kind="bar", -+ width=bar_width, -+ position=0, -+ align="center", -+ color="b", -+ alpha=0.5, -+ label="R Trees", -+ ax=ax, -+) -+python_mutation_frequencies.plot( -+ kind="bar", -+ width=bar_width, -+ position=1, -+ align="center", -+ color="r", -+ alpha=0.5, -+ label="Python Trees", -+ ax=ax, -+) -+ -+ax.set_xlabel("Mutation ID") -+ax.set_ylabel("Frequency") -+ax.set_title("Mutation Frequencies Comparison") - ax.legend() -  - plt.tight_layout() - plt.show() -- -diff --git a/warmup/plot_trees.py b/warmup/plot_trees.py -index 73dd895..d1400e9 100644 ---- a/warmup/plot_trees.py -+++ b/warmup/plot_trees.py -@@ -1,21 +1,22 @@ - import pandas as pd - import matplotlib.pyplot as plt -  --r_trees_path = '/home/laukeller/BSc Thesis/TreeMHN/Example/trees_10000.csv' --python_trees_path = '/home/laukeller/BSc Thesis/pMHN/src/pmhn/_trees/trees_10000.csv' -+r_trees_path = "/home/laukeller/BSc Thesis/TreeMHN/Example/trees_10000.csv" -+python_trees_path = "/home/laukeller/BSc Thesis/pMHN/src/pmhn/_trees/trees_10000.csv" -  - r_trees = pd.read_csv(r_trees_path) - python_trees = pd.read_csv(python_trees_path) -  --r_tree_sizes = r_trees.groupby('Tree_ID').size() --python_tree_sizes = python_trees.groupby('Tree_ID').size() -+r_tree_sizes = r_trees.groupby("Tree_ID").size() -+python_tree_sizes = python_trees.groupby("Tree_ID").size() -  --bin_edges = [x - 0.5 for x in range(2, 12)] + [11.5]  --plt.hist(r_tree_sizes, bins=bin_edges, alpha=0.5, edgecolor='k', label='R Trees') --plt.hist(python_tree_sizes, bins=bin_edges, alpha=0.5, edgecolor='k', label='Python Trees') --plt.xlabel('Tree Size') --plt.ylabel('Frequency') --plt.legend(loc='upper right') --plt.title('Tree Size Distribution') -+bin_edges = [x - 0.5 for x in range(2, 12)] + [11.5] -+plt.hist(r_tree_sizes, bins=bin_edges, alpha=0.5, edgecolor="k", label="R Trees") -+plt.hist( -+ python_tree_sizes, bins=bin_edges, alpha=0.5, edgecolor="k", label="Python Trees" -+) -+plt.xlabel("Tree Size") -+plt.ylabel("Frequency") -+plt.legend(loc="upper right") -+plt.title("Tree Size Distribution") - plt.show() -- -diff --git a/warmup/subtree.py b/warmup/subtree.py -index 0be796e..14ed810 100644 ---- a/warmup/subtree.py -+++ b/warmup/subtree.py -@@ -1,52 +1,61 @@ --from anytree import Node, RenderTree  -+from anytree import Node, RenderTree - from itertools import combinations, product --''' -+ -+""" - takes a variable number of lists as input and returns a list containing all possible combination of the input lists - in this case: takes a list of lists of subtrees (for each child one list of subtrees) as input, a subtree itself is a list of nodes - outputs all combinations of subtrees  --''' -+""" -+ -+ - def all_combinations_of_elements(*lists): - n = len(lists) - all_combinations = [] -  -- for r in range(1, n+1): -+ for r in range(1, n + 1): - for list_combination in combinations(lists, r): - for element_combination in product(*list_combination): -- all_combinations.append(list(element_combination))  -+ all_combinations.append(list(element_combination)) -  - return all_combinations -  --'''creates a subtree given a subtree (nodes_list) and the root node''' -+ -+"""creates a subtree given a subtree (nodes_list) and the root node""" -+ -  - def create_subtree(original_root, nodes_list): - nodes_dict = {} --  -+ - for node in [original_root] + list(original_root.descendants): - if node in nodes_list: - parent_node = next((n for n in nodes_list if n is node.parent), None) - nodes_dict[node] = Node(node.name, parent=nodes_dict.get(parent_node)) --  -+ - return nodes_dict.get(original_root) -  --'''returns a list of all subtrees of a tree, input is the root node -+ -+"""returns a list of all subtrees of a tree, input is the root node - a recursive approach is used: if one knows the subtrees of the children of the root node, - then one can find all combinations of the subtrees of the children and add the root node to each one of these combinations, --this way one obtains all subtrees of the root node''' -+this way one obtains all subtrees of the root node""" -+ -  - def subtrees(node): - if not node.children: - return [[node]] --  -+ - child_subtrees = [subtrees(child) for child in node.children] --  -+ - combined_subtrees = all_combinations_of_elements(*child_subtrees) --  -+ - result_subtrees = [] - result_subtrees.append([node]) - for combination in combined_subtrees: -- subtree_with_root = [node] + [item for sublist in combination for item in sublist]  -+ subtree_with_root = [node] + [ -+ item for sublist in combination for item in sublist -+ ] - result_subtrees.append(subtree_with_root) --  -+ - return result_subtrees -  -  -@@ -61,13 +70,13 @@ all_node_lists = subtrees(A) - all_node_lists = sorted(all_node_lists, key=len) - print(all_node_lists) - print("\n") --all_subtrees=[] -+all_subtrees = [] -  - for nodes_list in all_node_lists: -- subtree=create_subtree(A,nodes_list) -+ subtree = create_subtree(A, nodes_list) - all_subtrees.append(subtree) - i = 1 - for subtree in all_subtrees: - print(f"{i}. ") - print(RenderTree(subtree)) -- i += 1  -+ i += 1 -diff --git a/warmup/write_csv.py b/warmup/write_csv.py -index a5b888d..a06c8f3 100644 ---- a/warmup/write_csv.py -+++ b/warmup/write_csv.py -@@ -1,20 +1,24 @@ - import csv - import numpy as np - from anytree import Node --import _simulate  -+import pmhn._trees._simulate as _simulate -+ -  - def csv_to_numpy(file_path): -- with open(file_path, 'r') as file: -+ with open(file_path, "r") as file: - reader = csv.reader(file) - next(reader) - data_list = list(reader) - return np.array(data_list, dtype=float) -  -+ - def write_trees_to_csv(trees, output_file_path): -- with open(output_file_path, 'w', newline='') as file: -+ with open(output_file_path, "w", newline="") as file: - writer = csv.writer(file) -- writer.writerow(["Patient_ID", "Tree_ID", "Node_ID", "Mutation_ID", "Parent_ID"]) --  -+ writer.writerow( -+ ["Patient_ID", "Tree_ID", "Node_ID", "Mutation_ID", "Parent_ID"] -+ ) -+ - patient_id = 0 - for tree_dict in trees: - patient_id += 1 -@@ -26,22 +30,29 @@ def write_trees_to_csv(trees, output_file_path): - parent_id = node.parent.name if node.parent else node_id - writer.writerow([patient_id, tree_id, node_id, mutation_id, parent_id]) -  -+ - if __name__ == "__main__": - mhn_file_path = "/home/laukeller/BSc Thesis/TreeMHN/Example/MHN_Matrix.csv" - mhn_array = csv_to_numpy(mhn_file_path) - print(mhn_array) --  -+ - rng = np.random.default_rng() -- theta = mhn_array  -- mean_sampling_time = 1.0  --  -- tree_counts = [2000, 5000, 10000, 50000] --  -+ theta = mhn_array -+ mean_sampling_time = 1.0 -+ -+ tree_counts = [500, 5000, 10000, 50000] -+ - min_tree_size = 2 - max_tree_size = 11 - for n_points in tree_counts: -- trees_file_path = f"/home/laukeller/BSc Thesis/pMHN/src/pmhn/_trees/trees_{n_points}.csv" --  -- _, trees = _simulate.simulate_trees(rng, n_points, theta, mean_sampling_time, min_tree_size = min_tree_size, max_tree_size = max_tree_size) -- write_trees_to_csv(trees, trees_file_path) -+ trees_file_path = f"/home/laukeller/BSc Thesis/pMHN/warmup/trees_{n_points}.csv" -  -+ trees = _simulate.simulate_trees( -+ rng, -+ n_points, -+ theta, -+ mean_sampling_time, -+ min_tree_size=min_tree_size, -+ max_tree_size=max_tree_size, -+ ) -+ write_trees_to_csv(trees, trees_file_path) diff --git a/src/pmhn/_trees/_simulate.py b/src/pmhn/_trees/_simulate.py index 924ad7a..4b6c9fa 100644 --- a/src/pmhn/_trees/_simulate.py +++ b/src/pmhn/_trees/_simulate.py @@ -4,10 +4,18 @@ from pmhn._trees._interfaces import Tree -def generate_valid_tree(rng, theta: np.ndarray, sampling_time: float, mean_sampling_time: float, min_tree_size: int = None, max_tree_size: int = None) -> Tree: + +def generate_valid_tree( + rng, + theta: np.ndarray, + sampling_time: float, + mean_sampling_time: float, + min_tree_size: int = None, + max_tree_size: int = None, +) -> Tree: """ Generates a single valid tree with known sampling time. - + Args: rng: random number generator theta: real-valued (i.e., log-theta) matrix, @@ -19,46 +27,51 @@ def generate_valid_tree(rng, theta: np.ndarray, sampling_time: float, mean_sampl Returns: A valid mutation tree that meets the size constraints if specified. - Note: + Note: The min_tree_size and max_tree_size parameters consider the entire tree, i.e the root node is included. To disable the size constraints, leave min_tree_size and max_tree_size as None. If a tree is discarded, a new sampling time is drawn. """ - while True: + while True: tree = _simulate_tree(rng, theta, sampling_time, max_tree_size) - if (min_tree_size is None or len(tree) >= min_tree_size) and (max_tree_size is None or len(tree) <= max_tree_size): - return tree, sampling_time + if (min_tree_size is None or len(tree) >= min_tree_size) and ( + max_tree_size is None or len(tree) <= max_tree_size + ): + return tree, sampling_time else: - sampling_time = rng.exponential(scale = mean_sampling_time) - + sampling_time = rng.exponential(scale=mean_sampling_time) + + def _find_possible_mutations(old_mutations: list[int], n_mutations: int) -> list[int]: """ Args: old_mutations: list of ancestor mutations of a given node (including the node itself) n_mutations: total number of mutations Returns: - a list of possible mutations that could appear next for a given node - + a list of possible mutations that could appear next for a given node + Note: - We assume that mutations are labeled with a number between 1 and n_mutations, - so each element in old_mutations should be in that range (except for the root node = mutation 0). + We assume that mutations are labeled with a number between 1 and n_mutations, + so each element in old_mutations should be in that range (except for the root node = mutation 0). If this assumption is violated, an exception is raised. - + """ for mutation in old_mutations: if mutation > n_mutations or mutation < 0: - raise ValueError(f"Invalid mutation {mutation} in old_mutations. It should be 0 <= mutation <= {n_mutations}.") + raise ValueError( + f"Invalid mutation {mutation} in old_mutations. It should be 0 <= mutation <= {n_mutations}." + ) + + possible_mutations = list( + set([i + 1 for i in range(n_mutations)]).difference(set(old_mutations)) + ) + return possible_mutations + - possible_mutations=list(set([i+1 for i in range(n_mutations)]).difference(set(old_mutations))) - return possible_mutations - def _simulate_tree( - rng, - theta: np.ndarray, - sampling_time: float, - max_tree_size: int = None + rng, theta: np.ndarray, sampling_time: float, max_tree_size: int = None ) -> Tree: """Simulates a single tree with known sampling time. @@ -67,7 +80,7 @@ def _simulate_tree( theta: real-valued (i.e., log-theta) matrix, shape (n_mutations, n_mutations) sampling_time: known sampling time - max_tree_size: maximum size of the tree + max_tree_size: maximum size of the tree Returns: a mutation tree @@ -77,7 +90,7 @@ def _simulate_tree( Appendix A1 to the TreeMHN paper (with the difference that in the paper `Theta_{jl}` is used, which is `Theta_{jl} = exp( theta_{jl} )`. - + If the tree is larger than max_tree_size, the function returns immediately. """ # TODO(Pawel): This is part of https://github.com/cbg-ethz/pMHN/issues/14 @@ -95,21 +108,28 @@ def _simulate_tree( for node in U_current: path = list(node.path) old_mutations = [node.name for node in path] - possible_mutations = _find_possible_mutations(old_mutations = old_mutations,n_mutations = n_mutations) - for j in possible_mutations: - new_node = Node(j,parent=node) + possible_mutations = _find_possible_mutations( + old_mutations=old_mutations, n_mutations=n_mutations + ) + for j in possible_mutations: + new_node = Node(j, parent=node) # Here j lies in the range of 1 to n_mutations inclusive. - # However, Python uses 0-based indexing for arrays. Therefore, we subtract 1 from j when accessing - # elements in the log-theta matrix to correctly map the 1-indexed mutation to the 0-indexed matrix position. + # However, Python uses 0-based indexing for arrays. Therefore, we subtract 1 from j when accessing + # elements in the log-theta matrix to correctly map the 1-indexed mutation to the 0-indexed matrix position. l = theta[j - 1][j - 1] - for anc in [ancestor for ancestor in node.path if ancestor.parent is not None]: + for anc in [ + ancestor for ancestor in node.path if ancestor.parent is not None + ]: l += theta[j - 1][anc.name - 1] l = np.exp(l) waiting_time = node_time_map[node] + rng.exponential(1.0 / l) if waiting_time < sampling_time: node_time_map[new_node] = waiting_time U_next.append(new_node) - if max_tree_size is not None and len(node_time_map) == max_tree_size + 1: + if ( + max_tree_size is not None + and len(node_time_map) == max_tree_size + 1 + ): exit_while = True break if exit_while: @@ -127,7 +147,7 @@ def simulate_trees( theta: np.ndarray, mean_sampling_time: Union[np.ndarray, float, Sequence[float]], min_tree_size: int = None, - max_tree_size: int = None + max_tree_size: int = None, ) -> tuple[np.ndarray, list[Tree]]: """Simulates a data set of trees with known sampling times. @@ -138,9 +158,9 @@ def simulate_trees( mean_sampling_time: the mean sampling time. Can be a float (shared between all data point) or an array of shape (n_points,). - min_tree_size: minimum size of the trees - max_tree_size: maximum size of the trees - + min_tree_size: minimum size of the trees + max_tree_size: maximum size of the trees + Returns: sampling times, shape (n_points,) sampled trees, list of length `n_points` @@ -172,16 +192,20 @@ def simulate_trees( sampling_times = rng.exponential(scale=mean_sampling_time, size=n_points) - - trees, sampling_times = zip(*[ - generate_valid_tree( - rng, theta=th, sampling_time=t_s, mean_sampling_time=ms, - min_tree_size=min_tree_size, max_tree_size=max_tree_size + trees, sampling_times = zip( + *[ + generate_valid_tree( + rng, + theta=th, + sampling_time=t_s, + mean_sampling_time=ms, + min_tree_size=min_tree_size, + max_tree_size=max_tree_size, + ) + for th, t_s, ms in zip(theta, sampling_times, mean_sampling_time) + ] ) - for th, t_s, ms in zip(theta, sampling_times, mean_sampling_time) -]) - trees = list(trees) sampling_times = list(sampling_times) diff --git a/tests/trees/test_simulate.py b/tests/trees/test_simulate.py index 9d5295c..d1175fa 100644 --- a/tests/trees/test_simulate.py +++ b/tests/trees/test_simulate.py @@ -24,12 +24,14 @@ def test_generate_valid_tree(): ] ) mean_sampling_time = 1.0 - sampling_time = rng.exponential(scale = mean_sampling_time) + sampling_time = rng.exponential(scale=mean_sampling_time) min_tree_size = 2 max_tree_size = 11 for _ in range(10000): - tree, sampling_time = simulate.generate_valid_tree(rng, theta, sampling_time,mean_sampling_time, min_tree_size, max_tree_size) + tree, sampling_time = simulate.generate_valid_tree( + rng, theta, sampling_time, mean_sampling_time, min_tree_size, max_tree_size + ) assert min_tree_size <= len(tree) <= max_tree_size @@ -58,11 +60,12 @@ def test_generate_tree_no_size_constraints(): ] ) mean_sampling_time = 1.0 - sampling_time = rng.exponential(scale = mean_sampling_time) - + sampling_time = rng.exponential(scale=mean_sampling_time) for _ in range(10000): - tree, sampling_time = simulate.generate_valid_tree(rng, theta, sampling_time, mean_sampling_time=mean_sampling_time) + tree, sampling_time = simulate.generate_valid_tree( + rng, theta, sampling_time, mean_sampling_time=mean_sampling_time + ) for node, time in tree.items(): assert time < sampling_time @@ -88,13 +91,17 @@ def test_generate_tree_no_min_size_constraint(): ] ) mean_sampling_time = 1.0 - sampling_time = rng.exponential(scale = mean_sampling_time) + sampling_time = rng.exponential(scale=mean_sampling_time) max_tree_size = 8 for _ in range(10000): tree, sampling_time = simulate.generate_valid_tree( - rng, theta, sampling_time, mean_sampling_time=mean_sampling_time, max_tree_size=max_tree_size + rng, + theta, + sampling_time, + mean_sampling_time=mean_sampling_time, + max_tree_size=max_tree_size, ) assert len(tree) <= max_tree_size @@ -122,13 +129,17 @@ def test_generate_tree_no_max_size_constraint(): ] ) mean_sampling_time = 1.0 - sampling_time = rng.exponential(scale = mean_sampling_time) + sampling_time = rng.exponential(scale=mean_sampling_time) min_tree_size = 3 for _ in range(10000): tree, sampling_time = simulate.generate_valid_tree( - rng, theta, sampling_time, mean_sampling_time=mean_sampling_time, min_tree_size=min_tree_size + rng, + theta, + sampling_time, + mean_sampling_time=mean_sampling_time, + min_tree_size=min_tree_size, ) assert len(tree) >= min_tree_size From 34b0dc978d62c3d408395cf8bcc0576d2ff8d405 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pawe=C5=82=20Czy=C5=BC?= Date: Sat, 30 Sep 2023 10:08:32 +0200 Subject: [PATCH 13/45] Remove poetry.lock --- .gitignore | 3 + poetry.lock | 2300 --------------------------------------------------- 2 files changed, 3 insertions(+), 2300 deletions(-) delete mode 100644 poetry.lock diff --git a/.gitignore b/.gitignore index 42c07b7..2ecea2e 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,6 @@ +# We use convention that poetry.lock is not committed +poetry.lock + # Jupyter Notebooks are disallowed by default # Use Quarto Notebook (*.qmd) instead *.ipynb diff --git a/poetry.lock b/poetry.lock deleted file mode 100644 index fc13778..0000000 --- a/poetry.lock +++ /dev/null @@ -1,2300 +0,0 @@ -# This file is automatically @generated by Poetry 1.6.1 and should not be changed by hand. - -[[package]] -name = "anytree" -version = "2.9.0" -description = "Powerful and Lightweight Python Tree Data Structure with various plugins" -optional = false -python-versions = ">=3.7.2,<4" -files = [ - {file = "anytree-2.9.0-py3-none-any.whl", hash = "sha256:7f1ad0f9b225705b780ea0593c8ff52af05df9428e7cc34b9379b879fa462663"}, - {file = "anytree-2.9.0.tar.gz", hash = "sha256:06f7bc294293da2755f4699cc5da5c92d9182a5cfae2842c83fb56f02bd427c8"}, -] - -[package.dependencies] -six = "*" - -[[package]] -name = "arviz" -version = "0.16.1" -description = "Exploratory analysis of Bayesian models" -optional = false -python-versions = ">=3.9" -files = [ - {file = "arviz-0.16.1-py3-none-any.whl", hash = "sha256:872d1d685719f81a31f94c25278cbaef314df7f6cad0671935f26a006182b8d4"}, - {file = "arviz-0.16.1.tar.gz", hash = "sha256:35bab9072f66f5a8204d2a71911d09ce05056c177f1a780de53efa2714c27575"}, -] - -[package.dependencies] -h5netcdf = ">=1.0.2" -matplotlib = ">=3.2" -numpy = ">=1.21.0,<2.0" -packaging = "*" -pandas = ">=1.3.0" -scipy = ">=1.8.0" -setuptools = ">=60.0.0" -typing-extensions = ">=4.1.0" -xarray = ">=0.21.0" -xarray-einstats = ">=0.3" - -[package.extras] -all = ["bokeh (>=1.4.0,<3.0)", "contourpy", "dask[distributed]", "netcdf4", "numba", "ujson", "xarray-datatree", "zarr (>=2.5.0)"] - -[[package]] -name = "babel" -version = "2.12.1" -description = "Internationalization utilities" -optional = false -python-versions = ">=3.7" -files = [ - {file = "Babel-2.12.1-py3-none-any.whl", hash = "sha256:b4246fb7677d3b98f501a39d43396d3cafdc8eadb045f4a31be01863f655c610"}, - {file = "Babel-2.12.1.tar.gz", hash = "sha256:cc2d99999cd01d44420ae725a21c9e3711b3aadc7976d6147f622d8581963455"}, -] - -[[package]] -name = "black" -version = "23.9.1" -description = "The uncompromising code formatter." -optional = false -python-versions = ">=3.8" -files = [ - {file = "black-23.9.1-cp310-cp310-macosx_10_16_arm64.whl", hash = "sha256:d6bc09188020c9ac2555a498949401ab35bb6bf76d4e0f8ee251694664df6301"}, - {file = "black-23.9.1-cp310-cp310-macosx_10_16_universal2.whl", hash = "sha256:13ef033794029b85dfea8032c9d3b92b42b526f1ff4bf13b2182ce4e917f5100"}, - {file = "black-23.9.1-cp310-cp310-macosx_10_16_x86_64.whl", hash = "sha256:75a2dc41b183d4872d3a500d2b9c9016e67ed95738a3624f4751a0cb4818fe71"}, - {file = "black-23.9.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:13a2e4a93bb8ca74a749b6974925c27219bb3df4d42fc45e948a5d9feb5122b7"}, - {file = "black-23.9.1-cp310-cp310-win_amd64.whl", hash = "sha256:adc3e4442eef57f99b5590b245a328aad19c99552e0bdc7f0b04db6656debd80"}, - {file = "black-23.9.1-cp311-cp311-macosx_10_16_arm64.whl", hash = "sha256:8431445bf62d2a914b541da7ab3e2b4f3bc052d2ccbf157ebad18ea126efb91f"}, - {file = "black-23.9.1-cp311-cp311-macosx_10_16_universal2.whl", hash = "sha256:8fc1ddcf83f996247505db6b715294eba56ea9372e107fd54963c7553f2b6dfe"}, - {file = "black-23.9.1-cp311-cp311-macosx_10_16_x86_64.whl", hash = "sha256:7d30ec46de88091e4316b17ae58bbbfc12b2de05e069030f6b747dfc649ad186"}, - {file = "black-23.9.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:031e8c69f3d3b09e1aa471a926a1eeb0b9071f80b17689a655f7885ac9325a6f"}, - {file = "black-23.9.1-cp311-cp311-win_amd64.whl", hash = "sha256:538efb451cd50f43aba394e9ec7ad55a37598faae3348d723b59ea8e91616300"}, - {file = "black-23.9.1-cp38-cp38-macosx_10_16_arm64.whl", hash = "sha256:638619a559280de0c2aa4d76f504891c9860bb8fa214267358f0a20f27c12948"}, - {file = "black-23.9.1-cp38-cp38-macosx_10_16_universal2.whl", hash = "sha256:a732b82747235e0542c03bf352c126052c0fbc458d8a239a94701175b17d4855"}, - {file = "black-23.9.1-cp38-cp38-macosx_10_16_x86_64.whl", hash = "sha256:cf3a4d00e4cdb6734b64bf23cd4341421e8953615cba6b3670453737a72ec204"}, - {file = "black-23.9.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cf99f3de8b3273a8317681d8194ea222f10e0133a24a7548c73ce44ea1679377"}, - {file = "black-23.9.1-cp38-cp38-win_amd64.whl", hash = "sha256:14f04c990259576acd093871e7e9b14918eb28f1866f91968ff5524293f9c573"}, - {file = "black-23.9.1-cp39-cp39-macosx_10_16_arm64.whl", hash = "sha256:c619f063c2d68f19b2d7270f4cf3192cb81c9ec5bc5ba02df91471d0b88c4c5c"}, - {file = "black-23.9.1-cp39-cp39-macosx_10_16_universal2.whl", hash = "sha256:6a3b50e4b93f43b34a9d3ef00d9b6728b4a722c997c99ab09102fd5efdb88325"}, - {file = "black-23.9.1-cp39-cp39-macosx_10_16_x86_64.whl", hash = "sha256:c46767e8df1b7beefb0899c4a95fb43058fa8500b6db144f4ff3ca38eb2f6393"}, - {file = "black-23.9.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:50254ebfa56aa46a9fdd5d651f9637485068a1adf42270148cd101cdf56e0ad9"}, - {file = "black-23.9.1-cp39-cp39-win_amd64.whl", hash = "sha256:403397c033adbc45c2bd41747da1f7fc7eaa44efbee256b53842470d4ac5a70f"}, - {file = "black-23.9.1-py3-none-any.whl", hash = "sha256:6ccd59584cc834b6d127628713e4b6b968e5f79572da66284532525a042549f9"}, - {file = "black-23.9.1.tar.gz", hash = "sha256:24b6b3ff5c6d9ea08a8888f6977eae858e1f340d7260cf56d70a49823236b62d"}, -] - -[package.dependencies] -click = ">=8.0.0" -mypy-extensions = ">=0.4.3" -packaging = ">=22.0" -pathspec = ">=0.9.0" -platformdirs = ">=2" -tomli = {version = ">=1.1.0", markers = "python_version < \"3.11\""} -typing-extensions = {version = ">=4.0.1", markers = "python_version < \"3.11\""} - -[package.extras] -colorama = ["colorama (>=0.4.3)"] -d = ["aiohttp (>=3.7.4)"] -jupyter = ["ipython (>=7.8.0)", "tokenize-rt (>=3.2.0)"] -uvloop = ["uvloop (>=0.15.2)"] - -[[package]] -name = "cachetools" -version = "5.3.1" -description = "Extensible memoizing collections and decorators" -optional = false -python-versions = ">=3.7" -files = [ - {file = "cachetools-5.3.1-py3-none-any.whl", hash = "sha256:95ef631eeaea14ba2e36f06437f36463aac3a096799e876ee55e5cdccb102590"}, - {file = "cachetools-5.3.1.tar.gz", hash = "sha256:dce83f2d9b4e1f732a8cd44af8e8fab2dbe46201467fc98b3ef8f269092bf62b"}, -] - -[[package]] -name = "certifi" -version = "2023.7.22" -description = "Python package for providing Mozilla's CA Bundle." -optional = false -python-versions = ">=3.6" -files = [ - {file = "certifi-2023.7.22-py3-none-any.whl", hash = "sha256:92d6037539857d8206b8f6ae472e8b77db8058fec5937a1ef3f54304089edbb9"}, - {file = "certifi-2023.7.22.tar.gz", hash = "sha256:539cc1d13202e33ca466e88b2807e29f4c13049d6d87031a3c110744495cb082"}, -] - -[[package]] -name = "cfgv" -version = "3.4.0" -description = "Validate configuration and produce human readable error messages." -optional = false -python-versions = ">=3.8" -files = [ - {file = "cfgv-3.4.0-py2.py3-none-any.whl", hash = "sha256:b7265b1f29fd3316bfcd2b330d63d024f2bfd8bcb8b0272f8e19a504856c48f9"}, - {file = "cfgv-3.4.0.tar.gz", hash = "sha256:e52591d4c5f5dead8e0f673fb16db7949d2cfb3f7da4582893288f0ded8fe560"}, -] - -[[package]] -name = "cftime" -version = "1.6.2" -description = "Time-handling functionality from netcdf4-python" -optional = false -python-versions = ">=3.7" -files = [ - {file = "cftime-1.6.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:b4d2a1920f0aad663f25700b30621ff64af373499e52b544da1148dd8c09409a"}, - {file = "cftime-1.6.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:2ba7909a0cd4adcb16797d8d6ab2767e7ddb980b2bf9dbabfc71b3bdd94f072b"}, - {file = "cftime-1.6.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:acb294fdb80e33545ae54b4421df35c4e578708a5ffce1c00408b2294e70ecef"}, - {file = "cftime-1.6.2-cp310-cp310-win_amd64.whl", hash = "sha256:2abdac6ca5b8b6102f319122546739dfc42406b816c16f2a98a8f0cd406d3bf0"}, - {file = "cftime-1.6.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:eb7f8cd0996640b83020133b5ef6b97fc9216c3129eaeeaca361abdff5d82166"}, - {file = "cftime-1.6.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:8d49d69c64cee2c175478eed84c3a57fce083da4ceebce16440f72be561a8489"}, - {file = "cftime-1.6.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:455cec3627e6ca8694b0d9201da6581eb4381b58389f1fbcb51a14fa0e2b3d94"}, - {file = "cftime-1.6.2-cp311-cp311-win_amd64.whl", hash = "sha256:29c18601abea0fd160fbe423e05c7a56fe1d38dd250a6b010de499a132d3fe18"}, - {file = "cftime-1.6.2-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:afb5b38b51b8bc02f1656a9f15c52b0b20a3999adbe1ab9ac57f926e0065b48a"}, - {file = "cftime-1.6.2-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:aedfb7a783d19d7a30cb41951310f3bfe98f9f21fffc723c8af08a11962b0b17"}, - {file = "cftime-1.6.2-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3042048324b4d6a1066c978ec78101effdd84320e8862bfdbf8122d7ad7588ec"}, - {file = "cftime-1.6.2-cp37-none-win_amd64.whl", hash = "sha256:ee70fa069802652cf534de1dd3fc590b7d22d4127447bf96ac9849abcdadadf1"}, - {file = "cftime-1.6.2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:93f00f454329c1f2588ebca2650e8edf7607d6189dbdcc81b5f3be2080155cc4"}, - {file = "cftime-1.6.2-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:e83db2fdda900eb154a9f79dfb665ac6190781c61d2e18151996de5ee7ffd8a2"}, - {file = "cftime-1.6.2-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:56d0242fc4990584b265622622b25bb262a178097711d2d95e53ef52a9d23e7e"}, - {file = "cftime-1.6.2-cp38-cp38-win_amd64.whl", hash = "sha256:055d5d60a756c6c1857cf84d77655bb707057bb6c4a4fbb104a550e76c40aad9"}, - {file = "cftime-1.6.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:0955e1f3e1c09a9e0296b50f135ff9719cb2466f81c8ad4a10ef06fa394de984"}, - {file = "cftime-1.6.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:07fdef2f75a0f0952b0376fa4cd08ef8a1dad3b963976ac07517811d434936b7"}, - {file = "cftime-1.6.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:892d5dc38f8b998c83a2a01f131e63896d020586de473e1878f9e85acc70ad44"}, - {file = "cftime-1.6.2-cp39-cp39-win_amd64.whl", hash = "sha256:86fe550b94525c327578a90b2e13418ca5ba6c636d5efe3edec310e631757eea"}, - {file = "cftime-1.6.2.tar.gz", hash = "sha256:8614c00fb8a5046de304fdd86dbd224f99408185d7b245ac6628d0276596e6d2"}, -] - -[package.dependencies] -numpy = ">1.13.3" - -[[package]] -name = "charset-normalizer" -version = "3.2.0" -description = "The Real First Universal Charset Detector. Open, modern and actively maintained alternative to Chardet." -optional = false -python-versions = ">=3.7.0" -files = [ - {file = "charset-normalizer-3.2.0.tar.gz", hash = "sha256:3bb3d25a8e6c0aedd251753a79ae98a093c7e7b471faa3aa9a93a81431987ace"}, - {file = "charset_normalizer-3.2.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:0b87549028f680ca955556e3bd57013ab47474c3124dc069faa0b6545b6c9710"}, - {file = "charset_normalizer-3.2.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:7c70087bfee18a42b4040bb9ec1ca15a08242cf5867c58726530bdf3945672ed"}, - {file = "charset_normalizer-3.2.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:a103b3a7069b62f5d4890ae1b8f0597618f628b286b03d4bc9195230b154bfa9"}, - {file = "charset_normalizer-3.2.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:94aea8eff76ee6d1cdacb07dd2123a68283cb5569e0250feab1240058f53b623"}, - {file = "charset_normalizer-3.2.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:db901e2ac34c931d73054d9797383d0f8009991e723dab15109740a63e7f902a"}, - {file = "charset_normalizer-3.2.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b0dac0ff919ba34d4df1b6131f59ce95b08b9065233446be7e459f95554c0dc8"}, - {file = "charset_normalizer-3.2.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:193cbc708ea3aca45e7221ae58f0fd63f933753a9bfb498a3b474878f12caaad"}, - {file = "charset_normalizer-3.2.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:09393e1b2a9461950b1c9a45d5fd251dc7c6f228acab64da1c9c0165d9c7765c"}, - {file = "charset_normalizer-3.2.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:baacc6aee0b2ef6f3d308e197b5d7a81c0e70b06beae1f1fcacffdbd124fe0e3"}, - {file = "charset_normalizer-3.2.0-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:bf420121d4c8dce6b889f0e8e4ec0ca34b7f40186203f06a946fa0276ba54029"}, - {file = "charset_normalizer-3.2.0-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:c04a46716adde8d927adb9457bbe39cf473e1e2c2f5d0a16ceb837e5d841ad4f"}, - {file = "charset_normalizer-3.2.0-cp310-cp310-musllinux_1_1_s390x.whl", hash = "sha256:aaf63899c94de41fe3cf934601b0f7ccb6b428c6e4eeb80da72c58eab077b19a"}, - {file = "charset_normalizer-3.2.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:d62e51710986674142526ab9f78663ca2b0726066ae26b78b22e0f5e571238dd"}, - {file = "charset_normalizer-3.2.0-cp310-cp310-win32.whl", hash = "sha256:04e57ab9fbf9607b77f7d057974694b4f6b142da9ed4a199859d9d4d5c63fe96"}, - {file = "charset_normalizer-3.2.0-cp310-cp310-win_amd64.whl", hash = "sha256:48021783bdf96e3d6de03a6e39a1171ed5bd7e8bb93fc84cc649d11490f87cea"}, - {file = "charset_normalizer-3.2.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:4957669ef390f0e6719db3613ab3a7631e68424604a7b448f079bee145da6e09"}, - {file = "charset_normalizer-3.2.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:46fb8c61d794b78ec7134a715a3e564aafc8f6b5e338417cb19fe9f57a5a9bf2"}, - {file = "charset_normalizer-3.2.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:f779d3ad205f108d14e99bb3859aa7dd8e9c68874617c72354d7ecaec2a054ac"}, - {file = "charset_normalizer-3.2.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f25c229a6ba38a35ae6e25ca1264621cc25d4d38dca2942a7fce0b67a4efe918"}, - {file = "charset_normalizer-3.2.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:2efb1bd13885392adfda4614c33d3b68dee4921fd0ac1d3988f8cbb7d589e72a"}, - {file = "charset_normalizer-3.2.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:1f30b48dd7fa1474554b0b0f3fdfdd4c13b5c737a3c6284d3cdc424ec0ffff3a"}, - {file = "charset_normalizer-3.2.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:246de67b99b6851627d945db38147d1b209a899311b1305dd84916f2b88526c6"}, - {file = "charset_normalizer-3.2.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:9bd9b3b31adcb054116447ea22caa61a285d92e94d710aa5ec97992ff5eb7cf3"}, - {file = "charset_normalizer-3.2.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:8c2f5e83493748286002f9369f3e6607c565a6a90425a3a1fef5ae32a36d749d"}, - {file = "charset_normalizer-3.2.0-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:3170c9399da12c9dc66366e9d14da8bf7147e1e9d9ea566067bbce7bb74bd9c2"}, - {file = "charset_normalizer-3.2.0-cp311-cp311-musllinux_1_1_ppc64le.whl", hash = "sha256:7a4826ad2bd6b07ca615c74ab91f32f6c96d08f6fcc3902ceeedaec8cdc3bcd6"}, - {file = "charset_normalizer-3.2.0-cp311-cp311-musllinux_1_1_s390x.whl", hash = "sha256:3b1613dd5aee995ec6d4c69f00378bbd07614702a315a2cf6c1d21461fe17c23"}, - {file = "charset_normalizer-3.2.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:9e608aafdb55eb9f255034709e20d5a83b6d60c054df0802fa9c9883d0a937aa"}, - {file = "charset_normalizer-3.2.0-cp311-cp311-win32.whl", hash = "sha256:f2a1d0fd4242bd8643ce6f98927cf9c04540af6efa92323e9d3124f57727bfc1"}, - {file = "charset_normalizer-3.2.0-cp311-cp311-win_amd64.whl", hash = "sha256:681eb3d7e02e3c3655d1b16059fbfb605ac464c834a0c629048a30fad2b27489"}, - {file = "charset_normalizer-3.2.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:c57921cda3a80d0f2b8aec7e25c8aa14479ea92b5b51b6876d975d925a2ea346"}, - {file = "charset_normalizer-3.2.0-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:41b25eaa7d15909cf3ac4c96088c1f266a9a93ec44f87f1d13d4a0e86c81b982"}, - {file = "charset_normalizer-3.2.0-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f058f6963fd82eb143c692cecdc89e075fa0828db2e5b291070485390b2f1c9c"}, - {file = "charset_normalizer-3.2.0-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a7647ebdfb9682b7bb97e2a5e7cb6ae735b1c25008a70b906aecca294ee96cf4"}, - {file = "charset_normalizer-3.2.0-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:eef9df1eefada2c09a5e7a40991b9fc6ac6ef20b1372abd48d2794a316dc0449"}, - {file = "charset_normalizer-3.2.0-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e03b8895a6990c9ab2cdcd0f2fe44088ca1c65ae592b8f795c3294af00a461c3"}, - {file = "charset_normalizer-3.2.0-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:ee4006268ed33370957f55bf2e6f4d263eaf4dc3cfc473d1d90baff6ed36ce4a"}, - {file = "charset_normalizer-3.2.0-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:c4983bf937209c57240cff65906b18bb35e64ae872da6a0db937d7b4af845dd7"}, - {file = "charset_normalizer-3.2.0-cp37-cp37m-musllinux_1_1_ppc64le.whl", hash = "sha256:3bb7fda7260735efe66d5107fb7e6af6a7c04c7fce9b2514e04b7a74b06bf5dd"}, - {file = "charset_normalizer-3.2.0-cp37-cp37m-musllinux_1_1_s390x.whl", hash = "sha256:72814c01533f51d68702802d74f77ea026b5ec52793c791e2da806a3844a46c3"}, - {file = "charset_normalizer-3.2.0-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:70c610f6cbe4b9fce272c407dd9d07e33e6bf7b4aa1b7ffb6f6ded8e634e3592"}, - {file = "charset_normalizer-3.2.0-cp37-cp37m-win32.whl", hash = "sha256:a401b4598e5d3f4a9a811f3daf42ee2291790c7f9d74b18d75d6e21dda98a1a1"}, - {file = "charset_normalizer-3.2.0-cp37-cp37m-win_amd64.whl", hash = "sha256:c0b21078a4b56965e2b12f247467b234734491897e99c1d51cee628da9786959"}, - {file = "charset_normalizer-3.2.0-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:95eb302ff792e12aba9a8b8f8474ab229a83c103d74a750ec0bd1c1eea32e669"}, - {file = "charset_normalizer-3.2.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:1a100c6d595a7f316f1b6f01d20815d916e75ff98c27a01ae817439ea7726329"}, - {file = "charset_normalizer-3.2.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:6339d047dab2780cc6220f46306628e04d9750f02f983ddb37439ca47ced7149"}, - {file = "charset_normalizer-3.2.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e4b749b9cc6ee664a3300bb3a273c1ca8068c46be705b6c31cf5d276f8628a94"}, - {file = "charset_normalizer-3.2.0-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a38856a971c602f98472050165cea2cdc97709240373041b69030be15047691f"}, - {file = "charset_normalizer-3.2.0-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f87f746ee241d30d6ed93969de31e5ffd09a2961a051e60ae6bddde9ec3583aa"}, - {file = "charset_normalizer-3.2.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:89f1b185a01fe560bc8ae5f619e924407efca2191b56ce749ec84982fc59a32a"}, - {file = "charset_normalizer-3.2.0-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e1c8a2f4c69e08e89632defbfabec2feb8a8d99edc9f89ce33c4b9e36ab63037"}, - {file = "charset_normalizer-3.2.0-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:2f4ac36d8e2b4cc1aa71df3dd84ff8efbe3bfb97ac41242fbcfc053c67434f46"}, - {file = "charset_normalizer-3.2.0-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:a386ebe437176aab38c041de1260cd3ea459c6ce5263594399880bbc398225b2"}, - {file = "charset_normalizer-3.2.0-cp38-cp38-musllinux_1_1_ppc64le.whl", hash = "sha256:ccd16eb18a849fd8dcb23e23380e2f0a354e8daa0c984b8a732d9cfaba3a776d"}, - {file = "charset_normalizer-3.2.0-cp38-cp38-musllinux_1_1_s390x.whl", hash = "sha256:e6a5bf2cba5ae1bb80b154ed68a3cfa2fa00fde979a7f50d6598d3e17d9ac20c"}, - {file = "charset_normalizer-3.2.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:45de3f87179c1823e6d9e32156fb14c1927fcc9aba21433f088fdfb555b77c10"}, - {file = "charset_normalizer-3.2.0-cp38-cp38-win32.whl", hash = "sha256:1000fba1057b92a65daec275aec30586c3de2401ccdcd41f8a5c1e2c87078706"}, - {file = "charset_normalizer-3.2.0-cp38-cp38-win_amd64.whl", hash = "sha256:8b2c760cfc7042b27ebdb4a43a4453bd829a5742503599144d54a032c5dc7e9e"}, - {file = "charset_normalizer-3.2.0-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:855eafa5d5a2034b4621c74925d89c5efef61418570e5ef9b37717d9c796419c"}, - {file = "charset_normalizer-3.2.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:203f0c8871d5a7987be20c72442488a0b8cfd0f43b7973771640fc593f56321f"}, - {file = "charset_normalizer-3.2.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:e857a2232ba53ae940d3456f7533ce6ca98b81917d47adc3c7fd55dad8fab858"}, - {file = "charset_normalizer-3.2.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5e86d77b090dbddbe78867a0275cb4df08ea195e660f1f7f13435a4649e954e5"}, - {file = "charset_normalizer-3.2.0-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c4fb39a81950ec280984b3a44f5bd12819953dc5fa3a7e6fa7a80db5ee853952"}, - {file = "charset_normalizer-3.2.0-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2dee8e57f052ef5353cf608e0b4c871aee320dd1b87d351c28764fc0ca55f9f4"}, - {file = "charset_normalizer-3.2.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8700f06d0ce6f128de3ccdbc1acaea1ee264d2caa9ca05daaf492fde7c2a7200"}, - {file = "charset_normalizer-3.2.0-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1920d4ff15ce893210c1f0c0e9d19bfbecb7983c76b33f046c13a8ffbd570252"}, - {file = "charset_normalizer-3.2.0-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:c1c76a1743432b4b60ab3358c937a3fe1341c828ae6194108a94c69028247f22"}, - {file = "charset_normalizer-3.2.0-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:f7560358a6811e52e9c4d142d497f1a6e10103d3a6881f18d04dbce3729c0e2c"}, - {file = "charset_normalizer-3.2.0-cp39-cp39-musllinux_1_1_ppc64le.whl", hash = "sha256:c8063cf17b19661471ecbdb3df1c84f24ad2e389e326ccaf89e3fb2484d8dd7e"}, - {file = "charset_normalizer-3.2.0-cp39-cp39-musllinux_1_1_s390x.whl", hash = "sha256:cd6dbe0238f7743d0efe563ab46294f54f9bc8f4b9bcf57c3c666cc5bc9d1299"}, - {file = "charset_normalizer-3.2.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:1249cbbf3d3b04902ff081ffbb33ce3377fa6e4c7356f759f3cd076cc138d020"}, - {file = "charset_normalizer-3.2.0-cp39-cp39-win32.whl", hash = "sha256:6c409c0deba34f147f77efaa67b8e4bb83d2f11c8806405f76397ae5b8c0d1c9"}, - {file = "charset_normalizer-3.2.0-cp39-cp39-win_amd64.whl", hash = "sha256:7095f6fbfaa55defb6b733cfeb14efaae7a29f0b59d8cf213be4e7ca0b857b80"}, - {file = "charset_normalizer-3.2.0-py3-none-any.whl", hash = "sha256:8e098148dd37b4ce3baca71fb394c81dc5d9c7728c95df695d2dca218edf40e6"}, -] - -[[package]] -name = "click" -version = "8.1.7" -description = "Composable command line interface toolkit" -optional = false -python-versions = ">=3.7" -files = [ - {file = "click-8.1.7-py3-none-any.whl", hash = "sha256:ae74fb96c20a0277a1d615f1e4d73c8414f5a98db8b799a7931d1582f3390c28"}, - {file = "click-8.1.7.tar.gz", hash = "sha256:ca9853ad459e787e2192211578cc907e7594e294c7ccc834310722b41b9ca6de"}, -] - -[package.dependencies] -colorama = {version = "*", markers = "platform_system == \"Windows\""} - -[[package]] -name = "cloudpickle" -version = "2.2.1" -description = "Extended pickling support for Python objects" -optional = false -python-versions = ">=3.6" -files = [ - {file = "cloudpickle-2.2.1-py3-none-any.whl", hash = "sha256:61f594d1f4c295fa5cd9014ceb3a1fc4a70b0de1164b94fbc2d854ccba056f9f"}, - {file = "cloudpickle-2.2.1.tar.gz", hash = "sha256:d89684b8de9e34a2a43b3460fbca07d09d6e25ce858df4d5a44240403b6178f5"}, -] - -[[package]] -name = "colorama" -version = "0.4.6" -description = "Cross-platform colored terminal text." -optional = false -python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7" -files = [ - {file = "colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6"}, - {file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"}, -] - -[[package]] -name = "cons" -version = "0.4.6" -description = "An implementation of Lisp/Scheme-like cons in Python." -optional = false -python-versions = ">=3.6" -files = [ - {file = "cons-0.4.6.tar.gz", hash = "sha256:669fe9d5ee916d5e42b9cac6acc911df803d04f2e945c1604982a04d27a29b47"}, -] - -[package.dependencies] -logical-unification = ">=0.4.0" - -[[package]] -name = "contourpy" -version = "1.1.0" -description = "Python library for calculating contours of 2D quadrilateral grids" -optional = false -python-versions = ">=3.8" -files = [ - {file = "contourpy-1.1.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:89f06eff3ce2f4b3eb24c1055a26981bffe4e7264acd86f15b97e40530b794bc"}, - {file = "contourpy-1.1.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:dffcc2ddec1782dd2f2ce1ef16f070861af4fb78c69862ce0aab801495dda6a3"}, - {file = "contourpy-1.1.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:25ae46595e22f93592d39a7eac3d638cda552c3e1160255258b695f7b58e5655"}, - {file = "contourpy-1.1.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:17cfaf5ec9862bc93af1ec1f302457371c34e688fbd381f4035a06cd47324f48"}, - {file = "contourpy-1.1.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:18a64814ae7bce73925131381603fff0116e2df25230dfc80d6d690aa6e20b37"}, - {file = "contourpy-1.1.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:90c81f22b4f572f8a2110b0b741bb64e5a6427e0a198b2cdc1fbaf85f352a3aa"}, - {file = "contourpy-1.1.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:53cc3a40635abedbec7f1bde60f8c189c49e84ac180c665f2cd7c162cc454baa"}, - {file = "contourpy-1.1.0-cp310-cp310-win32.whl", hash = "sha256:9b2dd2ca3ac561aceef4c7c13ba654aaa404cf885b187427760d7f7d4c57cff8"}, - {file = "contourpy-1.1.0-cp310-cp310-win_amd64.whl", hash = "sha256:1f795597073b09d631782e7245016a4323cf1cf0b4e06eef7ea6627e06a37ff2"}, - {file = "contourpy-1.1.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:0b7b04ed0961647691cfe5d82115dd072af7ce8846d31a5fac6c142dcce8b882"}, - {file = "contourpy-1.1.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:27bc79200c742f9746d7dd51a734ee326a292d77e7d94c8af6e08d1e6c15d545"}, - {file = "contourpy-1.1.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:052cc634bf903c604ef1a00a5aa093c54f81a2612faedaa43295809ffdde885e"}, - {file = "contourpy-1.1.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9382a1c0bc46230fb881c36229bfa23d8c303b889b788b939365578d762b5c18"}, - {file = "contourpy-1.1.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e5cec36c5090e75a9ac9dbd0ff4a8cf7cecd60f1b6dc23a374c7d980a1cd710e"}, - {file = "contourpy-1.1.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1f0cbd657e9bde94cd0e33aa7df94fb73c1ab7799378d3b3f902eb8eb2e04a3a"}, - {file = "contourpy-1.1.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:181cbace49874f4358e2929aaf7ba84006acb76694102e88dd15af861996c16e"}, - {file = "contourpy-1.1.0-cp311-cp311-win32.whl", hash = "sha256:edb989d31065b1acef3828a3688f88b2abb799a7db891c9e282df5ec7e46221b"}, - {file = "contourpy-1.1.0-cp311-cp311-win_amd64.whl", hash = "sha256:fb3b7d9e6243bfa1efb93ccfe64ec610d85cfe5aec2c25f97fbbd2e58b531256"}, - {file = "contourpy-1.1.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:bcb41692aa09aeb19c7c213411854402f29f6613845ad2453d30bf421fe68fed"}, - {file = "contourpy-1.1.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:5d123a5bc63cd34c27ff9c7ac1cd978909e9c71da12e05be0231c608048bb2ae"}, - {file = "contourpy-1.1.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:62013a2cf68abc80dadfd2307299bfa8f5aa0dcaec5b2954caeb5fa094171103"}, - {file = "contourpy-1.1.0-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:0b6616375d7de55797d7a66ee7d087efe27f03d336c27cf1f32c02b8c1a5ac70"}, - {file = "contourpy-1.1.0-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:317267d915490d1e84577924bd61ba71bf8681a30e0d6c545f577363157e5e94"}, - {file = "contourpy-1.1.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d551f3a442655f3dcc1285723f9acd646ca5858834efeab4598d706206b09c9f"}, - {file = "contourpy-1.1.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:e7a117ce7df5a938fe035cad481b0189049e8d92433b4b33aa7fc609344aafa1"}, - {file = "contourpy-1.1.0-cp38-cp38-win32.whl", hash = "sha256:108dfb5b3e731046a96c60bdc46a1a0ebee0760418951abecbe0fc07b5b93b27"}, - {file = "contourpy-1.1.0-cp38-cp38-win_amd64.whl", hash = "sha256:d4f26b25b4f86087e7d75e63212756c38546e70f2a92d2be44f80114826e1cd4"}, - {file = "contourpy-1.1.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:bc00bb4225d57bff7ebb634646c0ee2a1298402ec10a5fe7af79df9a51c1bfd9"}, - {file = "contourpy-1.1.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:189ceb1525eb0655ab8487a9a9c41f42a73ba52d6789754788d1883fb06b2d8a"}, - {file = "contourpy-1.1.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9f2931ed4741f98f74b410b16e5213f71dcccee67518970c42f64153ea9313b9"}, - {file = "contourpy-1.1.0-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:30f511c05fab7f12e0b1b7730ebdc2ec8deedcfb505bc27eb570ff47c51a8f15"}, - {file = "contourpy-1.1.0-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:143dde50520a9f90e4a2703f367cf8ec96a73042b72e68fcd184e1279962eb6f"}, - {file = "contourpy-1.1.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e94bef2580e25b5fdb183bf98a2faa2adc5b638736b2c0a4da98691da641316a"}, - {file = "contourpy-1.1.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:ed614aea8462735e7d70141374bd7650afd1c3f3cb0c2dbbcbe44e14331bf002"}, - {file = "contourpy-1.1.0-cp39-cp39-win32.whl", hash = "sha256:71551f9520f008b2950bef5f16b0e3587506ef4f23c734b71ffb7b89f8721999"}, - {file = "contourpy-1.1.0-cp39-cp39-win_amd64.whl", hash = "sha256:438ba416d02f82b692e371858143970ed2eb6337d9cdbbede0d8ad9f3d7dd17d"}, - {file = "contourpy-1.1.0-pp38-pypy38_pp73-macosx_10_9_x86_64.whl", hash = "sha256:a698c6a7a432789e587168573a864a7ea374c6be8d4f31f9d87c001d5a843493"}, - {file = "contourpy-1.1.0-pp38-pypy38_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:397b0ac8a12880412da3551a8cb5a187d3298a72802b45a3bd1805e204ad8439"}, - {file = "contourpy-1.1.0-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:a67259c2b493b00e5a4d0f7bfae51fb4b3371395e47d079a4446e9b0f4d70e76"}, - {file = "contourpy-1.1.0-pp39-pypy39_pp73-macosx_10_9_x86_64.whl", hash = "sha256:2b836d22bd2c7bb2700348e4521b25e077255ebb6ab68e351ab5aa91ca27e027"}, - {file = "contourpy-1.1.0-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:084eaa568400cfaf7179b847ac871582199b1b44d5699198e9602ecbbb5f6104"}, - {file = "contourpy-1.1.0-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:911ff4fd53e26b019f898f32db0d4956c9d227d51338fb3b03ec72ff0084ee5f"}, - {file = "contourpy-1.1.0.tar.gz", hash = "sha256:e53046c3863828d21d531cc3b53786e6580eb1ba02477e8681009b6aa0870b21"}, -] - -[package.dependencies] -numpy = ">=1.16" - -[package.extras] -bokeh = ["bokeh", "selenium"] -docs = ["furo", "sphinx-copybutton"] -mypy = ["contourpy[bokeh,docs]", "docutils-stubs", "mypy (==1.2.0)", "types-Pillow"] -test = ["Pillow", "contourpy[test-no-images]", "matplotlib"] -test-no-images = ["pytest", "pytest-cov", "wurlitzer"] - -[[package]] -name = "contourpy" -version = "1.1.1" -description = "Python library for calculating contours of 2D quadrilateral grids" -optional = false -python-versions = ">=3.8" -files = [ - {file = "contourpy-1.1.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:46e24f5412c948d81736509377e255f6040e94216bf1a9b5ea1eaa9d29f6ec1b"}, - {file = "contourpy-1.1.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:0e48694d6a9c5a26ee85b10130c77a011a4fedf50a7279fa0bdaf44bafb4299d"}, - {file = "contourpy-1.1.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a66045af6cf00e19d02191ab578a50cb93b2028c3eefed999793698e9ea768ae"}, - {file = "contourpy-1.1.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4ebf42695f75ee1a952f98ce9775c873e4971732a87334b099dde90b6af6a916"}, - {file = "contourpy-1.1.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f6aec19457617ef468ff091669cca01fa7ea557b12b59a7908b9474bb9674cf0"}, - {file = "contourpy-1.1.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:462c59914dc6d81e0b11f37e560b8a7c2dbab6aca4f38be31519d442d6cde1a1"}, - {file = "contourpy-1.1.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:6d0a8efc258659edc5299f9ef32d8d81de8b53b45d67bf4bfa3067f31366764d"}, - {file = "contourpy-1.1.1-cp310-cp310-win32.whl", hash = "sha256:d6ab42f223e58b7dac1bb0af32194a7b9311065583cc75ff59dcf301afd8a431"}, - {file = "contourpy-1.1.1-cp310-cp310-win_amd64.whl", hash = "sha256:549174b0713d49871c6dee90a4b499d3f12f5e5f69641cd23c50a4542e2ca1eb"}, - {file = "contourpy-1.1.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:407d864db716a067cc696d61fa1ef6637fedf03606e8417fe2aeed20a061e6b2"}, - {file = "contourpy-1.1.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:dfe80c017973e6a4c367e037cb31601044dd55e6bfacd57370674867d15a899b"}, - {file = "contourpy-1.1.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e30aaf2b8a2bac57eb7e1650df1b3a4130e8d0c66fc2f861039d507a11760e1b"}, - {file = "contourpy-1.1.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3de23ca4f381c3770dee6d10ead6fff524d540c0f662e763ad1530bde5112532"}, - {file = "contourpy-1.1.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:566f0e41df06dfef2431defcfaa155f0acfa1ca4acbf8fd80895b1e7e2ada40e"}, - {file = "contourpy-1.1.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b04c2f0adaf255bf756cf08ebef1be132d3c7a06fe6f9877d55640c5e60c72c5"}, - {file = "contourpy-1.1.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:d0c188ae66b772d9d61d43c6030500344c13e3f73a00d1dc241da896f379bb62"}, - {file = "contourpy-1.1.1-cp311-cp311-win32.whl", hash = "sha256:0683e1ae20dc038075d92e0e0148f09ffcefab120e57f6b4c9c0f477ec171f33"}, - {file = "contourpy-1.1.1-cp311-cp311-win_amd64.whl", hash = "sha256:8636cd2fc5da0fb102a2504fa2c4bea3cbc149533b345d72cdf0e7a924decc45"}, - {file = "contourpy-1.1.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:560f1d68a33e89c62da5da4077ba98137a5e4d3a271b29f2f195d0fba2adcb6a"}, - {file = "contourpy-1.1.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:24216552104ae8f3b34120ef84825400b16eb6133af2e27a190fdc13529f023e"}, - {file = "contourpy-1.1.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:56de98a2fb23025882a18b60c7f0ea2d2d70bbbcfcf878f9067234b1c4818442"}, - {file = "contourpy-1.1.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:07d6f11dfaf80a84c97f1a5ba50d129d9303c5b4206f776e94037332e298dda8"}, - {file = "contourpy-1.1.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f1eaac5257a8f8a047248d60e8f9315c6cff58f7803971170d952555ef6344a7"}, - {file = "contourpy-1.1.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:19557fa407e70f20bfaba7d55b4d97b14f9480856c4fb65812e8a05fe1c6f9bf"}, - {file = "contourpy-1.1.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:081f3c0880712e40effc5f4c3b08feca6d064cb8cfbb372ca548105b86fd6c3d"}, - {file = "contourpy-1.1.1-cp312-cp312-win32.whl", hash = "sha256:059c3d2a94b930f4dafe8105bcdc1b21de99b30b51b5bce74c753686de858cb6"}, - {file = "contourpy-1.1.1-cp312-cp312-win_amd64.whl", hash = "sha256:f44d78b61740e4e8c71db1cf1fd56d9050a4747681c59ec1094750a658ceb970"}, - {file = "contourpy-1.1.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:70e5a10f8093d228bb2b552beeb318b8928b8a94763ef03b858ef3612b29395d"}, - {file = "contourpy-1.1.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:8394e652925a18ef0091115e3cc191fef350ab6dc3cc417f06da66bf98071ae9"}, - {file = "contourpy-1.1.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c5bd5680f844c3ff0008523a71949a3ff5e4953eb7701b28760805bc9bcff217"}, - {file = "contourpy-1.1.1-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:66544f853bfa85c0d07a68f6c648b2ec81dafd30f272565c37ab47a33b220684"}, - {file = "contourpy-1.1.1-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e0c02b75acfea5cab07585d25069207e478d12309557f90a61b5a3b4f77f46ce"}, - {file = "contourpy-1.1.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:41339b24471c58dc1499e56783fedc1afa4bb018bcd035cfb0ee2ad2a7501ef8"}, - {file = "contourpy-1.1.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:f29fb0b3f1217dfe9362ec55440d0743fe868497359f2cf93293f4b2701b8251"}, - {file = "contourpy-1.1.1-cp38-cp38-win32.whl", hash = "sha256:f9dc7f933975367251c1b34da882c4f0e0b2e24bb35dc906d2f598a40b72bfc7"}, - {file = "contourpy-1.1.1-cp38-cp38-win_amd64.whl", hash = "sha256:498e53573e8b94b1caeb9e62d7c2d053c263ebb6aa259c81050766beb50ff8d9"}, - {file = "contourpy-1.1.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:ba42e3810999a0ddd0439e6e5dbf6d034055cdc72b7c5c839f37a7c274cb4eba"}, - {file = "contourpy-1.1.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:6c06e4c6e234fcc65435223c7b2a90f286b7f1b2733058bdf1345d218cc59e34"}, - {file = "contourpy-1.1.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ca6fab080484e419528e98624fb5c4282148b847e3602dc8dbe0cb0669469887"}, - {file = "contourpy-1.1.1-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:93df44ab351119d14cd1e6b52a5063d3336f0754b72736cc63db59307dabb718"}, - {file = "contourpy-1.1.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:eafbef886566dc1047d7b3d4b14db0d5b7deb99638d8e1be4e23a7c7ac59ff0f"}, - {file = "contourpy-1.1.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:efe0fab26d598e1ec07d72cf03eaeeba8e42b4ecf6b9ccb5a356fde60ff08b85"}, - {file = "contourpy-1.1.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:f08e469821a5e4751c97fcd34bcb586bc243c39c2e39321822060ba902eac49e"}, - {file = "contourpy-1.1.1-cp39-cp39-win32.whl", hash = "sha256:bfc8a5e9238232a45ebc5cb3bfee71f1167064c8d382cadd6076f0d51cff1da0"}, - {file = "contourpy-1.1.1-cp39-cp39-win_amd64.whl", hash = "sha256:c84fdf3da00c2827d634de4fcf17e3e067490c4aea82833625c4c8e6cdea0887"}, - {file = "contourpy-1.1.1-pp38-pypy38_pp73-macosx_10_9_x86_64.whl", hash = "sha256:229a25f68046c5cf8067d6d6351c8b99e40da11b04d8416bf8d2b1d75922521e"}, - {file = "contourpy-1.1.1-pp38-pypy38_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a10dab5ea1bd4401c9483450b5b0ba5416be799bbd50fc7a6cc5e2a15e03e8a3"}, - {file = "contourpy-1.1.1-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:4f9147051cb8fdb29a51dc2482d792b3b23e50f8f57e3720ca2e3d438b7adf23"}, - {file = "contourpy-1.1.1-pp39-pypy39_pp73-macosx_10_9_x86_64.whl", hash = "sha256:a75cc163a5f4531a256f2c523bd80db509a49fc23721b36dd1ef2f60ff41c3cb"}, - {file = "contourpy-1.1.1-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3b53d5769aa1f2d4ea407c65f2d1d08002952fac1d9e9d307aa2e1023554a163"}, - {file = "contourpy-1.1.1-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:11b836b7dbfb74e049c302bbf74b4b8f6cb9d0b6ca1bf86cfa8ba144aedadd9c"}, - {file = "contourpy-1.1.1.tar.gz", hash = "sha256:96ba37c2e24b7212a77da85004c38e7c4d155d3e72a45eeaf22c1f03f607e8ab"}, -] - -[package.dependencies] -numpy = {version = ">=1.16,<2.0", markers = "python_version <= \"3.11\""} - -[package.extras] -bokeh = ["bokeh", "selenium"] -docs = ["furo", "sphinx (>=7.2)", "sphinx-copybutton"] -mypy = ["contourpy[bokeh,docs]", "docutils-stubs", "mypy (==1.4.1)", "types-Pillow"] -test = ["Pillow", "contourpy[test-no-images]", "matplotlib"] -test-no-images = ["pytest", "pytest-cov", "wurlitzer"] - -[[package]] -name = "coverage" -version = "7.3.1" -description = "Code coverage measurement for Python" -optional = false -python-versions = ">=3.8" -files = [ - {file = "coverage-7.3.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:cd0f7429ecfd1ff597389907045ff209c8fdb5b013d38cfa7c60728cb484b6e3"}, - {file = "coverage-7.3.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:966f10df9b2b2115da87f50f6a248e313c72a668248be1b9060ce935c871f276"}, - {file = "coverage-7.3.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0575c37e207bb9b98b6cf72fdaaa18ac909fb3d153083400c2d48e2e6d28bd8e"}, - {file = "coverage-7.3.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:245c5a99254e83875c7fed8b8b2536f040997a9b76ac4c1da5bff398c06e860f"}, - {file = "coverage-7.3.1-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4c96dd7798d83b960afc6c1feb9e5af537fc4908852ef025600374ff1a017392"}, - {file = "coverage-7.3.1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:de30c1aa80f30af0f6b2058a91505ea6e36d6535d437520067f525f7df123887"}, - {file = "coverage-7.3.1-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:50dd1e2dd13dbbd856ffef69196781edff26c800a74f070d3b3e3389cab2600d"}, - {file = "coverage-7.3.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:b9c0c19f70d30219113b18fe07e372b244fb2a773d4afde29d5a2f7930765136"}, - {file = "coverage-7.3.1-cp310-cp310-win32.whl", hash = "sha256:770f143980cc16eb601ccfd571846e89a5fe4c03b4193f2e485268f224ab602f"}, - {file = "coverage-7.3.1-cp310-cp310-win_amd64.whl", hash = "sha256:cdd088c00c39a27cfa5329349cc763a48761fdc785879220d54eb785c8a38520"}, - {file = "coverage-7.3.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:74bb470399dc1989b535cb41f5ca7ab2af561e40def22d7e188e0a445e7639e3"}, - {file = "coverage-7.3.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:025ded371f1ca280c035d91b43252adbb04d2aea4c7105252d3cbc227f03b375"}, - {file = "coverage-7.3.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a6191b3a6ad3e09b6cfd75b45c6aeeffe7e3b0ad46b268345d159b8df8d835f9"}, - {file = "coverage-7.3.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7eb0b188f30e41ddd659a529e385470aa6782f3b412f860ce22b2491c89b8593"}, - {file = "coverage-7.3.1-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:75c8f0df9dfd8ff745bccff75867d63ef336e57cc22b2908ee725cc552689ec8"}, - {file = "coverage-7.3.1-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:7eb3cd48d54b9bd0e73026dedce44773214064be93611deab0b6a43158c3d5a0"}, - {file = "coverage-7.3.1-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:ac3c5b7e75acac31e490b7851595212ed951889918d398b7afa12736c85e13ce"}, - {file = "coverage-7.3.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:5b4ee7080878077af0afa7238df1b967f00dc10763f6e1b66f5cced4abebb0a3"}, - {file = "coverage-7.3.1-cp311-cp311-win32.whl", hash = "sha256:229c0dd2ccf956bf5aeede7e3131ca48b65beacde2029f0361b54bf93d36f45a"}, - {file = "coverage-7.3.1-cp311-cp311-win_amd64.whl", hash = "sha256:c6f55d38818ca9596dc9019eae19a47410d5322408140d9a0076001a3dcb938c"}, - {file = "coverage-7.3.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:5289490dd1c3bb86de4730a92261ae66ea8d44b79ed3cc26464f4c2cde581fbc"}, - {file = "coverage-7.3.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:ca833941ec701fda15414be400c3259479bfde7ae6d806b69e63b3dc423b1832"}, - {file = "coverage-7.3.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cd694e19c031733e446c8024dedd12a00cda87e1c10bd7b8539a87963685e969"}, - {file = "coverage-7.3.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:aab8e9464c00da5cb9c536150b7fbcd8850d376d1151741dd0d16dfe1ba4fd26"}, - {file = "coverage-7.3.1-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:87d38444efffd5b056fcc026c1e8d862191881143c3aa80bb11fcf9dca9ae204"}, - {file = "coverage-7.3.1-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:8a07b692129b8a14ad7a37941a3029c291254feb7a4237f245cfae2de78de037"}, - {file = "coverage-7.3.1-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:2829c65c8faaf55b868ed7af3c7477b76b1c6ebeee99a28f59a2cb5907a45760"}, - {file = "coverage-7.3.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:1f111a7d85658ea52ffad7084088277135ec5f368457275fc57f11cebb15607f"}, - {file = "coverage-7.3.1-cp312-cp312-win32.whl", hash = "sha256:c397c70cd20f6df7d2a52283857af622d5f23300c4ca8e5bd8c7a543825baa5a"}, - {file = "coverage-7.3.1-cp312-cp312-win_amd64.whl", hash = "sha256:5ae4c6da8b3d123500f9525b50bf0168023313963e0e2e814badf9000dd6ef92"}, - {file = "coverage-7.3.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:ca70466ca3a17460e8fc9cea7123c8cbef5ada4be3140a1ef8f7b63f2f37108f"}, - {file = "coverage-7.3.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:f2781fd3cabc28278dc982a352f50c81c09a1a500cc2086dc4249853ea96b981"}, - {file = "coverage-7.3.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6407424621f40205bbe6325686417e5e552f6b2dba3535dd1f90afc88a61d465"}, - {file = "coverage-7.3.1-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:04312b036580ec505f2b77cbbdfb15137d5efdfade09156961f5277149f5e344"}, - {file = "coverage-7.3.1-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ac9ad38204887349853d7c313f53a7b1c210ce138c73859e925bc4e5d8fc18e7"}, - {file = "coverage-7.3.1-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:53669b79f3d599da95a0afbef039ac0fadbb236532feb042c534fbb81b1a4e40"}, - {file = "coverage-7.3.1-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:614f1f98b84eb256e4f35e726bfe5ca82349f8dfa576faabf8a49ca09e630086"}, - {file = "coverage-7.3.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:f1a317fdf5c122ad642db8a97964733ab7c3cf6009e1a8ae8821089993f175ff"}, - {file = "coverage-7.3.1-cp38-cp38-win32.whl", hash = "sha256:defbbb51121189722420a208957e26e49809feafca6afeef325df66c39c4fdb3"}, - {file = "coverage-7.3.1-cp38-cp38-win_amd64.whl", hash = "sha256:f4f456590eefb6e1b3c9ea6328c1e9fa0f1006e7481179d749b3376fc793478e"}, - {file = "coverage-7.3.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:f12d8b11a54f32688b165fd1a788c408f927b0960984b899be7e4c190ae758f1"}, - {file = "coverage-7.3.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:f09195dda68d94a53123883de75bb97b0e35f5f6f9f3aa5bf6e496da718f0cb6"}, - {file = "coverage-7.3.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c6601a60318f9c3945be6ea0f2a80571f4299b6801716f8a6e4846892737ebe4"}, - {file = "coverage-7.3.1-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:07d156269718670d00a3b06db2288b48527fc5f36859425ff7cec07c6b367745"}, - {file = "coverage-7.3.1-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:636a8ac0b044cfeccae76a36f3b18264edcc810a76a49884b96dd744613ec0b7"}, - {file = "coverage-7.3.1-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:5d991e13ad2ed3aced177f524e4d670f304c8233edad3210e02c465351f785a0"}, - {file = "coverage-7.3.1-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:586649ada7cf139445da386ab6f8ef00e6172f11a939fc3b2b7e7c9082052fa0"}, - {file = "coverage-7.3.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:4aba512a15a3e1e4fdbfed2f5392ec221434a614cc68100ca99dcad7af29f3f8"}, - {file = "coverage-7.3.1-cp39-cp39-win32.whl", hash = "sha256:6bc6f3f4692d806831c136c5acad5ccedd0262aa44c087c46b7101c77e139140"}, - {file = "coverage-7.3.1-cp39-cp39-win_amd64.whl", hash = "sha256:553d7094cb27db58ea91332e8b5681bac107e7242c23f7629ab1316ee73c4981"}, - {file = "coverage-7.3.1-pp38.pp39.pp310-none-any.whl", hash = "sha256:220eb51f5fb38dfdb7e5d54284ca4d0cd70ddac047d750111a68ab1798945194"}, - {file = "coverage-7.3.1.tar.gz", hash = "sha256:6cb7fe1581deb67b782c153136541e20901aa312ceedaf1467dcb35255787952"}, -] - -[package.dependencies] -tomli = {version = "*", optional = true, markers = "python_full_version <= \"3.11.0a6\" and extra == \"toml\""} - -[package.extras] -toml = ["tomli"] - -[[package]] -name = "cycler" -version = "0.11.0" -description = "Composable style cycles" -optional = false -python-versions = ">=3.6" -files = [ - {file = "cycler-0.11.0-py3-none-any.whl", hash = "sha256:3a27e95f763a428a739d2add979fa7494c912a32c17c4c38c4d5f082cad165a3"}, - {file = "cycler-0.11.0.tar.gz", hash = "sha256:9c87405839a19696e837b3b818fed3f5f69f16f1eec1a1ad77e043dcea9c772f"}, -] - -[[package]] -name = "distlib" -version = "0.3.7" -description = "Distribution utilities" -optional = false -python-versions = "*" -files = [ - {file = "distlib-0.3.7-py2.py3-none-any.whl", hash = "sha256:2e24928bc811348f0feb63014e97aaae3037f2cf48712d51ae61df7fd6075057"}, - {file = "distlib-0.3.7.tar.gz", hash = "sha256:9dafe54b34a028eafd95039d5e5d4851a13734540f1331060d31c9916e7147a8"}, -] - -[[package]] -name = "etuples" -version = "0.3.9" -description = "Python S-expression emulation using tuple-like objects." -optional = false -python-versions = ">=3.8" -files = [ - {file = "etuples-0.3.9.tar.gz", hash = "sha256:a474e586683d8ba8d842ba29305005ceed1c08371a4b4b0e0e232527137e5ea3"}, -] - -[package.dependencies] -cons = "*" -multipledispatch = "*" - -[[package]] -name = "exceptiongroup" -version = "1.1.3" -description = "Backport of PEP 654 (exception groups)" -optional = false -python-versions = ">=3.7" -files = [ - {file = "exceptiongroup-1.1.3-py3-none-any.whl", hash = "sha256:343280667a4585d195ca1cf9cef84a4e178c4b6cf2274caef9859782b567d5e3"}, - {file = "exceptiongroup-1.1.3.tar.gz", hash = "sha256:097acd85d473d75af5bb98e41b61ff7fe35efe6675e4f9370ec6ec5126d160e9"}, -] - -[package.extras] -test = ["pytest (>=6)"] - -[[package]] -name = "execnet" -version = "2.0.2" -description = "execnet: rapid multi-Python deployment" -optional = false -python-versions = ">=3.7" -files = [ - {file = "execnet-2.0.2-py3-none-any.whl", hash = "sha256:88256416ae766bc9e8895c76a87928c0012183da3cc4fc18016e6f050e025f41"}, - {file = "execnet-2.0.2.tar.gz", hash = "sha256:cc59bc4423742fd71ad227122eb0dd44db51efb3dc4095b45ac9a08c770096af"}, -] - -[package.extras] -testing = ["hatch", "pre-commit", "pytest", "tox"] - -[[package]] -name = "fastprogress" -version = "1.0.3" -description = "A nested progress with plotting options for fastai" -optional = false -python-versions = ">=3.6" -files = [ - {file = "fastprogress-1.0.3-py3-none-any.whl", hash = "sha256:6dfea88f7a4717b0a8d6ee2048beae5dbed369f932a368c5dd9caff34796f7c5"}, - {file = "fastprogress-1.0.3.tar.gz", hash = "sha256:7a17d2b438890f838c048eefce32c4ded47197ecc8ea042cecc33d3deb8022f5"}, -] - -[[package]] -name = "filelock" -version = "3.12.4" -description = "A platform independent file lock." -optional = false -python-versions = ">=3.8" -files = [ - {file = "filelock-3.12.4-py3-none-any.whl", hash = "sha256:08c21d87ded6e2b9da6728c3dff51baf1dcecf973b768ef35bcbc3447edb9ad4"}, - {file = "filelock-3.12.4.tar.gz", hash = "sha256:2e6f249f1f3654291606e046b09f1fd5eac39b360664c27f5aad072012f8bcbd"}, -] - -[package.extras] -docs = ["furo (>=2023.7.26)", "sphinx (>=7.1.2)", "sphinx-autodoc-typehints (>=1.24)"] -testing = ["covdefaults (>=2.3)", "coverage (>=7.3)", "diff-cover (>=7.7)", "pytest (>=7.4)", "pytest-cov (>=4.1)", "pytest-mock (>=3.11.1)", "pytest-timeout (>=2.1)"] -typing = ["typing-extensions (>=4.7.1)"] - -[[package]] -name = "fonttools" -version = "4.42.1" -description = "Tools to manipulate font files" -optional = false -python-versions = ">=3.8" -files = [ - {file = "fonttools-4.42.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:ed1a13a27f59d1fc1920394a7f596792e9d546c9ca5a044419dca70c37815d7c"}, - {file = "fonttools-4.42.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:c9b1ce7a45978b821a06d375b83763b27a3a5e8a2e4570b3065abad240a18760"}, - {file = "fonttools-4.42.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f720fa82a11c0f9042376fd509b5ed88dab7e3cd602eee63a1af08883b37342b"}, - {file = "fonttools-4.42.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:db55cbaea02a20b49fefbd8e9d62bd481aaabe1f2301dabc575acc6b358874fa"}, - {file = "fonttools-4.42.1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:3a35981d90feebeaef05e46e33e6b9e5b5e618504672ca9cd0ff96b171e4bfff"}, - {file = "fonttools-4.42.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:68a02bbe020dc22ee0540e040117535f06df9358106d3775e8817d826047f3fd"}, - {file = "fonttools-4.42.1-cp310-cp310-win32.whl", hash = "sha256:12a7c247d1b946829bfa2f331107a629ea77dc5391dfd34fdcd78efa61f354ca"}, - {file = "fonttools-4.42.1-cp310-cp310-win_amd64.whl", hash = "sha256:a398bdadb055f8de69f62b0fc70625f7cbdab436bbb31eef5816e28cab083ee8"}, - {file = "fonttools-4.42.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:689508b918332fb40ce117131633647731d098b1b10d092234aa959b4251add5"}, - {file = "fonttools-4.42.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:9e36344e48af3e3bde867a1ca54f97c308735dd8697005c2d24a86054a114a71"}, - {file = "fonttools-4.42.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:19b7db825c8adee96fac0692e6e1ecd858cae9affb3b4812cdb9d934a898b29e"}, - {file = "fonttools-4.42.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:113337c2d29665839b7d90b39f99b3cac731f72a0eda9306165a305c7c31d341"}, - {file = "fonttools-4.42.1-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:37983b6bdab42c501202500a2be3a572f50d4efe3237e0686ee9d5f794d76b35"}, - {file = "fonttools-4.42.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:6ed2662a3d9c832afa36405f8748c250be94ae5dfc5283d668308391f2102861"}, - {file = "fonttools-4.42.1-cp311-cp311-win32.whl", hash = "sha256:179737095eb98332a2744e8f12037b2977f22948cf23ff96656928923ddf560a"}, - {file = "fonttools-4.42.1-cp311-cp311-win_amd64.whl", hash = "sha256:f2b82f46917d8722e6b5eafeefb4fb585d23babd15d8246c664cd88a5bddd19c"}, - {file = "fonttools-4.42.1-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:62f481ac772fd68901573956231aea3e4b1ad87b9b1089a61613a91e2b50bb9b"}, - {file = "fonttools-4.42.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:f2f806990160d1ce42d287aa419df3ffc42dfefe60d473695fb048355fe0c6a0"}, - {file = "fonttools-4.42.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:db372213d39fa33af667c2aa586a0c1235e88e9c850f5dd5c8e1f17515861868"}, - {file = "fonttools-4.42.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5d18fc642fd0ac29236ff88ecfccff229ec0386090a839dd3f1162e9a7944a40"}, - {file = "fonttools-4.42.1-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:8708b98c278012ad267ee8a7433baeb809948855e81922878118464b274c909d"}, - {file = "fonttools-4.42.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:c95b0724a6deea2c8c5d3222191783ced0a2f09bd6d33f93e563f6f1a4b3b3a4"}, - {file = "fonttools-4.42.1-cp38-cp38-win32.whl", hash = "sha256:4aa79366e442dbca6e2c8595645a3a605d9eeabdb7a094d745ed6106816bef5d"}, - {file = "fonttools-4.42.1-cp38-cp38-win_amd64.whl", hash = "sha256:acb47f6f8680de24c1ab65ebde39dd035768e2a9b571a07c7b8da95f6c8815fd"}, - {file = "fonttools-4.42.1-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:5fb289b7a815638a7613d46bcf324c9106804725b2bb8ad913c12b6958ffc4ec"}, - {file = "fonttools-4.42.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:53eb5091ddc8b1199330bb7b4a8a2e7995ad5d43376cadce84523d8223ef3136"}, - {file = "fonttools-4.42.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:46a0ec8adbc6ff13494eb0c9c2e643b6f009ce7320cf640de106fb614e4d4360"}, - {file = "fonttools-4.42.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7cc7d685b8eeca7ae69dc6416833fbfea61660684b7089bca666067cb2937dcf"}, - {file = "fonttools-4.42.1-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:be24fcb80493b2c94eae21df70017351851652a37de514de553435b256b2f249"}, - {file = "fonttools-4.42.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:515607ec756d7865f23070682622c49d922901943697871fc292277cf1e71967"}, - {file = "fonttools-4.42.1-cp39-cp39-win32.whl", hash = "sha256:0eb79a2da5eb6457a6f8ab904838454accc7d4cccdaff1fd2bd3a0679ea33d64"}, - {file = "fonttools-4.42.1-cp39-cp39-win_amd64.whl", hash = "sha256:7286aed4ea271df9eab8d7a9b29e507094b51397812f7ce051ecd77915a6e26b"}, - {file = "fonttools-4.42.1-py3-none-any.whl", hash = "sha256:9398f244e28e0596e2ee6024f808b06060109e33ed38dcc9bded452fd9bbb853"}, - {file = "fonttools-4.42.1.tar.gz", hash = "sha256:c391cd5af88aacaf41dd7cfb96eeedfad297b5899a39e12f4c2c3706d0a3329d"}, -] - -[package.extras] -all = ["brotli (>=1.0.1)", "brotlicffi (>=0.8.0)", "fs (>=2.2.0,<3)", "lxml (>=4.0,<5)", "lz4 (>=1.7.4.2)", "matplotlib", "munkres", "scipy", "skia-pathops (>=0.5.0)", "sympy", "uharfbuzz (>=0.23.0)", "unicodedata2 (>=15.0.0)", "xattr", "zopfli (>=0.1.4)"] -graphite = ["lz4 (>=1.7.4.2)"] -interpolatable = ["munkres", "scipy"] -lxml = ["lxml (>=4.0,<5)"] -pathops = ["skia-pathops (>=0.5.0)"] -plot = ["matplotlib"] -repacker = ["uharfbuzz (>=0.23.0)"] -symfont = ["sympy"] -type1 = ["xattr"] -ufo = ["fs (>=2.2.0,<3)"] -unicode = ["unicodedata2 (>=15.0.0)"] -woff = ["brotli (>=1.0.1)", "brotlicffi (>=0.8.0)", "zopfli (>=0.1.4)"] - -[[package]] -name = "ghp-import" -version = "2.1.0" -description = "Copy your docs directly to the gh-pages branch." -optional = false -python-versions = "*" -files = [ - {file = "ghp-import-2.1.0.tar.gz", hash = "sha256:9c535c4c61193c2df8871222567d7fd7e5014d835f97dc7b7439069e2413d343"}, - {file = "ghp_import-2.1.0-py3-none-any.whl", hash = "sha256:8337dd7b50877f163d4c0289bc1f1c7f127550241988d568c1db512c4324a619"}, -] - -[package.dependencies] -python-dateutil = ">=2.8.1" - -[package.extras] -dev = ["flake8", "markdown", "twine", "wheel"] - -[[package]] -name = "griffe" -version = "0.36.2" -description = "Signatures for entire Python programs. Extract the structure, the frame, the skeleton of your project, to generate API documentation or find breaking changes in your API." -optional = false -python-versions = ">=3.8" -files = [ - {file = "griffe-0.36.2-py3-none-any.whl", hash = "sha256:ba71895a3f5f606b18dcd950e8a1f8e7332a37f90f24caeb002546593f2e0eee"}, - {file = "griffe-0.36.2.tar.gz", hash = "sha256:333ade7932bb9096781d83092602625dfbfe220e87a039d2801259a1bd41d1c2"}, -] - -[package.dependencies] -colorama = ">=0.4" - -[[package]] -name = "h5netcdf" -version = "1.2.0" -description = "netCDF4 via h5py" -optional = false -python-versions = ">=3.9" -files = [ - {file = "h5netcdf-1.2.0-py3-none-any.whl", hash = "sha256:aa53c39b94bcd4595a2e5a2f62f3fb4fb8a723b5ca0a05f2db352f014bcfe72c"}, - {file = "h5netcdf-1.2.0.tar.gz", hash = "sha256:7f6b2733bde06ea2575b79a6450d9bd5c38918ff4cb2a355bf22bbe8c86c6bcf"}, -] - -[package.dependencies] -h5py = "*" -packaging = "*" - -[package.extras] -test = ["netCDF4", "pytest"] - -[[package]] -name = "h5py" -version = "3.9.0" -description = "Read and write HDF5 files from Python" -optional = false -python-versions = ">=3.8" -files = [ - {file = "h5py-3.9.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:eb7bdd5e601dd1739698af383be03f3dad0465fe67184ebd5afca770f50df9d6"}, - {file = "h5py-3.9.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:78e44686334cbbf2dd21d9df15823bc38663f27a3061f6a032c68a3e30c47bf7"}, - {file = "h5py-3.9.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f68b41efd110ce9af1cbe6fa8af9f4dcbadace6db972d30828b911949e28fadd"}, - {file = "h5py-3.9.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:12aa556d540f11a2cae53ea7cfb94017353bd271fb3962e1296b342f6550d1b8"}, - {file = "h5py-3.9.0-cp310-cp310-win_amd64.whl", hash = "sha256:d97409e17915798029e297a84124705c8080da901307ea58f29234e09b073ddc"}, - {file = "h5py-3.9.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:551e358db05a874a0f827b22e95b30092f2303edc4b91bb62ad2f10e0236e1a0"}, - {file = "h5py-3.9.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:6822a814b9d8b8363ff102f76ea8d026f0ca25850bb579d85376029ee3e73b93"}, - {file = "h5py-3.9.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:54f01202cdea754ab4227dd27014bdbd561a4bbe4b631424fd812f7c2ce9c6ac"}, - {file = "h5py-3.9.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:64acceaf6aff92af091a4b83f6dee3cf8d3061f924a6bb3a33eb6c4658a8348b"}, - {file = "h5py-3.9.0-cp311-cp311-win_amd64.whl", hash = "sha256:804c7fb42a34c8ab3a3001901c977a5c24d2e9c586a0f3e7c0a389130b4276fc"}, - {file = "h5py-3.9.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:8d9492391ff5c3c80ec30ae2fe82a3f0efd1e750833739c25b0d090e3be1b095"}, - {file = "h5py-3.9.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:9da9e7e63376c32704e37ad4cea2dceae6964cee0d8515185b3ab9cbd6b947bc"}, - {file = "h5py-3.9.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a4e20897c88759cbcbd38fb45b507adc91af3e0f67722aa302d71f02dd44d286"}, - {file = "h5py-3.9.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dbf5225543ca35ce9f61c950b73899a82be7ba60d58340e76d0bd42bf659235a"}, - {file = "h5py-3.9.0-cp38-cp38-win_amd64.whl", hash = "sha256:36408f8c62f50007d14e000f9f3acf77e103b9e932c114cbe52a3089e50ebf94"}, - {file = "h5py-3.9.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:23e74b878bbe1653ab34ca49b83cac85529cd0b36b9d625516c5830cc5ca2eac"}, - {file = "h5py-3.9.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:3f457089c5d524b7998e3649bc63240679b8fb0a3859ea53bbb06841f3d755f1"}, - {file = "h5py-3.9.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a6284061f3214335e1eec883a6ee497dbe7a79f19e6a57fed2dd1f03acd5a8cb"}, - {file = "h5py-3.9.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:95f7a745efd0d56076999b52e8da5fad5d30823bac98b59c68ae75588d09991a"}, - {file = "h5py-3.9.0-cp39-cp39-win_amd64.whl", hash = "sha256:79bbca34696c6f9eeeb36a91776070c49a060b2879828e2c8fa6c58b8ed10dd1"}, - {file = "h5py-3.9.0.tar.gz", hash = "sha256:e604db6521c1e367c6bd7fad239c847f53cc46646f2d2651372d05ae5e95f817"}, -] - -[package.dependencies] -numpy = ">=1.17.3" - -[[package]] -name = "identify" -version = "2.5.29" -description = "File identification library for Python" -optional = false -python-versions = ">=3.8" -files = [ - {file = "identify-2.5.29-py2.py3-none-any.whl", hash = "sha256:24437fbf6f4d3fe6efd0eb9d67e24dd9106db99af5ceb27996a5f7895f24bf1b"}, - {file = "identify-2.5.29.tar.gz", hash = "sha256:d43d52b86b15918c137e3a74fff5224f60385cd0e9c38e99d07c257f02f151a5"}, -] - -[package.extras] -license = ["ukkonen"] - -[[package]] -name = "idna" -version = "3.4" -description = "Internationalized Domain Names in Applications (IDNA)" -optional = false -python-versions = ">=3.5" -files = [ - {file = "idna-3.4-py3-none-any.whl", hash = "sha256:90b77e79eaa3eba6de819a0c442c0b4ceefc341a7a2ab77d7562bf49f425c5c2"}, - {file = "idna-3.4.tar.gz", hash = "sha256:814f528e8dead7d329833b91c5faa87d60bf71824cd12a7530b5526063d02cb4"}, -] - -[[package]] -name = "iniconfig" -version = "2.0.0" -description = "brain-dead simple config-ini parsing" -optional = false -python-versions = ">=3.7" -files = [ - {file = "iniconfig-2.0.0-py3-none-any.whl", hash = "sha256:b6a85871a79d2e3b22d2d1b94ac2824226a63c6b741c88f7ae975f18b6778374"}, - {file = "iniconfig-2.0.0.tar.gz", hash = "sha256:2d91e135bf72d31a410b17c16da610a82cb55f6b0477d1a902134b24a455b8b3"}, -] - -[[package]] -name = "jinja2" -version = "3.1.2" -description = "A very fast and expressive template engine." -optional = false -python-versions = ">=3.7" -files = [ - {file = "Jinja2-3.1.2-py3-none-any.whl", hash = "sha256:6088930bfe239f0e6710546ab9c19c9ef35e29792895fed6e6e31a023a182a61"}, - {file = "Jinja2-3.1.2.tar.gz", hash = "sha256:31351a702a408a9e7595a8fc6150fc3f43bb6bf7e319770cbc0db9df9437e852"}, -] - -[package.dependencies] -MarkupSafe = ">=2.0" - -[package.extras] -i18n = ["Babel (>=2.7)"] - -[[package]] -name = "joblib" -version = "1.3.2" -description = "Lightweight pipelining with Python functions" -optional = false -python-versions = ">=3.7" -files = [ - {file = "joblib-1.3.2-py3-none-any.whl", hash = "sha256:ef4331c65f239985f3f2220ecc87db222f08fd22097a3dd5698f693875f8cbb9"}, - {file = "joblib-1.3.2.tar.gz", hash = "sha256:92f865e621e17784e7955080b6d042489e3b8e294949cc44c6eac304f59772b1"}, -] - -[[package]] -name = "kiwisolver" -version = "1.4.5" -description = "A fast implementation of the Cassowary constraint solver" -optional = false -python-versions = ">=3.7" -files = [ - {file = "kiwisolver-1.4.5-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:05703cf211d585109fcd72207a31bb170a0f22144d68298dc5e61b3c946518af"}, - {file = "kiwisolver-1.4.5-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:146d14bebb7f1dc4d5fbf74f8a6cb15ac42baadee8912eb84ac0b3b2a3dc6ac3"}, - {file = "kiwisolver-1.4.5-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:6ef7afcd2d281494c0a9101d5c571970708ad911d028137cd558f02b851c08b4"}, - {file = "kiwisolver-1.4.5-cp310-cp310-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:9eaa8b117dc8337728e834b9c6e2611f10c79e38f65157c4c38e9400286f5cb1"}, - {file = "kiwisolver-1.4.5-cp310-cp310-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:ec20916e7b4cbfb1f12380e46486ec4bcbaa91a9c448b97023fde0d5bbf9e4ff"}, - {file = "kiwisolver-1.4.5-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:39b42c68602539407884cf70d6a480a469b93b81b7701378ba5e2328660c847a"}, - {file = "kiwisolver-1.4.5-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:aa12042de0171fad672b6c59df69106d20d5596e4f87b5e8f76df757a7c399aa"}, - {file = "kiwisolver-1.4.5-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2a40773c71d7ccdd3798f6489aaac9eee213d566850a9533f8d26332d626b82c"}, - {file = "kiwisolver-1.4.5-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:19df6e621f6d8b4b9c4d45f40a66839294ff2bb235e64d2178f7522d9170ac5b"}, - {file = "kiwisolver-1.4.5-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:83d78376d0d4fd884e2c114d0621624b73d2aba4e2788182d286309ebdeed770"}, - {file = "kiwisolver-1.4.5-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:e391b1f0a8a5a10ab3b9bb6afcfd74f2175f24f8975fb87ecae700d1503cdee0"}, - {file = "kiwisolver-1.4.5-cp310-cp310-musllinux_1_1_s390x.whl", hash = "sha256:852542f9481f4a62dbb5dd99e8ab7aedfeb8fb6342349a181d4036877410f525"}, - {file = "kiwisolver-1.4.5-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:59edc41b24031bc25108e210c0def6f6c2191210492a972d585a06ff246bb79b"}, - {file = "kiwisolver-1.4.5-cp310-cp310-win32.whl", hash = "sha256:a6aa6315319a052b4ee378aa171959c898a6183f15c1e541821c5c59beaa0238"}, - {file = "kiwisolver-1.4.5-cp310-cp310-win_amd64.whl", hash = "sha256:d0ef46024e6a3d79c01ff13801cb19d0cad7fd859b15037aec74315540acc276"}, - {file = "kiwisolver-1.4.5-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:11863aa14a51fd6ec28688d76f1735f8f69ab1fabf388851a595d0721af042f5"}, - {file = "kiwisolver-1.4.5-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:8ab3919a9997ab7ef2fbbed0cc99bb28d3c13e6d4b1ad36e97e482558a91be90"}, - {file = "kiwisolver-1.4.5-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:fcc700eadbbccbf6bc1bcb9dbe0786b4b1cb91ca0dcda336eef5c2beed37b797"}, - {file = "kiwisolver-1.4.5-cp311-cp311-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:dfdd7c0b105af050eb3d64997809dc21da247cf44e63dc73ff0fd20b96be55a9"}, - {file = "kiwisolver-1.4.5-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:76c6a5964640638cdeaa0c359382e5703e9293030fe730018ca06bc2010c4437"}, - {file = "kiwisolver-1.4.5-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:bbea0db94288e29afcc4c28afbf3a7ccaf2d7e027489c449cf7e8f83c6346eb9"}, - {file = "kiwisolver-1.4.5-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:ceec1a6bc6cab1d6ff5d06592a91a692f90ec7505d6463a88a52cc0eb58545da"}, - {file = "kiwisolver-1.4.5-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:040c1aebeda72197ef477a906782b5ab0d387642e93bda547336b8957c61022e"}, - {file = "kiwisolver-1.4.5-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:f91de7223d4c7b793867797bacd1ee53bfe7359bd70d27b7b58a04efbb9436c8"}, - {file = "kiwisolver-1.4.5-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:faae4860798c31530dd184046a900e652c95513796ef51a12bc086710c2eec4d"}, - {file = "kiwisolver-1.4.5-cp311-cp311-musllinux_1_1_ppc64le.whl", hash = "sha256:b0157420efcb803e71d1b28e2c287518b8808b7cf1ab8af36718fd0a2c453eb0"}, - {file = "kiwisolver-1.4.5-cp311-cp311-musllinux_1_1_s390x.whl", hash = "sha256:06f54715b7737c2fecdbf140d1afb11a33d59508a47bf11bb38ecf21dc9ab79f"}, - {file = "kiwisolver-1.4.5-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:fdb7adb641a0d13bdcd4ef48e062363d8a9ad4a182ac7647ec88f695e719ae9f"}, - {file = "kiwisolver-1.4.5-cp311-cp311-win32.whl", hash = "sha256:bb86433b1cfe686da83ce32a9d3a8dd308e85c76b60896d58f082136f10bffac"}, - {file = "kiwisolver-1.4.5-cp311-cp311-win_amd64.whl", hash = "sha256:6c08e1312a9cf1074d17b17728d3dfce2a5125b2d791527f33ffbe805200a355"}, - {file = "kiwisolver-1.4.5-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:32d5cf40c4f7c7b3ca500f8985eb3fb3a7dfc023215e876f207956b5ea26632a"}, - {file = "kiwisolver-1.4.5-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:f846c260f483d1fd217fe5ed7c173fb109efa6b1fc8381c8b7552c5781756192"}, - {file = "kiwisolver-1.4.5-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:5ff5cf3571589b6d13bfbfd6bcd7a3f659e42f96b5fd1c4830c4cf21d4f5ef45"}, - {file = "kiwisolver-1.4.5-cp312-cp312-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7269d9e5f1084a653d575c7ec012ff57f0c042258bf5db0954bf551c158466e7"}, - {file = "kiwisolver-1.4.5-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:da802a19d6e15dffe4b0c24b38b3af68e6c1a68e6e1d8f30148c83864f3881db"}, - {file = "kiwisolver-1.4.5-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3aba7311af82e335dd1e36ffff68aaca609ca6290c2cb6d821a39aa075d8e3ff"}, - {file = "kiwisolver-1.4.5-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:763773d53f07244148ccac5b084da5adb90bfaee39c197554f01b286cf869228"}, - {file = "kiwisolver-1.4.5-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2270953c0d8cdab5d422bee7d2007f043473f9d2999631c86a223c9db56cbd16"}, - {file = "kiwisolver-1.4.5-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:d099e745a512f7e3bbe7249ca835f4d357c586d78d79ae8f1dcd4d8adeb9bda9"}, - {file = "kiwisolver-1.4.5-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:74db36e14a7d1ce0986fa104f7d5637aea5c82ca6326ed0ec5694280942d1162"}, - {file = "kiwisolver-1.4.5-cp312-cp312-musllinux_1_1_ppc64le.whl", hash = "sha256:7e5bab140c309cb3a6ce373a9e71eb7e4873c70c2dda01df6820474f9889d6d4"}, - {file = "kiwisolver-1.4.5-cp312-cp312-musllinux_1_1_s390x.whl", hash = "sha256:0f114aa76dc1b8f636d077979c0ac22e7cd8f3493abbab152f20eb8d3cda71f3"}, - {file = "kiwisolver-1.4.5-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:88a2df29d4724b9237fc0c6eaf2a1adae0cdc0b3e9f4d8e7dc54b16812d2d81a"}, - {file = "kiwisolver-1.4.5-cp312-cp312-win32.whl", hash = "sha256:72d40b33e834371fd330fb1472ca19d9b8327acb79a5821d4008391db8e29f20"}, - {file = "kiwisolver-1.4.5-cp312-cp312-win_amd64.whl", hash = "sha256:2c5674c4e74d939b9d91dda0fae10597ac7521768fec9e399c70a1f27e2ea2d9"}, - {file = "kiwisolver-1.4.5-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:3a2b053a0ab7a3960c98725cfb0bf5b48ba82f64ec95fe06f1d06c99b552e130"}, - {file = "kiwisolver-1.4.5-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3cd32d6c13807e5c66a7cbb79f90b553642f296ae4518a60d8d76243b0ad2898"}, - {file = "kiwisolver-1.4.5-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:59ec7b7c7e1a61061850d53aaf8e93db63dce0c936db1fda2658b70e4a1be709"}, - {file = "kiwisolver-1.4.5-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:da4cfb373035def307905d05041c1d06d8936452fe89d464743ae7fb8371078b"}, - {file = "kiwisolver-1.4.5-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:2400873bccc260b6ae184b2b8a4fec0e4082d30648eadb7c3d9a13405d861e89"}, - {file = "kiwisolver-1.4.5-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:1b04139c4236a0f3aff534479b58f6f849a8b351e1314826c2d230849ed48985"}, - {file = "kiwisolver-1.4.5-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:4e66e81a5779b65ac21764c295087de82235597a2293d18d943f8e9e32746265"}, - {file = "kiwisolver-1.4.5-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:7931d8f1f67c4be9ba1dd9c451fb0eeca1a25b89e4d3f89e828fe12a519b782a"}, - {file = "kiwisolver-1.4.5-cp37-cp37m-musllinux_1_1_ppc64le.whl", hash = "sha256:b3f7e75f3015df442238cca659f8baa5f42ce2a8582727981cbfa15fee0ee205"}, - {file = "kiwisolver-1.4.5-cp37-cp37m-musllinux_1_1_s390x.whl", hash = "sha256:bbf1d63eef84b2e8c89011b7f2235b1e0bf7dacc11cac9431fc6468e99ac77fb"}, - {file = "kiwisolver-1.4.5-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:4c380469bd3f970ef677bf2bcba2b6b0b4d5c75e7a020fb863ef75084efad66f"}, - {file = "kiwisolver-1.4.5-cp37-cp37m-win32.whl", hash = "sha256:9408acf3270c4b6baad483865191e3e582b638b1654a007c62e3efe96f09a9a3"}, - {file = "kiwisolver-1.4.5-cp37-cp37m-win_amd64.whl", hash = "sha256:5b94529f9b2591b7af5f3e0e730a4e0a41ea174af35a4fd067775f9bdfeee01a"}, - {file = "kiwisolver-1.4.5-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:11c7de8f692fc99816e8ac50d1d1aef4f75126eefc33ac79aac02c099fd3db71"}, - {file = "kiwisolver-1.4.5-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:53abb58632235cd154176ced1ae8f0d29a6657aa1aa9decf50b899b755bc2b93"}, - {file = "kiwisolver-1.4.5-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:88b9f257ca61b838b6f8094a62418421f87ac2a1069f7e896c36a7d86b5d4c29"}, - {file = "kiwisolver-1.4.5-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3195782b26fc03aa9c6913d5bad5aeb864bdc372924c093b0f1cebad603dd712"}, - {file = "kiwisolver-1.4.5-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:fc579bf0f502e54926519451b920e875f433aceb4624a3646b3252b5caa9e0b6"}, - {file = "kiwisolver-1.4.5-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5a580c91d686376f0f7c295357595c5a026e6cbc3d77b7c36e290201e7c11ecb"}, - {file = "kiwisolver-1.4.5-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:cfe6ab8da05c01ba6fbea630377b5da2cd9bcbc6338510116b01c1bc939a2c18"}, - {file = "kiwisolver-1.4.5-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:d2e5a98f0ec99beb3c10e13b387f8db39106d53993f498b295f0c914328b1333"}, - {file = "kiwisolver-1.4.5-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:a51a263952b1429e429ff236d2f5a21c5125437861baeed77f5e1cc2d2c7c6da"}, - {file = "kiwisolver-1.4.5-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:3edd2fa14e68c9be82c5b16689e8d63d89fe927e56debd6e1dbce7a26a17f81b"}, - {file = "kiwisolver-1.4.5-cp38-cp38-musllinux_1_1_ppc64le.whl", hash = "sha256:74d1b44c6cfc897df648cc9fdaa09bc3e7679926e6f96df05775d4fb3946571c"}, - {file = "kiwisolver-1.4.5-cp38-cp38-musllinux_1_1_s390x.whl", hash = "sha256:76d9289ed3f7501012e05abb8358bbb129149dbd173f1f57a1bf1c22d19ab7cc"}, - {file = "kiwisolver-1.4.5-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:92dea1ffe3714fa8eb6a314d2b3c773208d865a0e0d35e713ec54eea08a66250"}, - {file = "kiwisolver-1.4.5-cp38-cp38-win32.whl", hash = "sha256:5c90ae8c8d32e472be041e76f9d2f2dbff4d0b0be8bd4041770eddb18cf49a4e"}, - {file = "kiwisolver-1.4.5-cp38-cp38-win_amd64.whl", hash = "sha256:c7940c1dc63eb37a67721b10d703247552416f719c4188c54e04334321351ced"}, - {file = "kiwisolver-1.4.5-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:9407b6a5f0d675e8a827ad8742e1d6b49d9c1a1da5d952a67d50ef5f4170b18d"}, - {file = "kiwisolver-1.4.5-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:15568384086b6df3c65353820a4473575dbad192e35010f622c6ce3eebd57af9"}, - {file = "kiwisolver-1.4.5-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:0dc9db8e79f0036e8173c466d21ef18e1befc02de8bf8aa8dc0813a6dc8a7046"}, - {file = "kiwisolver-1.4.5-cp39-cp39-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:cdc8a402aaee9a798b50d8b827d7ecf75edc5fb35ea0f91f213ff927c15f4ff0"}, - {file = "kiwisolver-1.4.5-cp39-cp39-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:6c3bd3cde54cafb87d74d8db50b909705c62b17c2099b8f2e25b461882e544ff"}, - {file = "kiwisolver-1.4.5-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:955e8513d07a283056b1396e9a57ceddbd272d9252c14f154d450d227606eb54"}, - {file = "kiwisolver-1.4.5-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:346f5343b9e3f00b8db8ba359350eb124b98c99efd0b408728ac6ebf38173958"}, - {file = "kiwisolver-1.4.5-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b9098e0049e88c6a24ff64545cdfc50807818ba6c1b739cae221bbbcbc58aad3"}, - {file = "kiwisolver-1.4.5-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:00bd361b903dc4bbf4eb165f24d1acbee754fce22ded24c3d56eec268658a5cf"}, - {file = "kiwisolver-1.4.5-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:7b8b454bac16428b22560d0a1cf0a09875339cab69df61d7805bf48919415901"}, - {file = "kiwisolver-1.4.5-cp39-cp39-musllinux_1_1_ppc64le.whl", hash = "sha256:f1d072c2eb0ad60d4c183f3fb44ac6f73fb7a8f16a2694a91f988275cbf352f9"}, - {file = "kiwisolver-1.4.5-cp39-cp39-musllinux_1_1_s390x.whl", hash = "sha256:31a82d498054cac9f6d0b53d02bb85811185bcb477d4b60144f915f3b3126342"}, - {file = "kiwisolver-1.4.5-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:6512cb89e334e4700febbffaaa52761b65b4f5a3cf33f960213d5656cea36a77"}, - {file = "kiwisolver-1.4.5-cp39-cp39-win32.whl", hash = "sha256:9db8ea4c388fdb0f780fe91346fd438657ea602d58348753d9fb265ce1bca67f"}, - {file = "kiwisolver-1.4.5-cp39-cp39-win_amd64.whl", hash = "sha256:59415f46a37f7f2efeec758353dd2eae1b07640d8ca0f0c42548ec4125492635"}, - {file = "kiwisolver-1.4.5-pp37-pypy37_pp73-macosx_10_9_x86_64.whl", hash = "sha256:5c7b3b3a728dc6faf3fc372ef24f21d1e3cee2ac3e9596691d746e5a536de920"}, - {file = "kiwisolver-1.4.5-pp37-pypy37_pp73-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:620ced262a86244e2be10a676b646f29c34537d0d9cc8eb26c08f53d98013390"}, - {file = "kiwisolver-1.4.5-pp37-pypy37_pp73-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:378a214a1e3bbf5ac4a8708304318b4f890da88c9e6a07699c4ae7174c09a68d"}, - {file = "kiwisolver-1.4.5-pp37-pypy37_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:aaf7be1207676ac608a50cd08f102f6742dbfc70e8d60c4db1c6897f62f71523"}, - {file = "kiwisolver-1.4.5-pp37-pypy37_pp73-win_amd64.whl", hash = "sha256:ba55dce0a9b8ff59495ddd050a0225d58bd0983d09f87cfe2b6aec4f2c1234e4"}, - {file = "kiwisolver-1.4.5-pp38-pypy38_pp73-macosx_10_9_x86_64.whl", hash = "sha256:fd32ea360bcbb92d28933fc05ed09bffcb1704ba3fc7942e81db0fd4f81a7892"}, - {file = "kiwisolver-1.4.5-pp38-pypy38_pp73-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:5e7139af55d1688f8b960ee9ad5adafc4ac17c1c473fe07133ac092310d76544"}, - {file = "kiwisolver-1.4.5-pp38-pypy38_pp73-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:dced8146011d2bc2e883f9bd68618b8247387f4bbec46d7392b3c3b032640126"}, - {file = "kiwisolver-1.4.5-pp38-pypy38_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c9bf3325c47b11b2e51bca0824ea217c7cd84491d8ac4eefd1e409705ef092bd"}, - {file = "kiwisolver-1.4.5-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:5794cf59533bc3f1b1c821f7206a3617999db9fbefc345360aafe2e067514929"}, - {file = "kiwisolver-1.4.5-pp39-pypy39_pp73-macosx_10_9_x86_64.whl", hash = "sha256:e368f200bbc2e4f905b8e71eb38b3c04333bddaa6a2464a6355487b02bb7fb09"}, - {file = "kiwisolver-1.4.5-pp39-pypy39_pp73-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e5d706eba36b4c4d5bc6c6377bb6568098765e990cfc21ee16d13963fab7b3e7"}, - {file = "kiwisolver-1.4.5-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:85267bd1aa8880a9c88a8cb71e18d3d64d2751a790e6ca6c27b8ccc724bcd5ad"}, - {file = "kiwisolver-1.4.5-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:210ef2c3a1f03272649aff1ef992df2e724748918c4bc2d5a90352849eb40bea"}, - {file = "kiwisolver-1.4.5-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:11d011a7574eb3b82bcc9c1a1d35c1d7075677fdd15de527d91b46bd35e935ee"}, - {file = "kiwisolver-1.4.5.tar.gz", hash = "sha256:e57e563a57fb22a142da34f38acc2fc1a5c864bc29ca1517a88abc963e60d6ec"}, -] - -[[package]] -name = "logical-unification" -version = "0.4.6" -description = "Logical unification in Python" -optional = false -python-versions = ">=3.6" -files = [ - {file = "logical-unification-0.4.6.tar.gz", hash = "sha256:908435123f8a106fa4dcf9bf1b75c7beb309fa2bbecf277868af8f1c212650a0"}, -] - -[package.dependencies] -multipledispatch = "*" -toolz = "*" - -[[package]] -name = "markdown" -version = "3.4.4" -description = "Python implementation of John Gruber's Markdown." -optional = false -python-versions = ">=3.7" -files = [ - {file = "Markdown-3.4.4-py3-none-any.whl", hash = "sha256:a4c1b65c0957b4bd9e7d86ddc7b3c9868fb9670660f6f99f6d1bca8954d5a941"}, - {file = "Markdown-3.4.4.tar.gz", hash = "sha256:225c6123522495d4119a90b3a3ba31a1e87a70369e03f14799ea9c0d7183a3d6"}, -] - -[package.extras] -docs = ["mdx-gh-links (>=0.2)", "mkdocs (>=1.0)", "mkdocs-nature (>=0.4)"] -testing = ["coverage", "pyyaml"] - -[[package]] -name = "markupsafe" -version = "2.1.3" -description = "Safely add untrusted strings to HTML/XML markup." -optional = false -python-versions = ">=3.7" -files = [ - {file = "MarkupSafe-2.1.3-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:cd0f502fe016460680cd20aaa5a76d241d6f35a1c3350c474bac1273803893fa"}, - {file = "MarkupSafe-2.1.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:e09031c87a1e51556fdcb46e5bd4f59dfb743061cf93c4d6831bf894f125eb57"}, - {file = "MarkupSafe-2.1.3-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:68e78619a61ecf91e76aa3e6e8e33fc4894a2bebe93410754bd28fce0a8a4f9f"}, - {file = "MarkupSafe-2.1.3-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:65c1a9bcdadc6c28eecee2c119465aebff8f7a584dd719facdd9e825ec61ab52"}, - {file = "MarkupSafe-2.1.3-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:525808b8019e36eb524b8c68acdd63a37e75714eac50e988180b169d64480a00"}, - {file = "MarkupSafe-2.1.3-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:962f82a3086483f5e5f64dbad880d31038b698494799b097bc59c2edf392fce6"}, - {file = "MarkupSafe-2.1.3-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:aa7bd130efab1c280bed0f45501b7c8795f9fdbeb02e965371bbef3523627779"}, - {file = "MarkupSafe-2.1.3-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:c9c804664ebe8f83a211cace637506669e7890fec1b4195b505c214e50dd4eb7"}, - {file = "MarkupSafe-2.1.3-cp310-cp310-win32.whl", hash = "sha256:10bbfe99883db80bdbaff2dcf681dfc6533a614f700da1287707e8a5d78a8431"}, - {file = "MarkupSafe-2.1.3-cp310-cp310-win_amd64.whl", hash = "sha256:1577735524cdad32f9f694208aa75e422adba74f1baee7551620e43a3141f559"}, - {file = "MarkupSafe-2.1.3-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:ad9e82fb8f09ade1c3e1b996a6337afac2b8b9e365f926f5a61aacc71adc5b3c"}, - {file = "MarkupSafe-2.1.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:3c0fae6c3be832a0a0473ac912810b2877c8cb9d76ca48de1ed31e1c68386575"}, - {file = "MarkupSafe-2.1.3-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b076b6226fb84157e3f7c971a47ff3a679d837cf338547532ab866c57930dbee"}, - {file = "MarkupSafe-2.1.3-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bfce63a9e7834b12b87c64d6b155fdd9b3b96191b6bd334bf37db7ff1fe457f2"}, - {file = "MarkupSafe-2.1.3-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:338ae27d6b8745585f87218a3f23f1512dbf52c26c28e322dbe54bcede54ccb9"}, - {file = "MarkupSafe-2.1.3-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:e4dd52d80b8c83fdce44e12478ad2e85c64ea965e75d66dbeafb0a3e77308fcc"}, - {file = "MarkupSafe-2.1.3-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:df0be2b576a7abbf737b1575f048c23fb1d769f267ec4358296f31c2479db8f9"}, - {file = "MarkupSafe-2.1.3-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:5bbe06f8eeafd38e5d0a4894ffec89378b6c6a625ff57e3028921f8ff59318ac"}, - {file = "MarkupSafe-2.1.3-cp311-cp311-win32.whl", hash = "sha256:dd15ff04ffd7e05ffcb7fe79f1b98041b8ea30ae9234aed2a9168b5797c3effb"}, - {file = "MarkupSafe-2.1.3-cp311-cp311-win_amd64.whl", hash = "sha256:134da1eca9ec0ae528110ccc9e48041e0828d79f24121a1a146161103c76e686"}, - {file = "MarkupSafe-2.1.3-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:f698de3fd0c4e6972b92290a45bd9b1536bffe8c6759c62471efaa8acb4c37bc"}, - {file = "MarkupSafe-2.1.3-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:aa57bd9cf8ae831a362185ee444e15a93ecb2e344c8e52e4d721ea3ab6ef1823"}, - {file = "MarkupSafe-2.1.3-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ffcc3f7c66b5f5b7931a5aa68fc9cecc51e685ef90282f4a82f0f5e9b704ad11"}, - {file = "MarkupSafe-2.1.3-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:47d4f1c5f80fc62fdd7777d0d40a2e9dda0a05883ab11374334f6c4de38adffd"}, - {file = "MarkupSafe-2.1.3-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1f67c7038d560d92149c060157d623c542173016c4babc0c1913cca0564b9939"}, - {file = "MarkupSafe-2.1.3-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:9aad3c1755095ce347e26488214ef77e0485a3c34a50c5a5e2471dff60b9dd9c"}, - {file = "MarkupSafe-2.1.3-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:14ff806850827afd6b07a5f32bd917fb7f45b046ba40c57abdb636674a8b559c"}, - {file = "MarkupSafe-2.1.3-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:8f9293864fe09b8149f0cc42ce56e3f0e54de883a9de90cd427f191c346eb2e1"}, - {file = "MarkupSafe-2.1.3-cp312-cp312-win32.whl", hash = "sha256:715d3562f79d540f251b99ebd6d8baa547118974341db04f5ad06d5ea3eb8007"}, - {file = "MarkupSafe-2.1.3-cp312-cp312-win_amd64.whl", hash = "sha256:1b8dd8c3fd14349433c79fa8abeb573a55fc0fdd769133baac1f5e07abf54aeb"}, - {file = "MarkupSafe-2.1.3-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:8e254ae696c88d98da6555f5ace2279cf7cd5b3f52be2b5cf97feafe883b58d2"}, - {file = "MarkupSafe-2.1.3-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cb0932dc158471523c9637e807d9bfb93e06a95cbf010f1a38b98623b929ef2b"}, - {file = "MarkupSafe-2.1.3-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9402b03f1a1b4dc4c19845e5c749e3ab82d5078d16a2a4c2cd2df62d57bb0707"}, - {file = "MarkupSafe-2.1.3-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ca379055a47383d02a5400cb0d110cef0a776fc644cda797db0c5696cfd7e18e"}, - {file = "MarkupSafe-2.1.3-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:b7ff0f54cb4ff66dd38bebd335a38e2c22c41a8ee45aa608efc890ac3e3931bc"}, - {file = "MarkupSafe-2.1.3-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:c011a4149cfbcf9f03994ec2edffcb8b1dc2d2aede7ca243746df97a5d41ce48"}, - {file = "MarkupSafe-2.1.3-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:56d9f2ecac662ca1611d183feb03a3fa4406469dafe241673d521dd5ae92a155"}, - {file = "MarkupSafe-2.1.3-cp37-cp37m-win32.whl", hash = "sha256:8758846a7e80910096950b67071243da3e5a20ed2546e6392603c096778d48e0"}, - {file = "MarkupSafe-2.1.3-cp37-cp37m-win_amd64.whl", hash = "sha256:787003c0ddb00500e49a10f2844fac87aa6ce977b90b0feaaf9de23c22508b24"}, - {file = "MarkupSafe-2.1.3-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:2ef12179d3a291be237280175b542c07a36e7f60718296278d8593d21ca937d4"}, - {file = "MarkupSafe-2.1.3-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:2c1b19b3aaacc6e57b7e25710ff571c24d6c3613a45e905b1fde04d691b98ee0"}, - {file = "MarkupSafe-2.1.3-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8afafd99945ead6e075b973fefa56379c5b5c53fd8937dad92c662da5d8fd5ee"}, - {file = "MarkupSafe-2.1.3-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8c41976a29d078bb235fea9b2ecd3da465df42a562910f9022f1a03107bd02be"}, - {file = "MarkupSafe-2.1.3-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d080e0a5eb2529460b30190fcfcc4199bd7f827663f858a226a81bc27beaa97e"}, - {file = "MarkupSafe-2.1.3-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:69c0f17e9f5a7afdf2cc9fb2d1ce6aabdb3bafb7f38017c0b77862bcec2bbad8"}, - {file = "MarkupSafe-2.1.3-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:504b320cd4b7eff6f968eddf81127112db685e81f7e36e75f9f84f0df46041c3"}, - {file = "MarkupSafe-2.1.3-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:42de32b22b6b804f42c5d98be4f7e5e977ecdd9ee9b660fda1a3edf03b11792d"}, - {file = "MarkupSafe-2.1.3-cp38-cp38-win32.whl", hash = "sha256:ceb01949af7121f9fc39f7d27f91be8546f3fb112c608bc4029aef0bab86a2a5"}, - {file = "MarkupSafe-2.1.3-cp38-cp38-win_amd64.whl", hash = "sha256:1b40069d487e7edb2676d3fbdb2b0829ffa2cd63a2ec26c4938b2d34391b4ecc"}, - {file = "MarkupSafe-2.1.3-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:8023faf4e01efadfa183e863fefde0046de576c6f14659e8782065bcece22198"}, - {file = "MarkupSafe-2.1.3-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:6b2b56950d93e41f33b4223ead100ea0fe11f8e6ee5f641eb753ce4b77a7042b"}, - {file = "MarkupSafe-2.1.3-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9dcdfd0eaf283af041973bff14a2e143b8bd64e069f4c383416ecd79a81aab58"}, - {file = "MarkupSafe-2.1.3-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:05fb21170423db021895e1ea1e1f3ab3adb85d1c2333cbc2310f2a26bc77272e"}, - {file = "MarkupSafe-2.1.3-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:282c2cb35b5b673bbcadb33a585408104df04f14b2d9b01d4c345a3b92861c2c"}, - {file = "MarkupSafe-2.1.3-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:ab4a0df41e7c16a1392727727e7998a467472d0ad65f3ad5e6e765015df08636"}, - {file = "MarkupSafe-2.1.3-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:7ef3cb2ebbf91e330e3bb937efada0edd9003683db6b57bb108c4001f37a02ea"}, - {file = "MarkupSafe-2.1.3-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:0a4e4a1aff6c7ac4cd55792abf96c915634c2b97e3cc1c7129578aa68ebd754e"}, - {file = "MarkupSafe-2.1.3-cp39-cp39-win32.whl", hash = "sha256:fec21693218efe39aa7f8599346e90c705afa52c5b31ae019b2e57e8f6542bb2"}, - {file = "MarkupSafe-2.1.3-cp39-cp39-win_amd64.whl", hash = "sha256:3fd4abcb888d15a94f32b75d8fd18ee162ca0c064f35b11134be77050296d6ba"}, - {file = "MarkupSafe-2.1.3.tar.gz", hash = "sha256:af598ed32d6ae86f1b747b82783958b1a4ab8f617b06fe68795c7f026abbdcad"}, -] - -[[package]] -name = "matplotlib" -version = "3.8.0" -description = "Python plotting package" -optional = false -python-versions = ">=3.9" -files = [ - {file = "matplotlib-3.8.0-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:c4940bad88a932ddc69734274f6fb047207e008389489f2b6f77d9ca485f0e7a"}, - {file = "matplotlib-3.8.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:a33bd3045c7452ca1fa65676d88ba940867880e13e2546abb143035fa9072a9d"}, - {file = "matplotlib-3.8.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2ea6886e93401c22e534bbfd39201ce8931b75502895cfb115cbdbbe2d31f287"}, - {file = "matplotlib-3.8.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d670b9348e712ec176de225d425f150dc8e37b13010d85233c539b547da0be39"}, - {file = "matplotlib-3.8.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:7b37b74f00c4cb6af908cb9a00779d97d294e89fd2145ad43f0cdc23f635760c"}, - {file = "matplotlib-3.8.0-cp310-cp310-win_amd64.whl", hash = "sha256:0e723f5b96f3cd4aad99103dc93e9e3cdc4f18afdcc76951f4857b46f8e39d2d"}, - {file = "matplotlib-3.8.0-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:5dc945a9cb2deb7d197ba23eb4c210e591d52d77bf0ba27c35fc82dec9fa78d4"}, - {file = "matplotlib-3.8.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:f8b5a1bf27d078453aa7b5b27f52580e16360d02df6d3dc9504f3d2ce11f6309"}, - {file = "matplotlib-3.8.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6f25ffb6ad972cdffa7df8e5be4b1e3cadd2f8d43fc72085feb1518006178394"}, - {file = "matplotlib-3.8.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:eee482731c8c17d86d9ddb5194d38621f9b0f0d53c99006275a12523ab021732"}, - {file = "matplotlib-3.8.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:36eafe2128772195b373e1242df28d1b7ec6c04c15b090b8d9e335d55a323900"}, - {file = "matplotlib-3.8.0-cp311-cp311-win_amd64.whl", hash = "sha256:061ee58facb3580cd2d046a6d227fb77e9295599c5ec6ad069f06b5821ad1cfc"}, - {file = "matplotlib-3.8.0-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:3cc3776836d0f4f22654a7f2d2ec2004618d5cf86b7185318381f73b80fd8a2d"}, - {file = "matplotlib-3.8.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:6c49a2bd6981264bddcb8c317b6bd25febcece9e2ebfcbc34e7f4c0c867c09dc"}, - {file = "matplotlib-3.8.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:23ed11654fc83cd6cfdf6170b453e437674a050a452133a064d47f2f1371f8d3"}, - {file = "matplotlib-3.8.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dae97fdd6996b3a25da8ee43e3fc734fff502f396801063c6b76c20b56683196"}, - {file = "matplotlib-3.8.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:87df75f528020a6299f76a1d986c0ed4406e3b2bd44bc5e306e46bca7d45e53e"}, - {file = "matplotlib-3.8.0-cp312-cp312-win_amd64.whl", hash = "sha256:90d74a95fe055f73a6cd737beecc1b81c26f2893b7a3751d52b53ff06ca53f36"}, - {file = "matplotlib-3.8.0-cp39-cp39-macosx_10_12_x86_64.whl", hash = "sha256:c3499c312f5def8f362a2bf761d04fa2d452b333f3a9a3f58805273719bf20d9"}, - {file = "matplotlib-3.8.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:31e793c8bd4ea268cc5d3a695c27b30650ec35238626961d73085d5e94b6ab68"}, - {file = "matplotlib-3.8.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0d5ee602ef517a89d1f2c508ca189cfc395dd0b4a08284fb1b97a78eec354644"}, - {file = "matplotlib-3.8.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5de39dc61ca35342cf409e031f70f18219f2c48380d3886c1cf5ad9f17898e06"}, - {file = "matplotlib-3.8.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:dd386c80a98b5f51571b9484bf6c6976de383cd2a8cd972b6a9562d85c6d2087"}, - {file = "matplotlib-3.8.0-cp39-cp39-win_amd64.whl", hash = "sha256:f691b4ef47c7384d0936b2e8ebdeb5d526c81d004ad9403dfb9d4c76b9979a93"}, - {file = "matplotlib-3.8.0-pp39-pypy39_pp73-macosx_10_12_x86_64.whl", hash = "sha256:0b11f354aae62a2aa53ec5bb09946f5f06fc41793e351a04ff60223ea9162955"}, - {file = "matplotlib-3.8.0-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7f54b9fb87ca5acbcdd0f286021bedc162e1425fa5555ebf3b3dfc167b955ad9"}, - {file = "matplotlib-3.8.0-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:60a6e04dfd77c0d3bcfee61c3cd335fff1b917c2f303b32524cd1235e194ef99"}, - {file = "matplotlib-3.8.0.tar.gz", hash = "sha256:df8505e1c19d5c2c26aff3497a7cbd3ccfc2e97043d1e4db3e76afa399164b69"}, -] - -[package.dependencies] -contourpy = ">=1.0.1" -cycler = ">=0.10" -fonttools = ">=4.22.0" -kiwisolver = ">=1.0.1" -numpy = ">=1.21,<2" -packaging = ">=20.0" -pillow = ">=6.2.0" -pyparsing = ">=2.3.1" -python-dateutil = ">=2.7" -setuptools_scm = ">=7" - -[[package]] -name = "mergedeep" -version = "1.3.4" -description = "A deep merge function for 🐍." -optional = false -python-versions = ">=3.6" -files = [ - {file = "mergedeep-1.3.4-py3-none-any.whl", hash = "sha256:70775750742b25c0d8f36c55aed03d24c3384d17c951b3175d898bd778ef0307"}, - {file = "mergedeep-1.3.4.tar.gz", hash = "sha256:0096d52e9dad9939c3d975a774666af186eda617e6ca84df4c94dec30004f2a8"}, -] - -[[package]] -name = "mhn" -version = "0.0.14" -description = "A package to train and work with Mutual Hazard Networks" -optional = false -python-versions = ">=3.8" -files = [ - {file = "mhn-0.0.14.tar.gz", hash = "sha256:0aeaa4dfc4353c8488b5012a7e3f1095f4e7b05b0ddf1c10793cc313fa21c7a9"}, -] - -[package.dependencies] -numpy = ">=1.23.0" -pandas = ">=1.5.3" -scipy = ">=1.8.0" - -[[package]] -name = "minikanren" -version = "1.0.3" -description = "Relational programming in Python" -optional = false -python-versions = ">=3.6" -files = [ - {file = "miniKanren-1.0.3.tar.gz", hash = "sha256:1ec8bdb01144ad5e8752c7c297fb8a122db920f859276d25a72d164e998d7f6e"}, -] - -[package.dependencies] -cons = ">=0.4.0" -etuples = ">=0.3.1" -logical-unification = ">=0.4.1" -multipledispatch = "*" -toolz = "*" - -[[package]] -name = "mkdocs" -version = "1.5.3" -description = "Project documentation with Markdown." -optional = false -python-versions = ">=3.7" -files = [ - {file = "mkdocs-1.5.3-py3-none-any.whl", hash = "sha256:3b3a78e736b31158d64dbb2f8ba29bd46a379d0c6e324c2246c3bc3d2189cfc1"}, - {file = "mkdocs-1.5.3.tar.gz", hash = "sha256:eb7c99214dcb945313ba30426c2451b735992c73c2e10838f76d09e39ff4d0e2"}, -] - -[package.dependencies] -click = ">=7.0" -colorama = {version = ">=0.4", markers = "platform_system == \"Windows\""} -ghp-import = ">=1.0" -jinja2 = ">=2.11.1" -markdown = ">=3.2.1" -markupsafe = ">=2.0.1" -mergedeep = ">=1.3.4" -packaging = ">=20.5" -pathspec = ">=0.11.1" -platformdirs = ">=2.2.0" -pyyaml = ">=5.1" -pyyaml-env-tag = ">=0.1" -watchdog = ">=2.0" - -[package.extras] -i18n = ["babel (>=2.9.0)"] -min-versions = ["babel (==2.9.0)", "click (==7.0)", "colorama (==0.4)", "ghp-import (==1.0)", "importlib-metadata (==4.3)", "jinja2 (==2.11.1)", "markdown (==3.2.1)", "markupsafe (==2.0.1)", "mergedeep (==1.3.4)", "packaging (==20.5)", "pathspec (==0.11.1)", "platformdirs (==2.2.0)", "pyyaml (==5.1)", "pyyaml-env-tag (==0.1)", "typing-extensions (==3.10)", "watchdog (==2.0)"] - -[[package]] -name = "mkdocs-autorefs" -version = "0.5.0" -description = "Automatically link across pages in MkDocs." -optional = false -python-versions = ">=3.8" -files = [ - {file = "mkdocs_autorefs-0.5.0-py3-none-any.whl", hash = "sha256:7930fcb8ac1249f10e683967aeaddc0af49d90702af111a5e390e8b20b3d97ff"}, - {file = "mkdocs_autorefs-0.5.0.tar.gz", hash = "sha256:9a5054a94c08d28855cfab967ada10ed5be76e2bfad642302a610b252c3274c0"}, -] - -[package.dependencies] -Markdown = ">=3.3" -mkdocs = ">=1.1" - -[[package]] -name = "mkdocs-material" -version = "9.3.2" -description = "Documentation that simply works" -optional = false -python-versions = ">=3.8" -files = [ - {file = "mkdocs_material-9.3.2-py3-none-any.whl", hash = "sha256:f2fd5cef6f0266b4caad6414f31c6a51e3183dbdd341995ad8fa7f33bc998c3d"}, - {file = "mkdocs_material-9.3.2.tar.gz", hash = "sha256:7b3a35a7731af02d70d120224fcec053ce09bebbf83dff3366ab72abc4d5fc89"}, -] - -[package.dependencies] -babel = ">=2.10,<3.0" -colorama = ">=0.4,<1.0" -jinja2 = ">=3.0,<4.0" -markdown = ">=3.2,<4.0" -mkdocs = ">=1.5,<2.0" -mkdocs-material-extensions = ">=1.1,<2.0" -paginate = ">=0.5,<1.0" -pygments = ">=2.16,<3.0" -pymdown-extensions = ">=10.2,<11.0" -regex = ">=2022.4,<2023.0" -requests = ">=2.26,<3.0" - -[package.extras] -git = ["mkdocs-git-committers-plugin-2 (>=1.1,<2.0)", "mkdocs-git-revision-date-localized-plugin (>=1.2,<2.0)"] -imaging = ["cairosvg (>=2.6,<3.0)", "pillow (>=9.4,<10.0)"] -recommended = ["mkdocs-minify-plugin (>=0.7,<1.0)", "mkdocs-redirects (>=1.2,<2.0)", "mkdocs-rss-plugin (>=1.6,<2.0)"] - -[[package]] -name = "mkdocs-material-extensions" -version = "1.1.1" -description = "Extension pack for Python Markdown and MkDocs Material." -optional = false -python-versions = ">=3.7" -files = [ - {file = "mkdocs_material_extensions-1.1.1-py3-none-any.whl", hash = "sha256:e41d9f38e4798b6617ad98ca8f7f1157b1e4385ac1459ca1e4ea219b556df945"}, - {file = "mkdocs_material_extensions-1.1.1.tar.gz", hash = "sha256:9c003da71e2cc2493d910237448c672e00cefc800d3d6ae93d2fc69979e3bd93"}, -] - -[[package]] -name = "mkdocstrings" -version = "0.22.0" -description = "Automatic documentation from sources, for MkDocs." -optional = false -python-versions = ">=3.7" -files = [ - {file = "mkdocstrings-0.22.0-py3-none-any.whl", hash = "sha256:2d4095d461554ff6a778fdabdca3c00c468c2f1459d469f7a7f622a2b23212ba"}, - {file = "mkdocstrings-0.22.0.tar.gz", hash = "sha256:82a33b94150ebb3d4b5c73bab4598c3e21468c79ec072eff6931c8f3bfc38256"}, -] - -[package.dependencies] -Jinja2 = ">=2.11.1" -Markdown = ">=3.3" -MarkupSafe = ">=1.1" -mkdocs = ">=1.2" -mkdocs-autorefs = ">=0.3.1" -mkdocstrings-python = {version = ">=0.5.2", optional = true, markers = "extra == \"python\""} -pymdown-extensions = ">=6.3" - -[package.extras] -crystal = ["mkdocstrings-crystal (>=0.3.4)"] -python = ["mkdocstrings-python (>=0.5.2)"] -python-legacy = ["mkdocstrings-python-legacy (>=0.2.1)"] - -[[package]] -name = "mkdocstrings-python" -version = "1.7.0" -description = "A Python handler for mkdocstrings." -optional = false -python-versions = ">=3.8" -files = [ - {file = "mkdocstrings_python-1.7.0-py3-none-any.whl", hash = "sha256:85c5f009a5a0ebb6076b7818c82a2bb0eebd0b54662628fa8b25ee14a6207951"}, - {file = "mkdocstrings_python-1.7.0.tar.gz", hash = "sha256:5dac2712bd38a3ff0812b8650a68b232601d1474091b380a8b5bc102c8c0d80a"}, -] - -[package.dependencies] -griffe = ">=0.35" -mkdocstrings = ">=0.20" - -[[package]] -name = "multipledispatch" -version = "1.0.0" -description = "Multiple dispatch" -optional = false -python-versions = "*" -files = [ - {file = "multipledispatch-1.0.0-py3-none-any.whl", hash = "sha256:0c53cd8b077546da4e48869f49b13164bebafd0c2a5afceb6bb6a316e7fb46e4"}, - {file = "multipledispatch-1.0.0.tar.gz", hash = "sha256:5c839915465c68206c3e9c473357908216c28383b425361e5d144594bf85a7e0"}, -] - -[[package]] -name = "mypy-extensions" -version = "1.0.0" -description = "Type system extensions for programs checked with the mypy type checker." -optional = false -python-versions = ">=3.5" -files = [ - {file = "mypy_extensions-1.0.0-py3-none-any.whl", hash = "sha256:4392f6c0eb8a5668a69e23d168ffa70f0be9ccfd32b5cc2d26a34ae5b844552d"}, - {file = "mypy_extensions-1.0.0.tar.gz", hash = "sha256:75dbf8955dc00442a438fc4d0666508a9a97b6bd41aa2f0ffe9d2f2725af0782"}, -] - -[[package]] -name = "netcdf4" -version = "1.6.4" -description = "Provides an object-oriented python interface to the netCDF version 4 library" -optional = false -python-versions = ">=3.7" -files = [ - {file = "netCDF4-1.6.4-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:59eba11eac34f9c7bd209121b26954c71ff69a2eb2838d2ac339b39ea60a95c0"}, - {file = "netCDF4-1.6.4-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:541286ce7318033b326da2e4325f08037b561887d5fd25d12c2b0cad74eb1edc"}, - {file = "netCDF4-1.6.4-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6620f7962daff02df2fbcbc8f7af53edf21512bc7f5697d846d424a094f96345"}, - {file = "netCDF4-1.6.4-cp310-cp310-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:2dcf74f7b15ae6105c09df93af22ad61366a288fbb329fdbe3b9606c56c788f5"}, - {file = "netCDF4-1.6.4-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b368fc185cac7d8890a8cf1016488cd252a144008144d06a615925b09bf8d67e"}, - {file = "netCDF4-1.6.4-cp310-cp310-win_amd64.whl", hash = "sha256:eb19c51fa090b562be4f87ff736b629ffafc03d7bf1fa10f623f957d49417b4f"}, - {file = "netCDF4-1.6.4-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:08a7efab50d5a4da6227244e544a74f676b732ccdff2b8a4678174c15a3f8dff"}, - {file = "netCDF4-1.6.4-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:f9b7ad3f66d50970fa1bd5509c52bb8a99740c8a79fafa83bfd0bc7348b6ab5a"}, - {file = "netCDF4-1.6.4-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2c5cad50d4e6e5a3b35c76862c58393ddf93baa44de98b6e040ac21896145881"}, - {file = "netCDF4-1.6.4-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1c2395c53a37717b1047650e1c9de68a3d69542bb25df5c594e1e14e9480bb18"}, - {file = "netCDF4-1.6.4-cp311-cp311-win_amd64.whl", hash = "sha256:c85f77599c61a88d512d6536e181ff1c01fd16f4740367e4f8e5cacb36500293"}, - {file = "netCDF4-1.6.4-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:3e3efefdb9fc936503e89ff5b40b33f2c96527bae1e9672a2d369c5f7cb30bbd"}, - {file = "netCDF4-1.6.4-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:74a3e720a9764d13d669763c1f3c73f3f719ef181dccecad062440eea03f069e"}, - {file = "netCDF4-1.6.4-cp37-cp37m-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c67bc7fafced3f9370fdef6ce7abe235a771bed7ebe534f4ba3c84d9689dae23"}, - {file = "netCDF4-1.6.4-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b599a3b5c0d0e3affa70d7954c2b0c4ab7d7bdb52b0e413c811da9725982de33"}, - {file = "netCDF4-1.6.4-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:b21d47c23edd02ff83160c8ccc1e4d4946a91d454b246e7f63d7a6d63901707c"}, - {file = "netCDF4-1.6.4-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:8eff69acdf250ebaf817cc9bbfb457d241f92f989c7f9cca33eb29985c0f9797"}, - {file = "netCDF4-1.6.4-cp38-cp38-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4d6bcdf27fd611eaec891f4281caf5c56bbad6f5fc245ce3e6dd2dc4fc3fad56"}, - {file = "netCDF4-1.6.4-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c99a3d476242d489da26f71e318ddd314723ca766f4db11270863a764b107bc3"}, - {file = "netCDF4-1.6.4-cp38-cp38-win_amd64.whl", hash = "sha256:422077c7303cb17cbe8b9ace965cafda1f9d4c9b035809fc5c87091e7ff4d606"}, - {file = "netCDF4-1.6.4-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:0050e49889c357ae0a73f96e2b9988f250396689b78142de88a39ac8b1e702aa"}, - {file = "netCDF4-1.6.4-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:0a2cd2e2a9da98f1838e1edfeb011b840a434e04d156052b936ffe49f5298126"}, - {file = "netCDF4-1.6.4-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:695cd0a40df49b4218350686c44e13b3a4671e53ee33faf7439a89940cbccc68"}, - {file = "netCDF4-1.6.4-cp39-cp39-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e582e6e41b27fc3a3296239afe065941bda60118d585df0ad41603f6f962888e"}, - {file = "netCDF4-1.6.4-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f7b4ab3f0cd28a6685031e8b9be00c3047b4256da576c9de540440d013a31cea"}, - {file = "netCDF4-1.6.4-cp39-cp39-win_amd64.whl", hash = "sha256:1a950b3d8fcffca05e371a4cb64dc76580375588d1fca43b788192f409108ebe"}, - {file = "netCDF4-1.6.4.tar.gz", hash = "sha256:66da6542cbc7a6045cd1d979397dfd5a3f6c880c76d52b8f98bb108c82ee8c6e"}, -] - -[package.dependencies] -certifi = "*" -cftime = "*" -numpy = "*" - -[[package]] -name = "nodeenv" -version = "1.8.0" -description = "Node.js virtual environment builder" -optional = false -python-versions = ">=2.7,!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*" -files = [ - {file = "nodeenv-1.8.0-py2.py3-none-any.whl", hash = "sha256:df865724bb3c3adc86b3876fa209771517b0cfe596beff01a92700e0e8be4cec"}, - {file = "nodeenv-1.8.0.tar.gz", hash = "sha256:d51e0c37e64fbf47d017feac3145cdbb58836d7eee8c6f6d3b6880c5456227d2"}, -] - -[package.dependencies] -setuptools = "*" - -[[package]] -name = "numpy" -version = "1.25.2" -description = "Fundamental package for array computing in Python" -optional = false -python-versions = ">=3.9" -files = [ - {file = "numpy-1.25.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:db3ccc4e37a6873045580d413fe79b68e47a681af8db2e046f1dacfa11f86eb3"}, - {file = "numpy-1.25.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:90319e4f002795ccfc9050110bbbaa16c944b1c37c0baeea43c5fb881693ae1f"}, - {file = "numpy-1.25.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:dfe4a913e29b418d096e696ddd422d8a5d13ffba4ea91f9f60440a3b759b0187"}, - {file = "numpy-1.25.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f08f2e037bba04e707eebf4bc934f1972a315c883a9e0ebfa8a7756eabf9e357"}, - {file = "numpy-1.25.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:bec1e7213c7cb00d67093247f8c4db156fd03075f49876957dca4711306d39c9"}, - {file = "numpy-1.25.2-cp310-cp310-win32.whl", hash = "sha256:7dc869c0c75988e1c693d0e2d5b26034644399dd929bc049db55395b1379e044"}, - {file = "numpy-1.25.2-cp310-cp310-win_amd64.whl", hash = "sha256:834b386f2b8210dca38c71a6e0f4fd6922f7d3fcff935dbe3a570945acb1b545"}, - {file = "numpy-1.25.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:c5462d19336db4560041517dbb7759c21d181a67cb01b36ca109b2ae37d32418"}, - {file = "numpy-1.25.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:c5652ea24d33585ea39eb6a6a15dac87a1206a692719ff45d53c5282e66d4a8f"}, - {file = "numpy-1.25.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0d60fbae8e0019865fc4784745814cff1c421df5afee233db6d88ab4f14655a2"}, - {file = "numpy-1.25.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:60e7f0f7f6d0eee8364b9a6304c2845b9c491ac706048c7e8cf47b83123b8dbf"}, - {file = "numpy-1.25.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:bb33d5a1cf360304754913a350edda36d5b8c5331a8237268c48f91253c3a364"}, - {file = "numpy-1.25.2-cp311-cp311-win32.whl", hash = "sha256:5883c06bb92f2e6c8181df7b39971a5fb436288db58b5a1c3967702d4278691d"}, - {file = "numpy-1.25.2-cp311-cp311-win_amd64.whl", hash = "sha256:5c97325a0ba6f9d041feb9390924614b60b99209a71a69c876f71052521d42a4"}, - {file = "numpy-1.25.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:b79e513d7aac42ae918db3ad1341a015488530d0bb2a6abcbdd10a3a829ccfd3"}, - {file = "numpy-1.25.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:eb942bfb6f84df5ce05dbf4b46673ffed0d3da59f13635ea9b926af3deb76926"}, - {file = "numpy-1.25.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3e0746410e73384e70d286f93abf2520035250aad8c5714240b0492a7302fdca"}, - {file = "numpy-1.25.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d7806500e4f5bdd04095e849265e55de20d8cc4b661b038957354327f6d9b295"}, - {file = "numpy-1.25.2-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:8b77775f4b7df768967a7c8b3567e309f617dd5e99aeb886fa14dc1a0791141f"}, - {file = "numpy-1.25.2-cp39-cp39-win32.whl", hash = "sha256:2792d23d62ec51e50ce4d4b7d73de8f67a2fd3ea710dcbc8563a51a03fb07b01"}, - {file = "numpy-1.25.2-cp39-cp39-win_amd64.whl", hash = "sha256:76b4115d42a7dfc5d485d358728cdd8719be33cc5ec6ec08632a5d6fca2ed380"}, - {file = "numpy-1.25.2-pp39-pypy39_pp73-macosx_10_9_x86_64.whl", hash = "sha256:1a1329e26f46230bf77b02cc19e900db9b52f398d6722ca853349a782d4cff55"}, - {file = "numpy-1.25.2-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4c3abc71e8b6edba80a01a52e66d83c5d14433cbcd26a40c329ec7ed09f37901"}, - {file = "numpy-1.25.2-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:1b9735c27cea5d995496f46a8b1cd7b408b3f34b6d50459d9ac8fe3a20cc17bf"}, - {file = "numpy-1.25.2.tar.gz", hash = "sha256:fd608e19c8d7c55021dffd43bfe5492fab8cc105cc8986f813f8c3c048b38760"}, -] - -[[package]] -name = "packaging" -version = "23.1" -description = "Core utilities for Python packages" -optional = false -python-versions = ">=3.7" -files = [ - {file = "packaging-23.1-py3-none-any.whl", hash = "sha256:994793af429502c4ea2ebf6bf664629d07c1a9fe974af92966e4b8d2df7edc61"}, - {file = "packaging-23.1.tar.gz", hash = "sha256:a392980d2b6cffa644431898be54b0045151319d1e7ec34f0cfed48767dd334f"}, -] - -[[package]] -name = "paginate" -version = "0.5.6" -description = "Divides large result sets into pages for easier browsing" -optional = false -python-versions = "*" -files = [ - {file = "paginate-0.5.6.tar.gz", hash = "sha256:5e6007b6a9398177a7e1648d04fdd9f8c9766a1a945bceac82f1929e8c78af2d"}, -] - -[[package]] -name = "pandas" -version = "2.1.0" -description = "Powerful data structures for data analysis, time series, and statistics" -optional = false -python-versions = ">=3.9" -files = [ - {file = "pandas-2.1.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:40dd20439ff94f1b2ed55b393ecee9cb6f3b08104c2c40b0cb7186a2f0046242"}, - {file = "pandas-2.1.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:d4f38e4fedeba580285eaac7ede4f686c6701a9e618d8a857b138a126d067f2f"}, - {file = "pandas-2.1.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6e6a0fe052cf27ceb29be9429428b4918f3740e37ff185658f40d8702f0b3e09"}, - {file = "pandas-2.1.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9d81e1813191070440d4c7a413cb673052b3b4a984ffd86b8dd468c45742d3cc"}, - {file = "pandas-2.1.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:eb20252720b1cc1b7d0b2879ffc7e0542dd568f24d7c4b2347cb035206936421"}, - {file = "pandas-2.1.0-cp310-cp310-win_amd64.whl", hash = "sha256:38f74ef7ebc0ffb43b3d633e23d74882bce7e27bfa09607f3c5d3e03ffd9a4a5"}, - {file = "pandas-2.1.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:cda72cc8c4761c8f1d97b169661f23a86b16fdb240bdc341173aee17e4d6cedd"}, - {file = "pandas-2.1.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:d97daeac0db8c993420b10da4f5f5b39b01fc9ca689a17844e07c0a35ac96b4b"}, - {file = "pandas-2.1.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d8c58b1113892e0c8078f006a167cc210a92bdae23322bb4614f2f0b7a4b510f"}, - {file = "pandas-2.1.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:629124923bcf798965b054a540f9ccdfd60f71361255c81fa1ecd94a904b9dd3"}, - {file = "pandas-2.1.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:70cf866af3ab346a10debba8ea78077cf3a8cd14bd5e4bed3d41555a3280041c"}, - {file = "pandas-2.1.0-cp311-cp311-win_amd64.whl", hash = "sha256:d53c8c1001f6a192ff1de1efe03b31a423d0eee2e9e855e69d004308e046e694"}, - {file = "pandas-2.1.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:86f100b3876b8c6d1a2c66207288ead435dc71041ee4aea789e55ef0e06408cb"}, - {file = "pandas-2.1.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:28f330845ad21c11db51e02d8d69acc9035edfd1116926ff7245c7215db57957"}, - {file = "pandas-2.1.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b9a6ccf0963db88f9b12df6720e55f337447aea217f426a22d71f4213a3099a6"}, - {file = "pandas-2.1.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d99e678180bc59b0c9443314297bddce4ad35727a1a2656dbe585fd78710b3b9"}, - {file = "pandas-2.1.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:b31da36d376d50a1a492efb18097b9101bdbd8b3fbb3f49006e02d4495d4c644"}, - {file = "pandas-2.1.0-cp39-cp39-win_amd64.whl", hash = "sha256:0164b85937707ec7f70b34a6c3a578dbf0f50787f910f21ca3b26a7fd3363437"}, - {file = "pandas-2.1.0.tar.gz", hash = "sha256:62c24c7fc59e42b775ce0679cfa7b14a5f9bfb7643cfbe708c960699e05fb918"}, -] - -[package.dependencies] -numpy = [ - {version = ">=1.22.4", markers = "python_version < \"3.11\""}, - {version = ">=1.23.2", markers = "python_version >= \"3.11\""}, -] -python-dateutil = ">=2.8.2" -pytz = ">=2020.1" -tzdata = ">=2022.1" - -[package.extras] -all = ["PyQt5 (>=5.15.6)", "SQLAlchemy (>=1.4.36)", "beautifulsoup4 (>=4.11.1)", "bottleneck (>=1.3.4)", "dataframe-api-compat (>=0.1.7)", "fastparquet (>=0.8.1)", "fsspec (>=2022.05.0)", "gcsfs (>=2022.05.0)", "html5lib (>=1.1)", "hypothesis (>=6.46.1)", "jinja2 (>=3.1.2)", "lxml (>=4.8.0)", "matplotlib (>=3.6.1)", "numba (>=0.55.2)", "numexpr (>=2.8.0)", "odfpy (>=1.4.1)", "openpyxl (>=3.0.10)", "pandas-gbq (>=0.17.5)", "psycopg2 (>=2.9.3)", "pyarrow (>=7.0.0)", "pymysql (>=1.0.2)", "pyreadstat (>=1.1.5)", "pytest (>=7.3.2)", "pytest-asyncio (>=0.17.0)", "pytest-xdist (>=2.2.0)", "pyxlsb (>=1.0.9)", "qtpy (>=2.2.0)", "s3fs (>=2022.05.0)", "scipy (>=1.8.1)", "tables (>=3.7.0)", "tabulate (>=0.8.10)", "xarray (>=2022.03.0)", "xlrd (>=2.0.1)", "xlsxwriter (>=3.0.3)", "zstandard (>=0.17.0)"] -aws = ["s3fs (>=2022.05.0)"] -clipboard = ["PyQt5 (>=5.15.6)", "qtpy (>=2.2.0)"] -compression = ["zstandard (>=0.17.0)"] -computation = ["scipy (>=1.8.1)", "xarray (>=2022.03.0)"] -consortium-standard = ["dataframe-api-compat (>=0.1.7)"] -excel = ["odfpy (>=1.4.1)", "openpyxl (>=3.0.10)", "pyxlsb (>=1.0.9)", "xlrd (>=2.0.1)", "xlsxwriter (>=3.0.3)"] -feather = ["pyarrow (>=7.0.0)"] -fss = ["fsspec (>=2022.05.0)"] -gcp = ["gcsfs (>=2022.05.0)", "pandas-gbq (>=0.17.5)"] -hdf5 = ["tables (>=3.7.0)"] -html = ["beautifulsoup4 (>=4.11.1)", "html5lib (>=1.1)", "lxml (>=4.8.0)"] -mysql = ["SQLAlchemy (>=1.4.36)", "pymysql (>=1.0.2)"] -output-formatting = ["jinja2 (>=3.1.2)", "tabulate (>=0.8.10)"] -parquet = ["pyarrow (>=7.0.0)"] -performance = ["bottleneck (>=1.3.4)", "numba (>=0.55.2)", "numexpr (>=2.8.0)"] -plot = ["matplotlib (>=3.6.1)"] -postgresql = ["SQLAlchemy (>=1.4.36)", "psycopg2 (>=2.9.3)"] -spss = ["pyreadstat (>=1.1.5)"] -sql-other = ["SQLAlchemy (>=1.4.36)"] -test = ["hypothesis (>=6.46.1)", "pytest (>=7.3.2)", "pytest-asyncio (>=0.17.0)", "pytest-xdist (>=2.2.0)"] -xml = ["lxml (>=4.8.0)"] - -[[package]] -name = "pathspec" -version = "0.11.2" -description = "Utility library for gitignore style pattern matching of file paths." -optional = false -python-versions = ">=3.7" -files = [ - {file = "pathspec-0.11.2-py3-none-any.whl", hash = "sha256:1d6ed233af05e679efb96b1851550ea95bbb64b7c490b0f5aa52996c11e92a20"}, - {file = "pathspec-0.11.2.tar.gz", hash = "sha256:e0d8d0ac2f12da61956eb2306b69f9469b42f4deb0f3cb6ed47b9cce9996ced3"}, -] - -[[package]] -name = "pillow" -version = "10.0.1" -description = "Python Imaging Library (Fork)" -optional = false -python-versions = ">=3.8" -files = [ - {file = "Pillow-10.0.1-cp310-cp310-macosx_10_10_x86_64.whl", hash = "sha256:8f06be50669087250f319b706decf69ca71fdecd829091a37cc89398ca4dc17a"}, - {file = "Pillow-10.0.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:50bd5f1ebafe9362ad622072a1d2f5850ecfa44303531ff14353a4059113b12d"}, - {file = "Pillow-10.0.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e6a90167bcca1216606223a05e2cf991bb25b14695c518bc65639463d7db722d"}, - {file = "Pillow-10.0.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f11c9102c56ffb9ca87134bd025a43d2aba3f1155f508eff88f694b33a9c6d19"}, - {file = "Pillow-10.0.1-cp310-cp310-manylinux_2_28_aarch64.whl", hash = "sha256:186f7e04248103482ea6354af6d5bcedb62941ee08f7f788a1c7707bc720c66f"}, - {file = "Pillow-10.0.1-cp310-cp310-manylinux_2_28_x86_64.whl", hash = "sha256:0462b1496505a3462d0f35dc1c4d7b54069747d65d00ef48e736acda2c8cbdff"}, - {file = "Pillow-10.0.1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:d889b53ae2f030f756e61a7bff13684dcd77e9af8b10c6048fb2c559d6ed6eaf"}, - {file = "Pillow-10.0.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:552912dbca585b74d75279a7570dd29fa43b6d93594abb494ebb31ac19ace6bd"}, - {file = "Pillow-10.0.1-cp310-cp310-win_amd64.whl", hash = "sha256:787bb0169d2385a798888e1122c980c6eff26bf941a8ea79747d35d8f9210ca0"}, - {file = "Pillow-10.0.1-cp311-cp311-macosx_10_10_x86_64.whl", hash = "sha256:fd2a5403a75b54661182b75ec6132437a181209b901446ee5724b589af8edef1"}, - {file = "Pillow-10.0.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:2d7e91b4379f7a76b31c2dda84ab9e20c6220488e50f7822e59dac36b0cd92b1"}, - {file = "Pillow-10.0.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:19e9adb3f22d4c416e7cd79b01375b17159d6990003633ff1d8377e21b7f1b21"}, - {file = "Pillow-10.0.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:93139acd8109edcdeffd85e3af8ae7d88b258b3a1e13a038f542b79b6d255c54"}, - {file = "Pillow-10.0.1-cp311-cp311-manylinux_2_28_aarch64.whl", hash = "sha256:92a23b0431941a33242b1f0ce6c88a952e09feeea9af4e8be48236a68ffe2205"}, - {file = "Pillow-10.0.1-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:cbe68deb8580462ca0d9eb56a81912f59eb4542e1ef8f987405e35a0179f4ea2"}, - {file = "Pillow-10.0.1-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:522ff4ac3aaf839242c6f4e5b406634bfea002469656ae8358644fc6c4856a3b"}, - {file = "Pillow-10.0.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:84efb46e8d881bb06b35d1d541aa87f574b58e87f781cbba8d200daa835b42e1"}, - {file = "Pillow-10.0.1-cp311-cp311-win_amd64.whl", hash = "sha256:898f1d306298ff40dc1b9ca24824f0488f6f039bc0e25cfb549d3195ffa17088"}, - {file = "Pillow-10.0.1-cp312-cp312-macosx_10_10_x86_64.whl", hash = "sha256:bcf1207e2f2385a576832af02702de104be71301c2696d0012b1b93fe34aaa5b"}, - {file = "Pillow-10.0.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:5d6c9049c6274c1bb565021367431ad04481ebb54872edecfcd6088d27edd6ed"}, - {file = "Pillow-10.0.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:28444cb6ad49726127d6b340217f0627abc8732f1194fd5352dec5e6a0105635"}, - {file = "Pillow-10.0.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:de596695a75496deb3b499c8c4f8e60376e0516e1a774e7bc046f0f48cd620ad"}, - {file = "Pillow-10.0.1-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:2872f2d7846cf39b3dbff64bc1104cc48c76145854256451d33c5faa55c04d1a"}, - {file = "Pillow-10.0.1-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:4ce90f8a24e1c15465048959f1e94309dfef93af272633e8f37361b824532e91"}, - {file = "Pillow-10.0.1-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:ee7810cf7c83fa227ba9125de6084e5e8b08c59038a7b2c9045ef4dde61663b4"}, - {file = "Pillow-10.0.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:b1be1c872b9b5fcc229adeadbeb51422a9633abd847c0ff87dc4ef9bb184ae08"}, - {file = "Pillow-10.0.1-cp312-cp312-win_amd64.whl", hash = "sha256:98533fd7fa764e5f85eebe56c8e4094db912ccbe6fbf3a58778d543cadd0db08"}, - {file = "Pillow-10.0.1-cp38-cp38-macosx_10_10_x86_64.whl", hash = "sha256:764d2c0daf9c4d40ad12fbc0abd5da3af7f8aa11daf87e4fa1b834000f4b6b0a"}, - {file = "Pillow-10.0.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:fcb59711009b0168d6ee0bd8fb5eb259c4ab1717b2f538bbf36bacf207ef7a68"}, - {file = "Pillow-10.0.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:697a06bdcedd473b35e50a7e7506b1d8ceb832dc238a336bd6f4f5aa91a4b500"}, - {file = "Pillow-10.0.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9f665d1e6474af9f9da5e86c2a3a2d2d6204e04d5af9c06b9d42afa6ebde3f21"}, - {file = "Pillow-10.0.1-cp38-cp38-manylinux_2_28_aarch64.whl", hash = "sha256:2fa6dd2661838c66f1a5473f3b49ab610c98a128fc08afbe81b91a1f0bf8c51d"}, - {file = "Pillow-10.0.1-cp38-cp38-manylinux_2_28_x86_64.whl", hash = "sha256:3a04359f308ebee571a3127fdb1bd01f88ba6f6fb6d087f8dd2e0d9bff43f2a7"}, - {file = "Pillow-10.0.1-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:723bd25051454cea9990203405fa6b74e043ea76d4968166dfd2569b0210886a"}, - {file = "Pillow-10.0.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:71671503e3015da1b50bd18951e2f9daf5b6ffe36d16f1eb2c45711a301521a7"}, - {file = "Pillow-10.0.1-cp38-cp38-win_amd64.whl", hash = "sha256:44e7e4587392953e5e251190a964675f61e4dae88d1e6edbe9f36d6243547ff3"}, - {file = "Pillow-10.0.1-cp39-cp39-macosx_10_10_x86_64.whl", hash = "sha256:3855447d98cced8670aaa63683808df905e956f00348732448b5a6df67ee5849"}, - {file = "Pillow-10.0.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:ed2d9c0704f2dc4fa980b99d565c0c9a543fe5101c25b3d60488b8ba80f0cce1"}, - {file = "Pillow-10.0.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f5bb289bb835f9fe1a1e9300d011eef4d69661bb9b34d5e196e5e82c4cb09b37"}, - {file = "Pillow-10.0.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3a0d3e54ab1df9df51b914b2233cf779a5a10dfd1ce339d0421748232cea9876"}, - {file = "Pillow-10.0.1-cp39-cp39-manylinux_2_28_aarch64.whl", hash = "sha256:2cc6b86ece42a11f16f55fe8903595eff2b25e0358dec635d0a701ac9586588f"}, - {file = "Pillow-10.0.1-cp39-cp39-manylinux_2_28_x86_64.whl", hash = "sha256:ca26ba5767888c84bf5a0c1a32f069e8204ce8c21d00a49c90dabeba00ce0145"}, - {file = "Pillow-10.0.1-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:f0b4b06da13275bc02adfeb82643c4a6385bd08d26f03068c2796f60d125f6f2"}, - {file = "Pillow-10.0.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:bc2e3069569ea9dbe88d6b8ea38f439a6aad8f6e7a6283a38edf61ddefb3a9bf"}, - {file = "Pillow-10.0.1-cp39-cp39-win_amd64.whl", hash = "sha256:8b451d6ead6e3500b6ce5c7916a43d8d8d25ad74b9102a629baccc0808c54971"}, - {file = "Pillow-10.0.1-pp310-pypy310_pp73-macosx_10_10_x86_64.whl", hash = "sha256:32bec7423cdf25c9038fef614a853c9d25c07590e1a870ed471f47fb80b244db"}, - {file = "Pillow-10.0.1-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b7cf63d2c6928b51d35dfdbda6f2c1fddbe51a6bc4a9d4ee6ea0e11670dd981e"}, - {file = "Pillow-10.0.1-pp310-pypy310_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:f6d3d4c905e26354e8f9d82548475c46d8e0889538cb0657aa9c6f0872a37aa4"}, - {file = "Pillow-10.0.1-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:847e8d1017c741c735d3cd1883fa7b03ded4f825a6e5fcb9378fd813edee995f"}, - {file = "Pillow-10.0.1-pp39-pypy39_pp73-macosx_10_10_x86_64.whl", hash = "sha256:7f771e7219ff04b79e231d099c0a28ed83aa82af91fd5fa9fdb28f5b8d5addaf"}, - {file = "Pillow-10.0.1-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:459307cacdd4138edee3875bbe22a2492519e060660eaf378ba3b405d1c66317"}, - {file = "Pillow-10.0.1-pp39-pypy39_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:b059ac2c4c7a97daafa7dc850b43b2d3667def858a4f112d1aa082e5c3d6cf7d"}, - {file = "Pillow-10.0.1-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:d6caf3cd38449ec3cd8a68b375e0c6fe4b6fd04edb6c9766b55ef84a6e8ddf2d"}, - {file = "Pillow-10.0.1.tar.gz", hash = "sha256:d72967b06be9300fed5cfbc8b5bafceec48bf7cdc7dab66b1d2549035287191d"}, -] - -[package.extras] -docs = ["furo", "olefile", "sphinx (>=2.4)", "sphinx-copybutton", "sphinx-inline-tabs", "sphinx-removed-in", "sphinxext-opengraph"] -tests = ["check-manifest", "coverage", "defusedxml", "markdown2", "olefile", "packaging", "pyroma", "pytest", "pytest-cov", "pytest-timeout"] - -[[package]] -name = "platformdirs" -version = "3.10.0" -description = "A small Python package for determining appropriate platform-specific dirs, e.g. a \"user data dir\"." -optional = false -python-versions = ">=3.7" -files = [ - {file = "platformdirs-3.10.0-py3-none-any.whl", hash = "sha256:d7c24979f292f916dc9cbf8648319032f551ea8c49a4c9bf2fb556a02070ec1d"}, - {file = "platformdirs-3.10.0.tar.gz", hash = "sha256:b45696dab2d7cc691a3226759c0d3b00c47c8b6e293d96f6436f733303f77f6d"}, -] - -[package.extras] -docs = ["furo (>=2023.7.26)", "proselint (>=0.13)", "sphinx (>=7.1.1)", "sphinx-autodoc-typehints (>=1.24)"] -test = ["appdirs (==1.4.4)", "covdefaults (>=2.3)", "pytest (>=7.4)", "pytest-cov (>=4.1)", "pytest-mock (>=3.11.1)"] - -[[package]] -name = "pluggy" -version = "1.3.0" -description = "plugin and hook calling mechanisms for python" -optional = false -python-versions = ">=3.8" -files = [ - {file = "pluggy-1.3.0-py3-none-any.whl", hash = "sha256:d89c696a773f8bd377d18e5ecda92b7a3793cbe66c87060a6fb58c7b6e1061f7"}, - {file = "pluggy-1.3.0.tar.gz", hash = "sha256:cf61ae8f126ac6f7c451172cf30e3e43d3ca77615509771b3a984a0730651e12"}, -] - -[package.extras] -dev = ["pre-commit", "tox"] -testing = ["pytest", "pytest-benchmark"] - -[[package]] -name = "pre-commit" -version = "3.4.0" -description = "A framework for managing and maintaining multi-language pre-commit hooks." -optional = false -python-versions = ">=3.8" -files = [ - {file = "pre_commit-3.4.0-py2.py3-none-any.whl", hash = "sha256:96d529a951f8b677f730a7212442027e8ba53f9b04d217c4c67dc56c393ad945"}, - {file = "pre_commit-3.4.0.tar.gz", hash = "sha256:6bbd5129a64cad4c0dfaeeb12cd8f7ea7e15b77028d985341478c8af3c759522"}, -] - -[package.dependencies] -cfgv = ">=2.0.0" -identify = ">=1.0.0" -nodeenv = ">=0.11.1" -pyyaml = ">=5.1" -virtualenv = ">=20.10.0" - -[[package]] -name = "pydantic" -version = "1.10.12" -description = "Data validation and settings management using python type hints" -optional = false -python-versions = ">=3.7" -files = [ - {file = "pydantic-1.10.12-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:a1fcb59f2f355ec350073af41d927bf83a63b50e640f4dbaa01053a28b7a7718"}, - {file = "pydantic-1.10.12-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:b7ccf02d7eb340b216ec33e53a3a629856afe1c6e0ef91d84a4e6f2fb2ca70fe"}, - {file = "pydantic-1.10.12-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8fb2aa3ab3728d950bcc885a2e9eff6c8fc40bc0b7bb434e555c215491bcf48b"}, - {file = "pydantic-1.10.12-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:771735dc43cf8383959dc9b90aa281f0b6092321ca98677c5fb6125a6f56d58d"}, - {file = "pydantic-1.10.12-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:ca48477862372ac3770969b9d75f1bf66131d386dba79506c46d75e6b48c1e09"}, - {file = "pydantic-1.10.12-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:a5e7add47a5b5a40c49b3036d464e3c7802f8ae0d1e66035ea16aa5b7a3923ed"}, - {file = "pydantic-1.10.12-cp310-cp310-win_amd64.whl", hash = "sha256:e4129b528c6baa99a429f97ce733fff478ec955513630e61b49804b6cf9b224a"}, - {file = "pydantic-1.10.12-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:b0d191db0f92dfcb1dec210ca244fdae5cbe918c6050b342d619c09d31eea0cc"}, - {file = "pydantic-1.10.12-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:795e34e6cc065f8f498c89b894a3c6da294a936ee71e644e4bd44de048af1405"}, - {file = "pydantic-1.10.12-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:69328e15cfda2c392da4e713443c7dbffa1505bc9d566e71e55abe14c97ddc62"}, - {file = "pydantic-1.10.12-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:2031de0967c279df0d8a1c72b4ffc411ecd06bac607a212892757db7462fc494"}, - {file = "pydantic-1.10.12-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:ba5b2e6fe6ca2b7e013398bc7d7b170e21cce322d266ffcd57cca313e54fb246"}, - {file = "pydantic-1.10.12-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:2a7bac939fa326db1ab741c9d7f44c565a1d1e80908b3797f7f81a4f86bc8d33"}, - {file = "pydantic-1.10.12-cp311-cp311-win_amd64.whl", hash = "sha256:87afda5539d5140cb8ba9e8b8c8865cb5b1463924d38490d73d3ccfd80896b3f"}, - {file = "pydantic-1.10.12-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:549a8e3d81df0a85226963611950b12d2d334f214436a19537b2efed61b7639a"}, - {file = "pydantic-1.10.12-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:598da88dfa127b666852bef6d0d796573a8cf5009ffd62104094a4fe39599565"}, - {file = "pydantic-1.10.12-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ba5c4a8552bff16c61882db58544116d021d0b31ee7c66958d14cf386a5b5350"}, - {file = "pydantic-1.10.12-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:c79e6a11a07da7374f46970410b41d5e266f7f38f6a17a9c4823db80dadf4303"}, - {file = "pydantic-1.10.12-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:ab26038b8375581dc832a63c948f261ae0aa21f1d34c1293469f135fa92972a5"}, - {file = "pydantic-1.10.12-cp37-cp37m-win_amd64.whl", hash = "sha256:e0a16d274b588767602b7646fa05af2782576a6cf1022f4ba74cbb4db66f6ca8"}, - {file = "pydantic-1.10.12-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:6a9dfa722316f4acf4460afdf5d41d5246a80e249c7ff475c43a3a1e9d75cf62"}, - {file = "pydantic-1.10.12-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:a73f489aebd0c2121ed974054cb2759af8a9f747de120acd2c3394cf84176ccb"}, - {file = "pydantic-1.10.12-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6b30bcb8cbfccfcf02acb8f1a261143fab622831d9c0989707e0e659f77a18e0"}, - {file = "pydantic-1.10.12-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:2fcfb5296d7877af406ba1547dfde9943b1256d8928732267e2653c26938cd9c"}, - {file = "pydantic-1.10.12-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:2f9a6fab5f82ada41d56b0602606a5506aab165ca54e52bc4545028382ef1c5d"}, - {file = "pydantic-1.10.12-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:dea7adcc33d5d105896401a1f37d56b47d443a2b2605ff8a969a0ed5543f7e33"}, - {file = "pydantic-1.10.12-cp38-cp38-win_amd64.whl", hash = "sha256:1eb2085c13bce1612da8537b2d90f549c8cbb05c67e8f22854e201bde5d98a47"}, - {file = "pydantic-1.10.12-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:ef6c96b2baa2100ec91a4b428f80d8f28a3c9e53568219b6c298c1125572ebc6"}, - {file = "pydantic-1.10.12-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:6c076be61cd0177a8433c0adcb03475baf4ee91edf5a4e550161ad57fc90f523"}, - {file = "pydantic-1.10.12-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2d5a58feb9a39f481eda4d5ca220aa8b9d4f21a41274760b9bc66bfd72595b86"}, - {file = "pydantic-1.10.12-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e5f805d2d5d0a41633651a73fa4ecdd0b3d7a49de4ec3fadf062fe16501ddbf1"}, - {file = "pydantic-1.10.12-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:1289c180abd4bd4555bb927c42ee42abc3aee02b0fb2d1223fb7c6e5bef87dbe"}, - {file = "pydantic-1.10.12-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:5d1197e462e0364906cbc19681605cb7c036f2475c899b6f296104ad42b9f5fb"}, - {file = "pydantic-1.10.12-cp39-cp39-win_amd64.whl", hash = "sha256:fdbdd1d630195689f325c9ef1a12900524dceb503b00a987663ff4f58669b93d"}, - {file = "pydantic-1.10.12-py3-none-any.whl", hash = "sha256:b749a43aa51e32839c9d71dc67eb1e4221bb04af1033a32e3923d46f9effa942"}, - {file = "pydantic-1.10.12.tar.gz", hash = "sha256:0fe8a415cea8f340e7a9af9c54fc71a649b43e8ca3cc732986116b3cb135d303"}, -] - -[package.dependencies] -typing-extensions = ">=4.2.0" - -[package.extras] -dotenv = ["python-dotenv (>=0.10.4)"] -email = ["email-validator (>=1.0.3)"] - -[[package]] -name = "pygments" -version = "2.16.1" -description = "Pygments is a syntax highlighting package written in Python." -optional = false -python-versions = ">=3.7" -files = [ - {file = "Pygments-2.16.1-py3-none-any.whl", hash = "sha256:13fc09fa63bc8d8671a6d247e1eb303c4b343eaee81d861f3404db2935653692"}, - {file = "Pygments-2.16.1.tar.gz", hash = "sha256:1daff0494820c69bc8941e407aa20f577374ee88364ee10a98fdbe0aece96e29"}, -] - -[package.extras] -plugins = ["importlib-metadata"] - -[[package]] -name = "pymc" -version = "5.8.1" -description = "Probabilistic Programming in Python: Bayesian Modeling and Probabilistic Machine Learning with PyTensor" -optional = false -python-versions = ">=3.9" -files = [ - {file = "pymc-5.8.1-py3-none-any.whl", hash = "sha256:d04b4e142103ffca32c593e2147a8f7d88dd3eb3cbc25fac26e8fd72af9f4426"}, - {file = "pymc-5.8.1.tar.gz", hash = "sha256:71ab05caa6aa1ec6b33242e027e04ac200bad8db516f7e7faaaa5d415c6182c1"}, -] - -[package.dependencies] -arviz = ">=0.13.0" -cachetools = ">=4.2.1" -cloudpickle = "*" -fastprogress = ">=0.2.0" -numpy = ">=1.15.0" -pandas = ">=0.24.0" -pytensor = ">=2.16.1,<2.17" -scipy = ">=1.4.1" -typing-extensions = ">=3.7.4" - -[[package]] -name = "pymdown-extensions" -version = "10.3" -description = "Extension pack for Python Markdown." -optional = false -python-versions = ">=3.8" -files = [ - {file = "pymdown_extensions-10.3-py3-none-any.whl", hash = "sha256:77a82c621c58a83efc49a389159181d570e370fff9f810d3a4766a75fc678b66"}, - {file = "pymdown_extensions-10.3.tar.gz", hash = "sha256:94a0d8a03246712b64698af223848fd80aaf1ae4c4be29c8c61939b0467b5722"}, -] - -[package.dependencies] -markdown = ">=3.2" -pyyaml = "*" - -[package.extras] -extra = ["pygments (>=2.12)"] - -[[package]] -name = "pyparsing" -version = "3.1.1" -description = "pyparsing module - Classes and methods to define and execute parsing grammars" -optional = false -python-versions = ">=3.6.8" -files = [ - {file = "pyparsing-3.1.1-py3-none-any.whl", hash = "sha256:32c7c0b711493c72ff18a981d24f28aaf9c1fb7ed5e9667c9e84e3db623bdbfb"}, - {file = "pyparsing-3.1.1.tar.gz", hash = "sha256:ede28a1a32462f5a9705e07aea48001a08f7cf81a021585011deba701581a0db"}, -] - -[package.extras] -diagrams = ["jinja2", "railroad-diagrams"] - -[[package]] -name = "pyright" -version = "1.1.327" -description = "Command line wrapper for pyright" -optional = false -python-versions = ">=3.7" -files = [ - {file = "pyright-1.1.327-py3-none-any.whl", hash = "sha256:3462cda239e9140276238bbdbd0b59d77406f1c2e14d8cb8c20c8e25639c6b3c"}, - {file = "pyright-1.1.327.tar.gz", hash = "sha256:ba74148ad64f22020dbbed6781c4bdb38ecb8a7ca90dc3c87a4f08d1c0e11592"}, -] - -[package.dependencies] -nodeenv = ">=1.6.0" - -[package.extras] -all = ["twine (>=3.4.1)"] -dev = ["twine (>=3.4.1)"] - -[[package]] -name = "pytensor" -version = "2.16.1" -description = "Optimizing compiler for evaluating mathematical expressions on CPUs and GPUs." -optional = false -python-versions = ">=3.9" -files = [ - {file = "pytensor-2.16.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:8d521e9fa556ee085c837098310ca6561772e6012cb310ff66eed1306d6c68d4"}, - {file = "pytensor-2.16.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2b9629ee883cd7881c0e06e17b93da3a672028d4fb0198dbf3cbd959b55e79b8"}, - {file = "pytensor-2.16.1-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:e6b2d78b84e9b22ac1c817409aeb629de49886c01c1f0e9607cf968416c8956a"}, - {file = "pytensor-2.16.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:0fa2956f5219711673b71825f6316cfbb8dcbf4d398b13d9d3e2e73d7578c157"}, - {file = "pytensor-2.16.1-cp310-cp310-win_amd64.whl", hash = "sha256:67f25d3c794a25430efad499f356dbe389dd6700387d245fa293fa474adcbcaf"}, - {file = "pytensor-2.16.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:5ff70f49c66059a894b408d340f9d83c6cc5850bb096a035f1fbf8c609aedfab"}, - {file = "pytensor-2.16.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a8eb0dd68fa383d635e5fd0b89d22d3632c7cf8e90df3ccc37ea459bbf3e6c3f"}, - {file = "pytensor-2.16.1-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:5f89154c13d51e1d3b4f29d027d60f1dbce21e8bcf5a71d6fc9cd6c650b72353"}, - {file = "pytensor-2.16.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:37b787e5f864d8ecf6a285a3cfbf179df422e642f616a208da2f24f574d839a3"}, - {file = "pytensor-2.16.1-cp311-cp311-win_amd64.whl", hash = "sha256:78998d5f4a0501d30e9873232c6209165305ed236a50bee250c3743c258491c1"}, - {file = "pytensor-2.16.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:208b2fa548465f323ef5fd1779e37c7d4a14980c6263650e4a9f4befbe7c002e"}, - {file = "pytensor-2.16.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:096c4cbd4c22c50d01f3e3fe156d8182337011accc634cfec56043f8e93f39db"}, - {file = "pytensor-2.16.1-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:fa29fe4f15785e326d4af91b8882442820f94db8ce4293d97b4a83bd3b0e895a"}, - {file = "pytensor-2.16.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:47eca1d1fc0acd86e5b5867d40be2e361b399f6c4137510b158642cd66c76d75"}, - {file = "pytensor-2.16.1-cp39-cp39-win_amd64.whl", hash = "sha256:3191208f2e9816d205995acd94269f97db5a050f9e16f836fb7e0d8cbe824b67"}, - {file = "pytensor-2.16.1.tar.gz", hash = "sha256:dc586d5aea09e29dcc6927c3b99b13af86ea07736d1555be03e2d11282da821a"}, -] - -[package.dependencies] -cons = "*" -etuples = "*" -filelock = "*" -logical-unification = "*" -miniKanren = "*" -numpy = ">=1.17.0" -scipy = ">=0.14" -setuptools = ">=48.0.0" -typing-extensions = "*" - -[package.extras] -complete = ["pytensor[jax]", "pytensor[numba]"] -development = ["pytensor[complete]", "pytensor[rtd]", "pytensor[tests]"] -jax = ["jax", "jaxlib"] -numba = ["numba (>=0.55)", "numba-scipy (>=0.3.0)"] -rtd = ["pydot", "pydot-ng", "pydot2", "pygments", "sphinx (>=5.1.0,<6)"] -tests = ["coverage (>=5.1)", "pre-commit", "pytest", "pytest-benchmark", "pytest-cov (>=2.6.1)", "pytest-mock"] - -[[package]] -name = "pytest" -version = "7.4.2" -description = "pytest: simple powerful testing with Python" -optional = false -python-versions = ">=3.7" -files = [ - {file = "pytest-7.4.2-py3-none-any.whl", hash = "sha256:1d881c6124e08ff0a1bb75ba3ec0bfd8b5354a01c194ddd5a0a870a48d99b002"}, - {file = "pytest-7.4.2.tar.gz", hash = "sha256:a766259cfab564a2ad52cb1aae1b881a75c3eb7e34ca3779697c23ed47c47069"}, -] - -[package.dependencies] -colorama = {version = "*", markers = "sys_platform == \"win32\""} -exceptiongroup = {version = ">=1.0.0rc8", markers = "python_version < \"3.11\""} -iniconfig = "*" -packaging = "*" -pluggy = ">=0.12,<2.0" -tomli = {version = ">=1.0.0", markers = "python_version < \"3.11\""} - -[package.extras] -testing = ["argcomplete", "attrs (>=19.2.0)", "hypothesis (>=3.56)", "mock", "nose", "pygments (>=2.7.2)", "requests", "setuptools", "xmlschema"] - -[[package]] -name = "pytest-cov" -version = "4.1.0" -description = "Pytest plugin for measuring coverage." -optional = false -python-versions = ">=3.7" -files = [ - {file = "pytest-cov-4.1.0.tar.gz", hash = "sha256:3904b13dfbfec47f003b8e77fd5b589cd11904a21ddf1ab38a64f204d6a10ef6"}, - {file = "pytest_cov-4.1.0-py3-none-any.whl", hash = "sha256:6ba70b9e97e69fcc3fb45bfeab2d0a138fb65c4d0d6a41ef33983ad114be8c3a"}, -] - -[package.dependencies] -coverage = {version = ">=5.2.1", extras = ["toml"]} -pytest = ">=4.6" - -[package.extras] -testing = ["fields", "hunter", "process-tests", "pytest-xdist", "six", "virtualenv"] - -[[package]] -name = "pytest-xdist" -version = "3.3.1" -description = "pytest xdist plugin for distributed testing, most importantly across multiple CPUs" -optional = false -python-versions = ">=3.7" -files = [ - {file = "pytest-xdist-3.3.1.tar.gz", hash = "sha256:d5ee0520eb1b7bcca50a60a518ab7a7707992812c578198f8b44fdfac78e8c93"}, - {file = "pytest_xdist-3.3.1-py3-none-any.whl", hash = "sha256:ff9daa7793569e6a68544850fd3927cd257cc03a7ef76c95e86915355e82b5f2"}, -] - -[package.dependencies] -execnet = ">=1.1" -pytest = ">=6.2.0" - -[package.extras] -psutil = ["psutil (>=3.0)"] -setproctitle = ["setproctitle"] -testing = ["filelock"] - -[[package]] -name = "python-dateutil" -version = "2.8.2" -description = "Extensions to the standard Python datetime module" -optional = false -python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,>=2.7" -files = [ - {file = "python-dateutil-2.8.2.tar.gz", hash = "sha256:0123cacc1627ae19ddf3c27a5de5bd67ee4586fbdd6440d9748f8abb483d3e86"}, - {file = "python_dateutil-2.8.2-py2.py3-none-any.whl", hash = "sha256:961d03dc3453ebbc59dbdea9e4e11c5651520a876d0f4db161e8674aae935da9"}, -] - -[package.dependencies] -six = ">=1.5" - -[[package]] -name = "pytz" -version = "2023.3.post1" -description = "World timezone definitions, modern and historical" -optional = false -python-versions = "*" -files = [ - {file = "pytz-2023.3.post1-py2.py3-none-any.whl", hash = "sha256:ce42d816b81b68506614c11e8937d3aa9e41007ceb50bfdcb0749b921bf646c7"}, - {file = "pytz-2023.3.post1.tar.gz", hash = "sha256:7b4fddbeb94a1eba4b557da24f19fdf9db575192544270a9101d8509f9f43d7b"}, -] - -[[package]] -name = "pyyaml" -version = "6.0.1" -description = "YAML parser and emitter for Python" -optional = false -python-versions = ">=3.6" -files = [ - {file = "PyYAML-6.0.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:d858aa552c999bc8a8d57426ed01e40bef403cd8ccdd0fc5f6f04a00414cac2a"}, - {file = "PyYAML-6.0.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:fd66fc5d0da6d9815ba2cebeb4205f95818ff4b79c3ebe268e75d961704af52f"}, - {file = "PyYAML-6.0.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:69b023b2b4daa7548bcfbd4aa3da05b3a74b772db9e23b982788168117739938"}, - {file = "PyYAML-6.0.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:81e0b275a9ecc9c0c0c07b4b90ba548307583c125f54d5b6946cfee6360c733d"}, - {file = "PyYAML-6.0.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ba336e390cd8e4d1739f42dfe9bb83a3cc2e80f567d8805e11b46f4a943f5515"}, - {file = "PyYAML-6.0.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:326c013efe8048858a6d312ddd31d56e468118ad4cdeda36c719bf5bb6192290"}, - {file = "PyYAML-6.0.1-cp310-cp310-win32.whl", hash = "sha256:bd4af7373a854424dabd882decdc5579653d7868b8fb26dc7d0e99f823aa5924"}, - {file = "PyYAML-6.0.1-cp310-cp310-win_amd64.whl", hash = "sha256:fd1592b3fdf65fff2ad0004b5e363300ef59ced41c2e6b3a99d4089fa8c5435d"}, - {file = "PyYAML-6.0.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:6965a7bc3cf88e5a1c3bd2e0b5c22f8d677dc88a455344035f03399034eb3007"}, - {file = "PyYAML-6.0.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:f003ed9ad21d6a4713f0a9b5a7a0a79e08dd0f221aff4525a2be4c346ee60aab"}, - {file = "PyYAML-6.0.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:42f8152b8dbc4fe7d96729ec2b99c7097d656dc1213a3229ca5383f973a5ed6d"}, - {file = "PyYAML-6.0.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:062582fca9fabdd2c8b54a3ef1c978d786e0f6b3a1510e0ac93ef59e0ddae2bc"}, - {file = "PyYAML-6.0.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d2b04aac4d386b172d5b9692e2d2da8de7bfb6c387fa4f801fbf6fb2e6ba4673"}, - {file = "PyYAML-6.0.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:e7d73685e87afe9f3b36c799222440d6cf362062f78be1013661b00c5c6f678b"}, - {file = "PyYAML-6.0.1-cp311-cp311-win32.whl", hash = "sha256:1635fd110e8d85d55237ab316b5b011de701ea0f29d07611174a1b42f1444741"}, - {file = "PyYAML-6.0.1-cp311-cp311-win_amd64.whl", hash = "sha256:bf07ee2fef7014951eeb99f56f39c9bb4af143d8aa3c21b1677805985307da34"}, - {file = "PyYAML-6.0.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:855fb52b0dc35af121542a76b9a84f8d1cd886ea97c84703eaa6d88e37a2ad28"}, - {file = "PyYAML-6.0.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:40df9b996c2b73138957fe23a16a4f0ba614f4c0efce1e9406a184b6d07fa3a9"}, - {file = "PyYAML-6.0.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6c22bec3fbe2524cde73d7ada88f6566758a8f7227bfbf93a408a9d86bcc12a0"}, - {file = "PyYAML-6.0.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:8d4e9c88387b0f5c7d5f281e55304de64cf7f9c0021a3525bd3b1c542da3b0e4"}, - {file = "PyYAML-6.0.1-cp312-cp312-win32.whl", hash = "sha256:d483d2cdf104e7c9fa60c544d92981f12ad66a457afae824d146093b8c294c54"}, - {file = "PyYAML-6.0.1-cp312-cp312-win_amd64.whl", hash = "sha256:0d3304d8c0adc42be59c5f8a4d9e3d7379e6955ad754aa9d6ab7a398b59dd1df"}, - {file = "PyYAML-6.0.1-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:50550eb667afee136e9a77d6dc71ae76a44df8b3e51e41b77f6de2932bfe0f47"}, - {file = "PyYAML-6.0.1-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1fe35611261b29bd1de0070f0b2f47cb6ff71fa6595c077e42bd0c419fa27b98"}, - {file = "PyYAML-6.0.1-cp36-cp36m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:704219a11b772aea0d8ecd7058d0082713c3562b4e271b849ad7dc4a5c90c13c"}, - {file = "PyYAML-6.0.1-cp36-cp36m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:afd7e57eddb1a54f0f1a974bc4391af8bcce0b444685d936840f125cf046d5bd"}, - {file = "PyYAML-6.0.1-cp36-cp36m-win32.whl", hash = "sha256:fca0e3a251908a499833aa292323f32437106001d436eca0e6e7833256674585"}, - {file = "PyYAML-6.0.1-cp36-cp36m-win_amd64.whl", hash = "sha256:f22ac1c3cac4dbc50079e965eba2c1058622631e526bd9afd45fedd49ba781fa"}, - {file = "PyYAML-6.0.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:b1275ad35a5d18c62a7220633c913e1b42d44b46ee12554e5fd39c70a243d6a3"}, - {file = "PyYAML-6.0.1-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:18aeb1bf9a78867dc38b259769503436b7c72f7a1f1f4c93ff9a17de54319b27"}, - {file = "PyYAML-6.0.1-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:596106435fa6ad000c2991a98fa58eeb8656ef2325d7e158344fb33864ed87e3"}, - {file = "PyYAML-6.0.1-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:baa90d3f661d43131ca170712d903e6295d1f7a0f595074f151c0aed377c9b9c"}, - {file = "PyYAML-6.0.1-cp37-cp37m-win32.whl", hash = "sha256:9046c58c4395dff28dd494285c82ba00b546adfc7ef001486fbf0324bc174fba"}, - {file = "PyYAML-6.0.1-cp37-cp37m-win_amd64.whl", hash = "sha256:4fb147e7a67ef577a588a0e2c17b6db51dda102c71de36f8549b6816a96e1867"}, - {file = "PyYAML-6.0.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:1d4c7e777c441b20e32f52bd377e0c409713e8bb1386e1099c2415f26e479595"}, - {file = "PyYAML-6.0.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a0cd17c15d3bb3fa06978b4e8958dcdc6e0174ccea823003a106c7d4d7899ac5"}, - {file = "PyYAML-6.0.1-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:28c119d996beec18c05208a8bd78cbe4007878c6dd15091efb73a30e90539696"}, - {file = "PyYAML-6.0.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7e07cbde391ba96ab58e532ff4803f79c4129397514e1413a7dc761ccd755735"}, - {file = "PyYAML-6.0.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:49a183be227561de579b4a36efbb21b3eab9651dd81b1858589f796549873dd6"}, - {file = "PyYAML-6.0.1-cp38-cp38-win32.whl", hash = "sha256:184c5108a2aca3c5b3d3bf9395d50893a7ab82a38004c8f61c258d4428e80206"}, - {file = "PyYAML-6.0.1-cp38-cp38-win_amd64.whl", hash = "sha256:1e2722cc9fbb45d9b87631ac70924c11d3a401b2d7f410cc0e3bbf249f2dca62"}, - {file = "PyYAML-6.0.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:9eb6caa9a297fc2c2fb8862bc5370d0303ddba53ba97e71f08023b6cd73d16a8"}, - {file = "PyYAML-6.0.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:c8098ddcc2a85b61647b2590f825f3db38891662cfc2fc776415143f599bb859"}, - {file = "PyYAML-6.0.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5773183b6446b2c99bb77e77595dd486303b4faab2b086e7b17bc6bef28865f6"}, - {file = "PyYAML-6.0.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b786eecbdf8499b9ca1d697215862083bd6d2a99965554781d0d8d1ad31e13a0"}, - {file = "PyYAML-6.0.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bc1bf2925a1ecd43da378f4db9e4f799775d6367bdb94671027b73b393a7c42c"}, - {file = "PyYAML-6.0.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:04ac92ad1925b2cff1db0cfebffb6ffc43457495c9b3c39d3fcae417d7125dc5"}, - {file = "PyYAML-6.0.1-cp39-cp39-win32.whl", hash = "sha256:faca3bdcf85b2fc05d06ff3fbc1f83e1391b3e724afa3feba7d13eeab355484c"}, - {file = "PyYAML-6.0.1-cp39-cp39-win_amd64.whl", hash = "sha256:510c9deebc5c0225e8c96813043e62b680ba2f9c50a08d3724c7f28a747d1486"}, - {file = "PyYAML-6.0.1.tar.gz", hash = "sha256:bfdf460b1736c775f2ba9f6a92bca30bc2095067b8a9d77876d1fad6cc3b4a43"}, -] - -[[package]] -name = "pyyaml-env-tag" -version = "0.1" -description = "A custom YAML tag for referencing environment variables in YAML files. " -optional = false -python-versions = ">=3.6" -files = [ - {file = "pyyaml_env_tag-0.1-py3-none-any.whl", hash = "sha256:af31106dec8a4d68c60207c1886031cbf839b68aa7abccdb19868200532c2069"}, - {file = "pyyaml_env_tag-0.1.tar.gz", hash = "sha256:70092675bda14fdec33b31ba77e7543de9ddc88f2e5b99160396572d11525bdb"}, -] - -[package.dependencies] -pyyaml = "*" - -[[package]] -name = "regex" -version = "2022.10.31" -description = "Alternative regular expression module, to replace re." -optional = false -python-versions = ">=3.6" -files = [ - {file = "regex-2022.10.31-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:a8ff454ef0bb061e37df03557afda9d785c905dab15584860f982e88be73015f"}, - {file = "regex-2022.10.31-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:1eba476b1b242620c266edf6325b443a2e22b633217a9835a52d8da2b5c051f9"}, - {file = "regex-2022.10.31-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d0e5af9a9effb88535a472e19169e09ce750c3d442fb222254a276d77808620b"}, - {file = "regex-2022.10.31-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d03fe67b2325cb3f09be029fd5da8df9e6974f0cde2c2ac6a79d2634e791dd57"}, - {file = "regex-2022.10.31-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a9d0b68ac1743964755ae2d89772c7e6fb0118acd4d0b7464eaf3921c6b49dd4"}, - {file = "regex-2022.10.31-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8a45b6514861916c429e6059a55cf7db74670eaed2052a648e3e4d04f070e001"}, - {file = "regex-2022.10.31-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:8b0886885f7323beea6f552c28bff62cbe0983b9fbb94126531693ea6c5ebb90"}, - {file = "regex-2022.10.31-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:5aefb84a301327ad115e9d346c8e2760009131d9d4b4c6b213648d02e2abe144"}, - {file = "regex-2022.10.31-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:702d8fc6f25bbf412ee706bd73019da5e44a8400861dfff7ff31eb5b4a1276dc"}, - {file = "regex-2022.10.31-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:a3c1ebd4ed8e76e886507c9eddb1a891673686c813adf889b864a17fafcf6d66"}, - {file = "regex-2022.10.31-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:50921c140561d3db2ab9f5b11c5184846cde686bb5a9dc64cae442926e86f3af"}, - {file = "regex-2022.10.31-cp310-cp310-musllinux_1_1_s390x.whl", hash = "sha256:7db345956ecce0c99b97b042b4ca7326feeec6b75facd8390af73b18e2650ffc"}, - {file = "regex-2022.10.31-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:763b64853b0a8f4f9cfb41a76a4a85a9bcda7fdda5cb057016e7706fde928e66"}, - {file = "regex-2022.10.31-cp310-cp310-win32.whl", hash = "sha256:44136355e2f5e06bf6b23d337a75386371ba742ffa771440b85bed367c1318d1"}, - {file = "regex-2022.10.31-cp310-cp310-win_amd64.whl", hash = "sha256:bfff48c7bd23c6e2aec6454aaf6edc44444b229e94743b34bdcdda2e35126cf5"}, - {file = "regex-2022.10.31-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:4b4b1fe58cd102d75ef0552cf17242705ce0759f9695334a56644ad2d83903fe"}, - {file = "regex-2022.10.31-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:542e3e306d1669b25936b64917285cdffcd4f5c6f0247636fec037187bd93542"}, - {file = "regex-2022.10.31-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c27cc1e4b197092e50ddbf0118c788d9977f3f8f35bfbbd3e76c1846a3443df7"}, - {file = "regex-2022.10.31-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b8e38472739028e5f2c3a4aded0ab7eadc447f0d84f310c7a8bb697ec417229e"}, - {file = "regex-2022.10.31-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:76c598ca73ec73a2f568e2a72ba46c3b6c8690ad9a07092b18e48ceb936e9f0c"}, - {file = "regex-2022.10.31-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c28d3309ebd6d6b2cf82969b5179bed5fefe6142c70f354ece94324fa11bf6a1"}, - {file = "regex-2022.10.31-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:9af69f6746120998cd9c355e9c3c6aec7dff70d47247188feb4f829502be8ab4"}, - {file = "regex-2022.10.31-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:a5f9505efd574d1e5b4a76ac9dd92a12acb2b309551e9aa874c13c11caefbe4f"}, - {file = "regex-2022.10.31-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:5ff525698de226c0ca743bfa71fc6b378cda2ddcf0d22d7c37b1cc925c9650a5"}, - {file = "regex-2022.10.31-cp311-cp311-musllinux_1_1_ppc64le.whl", hash = "sha256:4fe7fda2fe7c8890d454f2cbc91d6c01baf206fbc96d89a80241a02985118c0c"}, - {file = "regex-2022.10.31-cp311-cp311-musllinux_1_1_s390x.whl", hash = "sha256:2cdc55ca07b4e70dda898d2ab7150ecf17c990076d3acd7a5f3b25cb23a69f1c"}, - {file = "regex-2022.10.31-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:44a6c2f6374e0033873e9ed577a54a3602b4f609867794c1a3ebba65e4c93ee7"}, - {file = "regex-2022.10.31-cp311-cp311-win32.whl", hash = "sha256:d8716f82502997b3d0895d1c64c3b834181b1eaca28f3f6336a71777e437c2af"}, - {file = "regex-2022.10.31-cp311-cp311-win_amd64.whl", hash = "sha256:61edbca89aa3f5ef7ecac8c23d975fe7261c12665f1d90a6b1af527bba86ce61"}, - {file = "regex-2022.10.31-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:0a069c8483466806ab94ea9068c34b200b8bfc66b6762f45a831c4baaa9e8cdd"}, - {file = "regex-2022.10.31-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d26166acf62f731f50bdd885b04b38828436d74e8e362bfcb8df221d868b5d9b"}, - {file = "regex-2022.10.31-cp36-cp36m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ac741bf78b9bb432e2d314439275235f41656e189856b11fb4e774d9f7246d81"}, - {file = "regex-2022.10.31-cp36-cp36m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:75f591b2055523fc02a4bbe598aa867df9e953255f0b7f7715d2a36a9c30065c"}, - {file = "regex-2022.10.31-cp36-cp36m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6b30bddd61d2a3261f025ad0f9ee2586988c6a00c780a2fb0a92cea2aa702c54"}, - {file = "regex-2022.10.31-cp36-cp36m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ef4163770525257876f10e8ece1cf25b71468316f61451ded1a6f44273eedeb5"}, - {file = "regex-2022.10.31-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:7b280948d00bd3973c1998f92e22aa3ecb76682e3a4255f33e1020bd32adf443"}, - {file = "regex-2022.10.31-cp36-cp36m-musllinux_1_1_aarch64.whl", hash = "sha256:d0213671691e341f6849bf33cd9fad21f7b1cb88b89e024f33370733fec58742"}, - {file = "regex-2022.10.31-cp36-cp36m-musllinux_1_1_i686.whl", hash = "sha256:22e7ebc231d28393dfdc19b185d97e14a0f178bedd78e85aad660e93b646604e"}, - {file = "regex-2022.10.31-cp36-cp36m-musllinux_1_1_ppc64le.whl", hash = "sha256:8ad241da7fac963d7573cc67a064c57c58766b62a9a20c452ca1f21050868dfa"}, - {file = "regex-2022.10.31-cp36-cp36m-musllinux_1_1_s390x.whl", hash = "sha256:586b36ebda81e6c1a9c5a5d0bfdc236399ba6595e1397842fd4a45648c30f35e"}, - {file = "regex-2022.10.31-cp36-cp36m-musllinux_1_1_x86_64.whl", hash = "sha256:0653d012b3bf45f194e5e6a41df9258811ac8fc395579fa82958a8b76286bea4"}, - {file = "regex-2022.10.31-cp36-cp36m-win32.whl", hash = "sha256:144486e029793a733e43b2e37df16a16df4ceb62102636ff3db6033994711066"}, - {file = "regex-2022.10.31-cp36-cp36m-win_amd64.whl", hash = "sha256:c14b63c9d7bab795d17392c7c1f9aaabbffd4cf4387725a0ac69109fb3b550c6"}, - {file = "regex-2022.10.31-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:4cac3405d8dda8bc6ed499557625585544dd5cbf32072dcc72b5a176cb1271c8"}, - {file = "regex-2022.10.31-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:23cbb932cc53a86ebde0fb72e7e645f9a5eec1a5af7aa9ce333e46286caef783"}, - {file = "regex-2022.10.31-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:74bcab50a13960f2a610cdcd066e25f1fd59e23b69637c92ad470784a51b1347"}, - {file = "regex-2022.10.31-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:78d680ef3e4d405f36f0d6d1ea54e740366f061645930072d39bca16a10d8c93"}, - {file = "regex-2022.10.31-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ce6910b56b700bea7be82c54ddf2e0ed792a577dfaa4a76b9af07d550af435c6"}, - {file = "regex-2022.10.31-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:659175b2144d199560d99a8d13b2228b85e6019b6e09e556209dfb8c37b78a11"}, - {file = "regex-2022.10.31-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:1ddf14031a3882f684b8642cb74eea3af93a2be68893901b2b387c5fd92a03ec"}, - {file = "regex-2022.10.31-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:b683e5fd7f74fb66e89a1ed16076dbab3f8e9f34c18b1979ded614fe10cdc4d9"}, - {file = "regex-2022.10.31-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:2bde29cc44fa81c0a0c8686992c3080b37c488df167a371500b2a43ce9f026d1"}, - {file = "regex-2022.10.31-cp37-cp37m-musllinux_1_1_ppc64le.whl", hash = "sha256:4919899577ba37f505aaebdf6e7dc812d55e8f097331312db7f1aab18767cce8"}, - {file = "regex-2022.10.31-cp37-cp37m-musllinux_1_1_s390x.whl", hash = "sha256:9c94f7cc91ab16b36ba5ce476f1904c91d6c92441f01cd61a8e2729442d6fcf5"}, - {file = "regex-2022.10.31-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:ae1e96785696b543394a4e3f15f3f225d44f3c55dafe3f206493031419fedf95"}, - {file = "regex-2022.10.31-cp37-cp37m-win32.whl", hash = "sha256:c670f4773f2f6f1957ff8a3962c7dd12e4be54d05839b216cb7fd70b5a1df394"}, - {file = "regex-2022.10.31-cp37-cp37m-win_amd64.whl", hash = "sha256:8e0caeff18b96ea90fc0eb6e3bdb2b10ab5b01a95128dfeccb64a7238decf5f0"}, - {file = "regex-2022.10.31-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:131d4be09bea7ce2577f9623e415cab287a3c8e0624f778c1d955ec7c281bd4d"}, - {file = "regex-2022.10.31-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:e613a98ead2005c4ce037c7b061f2409a1a4e45099edb0ef3200ee26ed2a69a8"}, - {file = "regex-2022.10.31-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:052b670fafbe30966bbe5d025e90b2a491f85dfe5b2583a163b5e60a85a321ad"}, - {file = "regex-2022.10.31-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:aa62a07ac93b7cb6b7d0389d8ef57ffc321d78f60c037b19dfa78d6b17c928ee"}, - {file = "regex-2022.10.31-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5352bea8a8f84b89d45ccc503f390a6be77917932b1c98c4cdc3565137acc714"}, - {file = "regex-2022.10.31-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:20f61c9944f0be2dc2b75689ba409938c14876c19d02f7585af4460b6a21403e"}, - {file = "regex-2022.10.31-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:29c04741b9ae13d1e94cf93fca257730b97ce6ea64cfe1eba11cf9ac4e85afb6"}, - {file = "regex-2022.10.31-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:543883e3496c8b6d58bd036c99486c3c8387c2fc01f7a342b760c1ea3158a318"}, - {file = "regex-2022.10.31-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:b7a8b43ee64ca8f4befa2bea4083f7c52c92864d8518244bfa6e88c751fa8fff"}, - {file = "regex-2022.10.31-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:6a9a19bea8495bb419dc5d38c4519567781cd8d571c72efc6aa959473d10221a"}, - {file = "regex-2022.10.31-cp38-cp38-musllinux_1_1_ppc64le.whl", hash = "sha256:6ffd55b5aedc6f25fd8d9f905c9376ca44fcf768673ffb9d160dd6f409bfda73"}, - {file = "regex-2022.10.31-cp38-cp38-musllinux_1_1_s390x.whl", hash = "sha256:4bdd56ee719a8f751cf5a593476a441c4e56c9b64dc1f0f30902858c4ef8771d"}, - {file = "regex-2022.10.31-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:8ca88da1bd78990b536c4a7765f719803eb4f8f9971cc22d6ca965c10a7f2c4c"}, - {file = "regex-2022.10.31-cp38-cp38-win32.whl", hash = "sha256:5a260758454580f11dd8743fa98319bb046037dfab4f7828008909d0aa5292bc"}, - {file = "regex-2022.10.31-cp38-cp38-win_amd64.whl", hash = "sha256:5e6a5567078b3eaed93558842346c9d678e116ab0135e22eb72db8325e90b453"}, - {file = "regex-2022.10.31-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:5217c25229b6a85049416a5c1e6451e9060a1edcf988641e309dbe3ab26d3e49"}, - {file = "regex-2022.10.31-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:4bf41b8b0a80708f7e0384519795e80dcb44d7199a35d52c15cc674d10b3081b"}, - {file = "regex-2022.10.31-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0cf0da36a212978be2c2e2e2d04bdff46f850108fccc1851332bcae51c8907cc"}, - {file = "regex-2022.10.31-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d403d781b0e06d2922435ce3b8d2376579f0c217ae491e273bab8d092727d244"}, - {file = "regex-2022.10.31-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a37d51fa9a00d265cf73f3de3930fa9c41548177ba4f0faf76e61d512c774690"}, - {file = "regex-2022.10.31-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e4f781ffedd17b0b834c8731b75cce2639d5a8afe961c1e58ee7f1f20b3af185"}, - {file = "regex-2022.10.31-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d243b36fbf3d73c25e48014961e83c19c9cc92530516ce3c43050ea6276a2ab7"}, - {file = "regex-2022.10.31-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:370f6e97d02bf2dd20d7468ce4f38e173a124e769762d00beadec3bc2f4b3bc4"}, - {file = "regex-2022.10.31-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:597f899f4ed42a38df7b0e46714880fb4e19a25c2f66e5c908805466721760f5"}, - {file = "regex-2022.10.31-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:7dbdce0c534bbf52274b94768b3498abdf675a691fec5f751b6057b3030f34c1"}, - {file = "regex-2022.10.31-cp39-cp39-musllinux_1_1_ppc64le.whl", hash = "sha256:22960019a842777a9fa5134c2364efaed5fbf9610ddc5c904bd3a400973b0eb8"}, - {file = "regex-2022.10.31-cp39-cp39-musllinux_1_1_s390x.whl", hash = "sha256:7f5a3ffc731494f1a57bd91c47dc483a1e10048131ffb52d901bfe2beb6102e8"}, - {file = "regex-2022.10.31-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:7ef6b5942e6bfc5706301a18a62300c60db9af7f6368042227ccb7eeb22d0892"}, - {file = "regex-2022.10.31-cp39-cp39-win32.whl", hash = "sha256:395161bbdbd04a8333b9ff9763a05e9ceb4fe210e3c7690f5e68cedd3d65d8e1"}, - {file = "regex-2022.10.31-cp39-cp39-win_amd64.whl", hash = "sha256:957403a978e10fb3ca42572a23e6f7badff39aa1ce2f4ade68ee452dc6807692"}, - {file = "regex-2022.10.31.tar.gz", hash = "sha256:a3a98921da9a1bf8457aeee6a551948a83601689e5ecdd736894ea9bbec77e83"}, -] - -[[package]] -name = "requests" -version = "2.31.0" -description = "Python HTTP for Humans." -optional = false -python-versions = ">=3.7" -files = [ - {file = "requests-2.31.0-py3-none-any.whl", hash = "sha256:58cd2187c01e70e6e26505bca751777aa9f2ee0b7f4300988b709f44e013003f"}, - {file = "requests-2.31.0.tar.gz", hash = "sha256:942c5a758f98d790eaed1a29cb6eefc7ffb0d1cf7af05c3d2791656dbd6ad1e1"}, -] - -[package.dependencies] -certifi = ">=2017.4.17" -charset-normalizer = ">=2,<4" -idna = ">=2.5,<4" -urllib3 = ">=1.21.1,<3" - -[package.extras] -socks = ["PySocks (>=1.5.6,!=1.5.7)"] -use-chardet-on-py3 = ["chardet (>=3.0.2,<6)"] - -[[package]] -name = "ruff" -version = "0.0.253" -description = "An extremely fast Python linter, written in Rust." -optional = false -python-versions = ">=3.7" -files = [ - {file = "ruff-0.0.253-py3-none-macosx_10_7_x86_64.whl", hash = "sha256:69126b80d4da50a394cfe9da947377841cc6c83b0e05cfe9933672ce5c61bfcf"}, - {file = "ruff-0.0.253-py3-none-macosx_10_9_x86_64.macosx_11_0_arm64.macosx_10_9_universal2.whl", hash = "sha256:0f44caf5bbdaeacc3cba4ee3369638e4f6e4e71c9ca773d2f3fc3f65e4bfb434"}, - {file = "ruff-0.0.253-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8144a2fd6533e7a0dbaaf9a3dde44b8414eebf5a86a1fe21e0471d052a3e9c14"}, - {file = "ruff-0.0.253-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:07603b362f0dad56e30e7ef2f37bf480732ff8bcf52fe4fd6c9445eb42259f42"}, - {file = "ruff-0.0.253-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:68f9a50f48510a443ec57bcf51656bbef47e5972290c450398108ac2a53dfd32"}, - {file = "ruff-0.0.253-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:c6ed42010c379d42b81b537957b413cf8531a00d0a6270913e8527d9d73c7e0c"}, - {file = "ruff-0.0.253-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ba4b3921fa9c59855b66e1a5ef140d0d872f15a83282bff5b5e3e8db89a45aa2"}, - {file = "ruff-0.0.253-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:60bda6fd99f9d3919df4362b671a12c83ef83279fc7bc1dc0e1aa689dfd91a71"}, - {file = "ruff-0.0.253-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:19061d9b5809a0505a233580b48b59b847823ab90e266f8ae40cb31d3708bacf"}, - {file = "ruff-0.0.253-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:6ee92a7688f327c664891567aa24e4a8cae8635934df95e0dbe65b0e991fcc6e"}, - {file = "ruff-0.0.253-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:f0ff811ea61684c6e9284afa701b8388818ab5ef8ebd6144c15c9ba64f459f1e"}, - {file = "ruff-0.0.253-py3-none-musllinux_1_2_i686.whl", hash = "sha256:4548734b2671b80ee4c20aa410d7d2a5b32f087f8759d4f5991c74b8cfa51d7b"}, - {file = "ruff-0.0.253-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:e2485f728f04bf3bd6142e55dd2869c769299b73a4bdbe1a795e98332df75561"}, - {file = "ruff-0.0.253-py3-none-win32.whl", hash = "sha256:a66109185382375246d7b0dae2f594801fd8ceb5f8206159c55791aaec9aa4bb"}, - {file = "ruff-0.0.253-py3-none-win_amd64.whl", hash = "sha256:a64e9f97a6b0bfce924e65fa845f669c969d42c30fb61e1e4d87b2c70d835cb9"}, - {file = "ruff-0.0.253-py3-none-win_arm64.whl", hash = "sha256:506987ac3bc212cd74bf1ca032756e67ada93c4add3b7541e3549bbad5e0fc40"}, - {file = "ruff-0.0.253.tar.gz", hash = "sha256:ab746c843a9673d2637bcbcb45da12ed4d44c0c90f0823484d6dcb660118b539"}, -] - -[[package]] -name = "scipy" -version = "1.9.3" -description = "Fundamental algorithms for scientific computing in Python" -optional = false -python-versions = ">=3.8" -files = [ - {file = "scipy-1.9.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:1884b66a54887e21addf9c16fb588720a8309a57b2e258ae1c7986d4444d3bc0"}, - {file = "scipy-1.9.3-cp310-cp310-macosx_12_0_arm64.whl", hash = "sha256:83b89e9586c62e787f5012e8475fbb12185bafb996a03257e9675cd73d3736dd"}, - {file = "scipy-1.9.3-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1a72d885fa44247f92743fc20732ae55564ff2a519e8302fb7e18717c5355a8b"}, - {file = "scipy-1.9.3-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d01e1dd7b15bd2449c8bfc6b7cc67d630700ed655654f0dfcf121600bad205c9"}, - {file = "scipy-1.9.3-cp310-cp310-win_amd64.whl", hash = "sha256:68239b6aa6f9c593da8be1509a05cb7f9efe98b80f43a5861cd24c7557e98523"}, - {file = "scipy-1.9.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:b41bc822679ad1c9a5f023bc93f6d0543129ca0f37c1ce294dd9d386f0a21096"}, - {file = "scipy-1.9.3-cp311-cp311-macosx_12_0_arm64.whl", hash = "sha256:90453d2b93ea82a9f434e4e1cba043e779ff67b92f7a0e85d05d286a3625df3c"}, - {file = "scipy-1.9.3-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:83c06e62a390a9167da60bedd4575a14c1f58ca9dfde59830fc42e5197283dab"}, - {file = "scipy-1.9.3-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:abaf921531b5aeaafced90157db505e10345e45038c39e5d9b6c7922d68085cb"}, - {file = "scipy-1.9.3-cp311-cp311-win_amd64.whl", hash = "sha256:06d2e1b4c491dc7d8eacea139a1b0b295f74e1a1a0f704c375028f8320d16e31"}, - {file = "scipy-1.9.3-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:5a04cd7d0d3eff6ea4719371cbc44df31411862b9646db617c99718ff68d4840"}, - {file = "scipy-1.9.3-cp38-cp38-macosx_12_0_arm64.whl", hash = "sha256:545c83ffb518094d8c9d83cce216c0c32f8c04aaf28b92cc8283eda0685162d5"}, - {file = "scipy-1.9.3-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0d54222d7a3ba6022fdf5773931b5d7c56efe41ede7f7128c7b1637700409108"}, - {file = "scipy-1.9.3-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cff3a5295234037e39500d35316a4c5794739433528310e117b8a9a0c76d20fc"}, - {file = "scipy-1.9.3-cp38-cp38-win_amd64.whl", hash = "sha256:2318bef588acc7a574f5bfdff9c172d0b1bf2c8143d9582e05f878e580a3781e"}, - {file = "scipy-1.9.3-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:d644a64e174c16cb4b2e41dfea6af722053e83d066da7343f333a54dae9bc31c"}, - {file = "scipy-1.9.3-cp39-cp39-macosx_12_0_arm64.whl", hash = "sha256:da8245491d73ed0a994ed9c2e380fd058ce2fa8a18da204681f2fe1f57f98f95"}, - {file = "scipy-1.9.3-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4db5b30849606a95dcf519763dd3ab6fe9bd91df49eba517359e450a7d80ce2e"}, - {file = "scipy-1.9.3-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c68db6b290cbd4049012990d7fe71a2abd9ffbe82c0056ebe0f01df8be5436b0"}, - {file = "scipy-1.9.3-cp39-cp39-win_amd64.whl", hash = "sha256:5b88e6d91ad9d59478fafe92a7c757d00c59e3bdc3331be8ada76a4f8d683f58"}, - {file = "scipy-1.9.3.tar.gz", hash = "sha256:fbc5c05c85c1a02be77b1ff591087c83bc44579c6d2bd9fb798bb64ea5e1a027"}, -] - -[package.dependencies] -numpy = ">=1.18.5,<1.26.0" - -[package.extras] -dev = ["flake8", "mypy", "pycodestyle", "typing_extensions"] -doc = ["matplotlib (>2)", "numpydoc", "pydata-sphinx-theme (==0.9.0)", "sphinx (!=4.1.0)", "sphinx-panels (>=0.5.2)", "sphinx-tabs"] -test = ["asv", "gmpy2", "mpmath", "pytest", "pytest-cov", "pytest-xdist", "scikit-umfpack", "threadpoolctl"] - -[[package]] -name = "seaborn" -version = "0.12.2" -description = "Statistical data visualization" -optional = false -python-versions = ">=3.7" -files = [ - {file = "seaborn-0.12.2-py3-none-any.whl", hash = "sha256:ebf15355a4dba46037dfd65b7350f014ceb1f13c05e814eda2c9f5fd731afc08"}, - {file = "seaborn-0.12.2.tar.gz", hash = "sha256:374645f36509d0dcab895cba5b47daf0586f77bfe3b36c97c607db7da5be0139"}, -] - -[package.dependencies] -matplotlib = ">=3.1,<3.6.1 || >3.6.1" -numpy = ">=1.17,<1.24.0 || >1.24.0" -pandas = ">=0.25" - -[package.extras] -dev = ["flake8", "flit", "mypy", "pandas-stubs", "pre-commit", "pytest", "pytest-cov", "pytest-xdist"] -docs = ["ipykernel", "nbconvert", "numpydoc", "pydata_sphinx_theme (==0.10.0rc2)", "pyyaml", "sphinx-copybutton", "sphinx-design", "sphinx-issues"] -stats = ["scipy (>=1.3)", "statsmodels (>=0.10)"] - -[[package]] -name = "setuptools" -version = "68.2.2" -description = "Easily download, build, install, upgrade, and uninstall Python packages" -optional = false -python-versions = ">=3.8" -files = [ - {file = "setuptools-68.2.2-py3-none-any.whl", hash = "sha256:b454a35605876da60632df1a60f736524eb73cc47bbc9f3f1ef1b644de74fd2a"}, - {file = "setuptools-68.2.2.tar.gz", hash = "sha256:4ac1475276d2f1c48684874089fefcd83bd7162ddaafb81fac866ba0db282a87"}, -] - -[package.extras] -docs = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "pygments-github-lexers (==0.0.5)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-favicon", "sphinx-hoverxref (<2)", "sphinx-inline-tabs", "sphinx-lint", "sphinx-notfound-page (>=1,<2)", "sphinx-reredirects", "sphinxcontrib-towncrier"] -testing = ["build[virtualenv]", "filelock (>=3.4.0)", "flake8-2020", "ini2toml[lite] (>=0.9)", "jaraco.develop (>=7.21)", "jaraco.envs (>=2.2)", "jaraco.path (>=3.2.0)", "pip (>=19.1)", "pytest (>=6)", "pytest-black (>=0.3.7)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=2.2)", "pytest-mypy (>=0.9.1)", "pytest-perf", "pytest-ruff", "pytest-timeout", "pytest-xdist", "tomli-w (>=1.0.0)", "virtualenv (>=13.0.0)", "wheel"] -testing-integration = ["build[virtualenv] (>=1.0.3)", "filelock (>=3.4.0)", "jaraco.envs (>=2.2)", "jaraco.path (>=3.2.0)", "packaging (>=23.1)", "pytest", "pytest-enabler", "pytest-xdist", "tomli", "virtualenv (>=13.0.0)", "wheel"] - -[[package]] -name = "setuptools-scm" -version = "7.1.0" -description = "the blessed package to manage your versions by scm tags" -optional = false -python-versions = ">=3.7" -files = [ - {file = "setuptools_scm-7.1.0-py3-none-any.whl", hash = "sha256:73988b6d848709e2af142aa48c986ea29592bbcfca5375678064708205253d8e"}, - {file = "setuptools_scm-7.1.0.tar.gz", hash = "sha256:6c508345a771aad7d56ebff0e70628bf2b0ec7573762be9960214730de278f27"}, -] - -[package.dependencies] -packaging = ">=20.0" -setuptools = "*" -tomli = {version = ">=1.0.0", markers = "python_version < \"3.11\""} -typing-extensions = "*" - -[package.extras] -test = ["pytest (>=6.2)", "virtualenv (>20)"] -toml = ["setuptools (>=42)"] - -[[package]] -name = "six" -version = "1.16.0" -description = "Python 2 and 3 compatibility utilities" -optional = false -python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*" -files = [ - {file = "six-1.16.0-py2.py3-none-any.whl", hash = "sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254"}, - {file = "six-1.16.0.tar.gz", hash = "sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926"}, -] - -[[package]] -name = "tomli" -version = "2.0.1" -description = "A lil' TOML parser" -optional = false -python-versions = ">=3.7" -files = [ - {file = "tomli-2.0.1-py3-none-any.whl", hash = "sha256:939de3e7a6161af0c887ef91b7d41a53e7c5a1ca976325f429cb46ea9bc30ecc"}, - {file = "tomli-2.0.1.tar.gz", hash = "sha256:de526c12914f0c550d15924c62d72abc48d6fe7364aa87328337a31007fe8a4f"}, -] - -[[package]] -name = "toolz" -version = "0.12.0" -description = "List processing tools and functional utilities" -optional = false -python-versions = ">=3.5" -files = [ - {file = "toolz-0.12.0-py3-none-any.whl", hash = "sha256:2059bd4148deb1884bb0eb770a3cde70e7f954cfbbdc2285f1f2de01fd21eb6f"}, - {file = "toolz-0.12.0.tar.gz", hash = "sha256:88c570861c440ee3f2f6037c4654613228ff40c93a6c25e0eba70d17282c6194"}, -] - -[[package]] -name = "typing-extensions" -version = "4.8.0" -description = "Backported and Experimental Type Hints for Python 3.8+" -optional = false -python-versions = ">=3.8" -files = [ - {file = "typing_extensions-4.8.0-py3-none-any.whl", hash = "sha256:8f92fc8806f9a6b641eaa5318da32b44d401efaac0f6678c9bc448ba3605faa0"}, - {file = "typing_extensions-4.8.0.tar.gz", hash = "sha256:df8e4339e9cb77357558cbdbceca33c303714cf861d1eef15e1070055ae8b7ef"}, -] - -[[package]] -name = "tzdata" -version = "2023.3" -description = "Provider of IANA time zone data" -optional = false -python-versions = ">=2" -files = [ - {file = "tzdata-2023.3-py2.py3-none-any.whl", hash = "sha256:7e65763eef3120314099b6939b5546db7adce1e7d6f2e179e3df563c70511eda"}, - {file = "tzdata-2023.3.tar.gz", hash = "sha256:11ef1e08e54acb0d4f95bdb1be05da659673de4acbd21bf9c69e94cc5e907a3a"}, -] - -[[package]] -name = "urllib3" -version = "2.0.4" -description = "HTTP library with thread-safe connection pooling, file post, and more." -optional = false -python-versions = ">=3.7" -files = [ - {file = "urllib3-2.0.4-py3-none-any.whl", hash = "sha256:de7df1803967d2c2a98e4b11bb7d6bd9210474c46e8a0401514e3a42a75ebde4"}, - {file = "urllib3-2.0.4.tar.gz", hash = "sha256:8d22f86aae8ef5e410d4f539fde9ce6b2113a001bb4d189e0aed70642d602b11"}, -] - -[package.extras] -brotli = ["brotli (>=1.0.9)", "brotlicffi (>=0.8.0)"] -secure = ["certifi", "cryptography (>=1.9)", "idna (>=2.0.0)", "pyopenssl (>=17.1.0)", "urllib3-secure-extra"] -socks = ["pysocks (>=1.5.6,!=1.5.7,<2.0)"] -zstd = ["zstandard (>=0.18.0)"] - -[[package]] -name = "virtualenv" -version = "20.24.5" -description = "Virtual Python Environment builder" -optional = false -python-versions = ">=3.7" -files = [ - {file = "virtualenv-20.24.5-py3-none-any.whl", hash = "sha256:b80039f280f4919c77b30f1c23294ae357c4c8701042086e3fc005963e4e537b"}, - {file = "virtualenv-20.24.5.tar.gz", hash = "sha256:e8361967f6da6fbdf1426483bfe9fca8287c242ac0bc30429905721cefbff752"}, -] - -[package.dependencies] -distlib = ">=0.3.7,<1" -filelock = ">=3.12.2,<4" -platformdirs = ">=3.9.1,<4" - -[package.extras] -docs = ["furo (>=2023.7.26)", "proselint (>=0.13)", "sphinx (>=7.1.2)", "sphinx-argparse (>=0.4)", "sphinxcontrib-towncrier (>=0.2.1a0)", "towncrier (>=23.6)"] -test = ["covdefaults (>=2.3)", "coverage (>=7.2.7)", "coverage-enable-subprocess (>=1)", "flaky (>=3.7)", "packaging (>=23.1)", "pytest (>=7.4)", "pytest-env (>=0.8.2)", "pytest-freezer (>=0.4.8)", "pytest-mock (>=3.11.1)", "pytest-randomly (>=3.12)", "pytest-timeout (>=2.1)", "setuptools (>=68)", "time-machine (>=2.10)"] - -[[package]] -name = "watchdog" -version = "3.0.0" -description = "Filesystem events monitoring" -optional = false -python-versions = ">=3.7" -files = [ - {file = "watchdog-3.0.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:336adfc6f5cc4e037d52db31194f7581ff744b67382eb6021c868322e32eef41"}, - {file = "watchdog-3.0.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:a70a8dcde91be523c35b2bf96196edc5730edb347e374c7de7cd20c43ed95397"}, - {file = "watchdog-3.0.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:adfdeab2da79ea2f76f87eb42a3ab1966a5313e5a69a0213a3cc06ef692b0e96"}, - {file = "watchdog-3.0.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:2b57a1e730af3156d13b7fdddfc23dea6487fceca29fc75c5a868beed29177ae"}, - {file = "watchdog-3.0.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:7ade88d0d778b1b222adebcc0927428f883db07017618a5e684fd03b83342bd9"}, - {file = "watchdog-3.0.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:7e447d172af52ad204d19982739aa2346245cc5ba6f579d16dac4bfec226d2e7"}, - {file = "watchdog-3.0.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:9fac43a7466eb73e64a9940ac9ed6369baa39b3bf221ae23493a9ec4d0022674"}, - {file = "watchdog-3.0.0-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:8ae9cda41fa114e28faf86cb137d751a17ffd0316d1c34ccf2235e8a84365c7f"}, - {file = "watchdog-3.0.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:25f70b4aa53bd743729c7475d7ec41093a580528b100e9a8c5b5efe8899592fc"}, - {file = "watchdog-3.0.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:4f94069eb16657d2c6faada4624c39464f65c05606af50bb7902e036e3219be3"}, - {file = "watchdog-3.0.0-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:7c5f84b5194c24dd573fa6472685b2a27cc5a17fe5f7b6fd40345378ca6812e3"}, - {file = "watchdog-3.0.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:3aa7f6a12e831ddfe78cdd4f8996af9cf334fd6346531b16cec61c3b3c0d8da0"}, - {file = "watchdog-3.0.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:233b5817932685d39a7896b1090353fc8efc1ef99c9c054e46c8002561252fb8"}, - {file = "watchdog-3.0.0-pp37-pypy37_pp73-macosx_10_9_x86_64.whl", hash = "sha256:13bbbb462ee42ec3c5723e1205be8ced776f05b100e4737518c67c8325cf6100"}, - {file = "watchdog-3.0.0-pp38-pypy38_pp73-macosx_10_9_x86_64.whl", hash = "sha256:8f3ceecd20d71067c7fd4c9e832d4e22584318983cabc013dbf3f70ea95de346"}, - {file = "watchdog-3.0.0-pp39-pypy39_pp73-macosx_10_9_x86_64.whl", hash = "sha256:c9d8c8ec7efb887333cf71e328e39cffbf771d8f8f95d308ea4125bf5f90ba64"}, - {file = "watchdog-3.0.0-py3-none-manylinux2014_aarch64.whl", hash = "sha256:0e06ab8858a76e1219e68c7573dfeba9dd1c0219476c5a44d5333b01d7e1743a"}, - {file = "watchdog-3.0.0-py3-none-manylinux2014_armv7l.whl", hash = "sha256:d00e6be486affb5781468457b21a6cbe848c33ef43f9ea4a73b4882e5f188a44"}, - {file = "watchdog-3.0.0-py3-none-manylinux2014_i686.whl", hash = "sha256:c07253088265c363d1ddf4b3cdb808d59a0468ecd017770ed716991620b8f77a"}, - {file = "watchdog-3.0.0-py3-none-manylinux2014_ppc64.whl", hash = "sha256:5113334cf8cf0ac8cd45e1f8309a603291b614191c9add34d33075727a967709"}, - {file = "watchdog-3.0.0-py3-none-manylinux2014_ppc64le.whl", hash = "sha256:51f90f73b4697bac9c9a78394c3acbbd331ccd3655c11be1a15ae6fe289a8c83"}, - {file = "watchdog-3.0.0-py3-none-manylinux2014_s390x.whl", hash = "sha256:ba07e92756c97e3aca0912b5cbc4e5ad802f4557212788e72a72a47ff376950d"}, - {file = "watchdog-3.0.0-py3-none-manylinux2014_x86_64.whl", hash = "sha256:d429c2430c93b7903914e4db9a966c7f2b068dd2ebdd2fa9b9ce094c7d459f33"}, - {file = "watchdog-3.0.0-py3-none-win32.whl", hash = "sha256:3ed7c71a9dccfe838c2f0b6314ed0d9b22e77d268c67e015450a29036a81f60f"}, - {file = "watchdog-3.0.0-py3-none-win_amd64.whl", hash = "sha256:4c9956d27be0bb08fc5f30d9d0179a855436e655f046d288e2bcc11adfae893c"}, - {file = "watchdog-3.0.0-py3-none-win_ia64.whl", hash = "sha256:5d9f3a10e02d7371cd929b5d8f11e87d4bad890212ed3901f9b4d68767bee759"}, - {file = "watchdog-3.0.0.tar.gz", hash = "sha256:4d98a320595da7a7c5a18fc48cb633c2e73cda78f93cac2ef42d42bf609a33f9"}, -] - -[package.extras] -watchmedo = ["PyYAML (>=3.10)"] - -[[package]] -name = "xarray" -version = "2023.8.0" -description = "N-D labeled arrays and datasets in Python" -optional = false -python-versions = ">=3.9" -files = [ - {file = "xarray-2023.8.0-py3-none-any.whl", hash = "sha256:eb42b56aea2c7d5db2a7d0c33fb005b78eb5c4421eb747f2ced138c70b5c204e"}, - {file = "xarray-2023.8.0.tar.gz", hash = "sha256:825c6d64202a731a4e49321edd1e9dfabf4be06802f1b8c8a3c00a3ebfc8cedf"}, -] - -[package.dependencies] -numpy = ">=1.21" -packaging = ">=21.3" -pandas = ">=1.4" - -[package.extras] -accel = ["bottleneck", "flox", "numbagg", "scipy"] -complete = ["bottleneck", "cftime", "dask[complete]", "flox", "fsspec", "h5netcdf", "matplotlib", "nc-time-axis", "netCDF4", "numbagg", "pooch", "pydap", "scipy", "seaborn", "zarr"] -docs = ["bottleneck", "cftime", "dask[complete]", "flox", "fsspec", "h5netcdf", "ipykernel", "ipython", "jupyter-client", "matplotlib", "nbsphinx", "nc-time-axis", "netCDF4", "numbagg", "pooch", "pydap", "scanpydoc", "scipy", "seaborn", "sphinx-autosummary-accessors", "sphinx-rtd-theme", "zarr"] -io = ["cftime", "fsspec", "h5netcdf", "netCDF4", "pooch", "pydap", "scipy", "zarr"] -parallel = ["dask[complete]"] -viz = ["matplotlib", "nc-time-axis", "seaborn"] - -[[package]] -name = "xarray-einstats" -version = "0.6.0" -description = "Stats, linear algebra and einops for xarray" -optional = false -python-versions = ">=3.9" -files = [ - {file = "xarray_einstats-0.6.0-py3-none-any.whl", hash = "sha256:4c6f556a9d8603245545cb88583c04398b10a70c572936a2f48678330545883a"}, - {file = "xarray_einstats-0.6.0.tar.gz", hash = "sha256:ace90601505cfbe2d374762e674557ed14e1725b024823372f7ef9fd237effad"}, -] - -[package.dependencies] -numpy = ">=1.21" -scipy = ">=1.7" -xarray = ">=2022.09.0" - -[package.extras] -doc = ["furo", "jupyter-sphinx", "matplotlib", "myst-nb", "myst-parser[linkify]", "numpydoc", "sphinx (>=4)", "sphinx-copybutton", "sphinx-design", "sphinx-togglebutton", "watermark"] -einops = ["einops"] -numba = ["numba (>=0.55)"] -test = ["hypothesis", "packaging", "pytest", "pytest-cov"] - -[metadata] -lock-version = "2.0" -python-versions = "^3.10" -content-hash = "1ad690eaead0f61cf7b27e373629283d6dcb895564dae20f854f51ec517f450f" From 9d6fc970e2c19e36f7d747ff84b87177a6f76bd1 Mon Sep 17 00:00:00 2001 From: Laurenz Keller Date: Sat, 30 Sep 2023 19:22:20 +0200 Subject: [PATCH 14/45] fixed ruff errors --- src/pmhn/_trees/_simulate.py | 26 ++++++++++++++++---------- tests/trees/test_simulate.py | 9 ++++++--- warmup/subtree.py | 31 ++++++++++++++++--------------- warmup/write_csv.py | 1 - 4 files changed, 38 insertions(+), 29 deletions(-) diff --git a/src/pmhn/_trees/_simulate.py b/src/pmhn/_trees/_simulate.py index 4b6c9fa..c3f0e58 100644 --- a/src/pmhn/_trees/_simulate.py +++ b/src/pmhn/_trees/_simulate.py @@ -47,21 +47,25 @@ def generate_valid_tree( def _find_possible_mutations(old_mutations: list[int], n_mutations: int) -> list[int]: """ Args: - old_mutations: list of ancestor mutations of a given node (including the node itself) + old_mutations: list of ancestor mutations of + a given node (including the node itself) n_mutations: total number of mutations Returns: a list of possible mutations that could appear next for a given node Note: - We assume that mutations are labeled with a number between 1 and n_mutations, - so each element in old_mutations should be in that range (except for the root node = mutation 0). + We assume that mutations are labeled with a number between + 1 and n_mutations, + so each element in old_mutations should be in that range + (except for the root node = mutation 0). If this assumption is violated, an exception is raised. """ for mutation in old_mutations: if mutation > n_mutations or mutation < 0: raise ValueError( - f"Invalid mutation {mutation} in old_mutations. It should be 0 <= mutation <= {n_mutations}." + f"Invalid mutation {mutation} in old_mutations." + f" It should be 0 <= mutation <= {n_mutations}." ) possible_mutations = list( @@ -114,15 +118,17 @@ def _simulate_tree( for j in possible_mutations: new_node = Node(j, parent=node) # Here j lies in the range of 1 to n_mutations inclusive. - # However, Python uses 0-based indexing for arrays. Therefore, we subtract 1 from j when accessing - # elements in the log-theta matrix to correctly map the 1-indexed mutation to the 0-indexed matrix position. - l = theta[j - 1][j - 1] + # However, Python uses 0-based indexing for arrays. Therefore, + # we subtract 1 from j when accessing elements in the + # log-theta matrix to correctly map the 1-indexed mutation + # to the 0-indexed matrix position. + lamb = theta[j - 1][j - 1] for anc in [ ancestor for ancestor in node.path if ancestor.parent is not None ]: - l += theta[j - 1][anc.name - 1] - l = np.exp(l) - waiting_time = node_time_map[node] + rng.exponential(1.0 / l) + lamb += theta[j - 1][anc.name - 1] + lamb = np.exp(lamb) + waiting_time = node_time_map[node] + rng.exponential(1.0 / lamb) if waiting_time < sampling_time: node_time_map[new_node] = waiting_time U_next.append(new_node) diff --git a/tests/trees/test_simulate.py b/tests/trees/test_simulate.py index d1175fa..d44a1f0 100644 --- a/tests/trees/test_simulate.py +++ b/tests/trees/test_simulate.py @@ -161,7 +161,8 @@ def test_find_possible_mutations_normal(): def test_find_possible_mutations_edge(): """ - We want to test if the possible_mutations list is correct. old_mutations with mutations on the edge. + We want to test if the possible_mutations list is correct. old_mutations + with mutations on the edge. """ old_mutations = [1, 10] @@ -173,7 +174,8 @@ def test_find_possible_mutations_edge(): def test_find_possible_mutations_except_positive(): """ - We want to test if an exception is correctly thrown when the old_mutations list is invalid (mutation number too large). + We want to test if an exception is correctly thrown when the + old_mutations list is invalid (mutation number too large). """ old_mutations = [212, 1, 3, 7] n_mutations = 10 @@ -188,7 +190,8 @@ def test_find_possible_mutations_except_positive(): def test_find_possible_mutations_except_negative(): """ - We want to test if an exception is correctly thrown when the old_mutations list is invalid (mutation number too small). + We want to test if an exception is correctly thrown + when the old_mutations list is invalid (mutation number too small). """ old_mutations = [-23, 0, 5] n_mutations = 10 diff --git a/warmup/subtree.py b/warmup/subtree.py index 14ed810..ac84883 100644 --- a/warmup/subtree.py +++ b/warmup/subtree.py @@ -1,14 +1,14 @@ from anytree import Node, RenderTree from itertools import combinations, product -""" -takes a variable number of lists as input and returns a list containing all possible combination of the input lists -in this case: takes a list of lists of subtrees (for each child one list of subtrees) as input, a subtree itself is a list of nodes -outputs all combinations of subtrees -""" - def all_combinations_of_elements(*lists): + """ + takes a variable number of lists as input and returns a list containing all possible + combination of the input lists in this case: takes a list of lists of subtrees + (for each child one list of subtrees) as input, a subtree itself is a list of nodes + outputs all combinations of subtrees + """ n = len(lists) all_combinations = [] @@ -20,10 +20,10 @@ def all_combinations_of_elements(*lists): return all_combinations -"""creates a subtree given a subtree (nodes_list) and the root node""" - - def create_subtree(original_root, nodes_list): + """ + creates a subtree given a subtree (nodes_list) and the root node + """ nodes_dict = {} for node in [original_root] + list(original_root.descendants): @@ -34,13 +34,14 @@ def create_subtree(original_root, nodes_list): return nodes_dict.get(original_root) -"""returns a list of all subtrees of a tree, input is the root node -a recursive approach is used: if one knows the subtrees of the children of the root node, -then one can find all combinations of the subtrees of the children and add the root node to each one of these combinations, -this way one obtains all subtrees of the root node""" - - def subtrees(node): + """ + returns a list of all subtrees of a tree, input is the root node + a recursive approach is used: if one knows the subtrees of the + children of the root node, then one can find all combinations of + the subtrees of the children and add the root node to each one + of these combinations, this way one obtains all subtrees of the root node + """ if not node.children: return [[node]] diff --git a/warmup/write_csv.py b/warmup/write_csv.py index 168bab3..9b003de 100644 --- a/warmup/write_csv.py +++ b/warmup/write_csv.py @@ -1,6 +1,5 @@ import csv import numpy as np -from anytree import Node import pmhn._trees._simulate as _simulate From 41cd4c1cef7875cd52aeebf95043745100b54825 Mon Sep 17 00:00:00 2001 From: Laurenz Keller Date: Sat, 30 Sep 2023 19:22:20 +0200 Subject: [PATCH 15/45] fixed ruff errors --- src/pmhn/_trees/_simulate.py | 26 ++++++++++++++++---------- tests/trees/test_simulate.py | 9 ++++++--- warmup/subtree.py | 31 ++++++++++++++++--------------- warmup/write_csv.py | 1 - 4 files changed, 38 insertions(+), 29 deletions(-) diff --git a/src/pmhn/_trees/_simulate.py b/src/pmhn/_trees/_simulate.py index 4b6c9fa..c3f0e58 100644 --- a/src/pmhn/_trees/_simulate.py +++ b/src/pmhn/_trees/_simulate.py @@ -47,21 +47,25 @@ def generate_valid_tree( def _find_possible_mutations(old_mutations: list[int], n_mutations: int) -> list[int]: """ Args: - old_mutations: list of ancestor mutations of a given node (including the node itself) + old_mutations: list of ancestor mutations of + a given node (including the node itself) n_mutations: total number of mutations Returns: a list of possible mutations that could appear next for a given node Note: - We assume that mutations are labeled with a number between 1 and n_mutations, - so each element in old_mutations should be in that range (except for the root node = mutation 0). + We assume that mutations are labeled with a number between + 1 and n_mutations, + so each element in old_mutations should be in that range + (except for the root node = mutation 0). If this assumption is violated, an exception is raised. """ for mutation in old_mutations: if mutation > n_mutations or mutation < 0: raise ValueError( - f"Invalid mutation {mutation} in old_mutations. It should be 0 <= mutation <= {n_mutations}." + f"Invalid mutation {mutation} in old_mutations." + f" It should be 0 <= mutation <= {n_mutations}." ) possible_mutations = list( @@ -114,15 +118,17 @@ def _simulate_tree( for j in possible_mutations: new_node = Node(j, parent=node) # Here j lies in the range of 1 to n_mutations inclusive. - # However, Python uses 0-based indexing for arrays. Therefore, we subtract 1 from j when accessing - # elements in the log-theta matrix to correctly map the 1-indexed mutation to the 0-indexed matrix position. - l = theta[j - 1][j - 1] + # However, Python uses 0-based indexing for arrays. Therefore, + # we subtract 1 from j when accessing elements in the + # log-theta matrix to correctly map the 1-indexed mutation + # to the 0-indexed matrix position. + lamb = theta[j - 1][j - 1] for anc in [ ancestor for ancestor in node.path if ancestor.parent is not None ]: - l += theta[j - 1][anc.name - 1] - l = np.exp(l) - waiting_time = node_time_map[node] + rng.exponential(1.0 / l) + lamb += theta[j - 1][anc.name - 1] + lamb = np.exp(lamb) + waiting_time = node_time_map[node] + rng.exponential(1.0 / lamb) if waiting_time < sampling_time: node_time_map[new_node] = waiting_time U_next.append(new_node) diff --git a/tests/trees/test_simulate.py b/tests/trees/test_simulate.py index d1175fa..d44a1f0 100644 --- a/tests/trees/test_simulate.py +++ b/tests/trees/test_simulate.py @@ -161,7 +161,8 @@ def test_find_possible_mutations_normal(): def test_find_possible_mutations_edge(): """ - We want to test if the possible_mutations list is correct. old_mutations with mutations on the edge. + We want to test if the possible_mutations list is correct. old_mutations + with mutations on the edge. """ old_mutations = [1, 10] @@ -173,7 +174,8 @@ def test_find_possible_mutations_edge(): def test_find_possible_mutations_except_positive(): """ - We want to test if an exception is correctly thrown when the old_mutations list is invalid (mutation number too large). + We want to test if an exception is correctly thrown when the + old_mutations list is invalid (mutation number too large). """ old_mutations = [212, 1, 3, 7] n_mutations = 10 @@ -188,7 +190,8 @@ def test_find_possible_mutations_except_positive(): def test_find_possible_mutations_except_negative(): """ - We want to test if an exception is correctly thrown when the old_mutations list is invalid (mutation number too small). + We want to test if an exception is correctly thrown + when the old_mutations list is invalid (mutation number too small). """ old_mutations = [-23, 0, 5] n_mutations = 10 diff --git a/warmup/subtree.py b/warmup/subtree.py index 14ed810..ac84883 100644 --- a/warmup/subtree.py +++ b/warmup/subtree.py @@ -1,14 +1,14 @@ from anytree import Node, RenderTree from itertools import combinations, product -""" -takes a variable number of lists as input and returns a list containing all possible combination of the input lists -in this case: takes a list of lists of subtrees (for each child one list of subtrees) as input, a subtree itself is a list of nodes -outputs all combinations of subtrees -""" - def all_combinations_of_elements(*lists): + """ + takes a variable number of lists as input and returns a list containing all possible + combination of the input lists in this case: takes a list of lists of subtrees + (for each child one list of subtrees) as input, a subtree itself is a list of nodes + outputs all combinations of subtrees + """ n = len(lists) all_combinations = [] @@ -20,10 +20,10 @@ def all_combinations_of_elements(*lists): return all_combinations -"""creates a subtree given a subtree (nodes_list) and the root node""" - - def create_subtree(original_root, nodes_list): + """ + creates a subtree given a subtree (nodes_list) and the root node + """ nodes_dict = {} for node in [original_root] + list(original_root.descendants): @@ -34,13 +34,14 @@ def create_subtree(original_root, nodes_list): return nodes_dict.get(original_root) -"""returns a list of all subtrees of a tree, input is the root node -a recursive approach is used: if one knows the subtrees of the children of the root node, -then one can find all combinations of the subtrees of the children and add the root node to each one of these combinations, -this way one obtains all subtrees of the root node""" - - def subtrees(node): + """ + returns a list of all subtrees of a tree, input is the root node + a recursive approach is used: if one knows the subtrees of the + children of the root node, then one can find all combinations of + the subtrees of the children and add the root node to each one + of these combinations, this way one obtains all subtrees of the root node + """ if not node.children: return [[node]] diff --git a/warmup/write_csv.py b/warmup/write_csv.py index 168bab3..9b003de 100644 --- a/warmup/write_csv.py +++ b/warmup/write_csv.py @@ -1,6 +1,5 @@ import csv import numpy as np -from anytree import Node import pmhn._trees._simulate as _simulate From 05bb4255904c05eeb33dc17a6f0e7aea1d5d966b Mon Sep 17 00:00:00 2001 From: Laurenz Keller Date: Sat, 30 Sep 2023 20:27:04 +0200 Subject: [PATCH 16/45] pyright fixed --- src/pmhn/_trees/_simulate.py | 54 +++++++++++++++++------------------- 1 file changed, 25 insertions(+), 29 deletions(-) diff --git a/src/pmhn/_trees/_simulate.py b/src/pmhn/_trees/_simulate.py index c3f0e58..2a514eb 100644 --- a/src/pmhn/_trees/_simulate.py +++ b/src/pmhn/_trees/_simulate.py @@ -1,18 +1,16 @@ -from typing import Union, Sequence +from typing import Union, Sequence, Optional from anytree import Node import numpy as np -from pmhn._trees._interfaces import Tree - def generate_valid_tree( rng, theta: np.ndarray, sampling_time: float, mean_sampling_time: float, - min_tree_size: int = None, - max_tree_size: int = None, -) -> Tree: + min_tree_size: Optional[int] = None, + max_tree_size: Optional[int] = None, +) -> tuple[dict[Node, float], float]: """ Generates a single valid tree with known sampling time. @@ -75,8 +73,8 @@ def _find_possible_mutations(old_mutations: list[int], n_mutations: int) -> list def _simulate_tree( - rng, theta: np.ndarray, sampling_time: float, max_tree_size: int = None -) -> Tree: + rng, theta: np.ndarray, sampling_time: float, max_tree_size: Optional[int] = None +) -> dict[Node, float]: """Simulates a single tree with known sampling time. Args: @@ -111,7 +109,7 @@ def _simulate_tree( U_next = [] for node in U_current: path = list(node.path) - old_mutations = [node.name for node in path] + old_mutations = [node.name for node in path] # type: ignore possible_mutations = _find_possible_mutations( old_mutations=old_mutations, n_mutations=n_mutations ) @@ -126,7 +124,7 @@ def _simulate_tree( for anc in [ ancestor for ancestor in node.path if ancestor.parent is not None ]: - lamb += theta[j - 1][anc.name - 1] + lamb += theta[j - 1][anc.name - 1] # type: ignore lamb = np.exp(lamb) waiting_time = node_time_map[node] + rng.exponential(1.0 / lamb) if waiting_time < sampling_time: @@ -152,9 +150,9 @@ def simulate_trees( n_points: int, theta: np.ndarray, mean_sampling_time: Union[np.ndarray, float, Sequence[float]], - min_tree_size: int = None, - max_tree_size: int = None, -) -> tuple[np.ndarray, list[Tree]]: + min_tree_size: Optional[int] = None, + max_tree_size: Optional[int] = None, +) -> tuple[np.ndarray, list[dict[Node, float]]]: """Simulates a data set of trees with known sampling times. Args: @@ -198,21 +196,19 @@ def simulate_trees( sampling_times = rng.exponential(scale=mean_sampling_time, size=n_points) - trees, sampling_times = zip( - *[ - generate_valid_tree( - rng, - theta=th, - sampling_time=t_s, - mean_sampling_time=ms, - min_tree_size=min_tree_size, - max_tree_size=max_tree_size, - ) - for th, t_s, ms in zip(theta, sampling_times, mean_sampling_time) - ] - ) - - trees = list(trees) - sampling_times = list(sampling_times) + generated_trees_and_times = [ + generate_valid_tree( + rng, + theta=th, + sampling_time=t_s, + mean_sampling_time=ms, + min_tree_size=min_tree_size, + max_tree_size=max_tree_size, + ) + for th, t_s, ms in zip(theta, sampling_times, mean_sampling_time) + ] + + trees = [tree for tree, _ in generated_trees_and_times] + sampling_times = np.array([time for _, time in generated_trees_and_times]) return sampling_times, trees From ca76812fb9f4e0946594f096a7d9a772e8a4c3c5 Mon Sep 17 00:00:00 2001 From: Laurenz Keller Date: Thu, 5 Oct 2023 00:22:41 +0200 Subject: [PATCH 17/45] modified _backend and added _tree_utils, created unit tests for both --- src/pmhn/_trees/_backend.py | 135 ++++++++++++++-- src/pmhn/_trees/_tree_utils.py | 189 ++++++++++++++++++++++ tests/trees/test_likelihood.py | 281 +++++++++++++++++++++++++++++++++ tests/trees/test_tree_utils.py | 177 +++++++++++++++++++++ warmup/likelihood.py | 30 ++++ 5 files changed, 802 insertions(+), 10 deletions(-) create mode 100644 src/pmhn/_trees/_tree_utils.py create mode 100644 tests/trees/test_likelihood.py create mode 100644 tests/trees/test_tree_utils.py create mode 100644 warmup/likelihood.py diff --git a/src/pmhn/_trees/_backend.py b/src/pmhn/_trees/_backend.py index 16a5f66..0b4627c 100644 --- a/src/pmhn/_trees/_backend.py +++ b/src/pmhn/_trees/_backend.py @@ -2,9 +2,12 @@ import numpy as np - - +from anytree import RenderTree +from scipy.sparse.linalg import spsolve_triangular +from scipy.sparse import csr_matrix from pmhn._trees._interfaces import Tree +from pmhn._trees._tree_utils import create_all_subtrees, bfs_compare +from anytree import Node class IndividualTreeMHNBackendInterface(Protocol): @@ -57,26 +60,138 @@ def gradient_and_loglikelihood( class OriginalTreeMHNBackend(IndividualTreeMHNBackendInterface): + def create_V_Mat( + self, tree: Node, theta: np.ndarray, sampling_rate: float + ) -> np.ndarray: + """Calculates the V matrix. + + Args: + tree: a tree + theta: real-valued (i.e., log-theta) matrix, + shape (n_mutations, n_mutations) + sampling_rate: a scalar of type float + Returns: + the V matrix. + """ + + subtrees = create_all_subtrees(tree) + for subtree in subtrees: + print(RenderTree(subtree)) + subtrees_size = len(subtrees) + Q = np.zeros((subtrees_size, subtrees_size)) + for i in range(subtrees_size): + for j in range(subtrees_size): + if i == j: + Q[i][j] = self.diag_entry(subtrees[i], theta) + else: + Q[i][j] = self.off_diag_entry(subtrees[i], subtrees[j], theta) + V = np.eye(subtrees_size) * sampling_rate - Q + return V + + def diag_entry(self, tree: Node, theta: np.ndarray) -> float: + """ + Calculates a diagonal entry of the V matrix. + + Args: + tree: a tree + theta: real-valued (i.e., log-theta) matrix, + shape (n_mutations, n_mutations) + + Returns: + the diagonal entry of the V matrix corresponding to tree + """ + lamb_sum = 0 + n_mutations = len(theta) + current_nodes = [tree] + while len(current_nodes) != 0: + next_nodes = [] + for node in current_nodes: + tree_mutations = list(node.path) + list(node.children) + exit_mutations = list( + set([i + 1 for i in range(n_mutations)]).difference( + set( + [ + tree_mutation.name # type: ignore + for tree_mutation in tree_mutations + ] + ) + ) + ) + print(exit_mutations) + for mutation in exit_mutations: + lamb = 0 + exit_subclone = [ + anc.name # type: ignore + for anc in node.path + if anc.parent is not None + ] + [mutation] + print(exit_subclone) + for j in exit_subclone: + lamb += theta[mutation - 1][j - 1] + lamb = np.exp(lamb) + lamb_sum -= lamb + print(lamb) + print(node.children) + for child in node.children: + next_nodes.append(child) + current_nodes = next_nodes + return lamb_sum + + def off_diag_entry(self, tree1: Node, tree2: Node, theta: np.ndarray) -> float: + """ + Calculates an off-diagonal entry of the V matrix. + + Args: + tree1: the first tree + tree2: the second tree + theta: real-valued (i.e., log-theta) matrix, + shape (n_mutations, n_mutations) + + Returns: + the off-diagonal entry of the V matrix corresponding to tree1 and tree2 + """ + exit_node = bfs_compare(tree1, tree2) + lamb = 0 + if exit_node is None: + return lamb + else: + for j in [ + node.name # type: ignore + for node in exit_node.path + if node.parent is not None + ]: + lamb += theta[exit_node.name - 1][j - 1] + lamb = np.exp(lamb) + return float(lamb) + def loglikelihood( - self, - tree: Tree, - theta: np.ndarray, + self, tree: Node, theta: np.ndarray, sampling_rate: float ) -> float: - """Calculates loglikelihood `log P(tree | theta)`. + """ + Calculates loglikelihood `log P(tree | theta)`. Args: tree: a tree theta: real-valued (i.e., log-theta) matrix, shape (n_mutations, n_mutations) - + sampling_rate: a scalar of type float Returns: loglikelihood of the tree """ # TODO(Pawel): this is part of https://github.com/cbg-ethz/pMHN/issues/15 # It can be implemented in any way. - raise NotImplementedError - - def gradient(self, tree: Tree, theta: np.ndarray) -> np.ndarray: + V = self.create_V_Mat(tree=tree, theta=theta, sampling_rate=sampling_rate) + V_size = V.shape[0] + b = np.zeros(V_size) + b[0] = 1 + V_transposed = V.transpose() + print(V_transposed) + V_csr = csr_matrix(V_transposed) + x = spsolve_triangular(V_csr, b, lower=True) + + return np.log(x[V_size - 1]) + np.log(sampling_rate) + + def gradient(self, tree: Node, theta: np.ndarray) -> np.ndarray: """Calculates the partial derivatives of `log P(tree | theta)` with respect to `theta`. diff --git a/src/pmhn/_trees/_tree_utils.py b/src/pmhn/_trees/_tree_utils.py new file mode 100644 index 0000000..6a81c6a --- /dev/null +++ b/src/pmhn/_trees/_tree_utils.py @@ -0,0 +1,189 @@ +from anytree import Node, RenderTree, LevelOrderGroupIter +from itertools import combinations, product +from typing import Optional + + +def all_combinations_of_elements(*lists): + """ + Takes a variable number of lists as input and returns a list + containing all possible combination of the input lists. In our + use case: It takes a list of lists of subtrees (for each child + one list of subtrees) as input where a subtree itself is a list + of nodes and outputs all combinations of subtrees. + + Args: + *lists: any number of lists + + Returns: + all combinations of the input lists + """ + n = len(lists) + all_combinations = [] + + for r in range(1, n + 1): + for list_combination in combinations(lists, r): + for element_combination in product(*list_combination): + all_combinations.append(list(element_combination)) + + return all_combinations + + +def create_subtree(original_root: Node, nodes_list: list[Node]) -> Optional[Node]: + """ + Creates a subtree given a list of nodes and the root node. + + Args: + original_root: the root node + nodes_list: a list of nodes + Returns: + a subtree + """ + nodes_dict = {} + + for node in [original_root] + list(original_root.descendants): + if node in nodes_list: + parent_node = next((n for n in nodes_list if n is node.parent), None) + nodes_dict[node] = Node(node.name, parent=nodes_dict.get(parent_node)) + + return nodes_dict.get(original_root) + + +def get_subtrees(node: Node) -> list[list[Node]]: + """ + Creates a list of all subtrees of a tree. + A recursive approach is employed: If one knows the subtrees of the + children of the root node, then one can find all combinations of + the subtrees of the children and add the root node to each one + of these combinations, this way one obtains all subtrees of the original tree. + + Args: + node: the root node + Returns: + a list of subtrees + """ + if not node.children: + return [[node]] + + child_subtrees = [get_subtrees(child) for child in node.children] + + combined_subtrees = all_combinations_of_elements(*child_subtrees) + + result_subtrees = [] + result_subtrees.append([node]) + for combination in combined_subtrees: + subtree_with_root = [node] + [ + item for sublist in combination for item in sublist + ] + result_subtrees.append(subtree_with_root) + + return result_subtrees + + +def create_all_subtrees(root: Node) -> list[Node]: + """ + Creates a list of subtrees and sorts the list in ascending subtree size. + + Args: + root: the root node + Returns: + the final list of subtrees + """ + all_node_lists = get_subtrees(root) + all_node_lists = sorted(all_node_lists, key=len) + all_subtrees = [] + for subtree in all_node_lists: + all_subtrees.append(create_subtree(root, subtree)) + return all_subtrees + + +def get_lineage(node: Node) -> list[int]: + """ + Creates a list of the names of the nodes that + are in the lineage of input node. + Args: + node: a node + Returns: + the lineage of a node + """ + return [ancestor.name for ancestor in node.path] # type: ignore + + +def check_equality(tree1: Optional[Node], tree2: Optional[Node]) -> bool: + """ + Checks if tree1 and tree2 are identical, note that direct + comparison with == is not possible. + + Args: + tree1: the first tree + tree2: the second tree + Returns: + (in)equality of the trees + """ + iter1 = list(LevelOrderGroupIter(tree1)) + iter2 = list(LevelOrderGroupIter(tree2)) + if tree1 is not None and tree2 is not None: + if len(tree1.descendants) != len(tree2.descendants): + return False + for nodes1, nodes2 in zip(iter1, iter2): + set_nodes1_lineages = {tuple(get_lineage(node)) for node in nodes1} + set_nodes2_lineages = {tuple(get_lineage(node)) for node in nodes2} + additional_nodes_lineages = set_nodes2_lineages ^ set_nodes1_lineages + if len(additional_nodes_lineages) != 0: + return False + + return True + + +def bfs_compare(tree1: Node, tree2: Node) -> Optional[Node]: + """ + Checks if tree1 is a subtree of tree2 and is smaller in + size by one. + + Args: + tree1: the first tree + tree2: the second tree + Returns: + the additional node in the second tree if available, otherwise None. + """ + diff_count = 0 + iter1 = list(LevelOrderGroupIter(tree1)) + iter2 = list(LevelOrderGroupIter(tree2)) + exit_node = None + if len(list(tree2.descendants)) - len(list(tree1.descendants)) != 1: + return None + for nodes1, nodes2 in zip(iter1, iter2): + set_nodes1_lineages = {tuple(get_lineage(node)) for node in nodes1} + set_nodes2_lineages = {tuple(get_lineage(node)) for node in nodes2} + additional_nodes_lineages = set_nodes2_lineages ^ set_nodes1_lineages + diff_count += len(additional_nodes_lineages) + + if diff_count == 1 and exit_node is None: + additional_node_lineage = additional_nodes_lineages.pop() + for node in nodes1: + if tuple(get_lineage(node)) == additional_node_lineage: + return None + + for node in nodes2: + if tuple(get_lineage(node)) == additional_node_lineage: + exit_node = node + if diff_count > 1: + return None + if len(iter1) < len(iter2): + return iter2[-1][0] + return exit_node + + +if __name__ == "__main__": + A = Node("0") + B = Node("1", parent=A) + C = Node("3", parent=A) + D = Node("3", parent=B) + + print(RenderTree(A)) + print("\n") + all_subtrees = create_all_subtrees(A) + i = 1 + for subtree in all_subtrees: + print(f"{i}. ") + print(RenderTree(subtree)) + i += 1 diff --git a/tests/trees/test_likelihood.py b/tests/trees/test_likelihood.py new file mode 100644 index 0000000..36a11f2 --- /dev/null +++ b/tests/trees/test_likelihood.py @@ -0,0 +1,281 @@ +from pmhn._trees._backend import OriginalTreeMHNBackend +from pmhn._trees._tree_utils import create_all_subtrees +from anytree import Node +import numpy as np + + +def test_create_V_Mat(): + """ + Checks if create_V_Mat is implemented correctly. + """ + + true_Q = np.array( + [ + [ + -(np.exp(-1.41) + np.exp(-2.26) + np.exp(-2.55)), + np.exp(-1.41), + np.exp(-2.55), + 0, + 0, + 0, + ], + [ + 0, + -( + np.exp(-2.26) + + np.exp(-2.55) + + np.exp(-1.12 - 2.26) + + np.exp(1 - 2.55) + ), + 0, + np.exp(1 - 2.55), + np.exp(-2.55), + 0, + ], + [ + 0, + 0, + -( + np.exp(-1.41) + + np.exp(-2.26) + + np.exp(-1.41 + 3) + + np.exp(-2.26 + 2) + ), + 0, + np.exp(-1.41), + 0, + ], + [ + 0, + 0, + 0, + -( + np.exp(-2.26) + + np.exp(-2.55) + + np.exp(-2.26 - 1.12) + + np.exp(-2.26 - 1.12 + 2) + ), + 0, + np.exp(-2.55), + ], + [ + 0, + 0, + 0, + 0, + -( + np.exp(-2.26) + + np.exp(-2.26 - 1.12) + + np.exp(-2.55 + 1) + + np.exp(-1.41 + 3) + + np.exp(-2.26 + 2) + ), + np.exp(-2.55 + 1), + ], + [ + 0, + 0, + 0, + 0, + 0, + -( + np.exp(-2.26) + + np.exp(-2.26 - 1.12) + + np.exp(-2.26 + 2 - 1.12) + + np.exp(-1.41 + 3) + + np.exp(-2.26 + 2) + ), + ], + ] + ) + A = Node(0) + B = Node(1, parent=A) + Node(3, parent=A) + Node(3, parent=B) + subtrees = create_all_subtrees(A) + subtrees_size = len(subtrees) + sampling_rate = 1.0 + true_V = np.eye(subtrees_size) * sampling_rate - true_Q + backend = OriginalTreeMHNBackend() + theta = np.array([[-1.41, 2, 3], [-1.12, -2.26, 2], [1, -0.86, -2.55]]) + + V = backend.create_V_Mat(A, theta, sampling_rate) + + assert np.allclose(V, true_V, atol=1e-8) + + +def test_diag_entry(): + r""" + + Checks if the diagonal values of the V matrix are calculated correctly. + + 0 + / | \ + 2 1 3 + | | + 3 3 + + + augmented tree: + + + 0 + / | \ + 2 1 3 + |\ |\ |\ + 3 1 3 2 1 2 + | | + 1 2 + + """ + A = Node(0) + B = Node(2, parent=A) + D = Node(1, parent=A) + Node(3, parent=A) + Node(3, parent=B) + Node(3, parent=D) + theta = np.array([[-1.41, 2, 3], [-1.12, -2.26, 2], [1, -0.86, -2.55]]) + true_diag_entry = -( + np.exp(-1.41 + 2) + + np.exp(-2.26 - 1.12) + + np.exp(-1.41 + 3) + + np.exp(-2.26 + 2) + + np.exp(-1.41 + 2 + 3) + + np.exp(-2.26 + 2 - 1.12) + ) + backend = OriginalTreeMHNBackend() + diag_entry = backend.diag_entry(A, theta) + + assert np.allclose(diag_entry, true_diag_entry, atol=1e-8) + + +def test_off_diag_entry_valid(): + r""" + Checks if the off-diagonal entries of the V matrix + are calculated correctly. + + first tree: + 0 + / | \ + 2 1 3 + | + 3 + + second tree: + 0 + / | \ + 2 1 3 + | | + 3 3 + + + """ + + theta = np.array([[-1.41, 2, 3], [-1.12, -2.26, 2], [1, -0.86, -2.55]]) + # first tree + A_1 = Node(0) + B_1 = Node(2, parent=A_1) + Node(1, parent=A_1) + Node(3, parent=A_1) + Node(3, parent=B_1) + + # second tree + A_2 = Node(0) + B_2 = Node(2, parent=A_2) + D_2 = Node(1, parent=A_2) + Node(3, parent=A_2) + Node(3, parent=B_2) + Node(3, parent=D_2) + + true_off_diag_entry = np.exp(-2.55 + 1) + backend = OriginalTreeMHNBackend() + off_diag_entry = backend.off_diag_entry(A_1, A_2, theta) + + assert np.allclose(off_diag_entry, true_off_diag_entry, atol=1e-8) + + +def test_off_diag_entry_invalid_size(): + r""" + + Checks if off_diag_entry successfully returns 0 when the size is invalid + (i.e the first tree is not smaller than the second tree by one). + + first tree: + 0 + / | \ + 2 1 3 + | + 3 + + second tree: + 0 + / | \ + 2 1 3 + | | | + 3 3 2 + + + """ + + # first tree + A_1 = Node(0) + B_1 = Node(2, parent=A_1) + Node(1, parent=A_1) + Node(3, parent=A_1) + Node(3, parent=B_1) + + # second tree + A_2 = Node(0) + B_2 = Node(2, parent=A_2) + C_2 = Node(1, parent=A_2) + D_2 = Node(3, parent=A_2) + Node(3, parent=B_2) + Node(3, parent=C_2) + Node(2, parent=D_2) + + theta = np.array([[-1.41, 2, 3], [-1.12, -2.26, 2], [1, -0.86, -2.55]]) + backend = OriginalTreeMHNBackend() + + assert backend.off_diag_entry(A_1, A_2, theta) == 0 + + +def test_off_diag_entry_not_subset(): + r""" + Checks if the off_diag_entry succesfully returns 0 when the + first tree is not a subtree of the second tree. + + first tree: + 0 + / | \ + 2 1 3 + | + 3 + + second tree: + 0 + / | \ + 2 1 3 + | | + 3 2 + + + """ + # first tree + A_1 = Node(0) + B_1 = Node(2, parent=A_1) + Node(1, parent=A_1) + Node(3, parent=A_1) + Node(3, parent=B_1) + + # second tree + A_2 = Node(0) + Node(2, parent=A_2) + C_2 = Node(1, parent=A_2) + D_2 = Node(3, parent=A_2) + Node(3, parent=C_2) + Node(2, parent=D_2) + + theta = np.array([[-1.41, 2, 3], [-1.12, -2.26, 2], [1, -0.86, -2.55]]) + backend = OriginalTreeMHNBackend() + + assert backend.off_diag_entry(A_1, A_2, theta) == 0 diff --git a/tests/trees/test_tree_utils.py b/tests/trees/test_tree_utils.py new file mode 100644 index 0000000..d973a95 --- /dev/null +++ b/tests/trees/test_tree_utils.py @@ -0,0 +1,177 @@ +from pmhn._trees._tree_utils import create_all_subtrees, check_equality, bfs_compare +from anytree import Node + + +def test_create_all_subtrees_simple(): + """ + Checks for a simple tree if all subtrees are found. + """ + A = Node(name=0) + Node(name=1, parent=A) + + subtrees = create_all_subtrees(A) + + A_test_1 = Node(name=0) + Node(name=1, parent=A_test_1) + + A_test_2 = Node(name=0) + + true_subtrees = [A_test_1, A_test_2] + + assert len(true_subtrees) == len(subtrees) + + for tree in true_subtrees: + assert ( + len( + [ + subtree + for subtree in subtrees + if check_equality(subtree, tree) is True + ] + ) + == 1 + ) + + +def test_create_all_subtrees_medium(): + """ + Checks for a tree if all subtrees are found using create_all_subtrees. + """ + A = Node(0) + B = Node(1, parent=A) + Node(3, parent=A) + Node(3, parent=B) + subtrees = create_all_subtrees(A) + + A_test_1 = Node(0) + Node(1, parent=A_test_1) + + A_test_2 = Node(0) + + A_test_3 = Node(0) + Node(3, parent=A_test_3) + + A_test_4 = Node(0) + B_test_4 = Node(1, parent=A_test_4) + Node(3, parent=B_test_4) + + A_test_5 = Node(0) + Node(1, parent=A_test_5) + Node(3, parent=A_test_5) + + A_test_6 = Node(0) + B_test_6 = Node(1, parent=A_test_6) + Node(3, parent=A_test_6) + Node(3, parent=B_test_6) + + true_subtrees = [A_test_1, A_test_2, A_test_3, A_test_4, A_test_5, A_test_6] + assert len(true_subtrees) == len(subtrees) + + for tree in true_subtrees: + assert ( + len( + [ + subtree + for subtree in subtrees + if check_equality(subtree, tree) is True + ] + ) + == 1 + ) + + +def test_bfs_compare_long(): + r""" + Checks for a tree if it is a subtree of another tree + and is smaller in size by one using bfs_compare. + + first tree: + 0 + | + 1 + | + 2 + | + 3 + | + 4 + + second tree: + + 0 + | + 1 + | + 2 + | + 3 + | + 4 + | + 5 + """ + # first tree + A = Node(0) + B = Node(1, parent=A) + C = Node(2, parent=B) + D = Node(3, parent=C) + Node(4, parent=D) + + # second tree + A_ = Node(0) + B_ = Node(1, parent=A_) + C_ = Node(2, parent=B_) + D_ = Node(3, parent=C_) + E_ = Node(4, parent=D_) + F_ = Node(5, parent=E_) + + assert check_equality(bfs_compare(A, A_), F_) + + +def test_bfs_compare_complex(): + r""" + + Checks for a tree if it is a subtree of another tree + and is smaller in size by one using bfs_compare. + + first tree: + 0 + / | \ + 2 1 3 + | | | + 3 2 1 + | | + 3 2 + + second tree: + 0 + / | \ + 2 1 3 + | |\ | + 3 2 3 1 + | | + 3 2 + """ + + A = Node(0) + B = Node(2, parent=A) + C = Node(1, parent=A) + D = Node(3, parent=A) + Node(3, parent=B) + F = Node(2, parent=C) + G = Node(1, parent=D) + Node(3, parent=F) + Node(2, parent=G) + + A_ = Node(0) + B_ = Node(2, parent=A_) + C_ = Node(1, parent=A_) + D_ = Node(3, parent=A_) + Node(3, parent=B_) + F_ = Node(2, parent=C_) + G_ = Node(1, parent=D_) + Node(3, parent=F_) + Node(2, parent=G_) + J_ = Node(3, parent=C_) + + assert check_equality(bfs_compare(A, A_), J_) diff --git a/warmup/likelihood.py b/warmup/likelihood.py new file mode 100644 index 0000000..4bc8bd5 --- /dev/null +++ b/warmup/likelihood.py @@ -0,0 +1,30 @@ +from pmhn._trees._backend import OriginalTreeMHNBackend +from pmhn._trees._tree_utils import create_all_subtrees, bfs_compare +from anytree import Node, RenderTree +import csv +import numpy as np + + +def csv_to_numpy(file_path): + with open(file_path, "r") as file: + reader = csv.reader(file) + next(reader) + data_list = list(reader) + return np.array(data_list, dtype=float) + + +A = Node(0) +B = Node(1, parent=A) +C = Node(3, parent=A) +D = Node(3, parent=B) + +mhn_file_path = "/home/laukeller/BSc Thesis/TreeMHN/Example/MHN_Matrix.csv" +mhn_array = csv_to_numpy(mhn_file_path) +print(mhn_array) +subtrees = create_all_subtrees(A) +node = bfs_compare(subtrees[0], subtrees[1]) +print(RenderTree(node)) +backend = OriginalTreeMHNBackend() + +loglikelihood = backend.loglikelihood(A, mhn_array, 1.0) +print(loglikelihood) From d6cd28696168aea6369376b03cd58b2b9838a010 Mon Sep 17 00:00:00 2001 From: Laurenz Keller Date: Sat, 7 Oct 2023 21:36:29 +0200 Subject: [PATCH 18/45] added files for likelihood comparison, changed absolute paths to relative paths, created modified_io, added 1e-10 to return value of likelihood function --- src/pmhn/_trees/_backend.py | 10 +- src/pmhn/_trees/_io_modified.py | 98 +++++++++++++++++++ warmup/children.py | 15 --- warmup/likelihood.py | 30 ------ .../R_py_loglikelihood_comparison.py | 72 ++++++++++++++ warmup/plot_mutation_freq.py | 44 --------- warmup/plot_trees.py | 22 ----- warmup/subtree.py | 83 ---------------- .../plot_all_mut_freq.py | 16 +-- .../{ => tree_generation}/plot_all_trees.py | 17 ++-- warmup/{ => tree_generation}/write_csv.py | 5 +- 11 files changed, 189 insertions(+), 223 deletions(-) create mode 100644 src/pmhn/_trees/_io_modified.py delete mode 100644 warmup/children.py delete mode 100644 warmup/likelihood.py create mode 100644 warmup/likelihood/R_py_loglikelihood_comparison.py delete mode 100644 warmup/plot_mutation_freq.py delete mode 100644 warmup/plot_trees.py delete mode 100644 warmup/subtree.py rename warmup/{ => tree_generation}/plot_all_mut_freq.py (69%) rename warmup/{ => tree_generation}/plot_all_trees.py (63%) rename warmup/{ => tree_generation}/write_csv.py (87%) diff --git a/src/pmhn/_trees/_backend.py b/src/pmhn/_trees/_backend.py index 0b4627c..a71a042 100644 --- a/src/pmhn/_trees/_backend.py +++ b/src/pmhn/_trees/_backend.py @@ -2,7 +2,6 @@ import numpy as np -from anytree import RenderTree from scipy.sparse.linalg import spsolve_triangular from scipy.sparse import csr_matrix from pmhn._trees._interfaces import Tree @@ -75,8 +74,6 @@ def create_V_Mat( """ subtrees = create_all_subtrees(tree) - for subtree in subtrees: - print(RenderTree(subtree)) subtrees_size = len(subtrees) Q = np.zeros((subtrees_size, subtrees_size)) for i in range(subtrees_size): @@ -117,7 +114,6 @@ def diag_entry(self, tree: Node, theta: np.ndarray) -> float: ) ) ) - print(exit_mutations) for mutation in exit_mutations: lamb = 0 exit_subclone = [ @@ -125,13 +121,10 @@ def diag_entry(self, tree: Node, theta: np.ndarray) -> float: for anc in node.path if anc.parent is not None ] + [mutation] - print(exit_subclone) for j in exit_subclone: lamb += theta[mutation - 1][j - 1] lamb = np.exp(lamb) lamb_sum -= lamb - print(lamb) - print(node.children) for child in node.children: next_nodes.append(child) current_nodes = next_nodes @@ -185,11 +178,10 @@ def loglikelihood( b = np.zeros(V_size) b[0] = 1 V_transposed = V.transpose() - print(V_transposed) V_csr = csr_matrix(V_transposed) x = spsolve_triangular(V_csr, b, lower=True) - return np.log(x[V_size - 1]) + np.log(sampling_rate) + return np.log(x[V_size - 1] + 1e-10) + np.log(sampling_rate) def gradient(self, tree: Node, theta: np.ndarray) -> np.ndarray: """Calculates the partial derivatives of `log P(tree | theta)` diff --git a/src/pmhn/_trees/_io_modified.py b/src/pmhn/_trees/_io_modified.py new file mode 100644 index 0000000..8b9b0dd --- /dev/null +++ b/src/pmhn/_trees/_io_modified.py @@ -0,0 +1,98 @@ +"""Utilities for parsing data frames into AnyTree trees.""" +import dataclasses +from typing import Any + +import anytree +import pandas as pd + + +@dataclasses.dataclass +class TreeNaming: + """Naming conventions used to parse a tree. + + Attrs: + node: column name with node id/name + parent: column name with the parent's id + data: a dictionary mapping column names to field names in nodes + + Example: + TreeNaming( + node="Node_ID", + parent="Parent_ID", + data={ + "Mutation_ID": "mutation", + "SomeValue": "value", + } + means that a data frame with columns + "Node_ID", "Parent_ID", "Mutation_ID", "SomeValue" + is expected. + + Created nodes will have additional fields + "mutation" and "value". + """ + + node: str = "Node_ID" + parent: str = "Parent_ID" + mutation: str = "Mutation_ID" + + +@dataclasses.dataclass +class ForestNaming: + """Naming conventions used to parse a forest (a set of trees). + + Attrs: + tree_name: column name storing the tree id/name + naming: TreeNaming object used to parse each tree + """ + + tree_name: str = "Tree_ID" + naming: TreeNaming = dataclasses.field(default_factory=TreeNaming) + + +def parse_tree(df: pd.DataFrame, naming: TreeNaming) -> anytree.Node: + root = None + nodes = {} # Maps a NodeID value to Node + + for _, row in df.iterrows(): + node_id = row[naming.node] + parent_id = row[naming.parent] + node_name = row[naming.mutation] + + # We found the root + if node_id == parent_id: + if root is not None: + raise ValueError( + f"Root is {root}, but {node_id} == {parent_id} " + "also looks like a root." + ) + root = anytree.Node(name=node_name, parent=None) + nodes[node_id] = root + else: + parent_node = nodes[parent_id] + nodes[node_id] = anytree.Node(name=node_name, parent=parent_node) + + if root is None: + raise ValueError("No root found.") + return root + + +def parse_forest(df: pd.DataFrame, naming: ForestNaming) -> dict[Any, anytree.Node]: + """Parses a data frame with a forest (a set of trees). + + Args: + df: data frame with columns specified as in `naming` + naming: specifies the naming conventions + + Returns: + dictionary with keys being the tree names + (read from the column `naming.tree_name`) + and values being the root nodes + + See also: + parse_tree, which powers this function + """ + result = {} + for tree_name, tree_df in df.groupby(naming.tree_name): + result[tree_name] = parse_tree(df=tree_df, naming=naming.naming) + + return result diff --git a/warmup/children.py b/warmup/children.py deleted file mode 100644 index d22c176..0000000 --- a/warmup/children.py +++ /dev/null @@ -1,15 +0,0 @@ -from anytree import Node, RenderTree, PreOrderIter - -A = Node("A") -B = Node("B", parent=A) -C = Node("C", parent=A) -D = Node("D", parent=B) -E = Node("E", parent=C) -F = Node("F", parent=C) - -all_nodes = list(PreOrderIter(A)) -print(RenderTree(A)) -children = {} -for node in all_nodes: - children[node.name] = [child.name for child in node.children] -print(children) diff --git a/warmup/likelihood.py b/warmup/likelihood.py deleted file mode 100644 index 4bc8bd5..0000000 --- a/warmup/likelihood.py +++ /dev/null @@ -1,30 +0,0 @@ -from pmhn._trees._backend import OriginalTreeMHNBackend -from pmhn._trees._tree_utils import create_all_subtrees, bfs_compare -from anytree import Node, RenderTree -import csv -import numpy as np - - -def csv_to_numpy(file_path): - with open(file_path, "r") as file: - reader = csv.reader(file) - next(reader) - data_list = list(reader) - return np.array(data_list, dtype=float) - - -A = Node(0) -B = Node(1, parent=A) -C = Node(3, parent=A) -D = Node(3, parent=B) - -mhn_file_path = "/home/laukeller/BSc Thesis/TreeMHN/Example/MHN_Matrix.csv" -mhn_array = csv_to_numpy(mhn_file_path) -print(mhn_array) -subtrees = create_all_subtrees(A) -node = bfs_compare(subtrees[0], subtrees[1]) -print(RenderTree(node)) -backend = OriginalTreeMHNBackend() - -loglikelihood = backend.loglikelihood(A, mhn_array, 1.0) -print(loglikelihood) diff --git a/warmup/likelihood/R_py_loglikelihood_comparison.py b/warmup/likelihood/R_py_loglikelihood_comparison.py new file mode 100644 index 0000000..bcfbcb9 --- /dev/null +++ b/warmup/likelihood/R_py_loglikelihood_comparison.py @@ -0,0 +1,72 @@ +import pandas as pd +import pmhn._trees._io_modified as io +from pmhn._trees._backend import OriginalTreeMHNBackend +import csv +import numpy as np + + +def csv_to_numpy(file_path): + with open(file_path, "r") as file: + reader = csv.reader(file) + next(reader) + data_list = list(reader) + return np.array(data_list, dtype=float) + + +# AML trees +df_AML = pd.read_csv("likelihood_R/trees_AML_R.csv") + +# randomly generated 500 trees using a random theta +df_500 = pd.read_csv("likelihood_R/trees_500_R.csv") + +# theta matrices +theta_AML = csv_to_numpy("likelihood_R/MHN_Matrix_AML.csv") +theta_500 = csv_to_numpy("likelihood_R/MHN_Matrix_500.csv") +# loglikelihoods in R +log_vec_R_AML = np.genfromtxt("likelihood_R/log_vec_R_AML.csv", delimiter=",") +log_vec_R_500 = np.genfromtxt("likelihood_R/log_vec_R_500.csv", delimiter=",") + +# define sampling rate +sampling_rate = 1.0 + +# use modified io +naming = io.ForestNaming( + tree_name="Tree_ID", + naming=io.TreeNaming(node="Node_ID", parent="Parent_ID", mutation="Mutation_ID"), +) + +# parse trees +trees_AML = io.parse_forest(df_AML, naming=naming) +trees_500 = io.parse_forest(df_500, naming=naming) + +# calculate loglikelihoods +log_vec_py_AML = np.empty(len(trees_AML)) +log_vec_py_500 = np.empty(len(trees_500)) +backend = OriginalTreeMHNBackend() + +for idx, tree in trees_AML.items(): + print(f"Processing tree {idx + 1} of {len(trees_AML)}") + log_value = backend.loglikelihood(tree, theta_AML, sampling_rate) + log_vec_py_AML[idx - 1] = log_value + print(f"log_value: {log_value}") + +for idx, tree in trees_500.items(): + print(f"Processing tree {idx + 1} of {len(trees_500)}") + log_value = backend.loglikelihood(tree, theta_500, sampling_rate) + log_vec_py_500[idx - 1] = log_value + print(f"log_value: {log_value}") + +# write Python loglikelihoods to CSV +np.savetxt("likelihood_py/log_vec_py_AML.csv", log_vec_py_AML, delimiter=",") +np.savetxt("likelihood_py/log_vec_py_500.csv", log_vec_py_500, delimiter=",") + + +# check if the loglikelihood vectors are the same +if np.allclose(log_vec_py_AML, log_vec_R_AML, 1e-10): + print("The loglikelihoods of the AML trees are the same in R and Python.") + +if np.allclose(log_vec_py_500, log_vec_R_500, 1e-10): + print( + "The loglikelihoods of the 500 randomly generated" + "trees are the same in R and Python." + ) diff --git a/warmup/plot_mutation_freq.py b/warmup/plot_mutation_freq.py deleted file mode 100644 index 52962b0..0000000 --- a/warmup/plot_mutation_freq.py +++ /dev/null @@ -1,44 +0,0 @@ -import pandas as pd -import matplotlib.pyplot as plt - -r_trees_path = "/home/laukeller/BSc Thesis/TreeMHN/Example/trees_10000.csv" -python_trees_path = "/home/laukeller/BSc Thesis/pMHN/src/pmhn/_trees/trees_10000.csv" - -r_trees = pd.read_csv(r_trees_path) -python_trees = pd.read_csv(python_trees_path) - -r_mutation_frequencies = r_trees["Mutation_ID"].value_counts().sort_index() -python_mutation_frequencies = python_trees["Mutation_ID"].value_counts().sort_index() - -fig, ax = plt.subplots(figsize=(10, 6)) - -bar_width = 0.35 - -r_mutation_frequencies.plot( - kind="bar", - width=bar_width, - position=0, - align="center", - color="b", - alpha=0.5, - label="R Trees", - ax=ax, -) -python_mutation_frequencies.plot( - kind="bar", - width=bar_width, - position=1, - align="center", - color="r", - alpha=0.5, - label="Python Trees", - ax=ax, -) - -ax.set_xlabel("Mutation ID") -ax.set_ylabel("Frequency") -ax.set_title("Mutation Frequencies Comparison") -ax.legend() - -plt.tight_layout() -plt.show() diff --git a/warmup/plot_trees.py b/warmup/plot_trees.py deleted file mode 100644 index d1400e9..0000000 --- a/warmup/plot_trees.py +++ /dev/null @@ -1,22 +0,0 @@ -import pandas as pd -import matplotlib.pyplot as plt - -r_trees_path = "/home/laukeller/BSc Thesis/TreeMHN/Example/trees_10000.csv" -python_trees_path = "/home/laukeller/BSc Thesis/pMHN/src/pmhn/_trees/trees_10000.csv" - -r_trees = pd.read_csv(r_trees_path) -python_trees = pd.read_csv(python_trees_path) - -r_tree_sizes = r_trees.groupby("Tree_ID").size() -python_tree_sizes = python_trees.groupby("Tree_ID").size() - -bin_edges = [x - 0.5 for x in range(2, 12)] + [11.5] -plt.hist(r_tree_sizes, bins=bin_edges, alpha=0.5, edgecolor="k", label="R Trees") -plt.hist( - python_tree_sizes, bins=bin_edges, alpha=0.5, edgecolor="k", label="Python Trees" -) -plt.xlabel("Tree Size") -plt.ylabel("Frequency") -plt.legend(loc="upper right") -plt.title("Tree Size Distribution") -plt.show() diff --git a/warmup/subtree.py b/warmup/subtree.py deleted file mode 100644 index ac84883..0000000 --- a/warmup/subtree.py +++ /dev/null @@ -1,83 +0,0 @@ -from anytree import Node, RenderTree -from itertools import combinations, product - - -def all_combinations_of_elements(*lists): - """ - takes a variable number of lists as input and returns a list containing all possible - combination of the input lists in this case: takes a list of lists of subtrees - (for each child one list of subtrees) as input, a subtree itself is a list of nodes - outputs all combinations of subtrees - """ - n = len(lists) - all_combinations = [] - - for r in range(1, n + 1): - for list_combination in combinations(lists, r): - for element_combination in product(*list_combination): - all_combinations.append(list(element_combination)) - - return all_combinations - - -def create_subtree(original_root, nodes_list): - """ - creates a subtree given a subtree (nodes_list) and the root node - """ - nodes_dict = {} - - for node in [original_root] + list(original_root.descendants): - if node in nodes_list: - parent_node = next((n for n in nodes_list if n is node.parent), None) - nodes_dict[node] = Node(node.name, parent=nodes_dict.get(parent_node)) - - return nodes_dict.get(original_root) - - -def subtrees(node): - """ - returns a list of all subtrees of a tree, input is the root node - a recursive approach is used: if one knows the subtrees of the - children of the root node, then one can find all combinations of - the subtrees of the children and add the root node to each one - of these combinations, this way one obtains all subtrees of the root node - """ - if not node.children: - return [[node]] - - child_subtrees = [subtrees(child) for child in node.children] - - combined_subtrees = all_combinations_of_elements(*child_subtrees) - - result_subtrees = [] - result_subtrees.append([node]) - for combination in combined_subtrees: - subtree_with_root = [node] + [ - item for sublist in combination for item in sublist - ] - result_subtrees.append(subtree_with_root) - - return result_subtrees - - -A = Node("0") -B = Node("1", parent=A) -C = Node("3", parent=A) -D = Node("3", parent=B) - -print(RenderTree(A)) -print("\n") -all_node_lists = subtrees(A) -all_node_lists = sorted(all_node_lists, key=len) -print(all_node_lists) -print("\n") -all_subtrees = [] - -for nodes_list in all_node_lists: - subtree = create_subtree(A, nodes_list) - all_subtrees.append(subtree) -i = 1 -for subtree in all_subtrees: - print(f"{i}. ") - print(RenderTree(subtree)) - i += 1 diff --git a/warmup/plot_all_mut_freq.py b/warmup/tree_generation/plot_all_mut_freq.py similarity index 69% rename from warmup/plot_all_mut_freq.py rename to warmup/tree_generation/plot_all_mut_freq.py index 15b05fb..7c27105 100644 --- a/warmup/plot_all_mut_freq.py +++ b/warmup/tree_generation/plot_all_mut_freq.py @@ -3,20 +3,20 @@ paths = { 500: ( - "/home/laukeller/BSc Thesis/TreeMHN/Example/trees_500.csv", - "/home/laukeller/BSc Thesis/pMHN/warmup/trees_500.csv", + "trees_R_data/trees_500.csv", + "trees_py_data/trees_500.csv", ), 5000: ( - "/home/laukeller/BSc Thesis/TreeMHN/Example/trees_5000.csv", - "/home/laukeller/BSc Thesis/pMHN/warmup/trees_5000.csv", + "trees_R_data/trees_5000.csv", + "trees_py_data/trees_5000.csv", ), 10000: ( - "/home/laukeller/BSc Thesis/TreeMHN/Example/trees_10000.csv", - "/home/laukeller/BSc Thesis/pMHN/warmup/trees_10000.csv", + "trees_R_data/trees_10000.csv", + "trees_py_data/trees_10000.csv", ), 50000: ( - "/home/laukeller/BSc Thesis/TreeMHN/Example/trees_50000.csv", - "/home/laukeller/BSc Thesis/pMHN/warmup/trees_50000.csv", + "trees_R_data/trees_50000.csv", + "trees_py_data/trees_50000.csv", ), } diff --git a/warmup/plot_all_trees.py b/warmup/tree_generation/plot_all_trees.py similarity index 63% rename from warmup/plot_all_trees.py rename to warmup/tree_generation/plot_all_trees.py index 1120bb5..b5f3eda 100644 --- a/warmup/plot_all_trees.py +++ b/warmup/tree_generation/plot_all_trees.py @@ -4,24 +4,23 @@ paths = { 500: ( - "/home/laukeller/BSc Thesis/TreeMHN/Example/trees_500.csv", - "/home/laukeller/BSc Thesis/pMHN/warmup/trees_500.csv", + "trees_R_data/trees_500.csv", + "trees_py_data/trees_500.csv", ), 5000: ( - "/home/laukeller/BSc Thesis/TreeMHN/Example/trees_5000.csv", - "/home/laukeller/BSc Thesis/pMHN/warmup/trees_5000.csv", + "trees_R_data/trees_5000.csv", + "trees_py_data/trees_5000.csv", ), 10000: ( - "/home/laukeller/BSc Thesis/TreeMHN/Example/trees_10000.csv", - "/home/laukeller/BSc Thesis/pMHN/warmup/trees_10000.csv", + "trees_R_data/trees_10000.csv", + "trees_py_data/trees_10000.csv", ), 50000: ( - "/home/laukeller/BSc Thesis/TreeMHN/Example/trees_50000.csv", - "/home/laukeller/BSc Thesis/pMHN/warmup/trees_50000.csv", + "trees_R_data/trees_50000.csv", + "trees_py_data/trees_50000.csv", ), } - fig, axs = plt.subplots(2, 2, figsize=(10, 10)) # 2x2 grid of subplots axs = axs.ravel() diff --git a/warmup/write_csv.py b/warmup/tree_generation/write_csv.py similarity index 87% rename from warmup/write_csv.py rename to warmup/tree_generation/write_csv.py index 9b003de..f1fa542 100644 --- a/warmup/write_csv.py +++ b/warmup/tree_generation/write_csv.py @@ -31,8 +31,7 @@ def write_trees_to_csv(trees, output_file_path): if __name__ == "__main__": - mhn_file_path = "/home/laukeller/BSc Thesis/TreeMHN/Example/MHN_Matrix.csv" - mhn_array = csv_to_numpy(mhn_file_path) + mhn_array = csv_to_numpy("MHN_Matrix.csv") print(mhn_array) rng = np.random.default_rng() @@ -44,7 +43,7 @@ def write_trees_to_csv(trees, output_file_path): min_tree_size = 2 max_tree_size = 11 for n_points in tree_counts: - trees_file_path = f"/home/laukeller/BSc Thesis/pMHN/warmup/trees_{n_points}.csv" + trees_file_path = f"trees_py_data/trees_{n_points}.csv" _, trees = _simulate.simulate_trees( rng, From 2da22a59fffd3cff275d25313b538532f5c5b26e Mon Sep 17 00:00:00 2001 From: Laurenz Keller Date: Sat, 7 Oct 2023 21:37:25 +0200 Subject: [PATCH 19/45] small change --- warmup/likelihood/R_py_loglikelihood_comparison.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/warmup/likelihood/R_py_loglikelihood_comparison.py b/warmup/likelihood/R_py_loglikelihood_comparison.py index bcfbcb9..fb044a4 100644 --- a/warmup/likelihood/R_py_loglikelihood_comparison.py +++ b/warmup/likelihood/R_py_loglikelihood_comparison.py @@ -68,5 +68,5 @@ def csv_to_numpy(file_path): if np.allclose(log_vec_py_500, log_vec_R_500, 1e-10): print( "The loglikelihoods of the 500 randomly generated" - "trees are the same in R and Python." + " trees are the same in R and Python." ) From 6fedbdc60bee10e8a7cb28c559fc2ea2a9fd6644 Mon Sep 17 00:00:00 2001 From: Laurenz Keller Date: Mon, 9 Oct 2023 01:27:39 +0200 Subject: [PATCH 20/45] added likelihood tests, modified test_tree_utils.py --- tests/trees/test_likelihood.py | 138 ++++++++++++++++++ tests/trees/test_tree_utils.py | 15 +- .../R_py_loglikelihood_comparison.py | 8 +- 3 files changed, 150 insertions(+), 11 deletions(-) diff --git a/tests/trees/test_likelihood.py b/tests/trees/test_likelihood.py index 36a11f2..5c553de 100644 --- a/tests/trees/test_likelihood.py +++ b/tests/trees/test_likelihood.py @@ -7,6 +7,13 @@ def test_create_V_Mat(): """ Checks if create_V_Mat is implemented correctly. + + tree: + 0 + / \ + 1 3 + / + 3 """ true_Q = np.array( @@ -279,3 +286,134 @@ def test_off_diag_entry_not_subset(): backend = OriginalTreeMHNBackend() assert backend.off_diag_entry(A_1, A_2, theta) == 0 + + +def test_likelihood_small_tree(): + """ + Checks if the likelihood of a small tree is calculated + correctly. + + tree: + 0 + | + 2 + | + 7 + """ + + A = Node(0) + B = Node(7, parent=A) + Node(2, parent=B) + theta = np.array( + [ + [-1.41, 0.00, 0.00, 4.91, 1.03, 0.00, -1.91, -0.74, -1.35, 1.48], + [-1.12, -2.26, 0.00, 0.82, 0.00, 0.00, 1.16, 0.00, -1.62, 0.00], + [0.00, -0.86, -2.55, 1.58, 0.00, 0.00, 1.02, -2.70, 0.00, 0.68], + [0.00, 0.00, 0.00, -3.69, 0.00, 0.00, -0.95, 1.42, 0.00, -1.01], + [-3.08, -1.42, -3.14, 0.00, -3.95, 3.90, -1.46, -2.00, 0.00, 2.87], + [-2.24, 0.00, 0.00, 0.00, 0.00, -2.38, -2.13, 1.50, 0.00, 1.35], + [0.00, 0.00, 0.00, 0.00, 1.52, 0.00, -1.79, 0.00, 0.00, 0.00], + [1.69, 0.76, 0.00, 1.29, 1.73, -0.82, -1.38, -4.65, 0.92, 0.00], + [-1.22, 0.00, 0.00, 0.00, 0.65, -1.14, 0.00, 0.00, -3.25, 0.00], + [0.97, 1.75, 0.00, -3.66, -1.28, 0.00, 1.66, 0.00, 0.00, -3.03], + ] + ) + sampling_rate = 1.0 + + backend = OriginalTreeMHNBackend() + log_value = backend.loglikelihood(A, theta, sampling_rate) + + assert np.allclose(log_value, -5.793104, atol=1e-5) + + +def test_likelihood_medium_tree(): + """ + Checks if the likelihood of a small tree is calculated + correctly. + + tree: + 0 + / \ + 2 3 + | | + 1 10 + | + 10 + """ + + A = Node(0) + B = Node(2, parent=A) + C = Node(3, parent=A) + D = Node(1, parent=B) + Node(10, parent=C) + Node(10, parent=D) + theta = np.array( + [ + [-1.41, 0.00, 0.00, 4.91, 1.03, 0.00, -1.91, -0.74, -1.35, 1.48], + [-1.12, -2.26, 0.00, 0.82, 0.00, 0.00, 1.16, 0.00, -1.62, 0.00], + [0.00, -0.86, -2.55, 1.58, 0.00, 0.00, 1.02, -2.70, 0.00, 0.68], + [0.00, 0.00, 0.00, -3.69, 0.00, 0.00, -0.95, 1.42, 0.00, -1.01], + [-3.08, -1.42, -3.14, 0.00, -3.95, 3.90, -1.46, -2.00, 0.00, 2.87], + [-2.24, 0.00, 0.00, 0.00, 0.00, -2.38, -2.13, 1.50, 0.00, 1.35], + [0.00, 0.00, 0.00, 0.00, 1.52, 0.00, -1.79, 0.00, 0.00, 0.00], + [1.69, 0.76, 0.00, 1.29, 1.73, -0.82, -1.38, -4.65, 0.92, 0.00], + [-1.22, 0.00, 0.00, 0.00, 0.65, -1.14, 0.00, 0.00, -3.25, 0.00], + [0.97, 1.75, 0.00, -3.66, -1.28, 0.00, 1.66, 0.00, 0.00, -3.03], + ] + ) + sampling_rate = 1.0 + + backend = OriginalTreeMHNBackend() + log_value = backend.loglikelihood(A, theta, sampling_rate) + + assert np.allclose(log_value, -14.729560, atol=1e-5) + + +def test_likelihood_large_tree(): + """ + Checks if the likelihood of a small tree is calculated + correctly. + + tree: + 0 + / \ + 3 6 + / \ + 4 5 + | / \ + 1 1 7 + | / \ + 7 3 10 + """ + + A = Node(0) + Node(3, parent=A) + C = Node(6, parent=A) + D = Node(4, parent=C) + E = Node(5, parent=C) + Node(1, parent=D) + G = Node(1, parent=E) + H = Node(7, parent=E) + Node(7, parent=G) + Node(3, parent=H) + Node(10, parent=H) + theta = np.array( + [ + [-1.41, 0.00, 0.00, 4.91, 1.03, 0.00, -1.91, -0.74, -1.35, 1.48], + [-1.12, -2.26, 0.00, 0.82, 0.00, 0.00, 1.16, 0.00, -1.62, 0.00], + [0.00, -0.86, -2.55, 1.58, 0.00, 0.00, 1.02, -2.70, 0.00, 0.68], + [0.00, 0.00, 0.00, -3.69, 0.00, 0.00, -0.95, 1.42, 0.00, -1.01], + [-3.08, -1.42, -3.14, 0.00, -3.95, 3.90, -1.46, -2.00, 0.00, 2.87], + [-2.24, 0.00, 0.00, 0.00, 0.00, -2.38, -2.13, 1.50, 0.00, 1.35], + [0.00, 0.00, 0.00, 0.00, 1.52, 0.00, -1.79, 0.00, 0.00, 0.00], + [1.69, 0.76, 0.00, 1.29, 1.73, -0.82, -1.38, -4.65, 0.92, 0.00], + [-1.22, 0.00, 0.00, 0.00, 0.65, -1.14, 0.00, 0.00, -3.25, 0.00], + [0.97, 1.75, 0.00, -3.66, -1.28, 0.00, 1.66, 0.00, 0.00, -3.03], + ] + ) + sampling_rate = 1.0 + + backend = OriginalTreeMHNBackend() + log_value = backend.loglikelihood(A, theta, sampling_rate) + + assert np.allclose(log_value, -22.288420, atol=1e-5) diff --git a/tests/trees/test_tree_utils.py b/tests/trees/test_tree_utils.py index d973a95..ee8116c 100644 --- a/tests/trees/test_tree_utils.py +++ b/tests/trees/test_tree_utils.py @@ -4,7 +4,7 @@ def test_create_all_subtrees_simple(): """ - Checks for a simple tree if all subtrees are found. + Checks for a simple tree if all subtrees are found using create_all_subtrees. """ A = Node(name=0) Node(name=1, parent=A) @@ -82,8 +82,8 @@ def test_create_all_subtrees_medium(): def test_bfs_compare_long(): r""" - Checks for a tree if it is a subtree of another tree - and is smaller in size by one using bfs_compare. + Tests if bfs_compare successfully returns the additional node + if a tree is a subtree of another tree and is smaller in size by one. first tree: 0 @@ -125,14 +125,15 @@ def test_bfs_compare_long(): E_ = Node(4, parent=D_) F_ = Node(5, parent=E_) - assert check_equality(bfs_compare(A, A_), F_) + assert bfs_compare(A, A_) == F_ def test_bfs_compare_complex(): r""" - Checks for a tree if it is a subtree of another tree - and is smaller in size by one using bfs_compare. + Tests if bfs_compare successfully returns the additional node + if a tree is a subtree of another tree and is smaller in size by one. + first tree: 0 @@ -174,4 +175,4 @@ def test_bfs_compare_complex(): Node(2, parent=G_) J_ = Node(3, parent=C_) - assert check_equality(bfs_compare(A, A_), J_) + assert bfs_compare(A, A_) == J_ diff --git a/warmup/likelihood/R_py_loglikelihood_comparison.py b/warmup/likelihood/R_py_loglikelihood_comparison.py index fb044a4..089a1d6 100644 --- a/warmup/likelihood/R_py_loglikelihood_comparison.py +++ b/warmup/likelihood/R_py_loglikelihood_comparison.py @@ -45,13 +45,13 @@ def csv_to_numpy(file_path): backend = OriginalTreeMHNBackend() for idx, tree in trees_AML.items(): - print(f"Processing tree {idx + 1} of {len(trees_AML)}") + print(f"Processing tree {idx} of {len(trees_AML)}") log_value = backend.loglikelihood(tree, theta_AML, sampling_rate) log_vec_py_AML[idx - 1] = log_value print(f"log_value: {log_value}") for idx, tree in trees_500.items(): - print(f"Processing tree {idx + 1} of {len(trees_500)}") + print(f"Processing tree {idx} of {len(trees_500)}") log_value = backend.loglikelihood(tree, theta_500, sampling_rate) log_vec_py_500[idx - 1] = log_value print(f"log_value: {log_value}") @@ -62,10 +62,10 @@ def csv_to_numpy(file_path): # check if the loglikelihood vectors are the same -if np.allclose(log_vec_py_AML, log_vec_R_AML, 1e-10): +if np.allclose(log_vec_py_AML, log_vec_R_AML, atol=1e-10): print("The loglikelihoods of the AML trees are the same in R and Python.") -if np.allclose(log_vec_py_500, log_vec_R_500, 1e-10): +if np.allclose(log_vec_py_500, log_vec_R_500, atol=1e-10): print( "The loglikelihoods of the 500 randomly generated" " trees are the same in R and Python." From 01beb2f6dd547d970c0a61c468f1935b0ca3f195 Mon Sep 17 00:00:00 2001 From: Laurenz Keller Date: Mon, 9 Oct 2023 11:53:23 +0200 Subject: [PATCH 21/45] minor changes --- src/pmhn/_trees/_io_modified.py | 25 +++++++++++++++---- .../R_py_loglikelihood_comparison.py | 8 +++++- 2 files changed, 27 insertions(+), 6 deletions(-) diff --git a/src/pmhn/_trees/_io_modified.py b/src/pmhn/_trees/_io_modified.py index 8b9b0dd..8594c75 100644 --- a/src/pmhn/_trees/_io_modified.py +++ b/src/pmhn/_trees/_io_modified.py @@ -33,7 +33,9 @@ class TreeNaming: node: str = "Node_ID" parent: str = "Parent_ID" - mutation: str = "Mutation_ID" + data: dict[str, str] = dataclasses.field( + default_factory=lambda: {"Mutation_ID": "mutation"} + ) @dataclasses.dataclass @@ -50,13 +52,25 @@ class ForestNaming: def parse_tree(df: pd.DataFrame, naming: TreeNaming) -> anytree.Node: + """Parses a data frame into a tree + + Args: + df: data frame with columns specified in `naming`. + naming: specifies the columns that should be present in `df` + + Returns: + the root node of the tree + """ root = None nodes = {} # Maps a NodeID value to Node for _, row in df.iterrows(): node_id = row[naming.node] parent_id = row[naming.parent] - node_name = row[naming.mutation] + values = {val: row[key] for key, val in naming.data.items()} + + if node_id in nodes: + raise ValueError(f"Node {node_id} already exists.") # We found the root if node_id == parent_id: @@ -65,11 +79,12 @@ def parse_tree(df: pd.DataFrame, naming: TreeNaming) -> anytree.Node: f"Root is {root}, but {node_id} == {parent_id} " "also looks like a root." ) - root = anytree.Node(name=node_name, parent=None) + root = anytree.Node(values["mutation"], parent=None, **values) nodes[node_id] = root else: - parent_node = nodes[parent_id] - nodes[node_id] = anytree.Node(name=node_name, parent=parent_node) + nodes[node_id] = anytree.Node( + values["mutation"], parent=nodes[parent_id], **values + ) if root is None: raise ValueError("No root found.") diff --git a/warmup/likelihood/R_py_loglikelihood_comparison.py b/warmup/likelihood/R_py_loglikelihood_comparison.py index 089a1d6..e882b73 100644 --- a/warmup/likelihood/R_py_loglikelihood_comparison.py +++ b/warmup/likelihood/R_py_loglikelihood_comparison.py @@ -32,7 +32,13 @@ def csv_to_numpy(file_path): # use modified io naming = io.ForestNaming( tree_name="Tree_ID", - naming=io.TreeNaming(node="Node_ID", parent="Parent_ID", mutation="Mutation_ID"), + naming=io.TreeNaming( + node="Node_ID", + parent="Parent_ID", + data={ + "Mutation_ID": "mutation", + }, + ), ) # parse trees From f1cdb6b013ee91522ce163e779c9cc6f58a815a8 Mon Sep 17 00:00:00 2001 From: Laurenz Keller Date: Mon, 9 Oct 2023 11:54:32 +0200 Subject: [PATCH 22/45] minor change --- tests/trees/test_likelihood.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/trees/test_likelihood.py b/tests/trees/test_likelihood.py index 5c553de..5f98ac4 100644 --- a/tests/trees/test_likelihood.py +++ b/tests/trees/test_likelihood.py @@ -328,7 +328,7 @@ def test_likelihood_small_tree(): def test_likelihood_medium_tree(): """ - Checks if the likelihood of a small tree is calculated + Checks if the likelihood of a medium-sized tree is calculated correctly. tree: @@ -371,7 +371,7 @@ def test_likelihood_medium_tree(): def test_likelihood_large_tree(): """ - Checks if the likelihood of a small tree is calculated + Checks if the likelihood of a large tree is calculated correctly. tree: From 52eea92f3adf71d4cba739f04eacab7622319fda Mon Sep 17 00:00:00 2001 From: Laurenz Keller Date: Thu, 12 Oct 2023 20:19:40 +0200 Subject: [PATCH 23/45] implemented 2 new versions of the likelihood calculation which relies on _tree_utils_new.py --- src/pmhn/_trees/_backend_new_v1.py | 212 ++++++++++++++++++ src/pmhn/_trees/_backend_new_v2.py | 196 ++++++++++++++++ src/pmhn/_trees/_tree_utils_new.py | 201 +++++++++++++++++ .../R_py_loglikelihood_comparison_new_v1.py | 80 +++++++ .../R_py_loglikelihood_comparison_new_v2.py | 80 +++++++ 5 files changed, 769 insertions(+) create mode 100644 src/pmhn/_trees/_backend_new_v1.py create mode 100644 src/pmhn/_trees/_backend_new_v2.py create mode 100644 src/pmhn/_trees/_tree_utils_new.py create mode 100644 warmup/likelihood/R_py_loglikelihood_comparison_new_v1.py create mode 100644 warmup/likelihood/R_py_loglikelihood_comparison_new_v2.py diff --git a/src/pmhn/_trees/_backend_new_v1.py b/src/pmhn/_trees/_backend_new_v1.py new file mode 100644 index 0000000..bec6d1c --- /dev/null +++ b/src/pmhn/_trees/_backend_new_v1.py @@ -0,0 +1,212 @@ +from typing import Protocol + + +import numpy as np +from scipy.sparse.linalg import spsolve_triangular +from scipy.sparse import csr_matrix +from pmhn._trees._interfaces import Tree +from pmhn._trees._tree_utils_new import create_all_subtrees, bfs_compare +from anytree import Node, LevelOrderGroupIter + + +class LoglikelihoodSingleTree: + def __init__(self, tree): + self._subtrees_dict = create_all_subtrees(tree) + + _subtrees_dict: dict[Node, int] + + +class IndividualTreeMHNBackendInterface(Protocol): + def loglikelihood( + self, + tree: Tree, + theta: np.ndarray, + ) -> float: + """Calculates loglikelihood `log P(tree | theta)`. + + Args: + tree: a tree + theta: real-valued (i.e., log-theta) matrix, + shape (n_mutations, n_mutations) + + Returns: + loglikelihood of the tree + """ + raise NotImplementedError + + def gradient( + self, + tree: Tree, + theta: np.ndarray, + ) -> np.ndarray: + """Calculates the partial derivatives of `log P(tree | theta)` + with respect to `theta`. + + Args: + tree: a tree + theta: real-valued matrix, + shape (n_mutations, n_mutatations) + + Returns: + gradient `d log P(tree | theta) / d theta`, + shape (n_mutations, n_mutations) + """ + raise NotImplementedError + + def gradient_and_loglikelihood( + self, tree: Tree, theta: np.ndarray + ) -> tuple[np.ndarray, float]: + """Returns the gradient and the loglikelihood. + + Note: + This function may be faster than calling `gradient` and `loglikelihood` + separately. + """ + return self.gradient(tree, theta), self.loglikelihood(tree, theta) + + +class OriginalTreeMHNBackend(IndividualTreeMHNBackendInterface): + def create_V_Mat( + self, + tree: LoglikelihoodSingleTree, + theta: np.ndarray, + sampling_rate: float, + all_mut: set[int], + ) -> np.ndarray: + """Calculates the V matrix. + + Args: + tree: a tree + theta: real-valued (i.e., log-theta) matrix, + shape (n_mutations, n_mutations) + sampling_rate: a scalar of type float + all_mut: set containing all possible mutations + Returns: + the V matrix. + """ + + subtrees_size = len(tree._subtrees_dict) + Q = np.zeros((subtrees_size, subtrees_size)) + for i, (subtree_i, subtree_size_i) in enumerate(tree._subtrees_dict.items()): + for j, (subtree_j, subtree_size_j) in enumerate( + tree._subtrees_dict.items() + ): + if i == j: + Q[i][j] = self.diag_entry(subtree_i, theta, all_mut) + elif subtree_size_j - subtree_size_i == 1: + Q[i][j] = self.off_diag_entry(subtree_i, subtree_j, theta) + V = np.eye(subtrees_size) * sampling_rate - Q + return V + + def diag_entry(self, tree: Node, theta: np.ndarray, all_mut: set[int]) -> float: + """ + Calculates a diagonal entry of the V matrix. + + Args: + tree: a tree + theta: real-valued (i.e., log-theta) matrix, + shape (n_mutations, n_mutations) + all_mut: set containing all possible mutations + Returns: + the diagonal entry of the V matrix corresponding to tree + """ + lamb_sum = 0 + + for level in LevelOrderGroupIter(tree): + for node in level: + tree_mutations = {n.name for n in node.path}.union( + {c.name for c in node.children} + ) + exit_mutations = set(all_mut).difference(tree_mutations) + + for mutation in exit_mutations: + lamb = 0 + exit_subclone = { + anc.name for anc in node.path if anc.parent is not None + }.union({mutation}) + for j in exit_subclone: + lamb += theta[mutation - 1][j - 1] + lamb = np.exp(lamb) + lamb_sum -= lamb + + return lamb_sum + + def off_diag_entry(self, tree1: Node, tree2: Node, theta: np.ndarray) -> float: + """ + Calculates an off-diagonal entry of the V matrix. + + Args: + tree1: the first tree + tree2: the second tree + theta: real-valued (i.e., log-theta) matrix, + shape (n_mutations, n_mutations) + Returns: + the off-diagonal entry of the V matrix corresponding to tree1 and tree2 + """ + exit_node = bfs_compare(tree1, tree2) + lamb = 0 + if exit_node is None: + return lamb + else: + for j in [ + node.name # type: ignore + for node in exit_node.path + if node.parent is not None + ]: + lamb += theta[exit_node.name - 1][j - 1] + lamb = np.exp(lamb) + return float(lamb) + + def loglikelihood( + self, tree: LoglikelihoodSingleTree, theta: np.ndarray, sampling_rate: float + ) -> float: + """ + Calculates loglikelihood `log P(tree | theta)`. + + Args: + tree: a tree + theta: real-valued (i.e., log-theta) matrix, + shape (n_mutations, n_mutations) + sampling_rate: a scalar of type float + Returns: + loglikelihood of the tree + """ + # TODO(Pawel): this is part of https://github.com/cbg-ethz/pMHN/issues/15 + # It can be implemented in any way. + n_mutations = len(theta) + all_mut = set(i + 1 for i in range(n_mutations)) + V = self.create_V_Mat( + tree=tree, theta=theta, sampling_rate=sampling_rate, all_mut=all_mut + ) + V_size = V.shape[0] + b = np.zeros(V_size) + b[0] = 1 + V_transposed = V.transpose() + V_csr = csr_matrix(V_transposed) + x = spsolve_triangular(V_csr, b, lower=True) + + return np.log(x[V_size - 1] + 1e-10) + np.log(sampling_rate) + + def gradient(self, tree: Node, theta: np.ndarray) -> np.ndarray: + """Calculates the partial derivatives of `log P(tree | theta)` + with respect to `theta`. + + Args: + tree: a tree + theta: real-valued matrix, shape (n_mutations, n_mutatations) + + Returns: + gradient `d log P(tree | theta) / d theta`, + shape (n_mutations, n_mutations) + """ + # TODO(Pawel): This is part of + # https://github.com/cbg-ethz/pMHN/issues/18, + # but it is *not* a priority. + # We will try to do the modelling as soon as possible, + # starting with a sequential Monte Carlo sampler + # and Metropolis transitions. + # Only after initial experiments + # (we will probably see that it's not scalable), + # we'll consider switching to Hamiltonian Monte Carlo, + # which requires gradients. + raise NotImplementedError diff --git a/src/pmhn/_trees/_backend_new_v2.py b/src/pmhn/_trees/_backend_new_v2.py new file mode 100644 index 0000000..d21f719 --- /dev/null +++ b/src/pmhn/_trees/_backend_new_v2.py @@ -0,0 +1,196 @@ +from typing import Protocol + + +import numpy as np +from pmhn._trees._interfaces import Tree +from pmhn._trees._tree_utils_new import create_all_subtrees, bfs_compare +from anytree import Node + + +class LoglikelihoodSingleTree: + def __init__(self, tree: Node): + self._subtrees_dict = create_all_subtrees(tree) + + _subtrees_dict: dict[Node, int] + + +class IndividualTreeMHNBackendInterface(Protocol): + def loglikelihood( + self, + tree: Tree, + theta: np.ndarray, + ) -> float: + """Calculates loglikelihood `log P(tree | theta)`. + + Args: + tree: a tree + theta: real-valued (i.e., log-theta) matrix, + shape (n_mutations, n_mutations) + + Returns: + loglikelihood of the tree + """ + raise NotImplementedError + + def gradient( + self, + tree: Tree, + theta: np.ndarray, + ) -> np.ndarray: + """Calculates the partial derivatives of `log P(tree | theta)` + with respect to `theta`. + + Args: + tree: a tree + theta: real-valued matrix, + shape (n_mutations, n_mutatations) + + Returns: + gradient `d log P(tree | theta) / d theta`, + shape (n_mutations, n_mutations) + """ + raise NotImplementedError + + def gradient_and_loglikelihood( + self, tree: Tree, theta: np.ndarray + ) -> tuple[np.ndarray, float]: + """Returns the gradient and the loglikelihood. + + Note: + This function may be faster than calling `gradient` and `loglikelihood` + separately. + """ + return self.gradient(tree, theta), self.loglikelihood(tree, theta) + + +class OriginalTreeMHNBackend(IndividualTreeMHNBackendInterface): + def diag_entry(self, tree: Node, theta: np.ndarray, all_mut: set[int]) -> float: + """ + Calculates a diagonal entry of the V matrix. + + Args: + tree: a tree + theta: real-valued (i.e., log-theta) matrix, + shape (n_mutations, n_mutations) + all_mut: set containing all possible mutations + Returns: + the diagonal entry of the V matrix corresponding to tree + """ + lamb_sum = 0 + current_nodes = [tree] + while len(current_nodes) != 0: + next_nodes = [] + for node in current_nodes: + tree_mutations = list(node.path) + list(node.children) + exit_mutations = list( + set(all_mut).difference( + set( + [ + tree_mutation.name # type: ignore + for tree_mutation in tree_mutations + ] + ) + ) + ) + for mutation in exit_mutations: + lamb = 0 + exit_subclone = [ + anc.name # type: ignore + for anc in node.path + if anc.parent is not None + ] + [mutation] + for j in exit_subclone: + lamb += theta[mutation - 1][j - 1] + lamb = np.exp(lamb) + lamb_sum -= lamb + for child in node.children: + next_nodes.append(child) + current_nodes = next_nodes + return lamb_sum + + def off_diag_entry(self, tree1: Node, tree2: Node, theta: np.ndarray) -> float: + """ + Calculates an off-diagonal entry of the V matrix. + + Args: + tree1: the first tree + tree2: the second tree + theta: real-valued (i.e., log-theta) matrix, + shape (n_mutations, n_mutations) + Returns: + the off-diagonal entry of the V matrix corresponding to tree1 and tree2 + """ + exit_node = bfs_compare(tree1, tree2) + lamb = 0 + if exit_node is None: + return lamb + else: + for j in [ + node.name # type: ignore + for node in exit_node.path + if node.parent is not None + ]: + lamb += theta[exit_node.name - 1][j - 1] + lamb = np.exp(lamb) + return float(lamb) + + def loglikelihood( + self, tree: LoglikelihoodSingleTree, theta: np.ndarray, sampling_rate: float + ) -> float: + """ + Calculates loglikelihood `log P(tree | theta)`. + + Args: + tree: a tree + theta: real-valued (i.e., log-theta) matrix, + shape (n_mutations, n_mutations) + sampling_rate: a scalar of type float + Returns: + loglikelihood of the tree + """ + # TODO(Pawel): this is part of https://github.com/cbg-ethz/pMHN/issues/15 + # It can be implemented in any way. + subtrees_size = len(tree._subtrees_dict) + x = np.zeros(subtrees_size) + x[0] = 1 + n_mutations = len(theta) + all_mut = set(i + 1 for i in range(n_mutations)) + for i, (subtree_i, subtree_size_i) in enumerate(tree._subtrees_dict.items()): + V_col = {} + V_diag = 0.0 + for j, (subtree_j, subtree_size_j) in enumerate( + tree._subtrees_dict.items() + ): + if i == j: + V_diag = sampling_rate - self.diag_entry(subtree_i, theta, all_mut) + elif subtree_size_i - subtree_size_j == 1: + V_col[j] = -self.off_diag_entry(subtree_j, subtree_i, theta) + for index, val in V_col.items(): + x[i] -= val * x[index] + x[i] /= V_diag + + return np.log(x[-1] + 1e-10) + np.log(sampling_rate) + + def gradient(self, tree: Node, theta: np.ndarray) -> np.ndarray: + """Calculates the partial derivatives of `log P(tree | theta)` + with respect to `theta`. + + Args: + tree: a tree + theta: real-valued matrix, shape (n_mutations, n_mutatations) + + Returns: + gradient `d log P(tree | theta) / d theta`, + shape (n_mutations, n_mutations) + """ + # TODO(Pawel): This is part of + # https://github.com/cbg-ethz/pMHN/issues/18, + # but it is *not* a priority. + # We will try to do the modelling as soon as possible, + # starting with a sequential Monte Carlo sampler + # and Metropolis transitions. + # Only after initial experiments + # (we will probably see that it's not scalable), + # we'll consider switching to Hamiltonian Monte Carlo, + # which requires gradients. + raise NotImplementedError diff --git a/src/pmhn/_trees/_tree_utils_new.py b/src/pmhn/_trees/_tree_utils_new.py new file mode 100644 index 0000000..936fc08 --- /dev/null +++ b/src/pmhn/_trees/_tree_utils_new.py @@ -0,0 +1,201 @@ +from anytree import Node, RenderTree, LevelOrderGroupIter +from itertools import combinations, product +from typing import Optional + + +def all_combinations_of_elements(*lists): + """ + Takes a variable number of lists as input and returns a list + containing all possible combination of the input lists. In our + use case: It takes a list of lists of subtrees (for each child + one list of subtrees) as input where a subtree itself is a list + of nodes and outputs all combinations of subtrees. + + Args: + *lists: any number of lists + + Returns: + all combinations of the input lists + """ + n = len(lists) + all_combinations = [] + + for r in range(1, n + 1): + for list_combination in combinations(lists, r): + for element_combination in product(*list_combination): + all_combinations.append(list(element_combination)) + + return all_combinations + + +def create_subtree(original_root: Node, nodes_list: list[Node]) -> Node: + """ + Creates a subtree given a list of nodes and the root node. + + Args: + original_root: the root node + nodes_list: a list of nodes + Returns: + a subtree + """ + nodes_dict = {} + + for node in [original_root] + list(original_root.descendants): + if node in nodes_list: + parent_node = next((n for n in nodes_list if n is node.parent), None) + nodes_dict[node] = Node(node.name, parent=nodes_dict.get(parent_node)) + return nodes_dict[original_root] + + +def get_subtrees(node: Node) -> list[list[Node]]: + """ + Creates a list of all subtrees of a tree. + A recursive approach is employed: If one knows the subtrees of the + children of the root node, then one can find all combinations of + the subtrees of the children and add the root node to each one + of these combinations, this way one obtains all subtrees of the original tree. + + Args: + node: the root node + Returns: + a list of subtrees + """ + if not node.children: + return [[node]] + + child_subtrees = [get_subtrees(child) for child in node.children] + + combined_subtrees = all_combinations_of_elements(*child_subtrees) + + result_subtrees = [] + result_subtrees.append([node]) + for combination in combined_subtrees: + subtree_with_root = [node] + [ + item for sublist in combination for item in sublist + ] + result_subtrees.append(subtree_with_root) + + return result_subtrees + + +def create_all_subtrees(root: Node) -> dict[Node, int]: + """ + Creates a dictionary where each key is a subtree, + and each value is the size of that subtree. + + Args: + root: the root node + Returns: + A dictionary mapping subtrees to their sizes + """ + all_node_lists = get_subtrees(root) + all_node_lists = sorted(all_node_lists, key=len) + all_subtrees_dict = { + create_subtree(root, node_list): len(node_list) for node_list in all_node_lists + } + return all_subtrees_dict + + +def get_lineage(node: Node) -> tuple[int]: + """ + Creates a tuple of the names of the nodes that + are in the lineage of the input node. + Args: + node: a node + Returns: + the lineage of a node + """ + return tuple(ancestor.name for ancestor in node.path) # type: ignore + + +def check_equality(tree1: Optional[Node], tree2: Optional[Node]) -> bool: + """ + Checks if tree1 and tree2 are identical, note that direct + comparison with == is not possible. + + Args: + tree1: the first tree + tree2: the second tree + Returns: + (in)equality of the trees + """ + iter1 = list(LevelOrderGroupIter(tree1)) + iter2 = list(LevelOrderGroupIter(tree2)) + if tree1 is not None and tree2 is not None: + if len(tree1.descendants) != len(tree2.descendants): + return False + for nodes1, nodes2 in zip(iter1, iter2): + set_nodes1_lineages = {tuple(get_lineage(node)) for node in nodes1} + set_nodes2_lineages = {tuple(get_lineage(node)) for node in nodes2} + additional_nodes_lineages = set_nodes2_lineages ^ set_nodes1_lineages + if len(additional_nodes_lineages) != 0: + return False + + return True + + +def bfs_compare(tree1: Node, tree2: Node) -> Optional[Node]: + """ + Checks if tree1 is a subtree of tree2 with the assumption + that tree2 is larger than the first tree by one. + + Args: + tree1: the first tree + tree2: the second tree + Returns: + the additional node in the second tree if available, otherwise None. + + """ + + diff_count = 0 + iter1 = list(LevelOrderGroupIter(tree1)) + iter2 = list(LevelOrderGroupIter(tree2)) + exit_node = None + + dict_nodes1_lineages = { + node: get_lineage(node) for nodes1 in iter1 for node in nodes1 + } + dict_nodes2_lineages = { + node: get_lineage(node) for nodes2 in iter2 for node in nodes2 + } + + for nodes1, nodes2 in zip(iter1, iter2): + set_nodes1_lineages = {dict_nodes1_lineages[node] for node in nodes1} + set_nodes2_lineages = {dict_nodes2_lineages[node] for node in nodes2} + additional_nodes_lineages = set_nodes2_lineages ^ set_nodes1_lineages + diff_count += len(additional_nodes_lineages) + + if diff_count == 1 and exit_node is None: + additional_node_lineage = additional_nodes_lineages.pop() + + for node in nodes1: + if dict_nodes1_lineages[node] == additional_node_lineage: + return None + + for node in nodes2: + if dict_nodes2_lineages[node] == additional_node_lineage: + exit_node = node + break + if diff_count > 1: + return None + + if diff_count == 0: + return iter2[-1][0] + + return exit_node + + +if __name__ == "__main__": + A = Node("0") + B = Node("1", parent=A) + C = Node("3", parent=A) + D = Node("3", parent=B) + + print(RenderTree(A)) + print("\n") + all_subtrees = create_all_subtrees(A) + i = 1 + for subtree in all_subtrees: + print(f"{i}. ") + print(RenderTree(subtree)) + i += 1 diff --git a/warmup/likelihood/R_py_loglikelihood_comparison_new_v1.py b/warmup/likelihood/R_py_loglikelihood_comparison_new_v1.py new file mode 100644 index 0000000..a6ca25a --- /dev/null +++ b/warmup/likelihood/R_py_loglikelihood_comparison_new_v1.py @@ -0,0 +1,80 @@ +import pandas as pd +import pmhn._trees._io_modified as io +from pmhn._trees._backend_new_v1 import OriginalTreeMHNBackend, LoglikelihoodSingleTree +import csv +import numpy as np + + +def csv_to_numpy(file_path): + with open(file_path, "r") as file: + reader = csv.reader(file) + next(reader) + data_list = list(reader) + return np.array(data_list, dtype=float) + + +# AML trees +df_AML = pd.read_csv("likelihood_R/trees_AML_R.csv") + +# randomly generated 500 trees using a random theta +df_500 = pd.read_csv("likelihood_R/trees_500_R.csv") + +# theta matrices +theta_AML = csv_to_numpy("likelihood_R/MHN_Matrix_AML.csv") +theta_500 = csv_to_numpy("likelihood_R/MHN_Matrix_500.csv") +# loglikelihoods in R +log_vec_R_AML = np.genfromtxt("likelihood_R/log_vec_R_AML.csv", delimiter=",") +log_vec_R_500 = np.genfromtxt("likelihood_R/log_vec_R_500.csv", delimiter=",") + +# define sampling rate +sampling_rate = 1.0 + +# use modified io +naming = io.ForestNaming( + tree_name="Tree_ID", + naming=io.TreeNaming( + node="Node_ID", + parent="Parent_ID", + data={ + "Mutation_ID": "mutation", + }, + ), +) + +# parse trees +trees_AML = io.parse_forest(df_AML, naming=naming) +trees_500 = io.parse_forest(df_500, naming=naming) + +# calculate loglikelihoods +log_vec_py_AML = np.empty(len(trees_AML)) +log_vec_py_500 = np.empty(len(trees_500)) +backend = OriginalTreeMHNBackend() + +for idx, tree in trees_AML.items(): + print(f"Processing tree {idx} of {len(trees_AML)}") + tree_log = LoglikelihoodSingleTree(tree) + log_value = backend.loglikelihood(tree_log, theta_AML, sampling_rate) + log_vec_py_AML[idx - 1] = log_value + print(f"log_value: {log_value}") + +for idx, tree in trees_500.items(): + print(f"Processing tree {idx} of {len(trees_500)}") + tree_log = LoglikelihoodSingleTree(tree) + log_value = backend.loglikelihood(tree_log, theta_500, sampling_rate) + log_vec_py_500[idx - 1] = log_value + print(f"log_value: {log_value}") + +# write Python loglikelihoods to CSV +np.savetxt("likelihood_py/log_vec_py_AML.csv", log_vec_py_AML, delimiter=",") +np.savetxt("likelihood_py/log_vec_py_500.csv", log_vec_py_500, delimiter=",") + + +# check if the loglikelihood vectors are the same +if np.allclose(log_vec_py_AML, log_vec_R_AML, atol=1e-10): + print("The loglikelihoods of the AML trees are the same in R and Python.") + +if np.allclose(log_vec_py_500, log_vec_R_500, atol=1e-10): + print( + "The loglikelihoods of the 500 randomly generated" + " trees are the same in R and Python." + ) diff --git a/warmup/likelihood/R_py_loglikelihood_comparison_new_v2.py b/warmup/likelihood/R_py_loglikelihood_comparison_new_v2.py new file mode 100644 index 0000000..05ce81b --- /dev/null +++ b/warmup/likelihood/R_py_loglikelihood_comparison_new_v2.py @@ -0,0 +1,80 @@ +import pandas as pd +import pmhn._trees._io_modified as io +from pmhn._trees._backend_new_v2 import OriginalTreeMHNBackend, LoglikelihoodSingleTree +import csv +import numpy as np + + +def csv_to_numpy(file_path): + with open(file_path, "r") as file: + reader = csv.reader(file) + next(reader) + data_list = list(reader) + return np.array(data_list, dtype=float) + + +# AML trees +df_AML = pd.read_csv("likelihood_R/trees_AML_R.csv") + +# randomly generated 500 trees using a random theta +df_500 = pd.read_csv("likelihood_R/trees_500_R.csv") + +# theta matrices +theta_AML = csv_to_numpy("likelihood_R/MHN_Matrix_AML.csv") +theta_500 = csv_to_numpy("likelihood_R/MHN_Matrix_500.csv") +# loglikelihoods in R +log_vec_R_AML = np.genfromtxt("likelihood_R/log_vec_R_AML.csv", delimiter=",") +log_vec_R_500 = np.genfromtxt("likelihood_R/log_vec_R_500.csv", delimiter=",") + +# define sampling rate +sampling_rate = 1.0 + +# use modified io +naming = io.ForestNaming( + tree_name="Tree_ID", + naming=io.TreeNaming( + node="Node_ID", + parent="Parent_ID", + data={ + "Mutation_ID": "mutation", + }, + ), +) + +# parse trees +trees_AML = io.parse_forest(df_AML, naming=naming) +trees_500 = io.parse_forest(df_500, naming=naming) + +# calculate loglikelihoods +log_vec_py_AML = np.empty(len(trees_AML)) +log_vec_py_500 = np.empty(len(trees_500)) +backend = OriginalTreeMHNBackend() + +for idx, tree in trees_AML.items(): + print(f"Processing tree {idx} of {len(trees_AML)}") + tree_log = LoglikelihoodSingleTree(tree) + log_value = backend.loglikelihood(tree_log, theta_AML, sampling_rate) + log_vec_py_AML[idx - 1] = log_value + print(f"log_value: {log_value}") + +for idx, tree in trees_500.items(): + print(f"Processing tree {idx} of {len(trees_500)}") + tree_log = LoglikelihoodSingleTree(tree) + log_value = backend.loglikelihood(tree_log, theta_500, sampling_rate) + log_vec_py_500[idx - 1] = log_value + print(f"log_value: {log_value}") + +# write Python loglikelihoods to CSV +np.savetxt("likelihood_py/log_vec_py_AML.csv", log_vec_py_AML, delimiter=",") +np.savetxt("likelihood_py/log_vec_py_500.csv", log_vec_py_500, delimiter=",") + + +# check if the loglikelihood vectors are the same +if np.allclose(log_vec_py_AML, log_vec_R_AML, atol=1e-10): + print("The loglikelihoods of the AML trees are the same in R and Python.") + +if np.allclose(log_vec_py_500, log_vec_R_500, atol=1e-10): + print( + "The loglikelihoods of the 500 randomly generated" + " trees are the same in R and Python." + ) From b839afde8897933e7fcb6dadae32fb3fc8040beb Mon Sep 17 00:00:00 2001 From: Laurenz Keller Date: Fri, 13 Oct 2023 23:00:26 +0200 Subject: [PATCH 24/45] memoization for finding all subtrees --- src/pmhn/_trees/_backend.py | 7 +- src/pmhn/_trees/_backend_new_v1.py | 7 +- src/pmhn/_trees/_backend_new_v2.py | 7 +- src/pmhn/_trees/_io.py | 8 +- src/pmhn/_trees/_io_modified.py | 113 ------------------ src/pmhn/_trees/_simulate.py | 2 +- src/pmhn/_trees/_tree_utils.py | 51 ++++++-- src/pmhn/_trees/_tree_utils_new.py | 74 ++++++++---- .../R_py_loglikelihood_comparison.py | 2 +- .../R_py_loglikelihood_comparison_new_v2.py | 2 +- 10 files changed, 114 insertions(+), 159 deletions(-) delete mode 100644 src/pmhn/_trees/_io_modified.py diff --git a/src/pmhn/_trees/_backend.py b/src/pmhn/_trees/_backend.py index a71a042..50d6651 100644 --- a/src/pmhn/_trees/_backend.py +++ b/src/pmhn/_trees/_backend.py @@ -59,6 +59,11 @@ def gradient_and_loglikelihood( class OriginalTreeMHNBackend(IndividualTreeMHNBackendInterface): + def __init__(self, jitter: float = 1e-10): + self._jitter = jitter + + _jitter: float + def create_V_Mat( self, tree: Node, theta: np.ndarray, sampling_rate: float ) -> np.ndarray: @@ -181,7 +186,7 @@ def loglikelihood( V_csr = csr_matrix(V_transposed) x = spsolve_triangular(V_csr, b, lower=True) - return np.log(x[V_size - 1] + 1e-10) + np.log(sampling_rate) + return np.log(x[V_size - 1] + self._jitter) + np.log(sampling_rate) def gradient(self, tree: Node, theta: np.ndarray) -> np.ndarray: """Calculates the partial derivatives of `log P(tree | theta)` diff --git a/src/pmhn/_trees/_backend_new_v1.py b/src/pmhn/_trees/_backend_new_v1.py index bec6d1c..8739765 100644 --- a/src/pmhn/_trees/_backend_new_v1.py +++ b/src/pmhn/_trees/_backend_new_v1.py @@ -66,6 +66,11 @@ def gradient_and_loglikelihood( class OriginalTreeMHNBackend(IndividualTreeMHNBackendInterface): + def __init__(self, jitter: float = 1e-10): + self._jitter = jitter + + _jitter: float + def create_V_Mat( self, tree: LoglikelihoodSingleTree, @@ -185,7 +190,7 @@ def loglikelihood( V_csr = csr_matrix(V_transposed) x = spsolve_triangular(V_csr, b, lower=True) - return np.log(x[V_size - 1] + 1e-10) + np.log(sampling_rate) + return np.log(x[V_size - 1] + self._jitter) + np.log(sampling_rate) def gradient(self, tree: Node, theta: np.ndarray) -> np.ndarray: """Calculates the partial derivatives of `log P(tree | theta)` diff --git a/src/pmhn/_trees/_backend_new_v2.py b/src/pmhn/_trees/_backend_new_v2.py index d21f719..397e94d 100644 --- a/src/pmhn/_trees/_backend_new_v2.py +++ b/src/pmhn/_trees/_backend_new_v2.py @@ -64,6 +64,11 @@ def gradient_and_loglikelihood( class OriginalTreeMHNBackend(IndividualTreeMHNBackendInterface): + def __init__(self, jitter: float = 1e-10): + self._jitter = jitter + + _jitter: float + def diag_entry(self, tree: Node, theta: np.ndarray, all_mut: set[int]) -> float: """ Calculates a diagonal entry of the V matrix. @@ -169,7 +174,7 @@ def loglikelihood( x[i] -= val * x[index] x[i] /= V_diag - return np.log(x[-1] + 1e-10) + np.log(sampling_rate) + return np.log(x[-1] + self._jitter) + np.log(sampling_rate) def gradient(self, tree: Node, theta: np.ndarray) -> np.ndarray: """Calculates the partial derivatives of `log P(tree | theta)` diff --git a/src/pmhn/_trees/_io.py b/src/pmhn/_trees/_io.py index f7026f0..8594c75 100644 --- a/src/pmhn/_trees/_io.py +++ b/src/pmhn/_trees/_io.py @@ -52,7 +52,7 @@ class ForestNaming: def parse_tree(df: pd.DataFrame, naming: TreeNaming) -> anytree.Node: - """Parses a data frame into a tree. + """Parses a data frame into a tree Args: df: data frame with columns specified in `naming`. @@ -79,10 +79,12 @@ def parse_tree(df: pd.DataFrame, naming: TreeNaming) -> anytree.Node: f"Root is {root}, but {node_id} == {parent_id} " "also looks like a root." ) - root = anytree.Node(node_id, parent=None, **values) + root = anytree.Node(values["mutation"], parent=None, **values) nodes[node_id] = root else: - nodes[node_id] = anytree.Node(node_id, parent=nodes[parent_id], **values) + nodes[node_id] = anytree.Node( + values["mutation"], parent=nodes[parent_id], **values + ) if root is None: raise ValueError("No root found.") diff --git a/src/pmhn/_trees/_io_modified.py b/src/pmhn/_trees/_io_modified.py deleted file mode 100644 index 8594c75..0000000 --- a/src/pmhn/_trees/_io_modified.py +++ /dev/null @@ -1,113 +0,0 @@ -"""Utilities for parsing data frames into AnyTree trees.""" -import dataclasses -from typing import Any - -import anytree -import pandas as pd - - -@dataclasses.dataclass -class TreeNaming: - """Naming conventions used to parse a tree. - - Attrs: - node: column name with node id/name - parent: column name with the parent's id - data: a dictionary mapping column names to field names in nodes - - Example: - TreeNaming( - node="Node_ID", - parent="Parent_ID", - data={ - "Mutation_ID": "mutation", - "SomeValue": "value", - } - means that a data frame with columns - "Node_ID", "Parent_ID", "Mutation_ID", "SomeValue" - is expected. - - Created nodes will have additional fields - "mutation" and "value". - """ - - node: str = "Node_ID" - parent: str = "Parent_ID" - data: dict[str, str] = dataclasses.field( - default_factory=lambda: {"Mutation_ID": "mutation"} - ) - - -@dataclasses.dataclass -class ForestNaming: - """Naming conventions used to parse a forest (a set of trees). - - Attrs: - tree_name: column name storing the tree id/name - naming: TreeNaming object used to parse each tree - """ - - tree_name: str = "Tree_ID" - naming: TreeNaming = dataclasses.field(default_factory=TreeNaming) - - -def parse_tree(df: pd.DataFrame, naming: TreeNaming) -> anytree.Node: - """Parses a data frame into a tree - - Args: - df: data frame with columns specified in `naming`. - naming: specifies the columns that should be present in `df` - - Returns: - the root node of the tree - """ - root = None - nodes = {} # Maps a NodeID value to Node - - for _, row in df.iterrows(): - node_id = row[naming.node] - parent_id = row[naming.parent] - values = {val: row[key] for key, val in naming.data.items()} - - if node_id in nodes: - raise ValueError(f"Node {node_id} already exists.") - - # We found the root - if node_id == parent_id: - if root is not None: - raise ValueError( - f"Root is {root}, but {node_id} == {parent_id} " - "also looks like a root." - ) - root = anytree.Node(values["mutation"], parent=None, **values) - nodes[node_id] = root - else: - nodes[node_id] = anytree.Node( - values["mutation"], parent=nodes[parent_id], **values - ) - - if root is None: - raise ValueError("No root found.") - return root - - -def parse_forest(df: pd.DataFrame, naming: ForestNaming) -> dict[Any, anytree.Node]: - """Parses a data frame with a forest (a set of trees). - - Args: - df: data frame with columns specified as in `naming` - naming: specifies the naming conventions - - Returns: - dictionary with keys being the tree names - (read from the column `naming.tree_name`) - and values being the root nodes - - See also: - parse_tree, which powers this function - """ - result = {} - for tree_name, tree_df in df.groupby(naming.tree_name): - result[tree_name] = parse_tree(df=tree_df, naming=naming.naming) - - return result diff --git a/src/pmhn/_trees/_simulate.py b/src/pmhn/_trees/_simulate.py index 2a514eb..d487390 100644 --- a/src/pmhn/_trees/_simulate.py +++ b/src/pmhn/_trees/_simulate.py @@ -67,7 +67,7 @@ def _find_possible_mutations(old_mutations: list[int], n_mutations: int) -> list ) possible_mutations = list( - set([i + 1 for i in range(n_mutations)]).difference(set(old_mutations)) + set(range(1, n_mutations + 1)).difference(set(old_mutations)) ) return possible_mutations diff --git a/src/pmhn/_trees/_tree_utils.py b/src/pmhn/_trees/_tree_utils.py index 6a81c6a..c292c89 100644 --- a/src/pmhn/_trees/_tree_utils.py +++ b/src/pmhn/_trees/_tree_utils.py @@ -5,27 +5,54 @@ def all_combinations_of_elements(*lists): """ - Takes a variable number of lists as input and returns a list - containing all possible combination of the input lists. In our - use case: It takes a list of lists of subtrees (for each child - one list of subtrees) as input where a subtree itself is a list - of nodes and outputs all combinations of subtrees. - + Takes a variable number of lists as input and returns a generator that yields + all possible combinations of the input lists. In our use case: It takes a list + of lists of subtrees as input where a subtree itself is a list of nodes and + outputs all possible combinations of the lists of subtrees. + + For instance, if we have the following tree: + + 0 + / | \ + 1 3 2 + | + 2 + + and assumed that we know the list of subtrees for the trees: + + 1 + | -> list of subtrees: [[1], [1, 2]] + 2 + + 3 -> list of subtrees: [[3]] + + and + + 2 -> list of subtrees: [[2]] + + , we can find the subtrees of the original tree by looking at + all possible combinations of the list of subtrees for the trees above + and add the root node (0) to each combination (this is done in the + get_subtrees function). + + So the input would be [[[1], [1, 2]],[[3]], [[2]]] + + The generator would yield the following combinations one at a time: + [[1]], [[1, 2]], [[3]], [[2]], [[1], [3]], [[1, 2], [3]], [[1], [2]], + [[1, 2], [2]], [[3], [2]], [[1], [3], [2]], [[1, 2], [3], [2]] + Args: *lists: any number of lists Returns: - all combinations of the input lists + A generator that yields all combinations of the input lists. + """ n = len(lists) - all_combinations = [] - for r in range(1, n + 1): for list_combination in combinations(lists, r): for element_combination in product(*list_combination): - all_combinations.append(list(element_combination)) - - return all_combinations + yield list(element_combination) def create_subtree(original_root: Node, nodes_list: list[Node]) -> Optional[Node]: diff --git a/src/pmhn/_trees/_tree_utils_new.py b/src/pmhn/_trees/_tree_utils_new.py index 936fc08..36354c3 100644 --- a/src/pmhn/_trees/_tree_utils_new.py +++ b/src/pmhn/_trees/_tree_utils_new.py @@ -5,27 +5,54 @@ def all_combinations_of_elements(*lists): """ - Takes a variable number of lists as input and returns a list - containing all possible combination of the input lists. In our - use case: It takes a list of lists of subtrees (for each child - one list of subtrees) as input where a subtree itself is a list - of nodes and outputs all combinations of subtrees. - + Takes a variable number of lists as input and returns a generator that yields + all possible combinations of the input lists. In our use case: It takes a list + of lists of subtrees as input where a subtree itself is a list of nodes and + outputs all possible combinations of the lists of subtrees. + + For instance, if we have the following tree: + + 0 + / | \ + 1 3 2 + | + 2 + + and assumed that we know the list of subtrees for the trees: + + 1 + | -> list of subtrees: [[1], [1, 2]] + 2 + + 3 -> list of subtrees: [[3]] + + and + + 2 -> list of subtrees: [[2]] + + , we can find the subtrees of the original tree by looking at + all possible combinations of the list of subtrees for the trees above + and add the root node (0) to each combination (this is done in the + get_subtrees function). + + So the input would be [[[1], [1, 2]],[[3]], [[2]]] + + The generator would yield the following combinations one at a time: + [[1]], [[1, 2]], [[3]], [[2]], [[1], [3]], [[1, 2], [3]], [[1], [2]], + [[1, 2], [2]], [[3], [2]], [[1], [3], [2]], [[1, 2], [3], [2]] + Args: *lists: any number of lists Returns: - all combinations of the input lists + A generator that yields all combinations of the input lists. + """ n = len(lists) - all_combinations = [] - for r in range(1, n + 1): for list_combination in combinations(lists, r): for element_combination in product(*list_combination): - all_combinations.append(list(element_combination)) - - return all_combinations + yield list(element_combination) def create_subtree(original_root: Node, nodes_list: list[Node]) -> Node: @@ -47,23 +74,18 @@ def create_subtree(original_root: Node, nodes_list: list[Node]) -> Node: return nodes_dict[original_root] -def get_subtrees(node: Node) -> list[list[Node]]: - """ - Creates a list of all subtrees of a tree. - A recursive approach is employed: If one knows the subtrees of the - children of the root node, then one can find all combinations of - the subtrees of the children and add the root node to each one - of these combinations, this way one obtains all subtrees of the original tree. +def get_subtrees(node: Node, memo: Optional[dict] = None) -> list[list[Node]]: + if memo is None: + memo = {} + + if node in memo: + return memo[node] - Args: - node: the root node - Returns: - a list of subtrees - """ if not node.children: + memo[node] = [[node]] return [[node]] - child_subtrees = [get_subtrees(child) for child in node.children] + child_subtrees = [get_subtrees(child, memo) for child in node.children] combined_subtrees = all_combinations_of_elements(*child_subtrees) @@ -75,6 +97,8 @@ def get_subtrees(node: Node) -> list[list[Node]]: ] result_subtrees.append(subtree_with_root) + memo[node] = result_subtrees + return result_subtrees diff --git a/warmup/likelihood/R_py_loglikelihood_comparison.py b/warmup/likelihood/R_py_loglikelihood_comparison.py index e882b73..a1d079a 100644 --- a/warmup/likelihood/R_py_loglikelihood_comparison.py +++ b/warmup/likelihood/R_py_loglikelihood_comparison.py @@ -1,5 +1,5 @@ import pandas as pd -import pmhn._trees._io_modified as io +import pmhn._trees._io as io from pmhn._trees._backend import OriginalTreeMHNBackend import csv import numpy as np diff --git a/warmup/likelihood/R_py_loglikelihood_comparison_new_v2.py b/warmup/likelihood/R_py_loglikelihood_comparison_new_v2.py index 05ce81b..00636b1 100644 --- a/warmup/likelihood/R_py_loglikelihood_comparison_new_v2.py +++ b/warmup/likelihood/R_py_loglikelihood_comparison_new_v2.py @@ -1,5 +1,5 @@ import pandas as pd -import pmhn._trees._io_modified as io +import pmhn._trees._io as io from pmhn._trees._backend_new_v2 import OriginalTreeMHNBackend, LoglikelihoodSingleTree import csv import numpy as np From 42ccc90a93a1e315310cf41110169fec75d2c505 Mon Sep 17 00:00:00 2001 From: Laurenz Keller Date: Mon, 16 Oct 2023 16:42:32 +0200 Subject: [PATCH 25/45] minor changes --- src/pmhn/_trees/_tree_utils_new.py | 9 ++ .../R_py_loglikelihood_comparison_new_v2.py | 2 - ...y_loglikelihood_comparison_new_v2_10000.py | 94 +++++++++++++++++++ 3 files changed, 103 insertions(+), 2 deletions(-) create mode 100644 warmup/likelihood/R_py_loglikelihood_comparison_new_v2_10000.py diff --git a/src/pmhn/_trees/_tree_utils_new.py b/src/pmhn/_trees/_tree_utils_new.py index 36354c3..778ce18 100644 --- a/src/pmhn/_trees/_tree_utils_new.py +++ b/src/pmhn/_trees/_tree_utils_new.py @@ -75,6 +75,15 @@ def create_subtree(original_root: Node, nodes_list: list[Node]) -> Node: def get_subtrees(node: Node, memo: Optional[dict] = None) -> list[list[Node]]: + """ + Creates a list of all subtrees of a tree. + + + Args: + node: the root node + Returns: + a list of subtrees where each subtree is a list of nodes + """ if memo is None: memo = {} diff --git a/warmup/likelihood/R_py_loglikelihood_comparison_new_v2.py b/warmup/likelihood/R_py_loglikelihood_comparison_new_v2.py index 00636b1..1d24aeb 100644 --- a/warmup/likelihood/R_py_loglikelihood_comparison_new_v2.py +++ b/warmup/likelihood/R_py_loglikelihood_comparison_new_v2.py @@ -49,7 +49,6 @@ def csv_to_numpy(file_path): log_vec_py_AML = np.empty(len(trees_AML)) log_vec_py_500 = np.empty(len(trees_500)) backend = OriginalTreeMHNBackend() - for idx, tree in trees_AML.items(): print(f"Processing tree {idx} of {len(trees_AML)}") tree_log = LoglikelihoodSingleTree(tree) @@ -63,7 +62,6 @@ def csv_to_numpy(file_path): log_value = backend.loglikelihood(tree_log, theta_500, sampling_rate) log_vec_py_500[idx - 1] = log_value print(f"log_value: {log_value}") - # write Python loglikelihoods to CSV np.savetxt("likelihood_py/log_vec_py_AML.csv", log_vec_py_AML, delimiter=",") np.savetxt("likelihood_py/log_vec_py_500.csv", log_vec_py_500, delimiter=",") diff --git a/warmup/likelihood/R_py_loglikelihood_comparison_new_v2_10000.py b/warmup/likelihood/R_py_loglikelihood_comparison_new_v2_10000.py new file mode 100644 index 0000000..2a47409 --- /dev/null +++ b/warmup/likelihood/R_py_loglikelihood_comparison_new_v2_10000.py @@ -0,0 +1,94 @@ +import pandas as pd +import pmhn._trees._io as io +from pmhn._trees._backend_new_v2 import OriginalTreeMHNBackend, LoglikelihoodSingleTree +import csv +import numpy as np + + +def csv_to_numpy(file_path): + with open(file_path, "r") as file: + reader = csv.reader(file) + next(reader) + data_list = list(reader) + return np.array(data_list, dtype=float) + + +# AML trees +df_AML = pd.read_csv("likelihood_R/trees_AML_R.csv") + +# randomly generated 500 trees using a random theta +df_500 = pd.read_csv("likelihood_R/trees_500_R.csv") + +df_10000 = pd.read_csv("likelihood_R/trees_10000_R.csv") +# theta matrices +theta_AML = csv_to_numpy("likelihood_R/MHN_Matrix_AML.csv") +theta_500 = csv_to_numpy("likelihood_R/MHN_Matrix_500.csv") +theta_10000 = csv_to_numpy("likelihood_R/MHN_Matrix_10000.csv") +# loglikelihoods in R +log_vec_R_AML = np.genfromtxt("likelihood_R/log_vec_R_AML.csv", delimiter=",") +log_vec_R_500 = np.genfromtxt("likelihood_R/log_vec_R_500.csv", delimiter=",") +log_vec_R_10000 = np.genfromtxt("likelihood_R/log_vec_R_10000.csv", delimiter=",") +# define sampling rate +sampling_rate = 1.0 + +# use modified io +naming = io.ForestNaming( + tree_name="Tree_ID", + naming=io.TreeNaming( + node="Node_ID", + parent="Parent_ID", + data={ + "Mutation_ID": "mutation", + }, + ), +) + +# parse trees +trees_AML = io.parse_forest(df_AML, naming=naming) +trees_500 = io.parse_forest(df_500, naming=naming) +trees_10000 = io.parse_forest(df_10000, naming=naming) +# calculate loglikelihoods +log_vec_py_AML = np.empty(len(trees_AML)) +log_vec_py_500 = np.empty(len(trees_500)) +log_vec_py_10000 = np.empty(len(trees_10000)) +backend = OriginalTreeMHNBackend() + +for idx, tree in trees_AML.items(): + print(f"Processing tree {idx} of {len(trees_AML)}") + tree_log = LoglikelihoodSingleTree(tree) + log_value = backend.loglikelihood(tree_log, theta_AML, sampling_rate) + log_vec_py_AML[idx - 1] = log_value + print(f"log_value: {log_value}") + +for idx, tree in trees_500.items(): + print(f"Processing tree {idx} of {len(trees_500)}") + tree_log = LoglikelihoodSingleTree(tree) + log_value = backend.loglikelihood(tree_log, theta_500, sampling_rate) + log_vec_py_500[idx - 1] = log_value + print(f"log_value: {log_value}") + +for idx, tree in trees_10000.items(): + print(f"Processing tree {idx} of {len(trees_10000)}") + tree_log = LoglikelihoodSingleTree(tree) + log_value = backend.loglikelihood(tree_log, theta_10000, sampling_rate) + log_vec_py_10000[idx - 1] = log_value + print(f"log_value: {log_value}") +# write Python loglikelihoods to CSV +np.savetxt("likelihood_py/log_vec_py_AML.csv", log_vec_py_AML, delimiter=",") +np.savetxt("likelihood_py/log_vec_py_500.csv", log_vec_py_500, delimiter=",") +np.savetxt("likelihood_py/log_vec_py_10000.csv", log_vec_py_10000, delimiter=",") + +# check if the loglikelihood vectors are the same +if np.allclose(log_vec_py_AML, log_vec_R_AML, atol=1e-10): + print("The loglikelihoods of the AML trees are the same in R and Python.") + +if np.allclose(log_vec_py_500, log_vec_R_500, atol=1e-10): + print( + "The loglikelihoods of the 500 randomly generated" + " trees are the same in R and Python." + ) +if np.allclose(log_vec_py_10000, log_vec_R_10000, atol=1e-10): + print( + "The loglikelihoods of the 10000 randomly generated" + " trees are the same in R and Python." + ) From 3479452fcad35f16b7990d290258dcb5141ade52 Mon Sep 17 00:00:00 2001 From: Laurenz Keller Date: Tue, 17 Oct 2023 00:46:27 +0200 Subject: [PATCH 26/45] small change --- src/pmhn/_trees/_io.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pmhn/_trees/_io.py b/src/pmhn/_trees/_io.py index 8594c75..6758bda 100644 --- a/src/pmhn/_trees/_io.py +++ b/src/pmhn/_trees/_io.py @@ -79,7 +79,7 @@ def parse_tree(df: pd.DataFrame, naming: TreeNaming) -> anytree.Node: f"Root is {root}, but {node_id} == {parent_id} " "also looks like a root." ) - root = anytree.Node(values["mutation"], parent=None, **values) + root = anytree.Node(values[next(iter(values))], parent=None, **values) nodes[node_id] = root else: nodes[node_id] = anytree.Node( From 6f365184949308b11169085570b619931d78019e Mon Sep 17 00:00:00 2001 From: Laurenz Keller Date: Tue, 17 Oct 2023 01:35:14 +0200 Subject: [PATCH 27/45] small change --- src/pmhn/_trees/_backend_new_v2.py | 20 ++++++++------------ src/pmhn/_trees/_io.py | 2 +- 2 files changed, 9 insertions(+), 13 deletions(-) diff --git a/src/pmhn/_trees/_backend_new_v2.py b/src/pmhn/_trees/_backend_new_v2.py index 397e94d..12747be 100644 --- a/src/pmhn/_trees/_backend_new_v2.py +++ b/src/pmhn/_trees/_backend_new_v2.py @@ -86,15 +86,11 @@ def diag_entry(self, tree: Node, theta: np.ndarray, all_mut: set[int]) -> float: while len(current_nodes) != 0: next_nodes = [] for node in current_nodes: - tree_mutations = list(node.path) + list(node.children) - exit_mutations = list( - set(all_mut).difference( - set( - [ - tree_mutation.name # type: ignore - for tree_mutation in tree_mutations - ] - ) + tree_mutations = set(node.path).union(node.children) + exit_mutations = all_mut.difference( + set( + tree_mutation.name # type: ignore + for tree_mutation in tree_mutations ) ) for mutation in exit_mutations: @@ -166,10 +162,10 @@ def loglikelihood( for j, (subtree_j, subtree_size_j) in enumerate( tree._subtrees_dict.items() ): - if i == j: - V_diag = sampling_rate - self.diag_entry(subtree_i, theta, all_mut) - elif subtree_size_i - subtree_size_j == 1: + if subtree_size_i - subtree_size_j == 1: V_col[j] = -self.off_diag_entry(subtree_j, subtree_i, theta) + elif i == j: + V_diag = sampling_rate - self.diag_entry(subtree_i, theta, all_mut) for index, val in V_col.items(): x[i] -= val * x[index] x[i] /= V_diag diff --git a/src/pmhn/_trees/_io.py b/src/pmhn/_trees/_io.py index 6758bda..8594c75 100644 --- a/src/pmhn/_trees/_io.py +++ b/src/pmhn/_trees/_io.py @@ -79,7 +79,7 @@ def parse_tree(df: pd.DataFrame, naming: TreeNaming) -> anytree.Node: f"Root is {root}, but {node_id} == {parent_id} " "also looks like a root." ) - root = anytree.Node(values[next(iter(values))], parent=None, **values) + root = anytree.Node(values["mutation"], parent=None, **values) nodes[node_id] = root else: nodes[node_id] = anytree.Node( From 64bdc6af848d29758245e7295b232b9106d2eb15 Mon Sep 17 00:00:00 2001 From: Laurenz Keller Date: Tue, 17 Oct 2023 02:09:01 +0200 Subject: [PATCH 28/45] small change --- src/pmhn/_trees/_tree_utils_new.py | 18 +++++++++++------- 1 file changed, 11 insertions(+), 7 deletions(-) diff --git a/src/pmhn/_trees/_tree_utils_new.py b/src/pmhn/_trees/_tree_utils_new.py index 778ce18..cff399d 100644 --- a/src/pmhn/_trees/_tree_utils_new.py +++ b/src/pmhn/_trees/_tree_utils_new.py @@ -186,15 +186,19 @@ def bfs_compare(tree1: Node, tree2: Node) -> Optional[Node]: exit_node = None dict_nodes1_lineages = { - node: get_lineage(node) for nodes1 in iter1 for node in nodes1 + (node, level): get_lineage(node) + for level, nodes1 in enumerate(iter1) + for node in nodes1 } dict_nodes2_lineages = { - node: get_lineage(node) for nodes2 in iter2 for node in nodes2 + (node, level): get_lineage(node) + for level, nodes2 in enumerate(iter2) + for node in nodes2 } - for nodes1, nodes2 in zip(iter1, iter2): - set_nodes1_lineages = {dict_nodes1_lineages[node] for node in nodes1} - set_nodes2_lineages = {dict_nodes2_lineages[node] for node in nodes2} + for level, (nodes1, nodes2) in enumerate(zip(iter1, iter2)): + set_nodes1_lineages = {dict_nodes1_lineages[(node, level)] for node in nodes1} + set_nodes2_lineages = {dict_nodes2_lineages[(node, level)] for node in nodes2} additional_nodes_lineages = set_nodes2_lineages ^ set_nodes1_lineages diff_count += len(additional_nodes_lineages) @@ -202,11 +206,11 @@ def bfs_compare(tree1: Node, tree2: Node) -> Optional[Node]: additional_node_lineage = additional_nodes_lineages.pop() for node in nodes1: - if dict_nodes1_lineages[node] == additional_node_lineage: + if dict_nodes1_lineages[(node, level)] == additional_node_lineage: return None for node in nodes2: - if dict_nodes2_lineages[node] == additional_node_lineage: + if dict_nodes2_lineages[(node, level)] == additional_node_lineage: exit_node = node break if diff_count > 1: From 6dc78fbe911b2428f19866e73a89c5cf58c997bc Mon Sep 17 00:00:00 2001 From: Laurenz Keller Date: Tue, 17 Oct 2023 10:46:58 +0200 Subject: [PATCH 29/45] small change --- src/pmhn/_trees/_tree_utils_new.py | 26 +++++++++----------------- 1 file changed, 9 insertions(+), 17 deletions(-) diff --git a/src/pmhn/_trees/_tree_utils_new.py b/src/pmhn/_trees/_tree_utils_new.py index cff399d..657a9fa 100644 --- a/src/pmhn/_trees/_tree_utils_new.py +++ b/src/pmhn/_trees/_tree_utils_new.py @@ -185,20 +185,11 @@ def bfs_compare(tree1: Node, tree2: Node) -> Optional[Node]: iter2 = list(LevelOrderGroupIter(tree2)) exit_node = None - dict_nodes1_lineages = { - (node, level): get_lineage(node) - for level, nodes1 in enumerate(iter1) - for node in nodes1 - } - dict_nodes2_lineages = { - (node, level): get_lineage(node) - for level, nodes2 in enumerate(iter2) - for node in nodes2 - } - for level, (nodes1, nodes2) in enumerate(zip(iter1, iter2)): - set_nodes1_lineages = {dict_nodes1_lineages[(node, level)] for node in nodes1} - set_nodes2_lineages = {dict_nodes2_lineages[(node, level)] for node in nodes2} + dict_nodes1_lineages = {node: get_lineage(node) for node in nodes1} + dict_nodes2_lineages = {node: get_lineage(node) for node in nodes2} + set_nodes1_lineages = set(dict_nodes1_lineages.values()) + set_nodes2_lineages = set(dict_nodes2_lineages.values()) additional_nodes_lineages = set_nodes2_lineages ^ set_nodes1_lineages diff_count += len(additional_nodes_lineages) @@ -206,15 +197,16 @@ def bfs_compare(tree1: Node, tree2: Node) -> Optional[Node]: additional_node_lineage = additional_nodes_lineages.pop() for node in nodes1: - if dict_nodes1_lineages[(node, level)] == additional_node_lineage: + if dict_nodes1_lineages[node] == additional_node_lineage: return None for node in nodes2: - if dict_nodes2_lineages[(node, level)] == additional_node_lineage: + if dict_nodes2_lineages[node] == additional_node_lineage: exit_node = node break - if diff_count > 1: - return None + + if diff_count > 1: + return None if diff_count == 0: return iter2[-1][0] From f490594242ba9f55b6c01023eb94549aafb73dee Mon Sep 17 00:00:00 2001 From: Laurenz Keller Date: Tue, 17 Oct 2023 22:41:32 +0200 Subject: [PATCH 30/45] small change --- src/pmhn/_trees/_tree_utils_new.py | 4 +- .../R_py_loglikelihood_comparison_new_v1.py | 2 +- ...glikelihood_comparison_new_v1_profiling.py | 86 +++++++++++++++++++ ...glikelihood_comparison_new_v2_profiling.py | 86 +++++++++++++++++++ 4 files changed, 175 insertions(+), 3 deletions(-) create mode 100644 warmup/likelihood/R_py_loglikelihood_comparison_new_v1_profiling.py create mode 100644 warmup/likelihood/R_py_loglikelihood_comparison_new_v2_profiling.py diff --git a/src/pmhn/_trees/_tree_utils_new.py b/src/pmhn/_trees/_tree_utils_new.py index 657a9fa..ba0ec75 100644 --- a/src/pmhn/_trees/_tree_utils_new.py +++ b/src/pmhn/_trees/_tree_utils_new.py @@ -158,8 +158,8 @@ def check_equality(tree1: Optional[Node], tree2: Optional[Node]) -> bool: if len(tree1.descendants) != len(tree2.descendants): return False for nodes1, nodes2 in zip(iter1, iter2): - set_nodes1_lineages = {tuple(get_lineage(node)) for node in nodes1} - set_nodes2_lineages = {tuple(get_lineage(node)) for node in nodes2} + set_nodes1_lineages = {get_lineage(node) for node in nodes1} + set_nodes2_lineages = {get_lineage(node) for node in nodes2} additional_nodes_lineages = set_nodes2_lineages ^ set_nodes1_lineages if len(additional_nodes_lineages) != 0: return False diff --git a/warmup/likelihood/R_py_loglikelihood_comparison_new_v1.py b/warmup/likelihood/R_py_loglikelihood_comparison_new_v1.py index a6ca25a..c278fd4 100644 --- a/warmup/likelihood/R_py_loglikelihood_comparison_new_v1.py +++ b/warmup/likelihood/R_py_loglikelihood_comparison_new_v1.py @@ -1,5 +1,5 @@ import pandas as pd -import pmhn._trees._io_modified as io +import pmhn._trees._io as io from pmhn._trees._backend_new_v1 import OriginalTreeMHNBackend, LoglikelihoodSingleTree import csv import numpy as np diff --git a/warmup/likelihood/R_py_loglikelihood_comparison_new_v1_profiling.py b/warmup/likelihood/R_py_loglikelihood_comparison_new_v1_profiling.py new file mode 100644 index 0000000..6aa4db4 --- /dev/null +++ b/warmup/likelihood/R_py_loglikelihood_comparison_new_v1_profiling.py @@ -0,0 +1,86 @@ +import pandas as pd +import pmhn._trees._io as io +from pmhn._trees._backend_new_v1 import OriginalTreeMHNBackend, LoglikelihoodSingleTree +import csv +import numpy as np +import pstats +import cProfile + + +def csv_to_numpy(file_path): + with open(file_path, "r") as file: + reader = csv.reader(file) + next(reader) + data_list = list(reader) + return np.array(data_list, dtype=float) + + +# AML trees +df_AML = pd.read_csv("likelihood_R/trees_AML_R.csv") + +# randomly generated 500 trees using a random theta +df_500 = pd.read_csv("likelihood_R/trees_500_R.csv") + +# theta matrices +theta_AML = csv_to_numpy("likelihood_R/MHN_Matrix_AML.csv") +theta_500 = csv_to_numpy("likelihood_R/MHN_Matrix_500.csv") +# loglikelihoods in R +log_vec_R_AML = np.genfromtxt("likelihood_R/log_vec_R_AML.csv", delimiter=",") +log_vec_R_500 = np.genfromtxt("likelihood_R/log_vec_R_500.csv", delimiter=",") + +# define sampling rate +sampling_rate = 1.0 + +# use modified io +naming = io.ForestNaming( + tree_name="Tree_ID", + naming=io.TreeNaming( + node="Node_ID", + parent="Parent_ID", + data={ + "Mutation_ID": "mutation", + }, + ), +) + +# parse trees +trees_AML = io.parse_forest(df_AML, naming=naming) +trees_500 = io.parse_forest(df_500, naming=naming) + +# calculate loglikelihoods +log_vec_py_AML = np.empty(len(trees_AML)) +log_vec_py_500 = np.empty(len(trees_500)) +backend = OriginalTreeMHNBackend() +profiler = cProfile.Profile() +profiler.enable() +for idx, tree in trees_AML.items(): + print(f"Processing tree {idx} of {len(trees_AML)}") + tree_log = LoglikelihoodSingleTree(tree) + log_value = backend.loglikelihood(tree_log, theta_AML, sampling_rate) + log_vec_py_AML[idx - 1] = log_value + print(f"log_value: {log_value}") + +for idx, tree in trees_500.items(): + print(f"Processing tree {idx} of {len(trees_500)}") + tree_log = LoglikelihoodSingleTree(tree) + log_value = backend.loglikelihood(tree_log, theta_500, sampling_rate) + log_vec_py_500[idx - 1] = log_value + print(f"log_value: {log_value}") +profiler.disable() + +# write Python loglikelihoods to CSV +np.savetxt("likelihood_py/log_vec_py_AML.csv", log_vec_py_AML, delimiter=",") +np.savetxt("likelihood_py/log_vec_py_500.csv", log_vec_py_500, delimiter=",") + + +# check if the loglikelihood vectors are the same +if np.allclose(log_vec_py_AML, log_vec_R_AML, atol=1e-10): + print("The loglikelihoods of the AML trees are the same in R and Python.") + +if np.allclose(log_vec_py_500, log_vec_R_500, atol=1e-10): + print( + "The loglikelihoods of the 500 randomly generated" + " trees are the same in R and Python." + ) +stats = pstats.Stats(profiler).sort_stats("cumtime") # Sort by cumulative time spent +stats.print_stats() diff --git a/warmup/likelihood/R_py_loglikelihood_comparison_new_v2_profiling.py b/warmup/likelihood/R_py_loglikelihood_comparison_new_v2_profiling.py new file mode 100644 index 0000000..25d4b0f --- /dev/null +++ b/warmup/likelihood/R_py_loglikelihood_comparison_new_v2_profiling.py @@ -0,0 +1,86 @@ +import pandas as pd +import pmhn._trees._io as io +from pmhn._trees._backend_new_v2 import OriginalTreeMHNBackend, LoglikelihoodSingleTree +import csv +import numpy as np +import pstats +import cProfile + + +def csv_to_numpy(file_path): + with open(file_path, "r") as file: + reader = csv.reader(file) + next(reader) + data_list = list(reader) + return np.array(data_list, dtype=float) + + +# AML trees +df_AML = pd.read_csv("likelihood_R/trees_AML_R.csv") + +# randomly generated 500 trees using a random theta +df_500 = pd.read_csv("likelihood_R/trees_500_R.csv") + +# theta matrices +theta_AML = csv_to_numpy("likelihood_R/MHN_Matrix_AML.csv") +theta_500 = csv_to_numpy("likelihood_R/MHN_Matrix_500.csv") +# loglikelihoods in R +log_vec_R_AML = np.genfromtxt("likelihood_R/log_vec_R_AML.csv", delimiter=",") +log_vec_R_500 = np.genfromtxt("likelihood_R/log_vec_R_500.csv", delimiter=",") + +# define sampling rate +sampling_rate = 1.0 + +# use modified io +naming = io.ForestNaming( + tree_name="Tree_ID", + naming=io.TreeNaming( + node="Node_ID", + parent="Parent_ID", + data={ + "Mutation_ID": "mutation", + }, + ), +) + +# parse trees +trees_AML = io.parse_forest(df_AML, naming=naming) +trees_500 = io.parse_forest(df_500, naming=naming) + +# calculate loglikelihoods +log_vec_py_AML = np.empty(len(trees_AML)) +log_vec_py_500 = np.empty(len(trees_500)) +backend = OriginalTreeMHNBackend() +profiler = cProfile.Profile() +profiler.enable() +for idx, tree in trees_AML.items(): + print(f"Processing tree {idx} of {len(trees_AML)}") + tree_log = LoglikelihoodSingleTree(tree) + log_value = backend.loglikelihood(tree_log, theta_AML, sampling_rate) + log_vec_py_AML[idx - 1] = log_value + print(f"log_value: {log_value}") + +for idx, tree in trees_500.items(): + print(f"Processing tree {idx} of {len(trees_500)}") + tree_log = LoglikelihoodSingleTree(tree) + log_value = backend.loglikelihood(tree_log, theta_500, sampling_rate) + log_vec_py_500[idx - 1] = log_value + print(f"log_value: {log_value}") +profiler.disable() + +# write Python loglikelihoods to CSV +np.savetxt("likelihood_py/log_vec_py_AML.csv", log_vec_py_AML, delimiter=",") +np.savetxt("likelihood_py/log_vec_py_500.csv", log_vec_py_500, delimiter=",") + + +# check if the loglikelihood vectors are the same +if np.allclose(log_vec_py_AML, log_vec_R_AML, atol=1e-10): + print("The loglikelihoods of the AML trees are the same in R and Python.") + +if np.allclose(log_vec_py_500, log_vec_R_500, atol=1e-10): + print( + "The loglikelihoods of the 500 randomly generated" + " trees are the same in R and Python." + ) +stats = pstats.Stats(profiler).sort_stats("cumtime") # Sort by cumulative time spent +stats.print_stats() From 6d492a67183c76b39a32912d28fd5368467aaeba Mon Sep 17 00:00:00 2001 From: Laurenz Keller Date: Wed, 18 Oct 2023 10:44:53 +0200 Subject: [PATCH 31/45] changed io file and unit tests for it --- src/pmhn/_trees/_io.py | 5 +++-- tests/trees/test_io.py | 16 +++++++++------- 2 files changed, 12 insertions(+), 9 deletions(-) diff --git a/src/pmhn/_trees/_io.py b/src/pmhn/_trees/_io.py index 8594c75..51602e7 100644 --- a/src/pmhn/_trees/_io.py +++ b/src/pmhn/_trees/_io.py @@ -33,6 +33,7 @@ class TreeNaming: node: str = "Node_ID" parent: str = "Parent_ID" + mutation: str = "Mutation_ID" data: dict[str, str] = dataclasses.field( default_factory=lambda: {"Mutation_ID": "mutation"} ) @@ -79,11 +80,11 @@ def parse_tree(df: pd.DataFrame, naming: TreeNaming) -> anytree.Node: f"Root is {root}, but {node_id} == {parent_id} " "also looks like a root." ) - root = anytree.Node(values["mutation"], parent=None, **values) + root = anytree.Node(row[naming.mutation], parent=None, **values) nodes[node_id] = root else: nodes[node_id] = anytree.Node( - values["mutation"], parent=nodes[parent_id], **values + row[naming.mutation], parent=nodes[parent_id], **values ) if root is None: diff --git a/tests/trees/test_io.py b/tests/trees/test_io.py index aabcbba..96e606c 100644 --- a/tests/trees/test_io.py +++ b/tests/trees/test_io.py @@ -12,15 +12,17 @@ def test_parse_tree() -> None: """ tree_df = pd.DataFrame( { - "NodeID": [1, 2, 5, 10], - "ParentID": [1, 1, 1, 5], - "ValueSquare": [1, 4, 25, 100], - "ValueDoubled": [2, 4, 10, 20], + "Node_ID": [1, 2, 3, 4], + "Parent_ID": [1, 1, 1, 2], + "Mutation_ID": [0, 5, 2, 10], + "ValueSquare": [0, 25, 4, 100], + "ValueDoubled": [0, 10, 4, 20], } ) naming = io.TreeNaming( - node="NodeID", - parent="ParentID", + node="Node_ID", + parent="Parent_ID", + mutation="Mutation_ID", data={ "ValueSquare": "square", "ValueDoubled": "doubled", @@ -34,7 +36,7 @@ def test_parse_tree() -> None: assert node.doubled == 2 * n if n == 2 or n == 5: - assert node.parent.name == 1 + assert node.parent.name == 0 elif n == 10: assert node.parent.name == 5 From a6c48364070a303f2549aab23aea7ba543682232 Mon Sep 17 00:00:00 2001 From: Laurenz Keller Date: Fri, 20 Oct 2023 17:20:50 +0200 Subject: [PATCH 32/45] genotype --- src/pmhn/_trees/_backend.py | 107 ++--- .../{_backend_new_v2.py => _backend_geno.py} | 143 +++--- src/pmhn/_trees/_backend_new_v1.py | 217 --------- src/pmhn/_trees/_tree_utils.py | 84 ++-- src/pmhn/_trees/_tree_utils_geno.py | 165 +++++++ src/pmhn/_trees/_tree_utils_new.py | 230 ---------- tests/trees/test_likelihood.py | 419 ------------------ .../R_py_loglikelihood_comparison.py | 15 +- ... => R_py_loglikelihood_comparison_geno.py} | 22 +- ...glikelihood_comparison_new_v1_profiling.py | 86 ---- .../R_py_loglikelihood_comparison_new_v2.py | 78 ---- ...y_loglikelihood_comparison_new_v2_10000.py | 94 ---- ...glikelihood_comparison_new_v2_profiling.py | 86 ---- 13 files changed, 357 insertions(+), 1389 deletions(-) rename src/pmhn/_trees/{_backend_new_v2.py => _backend_geno.py} (56%) delete mode 100644 src/pmhn/_trees/_backend_new_v1.py create mode 100644 src/pmhn/_trees/_tree_utils_geno.py delete mode 100644 src/pmhn/_trees/_tree_utils_new.py delete mode 100644 tests/trees/test_likelihood.py rename warmup/likelihood/{R_py_loglikelihood_comparison_new_v1.py => R_py_loglikelihood_comparison_geno.py} (78%) delete mode 100644 warmup/likelihood/R_py_loglikelihood_comparison_new_v1_profiling.py delete mode 100644 warmup/likelihood/R_py_loglikelihood_comparison_new_v2.py delete mode 100644 warmup/likelihood/R_py_loglikelihood_comparison_new_v2_10000.py delete mode 100644 warmup/likelihood/R_py_loglikelihood_comparison_new_v2_profiling.py diff --git a/src/pmhn/_trees/_backend.py b/src/pmhn/_trees/_backend.py index 50d6651..66fce4d 100644 --- a/src/pmhn/_trees/_backend.py +++ b/src/pmhn/_trees/_backend.py @@ -2,11 +2,16 @@ import numpy as np -from scipy.sparse.linalg import spsolve_triangular -from scipy.sparse import csr_matrix from pmhn._trees._interfaces import Tree from pmhn._trees._tree_utils import create_all_subtrees, bfs_compare -from anytree import Node +from anytree import Node, LevelOrderGroupIter + + +class LoglikelihoodSingleTree: + def __init__(self, tree: Node): + self._subtrees_dict = create_all_subtrees(tree) + + _subtrees_dict: dict[Node, int] class IndividualTreeMHNBackendInterface(Protocol): @@ -64,33 +69,7 @@ def __init__(self, jitter: float = 1e-10): _jitter: float - def create_V_Mat( - self, tree: Node, theta: np.ndarray, sampling_rate: float - ) -> np.ndarray: - """Calculates the V matrix. - - Args: - tree: a tree - theta: real-valued (i.e., log-theta) matrix, - shape (n_mutations, n_mutations) - sampling_rate: a scalar of type float - Returns: - the V matrix. - """ - - subtrees = create_all_subtrees(tree) - subtrees_size = len(subtrees) - Q = np.zeros((subtrees_size, subtrees_size)) - for i in range(subtrees_size): - for j in range(subtrees_size): - if i == j: - Q[i][j] = self.diag_entry(subtrees[i], theta) - else: - Q[i][j] = self.off_diag_entry(subtrees[i], subtrees[j], theta) - V = np.eye(subtrees_size) * sampling_rate - Q - return V - - def diag_entry(self, tree: Node, theta: np.ndarray) -> float: + def diag_entry(self, tree: Node, theta: np.ndarray, all_mut: set[int]) -> float: """ Calculates a diagonal entry of the V matrix. @@ -98,41 +77,29 @@ def diag_entry(self, tree: Node, theta: np.ndarray) -> float: tree: a tree theta: real-valued (i.e., log-theta) matrix, shape (n_mutations, n_mutations) - + all_mut: set containing all possible mutations Returns: the diagonal entry of the V matrix corresponding to tree """ lamb_sum = 0 - n_mutations = len(theta) - current_nodes = [tree] - while len(current_nodes) != 0: - next_nodes = [] - for node in current_nodes: - tree_mutations = list(node.path) + list(node.children) - exit_mutations = list( - set([i + 1 for i in range(n_mutations)]).difference( - set( - [ - tree_mutation.name # type: ignore - for tree_mutation in tree_mutations - ] - ) - ) + + for level in LevelOrderGroupIter(tree): + for node in level: + tree_mutations = {n.name for n in node.path}.union( + {c.name for c in node.children} ) + exit_mutations = set(all_mut).difference(tree_mutations) + for mutation in exit_mutations: lamb = 0 - exit_subclone = [ - anc.name # type: ignore - for anc in node.path - if anc.parent is not None - ] + [mutation] + exit_subclone = { + anc.name for anc in node.path if anc.parent is not None + }.union({mutation}) for j in exit_subclone: lamb += theta[mutation - 1][j - 1] lamb = np.exp(lamb) lamb_sum -= lamb - for child in node.children: - next_nodes.append(child) - current_nodes = next_nodes + return lamb_sum def off_diag_entry(self, tree1: Node, tree2: Node, theta: np.ndarray) -> float: @@ -144,7 +111,6 @@ def off_diag_entry(self, tree1: Node, tree2: Node, theta: np.ndarray) -> float: tree2: the second tree theta: real-valued (i.e., log-theta) matrix, shape (n_mutations, n_mutations) - Returns: the off-diagonal entry of the V matrix corresponding to tree1 and tree2 """ @@ -163,7 +129,7 @@ def off_diag_entry(self, tree1: Node, tree2: Node, theta: np.ndarray) -> float: return float(lamb) def loglikelihood( - self, tree: Node, theta: np.ndarray, sampling_rate: float + self, tree: LoglikelihoodSingleTree, theta: np.ndarray, sampling_rate: float ) -> float: """ Calculates loglikelihood `log P(tree | theta)`. @@ -178,15 +144,26 @@ def loglikelihood( """ # TODO(Pawel): this is part of https://github.com/cbg-ethz/pMHN/issues/15 # It can be implemented in any way. - V = self.create_V_Mat(tree=tree, theta=theta, sampling_rate=sampling_rate) - V_size = V.shape[0] - b = np.zeros(V_size) - b[0] = 1 - V_transposed = V.transpose() - V_csr = csr_matrix(V_transposed) - x = spsolve_triangular(V_csr, b, lower=True) - - return np.log(x[V_size - 1] + self._jitter) + np.log(sampling_rate) + subtrees_size = len(tree._subtrees_dict) + x = np.zeros(subtrees_size) + x[0] = 1 + n_mutations = len(theta) + all_mut = set(i + 1 for i in range(n_mutations)) + for i, (subtree_i, subtree_size_i) in enumerate(tree._subtrees_dict.items()): + V_col = {} + V_diag = 0.0 + for j, (subtree_j, subtree_size_j) in enumerate( + tree._subtrees_dict.items() + ): + if subtree_size_i - subtree_size_j == 1: + V_col[j] = -self.off_diag_entry(subtree_j, subtree_i, theta) + elif i == j: + V_diag = sampling_rate - self.diag_entry(subtree_i, theta, all_mut) + for index, val in V_col.items(): + x[i] -= val * x[index] + x[i] /= V_diag + + return np.log(x[-1] + self._jitter) + np.log(sampling_rate) def gradient(self, tree: Node, theta: np.ndarray) -> np.ndarray: """Calculates the partial derivatives of `log P(tree | theta)` diff --git a/src/pmhn/_trees/_backend_new_v2.py b/src/pmhn/_trees/_backend_geno.py similarity index 56% rename from src/pmhn/_trees/_backend_new_v2.py rename to src/pmhn/_trees/_backend_geno.py index 12747be..a26976d 100644 --- a/src/pmhn/_trees/_backend_new_v2.py +++ b/src/pmhn/_trees/_backend_geno.py @@ -3,15 +3,19 @@ import numpy as np from pmhn._trees._interfaces import Tree -from pmhn._trees._tree_utils_new import create_all_subtrees, bfs_compare +from pmhn._trees._tree_utils_geno import create_genotype_subtree_map from anytree import Node class LoglikelihoodSingleTree: def __init__(self, tree: Node): - self._subtrees_dict = create_all_subtrees(tree) + ( + self._genotype_subtree_node_map, + self._index_subclone_map, + ) = create_genotype_subtree_map(tree) - _subtrees_dict: dict[Node, int] + _genotype_subtree_node_map: dict[tuple[tuple[Node, int]], tuple[int, int]] + _index_subclone_map: dict[int, tuple[int]] class IndividualTreeMHNBackendInterface(Protocol): @@ -69,74 +73,68 @@ def __init__(self, jitter: float = 1e-10): _jitter: float - def diag_entry(self, tree: Node, theta: np.ndarray, all_mut: set[int]) -> float: - """ - Calculates a diagonal entry of the V matrix. - - Args: - tree: a tree - theta: real-valued (i.e., log-theta) matrix, - shape (n_mutations, n_mutations) - all_mut: set containing all possible mutations - Returns: - the diagonal entry of the V matrix corresponding to tree - """ + def diag_entry( + self, + tree: LoglikelihoodSingleTree, + genotype: tuple[tuple[Node, int]], + theta: np.ndarray, + all_mut: set[int], + ) -> float: lamb_sum = 0 - current_nodes = [tree] - while len(current_nodes) != 0: - next_nodes = [] - for node in current_nodes: - tree_mutations = set(node.path).union(node.children) - exit_mutations = all_mut.difference( - set( - tree_mutation.name # type: ignore - for tree_mutation in tree_mutations - ) - ) + for i, (node, val) in enumerate(genotype): + if val: + lineage = tree._index_subclone_map[i] + lineage = list(lineage) + tree_mutations = set(lineage + [c.name for c in node.children]) + + exit_mutations = all_mut.difference(tree_mutations) + for mutation in exit_mutations: lamb = 0 - exit_subclone = [ - anc.name # type: ignore - for anc in node.path - if anc.parent is not None - ] + [mutation] + exit_subclone = lineage + [mutation] for j in exit_subclone: - lamb += theta[mutation - 1][j - 1] + if j != 0: + lamb += theta[mutation - 1][j - 1] lamb = np.exp(lamb) lamb_sum -= lamb - for child in node.children: - next_nodes.append(child) - current_nodes = next_nodes return lamb_sum - def off_diag_entry(self, tree1: Node, tree2: Node, theta: np.ndarray) -> float: - """ - Calculates an off-diagonal entry of the V matrix. + def find_single_difference(self, arr1: np.ndarray, arr2: np.ndarray): + differing_indices = np.nonzero(np.bitwise_xor(arr1, arr2))[0] - Args: - tree1: the first tree - tree2: the second tree - theta: real-valued (i.e., log-theta) matrix, - shape (n_mutations, n_mutations) - Returns: - the off-diagonal entry of the V matrix corresponding to tree1 and tree2 - """ - exit_node = bfs_compare(tree1, tree2) - lamb = 0 - if exit_node is None: - return lamb + return ( + (True, differing_indices[0]) + if len(differing_indices) == 1 + else (False, None) + ) + + def off_diag_entry( + self, + tree: LoglikelihoodSingleTree, + genotype_i: np.ndarray, + genotype_j: np.ndarray, + theta: np.ndarray, + ): + is_single_diff, index = self.find_single_difference(genotype_i, genotype_j) + if index is None: + return 0 else: - for j in [ - node.name # type: ignore - for node in exit_node.path - if node.parent is not None - ]: - lamb += theta[exit_node.name - 1][j - 1] + lamb = 0 + lineage = tree._index_subclone_map[index] + exit_mutation = lineage[-1] + for mutation in lineage: + if mutation != 0: + lamb += theta[exit_mutation - 1][mutation - 1] lamb = np.exp(lamb) return float(lamb) def loglikelihood( - self, tree: LoglikelihoodSingleTree, theta: np.ndarray, sampling_rate: float + self, + tree: LoglikelihoodSingleTree, + theta: np.ndarray, + sampling_rate: float, + n_mutations: int, + all_mut: set[int], ) -> float: """ Calculates loglikelihood `log P(tree | theta)`. @@ -151,25 +149,32 @@ def loglikelihood( """ # TODO(Pawel): this is part of https://github.com/cbg-ethz/pMHN/issues/15 # It can be implemented in any way. - subtrees_size = len(tree._subtrees_dict) + subtrees_size = len(tree._genotype_subtree_node_map) x = np.zeros(subtrees_size) x[0] = 1 - n_mutations = len(theta) - all_mut = set(i + 1 for i in range(n_mutations)) - for i, (subtree_i, subtree_size_i) in enumerate(tree._subtrees_dict.items()): - V_col = {} + genotype_lists = [] + for genotype in tree._genotype_subtree_node_map.keys(): + genotype_lists.append(np.array([item[1] for item in genotype])) + for genotype_i, (i, subtree_size_i) in tree._genotype_subtree_node_map.items(): + V_col = [] V_diag = 0.0 - for j, (subtree_j, subtree_size_j) in enumerate( - tree._subtrees_dict.items() - ): + for j, subtree_size_j in tree._genotype_subtree_node_map.values(): if subtree_size_i - subtree_size_j == 1: - V_col[j] = -self.off_diag_entry(subtree_j, subtree_i, theta) + V_col.append( + ( + j, + -self.off_diag_entry( + tree, genotype_lists[j], genotype_lists[i], theta + ), + ) + ) elif i == j: - V_diag = sampling_rate - self.diag_entry(subtree_i, theta, all_mut) - for index, val in V_col.items(): + V_diag = sampling_rate - self.diag_entry( + tree, genotype_i, theta, all_mut + ) + for index, val in V_col: x[i] -= val * x[index] x[i] /= V_diag - return np.log(x[-1] + self._jitter) + np.log(sampling_rate) def gradient(self, tree: Node, theta: np.ndarray) -> np.ndarray: diff --git a/src/pmhn/_trees/_backend_new_v1.py b/src/pmhn/_trees/_backend_new_v1.py deleted file mode 100644 index 8739765..0000000 --- a/src/pmhn/_trees/_backend_new_v1.py +++ /dev/null @@ -1,217 +0,0 @@ -from typing import Protocol - - -import numpy as np -from scipy.sparse.linalg import spsolve_triangular -from scipy.sparse import csr_matrix -from pmhn._trees._interfaces import Tree -from pmhn._trees._tree_utils_new import create_all_subtrees, bfs_compare -from anytree import Node, LevelOrderGroupIter - - -class LoglikelihoodSingleTree: - def __init__(self, tree): - self._subtrees_dict = create_all_subtrees(tree) - - _subtrees_dict: dict[Node, int] - - -class IndividualTreeMHNBackendInterface(Protocol): - def loglikelihood( - self, - tree: Tree, - theta: np.ndarray, - ) -> float: - """Calculates loglikelihood `log P(tree | theta)`. - - Args: - tree: a tree - theta: real-valued (i.e., log-theta) matrix, - shape (n_mutations, n_mutations) - - Returns: - loglikelihood of the tree - """ - raise NotImplementedError - - def gradient( - self, - tree: Tree, - theta: np.ndarray, - ) -> np.ndarray: - """Calculates the partial derivatives of `log P(tree | theta)` - with respect to `theta`. - - Args: - tree: a tree - theta: real-valued matrix, - shape (n_mutations, n_mutatations) - - Returns: - gradient `d log P(tree | theta) / d theta`, - shape (n_mutations, n_mutations) - """ - raise NotImplementedError - - def gradient_and_loglikelihood( - self, tree: Tree, theta: np.ndarray - ) -> tuple[np.ndarray, float]: - """Returns the gradient and the loglikelihood. - - Note: - This function may be faster than calling `gradient` and `loglikelihood` - separately. - """ - return self.gradient(tree, theta), self.loglikelihood(tree, theta) - - -class OriginalTreeMHNBackend(IndividualTreeMHNBackendInterface): - def __init__(self, jitter: float = 1e-10): - self._jitter = jitter - - _jitter: float - - def create_V_Mat( - self, - tree: LoglikelihoodSingleTree, - theta: np.ndarray, - sampling_rate: float, - all_mut: set[int], - ) -> np.ndarray: - """Calculates the V matrix. - - Args: - tree: a tree - theta: real-valued (i.e., log-theta) matrix, - shape (n_mutations, n_mutations) - sampling_rate: a scalar of type float - all_mut: set containing all possible mutations - Returns: - the V matrix. - """ - - subtrees_size = len(tree._subtrees_dict) - Q = np.zeros((subtrees_size, subtrees_size)) - for i, (subtree_i, subtree_size_i) in enumerate(tree._subtrees_dict.items()): - for j, (subtree_j, subtree_size_j) in enumerate( - tree._subtrees_dict.items() - ): - if i == j: - Q[i][j] = self.diag_entry(subtree_i, theta, all_mut) - elif subtree_size_j - subtree_size_i == 1: - Q[i][j] = self.off_diag_entry(subtree_i, subtree_j, theta) - V = np.eye(subtrees_size) * sampling_rate - Q - return V - - def diag_entry(self, tree: Node, theta: np.ndarray, all_mut: set[int]) -> float: - """ - Calculates a diagonal entry of the V matrix. - - Args: - tree: a tree - theta: real-valued (i.e., log-theta) matrix, - shape (n_mutations, n_mutations) - all_mut: set containing all possible mutations - Returns: - the diagonal entry of the V matrix corresponding to tree - """ - lamb_sum = 0 - - for level in LevelOrderGroupIter(tree): - for node in level: - tree_mutations = {n.name for n in node.path}.union( - {c.name for c in node.children} - ) - exit_mutations = set(all_mut).difference(tree_mutations) - - for mutation in exit_mutations: - lamb = 0 - exit_subclone = { - anc.name for anc in node.path if anc.parent is not None - }.union({mutation}) - for j in exit_subclone: - lamb += theta[mutation - 1][j - 1] - lamb = np.exp(lamb) - lamb_sum -= lamb - - return lamb_sum - - def off_diag_entry(self, tree1: Node, tree2: Node, theta: np.ndarray) -> float: - """ - Calculates an off-diagonal entry of the V matrix. - - Args: - tree1: the first tree - tree2: the second tree - theta: real-valued (i.e., log-theta) matrix, - shape (n_mutations, n_mutations) - Returns: - the off-diagonal entry of the V matrix corresponding to tree1 and tree2 - """ - exit_node = bfs_compare(tree1, tree2) - lamb = 0 - if exit_node is None: - return lamb - else: - for j in [ - node.name # type: ignore - for node in exit_node.path - if node.parent is not None - ]: - lamb += theta[exit_node.name - 1][j - 1] - lamb = np.exp(lamb) - return float(lamb) - - def loglikelihood( - self, tree: LoglikelihoodSingleTree, theta: np.ndarray, sampling_rate: float - ) -> float: - """ - Calculates loglikelihood `log P(tree | theta)`. - - Args: - tree: a tree - theta: real-valued (i.e., log-theta) matrix, - shape (n_mutations, n_mutations) - sampling_rate: a scalar of type float - Returns: - loglikelihood of the tree - """ - # TODO(Pawel): this is part of https://github.com/cbg-ethz/pMHN/issues/15 - # It can be implemented in any way. - n_mutations = len(theta) - all_mut = set(i + 1 for i in range(n_mutations)) - V = self.create_V_Mat( - tree=tree, theta=theta, sampling_rate=sampling_rate, all_mut=all_mut - ) - V_size = V.shape[0] - b = np.zeros(V_size) - b[0] = 1 - V_transposed = V.transpose() - V_csr = csr_matrix(V_transposed) - x = spsolve_triangular(V_csr, b, lower=True) - - return np.log(x[V_size - 1] + self._jitter) + np.log(sampling_rate) - - def gradient(self, tree: Node, theta: np.ndarray) -> np.ndarray: - """Calculates the partial derivatives of `log P(tree | theta)` - with respect to `theta`. - - Args: - tree: a tree - theta: real-valued matrix, shape (n_mutations, n_mutatations) - - Returns: - gradient `d log P(tree | theta) / d theta`, - shape (n_mutations, n_mutations) - """ - # TODO(Pawel): This is part of - # https://github.com/cbg-ethz/pMHN/issues/18, - # but it is *not* a priority. - # We will try to do the modelling as soon as possible, - # starting with a sequential Monte Carlo sampler - # and Metropolis transitions. - # Only after initial experiments - # (we will probably see that it's not scalable), - # we'll consider switching to Hamiltonian Monte Carlo, - # which requires gradients. - raise NotImplementedError diff --git a/src/pmhn/_trees/_tree_utils.py b/src/pmhn/_trees/_tree_utils.py index c292c89..ba0ec75 100644 --- a/src/pmhn/_trees/_tree_utils.py +++ b/src/pmhn/_trees/_tree_utils.py @@ -55,7 +55,7 @@ def all_combinations_of_elements(*lists): yield list(element_combination) -def create_subtree(original_root: Node, nodes_list: list[Node]) -> Optional[Node]: +def create_subtree(original_root: Node, nodes_list: list[Node]) -> Node: """ Creates a subtree given a list of nodes and the root node. @@ -71,27 +71,30 @@ def create_subtree(original_root: Node, nodes_list: list[Node]) -> Optional[Node if node in nodes_list: parent_node = next((n for n in nodes_list if n is node.parent), None) nodes_dict[node] = Node(node.name, parent=nodes_dict.get(parent_node)) + return nodes_dict[original_root] - return nodes_dict.get(original_root) - -def get_subtrees(node: Node) -> list[list[Node]]: +def get_subtrees(node: Node, memo: Optional[dict] = None) -> list[list[Node]]: """ Creates a list of all subtrees of a tree. - A recursive approach is employed: If one knows the subtrees of the - children of the root node, then one can find all combinations of - the subtrees of the children and add the root node to each one - of these combinations, this way one obtains all subtrees of the original tree. + Args: node: the root node Returns: - a list of subtrees + a list of subtrees where each subtree is a list of nodes """ + if memo is None: + memo = {} + + if node in memo: + return memo[node] + if not node.children: + memo[node] = [[node]] return [[node]] - child_subtrees = [get_subtrees(child) for child in node.children] + child_subtrees = [get_subtrees(child, memo) for child in node.children] combined_subtrees = all_combinations_of_elements(*child_subtrees) @@ -103,36 +106,39 @@ def get_subtrees(node: Node) -> list[list[Node]]: ] result_subtrees.append(subtree_with_root) + memo[node] = result_subtrees + return result_subtrees -def create_all_subtrees(root: Node) -> list[Node]: +def create_all_subtrees(root: Node) -> dict[Node, int]: """ - Creates a list of subtrees and sorts the list in ascending subtree size. + Creates a dictionary where each key is a subtree, + and each value is the size of that subtree. Args: root: the root node Returns: - the final list of subtrees + A dictionary mapping subtrees to their sizes """ all_node_lists = get_subtrees(root) all_node_lists = sorted(all_node_lists, key=len) - all_subtrees = [] - for subtree in all_node_lists: - all_subtrees.append(create_subtree(root, subtree)) - return all_subtrees + all_subtrees_dict = { + create_subtree(root, node_list): len(node_list) for node_list in all_node_lists + } + return all_subtrees_dict -def get_lineage(node: Node) -> list[int]: +def get_lineage(node: Node) -> tuple[int]: """ - Creates a list of the names of the nodes that - are in the lineage of input node. + Creates a tuple of the names of the nodes that + are in the lineage of the input node. Args: node: a node Returns: the lineage of a node """ - return [ancestor.name for ancestor in node.path] # type: ignore + return tuple(ancestor.name for ancestor in node.path) # type: ignore def check_equality(tree1: Optional[Node], tree2: Optional[Node]) -> bool: @@ -152,8 +158,8 @@ def check_equality(tree1: Optional[Node], tree2: Optional[Node]) -> bool: if len(tree1.descendants) != len(tree2.descendants): return False for nodes1, nodes2 in zip(iter1, iter2): - set_nodes1_lineages = {tuple(get_lineage(node)) for node in nodes1} - set_nodes2_lineages = {tuple(get_lineage(node)) for node in nodes2} + set_nodes1_lineages = {get_lineage(node) for node in nodes1} + set_nodes2_lineages = {get_lineage(node) for node in nodes2} additional_nodes_lineages = set_nodes2_lineages ^ set_nodes1_lineages if len(additional_nodes_lineages) != 0: return False @@ -163,40 +169,48 @@ def check_equality(tree1: Optional[Node], tree2: Optional[Node]) -> bool: def bfs_compare(tree1: Node, tree2: Node) -> Optional[Node]: """ - Checks if tree1 is a subtree of tree2 and is smaller in - size by one. + Checks if tree1 is a subtree of tree2 with the assumption + that tree2 is larger than the first tree by one. Args: tree1: the first tree tree2: the second tree Returns: the additional node in the second tree if available, otherwise None. + """ + diff_count = 0 iter1 = list(LevelOrderGroupIter(tree1)) iter2 = list(LevelOrderGroupIter(tree2)) exit_node = None - if len(list(tree2.descendants)) - len(list(tree1.descendants)) != 1: - return None - for nodes1, nodes2 in zip(iter1, iter2): - set_nodes1_lineages = {tuple(get_lineage(node)) for node in nodes1} - set_nodes2_lineages = {tuple(get_lineage(node)) for node in nodes2} + + for level, (nodes1, nodes2) in enumerate(zip(iter1, iter2)): + dict_nodes1_lineages = {node: get_lineage(node) for node in nodes1} + dict_nodes2_lineages = {node: get_lineage(node) for node in nodes2} + set_nodes1_lineages = set(dict_nodes1_lineages.values()) + set_nodes2_lineages = set(dict_nodes2_lineages.values()) additional_nodes_lineages = set_nodes2_lineages ^ set_nodes1_lineages diff_count += len(additional_nodes_lineages) if diff_count == 1 and exit_node is None: additional_node_lineage = additional_nodes_lineages.pop() + for node in nodes1: - if tuple(get_lineage(node)) == additional_node_lineage: + if dict_nodes1_lineages[node] == additional_node_lineage: return None for node in nodes2: - if tuple(get_lineage(node)) == additional_node_lineage: + if dict_nodes2_lineages[node] == additional_node_lineage: exit_node = node - if diff_count > 1: - return None - if len(iter1) < len(iter2): + break + + if diff_count > 1: + return None + + if diff_count == 0: return iter2[-1][0] + return exit_node diff --git a/src/pmhn/_trees/_tree_utils_geno.py b/src/pmhn/_trees/_tree_utils_geno.py new file mode 100644 index 0000000..6bc0965 --- /dev/null +++ b/src/pmhn/_trees/_tree_utils_geno.py @@ -0,0 +1,165 @@ +from anytree import Node, LevelOrderGroupIter +from itertools import combinations, product +from typing import Optional + + +def all_combinations_of_elements(*lists): + """ + Takes a variable number of lists as input and returns a generator that yields + all possible combinations of the input lists. In our use case: It takes a list + of lists of subtrees as input where a subtree itself is a list of nodes and + outputs all possible combinations of the lists of subtrees. + + For instance, if we have the following tree: + + 0 + / | \ + 1 3 2 + | + 2 + + and assumed that we know the list of subtrees for the trees: + + 1 + | -> list of subtrees: [[1], [1, 2]] + 2 + + 3 -> list of subtrees: [[3]] + + and + + 2 -> list of subtrees: [[2]] + + , we can find the subtrees of the original tree by looking at + all possible combinations of the list of subtrees for the trees above + and add the root node (0) to each combination (this is done in the + get_subtrees function). + + So the input would be [[[1], [1, 2]],[[3]], [[2]]] + + The generator would yield the following combinations one at a time: + [[1]], [[1, 2]], [[3]], [[2]], [[1], [3]], [[1, 2], [3]], [[1], [2]], + [[1, 2], [2]], [[3], [2]], [[1], [3], [2]], [[1, 2], [3], [2]] + + Args: + *lists: any number of lists + + Returns: + A generator that yields all combinations of the input lists. + + """ + n = len(lists) + for r in range(1, n + 1): + for list_combination in combinations(lists, r): + for element_combination in product(*list_combination): + yield list(element_combination) + + +def create_subtree( + original_root: Node, nodes_list: list[Node], all_nodes_root: list[Node] +) -> Node: + nodes_dict = {} + node_to_parent = {node: node.parent for node in nodes_list} + + for node in all_nodes_root: + if node in nodes_list: + parent_node = node_to_parent.get(node) + nodes_dict[node] = Node(node.name, parent=nodes_dict.get(parent_node)) + + return nodes_dict[original_root] + + +def get_subtrees(node: Node, memo: Optional[dict] = None) -> list[list[Node]]: + """ + Creates a list of all subtrees of a tree. + + + Args: + node: the root node + Returns: + a list of subtrees where each subtree is a list of nodes + """ + if memo is None: + memo = {} + + if node in memo: + return memo[node] + + if not node.children: + memo[node] = [[node]] + return [[node]] + + child_subtrees = [get_subtrees(child, memo) for child in node.children] + + combined_subtrees = all_combinations_of_elements(*child_subtrees) + + result_subtrees = [[node]] + [ + [node] + [item for sublist in combination for item in sublist] + for combination in combined_subtrees + ] + + memo[node] = result_subtrees + + return result_subtrees + + +def get_lineage(node: Node) -> tuple[int]: + """ + Creates a tuple of the names of the nodes that + are in the lineage of the input node. + Args: + node: a node + Returns: + the lineage of a node + """ + return tuple(ancestor.name for ancestor in node.path) # type: ignore + + +def create_index_subclone_maps( + tree: Node, +) -> tuple[dict[int, tuple[int]], dict[tuple[int], int]]: + index_subclone_map = {} + subclone_index_map = {} + index = 0 + for level in LevelOrderGroupIter(tree): + for node in level: + index_subclone_map[index] = get_lineage(node) + subclone_index_map[get_lineage(node)] = index + index += 1 + return index_subclone_map, subclone_index_map + + +def create_genotype( + size: int, subtree: Node, subclone_index_map: dict[tuple[int], int] +) -> tuple[tuple[Optional[Node], int], ...]: + x = [(Node(None), int(0))] * size + for level in LevelOrderGroupIter(subtree): + for node in level: + lineage = get_lineage(node) + x[subclone_index_map[lineage]] = (node, 1) + return tuple(x) + + +def create_genotype_subtree_map( + root: Node, +) -> tuple[dict[tuple[tuple[Node, int]], tuple[int, int]], dict[int, tuple[int]]]: + index_subclone_map, subclone_index_map = create_index_subclone_maps(root) + subtree_genotype_node_map = {} + all_node_lists = get_subtrees(root) + all_nodes_root = all_node_lists[-1] + all_node_lists_with_len = [ + (node_list, len(node_list)) for node_list in all_node_lists + ] + size = len(all_node_lists) + for index, (node_list, node_list_len) in enumerate(all_node_lists_with_len): + subtree = create_subtree(root, node_list, all_nodes_root) + genotype_node = create_genotype(size, subtree, subclone_index_map) + subtree_genotype_node_map[genotype_node] = (index, node_list_len) + return subtree_genotype_node_map, index_subclone_map + + +if __name__ == "__main__": + A = Node("0") + B = Node("1", parent=A) + C = Node("3", parent=A) + D = Node("4", parent=B) diff --git a/src/pmhn/_trees/_tree_utils_new.py b/src/pmhn/_trees/_tree_utils_new.py deleted file mode 100644 index ba0ec75..0000000 --- a/src/pmhn/_trees/_tree_utils_new.py +++ /dev/null @@ -1,230 +0,0 @@ -from anytree import Node, RenderTree, LevelOrderGroupIter -from itertools import combinations, product -from typing import Optional - - -def all_combinations_of_elements(*lists): - """ - Takes a variable number of lists as input and returns a generator that yields - all possible combinations of the input lists. In our use case: It takes a list - of lists of subtrees as input where a subtree itself is a list of nodes and - outputs all possible combinations of the lists of subtrees. - - For instance, if we have the following tree: - - 0 - / | \ - 1 3 2 - | - 2 - - and assumed that we know the list of subtrees for the trees: - - 1 - | -> list of subtrees: [[1], [1, 2]] - 2 - - 3 -> list of subtrees: [[3]] - - and - - 2 -> list of subtrees: [[2]] - - , we can find the subtrees of the original tree by looking at - all possible combinations of the list of subtrees for the trees above - and add the root node (0) to each combination (this is done in the - get_subtrees function). - - So the input would be [[[1], [1, 2]],[[3]], [[2]]] - - The generator would yield the following combinations one at a time: - [[1]], [[1, 2]], [[3]], [[2]], [[1], [3]], [[1, 2], [3]], [[1], [2]], - [[1, 2], [2]], [[3], [2]], [[1], [3], [2]], [[1, 2], [3], [2]] - - Args: - *lists: any number of lists - - Returns: - A generator that yields all combinations of the input lists. - - """ - n = len(lists) - for r in range(1, n + 1): - for list_combination in combinations(lists, r): - for element_combination in product(*list_combination): - yield list(element_combination) - - -def create_subtree(original_root: Node, nodes_list: list[Node]) -> Node: - """ - Creates a subtree given a list of nodes and the root node. - - Args: - original_root: the root node - nodes_list: a list of nodes - Returns: - a subtree - """ - nodes_dict = {} - - for node in [original_root] + list(original_root.descendants): - if node in nodes_list: - parent_node = next((n for n in nodes_list if n is node.parent), None) - nodes_dict[node] = Node(node.name, parent=nodes_dict.get(parent_node)) - return nodes_dict[original_root] - - -def get_subtrees(node: Node, memo: Optional[dict] = None) -> list[list[Node]]: - """ - Creates a list of all subtrees of a tree. - - - Args: - node: the root node - Returns: - a list of subtrees where each subtree is a list of nodes - """ - if memo is None: - memo = {} - - if node in memo: - return memo[node] - - if not node.children: - memo[node] = [[node]] - return [[node]] - - child_subtrees = [get_subtrees(child, memo) for child in node.children] - - combined_subtrees = all_combinations_of_elements(*child_subtrees) - - result_subtrees = [] - result_subtrees.append([node]) - for combination in combined_subtrees: - subtree_with_root = [node] + [ - item for sublist in combination for item in sublist - ] - result_subtrees.append(subtree_with_root) - - memo[node] = result_subtrees - - return result_subtrees - - -def create_all_subtrees(root: Node) -> dict[Node, int]: - """ - Creates a dictionary where each key is a subtree, - and each value is the size of that subtree. - - Args: - root: the root node - Returns: - A dictionary mapping subtrees to their sizes - """ - all_node_lists = get_subtrees(root) - all_node_lists = sorted(all_node_lists, key=len) - all_subtrees_dict = { - create_subtree(root, node_list): len(node_list) for node_list in all_node_lists - } - return all_subtrees_dict - - -def get_lineage(node: Node) -> tuple[int]: - """ - Creates a tuple of the names of the nodes that - are in the lineage of the input node. - Args: - node: a node - Returns: - the lineage of a node - """ - return tuple(ancestor.name for ancestor in node.path) # type: ignore - - -def check_equality(tree1: Optional[Node], tree2: Optional[Node]) -> bool: - """ - Checks if tree1 and tree2 are identical, note that direct - comparison with == is not possible. - - Args: - tree1: the first tree - tree2: the second tree - Returns: - (in)equality of the trees - """ - iter1 = list(LevelOrderGroupIter(tree1)) - iter2 = list(LevelOrderGroupIter(tree2)) - if tree1 is not None and tree2 is not None: - if len(tree1.descendants) != len(tree2.descendants): - return False - for nodes1, nodes2 in zip(iter1, iter2): - set_nodes1_lineages = {get_lineage(node) for node in nodes1} - set_nodes2_lineages = {get_lineage(node) for node in nodes2} - additional_nodes_lineages = set_nodes2_lineages ^ set_nodes1_lineages - if len(additional_nodes_lineages) != 0: - return False - - return True - - -def bfs_compare(tree1: Node, tree2: Node) -> Optional[Node]: - """ - Checks if tree1 is a subtree of tree2 with the assumption - that tree2 is larger than the first tree by one. - - Args: - tree1: the first tree - tree2: the second tree - Returns: - the additional node in the second tree if available, otherwise None. - - """ - - diff_count = 0 - iter1 = list(LevelOrderGroupIter(tree1)) - iter2 = list(LevelOrderGroupIter(tree2)) - exit_node = None - - for level, (nodes1, nodes2) in enumerate(zip(iter1, iter2)): - dict_nodes1_lineages = {node: get_lineage(node) for node in nodes1} - dict_nodes2_lineages = {node: get_lineage(node) for node in nodes2} - set_nodes1_lineages = set(dict_nodes1_lineages.values()) - set_nodes2_lineages = set(dict_nodes2_lineages.values()) - additional_nodes_lineages = set_nodes2_lineages ^ set_nodes1_lineages - diff_count += len(additional_nodes_lineages) - - if diff_count == 1 and exit_node is None: - additional_node_lineage = additional_nodes_lineages.pop() - - for node in nodes1: - if dict_nodes1_lineages[node] == additional_node_lineage: - return None - - for node in nodes2: - if dict_nodes2_lineages[node] == additional_node_lineage: - exit_node = node - break - - if diff_count > 1: - return None - - if diff_count == 0: - return iter2[-1][0] - - return exit_node - - -if __name__ == "__main__": - A = Node("0") - B = Node("1", parent=A) - C = Node("3", parent=A) - D = Node("3", parent=B) - - print(RenderTree(A)) - print("\n") - all_subtrees = create_all_subtrees(A) - i = 1 - for subtree in all_subtrees: - print(f"{i}. ") - print(RenderTree(subtree)) - i += 1 diff --git a/tests/trees/test_likelihood.py b/tests/trees/test_likelihood.py deleted file mode 100644 index 5f98ac4..0000000 --- a/tests/trees/test_likelihood.py +++ /dev/null @@ -1,419 +0,0 @@ -from pmhn._trees._backend import OriginalTreeMHNBackend -from pmhn._trees._tree_utils import create_all_subtrees -from anytree import Node -import numpy as np - - -def test_create_V_Mat(): - """ - Checks if create_V_Mat is implemented correctly. - - tree: - 0 - / \ - 1 3 - / - 3 - """ - - true_Q = np.array( - [ - [ - -(np.exp(-1.41) + np.exp(-2.26) + np.exp(-2.55)), - np.exp(-1.41), - np.exp(-2.55), - 0, - 0, - 0, - ], - [ - 0, - -( - np.exp(-2.26) - + np.exp(-2.55) - + np.exp(-1.12 - 2.26) - + np.exp(1 - 2.55) - ), - 0, - np.exp(1 - 2.55), - np.exp(-2.55), - 0, - ], - [ - 0, - 0, - -( - np.exp(-1.41) - + np.exp(-2.26) - + np.exp(-1.41 + 3) - + np.exp(-2.26 + 2) - ), - 0, - np.exp(-1.41), - 0, - ], - [ - 0, - 0, - 0, - -( - np.exp(-2.26) - + np.exp(-2.55) - + np.exp(-2.26 - 1.12) - + np.exp(-2.26 - 1.12 + 2) - ), - 0, - np.exp(-2.55), - ], - [ - 0, - 0, - 0, - 0, - -( - np.exp(-2.26) - + np.exp(-2.26 - 1.12) - + np.exp(-2.55 + 1) - + np.exp(-1.41 + 3) - + np.exp(-2.26 + 2) - ), - np.exp(-2.55 + 1), - ], - [ - 0, - 0, - 0, - 0, - 0, - -( - np.exp(-2.26) - + np.exp(-2.26 - 1.12) - + np.exp(-2.26 + 2 - 1.12) - + np.exp(-1.41 + 3) - + np.exp(-2.26 + 2) - ), - ], - ] - ) - A = Node(0) - B = Node(1, parent=A) - Node(3, parent=A) - Node(3, parent=B) - subtrees = create_all_subtrees(A) - subtrees_size = len(subtrees) - sampling_rate = 1.0 - true_V = np.eye(subtrees_size) * sampling_rate - true_Q - backend = OriginalTreeMHNBackend() - theta = np.array([[-1.41, 2, 3], [-1.12, -2.26, 2], [1, -0.86, -2.55]]) - - V = backend.create_V_Mat(A, theta, sampling_rate) - - assert np.allclose(V, true_V, atol=1e-8) - - -def test_diag_entry(): - r""" - - Checks if the diagonal values of the V matrix are calculated correctly. - - 0 - / | \ - 2 1 3 - | | - 3 3 - - - augmented tree: - - - 0 - / | \ - 2 1 3 - |\ |\ |\ - 3 1 3 2 1 2 - | | - 1 2 - - """ - A = Node(0) - B = Node(2, parent=A) - D = Node(1, parent=A) - Node(3, parent=A) - Node(3, parent=B) - Node(3, parent=D) - theta = np.array([[-1.41, 2, 3], [-1.12, -2.26, 2], [1, -0.86, -2.55]]) - true_diag_entry = -( - np.exp(-1.41 + 2) - + np.exp(-2.26 - 1.12) - + np.exp(-1.41 + 3) - + np.exp(-2.26 + 2) - + np.exp(-1.41 + 2 + 3) - + np.exp(-2.26 + 2 - 1.12) - ) - backend = OriginalTreeMHNBackend() - diag_entry = backend.diag_entry(A, theta) - - assert np.allclose(diag_entry, true_diag_entry, atol=1e-8) - - -def test_off_diag_entry_valid(): - r""" - Checks if the off-diagonal entries of the V matrix - are calculated correctly. - - first tree: - 0 - / | \ - 2 1 3 - | - 3 - - second tree: - 0 - / | \ - 2 1 3 - | | - 3 3 - - - """ - - theta = np.array([[-1.41, 2, 3], [-1.12, -2.26, 2], [1, -0.86, -2.55]]) - # first tree - A_1 = Node(0) - B_1 = Node(2, parent=A_1) - Node(1, parent=A_1) - Node(3, parent=A_1) - Node(3, parent=B_1) - - # second tree - A_2 = Node(0) - B_2 = Node(2, parent=A_2) - D_2 = Node(1, parent=A_2) - Node(3, parent=A_2) - Node(3, parent=B_2) - Node(3, parent=D_2) - - true_off_diag_entry = np.exp(-2.55 + 1) - backend = OriginalTreeMHNBackend() - off_diag_entry = backend.off_diag_entry(A_1, A_2, theta) - - assert np.allclose(off_diag_entry, true_off_diag_entry, atol=1e-8) - - -def test_off_diag_entry_invalid_size(): - r""" - - Checks if off_diag_entry successfully returns 0 when the size is invalid - (i.e the first tree is not smaller than the second tree by one). - - first tree: - 0 - / | \ - 2 1 3 - | - 3 - - second tree: - 0 - / | \ - 2 1 3 - | | | - 3 3 2 - - - """ - - # first tree - A_1 = Node(0) - B_1 = Node(2, parent=A_1) - Node(1, parent=A_1) - Node(3, parent=A_1) - Node(3, parent=B_1) - - # second tree - A_2 = Node(0) - B_2 = Node(2, parent=A_2) - C_2 = Node(1, parent=A_2) - D_2 = Node(3, parent=A_2) - Node(3, parent=B_2) - Node(3, parent=C_2) - Node(2, parent=D_2) - - theta = np.array([[-1.41, 2, 3], [-1.12, -2.26, 2], [1, -0.86, -2.55]]) - backend = OriginalTreeMHNBackend() - - assert backend.off_diag_entry(A_1, A_2, theta) == 0 - - -def test_off_diag_entry_not_subset(): - r""" - Checks if the off_diag_entry succesfully returns 0 when the - first tree is not a subtree of the second tree. - - first tree: - 0 - / | \ - 2 1 3 - | - 3 - - second tree: - 0 - / | \ - 2 1 3 - | | - 3 2 - - - """ - # first tree - A_1 = Node(0) - B_1 = Node(2, parent=A_1) - Node(1, parent=A_1) - Node(3, parent=A_1) - Node(3, parent=B_1) - - # second tree - A_2 = Node(0) - Node(2, parent=A_2) - C_2 = Node(1, parent=A_2) - D_2 = Node(3, parent=A_2) - Node(3, parent=C_2) - Node(2, parent=D_2) - - theta = np.array([[-1.41, 2, 3], [-1.12, -2.26, 2], [1, -0.86, -2.55]]) - backend = OriginalTreeMHNBackend() - - assert backend.off_diag_entry(A_1, A_2, theta) == 0 - - -def test_likelihood_small_tree(): - """ - Checks if the likelihood of a small tree is calculated - correctly. - - tree: - 0 - | - 2 - | - 7 - """ - - A = Node(0) - B = Node(7, parent=A) - Node(2, parent=B) - theta = np.array( - [ - [-1.41, 0.00, 0.00, 4.91, 1.03, 0.00, -1.91, -0.74, -1.35, 1.48], - [-1.12, -2.26, 0.00, 0.82, 0.00, 0.00, 1.16, 0.00, -1.62, 0.00], - [0.00, -0.86, -2.55, 1.58, 0.00, 0.00, 1.02, -2.70, 0.00, 0.68], - [0.00, 0.00, 0.00, -3.69, 0.00, 0.00, -0.95, 1.42, 0.00, -1.01], - [-3.08, -1.42, -3.14, 0.00, -3.95, 3.90, -1.46, -2.00, 0.00, 2.87], - [-2.24, 0.00, 0.00, 0.00, 0.00, -2.38, -2.13, 1.50, 0.00, 1.35], - [0.00, 0.00, 0.00, 0.00, 1.52, 0.00, -1.79, 0.00, 0.00, 0.00], - [1.69, 0.76, 0.00, 1.29, 1.73, -0.82, -1.38, -4.65, 0.92, 0.00], - [-1.22, 0.00, 0.00, 0.00, 0.65, -1.14, 0.00, 0.00, -3.25, 0.00], - [0.97, 1.75, 0.00, -3.66, -1.28, 0.00, 1.66, 0.00, 0.00, -3.03], - ] - ) - sampling_rate = 1.0 - - backend = OriginalTreeMHNBackend() - log_value = backend.loglikelihood(A, theta, sampling_rate) - - assert np.allclose(log_value, -5.793104, atol=1e-5) - - -def test_likelihood_medium_tree(): - """ - Checks if the likelihood of a medium-sized tree is calculated - correctly. - - tree: - 0 - / \ - 2 3 - | | - 1 10 - | - 10 - """ - - A = Node(0) - B = Node(2, parent=A) - C = Node(3, parent=A) - D = Node(1, parent=B) - Node(10, parent=C) - Node(10, parent=D) - theta = np.array( - [ - [-1.41, 0.00, 0.00, 4.91, 1.03, 0.00, -1.91, -0.74, -1.35, 1.48], - [-1.12, -2.26, 0.00, 0.82, 0.00, 0.00, 1.16, 0.00, -1.62, 0.00], - [0.00, -0.86, -2.55, 1.58, 0.00, 0.00, 1.02, -2.70, 0.00, 0.68], - [0.00, 0.00, 0.00, -3.69, 0.00, 0.00, -0.95, 1.42, 0.00, -1.01], - [-3.08, -1.42, -3.14, 0.00, -3.95, 3.90, -1.46, -2.00, 0.00, 2.87], - [-2.24, 0.00, 0.00, 0.00, 0.00, -2.38, -2.13, 1.50, 0.00, 1.35], - [0.00, 0.00, 0.00, 0.00, 1.52, 0.00, -1.79, 0.00, 0.00, 0.00], - [1.69, 0.76, 0.00, 1.29, 1.73, -0.82, -1.38, -4.65, 0.92, 0.00], - [-1.22, 0.00, 0.00, 0.00, 0.65, -1.14, 0.00, 0.00, -3.25, 0.00], - [0.97, 1.75, 0.00, -3.66, -1.28, 0.00, 1.66, 0.00, 0.00, -3.03], - ] - ) - sampling_rate = 1.0 - - backend = OriginalTreeMHNBackend() - log_value = backend.loglikelihood(A, theta, sampling_rate) - - assert np.allclose(log_value, -14.729560, atol=1e-5) - - -def test_likelihood_large_tree(): - """ - Checks if the likelihood of a large tree is calculated - correctly. - - tree: - 0 - / \ - 3 6 - / \ - 4 5 - | / \ - 1 1 7 - | / \ - 7 3 10 - """ - - A = Node(0) - Node(3, parent=A) - C = Node(6, parent=A) - D = Node(4, parent=C) - E = Node(5, parent=C) - Node(1, parent=D) - G = Node(1, parent=E) - H = Node(7, parent=E) - Node(7, parent=G) - Node(3, parent=H) - Node(10, parent=H) - theta = np.array( - [ - [-1.41, 0.00, 0.00, 4.91, 1.03, 0.00, -1.91, -0.74, -1.35, 1.48], - [-1.12, -2.26, 0.00, 0.82, 0.00, 0.00, 1.16, 0.00, -1.62, 0.00], - [0.00, -0.86, -2.55, 1.58, 0.00, 0.00, 1.02, -2.70, 0.00, 0.68], - [0.00, 0.00, 0.00, -3.69, 0.00, 0.00, -0.95, 1.42, 0.00, -1.01], - [-3.08, -1.42, -3.14, 0.00, -3.95, 3.90, -1.46, -2.00, 0.00, 2.87], - [-2.24, 0.00, 0.00, 0.00, 0.00, -2.38, -2.13, 1.50, 0.00, 1.35], - [0.00, 0.00, 0.00, 0.00, 1.52, 0.00, -1.79, 0.00, 0.00, 0.00], - [1.69, 0.76, 0.00, 1.29, 1.73, -0.82, -1.38, -4.65, 0.92, 0.00], - [-1.22, 0.00, 0.00, 0.00, 0.65, -1.14, 0.00, 0.00, -3.25, 0.00], - [0.97, 1.75, 0.00, -3.66, -1.28, 0.00, 1.66, 0.00, 0.00, -3.03], - ] - ) - sampling_rate = 1.0 - - backend = OriginalTreeMHNBackend() - log_value = backend.loglikelihood(A, theta, sampling_rate) - - assert np.allclose(log_value, -22.288420, atol=1e-5) diff --git a/warmup/likelihood/R_py_loglikelihood_comparison.py b/warmup/likelihood/R_py_loglikelihood_comparison.py index a1d079a..b38a94d 100644 --- a/warmup/likelihood/R_py_loglikelihood_comparison.py +++ b/warmup/likelihood/R_py_loglikelihood_comparison.py @@ -1,8 +1,9 @@ import pandas as pd import pmhn._trees._io as io -from pmhn._trees._backend import OriginalTreeMHNBackend +from pmhn._trees._backend import OriginalTreeMHNBackend, LoglikelihoodSingleTree import csv import numpy as np +import time def csv_to_numpy(file_path): @@ -48,20 +49,24 @@ def csv_to_numpy(file_path): # calculate loglikelihoods log_vec_py_AML = np.empty(len(trees_AML)) log_vec_py_500 = np.empty(len(trees_500)) +start_time = time.time() backend = OriginalTreeMHNBackend() - for idx, tree in trees_AML.items(): print(f"Processing tree {idx} of {len(trees_AML)}") - log_value = backend.loglikelihood(tree, theta_AML, sampling_rate) + tree_log = LoglikelihoodSingleTree(tree) + log_value = backend.loglikelihood(tree_log, theta_AML, sampling_rate) log_vec_py_AML[idx - 1] = log_value print(f"log_value: {log_value}") for idx, tree in trees_500.items(): print(f"Processing tree {idx} of {len(trees_500)}") - log_value = backend.loglikelihood(tree, theta_500, sampling_rate) + tree_log = LoglikelihoodSingleTree(tree) + log_value = backend.loglikelihood(tree_log, theta_500, sampling_rate) log_vec_py_500[idx - 1] = log_value print(f"log_value: {log_value}") - +end_time = time.time() +elapsed_time = end_time - start_time +print(f"Time elapsed: {elapsed_time} seconds") # write Python loglikelihoods to CSV np.savetxt("likelihood_py/log_vec_py_AML.csv", log_vec_py_AML, delimiter=",") np.savetxt("likelihood_py/log_vec_py_500.csv", log_vec_py_500, delimiter=",") diff --git a/warmup/likelihood/R_py_loglikelihood_comparison_new_v1.py b/warmup/likelihood/R_py_loglikelihood_comparison_geno.py similarity index 78% rename from warmup/likelihood/R_py_loglikelihood_comparison_new_v1.py rename to warmup/likelihood/R_py_loglikelihood_comparison_geno.py index c278fd4..4c95e60 100644 --- a/warmup/likelihood/R_py_loglikelihood_comparison_new_v1.py +++ b/warmup/likelihood/R_py_loglikelihood_comparison_geno.py @@ -1,8 +1,9 @@ import pandas as pd import pmhn._trees._io as io -from pmhn._trees._backend_new_v1 import OriginalTreeMHNBackend, LoglikelihoodSingleTree +from pmhn._trees._backend_geno import OriginalTreeMHNBackend, LoglikelihoodSingleTree import csv import numpy as np +import time def csv_to_numpy(file_path): @@ -48,22 +49,33 @@ def csv_to_numpy(file_path): # calculate loglikelihoods log_vec_py_AML = np.empty(len(trees_AML)) log_vec_py_500 = np.empty(len(trees_500)) -backend = OriginalTreeMHNBackend() +start_time = time.time() +backend = OriginalTreeMHNBackend() +theta_AML_size = len(theta_AML) +all_mut_AML = set(i + 1 for i in range(theta_AML_size)) for idx, tree in trees_AML.items(): print(f"Processing tree {idx} of {len(trees_AML)}") tree_log = LoglikelihoodSingleTree(tree) - log_value = backend.loglikelihood(tree_log, theta_AML, sampling_rate) + log_value = backend.loglikelihood( + tree_log, theta_AML, sampling_rate, theta_AML_size, all_mut_AML + ) log_vec_py_AML[idx - 1] = log_value print(f"log_value: {log_value}") - +theta_500_size = len(theta_500) +all_mut_500 = set(i + 1 for i in range(theta_500_size)) for idx, tree in trees_500.items(): print(f"Processing tree {idx} of {len(trees_500)}") tree_log = LoglikelihoodSingleTree(tree) - log_value = backend.loglikelihood(tree_log, theta_500, sampling_rate) + log_value = backend.loglikelihood( + tree_log, theta_500, sampling_rate, theta_500_size, all_mut_500 + ) log_vec_py_500[idx - 1] = log_value print(f"log_value: {log_value}") +end_time = time.time() +elapsed_time = end_time - start_time +print(f"Time elapsed: {elapsed_time} seconds") # write Python loglikelihoods to CSV np.savetxt("likelihood_py/log_vec_py_AML.csv", log_vec_py_AML, delimiter=",") np.savetxt("likelihood_py/log_vec_py_500.csv", log_vec_py_500, delimiter=",") diff --git a/warmup/likelihood/R_py_loglikelihood_comparison_new_v1_profiling.py b/warmup/likelihood/R_py_loglikelihood_comparison_new_v1_profiling.py deleted file mode 100644 index 6aa4db4..0000000 --- a/warmup/likelihood/R_py_loglikelihood_comparison_new_v1_profiling.py +++ /dev/null @@ -1,86 +0,0 @@ -import pandas as pd -import pmhn._trees._io as io -from pmhn._trees._backend_new_v1 import OriginalTreeMHNBackend, LoglikelihoodSingleTree -import csv -import numpy as np -import pstats -import cProfile - - -def csv_to_numpy(file_path): - with open(file_path, "r") as file: - reader = csv.reader(file) - next(reader) - data_list = list(reader) - return np.array(data_list, dtype=float) - - -# AML trees -df_AML = pd.read_csv("likelihood_R/trees_AML_R.csv") - -# randomly generated 500 trees using a random theta -df_500 = pd.read_csv("likelihood_R/trees_500_R.csv") - -# theta matrices -theta_AML = csv_to_numpy("likelihood_R/MHN_Matrix_AML.csv") -theta_500 = csv_to_numpy("likelihood_R/MHN_Matrix_500.csv") -# loglikelihoods in R -log_vec_R_AML = np.genfromtxt("likelihood_R/log_vec_R_AML.csv", delimiter=",") -log_vec_R_500 = np.genfromtxt("likelihood_R/log_vec_R_500.csv", delimiter=",") - -# define sampling rate -sampling_rate = 1.0 - -# use modified io -naming = io.ForestNaming( - tree_name="Tree_ID", - naming=io.TreeNaming( - node="Node_ID", - parent="Parent_ID", - data={ - "Mutation_ID": "mutation", - }, - ), -) - -# parse trees -trees_AML = io.parse_forest(df_AML, naming=naming) -trees_500 = io.parse_forest(df_500, naming=naming) - -# calculate loglikelihoods -log_vec_py_AML = np.empty(len(trees_AML)) -log_vec_py_500 = np.empty(len(trees_500)) -backend = OriginalTreeMHNBackend() -profiler = cProfile.Profile() -profiler.enable() -for idx, tree in trees_AML.items(): - print(f"Processing tree {idx} of {len(trees_AML)}") - tree_log = LoglikelihoodSingleTree(tree) - log_value = backend.loglikelihood(tree_log, theta_AML, sampling_rate) - log_vec_py_AML[idx - 1] = log_value - print(f"log_value: {log_value}") - -for idx, tree in trees_500.items(): - print(f"Processing tree {idx} of {len(trees_500)}") - tree_log = LoglikelihoodSingleTree(tree) - log_value = backend.loglikelihood(tree_log, theta_500, sampling_rate) - log_vec_py_500[idx - 1] = log_value - print(f"log_value: {log_value}") -profiler.disable() - -# write Python loglikelihoods to CSV -np.savetxt("likelihood_py/log_vec_py_AML.csv", log_vec_py_AML, delimiter=",") -np.savetxt("likelihood_py/log_vec_py_500.csv", log_vec_py_500, delimiter=",") - - -# check if the loglikelihood vectors are the same -if np.allclose(log_vec_py_AML, log_vec_R_AML, atol=1e-10): - print("The loglikelihoods of the AML trees are the same in R and Python.") - -if np.allclose(log_vec_py_500, log_vec_R_500, atol=1e-10): - print( - "The loglikelihoods of the 500 randomly generated" - " trees are the same in R and Python." - ) -stats = pstats.Stats(profiler).sort_stats("cumtime") # Sort by cumulative time spent -stats.print_stats() diff --git a/warmup/likelihood/R_py_loglikelihood_comparison_new_v2.py b/warmup/likelihood/R_py_loglikelihood_comparison_new_v2.py deleted file mode 100644 index 1d24aeb..0000000 --- a/warmup/likelihood/R_py_loglikelihood_comparison_new_v2.py +++ /dev/null @@ -1,78 +0,0 @@ -import pandas as pd -import pmhn._trees._io as io -from pmhn._trees._backend_new_v2 import OriginalTreeMHNBackend, LoglikelihoodSingleTree -import csv -import numpy as np - - -def csv_to_numpy(file_path): - with open(file_path, "r") as file: - reader = csv.reader(file) - next(reader) - data_list = list(reader) - return np.array(data_list, dtype=float) - - -# AML trees -df_AML = pd.read_csv("likelihood_R/trees_AML_R.csv") - -# randomly generated 500 trees using a random theta -df_500 = pd.read_csv("likelihood_R/trees_500_R.csv") - -# theta matrices -theta_AML = csv_to_numpy("likelihood_R/MHN_Matrix_AML.csv") -theta_500 = csv_to_numpy("likelihood_R/MHN_Matrix_500.csv") -# loglikelihoods in R -log_vec_R_AML = np.genfromtxt("likelihood_R/log_vec_R_AML.csv", delimiter=",") -log_vec_R_500 = np.genfromtxt("likelihood_R/log_vec_R_500.csv", delimiter=",") - -# define sampling rate -sampling_rate = 1.0 - -# use modified io -naming = io.ForestNaming( - tree_name="Tree_ID", - naming=io.TreeNaming( - node="Node_ID", - parent="Parent_ID", - data={ - "Mutation_ID": "mutation", - }, - ), -) - -# parse trees -trees_AML = io.parse_forest(df_AML, naming=naming) -trees_500 = io.parse_forest(df_500, naming=naming) - -# calculate loglikelihoods -log_vec_py_AML = np.empty(len(trees_AML)) -log_vec_py_500 = np.empty(len(trees_500)) -backend = OriginalTreeMHNBackend() -for idx, tree in trees_AML.items(): - print(f"Processing tree {idx} of {len(trees_AML)}") - tree_log = LoglikelihoodSingleTree(tree) - log_value = backend.loglikelihood(tree_log, theta_AML, sampling_rate) - log_vec_py_AML[idx - 1] = log_value - print(f"log_value: {log_value}") - -for idx, tree in trees_500.items(): - print(f"Processing tree {idx} of {len(trees_500)}") - tree_log = LoglikelihoodSingleTree(tree) - log_value = backend.loglikelihood(tree_log, theta_500, sampling_rate) - log_vec_py_500[idx - 1] = log_value - print(f"log_value: {log_value}") -# write Python loglikelihoods to CSV -np.savetxt("likelihood_py/log_vec_py_AML.csv", log_vec_py_AML, delimiter=",") -np.savetxt("likelihood_py/log_vec_py_500.csv", log_vec_py_500, delimiter=",") - - -# check if the loglikelihood vectors are the same -if np.allclose(log_vec_py_AML, log_vec_R_AML, atol=1e-10): - print("The loglikelihoods of the AML trees are the same in R and Python.") - -if np.allclose(log_vec_py_500, log_vec_R_500, atol=1e-10): - print( - "The loglikelihoods of the 500 randomly generated" - " trees are the same in R and Python." - ) diff --git a/warmup/likelihood/R_py_loglikelihood_comparison_new_v2_10000.py b/warmup/likelihood/R_py_loglikelihood_comparison_new_v2_10000.py deleted file mode 100644 index 2a47409..0000000 --- a/warmup/likelihood/R_py_loglikelihood_comparison_new_v2_10000.py +++ /dev/null @@ -1,94 +0,0 @@ -import pandas as pd -import pmhn._trees._io as io -from pmhn._trees._backend_new_v2 import OriginalTreeMHNBackend, LoglikelihoodSingleTree -import csv -import numpy as np - - -def csv_to_numpy(file_path): - with open(file_path, "r") as file: - reader = csv.reader(file) - next(reader) - data_list = list(reader) - return np.array(data_list, dtype=float) - - -# AML trees -df_AML = pd.read_csv("likelihood_R/trees_AML_R.csv") - -# randomly generated 500 trees using a random theta -df_500 = pd.read_csv("likelihood_R/trees_500_R.csv") - -df_10000 = pd.read_csv("likelihood_R/trees_10000_R.csv") -# theta matrices -theta_AML = csv_to_numpy("likelihood_R/MHN_Matrix_AML.csv") -theta_500 = csv_to_numpy("likelihood_R/MHN_Matrix_500.csv") -theta_10000 = csv_to_numpy("likelihood_R/MHN_Matrix_10000.csv") -# loglikelihoods in R -log_vec_R_AML = np.genfromtxt("likelihood_R/log_vec_R_AML.csv", delimiter=",") -log_vec_R_500 = np.genfromtxt("likelihood_R/log_vec_R_500.csv", delimiter=",") -log_vec_R_10000 = np.genfromtxt("likelihood_R/log_vec_R_10000.csv", delimiter=",") -# define sampling rate -sampling_rate = 1.0 - -# use modified io -naming = io.ForestNaming( - tree_name="Tree_ID", - naming=io.TreeNaming( - node="Node_ID", - parent="Parent_ID", - data={ - "Mutation_ID": "mutation", - }, - ), -) - -# parse trees -trees_AML = io.parse_forest(df_AML, naming=naming) -trees_500 = io.parse_forest(df_500, naming=naming) -trees_10000 = io.parse_forest(df_10000, naming=naming) -# calculate loglikelihoods -log_vec_py_AML = np.empty(len(trees_AML)) -log_vec_py_500 = np.empty(len(trees_500)) -log_vec_py_10000 = np.empty(len(trees_10000)) -backend = OriginalTreeMHNBackend() - -for idx, tree in trees_AML.items(): - print(f"Processing tree {idx} of {len(trees_AML)}") - tree_log = LoglikelihoodSingleTree(tree) - log_value = backend.loglikelihood(tree_log, theta_AML, sampling_rate) - log_vec_py_AML[idx - 1] = log_value - print(f"log_value: {log_value}") - -for idx, tree in trees_500.items(): - print(f"Processing tree {idx} of {len(trees_500)}") - tree_log = LoglikelihoodSingleTree(tree) - log_value = backend.loglikelihood(tree_log, theta_500, sampling_rate) - log_vec_py_500[idx - 1] = log_value - print(f"log_value: {log_value}") - -for idx, tree in trees_10000.items(): - print(f"Processing tree {idx} of {len(trees_10000)}") - tree_log = LoglikelihoodSingleTree(tree) - log_value = backend.loglikelihood(tree_log, theta_10000, sampling_rate) - log_vec_py_10000[idx - 1] = log_value - print(f"log_value: {log_value}") -# write Python loglikelihoods to CSV -np.savetxt("likelihood_py/log_vec_py_AML.csv", log_vec_py_AML, delimiter=",") -np.savetxt("likelihood_py/log_vec_py_500.csv", log_vec_py_500, delimiter=",") -np.savetxt("likelihood_py/log_vec_py_10000.csv", log_vec_py_10000, delimiter=",") - -# check if the loglikelihood vectors are the same -if np.allclose(log_vec_py_AML, log_vec_R_AML, atol=1e-10): - print("The loglikelihoods of the AML trees are the same in R and Python.") - -if np.allclose(log_vec_py_500, log_vec_R_500, atol=1e-10): - print( - "The loglikelihoods of the 500 randomly generated" - " trees are the same in R and Python." - ) -if np.allclose(log_vec_py_10000, log_vec_R_10000, atol=1e-10): - print( - "The loglikelihoods of the 10000 randomly generated" - " trees are the same in R and Python." - ) diff --git a/warmup/likelihood/R_py_loglikelihood_comparison_new_v2_profiling.py b/warmup/likelihood/R_py_loglikelihood_comparison_new_v2_profiling.py deleted file mode 100644 index 25d4b0f..0000000 --- a/warmup/likelihood/R_py_loglikelihood_comparison_new_v2_profiling.py +++ /dev/null @@ -1,86 +0,0 @@ -import pandas as pd -import pmhn._trees._io as io -from pmhn._trees._backend_new_v2 import OriginalTreeMHNBackend, LoglikelihoodSingleTree -import csv -import numpy as np -import pstats -import cProfile - - -def csv_to_numpy(file_path): - with open(file_path, "r") as file: - reader = csv.reader(file) - next(reader) - data_list = list(reader) - return np.array(data_list, dtype=float) - - -# AML trees -df_AML = pd.read_csv("likelihood_R/trees_AML_R.csv") - -# randomly generated 500 trees using a random theta -df_500 = pd.read_csv("likelihood_R/trees_500_R.csv") - -# theta matrices -theta_AML = csv_to_numpy("likelihood_R/MHN_Matrix_AML.csv") -theta_500 = csv_to_numpy("likelihood_R/MHN_Matrix_500.csv") -# loglikelihoods in R -log_vec_R_AML = np.genfromtxt("likelihood_R/log_vec_R_AML.csv", delimiter=",") -log_vec_R_500 = np.genfromtxt("likelihood_R/log_vec_R_500.csv", delimiter=",") - -# define sampling rate -sampling_rate = 1.0 - -# use modified io -naming = io.ForestNaming( - tree_name="Tree_ID", - naming=io.TreeNaming( - node="Node_ID", - parent="Parent_ID", - data={ - "Mutation_ID": "mutation", - }, - ), -) - -# parse trees -trees_AML = io.parse_forest(df_AML, naming=naming) -trees_500 = io.parse_forest(df_500, naming=naming) - -# calculate loglikelihoods -log_vec_py_AML = np.empty(len(trees_AML)) -log_vec_py_500 = np.empty(len(trees_500)) -backend = OriginalTreeMHNBackend() -profiler = cProfile.Profile() -profiler.enable() -for idx, tree in trees_AML.items(): - print(f"Processing tree {idx} of {len(trees_AML)}") - tree_log = LoglikelihoodSingleTree(tree) - log_value = backend.loglikelihood(tree_log, theta_AML, sampling_rate) - log_vec_py_AML[idx - 1] = log_value - print(f"log_value: {log_value}") - -for idx, tree in trees_500.items(): - print(f"Processing tree {idx} of {len(trees_500)}") - tree_log = LoglikelihoodSingleTree(tree) - log_value = backend.loglikelihood(tree_log, theta_500, sampling_rate) - log_vec_py_500[idx - 1] = log_value - print(f"log_value: {log_value}") -profiler.disable() - -# write Python loglikelihoods to CSV -np.savetxt("likelihood_py/log_vec_py_AML.csv", log_vec_py_AML, delimiter=",") -np.savetxt("likelihood_py/log_vec_py_500.csv", log_vec_py_500, delimiter=",") - - -# check if the loglikelihood vectors are the same -if np.allclose(log_vec_py_AML, log_vec_R_AML, atol=1e-10): - print("The loglikelihoods of the AML trees are the same in R and Python.") - -if np.allclose(log_vec_py_500, log_vec_R_500, atol=1e-10): - print( - "The loglikelihoods of the 500 randomly generated" - " trees are the same in R and Python." - ) -stats = pstats.Stats(profiler).sort_stats("cumtime") # Sort by cumulative time spent -stats.print_stats() From 20a2f1e6c3a813b18cfeb22dc33ac3e531ade4b1 Mon Sep 17 00:00:00 2001 From: Laurenz Keller Date: Sun, 22 Oct 2023 00:08:11 +0200 Subject: [PATCH 33/45] small change --- src/pmhn/_trees/_backend_geno.py | 59 ++++++++--- src/pmhn/_trees/_tree_utils_geno.py | 99 +++++++++++------- ....R_py_loglikelihood_comparison_geno.py.swp | Bin 0 -> 12288 bytes .../R_py_loglikelihood_comparison_geno.py | 8 +- 4 files changed, 112 insertions(+), 54 deletions(-) create mode 100644 warmup/likelihood/.R_py_loglikelihood_comparison_geno.py.swp diff --git a/src/pmhn/_trees/_backend_geno.py b/src/pmhn/_trees/_backend_geno.py index a26976d..615c68d 100644 --- a/src/pmhn/_trees/_backend_geno.py +++ b/src/pmhn/_trees/_backend_geno.py @@ -1,9 +1,9 @@ -from typing import Protocol +from typing import Protocol, Optional import numpy as np from pmhn._trees._interfaces import Tree -from pmhn._trees._tree_utils_geno import create_genotype_subtree_map +from pmhn._trees._tree_utils_geno import create_mappings from anytree import Node @@ -12,7 +12,7 @@ def __init__(self, tree: Node): ( self._genotype_subtree_node_map, self._index_subclone_map, - ) = create_genotype_subtree_map(tree) + ) = create_mappings(tree) _genotype_subtree_node_map: dict[tuple[tuple[Node, int]], tuple[int, int]] _index_subclone_map: dict[int, tuple[int]] @@ -80,6 +80,18 @@ def diag_entry( theta: np.ndarray, all_mut: set[int], ) -> float: + """ + Calculates a diagonal entry of the V matrix. + + Args: + tree: a tree + genotype: the genotype of a subtree + theta: real-valued (i.e., log-theta) matrix, + shape (n_mutations, n_mutations) + all_mut: a set containing all possible mutations + Returns: + the diagonal entry of the V matrix corresponding to tree + """ lamb_sum = 0 for i, (node, val) in enumerate(genotype): if val: @@ -99,14 +111,23 @@ def diag_entry( lamb_sum -= lamb return lamb_sum - def find_single_difference(self, arr1: np.ndarray, arr2: np.ndarray): + def find_single_difference( + self, arr1: np.ndarray, arr2: np.ndarray + ) -> Optional[int]: + """ + Checks if two binary arrays of equal size differ in only one entry. + If so, the index of the differing entry is returned, otherwise None. + + Args: + arr1: the first array + arr2: the second array + Returns: + the index of the differing entry if there's + a single difference, otherwise None. + """ differing_indices = np.nonzero(np.bitwise_xor(arr1, arr2))[0] - return ( - (True, differing_indices[0]) - if len(differing_indices) == 1 - else (False, None) - ) + return differing_indices[0] if len(differing_indices) == 1 else None def off_diag_entry( self, @@ -114,8 +135,20 @@ def off_diag_entry( genotype_i: np.ndarray, genotype_j: np.ndarray, theta: np.ndarray, - ): - is_single_diff, index = self.find_single_difference(genotype_i, genotype_j) + ) -> float: + """ + Calculates an off-diagonal entry of the V matrix. + + Args: + tree: the original tree + genotype_i: the genotype of a subtree + genotype_j: the genotype of another subtree + theta: real-valued (i.e., log-theta) matrix, + shape (n_mutations, n_mutations) + Returns: + the off-diagonal entry of the V matrix corresponding to tree1 and tree2 + """ + index = self.find_single_difference(genotype_i, genotype_j) if index is None: return 0 else: @@ -133,7 +166,6 @@ def loglikelihood( tree: LoglikelihoodSingleTree, theta: np.ndarray, sampling_rate: float, - n_mutations: int, all_mut: set[int], ) -> float: """ @@ -144,8 +176,9 @@ def loglikelihood( theta: real-valued (i.e., log-theta) matrix, shape (n_mutations, n_mutations) sampling_rate: a scalar of type float + all_mut: a set containing all possible mutations Returns: - loglikelihood of the tree + the loglikelihood of tree """ # TODO(Pawel): this is part of https://github.com/cbg-ethz/pMHN/issues/15 # It can be implemented in any way. diff --git a/src/pmhn/_trees/_tree_utils_geno.py b/src/pmhn/_trees/_tree_utils_geno.py index 6bc0965..fb8fd3f 100644 --- a/src/pmhn/_trees/_tree_utils_geno.py +++ b/src/pmhn/_trees/_tree_utils_geno.py @@ -55,18 +55,24 @@ def all_combinations_of_elements(*lists): yield list(element_combination) -def create_subtree( - original_root: Node, nodes_list: list[Node], all_nodes_root: list[Node] -) -> Node: +def create_subtree(subtree_nodes: list[Node], original_tree_nodes: list[Node]) -> Node: + """ + Creates a certain subtree of the original tree. + + Args: + subtree_nodes: the nodes that are contained in both + the subtree and the original tree + original_tree_nodes: all nodes of the original tree + Returns: + a subtree + """ nodes_dict = {} - node_to_parent = {node: node.parent for node in nodes_list} - for node in all_nodes_root: - if node in nodes_list: - parent_node = node_to_parent.get(node) - nodes_dict[node] = Node(node.name, parent=nodes_dict.get(parent_node)) + for node in subtree_nodes: + parent_node = node.parent + nodes_dict[node] = Node(node.name, parent=nodes_dict.get(parent_node)) - return nodes_dict[original_root] + return nodes_dict[original_tree_nodes[0]] def get_subtrees(node: Node, memo: Optional[dict] = None) -> list[list[Node]]: @@ -77,7 +83,7 @@ def get_subtrees(node: Node, memo: Optional[dict] = None) -> list[list[Node]]: Args: node: the root node Returns: - a list of subtrees where each subtree is a list of nodes + a list of subtrees where each subtree is a list of nodes """ if memo is None: memo = {} @@ -110,18 +116,27 @@ def get_lineage(node: Node) -> tuple[int]: Args: node: a node Returns: - the lineage of a node + the lineage of a node """ return tuple(ancestor.name for ancestor in node.path) # type: ignore def create_index_subclone_maps( - tree: Node, + root: Node, ) -> tuple[dict[int, tuple[int]], dict[tuple[int], int]]: + """ + Assigns a unique index to each subclone in the provided + tree and generates two dictionaries: one mapping each unique + index to its corresponding subclone, and the other inverting this relationship. + Args: + root: the root node of a tree + Returns: + two dictionaries that contain the mappings + """ index_subclone_map = {} subclone_index_map = {} index = 0 - for level in LevelOrderGroupIter(tree): + for level in LevelOrderGroupIter(root): for node in level: index_subclone_map[index] = get_lineage(node) subclone_index_map[get_lineage(node)] = index @@ -130,36 +145,50 @@ def create_index_subclone_maps( def create_genotype( - size: int, subtree: Node, subclone_index_map: dict[tuple[int], int] + size: int, root: Node, subclone_index_map: dict[tuple[int], int] ) -> tuple[tuple[Optional[Node], int], ...]: + """ + Creates the genotype of a given tree. + + Args: + size: the size of the original tree + root: the root node of a subtree of the original tree + subclone_index_map: a dictionary that maps subclones to their indices + Returns: + a tuple of tuples, where each inner tuple represents a subclone from + the original tree. For each subclone, if it exists in the subtree, + the inner tuple contains the last node of that subclone and the value 1; + if it doesn't exist, the tuple contains None and the value 0. + """ x = [(Node(None), int(0))] * size - for level in LevelOrderGroupIter(subtree): + for level in LevelOrderGroupIter(root): for node in level: lineage = get_lineage(node) x[subclone_index_map[lineage]] = (node, 1) return tuple(x) -def create_genotype_subtree_map( +def create_mappings( root: Node, ) -> tuple[dict[tuple[tuple[Node, int]], tuple[int, int]], dict[int, tuple[int]]]: + """ + Creates the required mappings to calculate the likelihood of a tree. + + Args: + root: the root node of the original tree + Returns: + two dictionaries, one mapping genotypes to subtrees (here only the + index and length of the subtrees are needed) and the other one + mapping indices to subclones + """ index_subclone_map, subclone_index_map = create_index_subclone_maps(root) - subtree_genotype_node_map = {} - all_node_lists = get_subtrees(root) - all_nodes_root = all_node_lists[-1] - all_node_lists_with_len = [ - (node_list, len(node_list)) for node_list in all_node_lists - ] - size = len(all_node_lists) - for index, (node_list, node_list_len) in enumerate(all_node_lists_with_len): - subtree = create_subtree(root, node_list, all_nodes_root) - genotype_node = create_genotype(size, subtree, subclone_index_map) - subtree_genotype_node_map[genotype_node] = (index, node_list_len) - return subtree_genotype_node_map, index_subclone_map - - -if __name__ == "__main__": - A = Node("0") - B = Node("1", parent=A) - C = Node("3", parent=A) - D = Node("4", parent=B) + genotype_subtree_map = {} + subtrees = get_subtrees(root) + original_tree = subtrees[-1] + all_node_lists_with_len = [(subtree, len(subtree)) for subtree in subtrees] + size = len(subtrees) + for index, (subtree, subtree_size) in enumerate(all_node_lists_with_len): + subtree = create_subtree(subtree, original_tree) + genotype = create_genotype(size, subtree, subclone_index_map) + genotype_subtree_map[genotype] = (index, subtree_size) + return genotype_subtree_map, index_subclone_map diff --git a/warmup/likelihood/.R_py_loglikelihood_comparison_geno.py.swp b/warmup/likelihood/.R_py_loglikelihood_comparison_geno.py.swp new file mode 100644 index 0000000000000000000000000000000000000000..cc6770b34043811c08821d7dcb1f52bdb824bf4a GIT binary patch literal 12288 zcmeHN%ZnUE7_W%0s2hXg0|hC24tC=_6NIohhdg`)GLvPq1|dpQnXcJswqI1&WHTU8_$(H=d}+(&TjCFG7T_w#oaJ|T}BAtYeqH68?9sJ3mcsa#`aF+WMC|ZjROm0|GrLJRo1Ng>Fu{YQMQ{W!+>GH zFkl!k3>XFs1BL;^fMMW2&wxyJk>@bVYx0pkmEU*G-SeWk83qgkh5^HXVZbn87%&VN z1`Gp+0mFb{z%cMHWPo{u{DvR@FJBAi@&AAE`~Rm~3AqGJfCxAR90&S94`>6sf$#PZ z@&)ida1MA0coA3!4gm*&1>ldfxCboZXo1iU;`KfJ-`MY0QLeuUQftJz+1pk;P>mmEAT7u8E_ux z0uKYbfXm?fL*P81xIYJ&%P{c&WPsvwi27ESMWx_8rA%-tN1UcC)Zrs^t}DH|yqp!8jDv26H#%)+k!TMxHQV@buy<@n<8iie1v+M3(Zz}@H98pa z!5YPA6k`=OD8?gWQRNePhGD1d43}r6ZOxprN+zvh&>0EML3+k2%@@pgIH?u-l8dfY zx1616gX33f)1#TwR-@|zy7ORH$CFYS)59lDl8pMHwc;Z`c)*gBdy8}ey`S30MmF3LLSONakBX?~;fIJWU~@^O$a6Oq*(kZ3H~h@~QH} zQ49=YLD39{p+?PFb=S>JgfmQSOq&_s;YZ4)?b8Qohw8qn4`%Et94W}qLv|cetx$w2 z^MzE^=PB1$2@!Jd?}SLF?3N)y%Up<1^N$D-{Mar;XudK;sJwcJAXE)eQBesIDjkxi zCK`?lf7OpzpmGieKo4a(i}(p)bmJottEX| z-Ad6Y_ITIo4pJ8u?k?BGKYhx0qu@^r(}yL7(8%Z9oyQV8+HyVe@q_V*-w~+8|k4fR5RavAB6w6!Y+|K$I&Z#uk z>mUu4QY_YlWC;p460a#Z^IWK$^J1Fc?sD!zZHt#B3Jmb>P(9b;5twi+cDr9i~- zVl!Wvm9yw*UbwJ_evt2Y>%JV(IKj4}>fD6MQJq@Cawtows#+w-)oNs?lIn$!_+b(Y zsn@OISto!}5G6$wVnI|Q( Date: Sun, 22 Oct 2023 00:45:02 +0200 Subject: [PATCH 34/45] small change --- src/pmhn/_trees/_backend_geno.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/pmhn/_trees/_backend_geno.py b/src/pmhn/_trees/_backend_geno.py index 615c68d..6cd9244 100644 --- a/src/pmhn/_trees/_backend_geno.py +++ b/src/pmhn/_trees/_backend_geno.py @@ -90,7 +90,8 @@ def diag_entry( shape (n_mutations, n_mutations) all_mut: a set containing all possible mutations Returns: - the diagonal entry of the V matrix corresponding to tree + the diagonal entry of the V matrix corresponding to + genotype """ lamb_sum = 0 for i, (node, val) in enumerate(genotype): @@ -146,7 +147,8 @@ def off_diag_entry( theta: real-valued (i.e., log-theta) matrix, shape (n_mutations, n_mutations) Returns: - the off-diagonal entry of the V matrix corresponding to tree1 and tree2 + an off-diagonal entry of the V matrix corresponding to + the genotype_i and genotype_j """ index = self.find_single_difference(genotype_i, genotype_j) if index is None: From f96265ab03a0c91bf8b2f53fa15e37d8bf48c4cc Mon Sep 17 00:00:00 2001 From: Laurenz Keller Date: Sun, 22 Oct 2023 16:05:15 +0200 Subject: [PATCH 35/45] small change in _tree_utils_geno.py --- src/pmhn/_trees/_tree_utils_geno.py | 20 ++-- ....R_py_loglikelihood_comparison_geno.py.swp | Bin 12288 -> 0 bytes ...loglikelihood_comparison_geno_profiling.py | 93 ++++++++++++++++++ 3 files changed, 100 insertions(+), 13 deletions(-) delete mode 100644 warmup/likelihood/.R_py_loglikelihood_comparison_geno.py.swp create mode 100644 warmup/likelihood/R_py_loglikelihood_comparison_geno_profiling.py diff --git a/src/pmhn/_trees/_tree_utils_geno.py b/src/pmhn/_trees/_tree_utils_geno.py index fb8fd3f..4e39e1c 100644 --- a/src/pmhn/_trees/_tree_utils_geno.py +++ b/src/pmhn/_trees/_tree_utils_geno.py @@ -75,27 +75,23 @@ def create_subtree(subtree_nodes: list[Node], original_tree_nodes: list[Node]) - return nodes_dict[original_tree_nodes[0]] -def get_subtrees(node: Node, memo: Optional[dict] = None) -> list[list[Node]]: +def get_subtrees(node: Node) -> list[list[Node]]: """ Creates a list of all subtrees of a tree. - + A recursive approach is employed: If one knows the subtrees of the + children of the root node, then one can find all combinations of + the subtrees of the children and add the root node to each one + of these combinations, this way one obtains all subtrees of the original tree. Args: node: the root node Returns: - a list of subtrees where each subtree is a list of nodes + a list of subtrees """ - if memo is None: - memo = {} - - if node in memo: - return memo[node] - if not node.children: - memo[node] = [[node]] return [[node]] - child_subtrees = [get_subtrees(child, memo) for child in node.children] + child_subtrees = [get_subtrees(child) for child in node.children] combined_subtrees = all_combinations_of_elements(*child_subtrees) @@ -104,8 +100,6 @@ def get_subtrees(node: Node, memo: Optional[dict] = None) -> list[list[Node]]: for combination in combined_subtrees ] - memo[node] = result_subtrees - return result_subtrees diff --git a/warmup/likelihood/.R_py_loglikelihood_comparison_geno.py.swp b/warmup/likelihood/.R_py_loglikelihood_comparison_geno.py.swp deleted file mode 100644 index cc6770b34043811c08821d7dcb1f52bdb824bf4a..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 12288 zcmeHN%ZnUE7_W%0s2hXg0|hC24tC=_6NIohhdg`)GLvPq1|dpQnXcJswqI1&WHTU8_$(H=d}+(&TjCFG7T_w#oaJ|T}BAtYeqH68?9sJ3mcsa#`aF+WMC|ZjROm0|GrLJRo1Ng>Fu{YQMQ{W!+>GH zFkl!k3>XFs1BL;^fMMW2&wxyJk>@bVYx0pkmEU*G-SeWk83qgkh5^HXVZbn87%&VN z1`Gp+0mFb{z%cMHWPo{u{DvR@FJBAi@&AAE`~Rm~3AqGJfCxAR90&S94`>6sf$#PZ z@&)ida1MA0coA3!4gm*&1>ldfxCboZXo1iU;`KfJ-`MY0QLeuUQftJz+1pk;P>mmEAT7u8E_ux z0uKYbfXm?fL*P81xIYJ&%P{c&WPsvwi27ESMWx_8rA%-tN1UcC)Zrs^t}DH|yqp!8jDv26H#%)+k!TMxHQV@buy<@n<8iie1v+M3(Zz}@H98pa z!5YPA6k`=OD8?gWQRNePhGD1d43}r6ZOxprN+zvh&>0EML3+k2%@@pgIH?u-l8dfY zx1616gX33f)1#TwR-@|zy7ORH$CFYS)59lDl8pMHwc;Z`c)*gBdy8}ey`S30MmF3LLSONakBX?~;fIJWU~@^O$a6Oq*(kZ3H~h@~QH} zQ49=YLD39{p+?PFb=S>JgfmQSOq&_s;YZ4)?b8Qohw8qn4`%Et94W}qLv|cetx$w2 z^MzE^=PB1$2@!Jd?}SLF?3N)y%Up<1^N$D-{Mar;XudK;sJwcJAXE)eQBesIDjkxi zCK`?lf7OpzpmGieKo4a(i}(p)bmJottEX| z-Ad6Y_ITIo4pJ8u?k?BGKYhx0qu@^r(}yL7(8%Z9oyQV8+HyVe@q_V*-w~+8|k4fR5RavAB6w6!Y+|K$I&Z#uk z>mUu4QY_YlWC;p460a#Z^IWK$^J1Fc?sD!zZHt#B3Jmb>P(9b;5twi+cDr9i~- zVl!Wvm9yw*UbwJ_evt2Y>%JV(IKj4}>fD6MQJq@Cawtows#+w-)oNs?lIn$!_+b(Y zsn@OISto!}5G6$wVnI|Q( Date: Sun, 22 Oct 2023 16:43:24 +0200 Subject: [PATCH 36/45] small change in diag_entry --- src/pmhn/_trees/_backend_geno.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/pmhn/_trees/_backend_geno.py b/src/pmhn/_trees/_backend_geno.py index 6cd9244..11c79aa 100644 --- a/src/pmhn/_trees/_backend_geno.py +++ b/src/pmhn/_trees/_backend_geno.py @@ -1,6 +1,5 @@ from typing import Protocol, Optional - import numpy as np from pmhn._trees._interfaces import Tree from pmhn._trees._tree_utils_geno import create_mappings @@ -104,7 +103,8 @@ def diag_entry( for mutation in exit_mutations: lamb = 0 - exit_subclone = lineage + [mutation] + exit_subclone = lineage + lamb += theta[mutation - 1][mutation - 1] for j in exit_subclone: if j != 0: lamb += theta[mutation - 1][j - 1] From 18769d4409042c64375e4cf1f57aee533873b35b Mon Sep 17 00:00:00 2001 From: Laurenz Keller Date: Sun, 22 Oct 2023 16:46:05 +0200 Subject: [PATCH 37/45] small change in diag_entry function --- src/pmhn/_trees/_backend_geno.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/pmhn/_trees/_backend_geno.py b/src/pmhn/_trees/_backend_geno.py index 11c79aa..960d601 100644 --- a/src/pmhn/_trees/_backend_geno.py +++ b/src/pmhn/_trees/_backend_geno.py @@ -103,9 +103,8 @@ def diag_entry( for mutation in exit_mutations: lamb = 0 - exit_subclone = lineage lamb += theta[mutation - 1][mutation - 1] - for j in exit_subclone: + for j in lineage: if j != 0: lamb += theta[mutation - 1][j - 1] lamb = np.exp(lamb) From 502f07f0d4d939e3c697f0f4ee59507b05a0ee1f Mon Sep 17 00:00:00 2001 From: Laurenz Keller Date: Sun, 22 Oct 2023 16:48:20 +0200 Subject: [PATCH 38/45] small change in _tree_utils.py (memoization not needed) --- src/pmhn/_trees/_tree_utils.py | 31 +++++++++++-------------------- 1 file changed, 11 insertions(+), 20 deletions(-) diff --git a/src/pmhn/_trees/_tree_utils.py b/src/pmhn/_trees/_tree_utils.py index ba0ec75..9aef8c7 100644 --- a/src/pmhn/_trees/_tree_utils.py +++ b/src/pmhn/_trees/_tree_utils.py @@ -74,39 +74,30 @@ def create_subtree(original_root: Node, nodes_list: list[Node]) -> Node: return nodes_dict[original_root] -def get_subtrees(node: Node, memo: Optional[dict] = None) -> list[list[Node]]: +def get_subtrees(node: Node) -> list[list[Node]]: """ Creates a list of all subtrees of a tree. - + A recursive approach is employed: If one knows the subtrees of the + children of the root node, then one can find all combinations of + the subtrees of the children and add the root node to each one + of these combinations, this way one obtains all subtrees of the original tree. Args: node: the root node Returns: - a list of subtrees where each subtree is a list of nodes + a list of subtrees """ - if memo is None: - memo = {} - - if node in memo: - return memo[node] - if not node.children: - memo[node] = [[node]] return [[node]] - child_subtrees = [get_subtrees(child, memo) for child in node.children] + child_subtrees = [get_subtrees(child) for child in node.children] combined_subtrees = all_combinations_of_elements(*child_subtrees) - result_subtrees = [] - result_subtrees.append([node]) - for combination in combined_subtrees: - subtree_with_root = [node] + [ - item for sublist in combination for item in sublist - ] - result_subtrees.append(subtree_with_root) - - memo[node] = result_subtrees + result_subtrees = [[node]] + [ + [node] + [item for sublist in combination for item in sublist] + for combination in combined_subtrees + ] return result_subtrees From 614719c6152dc3122d4b3c4e22053255db45013d Mon Sep 17 00:00:00 2001 From: Laurenz Keller Date: Sun, 22 Oct 2023 17:56:19 +0200 Subject: [PATCH 39/45] small change --- warmup/likelihood/R_py_loglikelihood_comparison_geno.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/warmup/likelihood/R_py_loglikelihood_comparison_geno.py b/warmup/likelihood/R_py_loglikelihood_comparison_geno.py index 60af396..d9c9e20 100644 --- a/warmup/likelihood/R_py_loglikelihood_comparison_geno.py +++ b/warmup/likelihood/R_py_loglikelihood_comparison_geno.py @@ -53,7 +53,7 @@ def csv_to_numpy(file_path): start_time = time.time() backend = OriginalTreeMHNBackend() theta_AML_size = len(theta_AML) -all_mut_AML = set(i + 1 for i in range(theta_AML_size)) +all_mut_AML = set(range(1, theta_AML_size + 1)) for idx, tree in trees_AML.items(): print(f"Processing tree {idx} of {len(trees_AML)}") tree_log = LoglikelihoodSingleTree(tree) @@ -61,7 +61,7 @@ def csv_to_numpy(file_path): log_vec_py_AML[idx - 1] = log_value print(f"log_value: {log_value}") theta_500_size = len(theta_500) -all_mut_500 = set(i + 1 for i in range(theta_500_size)) +all_mut_500 = set(range(1, theta_500_size + 1)) for idx, tree in trees_500.items(): print(f"Processing tree {idx} of {len(trees_500)}") tree_log = LoglikelihoodSingleTree(tree) From 8377756f3fcd02d01f993c8b4f9cfdcd07cb05af Mon Sep 17 00:00:00 2001 From: Laurenz Keller Date: Mon, 23 Oct 2023 21:05:55 +0200 Subject: [PATCH 40/45] implemented pawel's suggestions --- src/pmhn/_trees/_backend.py | 8 +- src/pmhn/_trees/_backend_geno.py | 14 +-- src/pmhn/_trees/_tree_utils_geno.py | 8 +- ...py_loglikelihood_comparison_geno_seed43.py | 102 ++++++++++++++++++ 4 files changed, 116 insertions(+), 16 deletions(-) create mode 100644 warmup/likelihood/R_py_loglikelihood_comparison_geno_seed43.py diff --git a/src/pmhn/_trees/_backend.py b/src/pmhn/_trees/_backend.py index 66fce4d..b43b083 100644 --- a/src/pmhn/_trees/_backend.py +++ b/src/pmhn/_trees/_backend.py @@ -9,9 +9,7 @@ class LoglikelihoodSingleTree: def __init__(self, tree: Node): - self._subtrees_dict = create_all_subtrees(tree) - - _subtrees_dict: dict[Node, int] + self._subtrees_dict: dict[Node, int] = create_all_subtrees(tree) class IndividualTreeMHNBackendInterface(Protocol): @@ -65,9 +63,7 @@ def gradient_and_loglikelihood( class OriginalTreeMHNBackend(IndividualTreeMHNBackendInterface): def __init__(self, jitter: float = 1e-10): - self._jitter = jitter - - _jitter: float + self._jitter: float = jitter def diag_entry(self, tree: Node, theta: np.ndarray, all_mut: set[int]) -> float: """ diff --git a/src/pmhn/_trees/_backend_geno.py b/src/pmhn/_trees/_backend_geno.py index 960d601..85355eb 100644 --- a/src/pmhn/_trees/_backend_geno.py +++ b/src/pmhn/_trees/_backend_geno.py @@ -8,14 +8,16 @@ class LoglikelihoodSingleTree: def __init__(self, tree: Node): + self._genotype_subtree_node_map: dict[ + tuple[tuple[Node, int], ...], tuple[int, int] + ] + self._index_subclone_map: dict[int, tuple[int, ...]] + ( self._genotype_subtree_node_map, self._index_subclone_map, ) = create_mappings(tree) - _genotype_subtree_node_map: dict[tuple[tuple[Node, int]], tuple[int, int]] - _index_subclone_map: dict[int, tuple[int]] - class IndividualTreeMHNBackendInterface(Protocol): def loglikelihood( @@ -68,14 +70,12 @@ def gradient_and_loglikelihood( class OriginalTreeMHNBackend(IndividualTreeMHNBackendInterface): def __init__(self, jitter: float = 1e-10): - self._jitter = jitter - - _jitter: float + self._jitter: float = jitter def diag_entry( self, tree: LoglikelihoodSingleTree, - genotype: tuple[tuple[Node, int]], + genotype: tuple[tuple[Node, int], ...], theta: np.ndarray, all_mut: set[int], ) -> float: diff --git a/src/pmhn/_trees/_tree_utils_geno.py b/src/pmhn/_trees/_tree_utils_geno.py index 4e39e1c..ad1e001 100644 --- a/src/pmhn/_trees/_tree_utils_geno.py +++ b/src/pmhn/_trees/_tree_utils_geno.py @@ -117,7 +117,7 @@ def get_lineage(node: Node) -> tuple[int]: def create_index_subclone_maps( root: Node, -) -> tuple[dict[int, tuple[int]], dict[tuple[int], int]]: +) -> tuple[dict[int, tuple[int, ...]], dict[tuple[int, ...], int]]: """ Assigns a unique index to each subclone in the provided tree and generates two dictionaries: one mapping each unique @@ -139,7 +139,7 @@ def create_index_subclone_maps( def create_genotype( - size: int, root: Node, subclone_index_map: dict[tuple[int], int] + size: int, root: Node, subclone_index_map: dict[tuple[int, ...], int] ) -> tuple[tuple[Optional[Node], int], ...]: """ Creates the genotype of a given tree. @@ -164,7 +164,9 @@ def create_genotype( def create_mappings( root: Node, -) -> tuple[dict[tuple[tuple[Node, int]], tuple[int, int]], dict[int, tuple[int]]]: +) -> tuple[ + dict[tuple[tuple[Node, int], ...], tuple[int, int]], dict[int, tuple[int, ...]] +]: """ Creates the required mappings to calculate the likelihood of a tree. diff --git a/warmup/likelihood/R_py_loglikelihood_comparison_geno_seed43.py b/warmup/likelihood/R_py_loglikelihood_comparison_geno_seed43.py new file mode 100644 index 0000000..ad91e44 --- /dev/null +++ b/warmup/likelihood/R_py_loglikelihood_comparison_geno_seed43.py @@ -0,0 +1,102 @@ +import pandas as pd +import pmhn._trees._io as io +from pmhn._trees._backend_geno import OriginalTreeMHNBackend, LoglikelihoodSingleTree +import csv +import numpy as np +import time + + +def csv_to_numpy(file_path): + with open(file_path, "r") as file: + reader = csv.reader(file) + next(reader) + data_list = list(reader) + return np.array(data_list, dtype=float) + + +# AML trees +df_AML = pd.read_csv("likelihood_R/trees_AML_R.csv") + +# randomly generated 500 trees using a random theta +df_500 = pd.read_csv("likelihood_R/trees_500_R.csv") +df_1000 = pd.read_csv("likelihood_R/trees_1000_seed43_R.csv") +# theta matrices +theta_AML = csv_to_numpy("likelihood_R/MHN_Matrix_AML.csv") +theta_500 = csv_to_numpy("likelihood_R/MHN_Matrix_500.csv") +theta_1000 = csv_to_numpy("likelihood_R/MHN_Matrix_1000_seed43.csv") +# loglikelihoods in R +log_vec_R_AML = np.genfromtxt("likelihood_R/log_vec_R_AML.csv", delimiter=",") +log_vec_R_500 = np.genfromtxt("likelihood_R/log_vec_R_500.csv", delimiter=",") +log_vec_R_1000 = np.genfromtxt("likelihood_R/log_vec_R_1000_seed43.csv", delimiter=",") +# define sampling rate +sampling_rate = 1.0 + +# use modified io +naming = io.ForestNaming( + tree_name="Tree_ID", + naming=io.TreeNaming( + node="Node_ID", + parent="Parent_ID", + data={ + "Mutation_ID": "mutation", + }, + ), +) + +# parse trees +trees_AML = io.parse_forest(df_AML, naming=naming) +trees_500 = io.parse_forest(df_500, naming=naming) +trees_1000 = io.parse_forest(df_1000, naming=naming) +# calculate loglikelihoods +log_vec_py_AML = np.empty(len(trees_AML)) +log_vec_py_500 = np.empty(len(trees_500)) +log_vec_py_1000 = np.empty(len(trees_1000)) +start_time = time.time() +backend = OriginalTreeMHNBackend() +theta_AML_size = len(theta_AML) +all_mut_AML = set(range(1, theta_AML_size + 1)) +for idx, tree in trees_AML.items(): + print(f"Processing tree {idx} of {len(trees_AML)}") + tree_log = LoglikelihoodSingleTree(tree) + log_value = backend.loglikelihood(tree_log, theta_AML, sampling_rate, all_mut_AML) + log_vec_py_AML[idx - 1] = log_value + print(f"log_value: {log_value}") +theta_500_size = len(theta_500) +all_mut_500 = set(range(1, theta_500_size + 1)) +for idx, tree in trees_500.items(): + print(f"Processing tree {idx} of {len(trees_500)}") + tree_log = LoglikelihoodSingleTree(tree) + log_value = backend.loglikelihood(tree_log, theta_500, sampling_rate, all_mut_500) + log_vec_py_500[idx - 1] = log_value + print(f"log_value: {log_value}") +theta_1000_size = len(theta_1000) +all_mut_1000 = set(range(1, theta_1000_size + 1)) +for idx, tree in trees_1000.items(): + print(f"Processing tree {idx} of {len(trees_1000)}") + tree_log = LoglikelihoodSingleTree(tree) + log_value = backend.loglikelihood(tree_log, theta_1000, sampling_rate, all_mut_1000) + log_vec_py_1000[idx - 1] = log_value + print(f"log_value: {log_value}") +end_time = time.time() +elapsed_time = end_time - start_time +print(f"Time elapsed: {elapsed_time} seconds") +# write Python loglikelihoods to CSV +np.savetxt("likelihood_py/log_vec_py_AML.csv", log_vec_py_AML, delimiter=",") +np.savetxt("likelihood_py/log_vec_py_500.csv", log_vec_py_500, delimiter=",") + + +# check if the loglikelihood vectors are the same +if np.allclose(log_vec_py_AML, log_vec_R_AML, atol=1e-10): + print("The loglikelihoods of the AML trees are the same in R and Python.") + +if np.allclose(log_vec_py_500, log_vec_R_500, atol=1e-10): + print( + "The loglikelihoods of the 500 randomly generated" + " trees are the same in R and Python." + ) + +if np.allclose(log_vec_py_1000, log_vec_R_1000, atol=1e-10): + print( + "The loglikelihoods of the 1000 randomly generated" + " trees are the same in R and Python." + ) From 25a6f75ef36a1b2980f79faa55a89232c77187a7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pawe=C5=82=20Czy=C5=BC?= Date: Mon, 30 Oct 2023 11:09:37 +0100 Subject: [PATCH 41/45] Apply Black formatter. --- src/pmhn/_trees/_backend.py | 9 --------- src/pmhn/_trees/_tree_utils.py | 7 ------- warmup/likelihood/R_py_loglikelihood_comparison.py | 1 - 3 files changed, 17 deletions(-) diff --git a/src/pmhn/_trees/_backend.py b/src/pmhn/_trees/_backend.py index c705414..fb50165 100644 --- a/src/pmhn/_trees/_backend.py +++ b/src/pmhn/_trees/_backend.py @@ -13,7 +13,6 @@ def __init__(self, tree: Node): self._subtrees_dict: dict[Node, int] = create_all_subtrees(tree) - class IndividualTreeMHNBackendInterface(Protocol): def loglikelihood( self, @@ -65,11 +64,9 @@ def gradient_and_loglikelihood( class OriginalTreeMHNBackend(IndividualTreeMHNBackendInterface): def __init__(self, jitter: float = 1e-10): - self._jitter: float = jitter def diag_entry(self, tree: Node, theta: np.ndarray, all_mut: set[int]) -> float: - """ Calculates a diagonal entry of the V matrix. @@ -85,7 +82,6 @@ def diag_entry(self, tree: Node, theta: np.ndarray, all_mut: set[int]) -> float: """ lamb_sum = 0 - for level in LevelOrderGroupIter(tree): for node in level: tree_mutations = {n.name for n in node.path}.union( @@ -104,8 +100,6 @@ def diag_entry(self, tree: Node, theta: np.ndarray, all_mut: set[int]) -> float: lamb = np.exp(lamb) lamb_sum -= lamb - - return lamb_sum def off_diag_entry(self, tree1: Node, tree2: Node, theta: np.ndarray) -> float: @@ -136,9 +130,7 @@ def off_diag_entry(self, tree1: Node, tree2: Node, theta: np.ndarray) -> float: return float(lamb) def loglikelihood( - self, tree: LoglikelihoodSingleTree, theta: np.ndarray, sampling_rate: float - ) -> float: """ Calculates loglikelihood `log P(tree | theta)`. @@ -175,7 +167,6 @@ def loglikelihood( return np.log(x[-1] + self._jitter) + np.log(sampling_rate) - def gradient(self, tree: Node, theta: np.ndarray) -> np.ndarray: """Calculates the partial derivatives of `log P(tree | theta)` with respect to `theta`. diff --git a/src/pmhn/_trees/_tree_utils.py b/src/pmhn/_trees/_tree_utils.py index 24f3a90..6dff521 100644 --- a/src/pmhn/_trees/_tree_utils.py +++ b/src/pmhn/_trees/_tree_utils.py @@ -55,9 +55,7 @@ def all_combinations_of_elements(*lists): yield list(element_combination) - def create_subtree(original_root: Node, nodes_list: list[Node]) -> Node: - """ Creates a subtree given a list of nodes and the root node. @@ -76,7 +74,6 @@ def create_subtree(original_root: Node, nodes_list: list[Node]) -> Node: return nodes_dict[original_root] - def get_subtrees(node: Node) -> list[list[Node]]: """ Creates a list of all subtrees of a tree. @@ -103,7 +100,6 @@ def get_subtrees(node: Node) -> list[list[Node]]: for combination in combined_subtrees ] - return result_subtrees @@ -139,7 +135,6 @@ def get_lineage(node: Node) -> tuple[int]: return tuple(ancestor.name for ancestor in node.path) # type: ignore - def check_equality(tree1: Optional[Node], tree2: Optional[Node]) -> bool: """ Checks if tree1 and tree2 are identical, note that direct @@ -181,7 +176,6 @@ def bfs_compare(tree1: Node, tree2: Node) -> Optional[Node]: """ - diff_count = 0 iter1 = list(LevelOrderGroupIter(tree1)) iter2 = list(LevelOrderGroupIter(tree2)) @@ -214,7 +208,6 @@ def bfs_compare(tree1: Node, tree2: Node) -> Optional[Node]: if diff_count == 0: return iter2[-1][0] - return exit_node diff --git a/warmup/likelihood/R_py_loglikelihood_comparison.py b/warmup/likelihood/R_py_loglikelihood_comparison.py index a0cece3..f3296a6 100644 --- a/warmup/likelihood/R_py_loglikelihood_comparison.py +++ b/warmup/likelihood/R_py_loglikelihood_comparison.py @@ -6,7 +6,6 @@ import time - def csv_to_numpy(file_path): with open(file_path, "r") as file: reader = csv.reader(file) From 955de948ddb428f7e682725771534be136d651ff Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pawe=C5=82=20Czy=C5=BC?= Date: Mon, 30 Oct 2023 11:55:29 +0100 Subject: [PATCH 42/45] Remove redundant code from original backend --- src/pmhn/_trees/_backend.py | 112 ++----- tests/trees/test_likelihood.py | 293 +----------------- .../R_py_loglikelihood_comparison.py | 6 +- 3 files changed, 25 insertions(+), 386 deletions(-) diff --git a/src/pmhn/_trees/_backend.py b/src/pmhn/_trees/_backend.py index fb50165..5f97406 100644 --- a/src/pmhn/_trees/_backend.py +++ b/src/pmhn/_trees/_backend.py @@ -1,72 +1,21 @@ -from typing import Protocol - - import numpy as np -from pmhn._trees._interfaces import Tree from pmhn._trees._tree_utils import create_all_subtrees, bfs_compare from anytree import Node, LevelOrderGroupIter -class LoglikelihoodSingleTree: +class TreeWrapper: + """A wrapper for a tree which stores all subtrees.""" + def __init__(self, tree: Node): self._subtrees_dict: dict[Node, int] = create_all_subtrees(tree) -class IndividualTreeMHNBackendInterface(Protocol): - def loglikelihood( - self, - tree: Tree, - theta: np.ndarray, - ) -> float: - """Calculates loglikelihood `log P(tree | theta)`. - - Args: - tree: a tree - theta: real-valued (i.e., log-theta) matrix, - shape (n_mutations, n_mutations) - - Returns: - loglikelihood of the tree - """ - raise NotImplementedError - - def gradient( - self, - tree: Tree, - theta: np.ndarray, - ) -> np.ndarray: - """Calculates the partial derivatives of `log P(tree | theta)` - with respect to `theta`. - - Args: - tree: a tree - theta: real-valued matrix, - shape (n_mutations, n_mutatations) - - Returns: - gradient `d log P(tree | theta) / d theta`, - shape (n_mutations, n_mutations) - """ - raise NotImplementedError - - def gradient_and_loglikelihood( - self, tree: Tree, theta: np.ndarray - ) -> tuple[np.ndarray, float]: - """Returns the gradient and the loglikelihood. - - Note: - This function may be faster than calling `gradient` and `loglikelihood` - separately. - """ - return self.gradient(tree, theta), self.loglikelihood(tree, theta) - - -class OriginalTreeMHNBackend(IndividualTreeMHNBackendInterface): +class OriginalTreeMHNBackend: def __init__(self, jitter: float = 1e-10): self._jitter: float = jitter - def diag_entry(self, tree: Node, theta: np.ndarray, all_mut: set[int]) -> float: + def _diag_entry(self, tree: Node, theta: np.ndarray, all_mut: set[int]) -> float: """ Calculates a diagonal entry of the V matrix. @@ -102,7 +51,7 @@ def diag_entry(self, tree: Node, theta: np.ndarray, all_mut: set[int]) -> float: return lamb_sum - def off_diag_entry(self, tree1: Node, tree2: Node, theta: np.ndarray) -> float: + def _off_diag_entry(self, tree1: Node, tree2: Node, theta: np.ndarray) -> float: """ Calculates an off-diagonal entry of the V matrix. @@ -130,63 +79,38 @@ def off_diag_entry(self, tree1: Node, tree2: Node, theta: np.ndarray) -> float: return float(lamb) def loglikelihood( - self, tree: LoglikelihoodSingleTree, theta: np.ndarray, sampling_rate: float + self, tree_wrapper: TreeWrapper, theta: np.ndarray, sampling_rate: float ) -> float: - """ - Calculates loglikelihood `log P(tree | theta)`. + """Calculates loglikelihood `log P(tree | theta)`. Args: - tree: a tree + tree: a wrapper storing a tree (and its subtrees) theta: real-valued (i.e., log-theta) matrix, shape (n_mutations, n_mutations) - sampling_rate: a scalar of type float + sampling_rate: a scalar representing sampling rate + Returns: loglikelihood of the tree """ - # TODO(Pawel): this is part of https://github.com/cbg-ethz/pMHN/issues/15 - # It can be implemented in any way. - - subtrees_size = len(tree._subtrees_dict) + subtrees_size = len(tree_wrapper._subtrees_dict) x = np.zeros(subtrees_size) x[0] = 1 n_mutations = len(theta) all_mut = set(i + 1 for i in range(n_mutations)) - for i, (subtree_i, subtree_size_i) in enumerate(tree._subtrees_dict.items()): + for i, (subtree_i, subtree_size_i) in enumerate( + tree_wrapper._subtrees_dict.items() + ): V_col = {} V_diag = 0.0 for j, (subtree_j, subtree_size_j) in enumerate( - tree._subtrees_dict.items() + tree_wrapper._subtrees_dict.items() ): if subtree_size_i - subtree_size_j == 1: - V_col[j] = -self.off_diag_entry(subtree_j, subtree_i, theta) + V_col[j] = -self._off_diag_entry(subtree_j, subtree_i, theta) elif i == j: - V_diag = sampling_rate - self.diag_entry(subtree_i, theta, all_mut) + V_diag = sampling_rate - self._diag_entry(subtree_i, theta, all_mut) for index, val in V_col.items(): x[i] -= val * x[index] x[i] /= V_diag return np.log(x[-1] + self._jitter) + np.log(sampling_rate) - - def gradient(self, tree: Node, theta: np.ndarray) -> np.ndarray: - """Calculates the partial derivatives of `log P(tree | theta)` - with respect to `theta`. - - Args: - tree: a tree - theta: real-valued matrix, shape (n_mutations, n_mutatations) - - Returns: - gradient `d log P(tree | theta) / d theta`, - shape (n_mutations, n_mutations) - """ - # TODO(Pawel): This is part of - # https://github.com/cbg-ethz/pMHN/issues/18, - # but it is *not* a priority. - # We will try to do the modelling as soon as possible, - # starting with a sequential Monte Carlo sampler - # and Metropolis transitions. - # Only after initial experiments - # (we will probably see that it's not scalable), - # we'll consider switching to Hamiltonian Monte Carlo, - # which requires gradients. - raise NotImplementedError diff --git a/tests/trees/test_likelihood.py b/tests/trees/test_likelihood.py index 5f98ac4..21e6253 100644 --- a/tests/trees/test_likelihood.py +++ b/tests/trees/test_likelihood.py @@ -1,293 +1,8 @@ -from pmhn._trees._backend import OriginalTreeMHNBackend -from pmhn._trees._tree_utils import create_all_subtrees +from pmhn._trees._backend import OriginalTreeMHNBackend, TreeWrapper from anytree import Node import numpy as np -def test_create_V_Mat(): - """ - Checks if create_V_Mat is implemented correctly. - - tree: - 0 - / \ - 1 3 - / - 3 - """ - - true_Q = np.array( - [ - [ - -(np.exp(-1.41) + np.exp(-2.26) + np.exp(-2.55)), - np.exp(-1.41), - np.exp(-2.55), - 0, - 0, - 0, - ], - [ - 0, - -( - np.exp(-2.26) - + np.exp(-2.55) - + np.exp(-1.12 - 2.26) - + np.exp(1 - 2.55) - ), - 0, - np.exp(1 - 2.55), - np.exp(-2.55), - 0, - ], - [ - 0, - 0, - -( - np.exp(-1.41) - + np.exp(-2.26) - + np.exp(-1.41 + 3) - + np.exp(-2.26 + 2) - ), - 0, - np.exp(-1.41), - 0, - ], - [ - 0, - 0, - 0, - -( - np.exp(-2.26) - + np.exp(-2.55) - + np.exp(-2.26 - 1.12) - + np.exp(-2.26 - 1.12 + 2) - ), - 0, - np.exp(-2.55), - ], - [ - 0, - 0, - 0, - 0, - -( - np.exp(-2.26) - + np.exp(-2.26 - 1.12) - + np.exp(-2.55 + 1) - + np.exp(-1.41 + 3) - + np.exp(-2.26 + 2) - ), - np.exp(-2.55 + 1), - ], - [ - 0, - 0, - 0, - 0, - 0, - -( - np.exp(-2.26) - + np.exp(-2.26 - 1.12) - + np.exp(-2.26 + 2 - 1.12) - + np.exp(-1.41 + 3) - + np.exp(-2.26 + 2) - ), - ], - ] - ) - A = Node(0) - B = Node(1, parent=A) - Node(3, parent=A) - Node(3, parent=B) - subtrees = create_all_subtrees(A) - subtrees_size = len(subtrees) - sampling_rate = 1.0 - true_V = np.eye(subtrees_size) * sampling_rate - true_Q - backend = OriginalTreeMHNBackend() - theta = np.array([[-1.41, 2, 3], [-1.12, -2.26, 2], [1, -0.86, -2.55]]) - - V = backend.create_V_Mat(A, theta, sampling_rate) - - assert np.allclose(V, true_V, atol=1e-8) - - -def test_diag_entry(): - r""" - - Checks if the diagonal values of the V matrix are calculated correctly. - - 0 - / | \ - 2 1 3 - | | - 3 3 - - - augmented tree: - - - 0 - / | \ - 2 1 3 - |\ |\ |\ - 3 1 3 2 1 2 - | | - 1 2 - - """ - A = Node(0) - B = Node(2, parent=A) - D = Node(1, parent=A) - Node(3, parent=A) - Node(3, parent=B) - Node(3, parent=D) - theta = np.array([[-1.41, 2, 3], [-1.12, -2.26, 2], [1, -0.86, -2.55]]) - true_diag_entry = -( - np.exp(-1.41 + 2) - + np.exp(-2.26 - 1.12) - + np.exp(-1.41 + 3) - + np.exp(-2.26 + 2) - + np.exp(-1.41 + 2 + 3) - + np.exp(-2.26 + 2 - 1.12) - ) - backend = OriginalTreeMHNBackend() - diag_entry = backend.diag_entry(A, theta) - - assert np.allclose(diag_entry, true_diag_entry, atol=1e-8) - - -def test_off_diag_entry_valid(): - r""" - Checks if the off-diagonal entries of the V matrix - are calculated correctly. - - first tree: - 0 - / | \ - 2 1 3 - | - 3 - - second tree: - 0 - / | \ - 2 1 3 - | | - 3 3 - - - """ - - theta = np.array([[-1.41, 2, 3], [-1.12, -2.26, 2], [1, -0.86, -2.55]]) - # first tree - A_1 = Node(0) - B_1 = Node(2, parent=A_1) - Node(1, parent=A_1) - Node(3, parent=A_1) - Node(3, parent=B_1) - - # second tree - A_2 = Node(0) - B_2 = Node(2, parent=A_2) - D_2 = Node(1, parent=A_2) - Node(3, parent=A_2) - Node(3, parent=B_2) - Node(3, parent=D_2) - - true_off_diag_entry = np.exp(-2.55 + 1) - backend = OriginalTreeMHNBackend() - off_diag_entry = backend.off_diag_entry(A_1, A_2, theta) - - assert np.allclose(off_diag_entry, true_off_diag_entry, atol=1e-8) - - -def test_off_diag_entry_invalid_size(): - r""" - - Checks if off_diag_entry successfully returns 0 when the size is invalid - (i.e the first tree is not smaller than the second tree by one). - - first tree: - 0 - / | \ - 2 1 3 - | - 3 - - second tree: - 0 - / | \ - 2 1 3 - | | | - 3 3 2 - - - """ - - # first tree - A_1 = Node(0) - B_1 = Node(2, parent=A_1) - Node(1, parent=A_1) - Node(3, parent=A_1) - Node(3, parent=B_1) - - # second tree - A_2 = Node(0) - B_2 = Node(2, parent=A_2) - C_2 = Node(1, parent=A_2) - D_2 = Node(3, parent=A_2) - Node(3, parent=B_2) - Node(3, parent=C_2) - Node(2, parent=D_2) - - theta = np.array([[-1.41, 2, 3], [-1.12, -2.26, 2], [1, -0.86, -2.55]]) - backend = OriginalTreeMHNBackend() - - assert backend.off_diag_entry(A_1, A_2, theta) == 0 - - -def test_off_diag_entry_not_subset(): - r""" - Checks if the off_diag_entry succesfully returns 0 when the - first tree is not a subtree of the second tree. - - first tree: - 0 - / | \ - 2 1 3 - | - 3 - - second tree: - 0 - / | \ - 2 1 3 - | | - 3 2 - - - """ - # first tree - A_1 = Node(0) - B_1 = Node(2, parent=A_1) - Node(1, parent=A_1) - Node(3, parent=A_1) - Node(3, parent=B_1) - - # second tree - A_2 = Node(0) - Node(2, parent=A_2) - C_2 = Node(1, parent=A_2) - D_2 = Node(3, parent=A_2) - Node(3, parent=C_2) - Node(2, parent=D_2) - - theta = np.array([[-1.41, 2, 3], [-1.12, -2.26, 2], [1, -0.86, -2.55]]) - backend = OriginalTreeMHNBackend() - - assert backend.off_diag_entry(A_1, A_2, theta) == 0 - - def test_likelihood_small_tree(): """ Checks if the likelihood of a small tree is calculated @@ -321,7 +36,7 @@ def test_likelihood_small_tree(): sampling_rate = 1.0 backend = OriginalTreeMHNBackend() - log_value = backend.loglikelihood(A, theta, sampling_rate) + log_value = backend.loglikelihood(TreeWrapper(A), theta, sampling_rate) assert np.allclose(log_value, -5.793104, atol=1e-5) @@ -364,7 +79,7 @@ def test_likelihood_medium_tree(): sampling_rate = 1.0 backend = OriginalTreeMHNBackend() - log_value = backend.loglikelihood(A, theta, sampling_rate) + log_value = backend.loglikelihood(TreeWrapper(A), theta, sampling_rate) assert np.allclose(log_value, -14.729560, atol=1e-5) @@ -414,6 +129,6 @@ def test_likelihood_large_tree(): sampling_rate = 1.0 backend = OriginalTreeMHNBackend() - log_value = backend.loglikelihood(A, theta, sampling_rate) + log_value = backend.loglikelihood(TreeWrapper(A), theta, sampling_rate) assert np.allclose(log_value, -22.288420, atol=1e-5) diff --git a/warmup/likelihood/R_py_loglikelihood_comparison.py b/warmup/likelihood/R_py_loglikelihood_comparison.py index f3296a6..0f90674 100644 --- a/warmup/likelihood/R_py_loglikelihood_comparison.py +++ b/warmup/likelihood/R_py_loglikelihood_comparison.py @@ -1,6 +1,6 @@ import pandas as pd import pmhn._trees._io as io -from pmhn._trees._backend import OriginalTreeMHNBackend, LoglikelihoodSingleTree +from pmhn._trees._backend import OriginalTreeMHNBackend, TreeWrapper import csv import numpy as np import time @@ -53,7 +53,7 @@ def csv_to_numpy(file_path): backend = OriginalTreeMHNBackend() for idx, tree in trees_AML.items(): print(f"Processing tree {idx} of {len(trees_AML)}") - tree_log = LoglikelihoodSingleTree(tree) + tree_log = TreeWrapper(tree) log_value = backend.loglikelihood(tree_log, theta_AML, sampling_rate) log_vec_py_AML[idx - 1] = log_value @@ -61,7 +61,7 @@ def csv_to_numpy(file_path): for idx, tree in trees_500.items(): print(f"Processing tree {idx} of {len(trees_500)}") - tree_log = LoglikelihoodSingleTree(tree) + tree_log = TreeWrapper(tree) log_value = backend.loglikelihood(tree_log, theta_500, sampling_rate) log_vec_py_500[idx - 1] = log_value print(f"log_value: {log_value}") From 71ca14255883dfbdafa9b2778f4143beb387c2b1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pawe=C5=82=20Czy=C5=BC?= Date: Mon, 30 Oct 2023 12:16:48 +0100 Subject: [PATCH 43/45] Fix unit tests --- .../{_backend_geno.py => _backend_code.py} | 130 +++++------------- tests/trees/test_likelihood.py | 65 +++++++-- 2 files changed, 84 insertions(+), 111 deletions(-) rename src/pmhn/_trees/{_backend_geno.py => _backend_code.py} (54%) diff --git a/src/pmhn/_trees/_backend_geno.py b/src/pmhn/_trees/_backend_code.py similarity index 54% rename from src/pmhn/_trees/_backend_geno.py rename to src/pmhn/_trees/_backend_code.py index 85355eb..db81051 100644 --- a/src/pmhn/_trees/_backend_geno.py +++ b/src/pmhn/_trees/_backend_code.py @@ -1,13 +1,14 @@ -from typing import Protocol, Optional +from typing import Optional import numpy as np -from pmhn._trees._interfaces import Tree from pmhn._trees._tree_utils_geno import create_mappings from anytree import Node -class LoglikelihoodSingleTree: - def __init__(self, tree: Node): +class TreeWrapperCode: + """Tree wrapper using smart encoding of subtrees.""" + + def __init__(self, tree: Node) -> None: self._genotype_subtree_node_map: dict[ tuple[tuple[Node, int], ...], tuple[int, int] ] @@ -19,71 +20,21 @@ def __init__(self, tree: Node): ) = create_mappings(tree) -class IndividualTreeMHNBackendInterface(Protocol): - def loglikelihood( - self, - tree: Tree, - theta: np.ndarray, - ) -> float: - """Calculates loglikelihood `log P(tree | theta)`. - - Args: - tree: a tree - theta: real-valued (i.e., log-theta) matrix, - shape (n_mutations, n_mutations) - - Returns: - loglikelihood of the tree - """ - raise NotImplementedError - - def gradient( - self, - tree: Tree, - theta: np.ndarray, - ) -> np.ndarray: - """Calculates the partial derivatives of `log P(tree | theta)` - with respect to `theta`. - - Args: - tree: a tree - theta: real-valued matrix, - shape (n_mutations, n_mutatations) - - Returns: - gradient `d log P(tree | theta) / d theta`, - shape (n_mutations, n_mutations) - """ - raise NotImplementedError - - def gradient_and_loglikelihood( - self, tree: Tree, theta: np.ndarray - ) -> tuple[np.ndarray, float]: - """Returns the gradient and the loglikelihood. - - Note: - This function may be faster than calling `gradient` and `loglikelihood` - separately. - """ - return self.gradient(tree, theta), self.loglikelihood(tree, theta) - - -class OriginalTreeMHNBackend(IndividualTreeMHNBackendInterface): - def __init__(self, jitter: float = 1e-10): +class TreeMHNBackendCode: + def __init__(self, jitter: float = 1e-10) -> None: self._jitter: float = jitter - def diag_entry( + def _diag_entry( self, - tree: LoglikelihoodSingleTree, + tree_wrapper: TreeWrapperCode, genotype: tuple[tuple[Node, int], ...], theta: np.ndarray, all_mut: set[int], ) -> float: - """ - Calculates a diagonal entry of the V matrix. + """Calculates a diagonal entry of the V matrix. Args: - tree: a tree + tree: a tree wrappper genotype: the genotype of a subtree theta: real-valued (i.e., log-theta) matrix, shape (n_mutations, n_mutations) @@ -95,7 +46,7 @@ def diag_entry( lamb_sum = 0 for i, (node, val) in enumerate(genotype): if val: - lineage = tree._index_subclone_map[i] + lineage = tree_wrapper._index_subclone_map[i] lineage = list(lineage) tree_mutations = set(lineage + [c.name for c in node.children]) @@ -129,9 +80,9 @@ def find_single_difference( return differing_indices[0] if len(differing_indices) == 1 else None - def off_diag_entry( + def _off_diag_entry( self, - tree: LoglikelihoodSingleTree, + tree_wrapper: TreeWrapperCode, genotype_i: np.ndarray, genotype_j: np.ndarray, theta: np.ndarray, @@ -154,7 +105,7 @@ def off_diag_entry( return 0 else: lamb = 0 - lineage = tree._index_subclone_map[index] + lineage = tree_wrapper._index_subclone_map[index] exit_mutation = lineage[-1] for mutation in lineage: if mutation != 0: @@ -164,7 +115,7 @@ def off_diag_entry( def loglikelihood( self, - tree: LoglikelihoodSingleTree, + tree_wrapper: TreeWrapperCode, theta: np.ndarray, sampling_rate: float, all_mut: set[int], @@ -178,59 +129,40 @@ def loglikelihood( shape (n_mutations, n_mutations) sampling_rate: a scalar of type float all_mut: a set containing all possible mutations + Returns: the loglikelihood of tree """ - # TODO(Pawel): this is part of https://github.com/cbg-ethz/pMHN/issues/15 - # It can be implemented in any way. - subtrees_size = len(tree._genotype_subtree_node_map) + subtrees_size = len(tree_wrapper._genotype_subtree_node_map) x = np.zeros(subtrees_size) x[0] = 1 genotype_lists = [] - for genotype in tree._genotype_subtree_node_map.keys(): + for genotype in tree_wrapper._genotype_subtree_node_map.keys(): genotype_lists.append(np.array([item[1] for item in genotype])) - for genotype_i, (i, subtree_size_i) in tree._genotype_subtree_node_map.items(): + for genotype_i, ( + i, + subtree_size_i, + ) in tree_wrapper._genotype_subtree_node_map.items(): V_col = [] V_diag = 0.0 - for j, subtree_size_j in tree._genotype_subtree_node_map.values(): + for j, subtree_size_j in tree_wrapper._genotype_subtree_node_map.values(): if subtree_size_i - subtree_size_j == 1: V_col.append( ( j, - -self.off_diag_entry( - tree, genotype_lists[j], genotype_lists[i], theta + -self._off_diag_entry( + tree_wrapper, + genotype_lists[j], + genotype_lists[i], + theta, ), ) ) elif i == j: - V_diag = sampling_rate - self.diag_entry( - tree, genotype_i, theta, all_mut + V_diag = sampling_rate - self._diag_entry( + tree_wrapper, genotype_i, theta, all_mut ) for index, val in V_col: x[i] -= val * x[index] x[i] /= V_diag return np.log(x[-1] + self._jitter) + np.log(sampling_rate) - - def gradient(self, tree: Node, theta: np.ndarray) -> np.ndarray: - """Calculates the partial derivatives of `log P(tree | theta)` - with respect to `theta`. - - Args: - tree: a tree - theta: real-valued matrix, shape (n_mutations, n_mutatations) - - Returns: - gradient `d log P(tree | theta) / d theta`, - shape (n_mutations, n_mutations) - """ - # TODO(Pawel): This is part of - # https://github.com/cbg-ethz/pMHN/issues/18, - # but it is *not* a priority. - # We will try to do the modelling as soon as possible, - # starting with a sequential Monte Carlo sampler - # and Metropolis transitions. - # Only after initial experiments - # (we will probably see that it's not scalable), - # we'll consider switching to Hamiltonian Monte Carlo, - # which requires gradients. - raise NotImplementedError diff --git a/tests/trees/test_likelihood.py b/tests/trees/test_likelihood.py index 21e6253..548315a 100644 --- a/tests/trees/test_likelihood.py +++ b/tests/trees/test_likelihood.py @@ -1,9 +1,49 @@ -from pmhn._trees._backend import OriginalTreeMHNBackend, TreeWrapper -from anytree import Node import numpy as np +import pytest +from anytree import Node + +import pmhn._trees._backend as backend_orig +import pmhn._trees._backend_code as backend_geno + +def get_loglikelihood_functions() -> list: + """This is an auxiliary function which returns a list of + loglikelihood functions to be tested. -def test_likelihood_small_tree(): + Each of these functions has signature: + + loglikelihood( + tree: Node, + theta: np.ndarray, + sampling_rate: float, + all_mut: set[int], + ) -> float + + Note: + Whenever a new backend is used + (and it has a wrapper around trees for memoization), + it could just be added here. + """ + + def backend1( + tree: Node, theta: np.ndarray, sampling_rate: float, all_mut: set[int] + ) -> float: + return backend_orig.OriginalTreeMHNBackend().loglikelihood( + backend_orig.TreeWrapper(tree), theta, sampling_rate + ) + + def backend2( + tree: Node, theta: np.ndarray, sampling_rate: float, all_mut: set[int] + ) -> float: + return backend_geno.TreeMHNBackendCode().loglikelihood( + backend_geno.TreeWrapperCode(tree), theta, sampling_rate, all_mut + ) + + return [backend1, backend2] + + +@pytest.mark.parametrize("backend", get_loglikelihood_functions()) +def test_likelihood_small_tree(backend) -> None: """ Checks if the likelihood of a small tree is calculated correctly. @@ -34,14 +74,14 @@ def test_likelihood_small_tree(): ] ) sampling_rate = 1.0 + all_mut = set(range(1, 11)) - backend = OriginalTreeMHNBackend() - log_value = backend.loglikelihood(TreeWrapper(A), theta, sampling_rate) - + log_value = backend(A, theta, sampling_rate, all_mut) assert np.allclose(log_value, -5.793104, atol=1e-5) -def test_likelihood_medium_tree(): +@pytest.mark.parametrize("backend", get_loglikelihood_functions()) +def test_likelihood_medium_tree(backend) -> None: """ Checks if the likelihood of a medium-sized tree is calculated correctly. @@ -78,13 +118,14 @@ def test_likelihood_medium_tree(): ) sampling_rate = 1.0 - backend = OriginalTreeMHNBackend() - log_value = backend.loglikelihood(TreeWrapper(A), theta, sampling_rate) + all_mut = set(range(1, 11)) + log_value = backend(A, theta, sampling_rate, all_mut) assert np.allclose(log_value, -14.729560, atol=1e-5) -def test_likelihood_large_tree(): +@pytest.mark.parametrize("backend", get_loglikelihood_functions()) +def test_likelihood_large_tree(backend) -> None: """ Checks if the likelihood of a large tree is calculated correctly. @@ -127,8 +168,8 @@ def test_likelihood_large_tree(): ] ) sampling_rate = 1.0 + all_mut = set(range(1, 11)) - backend = OriginalTreeMHNBackend() - log_value = backend.loglikelihood(TreeWrapper(A), theta, sampling_rate) + log_value = backend(A, theta, sampling_rate, all_mut) assert np.allclose(log_value, -22.288420, atol=1e-5) From 263235a657016519d952108b65728dd82e69e5c3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pawe=C5=82=20Czy=C5=BC?= Date: Mon, 30 Oct 2023 12:19:42 +0100 Subject: [PATCH 44/45] Update the warmup files. --- .../likelihood/R_py_loglikelihood_comparison_geno.py | 8 ++++---- .../R_py_loglikelihood_comparison_geno_profiling.py | 8 ++++---- .../R_py_loglikelihood_comparison_geno_seed43.py | 10 +++++----- 3 files changed, 13 insertions(+), 13 deletions(-) diff --git a/warmup/likelihood/R_py_loglikelihood_comparison_geno.py b/warmup/likelihood/R_py_loglikelihood_comparison_geno.py index d9c9e20..cf8942a 100644 --- a/warmup/likelihood/R_py_loglikelihood_comparison_geno.py +++ b/warmup/likelihood/R_py_loglikelihood_comparison_geno.py @@ -1,6 +1,6 @@ import pandas as pd import pmhn._trees._io as io -from pmhn._trees._backend_geno import OriginalTreeMHNBackend, LoglikelihoodSingleTree +from pmhn._trees._backend_code import TreeMHNBackendCode, TreeWrapperCode import csv import numpy as np import time @@ -51,12 +51,12 @@ def csv_to_numpy(file_path): log_vec_py_500 = np.empty(len(trees_500)) start_time = time.time() -backend = OriginalTreeMHNBackend() +backend = TreeMHNBackendCode() theta_AML_size = len(theta_AML) all_mut_AML = set(range(1, theta_AML_size + 1)) for idx, tree in trees_AML.items(): print(f"Processing tree {idx} of {len(trees_AML)}") - tree_log = LoglikelihoodSingleTree(tree) + tree_log = TreeWrapperCode(tree) log_value = backend.loglikelihood(tree_log, theta_AML, sampling_rate, all_mut_AML) log_vec_py_AML[idx - 1] = log_value print(f"log_value: {log_value}") @@ -64,7 +64,7 @@ def csv_to_numpy(file_path): all_mut_500 = set(range(1, theta_500_size + 1)) for idx, tree in trees_500.items(): print(f"Processing tree {idx} of {len(trees_500)}") - tree_log = LoglikelihoodSingleTree(tree) + tree_log = TreeWrapperCode(tree) log_value = backend.loglikelihood(tree_log, theta_500, sampling_rate, all_mut_500) log_vec_py_500[idx - 1] = log_value print(f"log_value: {log_value}") diff --git a/warmup/likelihood/R_py_loglikelihood_comparison_geno_profiling.py b/warmup/likelihood/R_py_loglikelihood_comparison_geno_profiling.py index 269f1a4..61bb543 100644 --- a/warmup/likelihood/R_py_loglikelihood_comparison_geno_profiling.py +++ b/warmup/likelihood/R_py_loglikelihood_comparison_geno_profiling.py @@ -1,6 +1,6 @@ import pandas as pd import pmhn._trees._io as io -from pmhn._trees._backend_geno import OriginalTreeMHNBackend, LoglikelihoodSingleTree +from pmhn._trees._backend_code import TreeMHNBackendCode, TreeWrapperCode import csv import numpy as np import time @@ -54,12 +54,12 @@ def csv_to_numpy(file_path): profiler = cProfile.Profile() profiler.enable() start_time = time.time() -backend = OriginalTreeMHNBackend() +backend = TreeMHNBackendCode() theta_AML_size = len(theta_AML) all_mut_AML = set(i + 1 for i in range(theta_AML_size)) for idx, tree in trees_AML.items(): print(f"Processing tree {idx} of {len(trees_AML)}") - tree_log = LoglikelihoodSingleTree(tree) + tree_log = TreeWrapperCode(tree) log_value = backend.loglikelihood(tree_log, theta_AML, sampling_rate, all_mut_AML) log_vec_py_AML[idx - 1] = log_value print(f"log_value: {log_value}") @@ -67,7 +67,7 @@ def csv_to_numpy(file_path): all_mut_500 = set(i + 1 for i in range(theta_500_size)) for idx, tree in trees_500.items(): print(f"Processing tree {idx} of {len(trees_500)}") - tree_log = LoglikelihoodSingleTree(tree) + tree_log = TreeWrapperCode(tree) log_value = backend.loglikelihood(tree_log, theta_500, sampling_rate, all_mut_500) log_vec_py_500[idx - 1] = log_value print(f"log_value: {log_value}") diff --git a/warmup/likelihood/R_py_loglikelihood_comparison_geno_seed43.py b/warmup/likelihood/R_py_loglikelihood_comparison_geno_seed43.py index ad91e44..932fef0 100644 --- a/warmup/likelihood/R_py_loglikelihood_comparison_geno_seed43.py +++ b/warmup/likelihood/R_py_loglikelihood_comparison_geno_seed43.py @@ -1,6 +1,6 @@ import pandas as pd import pmhn._trees._io as io -from pmhn._trees._backend_geno import OriginalTreeMHNBackend, LoglikelihoodSingleTree +from pmhn._trees._backend_code import TreeWrapperCode, TreeMHNBackendCode import csv import numpy as np import time @@ -52,12 +52,12 @@ def csv_to_numpy(file_path): log_vec_py_500 = np.empty(len(trees_500)) log_vec_py_1000 = np.empty(len(trees_1000)) start_time = time.time() -backend = OriginalTreeMHNBackend() +backend = TreeMHNBackendCode() theta_AML_size = len(theta_AML) all_mut_AML = set(range(1, theta_AML_size + 1)) for idx, tree in trees_AML.items(): print(f"Processing tree {idx} of {len(trees_AML)}") - tree_log = LoglikelihoodSingleTree(tree) + tree_log = TreeWrapperCode(tree) log_value = backend.loglikelihood(tree_log, theta_AML, sampling_rate, all_mut_AML) log_vec_py_AML[idx - 1] = log_value print(f"log_value: {log_value}") @@ -65,7 +65,7 @@ def csv_to_numpy(file_path): all_mut_500 = set(range(1, theta_500_size + 1)) for idx, tree in trees_500.items(): print(f"Processing tree {idx} of {len(trees_500)}") - tree_log = LoglikelihoodSingleTree(tree) + tree_log = TreeWrapperCode(tree) log_value = backend.loglikelihood(tree_log, theta_500, sampling_rate, all_mut_500) log_vec_py_500[idx - 1] = log_value print(f"log_value: {log_value}") @@ -73,7 +73,7 @@ def csv_to_numpy(file_path): all_mut_1000 = set(range(1, theta_1000_size + 1)) for idx, tree in trees_1000.items(): print(f"Processing tree {idx} of {len(trees_1000)}") - tree_log = LoglikelihoodSingleTree(tree) + tree_log = TreeWrapperCode(tree) log_value = backend.loglikelihood(tree_log, theta_1000, sampling_rate, all_mut_1000) log_vec_py_1000[idx - 1] = log_value print(f"log_value: {log_value}") From 04a27ef886446a52a51295a5e0e0ec02c9136a36 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pawe=C5=82=20Czy=C5=BC?= Date: Mon, 30 Oct 2023 12:24:51 +0100 Subject: [PATCH 45/45] Ignore Pyright false positive --- tests/ppl/test_multiplemhn.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/ppl/test_multiplemhn.py b/tests/ppl/test_multiplemhn.py index 962e52b..f3d7704 100644 --- a/tests/ppl/test_multiplemhn.py +++ b/tests/ppl/test_multiplemhn.py @@ -16,7 +16,7 @@ def test_loglikelihood(n_patients: int, n_genes: int) -> None: mutations = rng.binomial(1, 0.5, size=(n_patients, n_genes)) thetas = rng.normal(size=(n_patients, n_genes, n_genes)) - loglikelihood = np.sum( + loglikelihood = np.sum( # pyright: ignore [ lmhn.MHNCythonBackend().gradient_and_loglikelihood( mutations=mutations[i].reshape((1, -1)), theta=thetas[i]