diff --git a/Dockerfile b/Dockerfile index 528fe6d..62f0ff3 100644 --- a/Dockerfile +++ b/Dockerfile @@ -19,7 +19,7 @@ ENV POETRY_CACHE_DIR="/root/.cache/pypoetry" \ POETRY_NO_INTERACTION=1 \ POETRY_VIRTUALENVS_CREATE=false \ POETRY_VIRTUALENVS_IN_PROJECT=false \ - POETRY_VERSION=1.7.1 + POETRY_VERSION=1.8.2 RUN python -m pip install -U pip setuptools wheel RUN python -m pip install poetry==$POETRY_VERSION diff --git a/examples/lightning_example/Readme.md b/examples/lightning_example/Readme.md new file mode 100644 index 0000000..ba5360e --- /dev/null +++ b/examples/lightning_example/Readme.md @@ -0,0 +1,25 @@ +# Minimal Lightning Example + +This is an example of how to implement a custom project using the causica package and its PyTorch Lightning interface. +This example is based upon the [Sales Notebook](../multi_investment_sales_attribution.ipynb). + +You can run the example code using the following: + +```bash +PYTHONPATH="." python -m causica.lightning.main \ +--config config/training.yaml \ +--data config/data.yaml \ +--model config/model.yaml \ +--trainer.accelerator gpu +``` + +## Code: +The code is structured as follows: + +- config: Lightning related configs + - data.yaml: Config related to the dataloader + - model.yaml: Config related to the model parameters + - training.yaml: Config related to the training parameters +- data_module.py: The data module for the Lightning model. This loads the data from storage, splits it into train, validation, and test set -- and lastly, creates the dataloaders. +- lightning_module.py: The Lightning module that defines the model, loss, and optimizer used for training. +- model_analysis.ipynb: Sample notebook to analyze the trained model. diff --git a/examples/lightning_example/config/data.yaml b/examples/lightning_example/config/data.yaml new file mode 100644 index 0000000..96312c1 --- /dev/null +++ b/examples/lightning_example/config/data.yaml @@ -0,0 +1,10 @@ +class_path: data_module.ExampleDataModule +init_args: + dataset_name: "Example dataset" + root_path: "https://azuastoragepublic.z6.web.core.windows.net/causal_ai_suite" + batch_size: 128 + standardize: true + log_normalize: + - "Revenue" + default_offset: 1.0 + log_normalize_min_margin: 5e-4 \ No newline at end of file diff --git a/examples/lightning_example/config/model.yaml b/examples/lightning_example/config/model.yaml new file mode 100644 index 0000000..a2a096a --- /dev/null +++ b/examples/lightning_example/config/model.yaml @@ -0,0 +1,15 @@ +class_path: lightning_module.ExampleDECIModule +init_args: + noise_dist: GAUSSIAN + prior_sparsity_lambda: 43.0 + init_rho: 30.0 + init_alpha: 0.20 + auglag_config: + class_path: causica.training.auglag.AugLagLRConfig + init_args: + max_inner_steps: 3400 + max_outer_steps: 8 + lr_init_dict: + functional_relationships: 3e-4 + noise_dist: 0.0070 + vardist: 0.0098 diff --git a/examples/lightning_example/config/training.yaml b/examples/lightning_example/config/training.yaml new file mode 100644 index 0000000..08ce706 --- /dev/null +++ b/examples/lightning_example/config/training.yaml @@ -0,0 +1,15 @@ +# This recreates the latest run: +# The seed of the run was: 65384781 +seed_everything: true +trainer: + max_epochs: 2000 + check_val_every_n_epoch: 10 +best_checkpoint_callback: + dirpath: "./outputs" + filename: "best_model" + save_top_k: 1 + mode: "max" + monitor: "batch_log_prob" + every_n_train_steps: 1 +last_checkpoint_callback: + save_last: true \ No newline at end of file diff --git a/examples/lightning_example/data_module.py b/examples/lightning_example/data_module.py new file mode 100644 index 0000000..6cff925 --- /dev/null +++ b/examples/lightning_example/data_module.py @@ -0,0 +1,191 @@ +import logging +import os +from collections import defaultdict +from operator import itemgetter + +import fsspec +import networkx as nx +import numpy as np +import pandas as pd +import torch +from tensordict import TensorDict +from torch.utils.data import DataLoader + +from causica.datasets.causica_dataset_format import ( + CounterfactualWithEffects, + InterventionWithEffects, + VariablesMetadata, +) +from causica.datasets.causica_dataset_format.load import ( + convert_one_hot, + get_categorical_sizes, + tensordict_from_variables_metadata, +) +from causica.datasets.interventional_data import CounterfactualData, InterventionData +from causica.datasets.tensordict_utils import identity, tensordict_shapes +from causica.datasets.variable_types import VariableTypeEnum +from causica.distributions.transforms import JointTransformModule +from causica.lightning.data_modules.variable_spec_data import VariableSpecDataModule + +logger = logging.getLogger(__name__) + + +def get_intervention_and_cfs(df: pd.DataFrame) -> tuple[list[CounterfactualWithEffects], list[InterventionWithEffects]]: + outcome = "Revenue" + treatment_columns = ["Tech Support", "Discount", "New Engagement Strategy"] + + # Generate CounterfactualData and InterventionData objects for the test set + cf_data: list[CounterfactualWithEffects] = [] + intervention_data: list[InterventionWithEffects] = [] + observations = df.loc[:, "Global Flag":"Revenue"] # type: ignore + for treatment in treatment_columns: + ite_values = df.loc[:, f"Total Treatment Effect: {treatment}"].values + interventions_with_effect: list[InterventionData | set[str] | None] = [] + for treatment_value in [0, 1]: + treatment_mask = df[treatment] == 1 - treatment_value + factual_td = TensorDict( + {key: observations[key].values[treatment_mask][..., None] for key in observations.columns}, + batch_size=(treatment_mask.sum(),), + ) + cf_td = factual_td.clone() + cf_td[treatment] = 1 - cf_td[treatment] + # Subtracting ITE from the factual outcome for negative treatments and adding it for positive treatments + masked_ite_values = ite_values[treatment_mask] + offset = (cf_td[treatment] * 2 - 1) * masked_ite_values + cf_td[outcome] = factual_td[outcome] + offset + + treatment_td = TensorDict({treatment: torch.tensor([treatment_value])}, batch_size=[]) + + cf_data.append( + ( + CounterfactualData( + factual_data=factual_td, counterfactual_data=cf_td, intervention_values=treatment_td + ), + None, + {outcome}, + ) + ) + interventions_with_effect.append( + InterventionData( + intervention_data=cf_td, + intervention_values=treatment_td, + condition_values=TensorDict({}, batch_size=[]), + ) + ) + interventions_with_effect.append({outcome}) + intervention_data.append(tuple(interventions_with_effect)) # type: ignore + + return cf_data, intervention_data + + +def get_constraint_prior(node_name_to_idx: dict[str, int]) -> np.ndarray: + num_nodes = len(node_name_to_idx) + constraint_matrix = np.full((num_nodes, num_nodes), np.nan, dtype=np.float32) + + revenue_idx = node_name_to_idx["Revenue"] + planning_summit_idx = node_name_to_idx["Planning Summit"] + constraint_matrix[revenue_idx, :] = 0.0 + constraint_matrix[revenue_idx, planning_summit_idx] = np.nan + + non_child_nodes = [ + "Commercial Flag", + "Major Flag", + "SMC Flag", + "PC Count", + "Employee Count", + "Global Flag", + "Size", + ] + non_child_idxs = itemgetter(*non_child_nodes)(node_name_to_idx) + constraint_matrix[:, non_child_idxs] = 0.0 + + engagement_nodes = ["Tech Support", "Discount", "New Engagement Strategy"] + engagement_idxs = itemgetter(*engagement_nodes)(node_name_to_idx) + for i in engagement_idxs: + constraint_matrix[engagement_idxs, i] = 0.0 + + return constraint_matrix + + +def _get_tensordict_from_df(df: pd.DataFrame, variables_metadata: VariablesMetadata, categorical_sizes) -> TensorDict: + return convert_one_hot( + tensordict_from_variables_metadata(df.to_numpy(), variables_metadata.variables), + one_hot_sizes=categorical_sizes, + ) + + +class ExampleDataModule(VariableSpecDataModule): + """Example of a lightning data module. + + This data module loads the multi-attribution dataset and prepares it for training. It uses the custom functions + `get_intervention_and_cfs` and `get_constraint_prior` to generate CounterfactualData and InterventionData objects + from the full dataframe. The true graph is also loaded from a file, but this is not available in most cases. + + The `prepare_data` handles all the data loading and preprocessing. In practice, this can be customised to any + scenario however appropriate. + """ + + def prepare_data(self): + # Load metadata telling us the data type of each column + variables_path = os.path.join(self.root_path, "multi_attribution_data_20220819_data_types.json") + with fsspec.open(variables_path, mode="r", encoding="utf-8") as f: + self.variables_metadata = VariablesMetadata.from_json(f.read()) + self.categorical_sizes = get_categorical_sizes(variables_list=self.variables_metadata.variables) + continuous_variables = [ + spec.name for spec in self.variables_metadata.variables if spec.type == VariableTypeEnum.CONTINUOUS + ] + + # Load the data as a DataFrame + df = pd.read_csv(os.path.join(self.root_path, "multi_attribution_data_20220819.csv")) + df[continuous_variables] = df[continuous_variables].astype(float) + + # Load the true graph. In most cases, this will not be available. + adjacency_path = os.path.join(self.root_path, "true_graph_gml_string.txt") + with fsspec.open(adjacency_path, mode="r", encoding="utf-8") as f: + self.true_adj = torch.tensor(nx.to_numpy_array(nx.parse_gml(f.read()))) + + # Split into train, validation, and test sets + shuffled_df = df.sample(frac=1, random_state=1337) + train_df, valid_df, test_df = np.split(shuffled_df, [int(0.7 * len(df)), int(0.8 * len(df))]) + + train_df = train_df.loc[:, "Global Flag":"Revenue"] + valid_df = valid_df.loc[:, "Global Flag":"Revenue"] + + # Convert the data to TensorDicts + self._dataset_train = _get_tensordict_from_df(train_df, self.variables_metadata, self.categorical_sizes) + self._dataset_valid = _get_tensordict_from_df(valid_df, self.variables_metadata, self.categorical_sizes) + self._dataset_test = _get_tensordict_from_df( + test_df.loc[:, "Global Flag":"Revenue"], self.variables_metadata, self.categorical_sizes + ) + + # Generate CounterfactualData and InterventionData objects for the test set + # In most cases this won't be available. + self.counterfactuals, self.interventions = get_intervention_and_cfs(test_df) + + # Generate the constraint / prior + node_name_to_idx = {key: i for i, key in enumerate(train_df.columns)} + self.constraint_prior = get_constraint_prior(node_name_to_idx) + + # Set up utility variables + self._variable_shapes = tensordict_shapes(self._dataset_train) + self._variable_types = {var.group_name: var.type for var in self.variables_metadata.variables} + self._column_names = defaultdict(list) + for variable in self.variables_metadata.variables: + self._column_names[variable.group_name].append(variable.name) + + # Normalize the data + if self.use_normalizer: + # Only applied to continuous variables + normalization_variables = {k for k, v in self._variable_types.items() if v == VariableTypeEnum.CONTINUOUS} + self.normalizer = self.create_normalizer(normalization_variables)( + self._dataset_train.select(*normalization_variables) + ) + self.normalize_data() + else: + self.normalizer = JointTransformModule({}) + + def test_dataloader(self): + return DataLoader(dataset=self.dataset_test, collate_fn=identity, batch_size=self.batch_size) + + def val_dataloader(self): + return DataLoader(dataset=self.dataset_valid, collate_fn=identity, batch_size=self.batch_size) diff --git a/examples/lightning_example/lightning_module.py b/examples/lightning_example/lightning_module.py new file mode 100644 index 0000000..188d5ba --- /dev/null +++ b/examples/lightning_example/lightning_module.py @@ -0,0 +1,188 @@ +import logging +from typing import Any, Optional, Sequence + +import numpy as np +import pytorch_lightning as pl +import torch +from data_module import ExampleDataModule +from pytorch_lightning.callbacks import LearningRateMonitor +from tensordict import TensorDict +from torchmetrics import MetricCollection +from torchmetrics.classification import Accuracy, F1Score +from torchmetrics.regression import MeanAbsoluteError, MeanAbsolutePercentageError +from torchmetrics.wrappers import MultitaskWrapper + +from causica.datasets.normalization import infer_compatible_log_normalizer_from_checkpoint +from causica.datasets.tensordict_utils import expand_tensordict_groups +from causica.graph.evaluation_metrics import adjacency_f1, orientation_f1 +from causica.lightning.modules.deci_module import DECIModule +from causica.training.evaluation import list_logsumexp, list_mean + +logger = logging.getLogger(__name__) + + +class ExampleDECIModule(DECIModule): + NUM_GRAPH_SAMPLES = 5 + + def prepare_data(self) -> None: + super().prepare_data() + datamodule = getattr(self.trainer, "datamodule", None) + if not isinstance(datamodule, ExampleDataModule): + raise ValueError("The trainer must have a ExampleDataModule.") + + def configure_callbacks(self) -> Sequence[pl.Callback]: + """Create a callback for the auglag callback.""" + callback = super().configure_callbacks() + callbacks_list = [callback] if isinstance(callback, pl.Callback) else list(callback) + return callbacks_list + [LearningRateMonitor(logging_interval="step")] + + def on_load_checkpoint(self, checkpoint: dict[str, Any]) -> None: + """Ensure correct loading of the normalizer from the checkpoint.""" + self.normalizer = infer_compatible_log_normalizer_from_checkpoint(checkpoint["state_dict"]) + + def setup(self, stage: Optional[str] = None): + if self.is_setup: + return # Already setup + + super().setup(stage) + + if stage is None: + # Skip the rest as we are loading from a checkpoint + return + + assert self.variable_types is not None + + if not hasattr(self.trainer, "datamodule"): + raise ValueError("Trainer must have a datamodule.") + + datamodule = getattr(self.trainer, "datamodule", None) + assert isinstance(datamodule, ExampleDataModule) + + if datamodule.normalizer is None: + raise ValueError("Data module must have a normalizer.") + self.normalizer = datamodule.normalizer + self.variable_names: dict[str, list[str]] = datamodule.column_names + # feature level metrics + self.metrics_wrapper_all = torch.nn.ModuleDict( + { + "validation": MultitaskWrapper( + { + "Revenue": MetricCollection( + {"mape": MeanAbsolutePercentageError(), "mae": MeanAbsoluteError()}, postfix=".Revenue" + ), + "Discount": MetricCollection( + {"accuracy": Accuracy(task="binary"), "f1": F1Score(task="binary")}, postfix=".Discount" + ), + } + ), + "test": MultitaskWrapper( + { + "Revenue": MetricCollection( + {"mape": MeanAbsolutePercentageError(), "mae": MeanAbsoluteError()}, postfix=".Revenue" + ), + "Discount": MetricCollection( + {"accuracy": Accuracy(task="binary"), "f1": F1Score(task="binary")}, postfix=".Discount" + ), + } + ), + } + ) + + def validation_step(self, batch, batch_idx, dataloader_idx=0): + _ = dataloader_idx + dataset_size = self.trainer.datamodule.dataset_valid.batch_size # type: ignore + return self.observational_evaluation_step( + batch, + batch_idx, + log_prefix="valid_eval", + dataset_size=dataset_size, + metrics_wrapper_dict=self.metrics_wrapper_all["validation"], + ) + + def test_step(self, batch, batch_idx, dataloader_idx=0): + _ = dataloader_idx + dataset_size = self.trainer.datamodule.dataset_test.batch_size # type: ignore + return self.observational_evaluation_step( + batch, + batch_idx, + log_prefix="test", + dataset_size=dataset_size, + metrics_wrapper_dict=self.metrics_wrapper_all["test"], + ) + + def on_test_epoch_start(self) -> None: + if getattr(self, "trainer", None) is not None and hasattr(self.trainer, "datamodule"): + true_adj_matrix = self.trainer.datamodule.true_adj + self.test_graph(true_adj_matrix) + + return super().on_test_epoch_start() + + def test_graph(self, true_adj_matrix): + """Evaluate the graph reconstruction performance of the model against the true graph.""" + sem_samples = self.sem_module().sample(torch.Size([self.NUM_GRAPH_SAMPLES])) + graph_samples = [sem.graph for sem in sem_samples] + + true_adj_matrix = true_adj_matrix.to(graph_samples[0].device) + + adj_f1 = list_mean([adjacency_f1(true_adj_matrix, graph) for graph in graph_samples]).item() + orient_f1 = list_mean([orientation_f1(true_adj_matrix, graph) for graph in graph_samples]).item() + graph_metrics = {"test.graph.adjacency.f1": adj_f1, "test.graph.orientation.f1": orient_f1} + self.log_dict(graph_metrics, add_dataloader_idx=False, on_epoch=True) + return graph_metrics + + @torch.no_grad() + def observational_evaluation_step( + self, + batch: TensorDict, + batch_idx: int, + log_prefix: str = "metrics", + dataset_size: torch.Size = torch.Size([1]), + metrics_wrapper_dict: Optional[MultitaskWrapper] = None, + ): + """Evaluate the log prob of the model for one batch using multiple graph samples. + Args: + metrics_wrapper_dict: dict containing MultiTaskWrapper obj for evaluation. + """ + _ = batch_idx + + assert len(log_prefix) > 0, "log_prefix must be a non-empty string" + + # This is required for binary variables + batch = batch.apply(lambda t: t.to(torch.float32)) + sems = self.sem_module().sample(torch.Size([self.NUM_GRAPH_SAMPLES])) + assert len(dataset_size) == 1, "Only one batch size is supported" + + # estimate log prob for each sample using graph samples and report the mean over graphs + test_logprob = list_logsumexp([sem.log_prob(batch) for sem in sems]) - np.log(self.NUM_GRAPH_SAMPLES) + + stacked: TensorDict = torch.stack([sem.func(batch, sem.graph) for sem in sems]) + mean_predictions = stacked.apply(lambda v: v.mean(axis=0), batch_size=batch.batch_size, inplace=False) + + assert self.normalizer is not None + + mean_predictions_orig = self.normalizer.inv(mean_predictions) + observations_orig = self.normalizer.inv(batch) + + # group_variable_names + mean_predictions_orig = expand_tensordict_groups(mean_predictions_orig, self.variable_names) + observations_orig = expand_tensordict_groups(observations_orig, self.variable_names) + mean_predictions = expand_tensordict_groups(mean_predictions, self.variable_names) + + assert metrics_wrapper_dict is not None + metrics_wrapper_dict.forward(mean_predictions_orig.to_dict(), observations_orig.to_dict()) + self.log( + f"{log_prefix}.test_LL", + torch.sum(test_logprob, dim=-1).item() / dataset_size[0], + reduce_fx=sum, + add_dataloader_idx=False, + ) + + self.log_dict( + { + f"{log_prefix}.{key}": val + for variable_metric in metrics_wrapper_dict.task_metrics.values() + for key, val in variable_metric.items() + }, + add_dataloader_idx=False, + on_epoch=True, + ) diff --git a/examples/lightning_example/model_analysis.ipynb b/examples/lightning_example/model_analysis.ipynb new file mode 100644 index 0000000..6e947cb --- /dev/null +++ b/examples/lightning_example/model_analysis.ipynb @@ -0,0 +1,222 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# Model Analysis Example\n", + "This notebook provides some example code for analysing the the behaviour of a model that has been trained using the custom lightning module." + ] + }, + { + "cell_type": "code", + "execution_count": 7, + "metadata": {}, + "outputs": [], + "source": [ + "# imports\n", + "from data_module import ExampleDataModule\n", + "from lightning_module import ExampleDECIModule\n", + "import matplotlib.pyplot as plt\n", + "import networkx as nx\n", + "\n", + "\n", + "def print_connections(model):\n", + " variable_groups = list(model.variable_types.keys())\n", + " adj_mat = model.sem_module().mode.graph.detach().cpu().numpy()\n", + "\n", + " for i, row in enumerate(adj_mat):\n", + " for j, col in enumerate(row):\n", + " if col > 0:\n", + " print(variable_groups[i], \"->\", variable_groups[j])" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "The next cell loads the model and data from the saved checkpoint." + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "metadata": {}, + "outputs": [ + { + "name": "stderr", + "output_type": "stream", + "text": [ + "INFO:torch.distributed.nn.jit.instantiator:Created a temporary directory at /tmp/tmpvp5l58zh\n", + "INFO:torch.distributed.nn.jit.instantiator:Writing /tmp/tmpvp5l58zh/_remote_module_non_scriptable.py\n", + "INFO:torch.distributed.nn.jit.instantiator:Writing /tmp/tmpvp5l58zh/_remote_module_non_scriptable.py\n" + ] + } + ], + "source": [ + "data_module = ExampleDataModule.load_from_checkpoint(\"outputs/last.ckpt\")\n", + "model = ExampleDECIModule.load_from_checkpoint(\"outputs/last.ckpt\")\n", + "\n", + "data_module.prepare_data()" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "We can then print the learned graph using the custom `print_connections` function." + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "IT Spend -> New Engagement Strategy\n", + "IT Spend -> Planning Summit\n", + "Employee Count -> Discount\n", + "Employee Count -> New Engagement Strategy\n", + "Employee Count -> New Product Adoption\n", + "PC Count -> Employee Count\n", + "PC Count -> Tech Support\n", + "PC Count -> New Product Adoption\n", + "PC Count -> Revenue\n", + "Size -> IT Spend\n", + "Size -> Planning Summit\n", + "Size -> Revenue\n", + "Tech Support -> New Product Adoption\n", + "Tech Support -> Revenue\n", + "Discount -> IT Spend\n", + "Discount -> Size\n", + "Discount -> Tech Support\n", + "New Engagement Strategy -> Size\n", + "New Engagement Strategy -> Planning Summit\n", + "New Engagement Strategy -> Revenue\n", + "Planning Summit -> Commercial Flag\n", + "Revenue -> Commercial Flag\n", + "Revenue -> IT Spend\n", + "Revenue -> Discount\n", + "Revenue -> New Product Adoption\n" + ] + } + ], + "source": [ + "print_connections(model)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "We can also use networkx to plot the graph." + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "metadata": {}, + "outputs": [], + "source": [ + "sem = model.sem_module().mode\n", + "graph = sem.graph" + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "metadata": {}, + "outputs": [], + "source": [ + "graph = nx.from_numpy_array(sem.graph.cpu().numpy(), create_using=nx.DiGraph)\n", + "graph = nx.relabel_nodes(graph, dict(enumerate(data_module.dataset_train.keys())))" + ] + }, + { + "cell_type": "code", + "execution_count": 6, + "metadata": {}, + "outputs": [ + { + "data": { + "image/png": "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", + "text/plain": [ + "
" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "fig, axis = plt.subplots(1, 1, figsize=(8, 8))\n", + "true_adj = nx.from_numpy_array(data_module.true_adj.numpy(), create_using=nx.DiGraph)\n", + "true_adj = nx.relabel_nodes(true_adj, dict(enumerate(data_module.dataset_train.keys())))\n", + "labels = {node: i for i, node in enumerate(data_module.dataset_train.keys())}\n", + "\n", + "try:\n", + " layout = nx.nx_agraph.graphviz_layout(true_adj, prog=\"dot\")\n", + "except (ModuleNotFoundError, ImportError):\n", + " layout = nx.layout.spring_layout(true_adj)\n", + "\n", + "for node, i in labels.items():\n", + " axis.scatter(layout[node][0], layout[node][1], label=f\"{i}: {node}\")\n", + "axis.legend()\n", + "nx.draw_networkx(true_adj, pos=layout, with_labels=True, arrows=True, labels=labels, ax=axis)" + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "metadata": {}, + "outputs": [ + { + "data": { + "image/png": "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", + "text/plain": [ + "
" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "graph = nx.from_numpy_array(sem.graph.cpu().numpy(), create_using=nx.DiGraph)\n", + "graph = nx.relabel_nodes(graph, dict(enumerate(data_module.dataset_train.keys())))\n", + "\n", + "fig, axis = plt.subplots(1, 1, figsize=(8, 8))\n", + "\n", + "for node, i in labels.items():\n", + " axis.scatter(layout[node][0], layout[node][1], label=f\"{i}: {node}\")\n", + "axis.legend()\n", + "\n", + "nx.draw_networkx(graph, pos=layout, with_labels=True, arrows=True, labels=labels, ax=axis)" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.10.12" + } + }, + "nbformat": 4, + "nbformat_minor": 2 +} diff --git a/examples/rhino_ecoli_example.ipynb b/examples/rhino_ecoli_example.ipynb new file mode 100644 index 0000000..7a09249 --- /dev/null +++ b/examples/rhino_ecoli_example.ipynb @@ -0,0 +1,438 @@ +{ + "cells": [ + { + "attachments": {}, + "cell_type": "markdown", + "metadata": {}, + "source": [ + "An example notebook showing how to train a [Rhino](https://arxiv.org/abs/2210.14706) model on Ecoli Data.\n", + "\n", + "This demonstrates how to assemble the various components of the library and how to perform training." + ] + }, + { + "cell_type": "code", + "execution_count": 1, + "metadata": { + "metadata": {} + }, + "outputs": [], + "source": [ + "import os\n", + "from dataclasses import dataclass\n", + "\n", + "import networkx as nx\n", + "import numpy as np\n", + "import pytorch_lightning as pl\n", + "import torch\n", + "from tensordict import TensorDict\n", + "from torch.utils.data import DataLoader\n", + "\n", + "from causica.datasets.causica_dataset_format import CAUSICA_DATASETS_PATH, DataEnum, load_data\n", + "from causica.datasets.tensordict_utils import (\n", + " tensordict_shapes,\n", + ")\n", + "from causica.datasets.timeseries_dataset import IndexedTimeseriesDataset\n", + "from causica.distributions import (\n", + " AdjacencyDistribution,\n", + " ContinuousNoiseDist,\n", + " DistributionModule,\n", + " ENCOAdjacencyDistributionModule,\n", + " GibbsDAGPrior,\n", + " JointNoiseModule,\n", + " create_noise_modules,\n", + " RhinoLaggedAdjacencyDistributionModule,\n", + " TemporalAdjacencyDistributionModule,\n", + ")\n", + "from causica.datasets.variable_types import VariableTypeEnum\n", + "from causica.functional_relationships import TemporalEmbedFunctionalRelationships\n", + "from causica.graph.dag_constraint import calculate_dagness\n", + "from causica.sem.sem_distribution import TemporalSEMDistributionModule\n", + "from causica.sem.temporal_distribution_parameters_sem import (\n", + " concatenate_lagged_and_instaneous_values,\n", + " split_lagged_and_instanteneous_values,\n", + ")\n", + "from causica.training.auglag import AugLagLossCalculator, AugLagLR, AugLagLRConfig" + ] + }, + { + "attachments": {}, + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Define various parameters of the training process." + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "metadata": {}, + "outputs": [], + "source": [ + "data_path = \"https://azuastoragepublic.z6.web.core.windows.net/Ecoli1_100/train.csv\"\n", + "adj_path = \"https://azuastoragepublic.z6.web.core.windows.net/Ecoli1_100/adj_matrix.npy\"\n", + "\n", + "device = \"cpu\"\n", + "dataset_train = IndexedTimeseriesDataset(series_index_key=0, data=data_path, adjacency_matrix=adj_path, device=device)\n", + "dataloader_train = DataLoader(\n", + " dataset=dataset_train,\n", + " collate_fn=torch.stack,\n", + " batch_size=int(os.environ.get(\"TEST_RUN\", 8)),\n", + " shuffle=True,\n", + " drop_last=True,\n", + ")" + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "metadata": {}, + "outputs": [], + "source": [ + "num_nodes = len(dataset_train._data.keys())\n", + "context_length = 21\n", + "lags = 21 - 1\n", + "\n", + "prior = GibbsDAGPrior(num_nodes=num_nodes, sparsity_lambda=0.3, context_length=context_length)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Create the Variational Posterior Distribution over Adjacency Matrices, which we will optimize." + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "metadata": {}, + "outputs": [], + "source": [ + "adjacency_dist: DistributionModule[AdjacencyDistribution] = TemporalAdjacencyDistributionModule(\n", + " ENCOAdjacencyDistributionModule(num_nodes),\n", + " RhinoLaggedAdjacencyDistributionModule(num_nodes, lags),\n", + ")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Create the Graph Neural network that will estimate the functional relationships. More info can be found [here](https://openreview.net/forum?id=S2pNPZM-w-f)." + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "metadata": {}, + "outputs": [], + "source": [ + "functional_relationships = TemporalEmbedFunctionalRelationships(\n", + " shapes=tensordict_shapes(dataset_train._data),\n", + " embedding_size=32,\n", + " out_dim_g=32,\n", + " num_layers_g=2,\n", + " num_layers_zeta=2,\n", + " context_length=context_length,\n", + ")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Create the Noise Distributions for each node assuming they are all continuous." + ] + }, + { + "cell_type": "code", + "execution_count": 6, + "metadata": {}, + "outputs": [], + "source": [ + "variable_shapes = tensordict_shapes(dataset_train._data)\n", + "types_dict = {key: VariableTypeEnum.CONTINUOUS for key in dataset_train._data.keys()}\n", + "\n", + "noise_submodules = create_noise_modules(variable_shapes, types_dict, ContinuousNoiseDist.GAUSSIAN)\n", + "noise_module = JointNoiseModule(noise_submodules)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Create the SEM Module which combines the variational adjacency distribution, the functional relationships and the noise distributions for each node." + ] + }, + { + "cell_type": "code", + "execution_count": 7, + "metadata": {}, + "outputs": [], + "source": [ + "sem_module: TemporalSEMDistributionModule = TemporalSEMDistributionModule(\n", + " adjacency_dist, functional_relationships, noise_module\n", + ")\n", + "sem_module = sem_module.to(device)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Define the [Augmented Lagrangian Scheduler](https://en.wikipedia.org/wiki/Augmented_Lagrangian_method).\n", + "\n", + "This allows Rhino to optimize towards a DAG, by slowly increasing the alpha and rho parameters as the optimization takes\n", + "place." + ] + }, + { + "cell_type": "code", + "execution_count": 8, + "metadata": {}, + "outputs": [], + "source": [ + "lr_init_dict = {\n", + " \"functional_relationships\": 3e-3,\n", + " \"vardist_inst\": 1e-2,\n", + " \"vardist_lagged\": 1e-2,\n", + " \"noise_dist\": 3e-4,\n", + "}\n", + "\n", + "auglag_config = AugLagLRConfig(lr_init_dict=lr_init_dict)\n", + "scheduler = AugLagLR(config=auglag_config)\n", + "auglag_loss = AugLagLossCalculator(init_alpha=0.0, init_rho=1.0)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Create the optimizer, with separate learning rates for each module." + ] + }, + { + "cell_type": "code", + "execution_count": 9, + "metadata": {}, + "outputs": [], + "source": [ + "modules = {\n", + " \"functional_relationships\": sem_module.functional_relationships,\n", + " \"vardist_inst\": sem_module.adjacency_module.inst_dist_module,\n", + " \"vardist_lagged\": sem_module.adjacency_module.lagged_dist_module,\n", + " \"noise_dist\": sem_module.noise_module,\n", + "}\n", + "\n", + "parameter_list = [\n", + " {\"params\": module.parameters(), \"lr\": auglag_config.lr_init_dict[name], \"name\": name}\n", + " for name, module in modules.items()\n", + "]\n", + "\n", + "optimizer = torch.optim.Adam(parameter_list)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "The main training loop.\n", + "\n", + "For each batch, we:\n", + "* Sample a graph from the SEM.\n", + "* Calculate the log probability of that batch, given the graph.\n", + "* Create the ELBO to be optimized.\n", + "* Calculate the DAG constraint\n", + "* Combine the DAG constraint with the ELBO to get the loss." + ] + }, + { + "cell_type": "code", + "execution_count": 10, + "metadata": { + "metadata": {} + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "epoch:0 loss:2.3792e+17 nll:123.08 dagness:31731050496.00000 num_edges:102465 alpha:0 rho:1 step:0|1 num_lr_updates:0\n", + "\n", + "epoch:10 loss:2.1545e+17 nll:90.606 dagness:30195685376.00000 num_edges:96460 alpha:0 rho:1 step:0|51 num_lr_updates:0\n" + ] + } + ], + "source": [ + "@dataclass(frozen=True)\n", + "class TrainingConfig:\n", + " noise_dist: ContinuousNoiseDist = ContinuousNoiseDist.SPLINE\n", + " batch_size: int = int(os.environ.get(\"TEST_RUN\", 128))\n", + " max_epoch: int = int(os.environ.get(\"TEST_RUN\", 20)) # used by testing to run the notebook as a script\n", + " gumbel_temp: float = 0.25\n", + " averaging_period: int = 10\n", + " prior_sparsity_lambda: float = 5.0\n", + " init_rho: float = 1.0\n", + " init_alpha: float = 0.0\n", + "\n", + "\n", + "training_config = TrainingConfig()\n", + "\n", + "num_samples = len(dataset_train)\n", + "for epoch in range(training_config.max_epoch):\n", + " for i, batch in enumerate(dataloader_train):\n", + " batch = batch.to_tensordict() # Force dense stacking for tensordict<0.4.0\n", + " batch.batch_size = batch.batch_size[:1] # Do not consider the time axis part of the batch dims\n", + "\n", + " optimizer.zero_grad()\n", + " sem_distribution = sem_module()\n", + " sem, *_ = sem_distribution.relaxed_sample(\n", + " torch.Size([]), temperature=training_config.gumbel_temp\n", + " ) # soft sample\n", + " graph_tries = 0\n", + " while sem.graph.isnan().any() and graph_tries < 2:\n", + " sem, *_ = sem_distribution.relaxed_sample(torch.Size([]), temperature=training_config.gumbel_temp)\n", + " graph_tries += 1\n", + " if sem.graph.isnan().any():\n", + " raise ValueError(f\"Failed to sample a valid graph after {graph_tries} tries\")\n", + " if graph_tries > 0:\n", + " print(f\"Used {graph_tries} tries to sample a valid graph\")\n", + "\n", + " batch_log_prob = sem.log_prob(batch).mean()\n", + " sem_distribution_entropy = sem_distribution.entropy()\n", + " prior_term = prior.log_prob(sem.graph)\n", + " objective = (-sem_distribution_entropy - prior_term) / num_samples - batch_log_prob\n", + " constraint = calculate_dagness(sem.graph[..., -1, :, :]) # Calculate dagness on instantaneous graph\n", + "\n", + " loss = auglag_loss(objective, constraint / num_samples)\n", + "\n", + " loss.backward()\n", + " optimizer.step()\n", + " # update the Auglag parameters\n", + " scheduler.step(\n", + " optimizer=optimizer,\n", + " loss=auglag_loss,\n", + " loss_value=loss,\n", + " lagrangian_penalty=constraint,\n", + " )\n", + " # log metrics\n", + " if epoch % 10 == 0 and i == 0:\n", + " print(\n", + " f\"epoch:{epoch} loss:{loss.item():.5g} nll:{-batch_log_prob.detach().cpu().numpy():.5g} \"\n", + " f\"dagness:{constraint.item():.5f} num_edges:{(sem.graph > 0.0).sum()} \"\n", + " f\"alpha:{auglag_loss.alpha:.5g} rho:{auglag_loss.rho:.5g} \"\n", + " f\"step:{scheduler.outer_opt_counter}|{scheduler.step_counter} \"\n", + " f\"num_lr_updates:{scheduler.num_lr_updates}\"\n", + " )" + ] + }, + { + "cell_type": "code", + "execution_count": 11, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Instantaneous nans: 1 Lagged nans: 16\n", + "\n" + ] + } + ], + "source": [ + "# The above used to throw errors because of NaNs in the lagged graph.\n", + "# This is an ephemeral issue that is not present in the final model.\n", + "inst_nans = np.sum(\n", + " [\n", + " sem_module.adjacency_module.inst_dist_module()\n", + " .relaxed_sample(temperature=training_config.gumbel_temp)\n", + " .isnan()\n", + " .any()\n", + " for _ in range(1000)\n", + " ]\n", + ")\n", + "lagged_nans = np.sum(\n", + " [\n", + " sem_module.adjacency_module.lagged_dist_module()\n", + " .relaxed_sample(temperature=training_config.gumbel_temp)\n", + " .isnan()\n", + " .any()\n", + " for _ in range(1000)\n", + " ]\n", + ")\n", + "\n", + "print(f\"Instantaneous nans: {inst_nans} Lagged nans: {lagged_nans}\")" + ] + }, + { + "cell_type": "code", + "execution_count": 12, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "batch shape: torch.Size([8, 21, 1]), torch.Size([8])\n", + "\n", + "Sample to noise shape: torch.Size([8, 1])\n", + "Sampled noise shape: torch.Size([1])\n", + "Generated sample shape: torch.Size([8, 1])\n" + ] + } + ], + "source": [ + "print(f\"batch shape: {batch[batch.sorted_keys[0]].shape}, {batch.batch_size}\")\n", + "# Sample to noise\n", + "noise = sem.sample_to_noise(batch)\n", + "print(f\"Sample to noise shape: {noise[noise.sorted_keys[0]].shape}\")\n", + "\n", + "# Sampled noise\n", + "sampled_noise = sem.sample_noise()\n", + "print(f\"Sampled noise shape: {sampled_noise[sampled_noise.sorted_keys[0]].shape}\")\n", + "\n", + "# Noise to sample still fails as it requires the history:\n", + "# sem.noise_to_sample(noise)\n", + "\n", + "history, inst = split_lagged_and_instanteneous_values(batch)\n", + "concat_noise = concatenate_lagged_and_instaneous_values(history, noise)\n", + "\n", + "new_sample = sem.noise_to_sample(concat_noise)\n", + "print(f\"Generated sample shape: {new_sample[new_sample.sorted_keys[0]].shape}\")\n", + "# Because sampling requires the history sem.sample() will fail as the history is not provided\n", + "\n", + "intervention_variable = sem.node_names[0]\n", + "intervention = TensorDict({intervention_variable: torch.zeros_like(batch[intervention_variable][0])}, batch_size=[])\n", + "# Interventions currently faily because we are intervening on the instanteneous values but want to use the history for\n", + "# forward predictions.\n", + "# do_sem = sem.do(intervention)\n", + "# do_sample = do_sem.noise_to_sample(concat_noise.clone())\n", + "\n", + "# print(f\"Generated sample after intervention shape: {do_sample[do_sample.sorted_keys[0]].shape}\")" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3 (ipykernel)", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.10.12" + } + }, + "nbformat": 4, + "nbformat_minor": 4 +} diff --git a/poetry.lock b/poetry.lock index 5838d48..15a3c3e 100644 --- a/poetry.lock +++ b/poetry.lock @@ -1,4 +1,4 @@ -# This file is automatically @generated by Poetry 1.7.1 and should not be changed by hand. +# This file is automatically @generated by Poetry 1.8.2 and should not be changed by hand. [[package]] name = "absl-py" @@ -13,87 +13,87 @@ files = [ [[package]] name = "aiohttp" -version = "3.9.3" +version = "3.9.5" description = "Async http client/server framework (asyncio)" optional = false python-versions = ">=3.8" files = [ - {file = "aiohttp-3.9.3-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:939677b61f9d72a4fa2a042a5eee2a99a24001a67c13da113b2e30396567db54"}, - {file = "aiohttp-3.9.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:1f5cd333fcf7590a18334c90f8c9147c837a6ec8a178e88d90a9b96ea03194cc"}, - {file = "aiohttp-3.9.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:82e6aa28dd46374f72093eda8bcd142f7771ee1eb9d1e223ff0fa7177a96b4a5"}, - {file = "aiohttp-3.9.3-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f56455b0c2c7cc3b0c584815264461d07b177f903a04481dfc33e08a89f0c26b"}, - {file = "aiohttp-3.9.3-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:bca77a198bb6e69795ef2f09a5f4c12758487f83f33d63acde5f0d4919815768"}, - {file = "aiohttp-3.9.3-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e083c285857b78ee21a96ba1eb1b5339733c3563f72980728ca2b08b53826ca5"}, - {file = "aiohttp-3.9.3-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ab40e6251c3873d86ea9b30a1ac6d7478c09277b32e14745d0d3c6e76e3c7e29"}, - {file = "aiohttp-3.9.3-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:df822ee7feaaeffb99c1a9e5e608800bd8eda6e5f18f5cfb0dc7eeb2eaa6bbec"}, - {file = "aiohttp-3.9.3-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:acef0899fea7492145d2bbaaaec7b345c87753168589cc7faf0afec9afe9b747"}, - {file = "aiohttp-3.9.3-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:cd73265a9e5ea618014802ab01babf1940cecb90c9762d8b9e7d2cc1e1969ec6"}, - {file = "aiohttp-3.9.3-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:a78ed8a53a1221393d9637c01870248a6f4ea5b214a59a92a36f18151739452c"}, - {file = "aiohttp-3.9.3-cp310-cp310-musllinux_1_1_s390x.whl", hash = "sha256:6b0e029353361f1746bac2e4cc19b32f972ec03f0f943b390c4ab3371840aabf"}, - {file = "aiohttp-3.9.3-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:7cf5c9458e1e90e3c390c2639f1017a0379a99a94fdfad3a1fd966a2874bba52"}, - {file = "aiohttp-3.9.3-cp310-cp310-win32.whl", hash = "sha256:3e59c23c52765951b69ec45ddbbc9403a8761ee6f57253250c6e1536cacc758b"}, - {file = "aiohttp-3.9.3-cp310-cp310-win_amd64.whl", hash = "sha256:055ce4f74b82551678291473f66dc9fb9048a50d8324278751926ff0ae7715e5"}, - {file = "aiohttp-3.9.3-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:6b88f9386ff1ad91ace19d2a1c0225896e28815ee09fc6a8932fded8cda97c3d"}, - {file = "aiohttp-3.9.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:c46956ed82961e31557b6857a5ca153c67e5476972e5f7190015018760938da2"}, - {file = "aiohttp-3.9.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:07b837ef0d2f252f96009e9b8435ec1fef68ef8b1461933253d318748ec1acdc"}, - {file = "aiohttp-3.9.3-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:dad46e6f620574b3b4801c68255492e0159d1712271cc99d8bdf35f2043ec266"}, - {file = "aiohttp-3.9.3-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5ed3e046ea7b14938112ccd53d91c1539af3e6679b222f9469981e3dac7ba1ce"}, - {file = "aiohttp-3.9.3-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:039df344b45ae0b34ac885ab5b53940b174530d4dd8a14ed8b0e2155b9dddccb"}, - {file = "aiohttp-3.9.3-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7943c414d3a8d9235f5f15c22ace69787c140c80b718dcd57caaade95f7cd93b"}, - {file = "aiohttp-3.9.3-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:84871a243359bb42c12728f04d181a389718710129b36b6aad0fc4655a7647d4"}, - {file = "aiohttp-3.9.3-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:5eafe2c065df5401ba06821b9a054d9cb2848867f3c59801b5d07a0be3a380ae"}, - {file = "aiohttp-3.9.3-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:9d3c9b50f19704552f23b4eaea1fc082fdd82c63429a6506446cbd8737823da3"}, - {file = "aiohttp-3.9.3-cp311-cp311-musllinux_1_1_ppc64le.whl", hash = "sha256:f033d80bc6283092613882dfe40419c6a6a1527e04fc69350e87a9df02bbc283"}, - {file = "aiohttp-3.9.3-cp311-cp311-musllinux_1_1_s390x.whl", hash = "sha256:2c895a656dd7e061b2fd6bb77d971cc38f2afc277229ce7dd3552de8313a483e"}, - {file = "aiohttp-3.9.3-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:1f5a71d25cd8106eab05f8704cd9167b6e5187bcdf8f090a66c6d88b634802b4"}, - {file = "aiohttp-3.9.3-cp311-cp311-win32.whl", hash = "sha256:50fca156d718f8ced687a373f9e140c1bb765ca16e3d6f4fe116e3df7c05b2c5"}, - {file = "aiohttp-3.9.3-cp311-cp311-win_amd64.whl", hash = "sha256:5fe9ce6c09668063b8447f85d43b8d1c4e5d3d7e92c63173e6180b2ac5d46dd8"}, - {file = "aiohttp-3.9.3-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:38a19bc3b686ad55804ae931012f78f7a534cce165d089a2059f658f6c91fa60"}, - {file = "aiohttp-3.9.3-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:770d015888c2a598b377bd2f663adfd947d78c0124cfe7b959e1ef39f5b13869"}, - {file = "aiohttp-3.9.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:ee43080e75fc92bf36219926c8e6de497f9b247301bbf88c5c7593d931426679"}, - {file = "aiohttp-3.9.3-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:52df73f14ed99cee84865b95a3d9e044f226320a87af208f068ecc33e0c35b96"}, - {file = "aiohttp-3.9.3-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:dc9b311743a78043b26ffaeeb9715dc360335e5517832f5a8e339f8a43581e4d"}, - {file = "aiohttp-3.9.3-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b955ed993491f1a5da7f92e98d5dad3c1e14dc175f74517c4e610b1f2456fb11"}, - {file = "aiohttp-3.9.3-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:504b6981675ace64c28bf4a05a508af5cde526e36492c98916127f5a02354d53"}, - {file = "aiohttp-3.9.3-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a6fe5571784af92b6bc2fda8d1925cccdf24642d49546d3144948a6a1ed58ca5"}, - {file = "aiohttp-3.9.3-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:ba39e9c8627edc56544c8628cc180d88605df3892beeb2b94c9bc857774848ca"}, - {file = "aiohttp-3.9.3-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:e5e46b578c0e9db71d04c4b506a2121c0cb371dd89af17a0586ff6769d4c58c1"}, - {file = "aiohttp-3.9.3-cp312-cp312-musllinux_1_1_ppc64le.whl", hash = "sha256:938a9653e1e0c592053f815f7028e41a3062e902095e5a7dc84617c87267ebd5"}, - {file = "aiohttp-3.9.3-cp312-cp312-musllinux_1_1_s390x.whl", hash = "sha256:c3452ea726c76e92f3b9fae4b34a151981a9ec0a4847a627c43d71a15ac32aa6"}, - {file = "aiohttp-3.9.3-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:ff30218887e62209942f91ac1be902cc80cddb86bf00fbc6783b7a43b2bea26f"}, - {file = "aiohttp-3.9.3-cp312-cp312-win32.whl", hash = "sha256:38f307b41e0bea3294a9a2a87833191e4bcf89bb0365e83a8be3a58b31fb7f38"}, - {file = "aiohttp-3.9.3-cp312-cp312-win_amd64.whl", hash = "sha256:b791a3143681a520c0a17e26ae7465f1b6f99461a28019d1a2f425236e6eedb5"}, - {file = "aiohttp-3.9.3-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:0ed621426d961df79aa3b963ac7af0d40392956ffa9be022024cd16297b30c8c"}, - {file = "aiohttp-3.9.3-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:7f46acd6a194287b7e41e87957bfe2ad1ad88318d447caf5b090012f2c5bb528"}, - {file = "aiohttp-3.9.3-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:feeb18a801aacb098220e2c3eea59a512362eb408d4afd0c242044c33ad6d542"}, - {file = "aiohttp-3.9.3-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f734e38fd8666f53da904c52a23ce517f1b07722118d750405af7e4123933511"}, - {file = "aiohttp-3.9.3-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b40670ec7e2156d8e57f70aec34a7216407848dfe6c693ef131ddf6e76feb672"}, - {file = "aiohttp-3.9.3-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:fdd215b7b7fd4a53994f238d0f46b7ba4ac4c0adb12452beee724ddd0743ae5d"}, - {file = "aiohttp-3.9.3-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:017a21b0df49039c8f46ca0971b3a7fdc1f56741ab1240cb90ca408049766168"}, - {file = "aiohttp-3.9.3-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e99abf0bba688259a496f966211c49a514e65afa9b3073a1fcee08856e04425b"}, - {file = "aiohttp-3.9.3-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:648056db9a9fa565d3fa851880f99f45e3f9a771dd3ff3bb0c048ea83fb28194"}, - {file = "aiohttp-3.9.3-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:8aacb477dc26797ee089721536a292a664846489c49d3ef9725f992449eda5a8"}, - {file = "aiohttp-3.9.3-cp38-cp38-musllinux_1_1_ppc64le.whl", hash = "sha256:522a11c934ea660ff8953eda090dcd2154d367dec1ae3c540aff9f8a5c109ab4"}, - {file = "aiohttp-3.9.3-cp38-cp38-musllinux_1_1_s390x.whl", hash = "sha256:5bce0dc147ca85caa5d33debc4f4d65e8e8b5c97c7f9f660f215fa74fc49a321"}, - {file = "aiohttp-3.9.3-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:4b4af9f25b49a7be47c0972139e59ec0e8285c371049df1a63b6ca81fdd216a2"}, - {file = "aiohttp-3.9.3-cp38-cp38-win32.whl", hash = "sha256:298abd678033b8571995650ccee753d9458dfa0377be4dba91e4491da3f2be63"}, - {file = "aiohttp-3.9.3-cp38-cp38-win_amd64.whl", hash = "sha256:69361bfdca5468c0488d7017b9b1e5ce769d40b46a9f4a2eed26b78619e9396c"}, - {file = "aiohttp-3.9.3-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:0fa43c32d1643f518491d9d3a730f85f5bbaedcbd7fbcae27435bb8b7a061b29"}, - {file = "aiohttp-3.9.3-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:835a55b7ca49468aaaac0b217092dfdff370e6c215c9224c52f30daaa735c1c1"}, - {file = "aiohttp-3.9.3-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:06a9b2c8837d9a94fae16c6223acc14b4dfdff216ab9b7202e07a9a09541168f"}, - {file = "aiohttp-3.9.3-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:abf151955990d23f84205286938796c55ff11bbfb4ccfada8c9c83ae6b3c89a3"}, - {file = "aiohttp-3.9.3-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:59c26c95975f26e662ca78fdf543d4eeaef70e533a672b4113dd888bd2423caa"}, - {file = "aiohttp-3.9.3-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f95511dd5d0e05fd9728bac4096319f80615aaef4acbecb35a990afebe953b0e"}, - {file = "aiohttp-3.9.3-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:595f105710293e76b9dc09f52e0dd896bd064a79346234b521f6b968ffdd8e58"}, - {file = "aiohttp-3.9.3-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c7c8b816c2b5af5c8a436df44ca08258fc1a13b449393a91484225fcb7545533"}, - {file = "aiohttp-3.9.3-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:f1088fa100bf46e7b398ffd9904f4808a0612e1d966b4aa43baa535d1b6341eb"}, - {file = "aiohttp-3.9.3-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:f59dfe57bb1ec82ac0698ebfcdb7bcd0e99c255bd637ff613760d5f33e7c81b3"}, - {file = "aiohttp-3.9.3-cp39-cp39-musllinux_1_1_ppc64le.whl", hash = "sha256:361a1026c9dd4aba0109e4040e2aecf9884f5cfe1b1b1bd3d09419c205e2e53d"}, - {file = "aiohttp-3.9.3-cp39-cp39-musllinux_1_1_s390x.whl", hash = "sha256:363afe77cfcbe3a36353d8ea133e904b108feea505aa4792dad6585a8192c55a"}, - {file = "aiohttp-3.9.3-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:8e2c45c208c62e955e8256949eb225bd8b66a4c9b6865729a786f2aa79b72e9d"}, - {file = "aiohttp-3.9.3-cp39-cp39-win32.whl", hash = "sha256:f7217af2e14da0856e082e96ff637f14ae45c10a5714b63c77f26d8884cf1051"}, - {file = "aiohttp-3.9.3-cp39-cp39-win_amd64.whl", hash = "sha256:27468897f628c627230dba07ec65dc8d0db566923c48f29e084ce382119802bc"}, - {file = "aiohttp-3.9.3.tar.gz", hash = "sha256:90842933e5d1ff760fae6caca4b2b3edba53ba8f4b71e95dacf2818a2aca06f7"}, + {file = "aiohttp-3.9.5-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:fcde4c397f673fdec23e6b05ebf8d4751314fa7c24f93334bf1f1364c1c69ac7"}, + {file = "aiohttp-3.9.5-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:5d6b3f1fabe465e819aed2c421a6743d8debbde79b6a8600739300630a01bf2c"}, + {file = "aiohttp-3.9.5-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:6ae79c1bc12c34082d92bf9422764f799aee4746fd7a392db46b7fd357d4a17a"}, + {file = "aiohttp-3.9.5-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4d3ebb9e1316ec74277d19c5f482f98cc65a73ccd5430540d6d11682cd857430"}, + {file = "aiohttp-3.9.5-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:84dabd95154f43a2ea80deffec9cb44d2e301e38a0c9d331cc4aa0166fe28ae3"}, + {file = "aiohttp-3.9.5-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c8a02fbeca6f63cb1f0475c799679057fc9268b77075ab7cf3f1c600e81dd46b"}, + {file = "aiohttp-3.9.5-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c26959ca7b75ff768e2776d8055bf9582a6267e24556bb7f7bd29e677932be72"}, + {file = "aiohttp-3.9.5-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:714d4e5231fed4ba2762ed489b4aec07b2b9953cf4ee31e9871caac895a839c0"}, + {file = "aiohttp-3.9.5-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:e7a6a8354f1b62e15d48e04350f13e726fa08b62c3d7b8401c0a1314f02e3558"}, + {file = "aiohttp-3.9.5-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:c413016880e03e69d166efb5a1a95d40f83d5a3a648d16486592c49ffb76d0db"}, + {file = "aiohttp-3.9.5-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:ff84aeb864e0fac81f676be9f4685f0527b660f1efdc40dcede3c251ef1e867f"}, + {file = "aiohttp-3.9.5-cp310-cp310-musllinux_1_1_s390x.whl", hash = "sha256:ad7f2919d7dac062f24d6f5fe95d401597fbb015a25771f85e692d043c9d7832"}, + {file = "aiohttp-3.9.5-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:702e2c7c187c1a498a4e2b03155d52658fdd6fda882d3d7fbb891a5cf108bb10"}, + {file = "aiohttp-3.9.5-cp310-cp310-win32.whl", hash = "sha256:67c3119f5ddc7261d47163ed86d760ddf0e625cd6246b4ed852e82159617b5fb"}, + {file = "aiohttp-3.9.5-cp310-cp310-win_amd64.whl", hash = "sha256:471f0ef53ccedec9995287f02caf0c068732f026455f07db3f01a46e49d76bbb"}, + {file = "aiohttp-3.9.5-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:e0ae53e33ee7476dd3d1132f932eeb39bf6125083820049d06edcdca4381f342"}, + {file = "aiohttp-3.9.5-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:c088c4d70d21f8ca5c0b8b5403fe84a7bc8e024161febdd4ef04575ef35d474d"}, + {file = "aiohttp-3.9.5-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:639d0042b7670222f33b0028de6b4e2fad6451462ce7df2af8aee37dcac55424"}, + {file = "aiohttp-3.9.5-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f26383adb94da5e7fb388d441bf09c61e5e35f455a3217bfd790c6b6bc64b2ee"}, + {file = "aiohttp-3.9.5-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:66331d00fb28dc90aa606d9a54304af76b335ae204d1836f65797d6fe27f1ca2"}, + {file = "aiohttp-3.9.5-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:4ff550491f5492ab5ed3533e76b8567f4b37bd2995e780a1f46bca2024223233"}, + {file = "aiohttp-3.9.5-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f22eb3a6c1080d862befa0a89c380b4dafce29dc6cd56083f630073d102eb595"}, + {file = "aiohttp-3.9.5-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a81b1143d42b66ffc40a441379387076243ef7b51019204fd3ec36b9f69e77d6"}, + {file = "aiohttp-3.9.5-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:f64fd07515dad67f24b6ea4a66ae2876c01031de91c93075b8093f07c0a2d93d"}, + {file = "aiohttp-3.9.5-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:93e22add827447d2e26d67c9ac0161756007f152fdc5210277d00a85f6c92323"}, + {file = "aiohttp-3.9.5-cp311-cp311-musllinux_1_1_ppc64le.whl", hash = "sha256:55b39c8684a46e56ef8c8d24faf02de4a2b2ac60d26cee93bc595651ff545de9"}, + {file = "aiohttp-3.9.5-cp311-cp311-musllinux_1_1_s390x.whl", hash = "sha256:4715a9b778f4293b9f8ae7a0a7cef9829f02ff8d6277a39d7f40565c737d3771"}, + {file = "aiohttp-3.9.5-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:afc52b8d969eff14e069a710057d15ab9ac17cd4b6753042c407dcea0e40bf75"}, + {file = "aiohttp-3.9.5-cp311-cp311-win32.whl", hash = "sha256:b3df71da99c98534be076196791adca8819761f0bf6e08e07fd7da25127150d6"}, + {file = "aiohttp-3.9.5-cp311-cp311-win_amd64.whl", hash = "sha256:88e311d98cc0bf45b62fc46c66753a83445f5ab20038bcc1b8a1cc05666f428a"}, + {file = "aiohttp-3.9.5-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:c7a4b7a6cf5b6eb11e109a9755fd4fda7d57395f8c575e166d363b9fc3ec4678"}, + {file = "aiohttp-3.9.5-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:0a158704edf0abcac8ac371fbb54044f3270bdbc93e254a82b6c82be1ef08f3c"}, + {file = "aiohttp-3.9.5-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:d153f652a687a8e95ad367a86a61e8d53d528b0530ef382ec5aaf533140ed00f"}, + {file = "aiohttp-3.9.5-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:82a6a97d9771cb48ae16979c3a3a9a18b600a8505b1115cfe354dfb2054468b4"}, + {file = "aiohttp-3.9.5-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:60cdbd56f4cad9f69c35eaac0fbbdf1f77b0ff9456cebd4902f3dd1cf096464c"}, + {file = "aiohttp-3.9.5-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8676e8fd73141ded15ea586de0b7cda1542960a7b9ad89b2b06428e97125d4fa"}, + {file = "aiohttp-3.9.5-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:da00da442a0e31f1c69d26d224e1efd3a1ca5bcbf210978a2ca7426dfcae9f58"}, + {file = "aiohttp-3.9.5-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:18f634d540dd099c262e9f887c8bbacc959847cfe5da7a0e2e1cf3f14dbf2daf"}, + {file = "aiohttp-3.9.5-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:320e8618eda64e19d11bdb3bd04ccc0a816c17eaecb7e4945d01deee2a22f95f"}, + {file = "aiohttp-3.9.5-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:2faa61a904b83142747fc6a6d7ad8fccff898c849123030f8e75d5d967fd4a81"}, + {file = "aiohttp-3.9.5-cp312-cp312-musllinux_1_1_ppc64le.whl", hash = "sha256:8c64a6dc3fe5db7b1b4d2b5cb84c4f677768bdc340611eca673afb7cf416ef5a"}, + {file = "aiohttp-3.9.5-cp312-cp312-musllinux_1_1_s390x.whl", hash = "sha256:393c7aba2b55559ef7ab791c94b44f7482a07bf7640d17b341b79081f5e5cd1a"}, + {file = "aiohttp-3.9.5-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:c671dc117c2c21a1ca10c116cfcd6e3e44da7fcde37bf83b2be485ab377b25da"}, + {file = "aiohttp-3.9.5-cp312-cp312-win32.whl", hash = "sha256:5a7ee16aab26e76add4afc45e8f8206c95d1d75540f1039b84a03c3b3800dd59"}, + {file = "aiohttp-3.9.5-cp312-cp312-win_amd64.whl", hash = "sha256:5ca51eadbd67045396bc92a4345d1790b7301c14d1848feaac1d6a6c9289e888"}, + {file = "aiohttp-3.9.5-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:694d828b5c41255e54bc2dddb51a9f5150b4eefa9886e38b52605a05d96566e8"}, + {file = "aiohttp-3.9.5-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:0605cc2c0088fcaae79f01c913a38611ad09ba68ff482402d3410bf59039bfb8"}, + {file = "aiohttp-3.9.5-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:4558e5012ee03d2638c681e156461d37b7a113fe13970d438d95d10173d25f78"}, + {file = "aiohttp-3.9.5-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9dbc053ac75ccc63dc3a3cc547b98c7258ec35a215a92bd9f983e0aac95d3d5b"}, + {file = "aiohttp-3.9.5-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4109adee842b90671f1b689901b948f347325045c15f46b39797ae1bf17019de"}, + {file = "aiohttp-3.9.5-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a6ea1a5b409a85477fd8e5ee6ad8f0e40bf2844c270955e09360418cfd09abac"}, + {file = "aiohttp-3.9.5-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f3c2890ca8c59ee683fd09adf32321a40fe1cf164e3387799efb2acebf090c11"}, + {file = "aiohttp-3.9.5-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3916c8692dbd9d55c523374a3b8213e628424d19116ac4308e434dbf6d95bbdd"}, + {file = "aiohttp-3.9.5-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:8d1964eb7617907c792ca00b341b5ec3e01ae8c280825deadbbd678447b127e1"}, + {file = "aiohttp-3.9.5-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:d5ab8e1f6bee051a4bf6195e38a5c13e5e161cb7bad83d8854524798bd9fcd6e"}, + {file = "aiohttp-3.9.5-cp38-cp38-musllinux_1_1_ppc64le.whl", hash = "sha256:52c27110f3862a1afbcb2af4281fc9fdc40327fa286c4625dfee247c3ba90156"}, + {file = "aiohttp-3.9.5-cp38-cp38-musllinux_1_1_s390x.whl", hash = "sha256:7f64cbd44443e80094309875d4f9c71d0401e966d191c3d469cde4642bc2e031"}, + {file = "aiohttp-3.9.5-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:8b4f72fbb66279624bfe83fd5eb6aea0022dad8eec62b71e7bf63ee1caadeafe"}, + {file = "aiohttp-3.9.5-cp38-cp38-win32.whl", hash = "sha256:6380c039ec52866c06d69b5c7aad5478b24ed11696f0e72f6b807cfb261453da"}, + {file = "aiohttp-3.9.5-cp38-cp38-win_amd64.whl", hash = "sha256:da22dab31d7180f8c3ac7c7635f3bcd53808f374f6aa333fe0b0b9e14b01f91a"}, + {file = "aiohttp-3.9.5-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:1732102949ff6087589408d76cd6dea656b93c896b011ecafff418c9661dc4ed"}, + {file = "aiohttp-3.9.5-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:c6021d296318cb6f9414b48e6a439a7f5d1f665464da507e8ff640848ee2a58a"}, + {file = "aiohttp-3.9.5-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:239f975589a944eeb1bad26b8b140a59a3a320067fb3cd10b75c3092405a1372"}, + {file = "aiohttp-3.9.5-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3b7b30258348082826d274504fbc7c849959f1989d86c29bc355107accec6cfb"}, + {file = "aiohttp-3.9.5-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:cd2adf5c87ff6d8b277814a28a535b59e20bfea40a101db6b3bdca7e9926bc24"}, + {file = "aiohttp-3.9.5-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e9a3d838441bebcf5cf442700e3963f58b5c33f015341f9ea86dcd7d503c07e2"}, + {file = "aiohttp-3.9.5-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9e3a1ae66e3d0c17cf65c08968a5ee3180c5a95920ec2731f53343fac9bad106"}, + {file = "aiohttp-3.9.5-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:9c69e77370cce2d6df5d12b4e12bdcca60c47ba13d1cbbc8645dd005a20b738b"}, + {file = "aiohttp-3.9.5-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:0cbf56238f4bbf49dab8c2dc2e6b1b68502b1e88d335bea59b3f5b9f4c001475"}, + {file = "aiohttp-3.9.5-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:d1469f228cd9ffddd396d9948b8c9cd8022b6d1bf1e40c6f25b0fb90b4f893ed"}, + {file = "aiohttp-3.9.5-cp39-cp39-musllinux_1_1_ppc64le.whl", hash = "sha256:45731330e754f5811c314901cebdf19dd776a44b31927fa4b4dbecab9e457b0c"}, + {file = "aiohttp-3.9.5-cp39-cp39-musllinux_1_1_s390x.whl", hash = "sha256:3fcb4046d2904378e3aeea1df51f697b0467f2aac55d232c87ba162709478c46"}, + {file = "aiohttp-3.9.5-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:8cf142aa6c1a751fcb364158fd710b8a9be874b81889c2bd13aa8893197455e2"}, + {file = "aiohttp-3.9.5-cp39-cp39-win32.whl", hash = "sha256:7b179eea70833c8dee51ec42f3b4097bd6370892fa93f510f76762105568cf09"}, + {file = "aiohttp-3.9.5-cp39-cp39-win_amd64.whl", hash = "sha256:38d80498e2e169bc61418ff36170e0aad0cd268da8b38a17c4cf29d254a8b3f1"}, + {file = "aiohttp-3.9.5.tar.gz", hash = "sha256:edea7d15772ceeb29db4aff55e482d4bcfb6ae160ce144f2682de02f6d693551"}, ] [package.dependencies] @@ -427,20 +427,20 @@ aio = ["aiohttp (>=3.0)"] [[package]] name = "azure-identity" -version = "1.15.0" +version = "1.16.0" description = "Microsoft Azure Identity Library for Python" optional = false -python-versions = ">=3.7" +python-versions = ">=3.8" files = [ - {file = "azure-identity-1.15.0.tar.gz", hash = "sha256:4c28fc246b7f9265610eb5261d65931183d019a23d4b0e99357facb2e6c227c8"}, - {file = "azure_identity-1.15.0-py3-none-any.whl", hash = "sha256:a14b1f01c7036f11f148f22cd8c16e05035293d714458d6b44ddf534d93eb912"}, + {file = "azure-identity-1.16.0.tar.gz", hash = "sha256:6ff1d667cdcd81da1ceab42f80a0be63ca846629f518a922f7317a7e3c844e1b"}, + {file = "azure_identity-1.16.0-py3-none-any.whl", hash = "sha256:722fdb60b8fdd55fa44dc378b8072f4b419b56a5e54c0de391f644949f3a826f"}, ] [package.dependencies] -azure-core = ">=1.23.0,<2.0.0" +azure-core = ">=1.23.0" cryptography = ">=2.5" -msal = ">=1.24.0,<2.0.0" -msal-extensions = ">=0.3.0,<2.0.0" +msal = ">=1.24.0" +msal-extensions = ">=0.3.0" [[package]] name = "azure-mgmt-core" @@ -603,13 +603,13 @@ css = ["tinycss2 (>=1.1.0,<1.3)"] [[package]] name = "blinker" -version = "1.7.0" +version = "1.8.1" description = "Fast, simple object-to-object and broadcast signaling" optional = false python-versions = ">=3.8" files = [ - {file = "blinker-1.7.0-py3-none-any.whl", hash = "sha256:c3f865d4d54db7abc53758a01601cf343fe55b84c1de4e3fa910e420b438d5b9"}, - {file = "blinker-1.7.0.tar.gz", hash = "sha256:e6820ff6fa4e4d1d8e2747c2283749c3f547e4fee112b98555cdcdae32996182"}, + {file = "blinker-1.8.1-py3-none-any.whl", hash = "sha256:5f1cdeff423b77c31b89de0565cd03e5275a03028f44b2b15f912632a58cced6"}, + {file = "blinker-1.8.1.tar.gz", hash = "sha256:da44ec748222dcd0105ef975eed946da197d5bdf8bafb6aa92f5bc89da63fa25"}, ] [[package]] @@ -824,32 +824,32 @@ files = [ [[package]] name = "cmake" -version = "3.28.3" +version = "3.29.2" description = "CMake is an open-source, cross-platform family of tools designed to build, test and package software" optional = false -python-versions = "*" +python-versions = ">=3.7" files = [ - {file = "cmake-3.28.3-py2.py3-none-macosx_10_10_universal2.macosx_10_10_x86_64.macosx_11_0_arm64.macosx_11_0_universal2.whl", hash = "sha256:f27187ae016b089d1c1fca6a24b3af58f9d79471097eaa3b7a7a7623ad12ea89"}, - {file = "cmake-3.28.3-py2.py3-none-manylinux2010_i686.manylinux_2_12_i686.whl", hash = "sha256:f5573c453f7a6c213c82741c173d174b5c6b576eea5cc00e2a8a5a30c40244b3"}, - {file = "cmake-3.28.3-py2.py3-none-manylinux2010_x86_64.manylinux_2_12_x86_64.whl", hash = "sha256:35b14086257dc7ce8e83c19d2d20f7953d584fa3c9d1904211d8498fe1134ecc"}, - {file = "cmake-3.28.3-py2.py3-none-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:795c4c7f0ad16cc6553085502a76aa7fcf36fd2f4c8420542d1c7f3be6f9de1e"}, - {file = "cmake-3.28.3-py2.py3-none-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:2b811a7c97b2b31a56397baeb5ca93119fa4d215846851059748427c67f14a58"}, - {file = "cmake-3.28.3-py2.py3-none-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:8415ed1a9335eb30b0e435c38bcaeb8fd9ae900a9594fe500f3bcba744be1dc7"}, - {file = "cmake-3.28.3-py2.py3-none-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:2745d4362ac23f2f979e71d44759af740c3890429cb8a7e2fd449a30a901632f"}, - {file = "cmake-3.28.3-py2.py3-none-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:d3bc42bf54ea3d64e5d81eb31275076817507cf4a6aa07a49ffc01985cae1f09"}, - {file = "cmake-3.28.3-py2.py3-none-musllinux_1_1_aarch64.whl", hash = "sha256:de10be2f470c41a3628e27157168f017ade2f14065588497e00f4582bc5eec07"}, - {file = "cmake-3.28.3-py2.py3-none-musllinux_1_1_i686.whl", hash = "sha256:5e4972e455fc24509561873cb06c9d9394852d77adde1cf970b859ad14a2a66f"}, - {file = "cmake-3.28.3-py2.py3-none-musllinux_1_1_ppc64le.whl", hash = "sha256:ea338ae68e0c5626f7c21f89b765eb0e81f7b497e977503a3bcce569984dc8a7"}, - {file = "cmake-3.28.3-py2.py3-none-musllinux_1_1_s390x.whl", hash = "sha256:c6415d382933854d2b5508c4d2218cfb1a8cb90f5f78b4e97183f80089868eea"}, - {file = "cmake-3.28.3-py2.py3-none-musllinux_1_1_x86_64.whl", hash = "sha256:cc67c5e5df8db0be57d25b81f7dc76e0ec79215f914e585a8045589a380bcd3c"}, - {file = "cmake-3.28.3-py2.py3-none-win32.whl", hash = "sha256:29d127e5ef256d389feac0884e918612b89eb3a8febff1acf83bb27bc65042ab"}, - {file = "cmake-3.28.3-py2.py3-none-win_amd64.whl", hash = "sha256:f6fc9755979d17970ca6d9688fb5cdd3702c9eaa7ac1ee97074e3d39d3400970"}, - {file = "cmake-3.28.3-py2.py3-none-win_arm64.whl", hash = "sha256:4b1b413cf7683d54ec2a0f3b17a4d7c6979eb469270439c0e7a082256c78ab96"}, - {file = "cmake-3.28.3.tar.gz", hash = "sha256:a8092815c739da7d6775c26ec30c2645f0fca9527a29e36a682faec7d39cde89"}, + {file = "cmake-3.29.2-py3-none-macosx_10_10_universal2.macosx_10_10_x86_64.macosx_11_0_arm64.macosx_11_0_universal2.whl", hash = "sha256:1d40c5451d6467b20a0a6015a5a6b6dc86f61b83f71f935740485b259100a34e"}, + {file = "cmake-3.29.2-py3-none-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:ed3108e646cd65a4e23fa1cbe8123569a29334a3f2a8ce214d871406b161bedb"}, + {file = "cmake-3.29.2-py3-none-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:40aafe612b03a9fa140cca4024ba60b74cd92372f3f349d8062cba1f021e5001"}, + {file = "cmake-3.29.2-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:027eebe9bb74c31759581a543f27bc1828fc76e6fc45b2b48b51f27847904669"}, + {file = "cmake-3.29.2-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f1f087985fc2460476b0901716fbddb2fd69b7fe7bf1350e1ab5dc508d22600e"}, + {file = "cmake-3.29.2-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:df2c63ce6d504aa4c91a42fd22d3887065ab029569691deb56ec19d0decd0ae9"}, + {file = "cmake-3.29.2-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:6ea5ce007893d7d1363e13433dde1c0c7c344372213a90ff3c56e896a335301d"}, + {file = "cmake-3.29.2-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f9e941e73202cfa667ee488d1d88b8a758b516dcfa2a2728e73dbdcbfbdebf57"}, + {file = "cmake-3.29.2-py3-none-musllinux_1_1_aarch64.whl", hash = "sha256:37222e23485338c72b7ea51f865d8c6847d519f7e2222922fb70b4896ca6e897"}, + {file = "cmake-3.29.2-py3-none-musllinux_1_1_i686.whl", hash = "sha256:eeed08932c748647488280dc97ac00bcfeae5d760451105200cfe66c52ce6468"}, + {file = "cmake-3.29.2-py3-none-musllinux_1_1_ppc64le.whl", hash = "sha256:db7a05df020ba67bacd3070dd1645c76ca96fabd06d6aaa63288fd845706e47a"}, + {file = "cmake-3.29.2-py3-none-musllinux_1_1_s390x.whl", hash = "sha256:83b35de822ddabaaa184a7d8f9827381350c42d627689c775b214347f57c9e41"}, + {file = "cmake-3.29.2-py3-none-musllinux_1_1_x86_64.whl", hash = "sha256:cc0e36752e581430a93e58a268e515bb4ec1373b9e9911571f2cac1d2a6b5bec"}, + {file = "cmake-3.29.2-py3-none-win32.whl", hash = "sha256:a941e26fba81cf74832c8a0e17e007452e05b6ad4941b3d2d18c75faa4a677d8"}, + {file = "cmake-3.29.2-py3-none-win_amd64.whl", hash = "sha256:23336c8ca01205d18d92ed8de6c54e570c352a58e378b7f9adc02ef00f433960"}, + {file = "cmake-3.29.2-py3-none-win_arm64.whl", hash = "sha256:e722a949f7c91084dba61f8f17a9854787182ab711ed0b84b1507b24a8e12e25"}, + {file = "cmake-3.29.2.tar.gz", hash = "sha256:6a4c1185cb2eca7263190a5754d0c9edf738d9e50bff464f78f48d0c05318e7c"}, ] [package.extras] -test = ["coverage (>=4.2)", "importlib-metadata (>=2.0)", "pytest (>=3.0.3)", "pytest-cov (>=2.4.0)"] +test = ["coverage (>=4.2)", "pytest (>=3.0.3)", "pytest-cov (>=2.4.0)"] [[package]] name = "colorama" @@ -881,126 +881,126 @@ test = ["pytest"] [[package]] name = "contourpy" -version = "1.2.0" +version = "1.2.1" description = "Python library for calculating contours of 2D quadrilateral grids" optional = false python-versions = ">=3.9" files = [ - {file = "contourpy-1.2.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:0274c1cb63625972c0c007ab14dd9ba9e199c36ae1a231ce45d725cbcbfd10a8"}, - {file = "contourpy-1.2.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:ab459a1cbbf18e8698399c595a01f6dcc5c138220ca3ea9e7e6126232d102bb4"}, - {file = "contourpy-1.2.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6fdd887f17c2f4572ce548461e4f96396681212d858cae7bd52ba3310bc6f00f"}, - {file = "contourpy-1.2.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5d16edfc3fc09968e09ddffada434b3bf989bf4911535e04eada58469873e28e"}, - {file = "contourpy-1.2.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:1c203f617abc0dde5792beb586f827021069fb6d403d7f4d5c2b543d87edceb9"}, - {file = "contourpy-1.2.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b69303ceb2e4d4f146bf82fda78891ef7bcd80c41bf16bfca3d0d7eb545448aa"}, - {file = "contourpy-1.2.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:884c3f9d42d7218304bc74a8a7693d172685c84bd7ab2bab1ee567b769696df9"}, - {file = "contourpy-1.2.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:4a1b1208102be6e851f20066bf0e7a96b7d48a07c9b0cfe6d0d4545c2f6cadab"}, - {file = "contourpy-1.2.0-cp310-cp310-win32.whl", hash = "sha256:34b9071c040d6fe45d9826cbbe3727d20d83f1b6110d219b83eb0e2a01d79488"}, - {file = "contourpy-1.2.0-cp310-cp310-win_amd64.whl", hash = "sha256:bd2f1ae63998da104f16a8b788f685e55d65760cd1929518fd94cd682bf03e41"}, - {file = "contourpy-1.2.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:dd10c26b4eadae44783c45ad6655220426f971c61d9b239e6f7b16d5cdaaa727"}, - {file = "contourpy-1.2.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:5c6b28956b7b232ae801406e529ad7b350d3f09a4fde958dfdf3c0520cdde0dd"}, - {file = "contourpy-1.2.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ebeac59e9e1eb4b84940d076d9f9a6cec0064e241818bcb6e32124cc5c3e377a"}, - {file = "contourpy-1.2.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:139d8d2e1c1dd52d78682f505e980f592ba53c9f73bd6be102233e358b401063"}, - {file = "contourpy-1.2.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:1e9dc350fb4c58adc64df3e0703ab076f60aac06e67d48b3848c23647ae4310e"}, - {file = "contourpy-1.2.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:18fc2b4ed8e4a8fe849d18dce4bd3c7ea637758c6343a1f2bae1e9bd4c9f4686"}, - {file = "contourpy-1.2.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:16a7380e943a6d52472096cb7ad5264ecee36ed60888e2a3d3814991a0107286"}, - {file = "contourpy-1.2.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:8d8faf05be5ec8e02a4d86f616fc2a0322ff4a4ce26c0f09d9f7fb5330a35c95"}, - {file = "contourpy-1.2.0-cp311-cp311-win32.whl", hash = "sha256:67b7f17679fa62ec82b7e3e611c43a016b887bd64fb933b3ae8638583006c6d6"}, - {file = "contourpy-1.2.0-cp311-cp311-win_amd64.whl", hash = "sha256:99ad97258985328b4f207a5e777c1b44a83bfe7cf1f87b99f9c11d4ee477c4de"}, - {file = "contourpy-1.2.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:575bcaf957a25d1194903a10bc9f316c136c19f24e0985a2b9b5608bdf5dbfe0"}, - {file = "contourpy-1.2.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:9e6c93b5b2dbcedad20a2f18ec22cae47da0d705d454308063421a3b290d9ea4"}, - {file = "contourpy-1.2.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:464b423bc2a009088f19bdf1f232299e8b6917963e2b7e1d277da5041f33a779"}, - {file = "contourpy-1.2.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:68ce4788b7d93e47f84edd3f1f95acdcd142ae60bc0e5493bfd120683d2d4316"}, - {file = "contourpy-1.2.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3d7d1f8871998cdff5d2ff6a087e5e1780139abe2838e85b0b46b7ae6cc25399"}, - {file = "contourpy-1.2.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6e739530c662a8d6d42c37c2ed52a6f0932c2d4a3e8c1f90692ad0ce1274abe0"}, - {file = "contourpy-1.2.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:247b9d16535acaa766d03037d8e8fb20866d054d3c7fbf6fd1f993f11fc60ca0"}, - {file = "contourpy-1.2.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:461e3ae84cd90b30f8d533f07d87c00379644205b1d33a5ea03381edc4b69431"}, - {file = "contourpy-1.2.0-cp312-cp312-win32.whl", hash = "sha256:1c2559d6cffc94890b0529ea7eeecc20d6fadc1539273aa27faf503eb4656d8f"}, - {file = "contourpy-1.2.0-cp312-cp312-win_amd64.whl", hash = "sha256:491b1917afdd8638a05b611a56d46587d5a632cabead889a5440f7c638bc6ed9"}, - {file = "contourpy-1.2.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:5fd1810973a375ca0e097dee059c407913ba35723b111df75671a1976efa04bc"}, - {file = "contourpy-1.2.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:999c71939aad2780f003979b25ac5b8f2df651dac7b38fb8ce6c46ba5abe6ae9"}, - {file = "contourpy-1.2.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b7caf9b241464c404613512d5594a6e2ff0cc9cb5615c9475cc1d9b514218ae8"}, - {file = "contourpy-1.2.0-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:266270c6f6608340f6c9836a0fb9b367be61dde0c9a9a18d5ece97774105ff3e"}, - {file = "contourpy-1.2.0-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:dbd50d0a0539ae2e96e537553aff6d02c10ed165ef40c65b0e27e744a0f10af8"}, - {file = "contourpy-1.2.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:11f8d2554e52f459918f7b8e6aa20ec2a3bce35ce95c1f0ef4ba36fbda306df5"}, - {file = "contourpy-1.2.0-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:ce96dd400486e80ac7d195b2d800b03e3e6a787e2a522bfb83755938465a819e"}, - {file = "contourpy-1.2.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:6d3364b999c62f539cd403f8123ae426da946e142312a514162adb2addd8d808"}, - {file = "contourpy-1.2.0-cp39-cp39-win32.whl", hash = "sha256:1c88dfb9e0c77612febebb6ac69d44a8d81e3dc60f993215425b62c1161353f4"}, - {file = "contourpy-1.2.0-cp39-cp39-win_amd64.whl", hash = "sha256:78e6ad33cf2e2e80c5dfaaa0beec3d61face0fb650557100ee36db808bfa6843"}, - {file = "contourpy-1.2.0-pp39-pypy39_pp73-macosx_10_9_x86_64.whl", hash = "sha256:be16975d94c320432657ad2402f6760990cb640c161ae6da1363051805fa8108"}, - {file = "contourpy-1.2.0-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b95a225d4948b26a28c08307a60ac00fb8671b14f2047fc5476613252a129776"}, - {file = "contourpy-1.2.0-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:0d7e03c0f9a4f90dc18d4e77e9ef4ec7b7bbb437f7f675be8e530d65ae6ef956"}, - {file = "contourpy-1.2.0.tar.gz", hash = "sha256:171f311cb758de7da13fc53af221ae47a5877be5a0843a9fe150818c51ed276a"}, -] - -[package.dependencies] -numpy = ">=1.20,<2.0" + {file = "contourpy-1.2.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:bd7c23df857d488f418439686d3b10ae2fbf9bc256cd045b37a8c16575ea1040"}, + {file = "contourpy-1.2.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:5b9eb0ca724a241683c9685a484da9d35c872fd42756574a7cfbf58af26677fd"}, + {file = "contourpy-1.2.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4c75507d0a55378240f781599c30e7776674dbaf883a46d1c90f37e563453480"}, + {file = "contourpy-1.2.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:11959f0ce4a6f7b76ec578576a0b61a28bdc0696194b6347ba3f1c53827178b9"}, + {file = "contourpy-1.2.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:eb3315a8a236ee19b6df481fc5f997436e8ade24a9f03dfdc6bd490fea20c6da"}, + {file = "contourpy-1.2.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:39f3ecaf76cd98e802f094e0d4fbc6dc9c45a8d0c4d185f0f6c2234e14e5f75b"}, + {file = "contourpy-1.2.1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:94b34f32646ca0414237168d68a9157cb3889f06b096612afdd296003fdd32fd"}, + {file = "contourpy-1.2.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:457499c79fa84593f22454bbd27670227874cd2ff5d6c84e60575c8b50a69619"}, + {file = "contourpy-1.2.1-cp310-cp310-win32.whl", hash = "sha256:ac58bdee53cbeba2ecad824fa8159493f0bf3b8ea4e93feb06c9a465d6c87da8"}, + {file = "contourpy-1.2.1-cp310-cp310-win_amd64.whl", hash = "sha256:9cffe0f850e89d7c0012a1fb8730f75edd4320a0a731ed0c183904fe6ecfc3a9"}, + {file = "contourpy-1.2.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:6022cecf8f44e36af10bd9118ca71f371078b4c168b6e0fab43d4a889985dbb5"}, + {file = "contourpy-1.2.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:ef5adb9a3b1d0c645ff694f9bca7702ec2c70f4d734f9922ea34de02294fdf72"}, + {file = "contourpy-1.2.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6150ffa5c767bc6332df27157d95442c379b7dce3a38dff89c0f39b63275696f"}, + {file = "contourpy-1.2.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4c863140fafc615c14a4bf4efd0f4425c02230eb8ef02784c9a156461e62c965"}, + {file = "contourpy-1.2.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:00e5388f71c1a0610e6fe56b5c44ab7ba14165cdd6d695429c5cd94021e390b2"}, + {file = "contourpy-1.2.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d4492d82b3bc7fbb7e3610747b159869468079fe149ec5c4d771fa1f614a14df"}, + {file = "contourpy-1.2.1-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:49e70d111fee47284d9dd867c9bb9a7058a3c617274900780c43e38d90fe1205"}, + {file = "contourpy-1.2.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:b59c0ffceff8d4d3996a45f2bb6f4c207f94684a96bf3d9728dbb77428dd8cb8"}, + {file = "contourpy-1.2.1-cp311-cp311-win32.whl", hash = "sha256:7b4182299f251060996af5249c286bae9361fa8c6a9cda5efc29fe8bfd6062ec"}, + {file = "contourpy-1.2.1-cp311-cp311-win_amd64.whl", hash = "sha256:2855c8b0b55958265e8b5888d6a615ba02883b225f2227461aa9127c578a4922"}, + {file = "contourpy-1.2.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:62828cada4a2b850dbef89c81f5a33741898b305db244904de418cc957ff05dc"}, + {file = "contourpy-1.2.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:309be79c0a354afff9ff7da4aaed7c3257e77edf6c1b448a779329431ee79d7e"}, + {file = "contourpy-1.2.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2e785e0f2ef0d567099b9ff92cbfb958d71c2d5b9259981cd9bee81bd194c9a4"}, + {file = "contourpy-1.2.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1cac0a8f71a041aa587410424ad46dfa6a11f6149ceb219ce7dd48f6b02b87a7"}, + {file = "contourpy-1.2.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:af3f4485884750dddd9c25cb7e3915d83c2db92488b38ccb77dd594eac84c4a0"}, + {file = "contourpy-1.2.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9ce6889abac9a42afd07a562c2d6d4b2b7134f83f18571d859b25624a331c90b"}, + {file = "contourpy-1.2.1-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:a1eea9aecf761c661d096d39ed9026574de8adb2ae1c5bd7b33558af884fb2ce"}, + {file = "contourpy-1.2.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:187fa1d4c6acc06adb0fae5544c59898ad781409e61a926ac7e84b8f276dcef4"}, + {file = "contourpy-1.2.1-cp312-cp312-win32.whl", hash = "sha256:c2528d60e398c7c4c799d56f907664673a807635b857df18f7ae64d3e6ce2d9f"}, + {file = "contourpy-1.2.1-cp312-cp312-win_amd64.whl", hash = "sha256:1a07fc092a4088ee952ddae19a2b2a85757b923217b7eed584fdf25f53a6e7ce"}, + {file = "contourpy-1.2.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:bb6834cbd983b19f06908b45bfc2dad6ac9479ae04abe923a275b5f48f1a186b"}, + {file = "contourpy-1.2.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:1d59e739ab0e3520e62a26c60707cc3ab0365d2f8fecea74bfe4de72dc56388f"}, + {file = "contourpy-1.2.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bd3db01f59fdcbce5b22afad19e390260d6d0222f35a1023d9adc5690a889364"}, + {file = "contourpy-1.2.1-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a12a813949e5066148712a0626895c26b2578874e4cc63160bb007e6df3436fe"}, + {file = "contourpy-1.2.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:fe0ccca550bb8e5abc22f530ec0466136379c01321fd94f30a22231e8a48d985"}, + {file = "contourpy-1.2.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e1d59258c3c67c865435d8fbeb35f8c59b8bef3d6f46c1f29f6123556af28445"}, + {file = "contourpy-1.2.1-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:f32c38afb74bd98ce26de7cc74a67b40afb7b05aae7b42924ea990d51e4dac02"}, + {file = "contourpy-1.2.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:d31a63bc6e6d87f77d71e1abbd7387ab817a66733734883d1fc0021ed9bfa083"}, + {file = "contourpy-1.2.1-cp39-cp39-win32.whl", hash = "sha256:ddcb8581510311e13421b1f544403c16e901c4e8f09083c881fab2be80ee31ba"}, + {file = "contourpy-1.2.1-cp39-cp39-win_amd64.whl", hash = "sha256:10a37ae557aabf2509c79715cd20b62e4c7c28b8cd62dd7d99e5ed3ce28c3fd9"}, + {file = "contourpy-1.2.1-pp39-pypy39_pp73-macosx_10_9_x86_64.whl", hash = "sha256:a31f94983fecbac95e58388210427d68cd30fe8a36927980fab9c20062645609"}, + {file = "contourpy-1.2.1-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ef2b055471c0eb466033760a521efb9d8a32b99ab907fc8358481a1dd29e3bd3"}, + {file = "contourpy-1.2.1-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:b33d2bc4f69caedcd0a275329eb2198f560b325605810895627be5d4b876bf7f"}, + {file = "contourpy-1.2.1.tar.gz", hash = "sha256:4d8908b3bee1c889e547867ca4cdc54e5ab6be6d3e078556814a22457f49423c"}, +] + +[package.dependencies] +numpy = ">=1.20" [package.extras] bokeh = ["bokeh", "selenium"] docs = ["furo", "sphinx (>=7.2)", "sphinx-copybutton"] -mypy = ["contourpy[bokeh,docs]", "docutils-stubs", "mypy (==1.6.1)", "types-Pillow"] +mypy = ["contourpy[bokeh,docs]", "docutils-stubs", "mypy (==1.8.0)", "types-Pillow"] test = ["Pillow", "contourpy[test-no-images]", "matplotlib"] test-no-images = ["pytest", "pytest-cov", "pytest-xdist", "wurlitzer"] [[package]] name = "coverage" -version = "7.4.4" +version = "7.5.0" description = "Code coverage measurement for Python" optional = false python-versions = ">=3.8" files = [ - {file = "coverage-7.4.4-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:e0be5efd5127542ef31f165de269f77560d6cdef525fffa446de6f7e9186cfb2"}, - {file = "coverage-7.4.4-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:ccd341521be3d1b3daeb41960ae94a5e87abe2f46f17224ba5d6f2b8398016cf"}, - {file = "coverage-7.4.4-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:09fa497a8ab37784fbb20ab699c246053ac294d13fc7eb40ec007a5043ec91f8"}, - {file = "coverage-7.4.4-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b1a93009cb80730c9bca5d6d4665494b725b6e8e157c1cb7f2db5b4b122ea562"}, - {file = "coverage-7.4.4-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:690db6517f09336559dc0b5f55342df62370a48f5469fabf502db2c6d1cffcd2"}, - {file = "coverage-7.4.4-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:09c3255458533cb76ef55da8cc49ffab9e33f083739c8bd4f58e79fecfe288f7"}, - {file = "coverage-7.4.4-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:8ce1415194b4a6bd0cdcc3a1dfbf58b63f910dcb7330fe15bdff542c56949f87"}, - {file = "coverage-7.4.4-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:b91cbc4b195444e7e258ba27ac33769c41b94967919f10037e6355e998af255c"}, - {file = "coverage-7.4.4-cp310-cp310-win32.whl", hash = "sha256:598825b51b81c808cb6f078dcb972f96af96b078faa47af7dfcdf282835baa8d"}, - {file = "coverage-7.4.4-cp310-cp310-win_amd64.whl", hash = "sha256:09ef9199ed6653989ebbcaacc9b62b514bb63ea2f90256e71fea3ed74bd8ff6f"}, - {file = "coverage-7.4.4-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:0f9f50e7ef2a71e2fae92774c99170eb8304e3fdf9c8c3c7ae9bab3e7229c5cf"}, - {file = "coverage-7.4.4-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:623512f8ba53c422fcfb2ce68362c97945095b864cda94a92edbaf5994201083"}, - {file = "coverage-7.4.4-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0513b9508b93da4e1716744ef6ebc507aff016ba115ffe8ecff744d1322a7b63"}, - {file = "coverage-7.4.4-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:40209e141059b9370a2657c9b15607815359ab3ef9918f0196b6fccce8d3230f"}, - {file = "coverage-7.4.4-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8a2b2b78c78293782fd3767d53e6474582f62443d0504b1554370bde86cc8227"}, - {file = "coverage-7.4.4-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:73bfb9c09951125d06ee473bed216e2c3742f530fc5acc1383883125de76d9cd"}, - {file = "coverage-7.4.4-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:1f384c3cc76aeedce208643697fb3e8437604b512255de6d18dae3f27655a384"}, - {file = "coverage-7.4.4-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:54eb8d1bf7cacfbf2a3186019bcf01d11c666bd495ed18717162f7eb1e9dd00b"}, - {file = "coverage-7.4.4-cp311-cp311-win32.whl", hash = "sha256:cac99918c7bba15302a2d81f0312c08054a3359eaa1929c7e4b26ebe41e9b286"}, - {file = "coverage-7.4.4-cp311-cp311-win_amd64.whl", hash = "sha256:b14706df8b2de49869ae03a5ccbc211f4041750cd4a66f698df89d44f4bd30ec"}, - {file = "coverage-7.4.4-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:201bef2eea65e0e9c56343115ba3814e896afe6d36ffd37bab783261db430f76"}, - {file = "coverage-7.4.4-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:41c9c5f3de16b903b610d09650e5e27adbfa7f500302718c9ffd1c12cf9d6818"}, - {file = "coverage-7.4.4-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d898fe162d26929b5960e4e138651f7427048e72c853607f2b200909794ed978"}, - {file = "coverage-7.4.4-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3ea79bb50e805cd6ac058dfa3b5c8f6c040cb87fe83de10845857f5535d1db70"}, - {file = "coverage-7.4.4-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ce4b94265ca988c3f8e479e741693d143026632672e3ff924f25fab50518dd51"}, - {file = "coverage-7.4.4-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:00838a35b882694afda09f85e469c96367daa3f3f2b097d846a7216993d37f4c"}, - {file = "coverage-7.4.4-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:fdfafb32984684eb03c2d83e1e51f64f0906b11e64482df3c5db936ce3839d48"}, - {file = "coverage-7.4.4-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:69eb372f7e2ece89f14751fbcbe470295d73ed41ecd37ca36ed2eb47512a6ab9"}, - {file = "coverage-7.4.4-cp312-cp312-win32.whl", hash = "sha256:137eb07173141545e07403cca94ab625cc1cc6bc4c1e97b6e3846270e7e1fea0"}, - {file = "coverage-7.4.4-cp312-cp312-win_amd64.whl", hash = "sha256:d71eec7d83298f1af3326ce0ff1d0ea83c7cb98f72b577097f9083b20bdaf05e"}, - {file = "coverage-7.4.4-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:d5ae728ff3b5401cc320d792866987e7e7e880e6ebd24433b70a33b643bb0384"}, - {file = "coverage-7.4.4-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:cc4f1358cb0c78edef3ed237ef2c86056206bb8d9140e73b6b89fbcfcbdd40e1"}, - {file = "coverage-7.4.4-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8130a2aa2acb8788e0b56938786c33c7c98562697bf9f4c7d6e8e5e3a0501e4a"}, - {file = "coverage-7.4.4-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:cf271892d13e43bc2b51e6908ec9a6a5094a4df1d8af0bfc360088ee6c684409"}, - {file = "coverage-7.4.4-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a4cdc86d54b5da0df6d3d3a2f0b710949286094c3a6700c21e9015932b81447e"}, - {file = "coverage-7.4.4-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:ae71e7ddb7a413dd60052e90528f2f65270aad4b509563af6d03d53e979feafd"}, - {file = "coverage-7.4.4-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:38dd60d7bf242c4ed5b38e094baf6401faa114fc09e9e6632374388a404f98e7"}, - {file = "coverage-7.4.4-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:aa5b1c1bfc28384f1f53b69a023d789f72b2e0ab1b3787aae16992a7ca21056c"}, - {file = "coverage-7.4.4-cp38-cp38-win32.whl", hash = "sha256:dfa8fe35a0bb90382837b238fff375de15f0dcdb9ae68ff85f7a63649c98527e"}, - {file = "coverage-7.4.4-cp38-cp38-win_amd64.whl", hash = "sha256:b2991665420a803495e0b90a79233c1433d6ed77ef282e8e152a324bbbc5e0c8"}, - {file = "coverage-7.4.4-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:3b799445b9f7ee8bf299cfaed6f5b226c0037b74886a4e11515e569b36fe310d"}, - {file = "coverage-7.4.4-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:b4d33f418f46362995f1e9d4f3a35a1b6322cb959c31d88ae56b0298e1c22357"}, - {file = "coverage-7.4.4-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:aadacf9a2f407a4688d700e4ebab33a7e2e408f2ca04dbf4aef17585389eff3e"}, - {file = "coverage-7.4.4-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7c95949560050d04d46b919301826525597f07b33beba6187d04fa64d47ac82e"}, - {file = "coverage-7.4.4-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ff7687ca3d7028d8a5f0ebae95a6e4827c5616b31a4ee1192bdfde697db110d4"}, - {file = "coverage-7.4.4-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:5fc1de20b2d4a061b3df27ab9b7c7111e9a710f10dc2b84d33a4ab25065994ec"}, - {file = "coverage-7.4.4-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:c74880fc64d4958159fbd537a091d2a585448a8f8508bf248d72112723974cbd"}, - {file = "coverage-7.4.4-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:742a76a12aa45b44d236815d282b03cfb1de3b4323f3e4ec933acfae08e54ade"}, - {file = "coverage-7.4.4-cp39-cp39-win32.whl", hash = "sha256:d89d7b2974cae412400e88f35d86af72208e1ede1a541954af5d944a8ba46c57"}, - {file = "coverage-7.4.4-cp39-cp39-win_amd64.whl", hash = "sha256:9ca28a302acb19b6af89e90f33ee3e1906961f94b54ea37de6737b7ca9d8827c"}, - {file = "coverage-7.4.4-pp38.pp39.pp310-none-any.whl", hash = "sha256:b2c5edc4ac10a7ef6605a966c58929ec6c1bd0917fb8c15cb3363f65aa40e677"}, - {file = "coverage-7.4.4.tar.gz", hash = "sha256:c901df83d097649e257e803be22592aedfd5182f07b3cc87d640bbb9afd50f49"}, + {file = "coverage-7.5.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:432949a32c3e3f820af808db1833d6d1631664d53dd3ce487aa25d574e18ad1c"}, + {file = "coverage-7.5.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:2bd7065249703cbeb6d4ce679c734bef0ee69baa7bff9724361ada04a15b7e3b"}, + {file = "coverage-7.5.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bbfe6389c5522b99768a93d89aca52ef92310a96b99782973b9d11e80511f932"}, + {file = "coverage-7.5.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:39793731182c4be939b4be0cdecde074b833f6171313cf53481f869937129ed3"}, + {file = "coverage-7.5.0-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:85a5dbe1ba1bf38d6c63b6d2c42132d45cbee6d9f0c51b52c59aa4afba057517"}, + {file = "coverage-7.5.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:357754dcdfd811462a725e7501a9b4556388e8ecf66e79df6f4b988fa3d0b39a"}, + {file = "coverage-7.5.0-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:a81eb64feded34f40c8986869a2f764f0fe2db58c0530d3a4afbcde50f314880"}, + {file = "coverage-7.5.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:51431d0abbed3a868e967f8257c5faf283d41ec882f58413cf295a389bb22e58"}, + {file = "coverage-7.5.0-cp310-cp310-win32.whl", hash = "sha256:f609ebcb0242d84b7adeee2b06c11a2ddaec5464d21888b2c8255f5fd6a98ae4"}, + {file = "coverage-7.5.0-cp310-cp310-win_amd64.whl", hash = "sha256:6782cd6216fab5a83216cc39f13ebe30adfac2fa72688c5a4d8d180cd52e8f6a"}, + {file = "coverage-7.5.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:e768d870801f68c74c2b669fc909839660180c366501d4cc4b87efd6b0eee375"}, + {file = "coverage-7.5.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:84921b10aeb2dd453247fd10de22907984eaf80901b578a5cf0bb1e279a587cb"}, + {file = "coverage-7.5.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:710c62b6e35a9a766b99b15cdc56d5aeda0914edae8bb467e9c355f75d14ee95"}, + {file = "coverage-7.5.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c379cdd3efc0658e652a14112d51a7668f6bfca7445c5a10dee7eabecabba19d"}, + {file = "coverage-7.5.0-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fea9d3ca80bcf17edb2c08a4704259dadac196fe5e9274067e7a20511fad1743"}, + {file = "coverage-7.5.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:41327143c5b1d715f5f98a397608f90ab9ebba606ae4e6f3389c2145410c52b1"}, + {file = "coverage-7.5.0-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:565b2e82d0968c977e0b0f7cbf25fd06d78d4856289abc79694c8edcce6eb2de"}, + {file = "coverage-7.5.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:cf3539007202ebfe03923128fedfdd245db5860a36810136ad95a564a2fdffff"}, + {file = "coverage-7.5.0-cp311-cp311-win32.whl", hash = "sha256:bf0b4b8d9caa8d64df838e0f8dcf68fb570c5733b726d1494b87f3da85db3a2d"}, + {file = "coverage-7.5.0-cp311-cp311-win_amd64.whl", hash = "sha256:9c6384cc90e37cfb60435bbbe0488444e54b98700f727f16f64d8bfda0b84656"}, + {file = "coverage-7.5.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:fed7a72d54bd52f4aeb6c6e951f363903bd7d70bc1cad64dd1f087980d309ab9"}, + {file = "coverage-7.5.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:cbe6581fcff7c8e262eb574244f81f5faaea539e712a058e6707a9d272fe5b64"}, + {file = "coverage-7.5.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ad97ec0da94b378e593ef532b980c15e377df9b9608c7c6da3506953182398af"}, + {file = "coverage-7.5.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:bd4bacd62aa2f1a1627352fe68885d6ee694bdaebb16038b6e680f2924a9b2cc"}, + {file = "coverage-7.5.0-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:adf032b6c105881f9d77fa17d9eebe0ad1f9bfb2ad25777811f97c5362aa07f2"}, + {file = "coverage-7.5.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:4ba01d9ba112b55bfa4b24808ec431197bb34f09f66f7cb4fd0258ff9d3711b1"}, + {file = "coverage-7.5.0-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:f0bfe42523893c188e9616d853c47685e1c575fe25f737adf473d0405dcfa7eb"}, + {file = "coverage-7.5.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:a9a7ef30a1b02547c1b23fa9a5564f03c9982fc71eb2ecb7f98c96d7a0db5cf2"}, + {file = "coverage-7.5.0-cp312-cp312-win32.whl", hash = "sha256:3c2b77f295edb9fcdb6a250f83e6481c679335ca7e6e4a955e4290350f2d22a4"}, + {file = "coverage-7.5.0-cp312-cp312-win_amd64.whl", hash = "sha256:427e1e627b0963ac02d7c8730ca6d935df10280d230508c0ba059505e9233475"}, + {file = "coverage-7.5.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:9dd88fce54abbdbf4c42fb1fea0e498973d07816f24c0e27a1ecaf91883ce69e"}, + {file = "coverage-7.5.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:a898c11dca8f8c97b467138004a30133974aacd572818c383596f8d5b2eb04a9"}, + {file = "coverage-7.5.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:07dfdd492d645eea1bd70fb1d6febdcf47db178b0d99161d8e4eed18e7f62fe7"}, + {file = "coverage-7.5.0-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d3d117890b6eee85887b1eed41eefe2e598ad6e40523d9f94c4c4b213258e4a4"}, + {file = "coverage-7.5.0-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6afd2e84e7da40fe23ca588379f815fb6dbbb1b757c883935ed11647205111cb"}, + {file = "coverage-7.5.0-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:a9960dd1891b2ddf13a7fe45339cd59ecee3abb6b8326d8b932d0c5da208104f"}, + {file = "coverage-7.5.0-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:ced268e82af993d7801a9db2dbc1d2322e786c5dc76295d8e89473d46c6b84d4"}, + {file = "coverage-7.5.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:e7c211f25777746d468d76f11719e64acb40eed410d81c26cefac641975beb88"}, + {file = "coverage-7.5.0-cp38-cp38-win32.whl", hash = "sha256:262fffc1f6c1a26125d5d573e1ec379285a3723363f3bd9c83923c9593a2ac25"}, + {file = "coverage-7.5.0-cp38-cp38-win_amd64.whl", hash = "sha256:eed462b4541c540d63ab57b3fc69e7d8c84d5957668854ee4e408b50e92ce26a"}, + {file = "coverage-7.5.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:d0194d654e360b3e6cc9b774e83235bae6b9b2cac3be09040880bb0e8a88f4a1"}, + {file = "coverage-7.5.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:33c020d3322662e74bc507fb11488773a96894aa82a622c35a5a28673c0c26f5"}, + {file = "coverage-7.5.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0cbdf2cae14a06827bec50bd58e49249452d211d9caddd8bd80e35b53cb04631"}, + {file = "coverage-7.5.0-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3235d7c781232e525b0761730e052388a01548bd7f67d0067a253887c6e8df46"}, + {file = "coverage-7.5.0-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:db2de4e546f0ec4b2787d625e0b16b78e99c3e21bc1722b4977c0dddf11ca84e"}, + {file = "coverage-7.5.0-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:4d0e206259b73af35c4ec1319fd04003776e11e859936658cb6ceffdeba0f5be"}, + {file = "coverage-7.5.0-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:2055c4fb9a6ff624253d432aa471a37202cd8f458c033d6d989be4499aed037b"}, + {file = "coverage-7.5.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:075299460948cd12722a970c7eae43d25d37989da682997687b34ae6b87c0ef0"}, + {file = "coverage-7.5.0-cp39-cp39-win32.whl", hash = "sha256:280132aada3bc2f0fac939a5771db4fbb84f245cb35b94fae4994d4c1f80dae7"}, + {file = "coverage-7.5.0-cp39-cp39-win_amd64.whl", hash = "sha256:c58536f6892559e030e6924896a44098bc1290663ea12532c78cef71d0df8493"}, + {file = "coverage-7.5.0-pp38.pp39.pp310-none-any.whl", hash = "sha256:2b57780b51084d5223eee7b59f0d4911c31c16ee5aa12737c7a02455829ff067"}, + {file = "coverage-7.5.0.tar.gz", hash = "sha256:cf62d17310f34084c59c01e027259076479128d11e4661bb6c9acb38c5e19bb8"}, ] [package.dependencies] @@ -1228,13 +1228,13 @@ files = [ [[package]] name = "exceptiongroup" -version = "1.2.0" +version = "1.2.1" description = "Backport of PEP 654 (exception groups)" optional = false python-versions = ">=3.7" files = [ - {file = "exceptiongroup-1.2.0-py3-none-any.whl", hash = "sha256:4bfd3996ac73b41e9b9628b04e079f193850720ea5945fc96a08633c66912f14"}, - {file = "exceptiongroup-1.2.0.tar.gz", hash = "sha256:91f5c769735f051a4290d52edd0858999b57e5876e9f85937691bd4c9fa3ed68"}, + {file = "exceptiongroup-1.2.1-py3-none-any.whl", hash = "sha256:5258b9ed329c5bbdd31a309f53cbfb0b155341807f6ff7606a1e801a891b29ad"}, + {file = "exceptiongroup-1.2.1.tar.gz", hash = "sha256:a4785e48b045528f5bfe627b6ad554ff32def154f42372786903b7abcfe1aa16"}, ] [package.extras] @@ -1270,29 +1270,29 @@ devel = ["colorama", "json-spec", "jsonschema", "pylint", "pytest", "pytest-benc [[package]] name = "filelock" -version = "3.13.1" +version = "3.13.4" description = "A platform independent file lock." optional = false python-versions = ">=3.8" files = [ - {file = "filelock-3.13.1-py3-none-any.whl", hash = "sha256:57dbda9b35157b05fb3e58ee91448612eb674172fab98ee235ccb0b5bee19a1c"}, - {file = "filelock-3.13.1.tar.gz", hash = "sha256:521f5f56c50f8426f5e03ad3b281b490a87ef15bc6c526f168290f0c7148d44e"}, + {file = "filelock-3.13.4-py3-none-any.whl", hash = "sha256:404e5e9253aa60ad457cae1be07c0f0ca90a63931200a47d9b6a6af84fd7b45f"}, + {file = "filelock-3.13.4.tar.gz", hash = "sha256:d13f466618bfde72bd2c18255e269f72542c6e70e7bac83a0232d6b1cc5c8cf4"}, ] [package.extras] -docs = ["furo (>=2023.9.10)", "sphinx (>=7.2.6)", "sphinx-autodoc-typehints (>=1.24)"] -testing = ["covdefaults (>=2.3)", "coverage (>=7.3.2)", "diff-cover (>=8)", "pytest (>=7.4.3)", "pytest-cov (>=4.1)", "pytest-mock (>=3.12)", "pytest-timeout (>=2.2)"] +docs = ["furo (>=2023.9.10)", "sphinx (>=7.2.6)", "sphinx-autodoc-typehints (>=1.25.2)"] +testing = ["covdefaults (>=2.3)", "coverage (>=7.3.2)", "diff-cover (>=8.0.1)", "pytest (>=7.4.3)", "pytest-cov (>=4.1)", "pytest-mock (>=3.12)", "pytest-timeout (>=2.2)"] typing = ["typing-extensions (>=4.8)"] [[package]] name = "flask" -version = "3.0.2" +version = "3.0.3" description = "A simple framework for building complex web applications." optional = false python-versions = ">=3.8" files = [ - {file = "flask-3.0.2-py3-none-any.whl", hash = "sha256:3232e0e9c850d781933cf0207523d1ece087eb8d87b23777ae38456e2fbe7c6e"}, - {file = "flask-3.0.2.tar.gz", hash = "sha256:822c03f4b799204250a7ee84b1eddc40665395333973dfb9deebfe425fefcb7d"}, + {file = "flask-3.0.3-py3-none-any.whl", hash = "sha256:34e815dfaa43340d1d15a5c3a02b8476004037eb4840b34910c6e21679d288f3"}, + {file = "flask-3.0.3.tar.gz", hash = "sha256:ceb27b0af3823ea2737928a4d99d125a06175b8512c445cbd9a9ce200ef76842"}, ] [package.dependencies] @@ -1308,53 +1308,53 @@ dotenv = ["python-dotenv"] [[package]] name = "fonttools" -version = "4.49.0" +version = "4.51.0" description = "Tools to manipulate font files" optional = false python-versions = ">=3.8" files = [ - {file = "fonttools-4.49.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:d970ecca0aac90d399e458f0b7a8a597e08f95de021f17785fb68e2dc0b99717"}, - {file = "fonttools-4.49.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:ac9a745b7609f489faa65e1dc842168c18530874a5f5b742ac3dd79e26bca8bc"}, - {file = "fonttools-4.49.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0ba0e00620ca28d4ca11fc700806fd69144b463aa3275e1b36e56c7c09915559"}, - {file = "fonttools-4.49.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cdee3ab220283057e7840d5fb768ad4c2ebe65bdba6f75d5d7bf47f4e0ed7d29"}, - {file = "fonttools-4.49.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:ce7033cb61f2bb65d8849658d3786188afd80f53dad8366a7232654804529532"}, - {file = "fonttools-4.49.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:07bc5ea02bb7bc3aa40a1eb0481ce20e8d9b9642a9536cde0218290dd6085828"}, - {file = "fonttools-4.49.0-cp310-cp310-win32.whl", hash = "sha256:86eef6aab7fd7c6c8545f3ebd00fd1d6729ca1f63b0cb4d621bccb7d1d1c852b"}, - {file = "fonttools-4.49.0-cp310-cp310-win_amd64.whl", hash = "sha256:1fac1b7eebfce75ea663e860e7c5b4a8831b858c17acd68263bc156125201abf"}, - {file = "fonttools-4.49.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:edc0cce355984bb3c1d1e89d6a661934d39586bb32191ebff98c600f8957c63e"}, - {file = "fonttools-4.49.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:83a0d9336de2cba86d886507dd6e0153df333ac787377325a39a2797ec529814"}, - {file = "fonttools-4.49.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:36c8865bdb5cfeec88f5028e7e592370a0657b676c6f1d84a2108e0564f90e22"}, - {file = "fonttools-4.49.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:33037d9e56e2562c710c8954d0f20d25b8386b397250d65581e544edc9d6b942"}, - {file = "fonttools-4.49.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:8fb022d799b96df3eaa27263e9eea306bd3d437cc9aa981820850281a02b6c9a"}, - {file = "fonttools-4.49.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:33c584c0ef7dc54f5dd4f84082eabd8d09d1871a3d8ca2986b0c0c98165f8e86"}, - {file = "fonttools-4.49.0-cp311-cp311-win32.whl", hash = "sha256:cbe61b158deb09cffdd8540dc4a948d6e8f4d5b4f3bf5cd7db09bd6a61fee64e"}, - {file = "fonttools-4.49.0-cp311-cp311-win_amd64.whl", hash = "sha256:fc11e5114f3f978d0cea7e9853627935b30d451742eeb4239a81a677bdee6bf6"}, - {file = "fonttools-4.49.0-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:d647a0e697e5daa98c87993726da8281c7233d9d4ffe410812a4896c7c57c075"}, - {file = "fonttools-4.49.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:f3bbe672df03563d1f3a691ae531f2e31f84061724c319652039e5a70927167e"}, - {file = "fonttools-4.49.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bebd91041dda0d511b0d303180ed36e31f4f54b106b1259b69fade68413aa7ff"}, - {file = "fonttools-4.49.0-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4145f91531fd43c50f9eb893faa08399816bb0b13c425667c48475c9f3a2b9b5"}, - {file = "fonttools-4.49.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:ea329dafb9670ffbdf4dbc3b0e5c264104abcd8441d56de77f06967f032943cb"}, - {file = "fonttools-4.49.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:c076a9e548521ecc13d944b1d261ff3d7825048c338722a4bd126d22316087b7"}, - {file = "fonttools-4.49.0-cp312-cp312-win32.whl", hash = "sha256:b607ea1e96768d13be26d2b400d10d3ebd1456343eb5eaddd2f47d1c4bd00880"}, - {file = "fonttools-4.49.0-cp312-cp312-win_amd64.whl", hash = "sha256:a974c49a981e187381b9cc2c07c6b902d0079b88ff01aed34695ec5360767034"}, - {file = "fonttools-4.49.0-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:b85ec0bdd7bdaa5c1946398cbb541e90a6dfc51df76dfa88e0aaa41b335940cb"}, - {file = "fonttools-4.49.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:af20acbe198a8a790618ee42db192eb128afcdcc4e96d99993aca0b60d1faeb4"}, - {file = "fonttools-4.49.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4d418b1fee41a1d14931f7ab4b92dc0bc323b490e41d7a333eec82c9f1780c75"}, - {file = "fonttools-4.49.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b44a52b8e6244b6548851b03b2b377a9702b88ddc21dcaf56a15a0393d425cb9"}, - {file = "fonttools-4.49.0-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:7c7125068e04a70739dad11857a4d47626f2b0bd54de39e8622e89701836eabd"}, - {file = "fonttools-4.49.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:29e89d0e1a7f18bc30f197cfadcbef5a13d99806447c7e245f5667579a808036"}, - {file = "fonttools-4.49.0-cp38-cp38-win32.whl", hash = "sha256:9d95fa0d22bf4f12d2fb7b07a46070cdfc19ef5a7b1c98bc172bfab5bf0d6844"}, - {file = "fonttools-4.49.0-cp38-cp38-win_amd64.whl", hash = "sha256:768947008b4dc552d02772e5ebd49e71430a466e2373008ce905f953afea755a"}, - {file = "fonttools-4.49.0-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:08877e355d3dde1c11973bb58d4acad1981e6d1140711230a4bfb40b2b937ccc"}, - {file = "fonttools-4.49.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:fdb54b076f25d6b0f0298dc706acee5052de20c83530fa165b60d1f2e9cbe3cb"}, - {file = "fonttools-4.49.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0af65c720520710cc01c293f9c70bd69684365c6015cc3671db2b7d807fe51f2"}, - {file = "fonttools-4.49.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1f255ce8ed7556658f6d23f6afd22a6d9bbc3edb9b96c96682124dc487e1bf42"}, - {file = "fonttools-4.49.0-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:d00af0884c0e65f60dfaf9340e26658836b935052fdd0439952ae42e44fdd2be"}, - {file = "fonttools-4.49.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:263832fae27481d48dfafcc43174644b6706639661e242902ceb30553557e16c"}, - {file = "fonttools-4.49.0-cp39-cp39-win32.whl", hash = "sha256:0404faea044577a01bb82d47a8fa4bc7a54067fa7e324785dd65d200d6dd1133"}, - {file = "fonttools-4.49.0-cp39-cp39-win_amd64.whl", hash = "sha256:b050d362df50fc6e38ae3954d8c29bf2da52be384649ee8245fdb5186b620836"}, - {file = "fonttools-4.49.0-py3-none-any.whl", hash = "sha256:af281525e5dd7fa0b39fb1667b8d5ca0e2a9079967e14c4bfe90fd1cd13e0f18"}, - {file = "fonttools-4.49.0.tar.gz", hash = "sha256:ebf46e7f01b7af7861310417d7c49591a85d99146fc23a5ba82fdb28af156321"}, + {file = "fonttools-4.51.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:84d7751f4468dd8cdd03ddada18b8b0857a5beec80bce9f435742abc9a851a74"}, + {file = "fonttools-4.51.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:8b4850fa2ef2cfbc1d1f689bc159ef0f45d8d83298c1425838095bf53ef46308"}, + {file = "fonttools-4.51.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b5b48a1121117047d82695d276c2af2ee3a24ffe0f502ed581acc2673ecf1037"}, + {file = "fonttools-4.51.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:180194c7fe60c989bb627d7ed5011f2bef1c4d36ecf3ec64daec8302f1ae0716"}, + {file = "fonttools-4.51.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:96a48e137c36be55e68845fc4284533bda2980f8d6f835e26bca79d7e2006438"}, + {file = "fonttools-4.51.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:806e7912c32a657fa39d2d6eb1d3012d35f841387c8fc6cf349ed70b7c340039"}, + {file = "fonttools-4.51.0-cp310-cp310-win32.whl", hash = "sha256:32b17504696f605e9e960647c5f64b35704782a502cc26a37b800b4d69ff3c77"}, + {file = "fonttools-4.51.0-cp310-cp310-win_amd64.whl", hash = "sha256:c7e91abdfae1b5c9e3a543f48ce96013f9a08c6c9668f1e6be0beabf0a569c1b"}, + {file = "fonttools-4.51.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:a8feca65bab31479d795b0d16c9a9852902e3a3c0630678efb0b2b7941ea9c74"}, + {file = "fonttools-4.51.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:8ac27f436e8af7779f0bb4d5425aa3535270494d3bc5459ed27de3f03151e4c2"}, + {file = "fonttools-4.51.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0e19bd9e9964a09cd2433a4b100ca7f34e34731e0758e13ba9a1ed6e5468cc0f"}, + {file = "fonttools-4.51.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b2b92381f37b39ba2fc98c3a45a9d6383bfc9916a87d66ccb6553f7bdd129097"}, + {file = "fonttools-4.51.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:5f6bc991d1610f5c3bbe997b0233cbc234b8e82fa99fc0b2932dc1ca5e5afec0"}, + {file = "fonttools-4.51.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:9696fe9f3f0c32e9a321d5268208a7cc9205a52f99b89479d1b035ed54c923f1"}, + {file = "fonttools-4.51.0-cp311-cp311-win32.whl", hash = "sha256:3bee3f3bd9fa1d5ee616ccfd13b27ca605c2b4270e45715bd2883e9504735034"}, + {file = "fonttools-4.51.0-cp311-cp311-win_amd64.whl", hash = "sha256:0f08c901d3866a8905363619e3741c33f0a83a680d92a9f0e575985c2634fcc1"}, + {file = "fonttools-4.51.0-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:4060acc2bfa2d8e98117828a238889f13b6f69d59f4f2d5857eece5277b829ba"}, + {file = "fonttools-4.51.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:1250e818b5f8a679ad79660855528120a8f0288f8f30ec88b83db51515411fcc"}, + {file = "fonttools-4.51.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:76f1777d8b3386479ffb4a282e74318e730014d86ce60f016908d9801af9ca2a"}, + {file = "fonttools-4.51.0-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8b5ad456813d93b9c4b7ee55302208db2b45324315129d85275c01f5cb7e61a2"}, + {file = "fonttools-4.51.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:68b3fb7775a923be73e739f92f7e8a72725fd333eab24834041365d2278c3671"}, + {file = "fonttools-4.51.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:8e2f1a4499e3b5ee82c19b5ee57f0294673125c65b0a1ff3764ea1f9db2f9ef5"}, + {file = "fonttools-4.51.0-cp312-cp312-win32.whl", hash = "sha256:278e50f6b003c6aed19bae2242b364e575bcb16304b53f2b64f6551b9c000e15"}, + {file = "fonttools-4.51.0-cp312-cp312-win_amd64.whl", hash = "sha256:b3c61423f22165541b9403ee39874dcae84cd57a9078b82e1dce8cb06b07fa2e"}, + {file = "fonttools-4.51.0-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:1621ee57da887c17312acc4b0e7ac30d3a4fb0fec6174b2e3754a74c26bbed1e"}, + {file = "fonttools-4.51.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:e9d9298be7a05bb4801f558522adbe2feea1b0b103d5294ebf24a92dd49b78e5"}, + {file = "fonttools-4.51.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ee1af4be1c5afe4c96ca23badd368d8dc75f611887fb0c0dac9f71ee5d6f110e"}, + {file = "fonttools-4.51.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c18b49adc721a7d0b8dfe7c3130c89b8704baf599fb396396d07d4aa69b824a1"}, + {file = "fonttools-4.51.0-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:de7c29bdbdd35811f14493ffd2534b88f0ce1b9065316433b22d63ca1cd21f14"}, + {file = "fonttools-4.51.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:cadf4e12a608ef1d13e039864f484c8a968840afa0258b0b843a0556497ea9ed"}, + {file = "fonttools-4.51.0-cp38-cp38-win32.whl", hash = "sha256:aefa011207ed36cd280babfaa8510b8176f1a77261833e895a9d96e57e44802f"}, + {file = "fonttools-4.51.0-cp38-cp38-win_amd64.whl", hash = "sha256:865a58b6e60b0938874af0968cd0553bcd88e0b2cb6e588727117bd099eef836"}, + {file = "fonttools-4.51.0-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:60a3409c9112aec02d5fb546f557bca6efa773dcb32ac147c6baf5f742e6258b"}, + {file = "fonttools-4.51.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:f7e89853d8bea103c8e3514b9f9dc86b5b4120afb4583b57eb10dfa5afbe0936"}, + {file = "fonttools-4.51.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:56fc244f2585d6c00b9bcc59e6593e646cf095a96fe68d62cd4da53dd1287b55"}, + {file = "fonttools-4.51.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0d145976194a5242fdd22df18a1b451481a88071feadf251221af110ca8f00ce"}, + {file = "fonttools-4.51.0-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:c5b8cab0c137ca229433570151b5c1fc6af212680b58b15abd797dcdd9dd5051"}, + {file = "fonttools-4.51.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:54dcf21a2f2d06ded676e3c3f9f74b2bafded3a8ff12f0983160b13e9f2fb4a7"}, + {file = "fonttools-4.51.0-cp39-cp39-win32.whl", hash = "sha256:0118ef998a0699a96c7b28457f15546815015a2710a1b23a7bf6c1be60c01636"}, + {file = "fonttools-4.51.0-cp39-cp39-win_amd64.whl", hash = "sha256:599bdb75e220241cedc6faebfafedd7670335d2e29620d207dd0378a4e9ccc5a"}, + {file = "fonttools-4.51.0-py3-none-any.whl", hash = "sha256:15c94eeef6b095831067f72c825eb0e2d48bb4cea0647c1b05c981ecba2bf39f"}, + {file = "fonttools-4.51.0.tar.gz", hash = "sha256:dc0673361331566d7a663d7ce0f6fdcbfbdc1f59c6e3ed1165ad7202ca183c68"}, ] [package.extras] @@ -1470,13 +1470,13 @@ files = [ [[package]] name = "fsspec" -version = "2024.2.0" +version = "2024.3.1" description = "File-system specification" optional = false python-versions = ">=3.8" files = [ - {file = "fsspec-2024.2.0-py3-none-any.whl", hash = "sha256:817f969556fa5916bc682e02ca2045f96ff7f586d45110fcb76022063ad2c7d8"}, - {file = "fsspec-2024.2.0.tar.gz", hash = "sha256:b6ad1a679f760dda52b1168c859d01b7b80648ea6f7f7c7f5a8a91dc3f3ecb84"}, + {file = "fsspec-2024.3.1-py3-none-any.whl", hash = "sha256:918d18d41bf73f0e2b261824baeb1b124bcf771767e3a26425cd7dec3332f512"}, + {file = "fsspec-2024.3.1.tar.gz", hash = "sha256:f39780e282d7d117ffb42bb96992f8a90795e4d0fb0f661a70ca39fe9c43ded9"}, ] [package.dependencies] @@ -1522,20 +1522,21 @@ smmap = ">=3.0.1,<6" [[package]] name = "gitpython" -version = "3.1.42" +version = "3.1.43" description = "GitPython is a Python library used to interact with Git repositories" optional = false python-versions = ">=3.7" files = [ - {file = "GitPython-3.1.42-py3-none-any.whl", hash = "sha256:1bf9cd7c9e7255f77778ea54359e54ac22a72a5b51288c457c881057b7bb9ecd"}, - {file = "GitPython-3.1.42.tar.gz", hash = "sha256:2d99869e0fef71a73cbd242528105af1d6c1b108c60dfabd994bf292f76c3ceb"}, + {file = "GitPython-3.1.43-py3-none-any.whl", hash = "sha256:eec7ec56b92aad751f9912a73404bc02ba212a23adb2c7098ee668417051a1ff"}, + {file = "GitPython-3.1.43.tar.gz", hash = "sha256:35f314a9f878467f5453cc1fee295c3e18e52f1b99f10f6cf5b1682e968a9e7c"}, ] [package.dependencies] gitdb = ">=4.0.1,<5" [package.extras] -test = ["black", "coverage[toml]", "ddt (>=1.1.1,!=1.4.3)", "mock", "mypy", "pre-commit", "pytest (>=7.3.1)", "pytest-cov", "pytest-instafail", "pytest-mock", "pytest-sugar"] +doc = ["sphinx (==4.3.2)", "sphinx-autodoc-typehints", "sphinx-rtd-theme", "sphinxcontrib-applehelp (>=1.0.2,<=1.0.4)", "sphinxcontrib-devhelp (==1.0.2)", "sphinxcontrib-htmlhelp (>=2.0.0,<=2.0.1)", "sphinxcontrib-qthelp (==1.0.3)", "sphinxcontrib-serializinghtml (==1.1.5)"] +test = ["coverage[toml]", "ddt (>=1.1.1,!=1.4.3)", "mock", "mypy", "pre-commit", "pytest (>=7.3.1)", "pytest-cov", "pytest-instafail", "pytest-mock", "pytest-sugar", "typing-extensions"] [[package]] name = "graphene" @@ -1655,69 +1656,69 @@ test = ["objgraph", "psutil"] [[package]] name = "grpcio" -version = "1.62.1" +version = "1.62.2" description = "HTTP/2-based RPC framework" optional = false python-versions = ">=3.7" files = [ - {file = "grpcio-1.62.1-cp310-cp310-linux_armv7l.whl", hash = "sha256:179bee6f5ed7b5f618844f760b6acf7e910988de77a4f75b95bbfaa8106f3c1e"}, - {file = "grpcio-1.62.1-cp310-cp310-macosx_12_0_universal2.whl", hash = "sha256:48611e4fa010e823ba2de8fd3f77c1322dd60cb0d180dc6630a7e157b205f7ea"}, - {file = "grpcio-1.62.1-cp310-cp310-manylinux_2_17_aarch64.whl", hash = "sha256:b2a0e71b0a2158aa4bce48be9f8f9eb45cbd17c78c7443616d00abbe2a509f6d"}, - {file = "grpcio-1.62.1-cp310-cp310-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:fbe80577c7880911d3ad65e5ecc997416c98f354efeba2f8d0f9112a67ed65a5"}, - {file = "grpcio-1.62.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:58f6c693d446964e3292425e1d16e21a97a48ba9172f2d0df9d7b640acb99243"}, - {file = "grpcio-1.62.1-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:77c339403db5a20ef4fed02e4d1a9a3d9866bf9c0afc77a42234677313ea22f3"}, - {file = "grpcio-1.62.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:b5a4ea906db7dec694098435d84bf2854fe158eb3cd51e1107e571246d4d1d70"}, - {file = "grpcio-1.62.1-cp310-cp310-win32.whl", hash = "sha256:4187201a53f8561c015bc745b81a1b2d278967b8de35f3399b84b0695e281d5f"}, - {file = "grpcio-1.62.1-cp310-cp310-win_amd64.whl", hash = "sha256:844d1f3fb11bd1ed362d3fdc495d0770cfab75761836193af166fee113421d66"}, - {file = "grpcio-1.62.1-cp311-cp311-linux_armv7l.whl", hash = "sha256:833379943d1728a005e44103f17ecd73d058d37d95783eb8f0b28ddc1f54d7b2"}, - {file = "grpcio-1.62.1-cp311-cp311-macosx_10_10_universal2.whl", hash = "sha256:c7fcc6a32e7b7b58f5a7d27530669337a5d587d4066060bcb9dee7a8c833dfb7"}, - {file = "grpcio-1.62.1-cp311-cp311-manylinux_2_17_aarch64.whl", hash = "sha256:fa7d28eb4d50b7cbe75bb8b45ed0da9a1dc5b219a0af59449676a29c2eed9698"}, - {file = "grpcio-1.62.1-cp311-cp311-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:48f7135c3de2f298b833be8b4ae20cafe37091634e91f61f5a7eb3d61ec6f660"}, - {file = "grpcio-1.62.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:71f11fd63365ade276c9d4a7b7df5c136f9030e3457107e1791b3737a9b9ed6a"}, - {file = "grpcio-1.62.1-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:4b49fd8fe9f9ac23b78437da94c54aa7e9996fbb220bac024a67469ce5d0825f"}, - {file = "grpcio-1.62.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:482ae2ae78679ba9ed5752099b32e5fe580443b4f798e1b71df412abf43375db"}, - {file = "grpcio-1.62.1-cp311-cp311-win32.whl", hash = "sha256:1faa02530b6c7426404372515fe5ddf66e199c2ee613f88f025c6f3bd816450c"}, - {file = "grpcio-1.62.1-cp311-cp311-win_amd64.whl", hash = "sha256:5bd90b8c395f39bc82a5fb32a0173e220e3f401ff697840f4003e15b96d1befc"}, - {file = "grpcio-1.62.1-cp312-cp312-linux_armv7l.whl", hash = "sha256:b134d5d71b4e0837fff574c00e49176051a1c532d26c052a1e43231f252d813b"}, - {file = "grpcio-1.62.1-cp312-cp312-macosx_10_10_universal2.whl", hash = "sha256:d1f6c96573dc09d50dbcbd91dbf71d5cf97640c9427c32584010fbbd4c0e0037"}, - {file = "grpcio-1.62.1-cp312-cp312-manylinux_2_17_aarch64.whl", hash = "sha256:359f821d4578f80f41909b9ee9b76fb249a21035a061a327f91c953493782c31"}, - {file = "grpcio-1.62.1-cp312-cp312-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a485f0c2010c696be269184bdb5ae72781344cb4e60db976c59d84dd6354fac9"}, - {file = "grpcio-1.62.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b50b09b4dc01767163d67e1532f948264167cd27f49e9377e3556c3cba1268e1"}, - {file = "grpcio-1.62.1-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:3227c667dccbe38f2c4d943238b887bac588d97c104815aecc62d2fd976e014b"}, - {file = "grpcio-1.62.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:3952b581eb121324853ce2b191dae08badb75cd493cb4e0243368aa9e61cfd41"}, - {file = "grpcio-1.62.1-cp312-cp312-win32.whl", hash = "sha256:83a17b303425104d6329c10eb34bba186ffa67161e63fa6cdae7776ff76df73f"}, - {file = "grpcio-1.62.1-cp312-cp312-win_amd64.whl", hash = "sha256:6696ffe440333a19d8d128e88d440f91fb92c75a80ce4b44d55800e656a3ef1d"}, - {file = "grpcio-1.62.1-cp37-cp37m-linux_armv7l.whl", hash = "sha256:e3393b0823f938253370ebef033c9fd23d27f3eae8eb9a8f6264900c7ea3fb5a"}, - {file = "grpcio-1.62.1-cp37-cp37m-macosx_10_10_universal2.whl", hash = "sha256:83e7ccb85a74beaeae2634f10eb858a0ed1a63081172649ff4261f929bacfd22"}, - {file = "grpcio-1.62.1-cp37-cp37m-manylinux_2_17_aarch64.whl", hash = "sha256:882020c87999d54667a284c7ddf065b359bd00251fcd70279ac486776dbf84ec"}, - {file = "grpcio-1.62.1-cp37-cp37m-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a10383035e864f386fe096fed5c47d27a2bf7173c56a6e26cffaaa5a361addb1"}, - {file = "grpcio-1.62.1-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:960edebedc6b9ada1ef58e1c71156f28689978188cd8cff3b646b57288a927d9"}, - {file = "grpcio-1.62.1-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:23e2e04b83f347d0aadde0c9b616f4726c3d76db04b438fd3904b289a725267f"}, - {file = "grpcio-1.62.1-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:978121758711916d34fe57c1f75b79cdfc73952f1481bb9583399331682d36f7"}, - {file = "grpcio-1.62.1-cp37-cp37m-win_amd64.whl", hash = "sha256:9084086190cc6d628f282e5615f987288b95457292e969b9205e45b442276407"}, - {file = "grpcio-1.62.1-cp38-cp38-linux_armv7l.whl", hash = "sha256:22bccdd7b23c420a27fd28540fb5dcbc97dc6be105f7698cb0e7d7a420d0e362"}, - {file = "grpcio-1.62.1-cp38-cp38-macosx_10_10_universal2.whl", hash = "sha256:8999bf1b57172dbc7c3e4bb3c732658e918f5c333b2942243f10d0d653953ba9"}, - {file = "grpcio-1.62.1-cp38-cp38-manylinux_2_17_aarch64.whl", hash = "sha256:d9e52558b8b8c2f4ac05ac86344a7417ccdd2b460a59616de49eb6933b07a0bd"}, - {file = "grpcio-1.62.1-cp38-cp38-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1714e7bc935780bc3de1b3fcbc7674209adf5208ff825799d579ffd6cd0bd505"}, - {file = "grpcio-1.62.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c8842ccbd8c0e253c1f189088228f9b433f7a93b7196b9e5b6f87dba393f5d5d"}, - {file = "grpcio-1.62.1-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:1f1e7b36bdff50103af95a80923bf1853f6823dd62f2d2a2524b66ed74103e49"}, - {file = "grpcio-1.62.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:bba97b8e8883a8038606480d6b6772289f4c907f6ba780fa1f7b7da7dfd76f06"}, - {file = "grpcio-1.62.1-cp38-cp38-win32.whl", hash = "sha256:a7f615270fe534548112a74e790cd9d4f5509d744dd718cd442bf016626c22e4"}, - {file = "grpcio-1.62.1-cp38-cp38-win_amd64.whl", hash = "sha256:e6c8c8693df718c5ecbc7babb12c69a4e3677fd11de8886f05ab22d4e6b1c43b"}, - {file = "grpcio-1.62.1-cp39-cp39-linux_armv7l.whl", hash = "sha256:73db2dc1b201d20ab7083e7041946910bb991e7e9761a0394bbc3c2632326483"}, - {file = "grpcio-1.62.1-cp39-cp39-macosx_10_10_universal2.whl", hash = "sha256:407b26b7f7bbd4f4751dbc9767a1f0716f9fe72d3d7e96bb3ccfc4aace07c8de"}, - {file = "grpcio-1.62.1-cp39-cp39-manylinux_2_17_aarch64.whl", hash = "sha256:f8de7c8cef9261a2d0a62edf2ccea3d741a523c6b8a6477a340a1f2e417658de"}, - {file = "grpcio-1.62.1-cp39-cp39-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:9bd5c8a1af40ec305d001c60236308a67e25419003e9bb3ebfab5695a8d0b369"}, - {file = "grpcio-1.62.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:be0477cb31da67846a33b1a75c611f88bfbcd427fe17701b6317aefceee1b96f"}, - {file = "grpcio-1.62.1-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:60dcd824df166ba266ee0cfaf35a31406cd16ef602b49f5d4dfb21f014b0dedd"}, - {file = "grpcio-1.62.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:973c49086cabab773525f6077f95e5a993bfc03ba8fc32e32f2c279497780585"}, - {file = "grpcio-1.62.1-cp39-cp39-win32.whl", hash = "sha256:12859468e8918d3bd243d213cd6fd6ab07208195dc140763c00dfe901ce1e1b4"}, - {file = "grpcio-1.62.1-cp39-cp39-win_amd64.whl", hash = "sha256:b7209117bbeebdfa5d898205cc55153a51285757902dd73c47de498ad4d11332"}, - {file = "grpcio-1.62.1.tar.gz", hash = "sha256:6c455e008fa86d9e9a9d85bb76da4277c0d7d9668a3bfa70dbe86e9f3c759947"}, -] - -[package.extras] -protobuf = ["grpcio-tools (>=1.62.1)"] + {file = "grpcio-1.62.2-cp310-cp310-linux_armv7l.whl", hash = "sha256:66344ea741124c38588a664237ac2fa16dfd226964cca23ddc96bd4accccbde5"}, + {file = "grpcio-1.62.2-cp310-cp310-macosx_12_0_universal2.whl", hash = "sha256:5dab7ac2c1e7cb6179c6bfad6b63174851102cbe0682294e6b1d6f0981ad7138"}, + {file = "grpcio-1.62.2-cp310-cp310-manylinux_2_17_aarch64.whl", hash = "sha256:3ad00f3f0718894749d5a8bb0fa125a7980a2f49523731a9b1fabf2b3522aa43"}, + {file = "grpcio-1.62.2-cp310-cp310-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:2e72ddfee62430ea80133d2cbe788e0d06b12f865765cb24a40009668bd8ea05"}, + {file = "grpcio-1.62.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:53d3a59a10af4c2558a8e563aed9f256259d2992ae0d3037817b2155f0341de1"}, + {file = "grpcio-1.62.2-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:a1511a303f8074f67af4119275b4f954189e8313541da7b88b1b3a71425cdb10"}, + {file = "grpcio-1.62.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:b94d41b7412ef149743fbc3178e59d95228a7064c5ab4760ae82b562bdffb199"}, + {file = "grpcio-1.62.2-cp310-cp310-win32.whl", hash = "sha256:a75af2fc7cb1fe25785be7bed1ab18cef959a376cdae7c6870184307614caa3f"}, + {file = "grpcio-1.62.2-cp310-cp310-win_amd64.whl", hash = "sha256:80407bc007754f108dc2061e37480238b0dc1952c855e86a4fc283501ee6bb5d"}, + {file = "grpcio-1.62.2-cp311-cp311-linux_armv7l.whl", hash = "sha256:c1624aa686d4b36790ed1c2e2306cc3498778dffaf7b8dd47066cf819028c3ad"}, + {file = "grpcio-1.62.2-cp311-cp311-macosx_10_10_universal2.whl", hash = "sha256:1c1bb80299bdef33309dff03932264636450c8fdb142ea39f47e06a7153d3063"}, + {file = "grpcio-1.62.2-cp311-cp311-manylinux_2_17_aarch64.whl", hash = "sha256:db068bbc9b1fa16479a82e1ecf172a93874540cb84be69f0b9cb9b7ac3c82670"}, + {file = "grpcio-1.62.2-cp311-cp311-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e2cc8a308780edbe2c4913d6a49dbdb5befacdf72d489a368566be44cadaef1a"}, + {file = "grpcio-1.62.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d0695ae31a89f1a8fc8256050329a91a9995b549a88619263a594ca31b76d756"}, + {file = "grpcio-1.62.2-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:88b4f9ee77191dcdd8810241e89340a12cbe050be3e0d5f2f091c15571cd3930"}, + {file = "grpcio-1.62.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:2a0204532aa2f1afd467024b02b4069246320405bc18abec7babab03e2644e75"}, + {file = "grpcio-1.62.2-cp311-cp311-win32.whl", hash = "sha256:6e784f60e575a0de554ef9251cbc2ceb8790914fe324f11e28450047f264ee6f"}, + {file = "grpcio-1.62.2-cp311-cp311-win_amd64.whl", hash = "sha256:112eaa7865dd9e6d7c0556c8b04ae3c3a2dc35d62ad3373ab7f6a562d8199200"}, + {file = "grpcio-1.62.2-cp312-cp312-linux_armv7l.whl", hash = "sha256:65034473fc09628a02fb85f26e73885cf1ed39ebd9cf270247b38689ff5942c5"}, + {file = "grpcio-1.62.2-cp312-cp312-macosx_10_10_universal2.whl", hash = "sha256:d2c1771d0ee3cf72d69bb5e82c6a82f27fbd504c8c782575eddb7839729fbaad"}, + {file = "grpcio-1.62.2-cp312-cp312-manylinux_2_17_aarch64.whl", hash = "sha256:3abe6838196da518863b5d549938ce3159d809218936851b395b09cad9b5d64a"}, + {file = "grpcio-1.62.2-cp312-cp312-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c5ffeb269f10cedb4f33142b89a061acda9f672fd1357331dbfd043422c94e9e"}, + {file = "grpcio-1.62.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:404d3b4b6b142b99ba1cff0b2177d26b623101ea2ce51c25ef6e53d9d0d87bcc"}, + {file = "grpcio-1.62.2-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:262cda97efdabb20853d3b5a4c546a535347c14b64c017f628ca0cc7fa780cc6"}, + {file = "grpcio-1.62.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:17708db5b11b966373e21519c4c73e5a750555f02fde82276ea2a267077c68ad"}, + {file = "grpcio-1.62.2-cp312-cp312-win32.whl", hash = "sha256:b7ec9e2f8ffc8436f6b642a10019fc513722858f295f7efc28de135d336ac189"}, + {file = "grpcio-1.62.2-cp312-cp312-win_amd64.whl", hash = "sha256:aa787b83a3cd5e482e5c79be030e2b4a122ecc6c5c6c4c42a023a2b581fdf17b"}, + {file = "grpcio-1.62.2-cp37-cp37m-linux_armv7l.whl", hash = "sha256:cfd23ad29bfa13fd4188433b0e250f84ec2c8ba66b14a9877e8bce05b524cf54"}, + {file = "grpcio-1.62.2-cp37-cp37m-macosx_10_10_universal2.whl", hash = "sha256:af15e9efa4d776dfcecd1d083f3ccfb04f876d613e90ef8432432efbeeac689d"}, + {file = "grpcio-1.62.2-cp37-cp37m-manylinux_2_17_aarch64.whl", hash = "sha256:f4aa94361bb5141a45ca9187464ae81a92a2a135ce2800b2203134f7a1a1d479"}, + {file = "grpcio-1.62.2-cp37-cp37m-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:82af3613a219512a28ee5c95578eb38d44dd03bca02fd918aa05603c41018051"}, + {file = "grpcio-1.62.2-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:55ddaf53474e8caeb29eb03e3202f9d827ad3110475a21245f3c7712022882a9"}, + {file = "grpcio-1.62.2-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:c79b518c56dddeec79e5500a53d8a4db90da995dfe1738c3ac57fe46348be049"}, + {file = "grpcio-1.62.2-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:a5eb4844e5e60bf2c446ef38c5b40d7752c6effdee882f716eb57ae87255d20a"}, + {file = "grpcio-1.62.2-cp37-cp37m-win_amd64.whl", hash = "sha256:aaae70364a2d1fb238afd6cc9fcb10442b66e397fd559d3f0968d28cc3ac929c"}, + {file = "grpcio-1.62.2-cp38-cp38-linux_armv7l.whl", hash = "sha256:1bcfe5070e4406f489e39325b76caeadab28c32bf9252d3ae960c79935a4cc36"}, + {file = "grpcio-1.62.2-cp38-cp38-macosx_10_10_universal2.whl", hash = "sha256:da6a7b6b938c15fa0f0568e482efaae9c3af31963eec2da4ff13a6d8ec2888e4"}, + {file = "grpcio-1.62.2-cp38-cp38-manylinux_2_17_aarch64.whl", hash = "sha256:41955b641c34db7d84db8d306937b72bc4968eef1c401bea73081a8d6c3d8033"}, + {file = "grpcio-1.62.2-cp38-cp38-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c772f225483905f675cb36a025969eef9712f4698364ecd3a63093760deea1bc"}, + {file = "grpcio-1.62.2-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:07ce1f775d37ca18c7a141300e5b71539690efa1f51fe17f812ca85b5e73262f"}, + {file = "grpcio-1.62.2-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:26f415f40f4a93579fd648f48dca1c13dfacdfd0290f4a30f9b9aeb745026811"}, + {file = "grpcio-1.62.2-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:db707e3685ff16fc1eccad68527d072ac8bdd2e390f6daa97bc394ea7de4acea"}, + {file = "grpcio-1.62.2-cp38-cp38-win32.whl", hash = "sha256:589ea8e75de5fd6df387de53af6c9189c5231e212b9aa306b6b0d4f07520fbb9"}, + {file = "grpcio-1.62.2-cp38-cp38-win_amd64.whl", hash = "sha256:3c3ed41f4d7a3aabf0f01ecc70d6b5d00ce1800d4af652a549de3f7cf35c4abd"}, + {file = "grpcio-1.62.2-cp39-cp39-linux_armv7l.whl", hash = "sha256:162ccf61499c893831b8437120600290a99c0bc1ce7b51f2c8d21ec87ff6af8b"}, + {file = "grpcio-1.62.2-cp39-cp39-macosx_10_10_universal2.whl", hash = "sha256:f27246d7da7d7e3bd8612f63785a7b0c39a244cf14b8dd9dd2f2fab939f2d7f1"}, + {file = "grpcio-1.62.2-cp39-cp39-manylinux_2_17_aarch64.whl", hash = "sha256:2507006c8a478f19e99b6fe36a2464696b89d40d88f34e4b709abe57e1337467"}, + {file = "grpcio-1.62.2-cp39-cp39-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a90ac47a8ce934e2c8d71e317d2f9e7e6aaceb2d199de940ce2c2eb611b8c0f4"}, + {file = "grpcio-1.62.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:99701979bcaaa7de8d5f60476487c5df8f27483624f1f7e300ff4669ee44d1f2"}, + {file = "grpcio-1.62.2-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:af7dc3f7a44f10863b1b0ecab4078f0a00f561aae1edbd01fd03ad4dcf61c9e9"}, + {file = "grpcio-1.62.2-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:fa63245271920786f4cb44dcada4983a3516be8f470924528cf658731864c14b"}, + {file = "grpcio-1.62.2-cp39-cp39-win32.whl", hash = "sha256:c6ad9c39704256ed91a1cffc1379d63f7d0278d6a0bad06b0330f5d30291e3a3"}, + {file = "grpcio-1.62.2-cp39-cp39-win_amd64.whl", hash = "sha256:16da954692fd61aa4941fbeda405a756cd96b97b5d95ca58a92547bba2c1624f"}, + {file = "grpcio-1.62.2.tar.gz", hash = "sha256:c77618071d96b7a8be2c10701a98537823b9c65ba256c0b9067e0594cdbd954d"}, +] + +[package.extras] +protobuf = ["grpcio-tools (>=1.62.2)"] [[package]] name = "gunicorn" @@ -1752,13 +1753,13 @@ files = [ [[package]] name = "httpcore" -version = "1.0.4" +version = "1.0.5" description = "A minimal low-level HTTP client." optional = false python-versions = ">=3.8" files = [ - {file = "httpcore-1.0.4-py3-none-any.whl", hash = "sha256:ac418c1db41bade2ad53ae2f3834a3a0f5ae76b56cf5aa497d2d033384fc7d73"}, - {file = "httpcore-1.0.4.tar.gz", hash = "sha256:cb2839ccfcba0d2d3c1131d3c3e26dfc327326fbe7a5dc0dbfe9f6c9151bb022"}, + {file = "httpcore-1.0.5-py3-none-any.whl", hash = "sha256:421f18bac248b25d310f3cacd198d55b8e6125c107797b609ff9b7a6ba7991b5"}, + {file = "httpcore-1.0.5.tar.gz", hash = "sha256:34a38e2f9291467ee3b44e89dd52615370e152954ba21721378a87b2960f7a61"}, ] [package.dependencies] @@ -1769,7 +1770,7 @@ h11 = ">=0.13,<0.15" asyncio = ["anyio (>=4.0,<5.0)"] http2 = ["h2 (>=3,<5)"] socks = ["socksio (==1.*)"] -trio = ["trio (>=0.22.0,<0.25.0)"] +trio = ["trio (>=0.22.0,<0.26.0)"] [[package]] name = "httpx" @@ -1813,13 +1814,13 @@ packaging = "*" [[package]] name = "identify" -version = "2.5.35" +version = "2.5.36" description = "File identification library for Python" optional = false python-versions = ">=3.8" files = [ - {file = "identify-2.5.35-py2.py3-none-any.whl", hash = "sha256:c4de0081837b211594f8e877a6b4fad7ca32bbfc1a9307fdd61c28bfe923f13e"}, - {file = "identify-2.5.35.tar.gz", hash = "sha256:10a7ca245cfcd756a554a7288159f72ff105ad233c7c4b9c6f0f4d108f5f6791"}, + {file = "identify-2.5.36-py2.py3-none-any.whl", hash = "sha256:37d93f380f4de590500d9dba7db359d0d3da95ffe7f9de1753faa159e71e7dfa"}, + {file = "identify-2.5.36.tar.gz", hash = "sha256:e5e00f54165f9047fbebeb4a560f9acfb8af4c88232be60a488e9b68d122745d"}, ] [package.extras] @@ -1827,15 +1828,72 @@ license = ["ukkonen"] [[package]] name = "idna" -version = "3.6" +version = "3.7" description = "Internationalized Domain Names in Applications (IDNA)" optional = false python-versions = ">=3.5" files = [ - {file = "idna-3.6-py3-none-any.whl", hash = "sha256:c05567e9c24a6b9faaa835c4821bad0590fbb9d5779e7caa6e1cc4978e7eb24f"}, - {file = "idna-3.6.tar.gz", hash = "sha256:9ecdbbd083b06798ae1e86adcbfe8ab1479cf864e4ee30fe4e46a003d12491ca"}, + {file = "idna-3.7-py3-none-any.whl", hash = "sha256:82fee1fc78add43492d3a1898bfa6d8a904cc97d8427f683ed8e798d07761aa0"}, + {file = "idna-3.7.tar.gz", hash = "sha256:028ff3aadf0609c1fd278d8ea3089299412a7a8b9bd005dd08b9f8285bcb5cfc"}, ] +[[package]] +name = "igraph" +version = "0.11.4" +description = "High performance graph data structures and algorithms" +optional = false +python-versions = ">=3.8" +files = [ + {file = "igraph-0.11.4-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:5e09502860c77f381cffcfd4ee13e3436ab5e079752c7ef04db1c90ecdce8cbb"}, + {file = "igraph-0.11.4-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:e876403a2d0121a57fec353f65da88a454fe14fa585087b1eab89cc80d19926d"}, + {file = "igraph-0.11.4-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8eae3aa83815624821e02c1cc8b63386c8b3712959f5159f6a4487f84a34eb12"}, + {file = "igraph-0.11.4-cp38-cp38-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b1c9039bb3a192a67210d5be4adc009ce9225da5ef1698ebfcde4f84e9c77c06"}, + {file = "igraph-0.11.4-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a070d0a2a2a21aef04b9c5319e55b0d021df2472849d9d1569e9c2b431306e23"}, + {file = "igraph-0.11.4-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:b2b07a25da5cac04a19d78957900fa370694871fc132eb4bb7c577b1dda3b663"}, + {file = "igraph-0.11.4-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:d1848ba6e8e64ad101156cbc320529329ad9e59cf67aa24b55ec59f5064c2e21"}, + {file = "igraph-0.11.4-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:a5b88109c5c41256f3c2e2d23969791ed3a0a684a509602dc704da1576d74c52"}, + {file = "igraph-0.11.4-cp38-cp38-win32.whl", hash = "sha256:2239e5f040811584035bc5251a147fb573d35f48e196be8ed35b3d5bf46b958c"}, + {file = "igraph-0.11.4-cp38-cp38-win_amd64.whl", hash = "sha256:822020b796f9882f3b4cddbe2e9bbb723a7ceab4cccccbbc879d425563dbb241"}, + {file = "igraph-0.11.4-cp39-abi3-macosx_10_9_x86_64.whl", hash = "sha256:28f78de54ef5874ac63914ea550a9390f4ee1faee41cff709e2edd2bd03d094e"}, + {file = "igraph-0.11.4-cp39-abi3-macosx_11_0_arm64.whl", hash = "sha256:a8c12fa6024642009859b68cc81dc6cedee904aff61ef5ee078cebe2c1fc6169"}, + {file = "igraph-0.11.4-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1e1089df6ef152259028bc989e2045e0f570a0367ac5725a0a076318165ed5b0"}, + {file = "igraph-0.11.4-cp39-abi3-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:456f7ef35271fe10cd95dc7437e7d2121b1fc1c3dc70d6a4290645f4eaf41c73"}, + {file = "igraph-0.11.4-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9e262f59da1c84f174b3b0ca960a3bf4988387441cf4ef70a4899731c5100cc4"}, + {file = "igraph-0.11.4-cp39-abi3-musllinux_1_1_aarch64.whl", hash = "sha256:8052b0ed6e4394876daf66b52d590539bfb859108d7acb7da7b6e5b9bf5766f8"}, + {file = "igraph-0.11.4-cp39-abi3-musllinux_1_1_i686.whl", hash = "sha256:3641d2ae9e76e2628367774f4a64095f6cbbea0c94f6bbad7fea2df9c5bf5e82"}, + {file = "igraph-0.11.4-cp39-abi3-musllinux_1_1_x86_64.whl", hash = "sha256:ca02917924dcb24e4ed4fc31a64350060f4ae5c74358a5ff8a334be74f454b70"}, + {file = "igraph-0.11.4-cp39-abi3-win32.whl", hash = "sha256:6db79fdc786b96c559e772a6bf400d606b4730b8bb32b5509cc15f23fdcda314"}, + {file = "igraph-0.11.4-cp39-abi3-win_amd64.whl", hash = "sha256:86703b0b526775df38b14b7bc6ee42ecd7f75df3c3f20be0b8e1b70df6a321ce"}, + {file = "igraph-0.11.4-pp310-pypy310_pp73-macosx_10_9_x86_64.whl", hash = "sha256:51b765bd0e3bb4b16cbee539496de0c1240957f5ea8795f3602ff6654bca578d"}, + {file = "igraph-0.11.4-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:138d0804a445aeeffc5e1eefd792d7d91cc30f3be3eea7c67fe928484579b90f"}, + {file = "igraph-0.11.4-pp310-pypy310_pp73-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:50d49808f4a72b28cb18471b9ea577837504b47fa1799357b5ee26763f791d14"}, + {file = "igraph-0.11.4-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7b323b5645eac4e8b1a16ab902c646605ec9052a88eb70f79284cf30e96d873e"}, + {file = "igraph-0.11.4-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:f5cf58eb72c88da7b4eae50b20282df1713f8c70191d1a8f970cd78eef786577"}, + {file = "igraph-0.11.4-pp38-pypy38_pp73-macosx_10_9_x86_64.whl", hash = "sha256:ebd1cf6620171051dca09987bf1acdb6c7fdd00af8f10e8a6ad20fc54a1bf6b5"}, + {file = "igraph-0.11.4-pp38-pypy38_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e8d40cf9e5c93215d7d896133896284a6c33681042695cc16ce3371b9056dbc7"}, + {file = "igraph-0.11.4-pp38-pypy38_pp73-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:2344b9cf87c81804dac3751f589c751652ae51c9c9e546deda31419d9716acb3"}, + {file = "igraph-0.11.4-pp38-pypy38_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:60d4e8f328c23ab5075b19eb0257a1041381d4516040e964232578767fdc4f1b"}, + {file = "igraph-0.11.4-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:7d3b64b2427024b4e27f57375f0f94ba89243b9be193539925791bd332c6c6ad"}, + {file = "igraph-0.11.4-pp39-pypy39_pp73-macosx_10_9_x86_64.whl", hash = "sha256:897a228feb3f905286e46b4cd33fccd61392a19c3d3c9fccad3362d7a86f3623"}, + {file = "igraph-0.11.4-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cb152bb30298027468f65374a6b2c6b7d28f2d7e0ed58c7d202221de5ce9a92e"}, + {file = "igraph-0.11.4-pp39-pypy39_pp73-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d92ef35f8ad1b7cf934704f154e03e21bd0a133f20e177f904f8b7ca4c341e88"}, + {file = "igraph-0.11.4-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:208f6ff8bec7e6994d64e0e1a209d0821dd33f23c3b4d0f30db0a9a9d35b631f"}, + {file = "igraph-0.11.4-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:82a968311f4ccdd2eeb88758d3df6f67e999476e5f760e284726c9ed4fee62d8"}, + {file = "igraph-0.11.4.tar.gz", hash = "sha256:2437ae0157af6824e2e65a23f7a1fa4fbf0f3664333c72aeca4fc01b83e18483"}, +] + +[package.dependencies] +texttable = ">=1.6.2" + +[package.extras] +cairo = ["cairocffi (>=1.2.0)"] +doc = ["Sphinx (>=7.0.0)", "pydoctor (>=23.4.0)", "sphinx-gallery (>=0.14.0)", "sphinx-rtd-theme (>=1.3.0)"] +matplotlib = ["matplotlib (>=3.6.0)"] +plotly = ["plotly (>=5.3.0)"] +plotting = ["cairocffi (>=1.2.0)"] +test = ["Pillow (>=9)", "cairocffi (>=1.2.0)", "matplotlib (>=3.6.0)", "networkx (>=2.5)", "numpy (>=1.19.0)", "pandas (>=1.1.0)", "plotly (>=5.3.0)", "pytest (>=7.0.1)", "pytest-timeout (>=2.1.0)", "scipy (>=1.5.0)"] +test-musl = ["cairocffi (>=1.2.0)", "networkx (>=2.5)", "pytest (>=7.0.1)", "pytest-timeout (>=2.1.0)"] + [[package]] name = "imagesize" version = "1.4.1" @@ -1849,13 +1907,13 @@ files = [ [[package]] name = "importlib-metadata" -version = "7.0.2" +version = "7.1.0" description = "Read metadata from Python packages" optional = false python-versions = ">=3.8" files = [ - {file = "importlib_metadata-7.0.2-py3-none-any.whl", hash = "sha256:f4bc4c0c070c490abf4ce96d715f68e95923320370efb66143df00199bb6c100"}, - {file = "importlib_metadata-7.0.2.tar.gz", hash = "sha256:198f568f3230878cb1b44fbd7975f87906c22336dba2e4a7f05278c281fbd792"}, + {file = "importlib_metadata-7.1.0-py3-none-any.whl", hash = "sha256:30962b96c0c223483ed6cc7280e7f0199feb01a0e40cfae4d4450fc6fab1f570"}, + {file = "importlib_metadata-7.1.0.tar.gz", hash = "sha256:b78938b926ee8d5f020fc4772d487045805a55ddbad2ecf21c6d60938dc7fcd2"}, ] [package.dependencies] @@ -1864,22 +1922,22 @@ zipp = ">=0.5" [package.extras] docs = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-lint"] perf = ["ipython"] -testing = ["flufl.flake8", "importlib-resources (>=1.3)", "packaging", "pyfakefs", "pytest (>=6)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=2.2)", "pytest-mypy", "pytest-perf (>=0.9.2)", "pytest-ruff (>=0.2.1)"] +testing = ["flufl.flake8", "importlib-resources (>=1.3)", "jaraco.test (>=5.4)", "packaging", "pyfakefs", "pytest (>=6)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=2.2)", "pytest-mypy", "pytest-perf (>=0.9.2)", "pytest-ruff (>=0.2.1)"] [[package]] name = "importlib-resources" -version = "6.3.0" +version = "6.4.0" description = "Read resources from Python packages" optional = false python-versions = ">=3.8" files = [ - {file = "importlib_resources-6.3.0-py3-none-any.whl", hash = "sha256:783407aa1cd05550e3aa123e8f7cfaebee35ffa9cb0242919e2d1e4172222705"}, - {file = "importlib_resources-6.3.0.tar.gz", hash = "sha256:166072a97e86917a9025876f34286f549b9caf1d10b35a1b372bffa1600c6569"}, + {file = "importlib_resources-6.4.0-py3-none-any.whl", hash = "sha256:50d10f043df931902d4194ea07ec57960f66a80449ff867bfe782b4c486ba78c"}, + {file = "importlib_resources-6.4.0.tar.gz", hash = "sha256:cdb2b453b8046ca4e3798eb1d84f3cce1446a0e8e7b5ef4efb600f19fc398145"}, ] [package.extras] docs = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (<7.2.5)", "sphinx (>=3.5)", "sphinx-lint"] -testing = ["jaraco.collections", "pytest (>=6)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=2.2)", "pytest-mypy", "pytest-ruff (>=0.2.1)", "zipp (>=3.17)"] +testing = ["jaraco.test (>=5.4)", "pytest (>=6)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=2.2)", "pytest-mypy", "pytest-ruff (>=0.2.1)", "zipp (>=3.17)"] [[package]] name = "iniconfig" @@ -1894,13 +1952,13 @@ files = [ [[package]] name = "ipykernel" -version = "6.29.3" +version = "6.29.4" description = "IPython Kernel for Jupyter" optional = false python-versions = ">=3.8" files = [ - {file = "ipykernel-6.29.3-py3-none-any.whl", hash = "sha256:5aa086a4175b0229d4eca211e181fb473ea78ffd9869af36ba7694c947302a21"}, - {file = "ipykernel-6.29.3.tar.gz", hash = "sha256:e14c250d1f9ea3989490225cc1a542781b095a18a19447fcf2b5eaf7d0ac5bd2"}, + {file = "ipykernel-6.29.4-py3-none-any.whl", hash = "sha256:1181e653d95c6808039c509ef8e67c4126b3b3af7781496c7cbfb5ed938a27da"}, + {file = "ipykernel-6.29.4.tar.gz", hash = "sha256:3d44070060f9475ac2092b760123fadf105d2e2493c24848b6691a7c4f42af5c"}, ] [package.dependencies] @@ -1927,13 +1985,13 @@ test = ["flaky", "ipyparallel", "pre-commit", "pytest (>=7.0)", "pytest-asyncio [[package]] name = "ipython" -version = "8.22.2" +version = "8.24.0" description = "IPython: Productive Interactive Computing" optional = false python-versions = ">=3.10" files = [ - {file = "ipython-8.22.2-py3-none-any.whl", hash = "sha256:3c86f284c8f3d8f2b6c662f885c4889a91df7cd52056fd02b7d8d6195d7f56e9"}, - {file = "ipython-8.22.2.tar.gz", hash = "sha256:2dcaad9049f9056f1fef63514f176c7d41f930daa78d05b82a176202818f2c14"}, + {file = "ipython-8.24.0-py3-none-any.whl", hash = "sha256:d7bf2f6c4314984e3e02393213bab8703cf163ede39672ce5918c51fe253a2a3"}, + {file = "ipython-8.24.0.tar.gz", hash = "sha256:010db3f8a728a578bb641fdd06c063b9fb8e96a9464c63aec6310fbcb5e80501"}, ] [package.dependencies] @@ -1947,18 +2005,20 @@ prompt-toolkit = ">=3.0.41,<3.1.0" pygments = ">=2.4.0" stack-data = "*" traitlets = ">=5.13.0" +typing-extensions = {version = ">=4.6", markers = "python_version < \"3.12\""} [package.extras] -all = ["ipython[black,doc,kernel,nbconvert,nbformat,notebook,parallel,qtconsole,terminal]", "ipython[test,test-extra]"] +all = ["ipython[black,doc,kernel,matplotlib,nbconvert,nbformat,notebook,parallel,qtconsole]", "ipython[test,test-extra]"] black = ["black"] doc = ["docrepr", "exceptiongroup", "ipykernel", "ipython[test]", "matplotlib", "setuptools (>=18.5)", "sphinx (>=1.3)", "sphinx-rtd-theme", "sphinxcontrib-jquery", "stack-data", "typing-extensions"] kernel = ["ipykernel"] +matplotlib = ["matplotlib"] nbconvert = ["nbconvert"] nbformat = ["nbformat"] notebook = ["ipywidgets", "notebook"] parallel = ["ipyparallel"] qtconsole = ["qtconsole"] -test = ["pickleshare", "pytest (<8)", "pytest-asyncio (<0.22)", "testpath"] +test = ["pickleshare", "pytest", "pytest-asyncio (<0.22)", "testpath"] test-extra = ["curio", "ipython[test]", "matplotlib (!=3.2.0)", "nbformat", "numpy (>=1.23)", "pandas", "trio"] [[package]] @@ -2026,13 +2086,13 @@ colors = ["colorama (>=0.4.6)"] [[package]] name = "itsdangerous" -version = "2.1.2" +version = "2.2.0" description = "Safely pass data to untrusted environments and back." optional = false -python-versions = ">=3.7" +python-versions = ">=3.8" files = [ - {file = "itsdangerous-2.1.2-py3-none-any.whl", hash = "sha256:2c2349112351b88699d8d4b6b075022c0808887cb7ad10069318a8b0bc88db44"}, - {file = "itsdangerous-2.1.2.tar.gz", hash = "sha256:5dbbc68b317e5e42f327f9021763545dc3fc3bfe22e6deb96aaf1fc38874156a"}, + {file = "itsdangerous-2.2.0-py3-none-any.whl", hash = "sha256:c6242fc49e35958c8b15141343aa660db5fc54d4f13a1db01a3f5891b98700ef"}, + {file = "itsdangerous-2.2.0.tar.gz", hash = "sha256:e0050c0b7da1eea53ffaf149c0cfbb5c6e2e2b69c4bef22c81fa6eb73e5f6173"}, ] [[package]] @@ -2073,29 +2133,26 @@ i18n = ["Babel (>=2.7)"] [[package]] name = "joblib" -version = "1.3.2" +version = "1.4.0" description = "Lightweight pipelining with Python functions" optional = false -python-versions = ">=3.7" +python-versions = ">=3.8" files = [ - {file = "joblib-1.3.2-py3-none-any.whl", hash = "sha256:ef4331c65f239985f3f2220ecc87db222f08fd22097a3dd5698f693875f8cbb9"}, - {file = "joblib-1.3.2.tar.gz", hash = "sha256:92f865e621e17784e7955080b6d042489e3b8e294949cc44c6eac304f59772b1"}, + {file = "joblib-1.4.0-py3-none-any.whl", hash = "sha256:42942470d4062537be4d54c83511186da1fc14ba354961a2114da91efa9a4ed7"}, + {file = "joblib-1.4.0.tar.gz", hash = "sha256:1eb0dc091919cd384490de890cb5dfd538410a6d4b3b54eef09fb8c50b409b1c"}, ] [[package]] name = "json5" -version = "0.9.22" +version = "0.9.25" description = "A Python implementation of the JSON5 data format." optional = false python-versions = ">=3.8" files = [ - {file = "json5-0.9.22-py3-none-any.whl", hash = "sha256:6621007c70897652f8b5d03885f732771c48d1925591ad989aa80c7e0e5ad32f"}, - {file = "json5-0.9.22.tar.gz", hash = "sha256:b729bde7650b2196a35903a597d2b704b8fdf8648bfb67368cfb79f1174a17bd"}, + {file = "json5-0.9.25-py3-none-any.whl", hash = "sha256:34ed7d834b1341a86987ed52f3f76cd8ee184394906b6e22a1e0deb9ab294e8f"}, + {file = "json5-0.9.25.tar.gz", hash = "sha256:548e41b9be043f9426776f05df8635a00fe06104ea51ed24b67f908856e151ae"}, ] -[package.extras] -dev = ["hypothesis"] - [[package]] name = "jsonargparse" version = "4.20.1" @@ -2135,18 +2192,19 @@ urls = ["requests (>=2.18.4)"] [[package]] name = "jsonpickle" -version = "3.0.3" -description = "Python library for serializing any arbitrary object graph into JSON" +version = "3.0.4" +description = "Serialize any Python object to JSON" optional = false python-versions = ">=3.7" files = [ - {file = "jsonpickle-3.0.3-py3-none-any.whl", hash = "sha256:e8d6dcc58f6722bea0321cd328fbda81c582461185688a535df02be0f699afb4"}, - {file = "jsonpickle-3.0.3.tar.gz", hash = "sha256:5691f44495327858ab3a95b9c440a79b41e35421be1a6e09a47b6c9b9421fd06"}, + {file = "jsonpickle-3.0.4-py3-none-any.whl", hash = "sha256:04ae7567a14269579e3af66b76bda284587458d7e8a204951ca8f71a3309952e"}, + {file = "jsonpickle-3.0.4.tar.gz", hash = "sha256:a1b14c8d6221cd8f394f2a97e735ea1d7edc927fbd135b26f2f8700657c8c62b"}, ] [package.extras] -docs = ["furo", "jaraco.packaging (>=9)", "rst.linker (>=1.9)", "sphinx"] -testing = ["ecdsa", "feedparser", "gmpy2", "numpy", "pandas", "pymongo", "pytest (>=3.5,!=3.7.3)", "pytest-checkdocs (>=1.2.3)", "pytest-cov", "pytest-enabler (>=1.0.1)", "pytest-ruff", "scikit-learn", "simplejson", "sqlalchemy", "ujson"] +docs = ["furo", "rst.linker (>=1.9)", "sphinx"] +packaging = ["build", "twine"] +testing = ["bson", "ecdsa", "feedparser", "gmpy2", "numpy", "pandas", "pymongo", "pytest (>=3.5,!=3.7.3)", "pytest-benchmark", "pytest-benchmark[histogram]", "pytest-checkdocs (>=1.2.3)", "pytest-cov", "pytest-enabler (>=1.0.1)", "pytest-ruff (>=0.2.1)", "scikit-learn", "scipy", "scipy (>=1.9.3)", "simplejson", "sqlalchemy", "ujson"] [[package]] name = "jsonpointer" @@ -2317,13 +2375,13 @@ test = ["ipykernel", "pre-commit", "pytest (<8)", "pytest-cov", "pytest-timeout" [[package]] name = "jupyter-events" -version = "0.9.1" +version = "0.10.0" description = "Jupyter Event System library" optional = false python-versions = ">=3.8" files = [ - {file = "jupyter_events-0.9.1-py3-none-any.whl", hash = "sha256:e51f43d2c25c2ddf02d7f7a5045f71fc1d5cb5ad04ef6db20da961c077654b9b"}, - {file = "jupyter_events-0.9.1.tar.gz", hash = "sha256:a52e86f59eb317ee71ff2d7500c94b963b8a24f0b7a1517e2e653e24258e15c7"}, + {file = "jupyter_events-0.10.0-py3-none-any.whl", hash = "sha256:4b72130875e59d57716d327ea70d3ebc3af1944d3717e5a498b8a06c6c159960"}, + {file = "jupyter_events-0.10.0.tar.gz", hash = "sha256:670b8229d3cc882ec782144ed22e0d29e1c2d639263f92ca8383e66682845e22"}, ] [package.dependencies] @@ -2342,13 +2400,13 @@ test = ["click", "pre-commit", "pytest (>=7.0)", "pytest-asyncio (>=0.19.0)", "p [[package]] name = "jupyter-lsp" -version = "2.2.4" +version = "2.2.5" description = "Multi-Language Server WebSocket proxy for Jupyter Notebook/Lab server" optional = false python-versions = ">=3.8" files = [ - {file = "jupyter-lsp-2.2.4.tar.gz", hash = "sha256:5e50033149344065348e688608f3c6d654ef06d9856b67655bd7b6bac9ee2d59"}, - {file = "jupyter_lsp-2.2.4-py3-none-any.whl", hash = "sha256:da61cb63a16b6dff5eac55c2699cc36eac975645adee02c41bdfc03bf4802e77"}, + {file = "jupyter-lsp-2.2.5.tar.gz", hash = "sha256:793147a05ad446f809fd53ef1cd19a9f5256fd0a2d6b7ce943a982cb4f545001"}, + {file = "jupyter_lsp-2.2.5-py3-none-any.whl", hash = "sha256:45fbddbd505f3fbfb0b6cb2f1bc5e15e83ab7c79cd6e89416b248cb3c00c11da"}, ] [package.dependencies] @@ -2356,39 +2414,39 @@ jupyter-server = ">=1.1.2" [[package]] name = "jupyter-server" -version = "2.13.0" +version = "2.14.0" description = "The backend—i.e. core services, APIs, and REST endpoints—to Jupyter web applications." optional = false python-versions = ">=3.8" files = [ - {file = "jupyter_server-2.13.0-py3-none-any.whl", hash = "sha256:77b2b49c3831fbbfbdb5048cef4350d12946191f833a24e5f83e5f8f4803e97b"}, - {file = "jupyter_server-2.13.0.tar.gz", hash = "sha256:c80bfb049ea20053c3d9641c2add4848b38073bf79f1729cea1faed32fc1c78e"}, + {file = "jupyter_server-2.14.0-py3-none-any.whl", hash = "sha256:fb6be52c713e80e004fac34b35a0990d6d36ba06fd0a2b2ed82b899143a64210"}, + {file = "jupyter_server-2.14.0.tar.gz", hash = "sha256:659154cea512083434fd7c93b7fe0897af7a2fd0b9dd4749282b42eaac4ae677"}, ] [package.dependencies] anyio = ">=3.1.0" -argon2-cffi = "*" -jinja2 = "*" +argon2-cffi = ">=21.1" +jinja2 = ">=3.0.3" jupyter-client = ">=7.4.4" jupyter-core = ">=4.12,<5.0.dev0 || >=5.1.dev0" jupyter-events = ">=0.9.0" -jupyter-server-terminals = "*" +jupyter-server-terminals = ">=0.4.4" nbconvert = ">=6.4.4" nbformat = ">=5.3.0" -overrides = "*" -packaging = "*" -prometheus-client = "*" -pywinpty = {version = "*", markers = "os_name == \"nt\""} +overrides = ">=5.0" +packaging = ">=22.0" +prometheus-client = ">=0.9" +pywinpty = {version = ">=2.0.1", markers = "os_name == \"nt\""} pyzmq = ">=24" send2trash = ">=1.8.2" terminado = ">=0.8.3" tornado = ">=6.2.0" traitlets = ">=5.6.0" -websocket-client = "*" +websocket-client = ">=1.7" [package.extras] docs = ["ipykernel", "jinja2", "jupyter-client", "jupyter-server", "myst-parser", "nbformat", "prometheus-client", "pydata-sphinx-theme", "send2trash", "sphinx-autodoc-typehints", "sphinxcontrib-github-alt", "sphinxcontrib-openapi (>=0.8.0)", "sphinxcontrib-spelling", "sphinxemoji", "tornado", "typing-extensions"] -test = ["flaky", "ipykernel", "pre-commit", "pytest (>=7.0)", "pytest-console-scripts", "pytest-jupyter[server] (>=0.7)", "pytest-timeout", "requests"] +test = ["flaky", "ipykernel", "pre-commit", "pytest (>=7.0,<9)", "pytest-console-scripts", "pytest-jupyter[server] (>=0.7)", "pytest-timeout", "requests"] [[package]] name = "jupyter-server-terminals" @@ -2411,27 +2469,27 @@ test = ["jupyter-server (>=2.0.0)", "pytest (>=7.0)", "pytest-jupyter[server] (> [[package]] name = "jupyterlab" -version = "4.1.5" +version = "4.1.8" description = "JupyterLab computational environment" optional = false python-versions = ">=3.8" files = [ - {file = "jupyterlab-4.1.5-py3-none-any.whl", hash = "sha256:3bc843382a25e1ab7bc31d9e39295a9f0463626692b7995597709c0ab236ab2c"}, - {file = "jupyterlab-4.1.5.tar.gz", hash = "sha256:c9ad75290cb10bfaff3624bf3fbb852319b4cce4c456613f8ebbaa98d03524db"}, + {file = "jupyterlab-4.1.8-py3-none-any.whl", hash = "sha256:c3baf3a2f91f89d110ed5786cd18672b9a357129d4e389d2a0dead15e11a4d2c"}, + {file = "jupyterlab-4.1.8.tar.gz", hash = "sha256:3384aded8680e7ce504fd63b8bb89a39df21c9c7694d9e7dc4a68742cdb30f9b"}, ] [package.dependencies] async-lru = ">=1.0.0" httpx = ">=0.25.0" -ipykernel = "*" +ipykernel = ">=6.5.0" jinja2 = ">=3.0.3" jupyter-core = "*" jupyter-lsp = ">=2.0.0" jupyter-server = ">=2.4.0,<3" -jupyterlab-server = ">=2.19.0,<3" +jupyterlab-server = ">=2.27.1,<3" notebook-shim = ">=0.2" packaging = "*" -tomli = {version = "*", markers = "python_version < \"3.11\""} +tomli = {version = ">=1.2.2", markers = "python_version < \"3.11\""} tornado = ">=6.2.0" traitlets = "*" @@ -2440,6 +2498,7 @@ dev = ["build", "bump2version", "coverage", "hatch", "pre-commit", "pytest-cov", docs = ["jsx-lexer", "myst-parser", "pydata-sphinx-theme (>=0.13.0)", "pytest", "pytest-check-links", "pytest-jupyter", "sphinx (>=1.8,<7.3.0)", "sphinx-copybutton"] docs-screenshots = ["altair (==5.2.0)", "ipython (==8.16.1)", "ipywidgets (==8.1.1)", "jupyterlab-geojson (==3.4.0)", "jupyterlab-language-pack-zh-cn (==4.0.post6)", "matplotlib (==3.8.2)", "nbconvert (>=7.0.0)", "pandas (==2.2.0)", "scipy (==1.12.0)", "vega-datasets (==0.9.0)"] test = ["coverage", "pytest (>=7.0)", "pytest-check-links (>=0.7)", "pytest-console-scripts", "pytest-cov", "pytest-jupyter (>=0.5.3)", "pytest-timeout", "pytest-tornasync", "requests", "requests-cache", "virtualenv"] +upgrade-extension = ["copier (>=8.0,<9.0)", "jinja2-time (<0.3)", "pydantic (<2.0)", "pyyaml-include (<2.0)", "tomli-w (<2.0)"] [[package]] name = "jupyterlab-pygments" @@ -2454,13 +2513,13 @@ files = [ [[package]] name = "jupyterlab-server" -version = "2.25.4" +version = "2.27.1" description = "A set of server components for JupyterLab and JupyterLab like applications." optional = false python-versions = ">=3.8" files = [ - {file = "jupyterlab_server-2.25.4-py3-none-any.whl", hash = "sha256:eb645ecc8f9b24bac5decc7803b6d5363250e16ec5af814e516bc2c54dd88081"}, - {file = "jupyterlab_server-2.25.4.tar.gz", hash = "sha256:2098198e1e82e0db982440f9b5136175d73bea2cd42a6480aa6fd502cb23c4f9"}, + {file = "jupyterlab_server-2.27.1-py3-none-any.whl", hash = "sha256:f5e26156e5258b24d532c84e7c74cc212e203bff93eb856f81c24c16daeecc75"}, + {file = "jupyterlab_server-2.27.1.tar.gz", hash = "sha256:097b5ac709b676c7284ac9c5e373f11930a561f52cd5a86e4fc7e5a9c8a8631d"}, ] [package.dependencies] @@ -2678,13 +2737,13 @@ files = [ [[package]] name = "lightning-utilities" -version = "0.10.1" +version = "0.11.2" description = "Lightning toolbox for across the our ecosystem." optional = false -python-versions = ">=3.7" +python-versions = ">=3.8" files = [ - {file = "lightning-utilities-0.10.1.tar.gz", hash = "sha256:362755023dcf93b8fa519bc002ae41794943a3ffbc5318e40804d36aa14bf1fd"}, - {file = "lightning_utilities-0.10.1-py3-none-any.whl", hash = "sha256:e67be3f328b1c14f2b36cc09e84642db5b50afeab94e7704969b2130fe6a3bda"}, + {file = "lightning-utilities-0.11.2.tar.gz", hash = "sha256:adf4cf9c5d912fe505db4729e51d1369c6927f3a8ac55a9dff895ce5c0da08d9"}, + {file = "lightning_utilities-0.11.2-py3-none-any.whl", hash = "sha256:541f471ed94e18a28d72879338c8c52e873bb46f4c47644d89228faeb6751159"}, ] [package.dependencies] @@ -2699,23 +2758,24 @@ typing = ["mypy (>=1.0.0)", "types-setuptools"] [[package]] name = "lit" -version = "18.1.1" +version = "18.1.4" description = "A Software Testing Tool" optional = false python-versions = "*" files = [ - {file = "lit-18.1.1.tar.gz", hash = "sha256:b687537151649101f9802b22615ffdd97e1ee90c07119129af09bf2bdca23472"}, + {file = "lit-18.1.4-py3-none-any.whl", hash = "sha256:167a8beec8cc3628a38b31727b647953541df043168c84c4e99397515d489054"}, + {file = "lit-18.1.4.tar.gz", hash = "sha256:e6ca26eb0a86aef88cb674616100e32d1250d05cfec4ca57a74acabb0a26de78"}, ] [[package]] name = "mako" -version = "1.3.2" +version = "1.3.3" description = "A super-fast templating language that borrows the best ideas from the existing templating languages." optional = false python-versions = ">=3.8" files = [ - {file = "Mako-1.3.2-py3-none-any.whl", hash = "sha256:32a99d70754dfce237019d17ffe4a282d2d3351b9c476e90d8a60e63f133b80c"}, - {file = "Mako-1.3.2.tar.gz", hash = "sha256:2a0c8ad7f6274271b3bb7467dd37cf9cc6dab4bc19cb69a4ef10669402de698e"}, + {file = "Mako-1.3.3-py3-none-any.whl", hash = "sha256:5324b88089a8978bf76d1629774fcc2f1c07b82acdf00f4c5dd8ceadfffc4b40"}, + {file = "Mako-1.3.3.tar.gz", hash = "sha256:e16c01d9ab9c11f7290eef1cfefc093fb5a45ee4a3da09e2fec2e4d1bae54e73"}, ] [package.dependencies] @@ -2855,39 +2915,39 @@ tests = ["pytest", "pytz", "simplejson"] [[package]] name = "matplotlib" -version = "3.8.3" +version = "3.8.4" description = "Python plotting package" optional = false python-versions = ">=3.9" files = [ - {file = "matplotlib-3.8.3-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:cf60138ccc8004f117ab2a2bad513cc4d122e55864b4fe7adf4db20ca68a078f"}, - {file = "matplotlib-3.8.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:5f557156f7116be3340cdeef7f128fa99b0d5d287d5f41a16e169819dcf22357"}, - {file = "matplotlib-3.8.3-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f386cf162b059809ecfac3bcc491a9ea17da69fa35c8ded8ad154cd4b933d5ec"}, - {file = "matplotlib-3.8.3-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b3c5f96f57b0369c288bf6f9b5274ba45787f7e0589a34d24bdbaf6d3344632f"}, - {file = "matplotlib-3.8.3-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:83e0f72e2c116ca7e571c57aa29b0fe697d4c6425c4e87c6e994159e0c008635"}, - {file = "matplotlib-3.8.3-cp310-cp310-win_amd64.whl", hash = "sha256:1c5c8290074ba31a41db1dc332dc2b62def469ff33766cbe325d32a3ee291aea"}, - {file = "matplotlib-3.8.3-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:5184e07c7e1d6d1481862ee361905b7059f7fe065fc837f7c3dc11eeb3f2f900"}, - {file = "matplotlib-3.8.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:d7e7e0993d0758933b1a241a432b42c2db22dfa37d4108342ab4afb9557cbe3e"}, - {file = "matplotlib-3.8.3-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:04b36ad07eac9740fc76c2aa16edf94e50b297d6eb4c081e3add863de4bb19a7"}, - {file = "matplotlib-3.8.3-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7c42dae72a62f14982f1474f7e5c9959fc4bc70c9de11cc5244c6e766200ba65"}, - {file = "matplotlib-3.8.3-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:bf5932eee0d428192c40b7eac1399d608f5d995f975cdb9d1e6b48539a5ad8d0"}, - {file = "matplotlib-3.8.3-cp311-cp311-win_amd64.whl", hash = "sha256:40321634e3a05ed02abf7c7b47a50be50b53ef3eaa3a573847431a545585b407"}, - {file = "matplotlib-3.8.3-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:09074f8057917d17ab52c242fdf4916f30e99959c1908958b1fc6032e2d0f6d4"}, - {file = "matplotlib-3.8.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:5745f6d0fb5acfabbb2790318db03809a253096e98c91b9a31969df28ee604aa"}, - {file = "matplotlib-3.8.3-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b97653d869a71721b639714b42d87cda4cfee0ee74b47c569e4874c7590c55c5"}, - {file = "matplotlib-3.8.3-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:242489efdb75b690c9c2e70bb5c6550727058c8a614e4c7716f363c27e10bba1"}, - {file = "matplotlib-3.8.3-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:83c0653c64b73926730bd9ea14aa0f50f202ba187c307a881673bad4985967b7"}, - {file = "matplotlib-3.8.3-cp312-cp312-win_amd64.whl", hash = "sha256:ef6c1025a570354297d6c15f7d0f296d95f88bd3850066b7f1e7b4f2f4c13a39"}, - {file = "matplotlib-3.8.3-cp39-cp39-macosx_10_12_x86_64.whl", hash = "sha256:c4af3f7317f8a1009bbb2d0bf23dfaba859eb7dd4ccbd604eba146dccaaaf0a4"}, - {file = "matplotlib-3.8.3-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:4c6e00a65d017d26009bac6808f637b75ceade3e1ff91a138576f6b3065eeeba"}, - {file = "matplotlib-3.8.3-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e7b49ab49a3bea17802df6872f8d44f664ba8f9be0632a60c99b20b6db2165b7"}, - {file = "matplotlib-3.8.3-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6728dde0a3997396b053602dbd907a9bd64ec7d5cf99e728b404083698d3ca01"}, - {file = "matplotlib-3.8.3-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:813925d08fb86aba139f2d31864928d67511f64e5945ca909ad5bc09a96189bb"}, - {file = "matplotlib-3.8.3-cp39-cp39-win_amd64.whl", hash = "sha256:cd3a0c2be76f4e7be03d34a14d49ded6acf22ef61f88da600a18a5cd8b3c5f3c"}, - {file = "matplotlib-3.8.3-pp39-pypy39_pp73-macosx_10_12_x86_64.whl", hash = "sha256:fa93695d5c08544f4a0dfd0965f378e7afc410d8672816aff1e81be1f45dbf2e"}, - {file = "matplotlib-3.8.3-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e9764df0e8778f06414b9d281a75235c1e85071f64bb5d71564b97c1306a2afc"}, - {file = "matplotlib-3.8.3-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:5e431a09e6fab4012b01fc155db0ce6dccacdbabe8198197f523a4ef4805eb26"}, - {file = "matplotlib-3.8.3.tar.gz", hash = "sha256:7b416239e9ae38be54b028abbf9048aff5054a9aba5416bef0bd17f9162ce161"}, + {file = "matplotlib-3.8.4-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:abc9d838f93583650c35eca41cfcec65b2e7cb50fd486da6f0c49b5e1ed23014"}, + {file = "matplotlib-3.8.4-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:8f65c9f002d281a6e904976007b2d46a1ee2bcea3a68a8c12dda24709ddc9106"}, + {file = "matplotlib-3.8.4-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ce1edd9f5383b504dbc26eeea404ed0a00656c526638129028b758fd43fc5f10"}, + {file = "matplotlib-3.8.4-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ecd79298550cba13a43c340581a3ec9c707bd895a6a061a78fa2524660482fc0"}, + {file = "matplotlib-3.8.4-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:90df07db7b599fe7035d2f74ab7e438b656528c68ba6bb59b7dc46af39ee48ef"}, + {file = "matplotlib-3.8.4-cp310-cp310-win_amd64.whl", hash = "sha256:ac24233e8f2939ac4fd2919eed1e9c0871eac8057666070e94cbf0b33dd9c338"}, + {file = "matplotlib-3.8.4-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:72f9322712e4562e792b2961971891b9fbbb0e525011e09ea0d1f416c4645661"}, + {file = "matplotlib-3.8.4-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:232ce322bfd020a434caaffbd9a95333f7c2491e59cfc014041d95e38ab90d1c"}, + {file = "matplotlib-3.8.4-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6addbd5b488aedb7f9bc19f91cd87ea476206f45d7116fcfe3d31416702a82fa"}, + {file = "matplotlib-3.8.4-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cc4ccdc64e3039fc303defd119658148f2349239871db72cd74e2eeaa9b80b71"}, + {file = "matplotlib-3.8.4-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:b7a2a253d3b36d90c8993b4620183b55665a429da8357a4f621e78cd48b2b30b"}, + {file = "matplotlib-3.8.4-cp311-cp311-win_amd64.whl", hash = "sha256:8080d5081a86e690d7688ffa542532e87f224c38a6ed71f8fbed34dd1d9fedae"}, + {file = "matplotlib-3.8.4-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:6485ac1f2e84676cff22e693eaa4fbed50ef5dc37173ce1f023daef4687df616"}, + {file = "matplotlib-3.8.4-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:c89ee9314ef48c72fe92ce55c4e95f2f39d70208f9f1d9db4e64079420d8d732"}, + {file = "matplotlib-3.8.4-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:50bac6e4d77e4262c4340d7a985c30912054745ec99756ce213bfbc3cb3808eb"}, + {file = "matplotlib-3.8.4-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f51c4c869d4b60d769f7b4406eec39596648d9d70246428745a681c327a8ad30"}, + {file = "matplotlib-3.8.4-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:b12ba985837e4899b762b81f5b2845bd1a28f4fdd1a126d9ace64e9c4eb2fb25"}, + {file = "matplotlib-3.8.4-cp312-cp312-win_amd64.whl", hash = "sha256:7a6769f58ce51791b4cb8b4d7642489df347697cd3e23d88266aaaee93b41d9a"}, + {file = "matplotlib-3.8.4-cp39-cp39-macosx_10_12_x86_64.whl", hash = "sha256:843cbde2f0946dadd8c5c11c6d91847abd18ec76859dc319362a0964493f0ba6"}, + {file = "matplotlib-3.8.4-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:1c13f041a7178f9780fb61cc3a2b10423d5e125480e4be51beaf62b172413b67"}, + {file = "matplotlib-3.8.4-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fb44f53af0a62dc80bba4443d9b27f2fde6acfdac281d95bc872dc148a6509cc"}, + {file = "matplotlib-3.8.4-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:606e3b90897554c989b1e38a258c626d46c873523de432b1462f295db13de6f9"}, + {file = "matplotlib-3.8.4-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:9bb0189011785ea794ee827b68777db3ca3f93f3e339ea4d920315a0e5a78d54"}, + {file = "matplotlib-3.8.4-cp39-cp39-win_amd64.whl", hash = "sha256:6209e5c9aaccc056e63b547a8152661324404dd92340a6e479b3a7f24b42a5d0"}, + {file = "matplotlib-3.8.4-pp39-pypy39_pp73-macosx_10_12_x86_64.whl", hash = "sha256:c7064120a59ce6f64103c9cefba8ffe6fba87f2c61d67c401186423c9a20fd35"}, + {file = "matplotlib-3.8.4-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a0e47eda4eb2614300fc7bb4657fced3e83d6334d03da2173b09e447418d499f"}, + {file = "matplotlib-3.8.4-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:493e9f6aa5819156b58fce42b296ea31969f2aab71c5b680b4ea7a3cb5c07d94"}, + {file = "matplotlib-3.8.4.tar.gz", hash = "sha256:8aac397d5e9ec158960e31c381c5ffc52ddd52bd9a47717e2a694038167dffea"}, ] [package.dependencies] @@ -2895,7 +2955,7 @@ contourpy = ">=1.0.1" cycler = ">=0.10" fonttools = ">=4.22.0" kiwisolver = ">=1.3.1" -numpy = ">=1.21,<2" +numpy = ">=1.21" packaging = ">=20.0" pillow = ">=8" pyparsing = ">=2.3.1" @@ -2903,13 +2963,13 @@ python-dateutil = ">=2.7" [[package]] name = "matplotlib-inline" -version = "0.1.6" +version = "0.1.7" description = "Inline Matplotlib backend for Jupyter" optional = false -python-versions = ">=3.5" +python-versions = ">=3.8" files = [ - {file = "matplotlib-inline-0.1.6.tar.gz", hash = "sha256:f887e5f10ba98e8d2b150ddcf4702c1e5f8b3a20005eb0f74bfdbd360ee6f304"}, - {file = "matplotlib_inline-0.1.6-py3-none-any.whl", hash = "sha256:f1f41aab5328aa5aaea9b16d083b128102f8712542f819fe7e6a420ff581b311"}, + {file = "matplotlib_inline-0.1.7-py3-none-any.whl", hash = "sha256:df192d39a4ff8f21b1895d72e6a13f5fcc5099f00fa84384e0ea28c2cc0653ca"}, + {file = "matplotlib_inline-0.1.7.tar.gz", hash = "sha256:8423b23ec666be3d16e16b60bdd8ac4e86e840ebd1dd11a30b9f117f2fa0ab90"}, ] [package.dependencies] @@ -2969,13 +3029,13 @@ files = [ [[package]] name = "mlflow" -version = "2.11.1" -description = "MLflow: A Platform for ML Development and Productionization" +version = "2.12.1" +description = "MLflow is an open source platform for the complete machine learning lifecycle" optional = false python-versions = ">=3.8" files = [ - {file = "mlflow-2.11.1-py3-none-any.whl", hash = "sha256:4d62a0bd0da9f4b16256e686b06905a6f8533eb587cd8e2dc7ee8324cf94ab35"}, - {file = "mlflow-2.11.1.tar.gz", hash = "sha256:a2ec29ee862f19955208fb8e05e51e5e9d2edc7175e37cf136d943b981a824e9"}, + {file = "mlflow-2.12.1-py3-none-any.whl", hash = "sha256:4c8f631df7ceea75c53464c976f3fef5bd0f80fc4ba5e871f3416d8c139a0312"}, + {file = "mlflow-2.12.1.tar.gz", hash = "sha256:aa92aebb2379a9c5484cbe901cdf779d5408ac96a641e4b1f8a2d1ff974db7c9"}, ] [package.dependencies] @@ -2996,9 +3056,9 @@ Jinja2 = [ markdown = ">=3.3,<4" matplotlib = "<4" numpy = "<2" -packaging = "<24" +packaging = "<25" pandas = "<3" -protobuf = ">=3.12.0,<5" +protobuf = ">=3.12.0,<6" pyarrow = ">=4.0.0,<16" pytz = "<2025" pyyaml = ">=5.1,<7" @@ -3021,13 +3081,13 @@ xethub = ["mlflow-xethub"] [[package]] name = "mlflow-skinny" -version = "2.11.1" -description = "MLflow: A Platform for ML Development and Productionization" +version = "2.12.1" +description = "MLflow is an open source platform for the complete machine learning lifecycle" optional = false python-versions = ">=3.8" files = [ - {file = "mlflow-skinny-2.11.1.tar.gz", hash = "sha256:dcb45fe3ad24326a6e939c83782708687fc6ca1273af9be2120449bbb7e63b56"}, - {file = "mlflow_skinny-2.11.1-py3-none-any.whl", hash = "sha256:ab210fd2b6a661e77fdcb1cfb163c55e632dde6b1d3031bdf5423a2edcba2ebf"}, + {file = "mlflow_skinny-2.12.1-py3-none-any.whl", hash = "sha256:51539de93a7f8b74b2d6b4307f204235469f6fadd19078599abd12a4bcf6b5b0"}, + {file = "mlflow_skinny-2.12.1.tar.gz", hash = "sha256:a5c3bb2f111867db988d4cdd782b6224fca4fc3d86706e9e587c379973ce7353"}, ] [package.dependencies] @@ -3036,8 +3096,8 @@ cloudpickle = "<4" entrypoints = "<1" gitpython = ">=3.1.9,<4" importlib-metadata = ">=3.7.0,<4.7.0 || >4.7.0,<8" -packaging = "<24" -protobuf = ">=3.12.0,<5" +packaging = "<25" +protobuf = ">=3.12.0,<6" pytz = "<2025" pyyaml = ">=5.1,<7" requests = ">=2.17.3,<3" @@ -3071,13 +3131,13 @@ tests = ["pytest (>=4.6)"] [[package]] name = "msal" -version = "1.27.0" +version = "1.28.0" description = "The Microsoft Authentication Library (MSAL) for Python library enables your app to access the Microsoft Cloud by supporting authentication of users with Microsoft Azure Active Directory accounts (AAD) and Microsoft Accounts (MSA) using industry standard OAuth2 and OpenID Connect." optional = false -python-versions = ">=2.7" +python-versions = ">=3.7" files = [ - {file = "msal-1.27.0-py2.py3-none-any.whl", hash = "sha256:572d07149b83e7343a85a3bcef8e581167b4ac76befcbbb6eef0c0e19643cdc0"}, - {file = "msal-1.27.0.tar.gz", hash = "sha256:3109503c038ba6b307152b0e8d34f98113f2e7a78986e28d0baf5b5303afda52"}, + {file = "msal-1.28.0-py3-none-any.whl", hash = "sha256:3064f80221a21cd535ad8c3fafbb3a3582cd9c7e9af0bb789ae14f726a0ca99b"}, + {file = "msal-1.28.0.tar.gz", hash = "sha256:80bbabe34567cb734efd2ec1869b2d98195c927455369d8077b3c542088c5c9d"}, ] [package.dependencies] @@ -3229,38 +3289,38 @@ files = [ [[package]] name = "mypy" -version = "1.9.0" +version = "1.10.0" description = "Optional static typing for Python" optional = false python-versions = ">=3.8" files = [ - {file = "mypy-1.9.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:f8a67616990062232ee4c3952f41c779afac41405806042a8126fe96e098419f"}, - {file = "mypy-1.9.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:d357423fa57a489e8c47b7c85dfb96698caba13d66e086b412298a1a0ea3b0ed"}, - {file = "mypy-1.9.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:49c87c15aed320de9b438ae7b00c1ac91cd393c1b854c2ce538e2a72d55df150"}, - {file = "mypy-1.9.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:48533cdd345c3c2e5ef48ba3b0d3880b257b423e7995dada04248725c6f77374"}, - {file = "mypy-1.9.0-cp310-cp310-win_amd64.whl", hash = "sha256:4d3dbd346cfec7cb98e6cbb6e0f3c23618af826316188d587d1c1bc34f0ede03"}, - {file = "mypy-1.9.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:653265f9a2784db65bfca694d1edd23093ce49740b2244cde583aeb134c008f3"}, - {file = "mypy-1.9.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:3a3c007ff3ee90f69cf0a15cbcdf0995749569b86b6d2f327af01fd1b8aee9dc"}, - {file = "mypy-1.9.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2418488264eb41f69cc64a69a745fad4a8f86649af4b1041a4c64ee61fc61129"}, - {file = "mypy-1.9.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:68edad3dc7d70f2f17ae4c6c1b9471a56138ca22722487eebacfd1eb5321d612"}, - {file = "mypy-1.9.0-cp311-cp311-win_amd64.whl", hash = "sha256:85ca5fcc24f0b4aeedc1d02f93707bccc04733f21d41c88334c5482219b1ccb3"}, - {file = "mypy-1.9.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:aceb1db093b04db5cd390821464504111b8ec3e351eb85afd1433490163d60cd"}, - {file = "mypy-1.9.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:0235391f1c6f6ce487b23b9dbd1327b4ec33bb93934aa986efe8a9563d9349e6"}, - {file = "mypy-1.9.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d4d5ddc13421ba3e2e082a6c2d74c2ddb3979c39b582dacd53dd5d9431237185"}, - {file = "mypy-1.9.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:190da1ee69b427d7efa8aa0d5e5ccd67a4fb04038c380237a0d96829cb157913"}, - {file = "mypy-1.9.0-cp312-cp312-win_amd64.whl", hash = "sha256:fe28657de3bfec596bbeef01cb219833ad9d38dd5393fc649f4b366840baefe6"}, - {file = "mypy-1.9.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:e54396d70be04b34f31d2edf3362c1edd023246c82f1730bbf8768c28db5361b"}, - {file = "mypy-1.9.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:5e6061f44f2313b94f920e91b204ec600982961e07a17e0f6cd83371cb23f5c2"}, - {file = "mypy-1.9.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:81a10926e5473c5fc3da8abb04119a1f5811a236dc3a38d92015cb1e6ba4cb9e"}, - {file = "mypy-1.9.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:b685154e22e4e9199fc95f298661deea28aaede5ae16ccc8cbb1045e716b3e04"}, - {file = "mypy-1.9.0-cp38-cp38-win_amd64.whl", hash = "sha256:5d741d3fc7c4da608764073089e5f58ef6352bedc223ff58f2f038c2c4698a89"}, - {file = "mypy-1.9.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:587ce887f75dd9700252a3abbc9c97bbe165a4a630597845c61279cf32dfbf02"}, - {file = "mypy-1.9.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:f88566144752999351725ac623471661c9d1cd8caa0134ff98cceeea181789f4"}, - {file = "mypy-1.9.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:61758fabd58ce4b0720ae1e2fea5cfd4431591d6d590b197775329264f86311d"}, - {file = "mypy-1.9.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:e49499be624dead83927e70c756970a0bc8240e9f769389cdf5714b0784ca6bf"}, - {file = "mypy-1.9.0-cp39-cp39-win_amd64.whl", hash = "sha256:571741dc4194b4f82d344b15e8837e8c5fcc462d66d076748142327626a1b6e9"}, - {file = "mypy-1.9.0-py3-none-any.whl", hash = "sha256:a260627a570559181a9ea5de61ac6297aa5af202f06fd7ab093ce74e7181e43e"}, - {file = "mypy-1.9.0.tar.gz", hash = "sha256:3cc5da0127e6a478cddd906068496a97a7618a21ce9b54bde5bf7e539c7af974"}, + {file = "mypy-1.10.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:da1cbf08fb3b851ab3b9523a884c232774008267b1f83371ace57f412fe308c2"}, + {file = "mypy-1.10.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:12b6bfc1b1a66095ab413160a6e520e1dc076a28f3e22f7fb25ba3b000b4ef99"}, + {file = "mypy-1.10.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9e36fb078cce9904c7989b9693e41cb9711e0600139ce3970c6ef814b6ebc2b2"}, + {file = "mypy-1.10.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:2b0695d605ddcd3eb2f736cd8b4e388288c21e7de85001e9f85df9187f2b50f9"}, + {file = "mypy-1.10.0-cp310-cp310-win_amd64.whl", hash = "sha256:cd777b780312ddb135bceb9bc8722a73ec95e042f911cc279e2ec3c667076051"}, + {file = "mypy-1.10.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:3be66771aa5c97602f382230165b856c231d1277c511c9a8dd058be4784472e1"}, + {file = "mypy-1.10.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:8b2cbaca148d0754a54d44121b5825ae71868c7592a53b7292eeb0f3fdae95ee"}, + {file = "mypy-1.10.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1ec404a7cbe9fc0e92cb0e67f55ce0c025014e26d33e54d9e506a0f2d07fe5de"}, + {file = "mypy-1.10.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:e22e1527dc3d4aa94311d246b59e47f6455b8729f4968765ac1eacf9a4760bc7"}, + {file = "mypy-1.10.0-cp311-cp311-win_amd64.whl", hash = "sha256:a87dbfa85971e8d59c9cc1fcf534efe664d8949e4c0b6b44e8ca548e746a8d53"}, + {file = "mypy-1.10.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:a781f6ad4bab20eef8b65174a57e5203f4be627b46291f4589879bf4e257b97b"}, + {file = "mypy-1.10.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:b808e12113505b97d9023b0b5e0c0705a90571c6feefc6f215c1df9381256e30"}, + {file = "mypy-1.10.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8f55583b12156c399dce2df7d16f8a5095291354f1e839c252ec6c0611e86e2e"}, + {file = "mypy-1.10.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:4cf18f9d0efa1b16478c4c129eabec36148032575391095f73cae2e722fcf9d5"}, + {file = "mypy-1.10.0-cp312-cp312-win_amd64.whl", hash = "sha256:bc6ac273b23c6b82da3bb25f4136c4fd42665f17f2cd850771cb600bdd2ebeda"}, + {file = "mypy-1.10.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:9fd50226364cd2737351c79807775136b0abe084433b55b2e29181a4c3c878c0"}, + {file = "mypy-1.10.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:f90cff89eea89273727d8783fef5d4a934be2fdca11b47def50cf5d311aff727"}, + {file = "mypy-1.10.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fcfc70599efde5c67862a07a1aaf50e55bce629ace26bb19dc17cece5dd31ca4"}, + {file = "mypy-1.10.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:075cbf81f3e134eadaf247de187bd604748171d6b79736fa9b6c9685b4083061"}, + {file = "mypy-1.10.0-cp38-cp38-win_amd64.whl", hash = "sha256:3f298531bca95ff615b6e9f2fc0333aae27fa48052903a0ac90215021cdcfa4f"}, + {file = "mypy-1.10.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:fa7ef5244615a2523b56c034becde4e9e3f9b034854c93639adb667ec9ec2976"}, + {file = "mypy-1.10.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:3236a4c8f535a0631f85f5fcdffba71c7feeef76a6002fcba7c1a8e57c8be1ec"}, + {file = "mypy-1.10.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4a2b5cdbb5dd35aa08ea9114436e0d79aceb2f38e32c21684dcf8e24e1e92821"}, + {file = "mypy-1.10.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:92f93b21c0fe73dc00abf91022234c79d793318b8a96faac147cd579c1671746"}, + {file = "mypy-1.10.0-cp39-cp39-win_amd64.whl", hash = "sha256:28d0e038361b45f099cc086d9dd99c15ff14d0188f44ac883010e172ce86c38a"}, + {file = "mypy-1.10.0-py3-none-any.whl", hash = "sha256:f8c083976eb530019175aabadb60921e73b4f45736760826aa1689dda8208aee"}, + {file = "mypy-1.10.0.tar.gz", hash = "sha256:3d087fcbec056c4ee34974da493a826ce316947485cef3901f511848e687c131"}, ] [package.dependencies] @@ -3287,13 +3347,13 @@ files = [ [[package]] name = "myst-nb" -version = "1.0.0" +version = "1.1.0" description = "A Jupyter Notebook Sphinx reader built on top of the MyST markdown parser." optional = false python-versions = ">=3.9" files = [ - {file = "myst_nb-1.0.0-py3-none-any.whl", hash = "sha256:ee8febc6dd7d9e32bede0c66a9b962b2e2fdab697428ee9fbfd4919d82380911"}, - {file = "myst_nb-1.0.0.tar.gz", hash = "sha256:9077e42a1c6b441ea55078506f83555dda5d6c816ef4930841d71d239e3e0c5e"}, + {file = "myst_nb-1.1.0-py3-none-any.whl", hash = "sha256:0ac29b2a346f9a1257edbfb5d6c47d528728a37e6b9438903c2821f69fda9235"}, + {file = "myst_nb-1.1.0.tar.gz", hash = "sha256:9278840e844f5d780b5acc5400cbf63d97caaccf8eb442a55ebd9a03e2522d5e"}, ] [package.dependencies] @@ -3311,7 +3371,7 @@ typing-extensions = "*" [package.extras] code-style = ["pre-commit"] rtd = ["alabaster", "altair", "bokeh", "coconut (>=1.4.3,<3.1.0)", "ipykernel (>=5.5,<7.0)", "ipywidgets", "jupytext (>=1.11.2,<1.16.0)", "matplotlib", "numpy", "pandas", "plotly", "sphinx-book-theme (>=0.3)", "sphinx-copybutton", "sphinx-design (>=0.4.0,<0.5.0)", "sphinxcontrib-bibtex", "sympy"] -testing = ["beautifulsoup4", "coverage (>=6.4,<8.0)", "ipykernel (>=5.5,<7.0)", "ipython (!=8.1.0,<8.17)", "ipywidgets (>=8)", "jupytext (>=1.11.2,<1.16.0)", "matplotlib (==3.7.*)", "nbdime", "numpy", "pandas", "pytest (>=7.1,<8.0)", "pytest-cov (>=3,<5)", "pytest-param-files (>=0.3.3,<0.4.0)", "pytest-regressions", "sympy (>=1.10.1)"] +testing = ["beautifulsoup4", "coverage (>=6.4,<8.0)", "ipykernel (>=5.5,<7.0)", "ipython (!=8.1.0,<8.17)", "ipywidgets (>=8)", "jupytext (>=1.11.2,<1.16.0)", "matplotlib (==3.7.*)", "nbdime", "numpy", "pandas (==1.5.*)", "pyarrow", "pytest (>=7.1,<8.0)", "pytest-cov (>=3,<5)", "pytest-param-files (>=0.3.3,<0.4.0)", "pytest-regressions", "sympy (>=1.10.1)"] [[package]] name = "myst-parser" @@ -3363,13 +3423,13 @@ test = ["flaky", "ipykernel (>=6.19.3)", "ipython", "ipywidgets", "nbconvert (>= [[package]] name = "nbconvert" -version = "7.16.2" +version = "7.16.3" description = "Converting Jupyter Notebooks (.ipynb files) to other formats. Output formats include asciidoc, html, latex, markdown, pdf, py, rst, script. nbconvert can be used both as a Python library (`import nbconvert`) or as a command line tool (invoked as `jupyter nbconvert ...`)." optional = false python-versions = ">=3.8" files = [ - {file = "nbconvert-7.16.2-py3-none-any.whl", hash = "sha256:0c01c23981a8de0220255706822c40b751438e32467d6a686e26be08ba784382"}, - {file = "nbconvert-7.16.2.tar.gz", hash = "sha256:8310edd41e1c43947e4ecf16614c61469ebc024898eb808cce0999860fc9fb16"}, + {file = "nbconvert-7.16.3-py3-none-any.whl", hash = "sha256:ddeff14beeeedf3dd0bc506623e41e4507e551736de59df69a91f86700292b3b"}, + {file = "nbconvert-7.16.3.tar.gz", hash = "sha256:a6733b78ce3d47c3f85e504998495b07e6ea9cf9bf6ec1c98dda63ec6ad19142"}, ] [package.dependencies] @@ -3395,24 +3455,24 @@ docs = ["ipykernel", "ipython", "myst-parser", "nbsphinx (>=0.2.12)", "pydata-sp qtpdf = ["nbconvert[qtpng]"] qtpng = ["pyqtwebengine (>=5.15)"] serve = ["tornado (>=6.1)"] -test = ["flaky", "ipykernel", "ipywidgets (>=7.5)", "pytest"] +test = ["flaky", "ipykernel", "ipywidgets (>=7.5)", "pytest (>=7)"] webpdf = ["playwright"] [[package]] name = "nbformat" -version = "5.10.3" +version = "5.10.4" description = "The Jupyter Notebook format" optional = false python-versions = ">=3.8" files = [ - {file = "nbformat-5.10.3-py3-none-any.whl", hash = "sha256:d9476ca28676799af85385f409b49d95e199951477a159a576ef2a675151e5e8"}, - {file = "nbformat-5.10.3.tar.gz", hash = "sha256:60ed5e910ef7c6264b87d644f276b1b49e24011930deef54605188ddeb211685"}, + {file = "nbformat-5.10.4-py3-none-any.whl", hash = "sha256:3b48d6c8fbca4b299bf3982ea7db1af21580e4fec269ad087b9e81588891200b"}, + {file = "nbformat-5.10.4.tar.gz", hash = "sha256:322168b14f937a5d11362988ecac2a4952d3d8e3a2cbeb2319584631226d5b3a"}, ] [package.dependencies] -fastjsonschema = "*" +fastjsonschema = ">=2.15" jsonschema = ">=2.6" -jupyter-core = "*" +jupyter-core = ">=4.12,<5.0.dev0 || >=5.1.dev0" traitlets = ">=5.1" [package.extras] @@ -3432,20 +3492,20 @@ files = [ [[package]] name = "networkx" -version = "3.2.1" +version = "3.3" description = "Python package for creating and manipulating graphs and networks" optional = false -python-versions = ">=3.9" +python-versions = ">=3.10" files = [ - {file = "networkx-3.2.1-py3-none-any.whl", hash = "sha256:f18c69adc97877c42332c170849c96cefa91881c99a7cb3e95b7c659ebdc1ec2"}, - {file = "networkx-3.2.1.tar.gz", hash = "sha256:9f1bb5cf3409bf324e0a722c20bdb4c20ee39bf1c30ce8ae499c8502b0b5e0c6"}, + {file = "networkx-3.3-py3-none-any.whl", hash = "sha256:28575580c6ebdaf4505b22c6256a2b9de86b316dc63ba9e93abde3d78dfdbcf2"}, + {file = "networkx-3.3.tar.gz", hash = "sha256:0c127d8b2f4865f59ae9cb8aafcd60b5c70f3241ebd66f7defad7c4ab90126c9"}, ] [package.extras] -default = ["matplotlib (>=3.5)", "numpy (>=1.22)", "pandas (>=1.4)", "scipy (>=1.9,!=1.11.0,!=1.11.1)"] -developer = ["changelist (==0.4)", "mypy (>=1.1)", "pre-commit (>=3.2)", "rtoml"] -doc = ["nb2plots (>=0.7)", "nbconvert (<7.9)", "numpydoc (>=1.6)", "pillow (>=9.4)", "pydata-sphinx-theme (>=0.14)", "sphinx (>=7)", "sphinx-gallery (>=0.14)", "texext (>=0.6.7)"] -extra = ["lxml (>=4.6)", "pydot (>=1.4.2)", "pygraphviz (>=1.11)", "sympy (>=1.10)"] +default = ["matplotlib (>=3.6)", "numpy (>=1.23)", "pandas (>=1.4)", "scipy (>=1.9,!=1.11.0,!=1.11.1)"] +developer = ["changelist (==0.5)", "mypy (>=1.1)", "pre-commit (>=3.2)", "rtoml"] +doc = ["myst-nb (>=1.0)", "numpydoc (>=1.7)", "pillow (>=9.4)", "pydata-sphinx-theme (>=0.14)", "sphinx (>=7)", "sphinx-gallery (>=0.14)", "texext (>=0.6.7)"] +extra = ["lxml (>=4.6)", "pydot (>=2.0)", "pygraphviz (>=1.12)", "sympy (>=1.10)"] test = ["pytest (>=7.2)", "pytest-cov (>=4.0)"] [[package]] @@ -3464,13 +3524,13 @@ setuptools = "*" [[package]] name = "notebook" -version = "7.1.2" +version = "7.1.3" description = "Jupyter Notebook - A web-based notebook environment for interactive computing" optional = false python-versions = ">=3.8" files = [ - {file = "notebook-7.1.2-py3-none-any.whl", hash = "sha256:fc6c24b9aef18d0cd57157c9c47e95833b9b0bdc599652639acf0bdb61dc7d5f"}, - {file = "notebook-7.1.2.tar.gz", hash = "sha256:efc2c80043909e0faa17fce9e9b37c059c03af0ec99a4d4db84cb21d9d2e936a"}, + {file = "notebook-7.1.3-py3-none-any.whl", hash = "sha256:919b911e59f41f6e3857ce93c9d93535ba66bb090059712770e5968c07e1004d"}, + {file = "notebook-7.1.3.tar.gz", hash = "sha256:41fcebff44cf7bb9377180808bcbae066629b55d8c7722f1ebbe75ca44f9cfc1"}, ] [package.dependencies] @@ -3549,18 +3609,17 @@ files = [ [[package]] name = "numpydoc" -version = "1.6.0" +version = "1.7.0" description = "Sphinx extension to support docstrings in Numpy format" optional = false python-versions = ">=3.8" files = [ - {file = "numpydoc-1.6.0-py3-none-any.whl", hash = "sha256:b6ddaa654a52bdf967763c1e773be41f1c3ae3da39ee0de973f2680048acafaa"}, - {file = "numpydoc-1.6.0.tar.gz", hash = "sha256:ae7a5380f0a06373c3afe16ccd15bd79bc6b07f2704cbc6f1e7ecc94b4f5fc0d"}, + {file = "numpydoc-1.7.0-py3-none-any.whl", hash = "sha256:5a56419d931310d79a06cfc2a126d1558700feeb9b4f3d8dcae1a8134be829c9"}, + {file = "numpydoc-1.7.0.tar.gz", hash = "sha256:866e5ae5b6509dcf873fc6381120f5c31acf13b135636c1a81d68c166a95f921"}, ] [package.dependencies] -Jinja2 = ">=2.10" -sphinx = ">=5" +sphinx = ">=6" tabulate = ">=0.8.10" tomli = {version = ">=1.1.0", markers = "python_version < \"3.11\""} @@ -3771,13 +3830,13 @@ files = [ [[package]] name = "packaging" -version = "23.2" +version = "24.0" description = "Core utilities for Python packages" optional = false python-versions = ">=3.7" files = [ - {file = "packaging-23.2-py3-none-any.whl", hash = "sha256:8c491190033a9af7e1d931d0b5dacc2ef47509b34dd0de67ed209b5203fc88c7"}, - {file = "packaging-23.2.tar.gz", hash = "sha256:048fb0e9405036518eaaf48a55953c750c11e1a1b68e0dd1a9d62ed0c092cfc5"}, + {file = "packaging-24.0-py3-none-any.whl", hash = "sha256:2ddfb553fdf02fb784c234c7ba6ccc288296ceabec964ad2eae3777778130bc5"}, + {file = "packaging-24.0.tar.gz", hash = "sha256:eb82c5e3e56209074766e6885bb04b8c38a0c015d0a30036ebe7ece34c9989e9"}, ] [[package]] @@ -3837,18 +3896,18 @@ files = [ [[package]] name = "parso" -version = "0.8.3" +version = "0.8.4" description = "A Python Parser" optional = false python-versions = ">=3.6" files = [ - {file = "parso-0.8.3-py2.py3-none-any.whl", hash = "sha256:c001d4636cd3aecdaf33cbb40aebb59b094be2a74c556778ef5576c175e19e75"}, - {file = "parso-0.8.3.tar.gz", hash = "sha256:8c07be290bb59f03588915921e29e8a50002acaf2cdc5fa0e0114f91709fafa0"}, + {file = "parso-0.8.4-py2.py3-none-any.whl", hash = "sha256:a418670a20291dacd2dddc80c377c5c3791378ee1e8d12bffc35420643d43f18"}, + {file = "parso-0.8.4.tar.gz", hash = "sha256:eb3a7b58240fb99099a345571deecc0f9540ea5f4dd2fe14c2a99d6b281ab92d"}, ] [package.extras] -qa = ["flake8 (==3.8.3)", "mypy (==0.782)"] -testing = ["docopt", "pytest (<6.0.0)"] +qa = ["flake8 (==5.0.4)", "mypy (==0.971)", "types-setuptools (==67.2.0.1)"] +testing = ["docopt", "pytest"] [[package]] name = "pathspec" @@ -3877,79 +3936,80 @@ ptyprocess = ">=0.5" [[package]] name = "pillow" -version = "10.2.0" +version = "10.3.0" description = "Python Imaging Library (Fork)" optional = false python-versions = ">=3.8" files = [ - {file = "pillow-10.2.0-cp310-cp310-macosx_10_10_x86_64.whl", hash = "sha256:7823bdd049099efa16e4246bdf15e5a13dbb18a51b68fa06d6c1d4d8b99a796e"}, - {file = "pillow-10.2.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:83b2021f2ade7d1ed556bc50a399127d7fb245e725aa0113ebd05cfe88aaf588"}, - {file = "pillow-10.2.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6fad5ff2f13d69b7e74ce5b4ecd12cc0ec530fcee76356cac6742785ff71c452"}, - {file = "pillow-10.2.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:da2b52b37dad6d9ec64e653637a096905b258d2fc2b984c41ae7d08b938a67e4"}, - {file = "pillow-10.2.0-cp310-cp310-manylinux_2_28_aarch64.whl", hash = "sha256:47c0995fc4e7f79b5cfcab1fc437ff2890b770440f7696a3ba065ee0fd496563"}, - {file = "pillow-10.2.0-cp310-cp310-manylinux_2_28_x86_64.whl", hash = "sha256:322bdf3c9b556e9ffb18f93462e5f749d3444ce081290352c6070d014c93feb2"}, - {file = "pillow-10.2.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:51f1a1bffc50e2e9492e87d8e09a17c5eea8409cda8d3f277eb6edc82813c17c"}, - {file = "pillow-10.2.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:69ffdd6120a4737710a9eee73e1d2e37db89b620f702754b8f6e62594471dee0"}, - {file = "pillow-10.2.0-cp310-cp310-win32.whl", hash = "sha256:c6dafac9e0f2b3c78df97e79af707cdc5ef8e88208d686a4847bab8266870023"}, - {file = "pillow-10.2.0-cp310-cp310-win_amd64.whl", hash = "sha256:aebb6044806f2e16ecc07b2a2637ee1ef67a11840a66752751714a0d924adf72"}, - {file = "pillow-10.2.0-cp310-cp310-win_arm64.whl", hash = "sha256:7049e301399273a0136ff39b84c3678e314f2158f50f517bc50285fb5ec847ad"}, - {file = "pillow-10.2.0-cp311-cp311-macosx_10_10_x86_64.whl", hash = "sha256:35bb52c37f256f662abdfa49d2dfa6ce5d93281d323a9af377a120e89a9eafb5"}, - {file = "pillow-10.2.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:9c23f307202661071d94b5e384e1e1dc7dfb972a28a2310e4ee16103e66ddb67"}, - {file = "pillow-10.2.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:773efe0603db30c281521a7c0214cad7836c03b8ccff897beae9b47c0b657d61"}, - {file = "pillow-10.2.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:11fa2e5984b949b0dd6d7a94d967743d87c577ff0b83392f17cb3990d0d2fd6e"}, - {file = "pillow-10.2.0-cp311-cp311-manylinux_2_28_aarch64.whl", hash = "sha256:716d30ed977be8b37d3ef185fecb9e5a1d62d110dfbdcd1e2a122ab46fddb03f"}, - {file = "pillow-10.2.0-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:a086c2af425c5f62a65e12fbf385f7c9fcb8f107d0849dba5839461a129cf311"}, - {file = "pillow-10.2.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:c8de2789052ed501dd829e9cae8d3dcce7acb4777ea4a479c14521c942d395b1"}, - {file = "pillow-10.2.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:609448742444d9290fd687940ac0b57fb35e6fd92bdb65386e08e99af60bf757"}, - {file = "pillow-10.2.0-cp311-cp311-win32.whl", hash = "sha256:823ef7a27cf86df6597fa0671066c1b596f69eba53efa3d1e1cb8b30f3533068"}, - {file = "pillow-10.2.0-cp311-cp311-win_amd64.whl", hash = "sha256:1da3b2703afd040cf65ec97efea81cfba59cdbed9c11d8efc5ab09df9509fc56"}, - {file = "pillow-10.2.0-cp311-cp311-win_arm64.whl", hash = "sha256:edca80cbfb2b68d7b56930b84a0e45ae1694aeba0541f798e908a49d66b837f1"}, - {file = "pillow-10.2.0-cp312-cp312-macosx_10_10_x86_64.whl", hash = "sha256:1b5e1b74d1bd1b78bc3477528919414874748dd363e6272efd5abf7654e68bef"}, - {file = "pillow-10.2.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:0eae2073305f451d8ecacb5474997c08569fb4eb4ac231ffa4ad7d342fdc25ac"}, - {file = "pillow-10.2.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b7c2286c23cd350b80d2fc9d424fc797575fb16f854b831d16fd47ceec078f2c"}, - {file = "pillow-10.2.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1e23412b5c41e58cec602f1135c57dfcf15482013ce6e5f093a86db69646a5aa"}, - {file = "pillow-10.2.0-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:52a50aa3fb3acb9cf7213573ef55d31d6eca37f5709c69e6858fe3bc04a5c2a2"}, - {file = "pillow-10.2.0-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:127cee571038f252a552760076407f9cff79761c3d436a12af6000cd182a9d04"}, - {file = "pillow-10.2.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:8d12251f02d69d8310b046e82572ed486685c38f02176bd08baf216746eb947f"}, - {file = "pillow-10.2.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:54f1852cd531aa981bc0965b7d609f5f6cc8ce8c41b1139f6ed6b3c54ab82bfb"}, - {file = "pillow-10.2.0-cp312-cp312-win32.whl", hash = "sha256:257d8788df5ca62c980314053197f4d46eefedf4e6175bc9412f14412ec4ea2f"}, - {file = "pillow-10.2.0-cp312-cp312-win_amd64.whl", hash = "sha256:154e939c5f0053a383de4fd3d3da48d9427a7e985f58af8e94d0b3c9fcfcf4f9"}, - {file = "pillow-10.2.0-cp312-cp312-win_arm64.whl", hash = "sha256:f379abd2f1e3dddb2b61bc67977a6b5a0a3f7485538bcc6f39ec76163891ee48"}, - {file = "pillow-10.2.0-cp38-cp38-macosx_10_10_x86_64.whl", hash = "sha256:8373c6c251f7ef8bda6675dd6d2b3a0fcc31edf1201266b5cf608b62a37407f9"}, - {file = "pillow-10.2.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:870ea1ada0899fd0b79643990809323b389d4d1d46c192f97342eeb6ee0b8483"}, - {file = "pillow-10.2.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b4b6b1e20608493548b1f32bce8cca185bf0480983890403d3b8753e44077129"}, - {file = "pillow-10.2.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3031709084b6e7852d00479fd1d310b07d0ba82765f973b543c8af5061cf990e"}, - {file = "pillow-10.2.0-cp38-cp38-manylinux_2_28_aarch64.whl", hash = "sha256:3ff074fc97dd4e80543a3e91f69d58889baf2002b6be64347ea8cf5533188213"}, - {file = "pillow-10.2.0-cp38-cp38-manylinux_2_28_x86_64.whl", hash = "sha256:cb4c38abeef13c61d6916f264d4845fab99d7b711be96c326b84df9e3e0ff62d"}, - {file = "pillow-10.2.0-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:b1b3020d90c2d8e1dae29cf3ce54f8094f7938460fb5ce8bc5c01450b01fbaf6"}, - {file = "pillow-10.2.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:170aeb00224ab3dc54230c797f8404507240dd868cf52066f66a41b33169bdbe"}, - {file = "pillow-10.2.0-cp38-cp38-win32.whl", hash = "sha256:c4225f5220f46b2fde568c74fca27ae9771536c2e29d7c04f4fb62c83275ac4e"}, - {file = "pillow-10.2.0-cp38-cp38-win_amd64.whl", hash = "sha256:0689b5a8c5288bc0504d9fcee48f61a6a586b9b98514d7d29b840143d6734f39"}, - {file = "pillow-10.2.0-cp39-cp39-macosx_10_10_x86_64.whl", hash = "sha256:b792a349405fbc0163190fde0dc7b3fef3c9268292586cf5645598b48e63dc67"}, - {file = "pillow-10.2.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:c570f24be1e468e3f0ce7ef56a89a60f0e05b30a3669a459e419c6eac2c35364"}, - {file = "pillow-10.2.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d8ecd059fdaf60c1963c58ceb8997b32e9dc1b911f5da5307aab614f1ce5c2fb"}, - {file = "pillow-10.2.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c365fd1703040de1ec284b176d6af5abe21b427cb3a5ff68e0759e1e313a5e7e"}, - {file = "pillow-10.2.0-cp39-cp39-manylinux_2_28_aarch64.whl", hash = "sha256:70c61d4c475835a19b3a5aa42492409878bbca7438554a1f89d20d58a7c75c01"}, - {file = "pillow-10.2.0-cp39-cp39-manylinux_2_28_x86_64.whl", hash = "sha256:b6f491cdf80ae540738859d9766783e3b3c8e5bd37f5dfa0b76abdecc5081f13"}, - {file = "pillow-10.2.0-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:9d189550615b4948f45252d7f005e53c2040cea1af5b60d6f79491a6e147eef7"}, - {file = "pillow-10.2.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:49d9ba1ed0ef3e061088cd1e7538a0759aab559e2e0a80a36f9fd9d8c0c21591"}, - {file = "pillow-10.2.0-cp39-cp39-win32.whl", hash = "sha256:babf5acfede515f176833ed6028754cbcd0d206f7f614ea3447d67c33be12516"}, - {file = "pillow-10.2.0-cp39-cp39-win_amd64.whl", hash = "sha256:0304004f8067386b477d20a518b50f3fa658a28d44e4116970abfcd94fac34a8"}, - {file = "pillow-10.2.0-cp39-cp39-win_arm64.whl", hash = "sha256:0fb3e7fc88a14eacd303e90481ad983fd5b69c761e9e6ef94c983f91025da869"}, - {file = "pillow-10.2.0-pp310-pypy310_pp73-macosx_10_10_x86_64.whl", hash = "sha256:322209c642aabdd6207517e9739c704dc9f9db943015535783239022002f054a"}, - {file = "pillow-10.2.0-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3eedd52442c0a5ff4f887fab0c1c0bb164d8635b32c894bc1faf4c618dd89df2"}, - {file = "pillow-10.2.0-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cb28c753fd5eb3dd859b4ee95de66cc62af91bcff5db5f2571d32a520baf1f04"}, - {file = "pillow-10.2.0-pp310-pypy310_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:33870dc4653c5017bf4c8873e5488d8f8d5f8935e2f1fb9a2208c47cdd66efd2"}, - {file = "pillow-10.2.0-pp310-pypy310_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:3c31822339516fb3c82d03f30e22b1d038da87ef27b6a78c9549888f8ceda39a"}, - {file = "pillow-10.2.0-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:a2b56ba36e05f973d450582fb015594aaa78834fefe8dfb8fcd79b93e64ba4c6"}, - {file = "pillow-10.2.0-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:d8e6aeb9201e655354b3ad049cb77d19813ad4ece0df1249d3c793de3774f8c7"}, - {file = "pillow-10.2.0-pp39-pypy39_pp73-macosx_10_10_x86_64.whl", hash = "sha256:2247178effb34a77c11c0e8ac355c7a741ceca0a732b27bf11e747bbc950722f"}, - {file = "pillow-10.2.0-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:15587643b9e5eb26c48e49a7b33659790d28f190fc514a322d55da2fb5c2950e"}, - {file = "pillow-10.2.0-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:753cd8f2086b2b80180d9b3010dd4ed147efc167c90d3bf593fe2af21265e5a5"}, - {file = "pillow-10.2.0-pp39-pypy39_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:7c8f97e8e7a9009bcacbe3766a36175056c12f9a44e6e6f2d5caad06dcfbf03b"}, - {file = "pillow-10.2.0-pp39-pypy39_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:d1b35bcd6c5543b9cb547dee3150c93008f8dd0f1fef78fc0cd2b141c5baf58a"}, - {file = "pillow-10.2.0-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:fe4c15f6c9285dc54ce6553a3ce908ed37c8f3825b5a51a15c91442bb955b868"}, - {file = "pillow-10.2.0.tar.gz", hash = "sha256:e87f0b2c78157e12d7686b27d63c070fd65d994e8ddae6f328e0dcf4a0cd007e"}, + {file = "pillow-10.3.0-cp310-cp310-macosx_10_10_x86_64.whl", hash = "sha256:90b9e29824800e90c84e4022dd5cc16eb2d9605ee13f05d47641eb183cd73d45"}, + {file = "pillow-10.3.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:a2c405445c79c3f5a124573a051062300936b0281fee57637e706453e452746c"}, + {file = "pillow-10.3.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:78618cdbccaa74d3f88d0ad6cb8ac3007f1a6fa5c6f19af64b55ca170bfa1edf"}, + {file = "pillow-10.3.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:261ddb7ca91fcf71757979534fb4c128448b5b4c55cb6152d280312062f69599"}, + {file = "pillow-10.3.0-cp310-cp310-manylinux_2_28_aarch64.whl", hash = "sha256:ce49c67f4ea0609933d01c0731b34b8695a7a748d6c8d186f95e7d085d2fe475"}, + {file = "pillow-10.3.0-cp310-cp310-manylinux_2_28_x86_64.whl", hash = "sha256:b14f16f94cbc61215115b9b1236f9c18403c15dd3c52cf629072afa9d54c1cbf"}, + {file = "pillow-10.3.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:d33891be6df59d93df4d846640f0e46f1a807339f09e79a8040bc887bdcd7ed3"}, + {file = "pillow-10.3.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:b50811d664d392f02f7761621303eba9d1b056fb1868c8cdf4231279645c25f5"}, + {file = "pillow-10.3.0-cp310-cp310-win32.whl", hash = "sha256:ca2870d5d10d8726a27396d3ca4cf7976cec0f3cb706debe88e3a5bd4610f7d2"}, + {file = "pillow-10.3.0-cp310-cp310-win_amd64.whl", hash = "sha256:f0d0591a0aeaefdaf9a5e545e7485f89910c977087e7de2b6c388aec32011e9f"}, + {file = "pillow-10.3.0-cp310-cp310-win_arm64.whl", hash = "sha256:ccce24b7ad89adb5a1e34a6ba96ac2530046763912806ad4c247356a8f33a67b"}, + {file = "pillow-10.3.0-cp311-cp311-macosx_10_10_x86_64.whl", hash = "sha256:5f77cf66e96ae734717d341c145c5949c63180842a545c47a0ce7ae52ca83795"}, + {file = "pillow-10.3.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:e4b878386c4bf293578b48fc570b84ecfe477d3b77ba39a6e87150af77f40c57"}, + {file = "pillow-10.3.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fdcbb4068117dfd9ce0138d068ac512843c52295ed996ae6dd1faf537b6dbc27"}, + {file = "pillow-10.3.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9797a6c8fe16f25749b371c02e2ade0efb51155e767a971c61734b1bf6293994"}, + {file = "pillow-10.3.0-cp311-cp311-manylinux_2_28_aarch64.whl", hash = "sha256:9e91179a242bbc99be65e139e30690e081fe6cb91a8e77faf4c409653de39451"}, + {file = "pillow-10.3.0-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:1b87bd9d81d179bd8ab871603bd80d8645729939f90b71e62914e816a76fc6bd"}, + {file = "pillow-10.3.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:81d09caa7b27ef4e61cb7d8fbf1714f5aec1c6b6c5270ee53504981e6e9121ad"}, + {file = "pillow-10.3.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:048ad577748b9fa4a99a0548c64f2cb8d672d5bf2e643a739ac8faff1164238c"}, + {file = "pillow-10.3.0-cp311-cp311-win32.whl", hash = "sha256:7161ec49ef0800947dc5570f86568a7bb36fa97dd09e9827dc02b718c5643f09"}, + {file = "pillow-10.3.0-cp311-cp311-win_amd64.whl", hash = "sha256:8eb0908e954d093b02a543dc963984d6e99ad2b5e36503d8a0aaf040505f747d"}, + {file = "pillow-10.3.0-cp311-cp311-win_arm64.whl", hash = "sha256:4e6f7d1c414191c1199f8996d3f2282b9ebea0945693fb67392c75a3a320941f"}, + {file = "pillow-10.3.0-cp312-cp312-macosx_10_10_x86_64.whl", hash = "sha256:e46f38133e5a060d46bd630faa4d9fa0202377495df1f068a8299fd78c84de84"}, + {file = "pillow-10.3.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:50b8eae8f7334ec826d6eeffaeeb00e36b5e24aa0b9df322c247539714c6df19"}, + {file = "pillow-10.3.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9d3bea1c75f8c53ee4d505c3e67d8c158ad4df0d83170605b50b64025917f338"}, + {file = "pillow-10.3.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:19aeb96d43902f0a783946a0a87dbdad5c84c936025b8419da0a0cd7724356b1"}, + {file = "pillow-10.3.0-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:74d28c17412d9caa1066f7a31df8403ec23d5268ba46cd0ad2c50fb82ae40462"}, + {file = "pillow-10.3.0-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:ff61bfd9253c3915e6d41c651d5f962da23eda633cf02262990094a18a55371a"}, + {file = "pillow-10.3.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:d886f5d353333b4771d21267c7ecc75b710f1a73d72d03ca06df49b09015a9ef"}, + {file = "pillow-10.3.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:4b5ec25d8b17217d635f8935dbc1b9aa5907962fae29dff220f2659487891cd3"}, + {file = "pillow-10.3.0-cp312-cp312-win32.whl", hash = "sha256:51243f1ed5161b9945011a7360e997729776f6e5d7005ba0c6879267d4c5139d"}, + {file = "pillow-10.3.0-cp312-cp312-win_amd64.whl", hash = "sha256:412444afb8c4c7a6cc11a47dade32982439925537e483be7c0ae0cf96c4f6a0b"}, + {file = "pillow-10.3.0-cp312-cp312-win_arm64.whl", hash = "sha256:798232c92e7665fe82ac085f9d8e8ca98826f8e27859d9a96b41d519ecd2e49a"}, + {file = "pillow-10.3.0-cp38-cp38-macosx_10_10_x86_64.whl", hash = "sha256:4eaa22f0d22b1a7e93ff0a596d57fdede2e550aecffb5a1ef1106aaece48e96b"}, + {file = "pillow-10.3.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:cd5e14fbf22a87321b24c88669aad3a51ec052eb145315b3da3b7e3cc105b9a2"}, + {file = "pillow-10.3.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1530e8f3a4b965eb6a7785cf17a426c779333eb62c9a7d1bbcf3ffd5bf77a4aa"}, + {file = "pillow-10.3.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5d512aafa1d32efa014fa041d38868fda85028e3f930a96f85d49c7d8ddc0383"}, + {file = "pillow-10.3.0-cp38-cp38-manylinux_2_28_aarch64.whl", hash = "sha256:339894035d0ede518b16073bdc2feef4c991ee991a29774b33e515f1d308e08d"}, + {file = "pillow-10.3.0-cp38-cp38-manylinux_2_28_x86_64.whl", hash = "sha256:aa7e402ce11f0885305bfb6afb3434b3cd8f53b563ac065452d9d5654c7b86fd"}, + {file = "pillow-10.3.0-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:0ea2a783a2bdf2a561808fe4a7a12e9aa3799b701ba305de596bc48b8bdfce9d"}, + {file = "pillow-10.3.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:c78e1b00a87ce43bb37642c0812315b411e856a905d58d597750eb79802aaaa3"}, + {file = "pillow-10.3.0-cp38-cp38-win32.whl", hash = "sha256:72d622d262e463dfb7595202d229f5f3ab4b852289a1cd09650362db23b9eb0b"}, + {file = "pillow-10.3.0-cp38-cp38-win_amd64.whl", hash = "sha256:2034f6759a722da3a3dbd91a81148cf884e91d1b747992ca288ab88c1de15999"}, + {file = "pillow-10.3.0-cp39-cp39-macosx_10_10_x86_64.whl", hash = "sha256:2ed854e716a89b1afcedea551cd85f2eb2a807613752ab997b9974aaa0d56936"}, + {file = "pillow-10.3.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:dc1a390a82755a8c26c9964d457d4c9cbec5405896cba94cf51f36ea0d855002"}, + {file = "pillow-10.3.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4203efca580f0dd6f882ca211f923168548f7ba334c189e9eab1178ab840bf60"}, + {file = "pillow-10.3.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3102045a10945173d38336f6e71a8dc71bcaeed55c3123ad4af82c52807b9375"}, + {file = "pillow-10.3.0-cp39-cp39-manylinux_2_28_aarch64.whl", hash = "sha256:6fb1b30043271ec92dc65f6d9f0b7a830c210b8a96423074b15c7bc999975f57"}, + {file = "pillow-10.3.0-cp39-cp39-manylinux_2_28_x86_64.whl", hash = "sha256:1dfc94946bc60ea375cc39cff0b8da6c7e5f8fcdc1d946beb8da5c216156ddd8"}, + {file = "pillow-10.3.0-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:b09b86b27a064c9624d0a6c54da01c1beaf5b6cadfa609cf63789b1d08a797b9"}, + {file = "pillow-10.3.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:d3b2348a78bc939b4fed6552abfd2e7988e0f81443ef3911a4b8498ca084f6eb"}, + {file = "pillow-10.3.0-cp39-cp39-win32.whl", hash = "sha256:45ebc7b45406febf07fef35d856f0293a92e7417ae7933207e90bf9090b70572"}, + {file = "pillow-10.3.0-cp39-cp39-win_amd64.whl", hash = "sha256:0ba26351b137ca4e0db0342d5d00d2e355eb29372c05afd544ebf47c0956ffeb"}, + {file = "pillow-10.3.0-cp39-cp39-win_arm64.whl", hash = "sha256:50fd3f6b26e3441ae07b7c979309638b72abc1a25da31a81a7fbd9495713ef4f"}, + {file = "pillow-10.3.0-pp310-pypy310_pp73-macosx_10_10_x86_64.whl", hash = "sha256:6b02471b72526ab8a18c39cb7967b72d194ec53c1fd0a70b050565a0f366d355"}, + {file = "pillow-10.3.0-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:8ab74c06ffdab957d7670c2a5a6e1a70181cd10b727cd788c4dd9005b6a8acd9"}, + {file = "pillow-10.3.0-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:048eeade4c33fdf7e08da40ef402e748df113fd0b4584e32c4af74fe78baaeb2"}, + {file = "pillow-10.3.0-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9e2ec1e921fd07c7cda7962bad283acc2f2a9ccc1b971ee4b216b75fad6f0463"}, + {file = "pillow-10.3.0-pp310-pypy310_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:4c8e73e99da7db1b4cad7f8d682cf6abad7844da39834c288fbfa394a47bbced"}, + {file = "pillow-10.3.0-pp310-pypy310_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:16563993329b79513f59142a6b02055e10514c1a8e86dca8b48a893e33cf91e3"}, + {file = "pillow-10.3.0-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:dd78700f5788ae180b5ee8902c6aea5a5726bac7c364b202b4b3e3ba2d293170"}, + {file = "pillow-10.3.0-pp39-pypy39_pp73-macosx_10_10_x86_64.whl", hash = "sha256:aff76a55a8aa8364d25400a210a65ff59d0168e0b4285ba6bf2bd83cf675ba32"}, + {file = "pillow-10.3.0-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:b7bc2176354defba3edc2b9a777744462da2f8e921fbaf61e52acb95bafa9828"}, + {file = "pillow-10.3.0-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:793b4e24db2e8742ca6423d3fde8396db336698c55cd34b660663ee9e45ed37f"}, + {file = "pillow-10.3.0-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d93480005693d247f8346bc8ee28c72a2191bdf1f6b5db469c096c0c867ac015"}, + {file = "pillow-10.3.0-pp39-pypy39_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:c83341b89884e2b2e55886e8fbbf37c3fa5efd6c8907124aeb72f285ae5696e5"}, + {file = "pillow-10.3.0-pp39-pypy39_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:1a1d1915db1a4fdb2754b9de292642a39a7fb28f1736699527bb649484fb966a"}, + {file = "pillow-10.3.0-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:a0eaa93d054751ee9964afa21c06247779b90440ca41d184aeb5d410f20ff591"}, + {file = "pillow-10.3.0.tar.gz", hash = "sha256:9d2455fbf44c914840c793e89aa82d0e1763a14253a000743719ae5946814b2d"}, ] [package.extras] @@ -3962,28 +4022,29 @@ xmp = ["defusedxml"] [[package]] name = "platformdirs" -version = "4.2.0" -description = "A small Python package for determining appropriate platform-specific dirs, e.g. a \"user data dir\"." +version = "4.2.1" +description = "A small Python package for determining appropriate platform-specific dirs, e.g. a `user data dir`." optional = false python-versions = ">=3.8" files = [ - {file = "platformdirs-4.2.0-py3-none-any.whl", hash = "sha256:0614df2a2f37e1a662acbd8e2b25b92ccf8632929bc6d43467e17fe89c75e068"}, - {file = "platformdirs-4.2.0.tar.gz", hash = "sha256:ef0cc731df711022c174543cb70a9b5bd22e5a9337c8624ef2c2ceb8ddad8768"}, + {file = "platformdirs-4.2.1-py3-none-any.whl", hash = "sha256:17d5a1161b3fd67b390023cb2d3b026bbd40abde6fdb052dfbd3a29c3ba22ee1"}, + {file = "platformdirs-4.2.1.tar.gz", hash = "sha256:031cd18d4ec63ec53e82dceaac0417d218a6863f7745dfcc9efe7793b7039bdf"}, ] [package.extras] docs = ["furo (>=2023.9.10)", "proselint (>=0.13)", "sphinx (>=7.2.6)", "sphinx-autodoc-typehints (>=1.25.2)"] test = ["appdirs (==1.4.4)", "covdefaults (>=2.3)", "pytest (>=7.4.3)", "pytest-cov (>=4.1)", "pytest-mock (>=3.12)"] +type = ["mypy (>=1.8)"] [[package]] name = "pluggy" -version = "1.4.0" +version = "1.5.0" description = "plugin and hook calling mechanisms for python" optional = false python-versions = ">=3.8" files = [ - {file = "pluggy-1.4.0-py3-none-any.whl", hash = "sha256:7db9f7b503d67d1c5b95f59773ebb58a8c1c288129a88665838012cfb07b8981"}, - {file = "pluggy-1.4.0.tar.gz", hash = "sha256:8c85c2876142a764e5b7548e7d9a0e0ddb46f5185161049a79b7e974454223be"}, + {file = "pluggy-1.5.0-py3-none-any.whl", hash = "sha256:44e1ad92c8ca002de6377e165f3e0f1be63266ab4d554740532335b9d75ea669"}, + {file = "pluggy-1.5.0.tar.gz", hash = "sha256:2cffa88e94fdc978c4c574f15f9e59b7f4201d439195c3715ca9e2486f1d0cf1"}, ] [package.extras] @@ -4057,22 +4118,22 @@ wcwidth = "*" [[package]] name = "protobuf" -version = "4.25.3" +version = "5.26.1" description = "" optional = false python-versions = ">=3.8" files = [ - {file = "protobuf-4.25.3-cp310-abi3-win32.whl", hash = "sha256:d4198877797a83cbfe9bffa3803602bbe1625dc30d8a097365dbc762e5790faa"}, - {file = "protobuf-4.25.3-cp310-abi3-win_amd64.whl", hash = "sha256:209ba4cc916bab46f64e56b85b090607a676f66b473e6b762e6f1d9d591eb2e8"}, - {file = "protobuf-4.25.3-cp37-abi3-macosx_10_9_universal2.whl", hash = "sha256:f1279ab38ecbfae7e456a108c5c0681e4956d5b1090027c1de0f934dfdb4b35c"}, - {file = "protobuf-4.25.3-cp37-abi3-manylinux2014_aarch64.whl", hash = "sha256:e7cb0ae90dd83727f0c0718634ed56837bfeeee29a5f82a7514c03ee1364c019"}, - {file = "protobuf-4.25.3-cp37-abi3-manylinux2014_x86_64.whl", hash = "sha256:7c8daa26095f82482307bc717364e7c13f4f1c99659be82890dcfc215194554d"}, - {file = "protobuf-4.25.3-cp38-cp38-win32.whl", hash = "sha256:f4f118245c4a087776e0a8408be33cf09f6c547442c00395fbfb116fac2f8ac2"}, - {file = "protobuf-4.25.3-cp38-cp38-win_amd64.whl", hash = "sha256:c053062984e61144385022e53678fbded7aea14ebb3e0305ae3592fb219ccfa4"}, - {file = "protobuf-4.25.3-cp39-cp39-win32.whl", hash = "sha256:19b270aeaa0099f16d3ca02628546b8baefe2955bbe23224aaf856134eccf1e4"}, - {file = "protobuf-4.25.3-cp39-cp39-win_amd64.whl", hash = "sha256:e3c97a1555fd6388f857770ff8b9703083de6bf1f9274a002a332d65fbb56c8c"}, - {file = "protobuf-4.25.3-py3-none-any.whl", hash = "sha256:f0700d54bcf45424477e46a9f0944155b46fb0639d69728739c0e47bab83f2b9"}, - {file = "protobuf-4.25.3.tar.gz", hash = "sha256:25b5d0b42fd000320bd7830b349e3b696435f3b329810427a6bcce6a5492cc5c"}, + {file = "protobuf-5.26.1-cp310-abi3-win32.whl", hash = "sha256:3c388ea6ddfe735f8cf69e3f7dc7611e73107b60bdfcf5d0f024c3ccd3794e23"}, + {file = "protobuf-5.26.1-cp310-abi3-win_amd64.whl", hash = "sha256:e6039957449cb918f331d32ffafa8eb9255769c96aa0560d9a5bf0b4e00a2a33"}, + {file = "protobuf-5.26.1-cp37-abi3-macosx_10_9_universal2.whl", hash = "sha256:38aa5f535721d5bb99861166c445c4105c4e285c765fbb2ac10f116e32dcd46d"}, + {file = "protobuf-5.26.1-cp37-abi3-manylinux2014_aarch64.whl", hash = "sha256:fbfe61e7ee8c1860855696e3ac6cfd1b01af5498facc6834fcc345c9684fb2ca"}, + {file = "protobuf-5.26.1-cp37-abi3-manylinux2014_x86_64.whl", hash = "sha256:f7417703f841167e5a27d48be13389d52ad705ec09eade63dfc3180a959215d7"}, + {file = "protobuf-5.26.1-cp38-cp38-win32.whl", hash = "sha256:d693d2504ca96750d92d9de8a103102dd648fda04540495535f0fec7577ed8fc"}, + {file = "protobuf-5.26.1-cp38-cp38-win_amd64.whl", hash = "sha256:9b557c317ebe6836835ec4ef74ec3e994ad0894ea424314ad3552bc6e8835b4e"}, + {file = "protobuf-5.26.1-cp39-cp39-win32.whl", hash = "sha256:b9ba3ca83c2e31219ffbeb9d76b63aad35a3eb1544170c55336993d7a18ae72c"}, + {file = "protobuf-5.26.1-cp39-cp39-win_amd64.whl", hash = "sha256:7ee014c2c87582e101d6b54260af03b6596728505c79f17c8586e7523aaa8f8c"}, + {file = "protobuf-5.26.1-py3-none-any.whl", hash = "sha256:da612f2720c0183417194eeaa2523215c4fcc1a1949772dc65f05047e08d5932"}, + {file = "protobuf-5.26.1.tar.gz", hash = "sha256:8ca2a1d97c290ec7b16e4e5dff2e5ae150cc1582f55b5ab300d45cb0dfa90e51"}, ] [[package]] @@ -4130,47 +4191,47 @@ tests = ["pytest"] [[package]] name = "pyarrow" -version = "15.0.1" +version = "15.0.2" description = "Python library for Apache Arrow" optional = false python-versions = ">=3.8" files = [ - {file = "pyarrow-15.0.1-cp310-cp310-macosx_10_15_x86_64.whl", hash = "sha256:c2ddb3be5ea938c329a84171694fc230b241ce1b6b0ff1a0280509af51c375fa"}, - {file = "pyarrow-15.0.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:7543ea88a0ff72f8e6baaf9bfdbec2c62aeabdbede9e4a571c71cc3bc43b6302"}, - {file = "pyarrow-15.0.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1519e218a6941fc074e4501088d891afcb2adf77c236e03c34babcf3d6a0d1c7"}, - {file = "pyarrow-15.0.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:28cafa86e1944761970d3b3fc0411b14ff9b5c2b73cd22aaf470d7a3976335f5"}, - {file = "pyarrow-15.0.1-cp310-cp310-manylinux_2_28_aarch64.whl", hash = "sha256:be5c3d463e33d03eab496e1af7916b1d44001c08f0f458ad27dc16093a020638"}, - {file = "pyarrow-15.0.1-cp310-cp310-manylinux_2_28_x86_64.whl", hash = "sha256:47b1eda15d3aa3f49a07b1808648e1397e5dc6a80a30bf87faa8e2d02dad7ac3"}, - {file = "pyarrow-15.0.1-cp310-cp310-win_amd64.whl", hash = "sha256:e524a31be7db22deebbbcf242b189063ab9a7652c62471d296b31bc6e3cae77b"}, - {file = "pyarrow-15.0.1-cp311-cp311-macosx_10_15_x86_64.whl", hash = "sha256:a476fefe8bdd56122fb0d4881b785413e025858803cc1302d0d788d3522b374d"}, - {file = "pyarrow-15.0.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:309e6191be385f2e220586bfdb643f9bb21d7e1bc6dd0a6963dc538e347b2431"}, - {file = "pyarrow-15.0.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:83bc586903dbeb4365cbc72b602f99f70b96c5882e5dfac5278813c7d624ca3c"}, - {file = "pyarrow-15.0.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:07e652daac6d8b05280cd2af31c0fb61a4490ec6a53dc01588014d9fa3fdbee9"}, - {file = "pyarrow-15.0.1-cp311-cp311-manylinux_2_28_aarch64.whl", hash = "sha256:abad2e08652df153a72177ce20c897d083b0c4ebeec051239e2654ddf4d3c996"}, - {file = "pyarrow-15.0.1-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:cde663352bc83ad75ba7b3206e049ca1a69809223942362a8649e37bd22f9e3b"}, - {file = "pyarrow-15.0.1-cp311-cp311-win_amd64.whl", hash = "sha256:1b6e237dd7a08482a8b8f3f6512d258d2460f182931832a8c6ef3953203d31e1"}, - {file = "pyarrow-15.0.1-cp312-cp312-macosx_10_15_x86_64.whl", hash = "sha256:7bd167536ee23192760b8c731d39b7cfd37914c27fd4582335ffd08450ff799d"}, - {file = "pyarrow-15.0.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:7c08bb31eb2984ba5c3747d375bb522e7e536b8b25b149c9cb5e1c49b0ccb736"}, - {file = "pyarrow-15.0.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c0f9c1d630ed2524bd1ddf28ec92780a7b599fd54704cd653519f7ff5aec177a"}, - {file = "pyarrow-15.0.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5186048493395220550bca7b524420471aac2d77af831f584ce132680f55c3df"}, - {file = "pyarrow-15.0.1-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:31dc30c7ec8958da3a3d9f31d6c3630429b2091ede0ecd0d989fd6bec129f0e4"}, - {file = "pyarrow-15.0.1-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:3f111a014fb8ac2297b43a74bf4495cc479a332908f7ee49cb7cbd50714cb0c1"}, - {file = "pyarrow-15.0.1-cp312-cp312-win_amd64.whl", hash = "sha256:a6d1f7c15d7f68f08490d0cb34611497c74285b8a6bbeab4ef3fc20117310983"}, - {file = "pyarrow-15.0.1-cp38-cp38-macosx_10_15_x86_64.whl", hash = "sha256:9ad931b996f51c2f978ed517b55cb3c6078272fb4ec579e3da5a8c14873b698d"}, - {file = "pyarrow-15.0.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:738f6b53ab1c2f66b2bde8a1d77e186aeaab702d849e0dfa1158c9e2c030add3"}, - {file = "pyarrow-15.0.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2c1c3fc16bc74e33bf8f1e5a212938ed8d88e902f372c4dac6b5bad328567d2f"}, - {file = "pyarrow-15.0.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e1fa92512128f6c1b8dde0468c1454dd70f3bff623970e370d52efd4d24fd0be"}, - {file = "pyarrow-15.0.1-cp38-cp38-manylinux_2_28_aarch64.whl", hash = "sha256:b4157f307c202cbbdac147d9b07447a281fa8e63494f7fc85081da351ec6ace9"}, - {file = "pyarrow-15.0.1-cp38-cp38-manylinux_2_28_x86_64.whl", hash = "sha256:b75e7da26f383787f80ad76143b44844ffa28648fcc7099a83df1538c078d2f2"}, - {file = "pyarrow-15.0.1-cp38-cp38-win_amd64.whl", hash = "sha256:3a99eac76ae14096c209850935057b9e8ce97a78397c5cde8724674774f34e5d"}, - {file = "pyarrow-15.0.1-cp39-cp39-macosx_10_15_x86_64.whl", hash = "sha256:dd532d3177e031e9b2d2df19fd003d0cc0520d1747659fcabbd4d9bb87de508c"}, - {file = "pyarrow-15.0.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:ce8c89848fd37e5313fc2ce601483038ee5566db96ba0808d5883b2e2e55dc53"}, - {file = "pyarrow-15.0.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:862eac5e5f3b6477f7a92b2f27e560e1f4e5e9edfca9ea9da8a7478bb4abd5ce"}, - {file = "pyarrow-15.0.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8f0ea3a29cd5cb99bf14c1c4533eceaa00ea8fb580950fb5a89a5c771a994a4e"}, - {file = "pyarrow-15.0.1-cp39-cp39-manylinux_2_28_aarch64.whl", hash = "sha256:bb902f780cfd624b2e8fd8501fadab17618fdb548532620ef3d91312aaf0888a"}, - {file = "pyarrow-15.0.1-cp39-cp39-manylinux_2_28_x86_64.whl", hash = "sha256:4f87757f02735a6bb4ad2e1b98279ac45d53b748d5baf52401516413007c6999"}, - {file = "pyarrow-15.0.1-cp39-cp39-win_amd64.whl", hash = "sha256:efd3816c7fbfcbd406ac0f69873cebb052effd7cdc153ae5836d1b00845845d7"}, - {file = "pyarrow-15.0.1.tar.gz", hash = "sha256:21d812548d39d490e0c6928a7c663f37b96bf764034123d4b4ab4530ecc757a9"}, + {file = "pyarrow-15.0.2-cp310-cp310-macosx_10_15_x86_64.whl", hash = "sha256:88b340f0a1d05b5ccc3d2d986279045655b1fe8e41aba6ca44ea28da0d1455d8"}, + {file = "pyarrow-15.0.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:eaa8f96cecf32da508e6c7f69bb8401f03745c050c1dd42ec2596f2e98deecac"}, + {file = "pyarrow-15.0.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:23c6753ed4f6adb8461e7c383e418391b8d8453c5d67e17f416c3a5d5709afbd"}, + {file = "pyarrow-15.0.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f639c059035011db8c0497e541a8a45d98a58dbe34dc8fadd0ef128f2cee46e5"}, + {file = "pyarrow-15.0.2-cp310-cp310-manylinux_2_28_aarch64.whl", hash = "sha256:290e36a59a0993e9a5224ed2fb3e53375770f07379a0ea03ee2fce2e6d30b423"}, + {file = "pyarrow-15.0.2-cp310-cp310-manylinux_2_28_x86_64.whl", hash = "sha256:06c2bb2a98bc792f040bef31ad3e9be6a63d0cb39189227c08a7d955db96816e"}, + {file = "pyarrow-15.0.2-cp310-cp310-win_amd64.whl", hash = "sha256:f7a197f3670606a960ddc12adbe8075cea5f707ad7bf0dffa09637fdbb89f76c"}, + {file = "pyarrow-15.0.2-cp311-cp311-macosx_10_15_x86_64.whl", hash = "sha256:5f8bc839ea36b1f99984c78e06e7a06054693dc2af8920f6fb416b5bca9944e4"}, + {file = "pyarrow-15.0.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:f5e81dfb4e519baa6b4c80410421528c214427e77ca0ea9461eb4097c328fa33"}, + {file = "pyarrow-15.0.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3a4f240852b302a7af4646c8bfe9950c4691a419847001178662a98915fd7ee7"}, + {file = "pyarrow-15.0.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4e7d9cfb5a1e648e172428c7a42b744610956f3b70f524aa3a6c02a448ba853e"}, + {file = "pyarrow-15.0.2-cp311-cp311-manylinux_2_28_aarch64.whl", hash = "sha256:2d4f905209de70c0eb5b2de6763104d5a9a37430f137678edfb9a675bac9cd98"}, + {file = "pyarrow-15.0.2-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:90adb99e8ce5f36fbecbbc422e7dcbcbed07d985eed6062e459e23f9e71fd197"}, + {file = "pyarrow-15.0.2-cp311-cp311-win_amd64.whl", hash = "sha256:b116e7fd7889294cbd24eb90cd9bdd3850be3738d61297855a71ac3b8124ee38"}, + {file = "pyarrow-15.0.2-cp312-cp312-macosx_10_15_x86_64.whl", hash = "sha256:25335e6f1f07fdaa026a61c758ee7d19ce824a866b27bba744348fa73bb5a440"}, + {file = "pyarrow-15.0.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:90f19e976d9c3d8e73c80be84ddbe2f830b6304e4c576349d9360e335cd627fc"}, + {file = "pyarrow-15.0.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a22366249bf5fd40ddacc4f03cd3160f2d7c247692945afb1899bab8a140ddfb"}, + {file = "pyarrow-15.0.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c2a335198f886b07e4b5ea16d08ee06557e07db54a8400cc0d03c7f6a22f785f"}, + {file = "pyarrow-15.0.2-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:3e6d459c0c22f0b9c810a3917a1de3ee704b021a5fb8b3bacf968eece6df098f"}, + {file = "pyarrow-15.0.2-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:033b7cad32198754d93465dcfb71d0ba7cb7cd5c9afd7052cab7214676eec38b"}, + {file = "pyarrow-15.0.2-cp312-cp312-win_amd64.whl", hash = "sha256:29850d050379d6e8b5a693098f4de7fd6a2bea4365bfd073d7c57c57b95041ee"}, + {file = "pyarrow-15.0.2-cp38-cp38-macosx_10_15_x86_64.whl", hash = "sha256:7167107d7fb6dcadb375b4b691b7e316f4368f39f6f45405a05535d7ad5e5058"}, + {file = "pyarrow-15.0.2-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:e85241b44cc3d365ef950432a1b3bd44ac54626f37b2e3a0cc89c20e45dfd8bf"}, + {file = "pyarrow-15.0.2-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:248723e4ed3255fcd73edcecc209744d58a9ca852e4cf3d2577811b6d4b59818"}, + {file = "pyarrow-15.0.2-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3ff3bdfe6f1b81ca5b73b70a8d482d37a766433823e0c21e22d1d7dde76ca33f"}, + {file = "pyarrow-15.0.2-cp38-cp38-manylinux_2_28_aarch64.whl", hash = "sha256:f3d77463dee7e9f284ef42d341689b459a63ff2e75cee2b9302058d0d98fe142"}, + {file = "pyarrow-15.0.2-cp38-cp38-manylinux_2_28_x86_64.whl", hash = "sha256:8c1faf2482fb89766e79745670cbca04e7018497d85be9242d5350cba21357e1"}, + {file = "pyarrow-15.0.2-cp38-cp38-win_amd64.whl", hash = "sha256:28f3016958a8e45a1069303a4a4f6a7d4910643fc08adb1e2e4a7ff056272ad3"}, + {file = "pyarrow-15.0.2-cp39-cp39-macosx_10_15_x86_64.whl", hash = "sha256:89722cb64286ab3d4daf168386f6968c126057b8c7ec3ef96302e81d8cdb8ae4"}, + {file = "pyarrow-15.0.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:cd0ba387705044b3ac77b1b317165c0498299b08261d8122c96051024f953cd5"}, + {file = "pyarrow-15.0.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ad2459bf1f22b6a5cdcc27ebfd99307d5526b62d217b984b9f5c974651398832"}, + {file = "pyarrow-15.0.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:58922e4bfece8b02abf7159f1f53a8f4d9f8e08f2d988109126c17c3bb261f22"}, + {file = "pyarrow-15.0.2-cp39-cp39-manylinux_2_28_aarch64.whl", hash = "sha256:adccc81d3dc0478ea0b498807b39a8d41628fa9210729b2f718b78cb997c7c91"}, + {file = "pyarrow-15.0.2-cp39-cp39-manylinux_2_28_x86_64.whl", hash = "sha256:8bd2baa5fe531571847983f36a30ddbf65261ef23e496862ece83bdceb70420d"}, + {file = "pyarrow-15.0.2-cp39-cp39-win_amd64.whl", hash = "sha256:6669799a1d4ca9da9c7e06ef48368320f5856f36f9a4dd31a11839dda3f6cc8c"}, + {file = "pyarrow-15.0.2.tar.gz", hash = "sha256:9c9bc803cb3b7bfacc1e96ffbfd923601065d9d3f911179d81e72d99fd74a3d9"}, ] [package.dependencies] @@ -4178,29 +4239,29 @@ numpy = ">=1.16.6,<2" [[package]] name = "pycparser" -version = "2.21" +version = "2.22" description = "C parser in Python" optional = false -python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" +python-versions = ">=3.8" files = [ - {file = "pycparser-2.21-py2.py3-none-any.whl", hash = "sha256:8ee45429555515e1f6b185e78100aea234072576aa43ab53aefcae078162fca9"}, - {file = "pycparser-2.21.tar.gz", hash = "sha256:e644fdec12f7872f86c58ff790da456218b10f863970249516d60a5eaca77206"}, + {file = "pycparser-2.22-py3-none-any.whl", hash = "sha256:c3702b6d3dd8c7abc1afa565d7e63d53a1d0bd86cdc24edd75470f4de499cfcc"}, + {file = "pycparser-2.22.tar.gz", hash = "sha256:491c8be9c040f5390f5bf44a5b07752bd07f56edf992381b05c701439eec10f6"}, ] [[package]] name = "pydantic" -version = "2.6.4" +version = "2.7.1" description = "Data validation using Python type hints" optional = false python-versions = ">=3.8" files = [ - {file = "pydantic-2.6.4-py3-none-any.whl", hash = "sha256:cc46fce86607580867bdc3361ad462bab9c222ef042d3da86f2fb333e1d916c5"}, - {file = "pydantic-2.6.4.tar.gz", hash = "sha256:b1704e0847db01817624a6b86766967f552dd9dbf3afba4004409f908dcc84e6"}, + {file = "pydantic-2.7.1-py3-none-any.whl", hash = "sha256:e029badca45266732a9a79898a15ae2e8b14840b1eabbb25844be28f0b33f3d5"}, + {file = "pydantic-2.7.1.tar.gz", hash = "sha256:e9dbb5eada8abe4d9ae5f46b9939aead650cd2b68f249bb3a8139dbe125803cc"}, ] [package.dependencies] annotated-types = ">=0.4.0" -pydantic-core = "2.16.3" +pydantic-core = "2.18.2" typing-extensions = ">=4.6.1" [package.extras] @@ -4208,90 +4269,90 @@ email = ["email-validator (>=2.0.0)"] [[package]] name = "pydantic-core" -version = "2.16.3" -description = "" +version = "2.18.2" +description = "Core functionality for Pydantic validation and serialization" optional = false python-versions = ">=3.8" files = [ - {file = "pydantic_core-2.16.3-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:75b81e678d1c1ede0785c7f46690621e4c6e63ccd9192af1f0bd9d504bbb6bf4"}, - {file = "pydantic_core-2.16.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:9c865a7ee6f93783bd5d781af5a4c43dadc37053a5b42f7d18dc019f8c9d2bd1"}, - {file = "pydantic_core-2.16.3-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:162e498303d2b1c036b957a1278fa0899d02b2842f1ff901b6395104c5554a45"}, - {file = "pydantic_core-2.16.3-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:2f583bd01bbfbff4eaee0868e6fc607efdfcc2b03c1c766b06a707abbc856187"}, - {file = "pydantic_core-2.16.3-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b926dd38db1519ed3043a4de50214e0d600d404099c3392f098a7f9d75029ff8"}, - {file = "pydantic_core-2.16.3-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:716b542728d4c742353448765aa7cdaa519a7b82f9564130e2b3f6766018c9ec"}, - {file = "pydantic_core-2.16.3-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fc4ad7f7ee1a13d9cb49d8198cd7d7e3aa93e425f371a68235f784e99741561f"}, - {file = "pydantic_core-2.16.3-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:bd87f48924f360e5d1c5f770d6155ce0e7d83f7b4e10c2f9ec001c73cf475c99"}, - {file = "pydantic_core-2.16.3-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:0df446663464884297c793874573549229f9eca73b59360878f382a0fc085979"}, - {file = "pydantic_core-2.16.3-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:4df8a199d9f6afc5ae9a65f8f95ee52cae389a8c6b20163762bde0426275b7db"}, - {file = "pydantic_core-2.16.3-cp310-none-win32.whl", hash = "sha256:456855f57b413f077dff513a5a28ed838dbbb15082ba00f80750377eed23d132"}, - {file = "pydantic_core-2.16.3-cp310-none-win_amd64.whl", hash = "sha256:732da3243e1b8d3eab8c6ae23ae6a58548849d2e4a4e03a1924c8ddf71a387cb"}, - {file = "pydantic_core-2.16.3-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:519ae0312616026bf4cedc0fe459e982734f3ca82ee8c7246c19b650b60a5ee4"}, - {file = "pydantic_core-2.16.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:b3992a322a5617ded0a9f23fd06dbc1e4bd7cf39bc4ccf344b10f80af58beacd"}, - {file = "pydantic_core-2.16.3-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8d62da299c6ecb04df729e4b5c52dc0d53f4f8430b4492b93aa8de1f541c4aac"}, - {file = "pydantic_core-2.16.3-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:2acca2be4bb2f2147ada8cac612f8a98fc09f41c89f87add7256ad27332c2fda"}, - {file = "pydantic_core-2.16.3-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1b662180108c55dfbf1280d865b2d116633d436cfc0bba82323554873967b340"}, - {file = "pydantic_core-2.16.3-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e7c6ed0dc9d8e65f24f5824291550139fe6f37fac03788d4580da0d33bc00c97"}, - {file = "pydantic_core-2.16.3-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a6b1bb0827f56654b4437955555dc3aeeebeddc47c2d7ed575477f082622c49e"}, - {file = "pydantic_core-2.16.3-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:e56f8186d6210ac7ece503193ec84104da7ceb98f68ce18c07282fcc2452e76f"}, - {file = "pydantic_core-2.16.3-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:936e5db01dd49476fa8f4383c259b8b1303d5dd5fb34c97de194560698cc2c5e"}, - {file = "pydantic_core-2.16.3-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:33809aebac276089b78db106ee692bdc9044710e26f24a9a2eaa35a0f9fa70ba"}, - {file = "pydantic_core-2.16.3-cp311-none-win32.whl", hash = "sha256:ded1c35f15c9dea16ead9bffcde9bb5c7c031bff076355dc58dcb1cb436c4721"}, - {file = "pydantic_core-2.16.3-cp311-none-win_amd64.whl", hash = "sha256:d89ca19cdd0dd5f31606a9329e309d4fcbb3df860960acec32630297d61820df"}, - {file = "pydantic_core-2.16.3-cp311-none-win_arm64.whl", hash = "sha256:6162f8d2dc27ba21027f261e4fa26f8bcb3cf9784b7f9499466a311ac284b5b9"}, - {file = "pydantic_core-2.16.3-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:0f56ae86b60ea987ae8bcd6654a887238fd53d1384f9b222ac457070b7ac4cff"}, - {file = "pydantic_core-2.16.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:c9bd22a2a639e26171068f8ebb5400ce2c1bc7d17959f60a3b753ae13c632975"}, - {file = "pydantic_core-2.16.3-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4204e773b4b408062960e65468d5346bdfe139247ee5f1ca2a378983e11388a2"}, - {file = "pydantic_core-2.16.3-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:f651dd19363c632f4abe3480a7c87a9773be27cfe1341aef06e8759599454120"}, - {file = "pydantic_core-2.16.3-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:aaf09e615a0bf98d406657e0008e4a8701b11481840be7d31755dc9f97c44053"}, - {file = "pydantic_core-2.16.3-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8e47755d8152c1ab5b55928ab422a76e2e7b22b5ed8e90a7d584268dd49e9c6b"}, - {file = "pydantic_core-2.16.3-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:500960cb3a0543a724a81ba859da816e8cf01b0e6aaeedf2c3775d12ee49cade"}, - {file = "pydantic_core-2.16.3-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:cf6204fe865da605285c34cf1172879d0314ff267b1c35ff59de7154f35fdc2e"}, - {file = "pydantic_core-2.16.3-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:d33dd21f572545649f90c38c227cc8631268ba25c460b5569abebdd0ec5974ca"}, - {file = "pydantic_core-2.16.3-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:49d5d58abd4b83fb8ce763be7794d09b2f50f10aa65c0f0c1696c677edeb7cbf"}, - {file = "pydantic_core-2.16.3-cp312-none-win32.whl", hash = "sha256:f53aace168a2a10582e570b7736cc5bef12cae9cf21775e3eafac597e8551fbe"}, - {file = "pydantic_core-2.16.3-cp312-none-win_amd64.whl", hash = "sha256:0d32576b1de5a30d9a97f300cc6a3f4694c428d956adbc7e6e2f9cad279e45ed"}, - {file = "pydantic_core-2.16.3-cp312-none-win_arm64.whl", hash = "sha256:ec08be75bb268473677edb83ba71e7e74b43c008e4a7b1907c6d57e940bf34b6"}, - {file = "pydantic_core-2.16.3-cp38-cp38-macosx_10_12_x86_64.whl", hash = "sha256:b1f6f5938d63c6139860f044e2538baeee6f0b251a1816e7adb6cbce106a1f01"}, - {file = "pydantic_core-2.16.3-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:2a1ef6a36fdbf71538142ed604ad19b82f67b05749512e47f247a6ddd06afdc7"}, - {file = "pydantic_core-2.16.3-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:704d35ecc7e9c31d48926150afada60401c55efa3b46cd1ded5a01bdffaf1d48"}, - {file = "pydantic_core-2.16.3-cp38-cp38-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:d937653a696465677ed583124b94a4b2d79f5e30b2c46115a68e482c6a591c8a"}, - {file = "pydantic_core-2.16.3-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c9803edf8e29bd825f43481f19c37f50d2b01899448273b3a7758441b512acf8"}, - {file = "pydantic_core-2.16.3-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:72282ad4892a9fb2da25defeac8c2e84352c108705c972db82ab121d15f14e6d"}, - {file = "pydantic_core-2.16.3-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7f752826b5b8361193df55afcdf8ca6a57d0232653494ba473630a83ba50d8c9"}, - {file = "pydantic_core-2.16.3-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:4384a8f68ddb31a0b0c3deae88765f5868a1b9148939c3f4121233314ad5532c"}, - {file = "pydantic_core-2.16.3-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:a4b2bf78342c40b3dc830880106f54328928ff03e357935ad26c7128bbd66ce8"}, - {file = "pydantic_core-2.16.3-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:13dcc4802961b5f843a9385fc821a0b0135e8c07fc3d9949fd49627c1a5e6ae5"}, - {file = "pydantic_core-2.16.3-cp38-none-win32.whl", hash = "sha256:e3e70c94a0c3841e6aa831edab1619ad5c511199be94d0c11ba75fe06efe107a"}, - {file = "pydantic_core-2.16.3-cp38-none-win_amd64.whl", hash = "sha256:ecdf6bf5f578615f2e985a5e1f6572e23aa632c4bd1dc67f8f406d445ac115ed"}, - {file = "pydantic_core-2.16.3-cp39-cp39-macosx_10_12_x86_64.whl", hash = "sha256:bda1ee3e08252b8d41fa5537413ffdddd58fa73107171a126d3b9ff001b9b820"}, - {file = "pydantic_core-2.16.3-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:21b888c973e4f26b7a96491c0965a8a312e13be108022ee510248fe379a5fa23"}, - {file = "pydantic_core-2.16.3-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:be0ec334369316fa73448cc8c982c01e5d2a81c95969d58b8f6e272884df0074"}, - {file = "pydantic_core-2.16.3-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:b5b6079cc452a7c53dd378c6f881ac528246b3ac9aae0f8eef98498a75657805"}, - {file = "pydantic_core-2.16.3-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:7ee8d5f878dccb6d499ba4d30d757111847b6849ae07acdd1205fffa1fc1253c"}, - {file = "pydantic_core-2.16.3-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7233d65d9d651242a68801159763d09e9ec96e8a158dbf118dc090cd77a104c9"}, - {file = "pydantic_core-2.16.3-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c6119dc90483a5cb50a1306adb8d52c66e447da88ea44f323e0ae1a5fcb14256"}, - {file = "pydantic_core-2.16.3-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:578114bc803a4c1ff9946d977c221e4376620a46cf78da267d946397dc9514a8"}, - {file = "pydantic_core-2.16.3-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:d8f99b147ff3fcf6b3cc60cb0c39ea443884d5559a30b1481e92495f2310ff2b"}, - {file = "pydantic_core-2.16.3-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:4ac6b4ce1e7283d715c4b729d8f9dab9627586dafce81d9eaa009dd7f25dd972"}, - {file = "pydantic_core-2.16.3-cp39-none-win32.whl", hash = "sha256:e7774b570e61cb998490c5235740d475413a1f6de823169b4cf94e2fe9e9f6b2"}, - {file = "pydantic_core-2.16.3-cp39-none-win_amd64.whl", hash = "sha256:9091632a25b8b87b9a605ec0e61f241c456e9248bfdcf7abdf344fdb169c81cf"}, - {file = "pydantic_core-2.16.3-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:36fa178aacbc277bc6b62a2c3da95226520da4f4e9e206fdf076484363895d2c"}, - {file = "pydantic_core-2.16.3-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:dcca5d2bf65c6fb591fff92da03f94cd4f315972f97c21975398bd4bd046854a"}, - {file = "pydantic_core-2.16.3-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2a72fb9963cba4cd5793854fd12f4cfee731e86df140f59ff52a49b3552db241"}, - {file = "pydantic_core-2.16.3-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b60cc1a081f80a2105a59385b92d82278b15d80ebb3adb200542ae165cd7d183"}, - {file = "pydantic_core-2.16.3-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:cbcc558401de90a746d02ef330c528f2e668c83350f045833543cd57ecead1ad"}, - {file = "pydantic_core-2.16.3-pp310-pypy310_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:fee427241c2d9fb7192b658190f9f5fd6dfe41e02f3c1489d2ec1e6a5ab1e04a"}, - {file = "pydantic_core-2.16.3-pp310-pypy310_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:f4cb85f693044e0f71f394ff76c98ddc1bc0953e48c061725e540396d5c8a2e1"}, - {file = "pydantic_core-2.16.3-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:b29eeb887aa931c2fcef5aa515d9d176d25006794610c264ddc114c053bf96fe"}, - {file = "pydantic_core-2.16.3-pp39-pypy39_pp73-macosx_10_12_x86_64.whl", hash = "sha256:a425479ee40ff021f8216c9d07a6a3b54b31c8267c6e17aa88b70d7ebd0e5e5b"}, - {file = "pydantic_core-2.16.3-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:5c5cbc703168d1b7a838668998308018a2718c2130595e8e190220238addc96f"}, - {file = "pydantic_core-2.16.3-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:99b6add4c0b39a513d323d3b93bc173dac663c27b99860dd5bf491b240d26137"}, - {file = "pydantic_core-2.16.3-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:75f76ee558751746d6a38f89d60b6228fa174e5172d143886af0f85aa306fd89"}, - {file = "pydantic_core-2.16.3-pp39-pypy39_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:00ee1c97b5364b84cb0bd82e9bbf645d5e2871fb8c58059d158412fee2d33d8a"}, - {file = "pydantic_core-2.16.3-pp39-pypy39_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:287073c66748f624be4cef893ef9174e3eb88fe0b8a78dc22e88eca4bc357ca6"}, - {file = "pydantic_core-2.16.3-pp39-pypy39_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:ed25e1835c00a332cb10c683cd39da96a719ab1dfc08427d476bce41b92531fc"}, - {file = "pydantic_core-2.16.3-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:86b3d0033580bd6bbe07590152007275bd7af95f98eaa5bd36f3da219dcd93da"}, - {file = "pydantic_core-2.16.3.tar.gz", hash = "sha256:1cac689f80a3abab2d3c0048b29eea5751114054f032a941a32de4c852c59cad"}, + {file = "pydantic_core-2.18.2-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:9e08e867b306f525802df7cd16c44ff5ebbe747ff0ca6cf3fde7f36c05a59a81"}, + {file = "pydantic_core-2.18.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:f0a21cbaa69900cbe1a2e7cad2aa74ac3cf21b10c3efb0fa0b80305274c0e8a2"}, + {file = "pydantic_core-2.18.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0680b1f1f11fda801397de52c36ce38ef1c1dc841a0927a94f226dea29c3ae3d"}, + {file = "pydantic_core-2.18.2-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:95b9d5e72481d3780ba3442eac863eae92ae43a5f3adb5b4d0a1de89d42bb250"}, + {file = "pydantic_core-2.18.2-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c4fcf5cd9c4b655ad666ca332b9a081112cd7a58a8b5a6ca7a3104bc950f2038"}, + {file = "pydantic_core-2.18.2-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9b5155ff768083cb1d62f3e143b49a8a3432e6789a3abee8acd005c3c7af1c74"}, + {file = "pydantic_core-2.18.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:553ef617b6836fc7e4df130bb851e32fe357ce36336d897fd6646d6058d980af"}, + {file = "pydantic_core-2.18.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:b89ed9eb7d616ef5714e5590e6cf7f23b02d0d539767d33561e3675d6f9e3857"}, + {file = "pydantic_core-2.18.2-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:75f7e9488238e920ab6204399ded280dc4c307d034f3924cd7f90a38b1829563"}, + {file = "pydantic_core-2.18.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:ef26c9e94a8c04a1b2924149a9cb081836913818e55681722d7f29af88fe7b38"}, + {file = "pydantic_core-2.18.2-cp310-none-win32.whl", hash = "sha256:182245ff6b0039e82b6bb585ed55a64d7c81c560715d1bad0cbad6dfa07b4027"}, + {file = "pydantic_core-2.18.2-cp310-none-win_amd64.whl", hash = "sha256:e23ec367a948b6d812301afc1b13f8094ab7b2c280af66ef450efc357d2ae543"}, + {file = "pydantic_core-2.18.2-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:219da3f096d50a157f33645a1cf31c0ad1fe829a92181dd1311022f986e5fbe3"}, + {file = "pydantic_core-2.18.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:cc1cfd88a64e012b74e94cd00bbe0f9c6df57049c97f02bb07d39e9c852e19a4"}, + {file = "pydantic_core-2.18.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:05b7133a6e6aeb8df37d6f413f7705a37ab4031597f64ab56384c94d98fa0e90"}, + {file = "pydantic_core-2.18.2-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:224c421235f6102e8737032483f43c1a8cfb1d2f45740c44166219599358c2cd"}, + {file = "pydantic_core-2.18.2-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b14d82cdb934e99dda6d9d60dc84a24379820176cc4a0d123f88df319ae9c150"}, + {file = "pydantic_core-2.18.2-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2728b01246a3bba6de144f9e3115b532ee44bd6cf39795194fb75491824a1413"}, + {file = "pydantic_core-2.18.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:470b94480bb5ee929f5acba6995251ada5e059a5ef3e0dfc63cca287283ebfa6"}, + {file = "pydantic_core-2.18.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:997abc4df705d1295a42f95b4eec4950a37ad8ae46d913caeee117b6b198811c"}, + {file = "pydantic_core-2.18.2-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:75250dbc5290e3f1a0f4618db35e51a165186f9034eff158f3d490b3fed9f8a0"}, + {file = "pydantic_core-2.18.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:4456f2dca97c425231d7315737d45239b2b51a50dc2b6f0c2bb181fce6207664"}, + {file = "pydantic_core-2.18.2-cp311-none-win32.whl", hash = "sha256:269322dcc3d8bdb69f054681edff86276b2ff972447863cf34c8b860f5188e2e"}, + {file = "pydantic_core-2.18.2-cp311-none-win_amd64.whl", hash = "sha256:800d60565aec896f25bc3cfa56d2277d52d5182af08162f7954f938c06dc4ee3"}, + {file = "pydantic_core-2.18.2-cp311-none-win_arm64.whl", hash = "sha256:1404c69d6a676245199767ba4f633cce5f4ad4181f9d0ccb0577e1f66cf4c46d"}, + {file = "pydantic_core-2.18.2-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:fb2bd7be70c0fe4dfd32c951bc813d9fe6ebcbfdd15a07527796c8204bd36242"}, + {file = "pydantic_core-2.18.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:6132dd3bd52838acddca05a72aafb6eab6536aa145e923bb50f45e78b7251043"}, + {file = "pydantic_core-2.18.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d7d904828195733c183d20a54230c0df0eb46ec746ea1a666730787353e87182"}, + {file = "pydantic_core-2.18.2-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:c9bd70772c720142be1020eac55f8143a34ec9f82d75a8e7a07852023e46617f"}, + {file = "pydantic_core-2.18.2-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:2b8ed04b3582771764538f7ee7001b02e1170223cf9b75dff0bc698fadb00cf3"}, + {file = "pydantic_core-2.18.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e6dac87ddb34aaec85f873d737e9d06a3555a1cc1a8e0c44b7f8d5daeb89d86f"}, + {file = "pydantic_core-2.18.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7ca4ae5a27ad7a4ee5170aebce1574b375de390bc01284f87b18d43a3984df72"}, + {file = "pydantic_core-2.18.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:886eec03591b7cf058467a70a87733b35f44707bd86cf64a615584fd72488b7c"}, + {file = "pydantic_core-2.18.2-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:ca7b0c1f1c983e064caa85f3792dd2fe3526b3505378874afa84baf662e12241"}, + {file = "pydantic_core-2.18.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:4b4356d3538c3649337df4074e81b85f0616b79731fe22dd11b99499b2ebbdf3"}, + {file = "pydantic_core-2.18.2-cp312-none-win32.whl", hash = "sha256:8b172601454f2d7701121bbec3425dd71efcb787a027edf49724c9cefc14c038"}, + {file = "pydantic_core-2.18.2-cp312-none-win_amd64.whl", hash = "sha256:b1bd7e47b1558ea872bd16c8502c414f9e90dcf12f1395129d7bb42a09a95438"}, + {file = "pydantic_core-2.18.2-cp312-none-win_arm64.whl", hash = "sha256:98758d627ff397e752bc339272c14c98199c613f922d4a384ddc07526c86a2ec"}, + {file = "pydantic_core-2.18.2-cp38-cp38-macosx_10_12_x86_64.whl", hash = "sha256:9fdad8e35f278b2c3eb77cbdc5c0a49dada440657bf738d6905ce106dc1de439"}, + {file = "pydantic_core-2.18.2-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:1d90c3265ae107f91a4f279f4d6f6f1d4907ac76c6868b27dc7fb33688cfb347"}, + {file = "pydantic_core-2.18.2-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:390193c770399861d8df9670fb0d1874f330c79caaca4642332df7c682bf6b91"}, + {file = "pydantic_core-2.18.2-cp38-cp38-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:82d5d4d78e4448683cb467897fe24e2b74bb7b973a541ea1dcfec1d3cbce39fb"}, + {file = "pydantic_core-2.18.2-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4774f3184d2ef3e14e8693194f661dea5a4d6ca4e3dc8e39786d33a94865cefd"}, + {file = "pydantic_core-2.18.2-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d4d938ec0adf5167cb335acb25a4ee69a8107e4984f8fbd2e897021d9e4ca21b"}, + {file = "pydantic_core-2.18.2-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e0e8b1be28239fc64a88a8189d1df7fad8be8c1ae47fcc33e43d4be15f99cc70"}, + {file = "pydantic_core-2.18.2-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:868649da93e5a3d5eacc2b5b3b9235c98ccdbfd443832f31e075f54419e1b96b"}, + {file = "pydantic_core-2.18.2-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:78363590ef93d5d226ba21a90a03ea89a20738ee5b7da83d771d283fd8a56761"}, + {file = "pydantic_core-2.18.2-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:852e966fbd035a6468fc0a3496589b45e2208ec7ca95c26470a54daed82a0788"}, + {file = "pydantic_core-2.18.2-cp38-none-win32.whl", hash = "sha256:6a46e22a707e7ad4484ac9ee9f290f9d501df45954184e23fc29408dfad61350"}, + {file = "pydantic_core-2.18.2-cp38-none-win_amd64.whl", hash = "sha256:d91cb5ea8b11607cc757675051f61b3d93f15eca3cefb3e6c704a5d6e8440f4e"}, + {file = "pydantic_core-2.18.2-cp39-cp39-macosx_10_12_x86_64.whl", hash = "sha256:ae0a8a797a5e56c053610fa7be147993fe50960fa43609ff2a9552b0e07013e8"}, + {file = "pydantic_core-2.18.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:042473b6280246b1dbf530559246f6842b56119c2926d1e52b631bdc46075f2a"}, + {file = "pydantic_core-2.18.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1a388a77e629b9ec814c1b1e6b3b595fe521d2cdc625fcca26fbc2d44c816804"}, + {file = "pydantic_core-2.18.2-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:e25add29b8f3b233ae90ccef2d902d0ae0432eb0d45370fe315d1a5cf231004b"}, + {file = "pydantic_core-2.18.2-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f459a5ce8434614dfd39bbebf1041952ae01da6bed9855008cb33b875cb024c0"}, + {file = "pydantic_core-2.18.2-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:eff2de745698eb46eeb51193a9f41d67d834d50e424aef27df2fcdee1b153845"}, + {file = "pydantic_core-2.18.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a8309f67285bdfe65c372ea3722b7a5642680f3dba538566340a9d36e920b5f0"}, + {file = "pydantic_core-2.18.2-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:f93a8a2e3938ff656a7c1bc57193b1319960ac015b6e87d76c76bf14fe0244b4"}, + {file = "pydantic_core-2.18.2-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:22057013c8c1e272eb8d0eebc796701167d8377441ec894a8fed1af64a0bf399"}, + {file = "pydantic_core-2.18.2-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:cfeecd1ac6cc1fb2692c3d5110781c965aabd4ec5d32799773ca7b1456ac636b"}, + {file = "pydantic_core-2.18.2-cp39-none-win32.whl", hash = "sha256:0d69b4c2f6bb3e130dba60d34c0845ba31b69babdd3f78f7c0c8fae5021a253e"}, + {file = "pydantic_core-2.18.2-cp39-none-win_amd64.whl", hash = "sha256:d9319e499827271b09b4e411905b24a426b8fb69464dfa1696258f53a3334641"}, + {file = "pydantic_core-2.18.2-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:a1874c6dd4113308bd0eb568418e6114b252afe44319ead2b4081e9b9521fe75"}, + {file = "pydantic_core-2.18.2-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:ccdd111c03bfd3666bd2472b674c6899550e09e9f298954cfc896ab92b5b0e6d"}, + {file = "pydantic_core-2.18.2-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e18609ceaa6eed63753037fc06ebb16041d17d28199ae5aba0052c51449650a9"}, + {file = "pydantic_core-2.18.2-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6e5c584d357c4e2baf0ff7baf44f4994be121e16a2c88918a5817331fc7599d7"}, + {file = "pydantic_core-2.18.2-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:43f0f463cf89ace478de71a318b1b4f05ebc456a9b9300d027b4b57c1a2064fb"}, + {file = "pydantic_core-2.18.2-pp310-pypy310_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:e1b395e58b10b73b07b7cf740d728dd4ff9365ac46c18751bf8b3d8cca8f625a"}, + {file = "pydantic_core-2.18.2-pp310-pypy310_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:0098300eebb1c837271d3d1a2cd2911e7c11b396eac9661655ee524a7f10587b"}, + {file = "pydantic_core-2.18.2-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:36789b70d613fbac0a25bb07ab3d9dba4d2e38af609c020cf4d888d165ee0bf3"}, + {file = "pydantic_core-2.18.2-pp39-pypy39_pp73-macosx_10_12_x86_64.whl", hash = "sha256:3f9a801e7c8f1ef8718da265bba008fa121243dfe37c1cea17840b0944dfd72c"}, + {file = "pydantic_core-2.18.2-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:3a6515ebc6e69d85502b4951d89131ca4e036078ea35533bb76327f8424531ce"}, + {file = "pydantic_core-2.18.2-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:20aca1e2298c56ececfd8ed159ae4dde2df0781988c97ef77d5c16ff4bd5b400"}, + {file = "pydantic_core-2.18.2-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:223ee893d77a310a0391dca6df00f70bbc2f36a71a895cecd9a0e762dc37b349"}, + {file = "pydantic_core-2.18.2-pp39-pypy39_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:2334ce8c673ee93a1d6a65bd90327588387ba073c17e61bf19b4fd97d688d63c"}, + {file = "pydantic_core-2.18.2-pp39-pypy39_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:cbca948f2d14b09d20268cda7b0367723d79063f26c4ffc523af9042cad95592"}, + {file = "pydantic_core-2.18.2-pp39-pypy39_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:b3ef08e20ec49e02d5c6717a91bb5af9b20f1805583cb0adfe9ba2c6b505b5ae"}, + {file = "pydantic_core-2.18.2-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:c6fdc8627910eed0c01aed6a390a252fe3ea6d472ee70fdde56273f198938374"}, + {file = "pydantic_core-2.18.2.tar.gz", hash = "sha256:2e29d20810dfc3043ee13ac7d9e25105799817683348823f305ab3f349b9386e"}, ] [package.dependencies] @@ -4299,13 +4360,13 @@ typing-extensions = ">=4.6.0,<4.7.0 || >4.7.0" [[package]] name = "pydantic-extra-types" -version = "2.6.0" +version = "2.7.0" description = "Extra Pydantic types." optional = false python-versions = ">=3.8" files = [ - {file = "pydantic_extra_types-2.6.0-py3-none-any.whl", hash = "sha256:d291d521c2e2bf2e6f11971caf8d639518124ae26a76d2e712599e98c4ef2b2b"}, - {file = "pydantic_extra_types-2.6.0.tar.gz", hash = "sha256:e9a93cfb245158462acb76621785219f80ad112303a0a7784d2ada65e6ed6cba"}, + {file = "pydantic_extra_types-2.7.0-py3-none-any.whl", hash = "sha256:ac01bbdaa4f85e4c4744a9792c5b0cfe61efa5028a0e670c3d8bfbf8b36c8543"}, + {file = "pydantic_extra_types-2.7.0.tar.gz", hash = "sha256:b9d9ddd755fa5960ec5a77cffcbd5d8796a0116e1dfc8f7c3a27fa0041693382"}, ] [package.dependencies] @@ -4602,104 +4663,99 @@ files = [ [[package]] name = "pyzmq" -version = "25.1.2" +version = "26.0.2" description = "Python bindings for 0MQ" optional = false -python-versions = ">=3.6" +python-versions = ">=3.7" files = [ - {file = "pyzmq-25.1.2-cp310-cp310-macosx_10_15_universal2.whl", hash = "sha256:e624c789359f1a16f83f35e2c705d07663ff2b4d4479bad35621178d8f0f6ea4"}, - {file = "pyzmq-25.1.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:49151b0efece79f6a79d41a461d78535356136ee70084a1c22532fc6383f4ad0"}, - {file = "pyzmq-25.1.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d9a5f194cf730f2b24d6af1f833c14c10f41023da46a7f736f48b6d35061e76e"}, - {file = "pyzmq-25.1.2-cp310-cp310-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:faf79a302f834d9e8304fafdc11d0d042266667ac45209afa57e5efc998e3872"}, - {file = "pyzmq-25.1.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7f51a7b4ead28d3fca8dda53216314a553b0f7a91ee8fc46a72b402a78c3e43d"}, - {file = "pyzmq-25.1.2-cp310-cp310-manylinux_2_28_x86_64.whl", hash = "sha256:0ddd6d71d4ef17ba5a87becf7ddf01b371eaba553c603477679ae817a8d84d75"}, - {file = "pyzmq-25.1.2-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:246747b88917e4867e2367b005fc8eefbb4a54b7db363d6c92f89d69abfff4b6"}, - {file = "pyzmq-25.1.2-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:00c48ae2fd81e2a50c3485de1b9d5c7c57cd85dc8ec55683eac16846e57ac979"}, - {file = "pyzmq-25.1.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:5a68d491fc20762b630e5db2191dd07ff89834086740f70e978bb2ef2668be08"}, - {file = "pyzmq-25.1.2-cp310-cp310-win32.whl", hash = "sha256:09dfe949e83087da88c4a76767df04b22304a682d6154de2c572625c62ad6886"}, - {file = "pyzmq-25.1.2-cp310-cp310-win_amd64.whl", hash = "sha256:fa99973d2ed20417744fca0073390ad65ce225b546febb0580358e36aa90dba6"}, - {file = "pyzmq-25.1.2-cp311-cp311-macosx_10_15_universal2.whl", hash = "sha256:82544e0e2d0c1811482d37eef297020a040c32e0687c1f6fc23a75b75db8062c"}, - {file = "pyzmq-25.1.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:01171fc48542348cd1a360a4b6c3e7d8f46cdcf53a8d40f84db6707a6768acc1"}, - {file = "pyzmq-25.1.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bc69c96735ab501419c432110016329bf0dea8898ce16fab97c6d9106dc0b348"}, - {file = "pyzmq-25.1.2-cp311-cp311-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3e124e6b1dd3dfbeb695435dff0e383256655bb18082e094a8dd1f6293114642"}, - {file = "pyzmq-25.1.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7598d2ba821caa37a0f9d54c25164a4fa351ce019d64d0b44b45540950458840"}, - {file = "pyzmq-25.1.2-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:d1299d7e964c13607efd148ca1f07dcbf27c3ab9e125d1d0ae1d580a1682399d"}, - {file = "pyzmq-25.1.2-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:4e6f689880d5ad87918430957297c975203a082d9a036cc426648fcbedae769b"}, - {file = "pyzmq-25.1.2-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:cc69949484171cc961e6ecd4a8911b9ce7a0d1f738fcae717177c231bf77437b"}, - {file = "pyzmq-25.1.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:9880078f683466b7f567b8624bfc16cad65077be046b6e8abb53bed4eeb82dd3"}, - {file = "pyzmq-25.1.2-cp311-cp311-win32.whl", hash = "sha256:4e5837af3e5aaa99a091302df5ee001149baff06ad22b722d34e30df5f0d9097"}, - {file = "pyzmq-25.1.2-cp311-cp311-win_amd64.whl", hash = "sha256:25c2dbb97d38b5ac9fd15586e048ec5eb1e38f3d47fe7d92167b0c77bb3584e9"}, - {file = "pyzmq-25.1.2-cp312-cp312-macosx_10_15_universal2.whl", hash = "sha256:11e70516688190e9c2db14fcf93c04192b02d457b582a1f6190b154691b4c93a"}, - {file = "pyzmq-25.1.2-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:313c3794d650d1fccaaab2df942af9f2c01d6217c846177cfcbc693c7410839e"}, - {file = "pyzmq-25.1.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1b3cbba2f47062b85fe0ef9de5b987612140a9ba3a9c6d2543c6dec9f7c2ab27"}, - {file = "pyzmq-25.1.2-cp312-cp312-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:fc31baa0c32a2ca660784d5af3b9487e13b61b3032cb01a115fce6588e1bed30"}, - {file = "pyzmq-25.1.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:02c9087b109070c5ab0b383079fa1b5f797f8d43e9a66c07a4b8b8bdecfd88ee"}, - {file = "pyzmq-25.1.2-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:f8429b17cbb746c3e043cb986328da023657e79d5ed258b711c06a70c2ea7537"}, - {file = "pyzmq-25.1.2-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:5074adeacede5f810b7ef39607ee59d94e948b4fd954495bdb072f8c54558181"}, - {file = "pyzmq-25.1.2-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:7ae8f354b895cbd85212da245f1a5ad8159e7840e37d78b476bb4f4c3f32a9fe"}, - {file = "pyzmq-25.1.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:b264bf2cc96b5bc43ce0e852be995e400376bd87ceb363822e2cb1964fcdc737"}, - {file = "pyzmq-25.1.2-cp312-cp312-win32.whl", hash = "sha256:02bbc1a87b76e04fd780b45e7f695471ae6de747769e540da909173d50ff8e2d"}, - {file = "pyzmq-25.1.2-cp312-cp312-win_amd64.whl", hash = "sha256:ced111c2e81506abd1dc142e6cd7b68dd53747b3b7ae5edbea4578c5eeff96b7"}, - {file = "pyzmq-25.1.2-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:7b6d09a8962a91151f0976008eb7b29b433a560fde056ec7a3db9ec8f1075438"}, - {file = "pyzmq-25.1.2-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:967668420f36878a3c9ecb5ab33c9d0ff8d054f9c0233d995a6d25b0e95e1b6b"}, - {file = "pyzmq-25.1.2-cp36-cp36m-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:5edac3f57c7ddaacdb4d40f6ef2f9e299471fc38d112f4bc6d60ab9365445fb0"}, - {file = "pyzmq-25.1.2-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:0dabfb10ef897f3b7e101cacba1437bd3a5032ee667b7ead32bbcdd1a8422fe7"}, - {file = "pyzmq-25.1.2-cp36-cp36m-musllinux_1_1_aarch64.whl", hash = "sha256:2c6441e0398c2baacfe5ba30c937d274cfc2dc5b55e82e3749e333aabffde561"}, - {file = "pyzmq-25.1.2-cp36-cp36m-musllinux_1_1_i686.whl", hash = "sha256:16b726c1f6c2e7625706549f9dbe9b06004dfbec30dbed4bf50cbdfc73e5b32a"}, - {file = "pyzmq-25.1.2-cp36-cp36m-musllinux_1_1_x86_64.whl", hash = "sha256:a86c2dd76ef71a773e70551a07318b8e52379f58dafa7ae1e0a4be78efd1ff16"}, - {file = "pyzmq-25.1.2-cp36-cp36m-win32.whl", hash = "sha256:359f7f74b5d3c65dae137f33eb2bcfa7ad9ebefd1cab85c935f063f1dbb245cc"}, - {file = "pyzmq-25.1.2-cp36-cp36m-win_amd64.whl", hash = "sha256:55875492f820d0eb3417b51d96fea549cde77893ae3790fd25491c5754ea2f68"}, - {file = "pyzmq-25.1.2-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:b8c8a419dfb02e91b453615c69568442e897aaf77561ee0064d789705ff37a92"}, - {file = "pyzmq-25.1.2-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8807c87fa893527ae8a524c15fc505d9950d5e856f03dae5921b5e9aa3b8783b"}, - {file = "pyzmq-25.1.2-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:5e319ed7d6b8f5fad9b76daa0a68497bc6f129858ad956331a5835785761e003"}, - {file = "pyzmq-25.1.2-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:3c53687dde4d9d473c587ae80cc328e5b102b517447456184b485587ebd18b62"}, - {file = "pyzmq-25.1.2-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:9add2e5b33d2cd765ad96d5eb734a5e795a0755f7fc49aa04f76d7ddda73fd70"}, - {file = "pyzmq-25.1.2-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:e690145a8c0c273c28d3b89d6fb32c45e0d9605b2293c10e650265bf5c11cfec"}, - {file = "pyzmq-25.1.2-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:00a06faa7165634f0cac1abb27e54d7a0b3b44eb9994530b8ec73cf52e15353b"}, - {file = "pyzmq-25.1.2-cp37-cp37m-win32.whl", hash = "sha256:0f97bc2f1f13cb16905a5f3e1fbdf100e712d841482b2237484360f8bc4cb3d7"}, - {file = "pyzmq-25.1.2-cp37-cp37m-win_amd64.whl", hash = "sha256:6cc0020b74b2e410287e5942e1e10886ff81ac77789eb20bec13f7ae681f0fdd"}, - {file = "pyzmq-25.1.2-cp38-cp38-macosx_10_15_universal2.whl", hash = "sha256:bef02cfcbded83473bdd86dd8d3729cd82b2e569b75844fb4ea08fee3c26ae41"}, - {file = "pyzmq-25.1.2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:e10a4b5a4b1192d74853cc71a5e9fd022594573926c2a3a4802020360aa719d8"}, - {file = "pyzmq-25.1.2-cp38-cp38-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:8c5f80e578427d4695adac6fdf4370c14a2feafdc8cb35549c219b90652536ae"}, - {file = "pyzmq-25.1.2-cp38-cp38-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:5dde6751e857910c1339890f3524de74007958557593b9e7e8c5f01cd919f8a7"}, - {file = "pyzmq-25.1.2-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ea1608dd169da230a0ad602d5b1ebd39807ac96cae1845c3ceed39af08a5c6df"}, - {file = "pyzmq-25.1.2-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:0f513130c4c361201da9bc69df25a086487250e16b5571ead521b31ff6b02220"}, - {file = "pyzmq-25.1.2-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:019744b99da30330798bb37df33549d59d380c78e516e3bab9c9b84f87a9592f"}, - {file = "pyzmq-25.1.2-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:2e2713ef44be5d52dd8b8e2023d706bf66cb22072e97fc71b168e01d25192755"}, - {file = "pyzmq-25.1.2-cp38-cp38-win32.whl", hash = "sha256:07cd61a20a535524906595e09344505a9bd46f1da7a07e504b315d41cd42eb07"}, - {file = "pyzmq-25.1.2-cp38-cp38-win_amd64.whl", hash = "sha256:eb7e49a17fb8c77d3119d41a4523e432eb0c6932187c37deb6fbb00cc3028088"}, - {file = "pyzmq-25.1.2-cp39-cp39-macosx_10_15_universal2.whl", hash = "sha256:94504ff66f278ab4b7e03e4cba7e7e400cb73bfa9d3d71f58d8972a8dc67e7a6"}, - {file = "pyzmq-25.1.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:6dd0d50bbf9dca1d0bdea219ae6b40f713a3fb477c06ca3714f208fd69e16fd8"}, - {file = "pyzmq-25.1.2-cp39-cp39-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:004ff469d21e86f0ef0369717351073e0e577428e514c47c8480770d5e24a565"}, - {file = "pyzmq-25.1.2-cp39-cp39-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:c0b5ca88a8928147b7b1e2dfa09f3b6c256bc1135a1338536cbc9ea13d3b7add"}, - {file = "pyzmq-25.1.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2c9a79f1d2495b167119d02be7448bfba57fad2a4207c4f68abc0bab4b92925b"}, - {file = "pyzmq-25.1.2-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:518efd91c3d8ac9f9b4f7dd0e2b7b8bf1a4fe82a308009016b07eaa48681af82"}, - {file = "pyzmq-25.1.2-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:1ec23bd7b3a893ae676d0e54ad47d18064e6c5ae1fadc2f195143fb27373f7f6"}, - {file = "pyzmq-25.1.2-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:db36c27baed588a5a8346b971477b718fdc66cf5b80cbfbd914b4d6d355e44e2"}, - {file = "pyzmq-25.1.2-cp39-cp39-win32.whl", hash = "sha256:39b1067f13aba39d794a24761e385e2eddc26295826530a8c7b6c6c341584289"}, - {file = "pyzmq-25.1.2-cp39-cp39-win_amd64.whl", hash = "sha256:8e9f3fabc445d0ce320ea2c59a75fe3ea591fdbdeebec5db6de530dd4b09412e"}, - {file = "pyzmq-25.1.2-pp310-pypy310_pp73-macosx_10_9_x86_64.whl", hash = "sha256:a8c1d566344aee826b74e472e16edae0a02e2a044f14f7c24e123002dcff1c05"}, - {file = "pyzmq-25.1.2-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:759cfd391a0996345ba94b6a5110fca9c557ad4166d86a6e81ea526c376a01e8"}, - {file = "pyzmq-25.1.2-pp310-pypy310_pp73-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7c61e346ac34b74028ede1c6b4bcecf649d69b707b3ff9dc0fab453821b04d1e"}, - {file = "pyzmq-25.1.2-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4cb8fc1f8d69b411b8ec0b5f1ffbcaf14c1db95b6bccea21d83610987435f1a4"}, - {file = "pyzmq-25.1.2-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:3c00c9b7d1ca8165c610437ca0c92e7b5607b2f9076f4eb4b095c85d6e680a1d"}, - {file = "pyzmq-25.1.2-pp37-pypy37_pp73-macosx_10_9_x86_64.whl", hash = "sha256:df0c7a16ebb94452d2909b9a7b3337940e9a87a824c4fc1c7c36bb4404cb0cde"}, - {file = "pyzmq-25.1.2-pp37-pypy37_pp73-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:45999e7f7ed5c390f2e87ece7f6c56bf979fb213550229e711e45ecc7d42ccb8"}, - {file = "pyzmq-25.1.2-pp37-pypy37_pp73-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:ac170e9e048b40c605358667aca3d94e98f604a18c44bdb4c102e67070f3ac9b"}, - {file = "pyzmq-25.1.2-pp37-pypy37_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d1b604734bec94f05f81b360a272fc824334267426ae9905ff32dc2be433ab96"}, - {file = "pyzmq-25.1.2-pp37-pypy37_pp73-win_amd64.whl", hash = "sha256:a793ac733e3d895d96f865f1806f160696422554e46d30105807fdc9841b9f7d"}, - {file = "pyzmq-25.1.2-pp38-pypy38_pp73-macosx_10_9_x86_64.whl", hash = "sha256:0806175f2ae5ad4b835ecd87f5f85583316b69f17e97786f7443baaf54b9bb98"}, - {file = "pyzmq-25.1.2-pp38-pypy38_pp73-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:ef12e259e7bc317c7597d4f6ef59b97b913e162d83b421dd0db3d6410f17a244"}, - {file = "pyzmq-25.1.2-pp38-pypy38_pp73-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:ea253b368eb41116011add00f8d5726762320b1bda892f744c91997b65754d73"}, - {file = "pyzmq-25.1.2-pp38-pypy38_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1b9b1f2ad6498445a941d9a4fee096d387fee436e45cc660e72e768d3d8ee611"}, - {file = "pyzmq-25.1.2-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:8b14c75979ce932c53b79976a395cb2a8cd3aaf14aef75e8c2cb55a330b9b49d"}, - {file = "pyzmq-25.1.2-pp39-pypy39_pp73-macosx_10_9_x86_64.whl", hash = "sha256:889370d5174a741a62566c003ee8ddba4b04c3f09a97b8000092b7ca83ec9c49"}, - {file = "pyzmq-25.1.2-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9a18fff090441a40ffda8a7f4f18f03dc56ae73f148f1832e109f9bffa85df15"}, - {file = "pyzmq-25.1.2-pp39-pypy39_pp73-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:99a6b36f95c98839ad98f8c553d8507644c880cf1e0a57fe5e3a3f3969040882"}, - {file = "pyzmq-25.1.2-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4345c9a27f4310afbb9c01750e9461ff33d6fb74cd2456b107525bbeebcb5be3"}, - {file = "pyzmq-25.1.2-pp39-pypy39_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:3516e0b6224cf6e43e341d56da15fd33bdc37fa0c06af4f029f7d7dfceceabbc"}, - {file = "pyzmq-25.1.2-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:146b9b1f29ead41255387fb07be56dc29639262c0f7344f570eecdcd8d683314"}, - {file = "pyzmq-25.1.2.tar.gz", hash = "sha256:93f1aa311e8bb912e34f004cf186407a4e90eec4f0ecc0efd26056bf7eda0226"}, + {file = "pyzmq-26.0.2-cp310-cp310-macosx_10_15_universal2.whl", hash = "sha256:1a60a03b01e8c9c58932ec0cca15b1712d911c2800eb82d4281bc1ae5b6dad50"}, + {file = "pyzmq-26.0.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:949067079e14ea1973bd740255e0840118c163d4bce8837f539d749f145cf5c3"}, + {file = "pyzmq-26.0.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:37e7edfa6cf96d036a403775c96afa25058d1bb940a79786a9a2fc94a783abe3"}, + {file = "pyzmq-26.0.2-cp310-cp310-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:903cc7a84a7d4326b43755c368780800e035aa3d711deae84a533fdffa8755b0"}, + {file = "pyzmq-26.0.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6cb2e41af165e5f327d06fbdd79a42a4e930267fade4e9f92d17f3ccce03f3a7"}, + {file = "pyzmq-26.0.2-cp310-cp310-manylinux_2_28_x86_64.whl", hash = "sha256:55353b8189adcfc4c125fc4ce59d477744118e9c0ec379dd0999c5fa120ac4f5"}, + {file = "pyzmq-26.0.2-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:f961423ff6236a752ced80057a20e623044df95924ed1009f844cde8b3a595f9"}, + {file = "pyzmq-26.0.2-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:ba77fe84fe4f5f3dc0ef681a6d366685c8ffe1c8439c1d7530997b05ac06a04b"}, + {file = "pyzmq-26.0.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:52589f0a745ef61b9c75c872cf91f8c1f7c0668eb3dd99d7abd639d8c0fb9ca7"}, + {file = "pyzmq-26.0.2-cp310-cp310-win32.whl", hash = "sha256:b7b6d2a46c7afe2ad03ec8faf9967090c8ceae85c4d8934d17d7cae6f9062b64"}, + {file = "pyzmq-26.0.2-cp310-cp310-win_amd64.whl", hash = "sha256:86531e20de249d9204cc6d8b13d5a30537748c78820215161d8a3b9ea58ca111"}, + {file = "pyzmq-26.0.2-cp310-cp310-win_arm64.whl", hash = "sha256:f26a05029ecd2bd306b941ff8cb80f7620b7901421052bc429d238305b1cbf2f"}, + {file = "pyzmq-26.0.2-cp311-cp311-macosx_10_15_universal2.whl", hash = "sha256:70770e296a9cb03d955540c99360aab861cbb3cba29516abbd106a15dbd91268"}, + {file = "pyzmq-26.0.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:2740fd7161b39e178554ebf21aa5667a1c9ef0cd2cb74298fd4ef017dae7aec4"}, + {file = "pyzmq-26.0.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f5e3706c32dea077faa42b1c92d825b7f86c866f72532d342e0be5e64d14d858"}, + {file = "pyzmq-26.0.2-cp311-cp311-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0fa1416876194927f7723d6b7171b95e1115602967fc6bfccbc0d2d51d8ebae1"}, + {file = "pyzmq-26.0.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4ef9a79a48794099c57dc2df00340b5d47c5caa1792f9ddb8c7a26b1280bd575"}, + {file = "pyzmq-26.0.2-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:1c60fcdfa3229aeee4291c5d60faed3a813b18bdadb86299c4bf49e8e51e8605"}, + {file = "pyzmq-26.0.2-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:e943c39c206b04df2eb5d71305761d7c3ca75fd49452115ea92db1b5b98dbdef"}, + {file = "pyzmq-26.0.2-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:8da0ed8a598693731c76659880a668f4748b59158f26ed283a93f7f04d47447e"}, + {file = "pyzmq-26.0.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:7bf51970b11d67096bede97cdbad0f4333f7664f4708b9b2acb352bf4faa3140"}, + {file = "pyzmq-26.0.2-cp311-cp311-win32.whl", hash = "sha256:6f8e6bd5d066be605faa9fe5ec10aa1a46ad9f18fc8646f2b9aaefc8fb575742"}, + {file = "pyzmq-26.0.2-cp311-cp311-win_amd64.whl", hash = "sha256:6d03da3a0ae691b361edcb39530075461202f699ce05adbb15055a0e1c9bcaa4"}, + {file = "pyzmq-26.0.2-cp311-cp311-win_arm64.whl", hash = "sha256:f84e33321b68ff00b60e9dbd1a483e31ab6022c577c8de525b8e771bd274ce68"}, + {file = "pyzmq-26.0.2-cp312-cp312-macosx_10_15_universal2.whl", hash = "sha256:44c33ebd1c62a01db7fbc24e18bdda569d6639217d13d5929e986a2b0f69070d"}, + {file = "pyzmq-26.0.2-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:ac04f904b4fce4afea9cdccbb78e24d468cb610a839d5a698853e14e2a3f9ecf"}, + {file = "pyzmq-26.0.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f2133de5ba9adc5f481884ccb699eac9ce789708292945c05746880f95b241c0"}, + {file = "pyzmq-26.0.2-cp312-cp312-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7753c67c570d7fc80c2dc59b90ca1196f1224e0e2e29a548980c95fe0fe27fc1"}, + {file = "pyzmq-26.0.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8d4e51632e6b12e65e8d9d7612446ecda2eda637a868afa7bce16270194650dd"}, + {file = "pyzmq-26.0.2-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:d6c38806f6ecd0acf3104b8d7e76a206bcf56dadd6ce03720d2fa9d9157d5718"}, + {file = "pyzmq-26.0.2-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:48f496bbe14686b51cec15406323ae6942851e14022efd7fc0e2ecd092c5982c"}, + {file = "pyzmq-26.0.2-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:e84a3161149c75bb7a7dc8646384186c34033e286a67fec1ad1bdedea165e7f4"}, + {file = "pyzmq-26.0.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:dabf796c67aa9f5a4fcc956d47f0d48b5c1ed288d628cf53aa1cf08e88654343"}, + {file = "pyzmq-26.0.2-cp312-cp312-win32.whl", hash = "sha256:3eee4c676af1b109f708d80ef0cf57ecb8aaa5900d1edaf90406aea7e0e20e37"}, + {file = "pyzmq-26.0.2-cp312-cp312-win_amd64.whl", hash = "sha256:26721fec65846b3e4450dad050d67d31b017f97e67f7e0647b5f98aa47f828cf"}, + {file = "pyzmq-26.0.2-cp312-cp312-win_arm64.whl", hash = "sha256:653955c6c233e90de128a1b8e882abc7216f41f44218056bd519969c8c413a15"}, + {file = "pyzmq-26.0.2-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:becd8d8fb068fbb5a52096efd83a2d8e54354383f691781f53a4c26aee944542"}, + {file = "pyzmq-26.0.2-cp37-cp37m-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:7a15e5465e7083c12517209c9dd24722b25e9b63c49a563922922fc03554eb35"}, + {file = "pyzmq-26.0.2-cp37-cp37m-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:e8158ac8616941f874841f9fa0f6d2f1466178c2ff91ea08353fdc19de0d40c2"}, + {file = "pyzmq-26.0.2-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ea2c6a53e28c7066ea7db86fcc0b71d78d01b818bb11d4a4341ec35059885295"}, + {file = "pyzmq-26.0.2-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:bdbc7dab0b0e9c62c97b732899c4242e3282ba803bad668e03650b59b165466e"}, + {file = "pyzmq-26.0.2-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:e74b6d5ef57bb65bf1b4a37453d8d86d88550dde3fb0f23b1f1a24e60c70af5b"}, + {file = "pyzmq-26.0.2-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:ed4c6ee624ecbc77b18aeeb07bf0700d26571ab95b8f723f0d02e056b5bce438"}, + {file = "pyzmq-26.0.2-cp37-cp37m-win32.whl", hash = "sha256:8a98b3cb0484b83c19d8fb5524c8a469cd9f10e743f5904ac285d92678ee761f"}, + {file = "pyzmq-26.0.2-cp37-cp37m-win_amd64.whl", hash = "sha256:aa5f95d71b6eca9cec28aa0a2f8310ea53dea313b63db74932879ff860c1fb8d"}, + {file = "pyzmq-26.0.2-cp38-cp38-macosx_10_15_universal2.whl", hash = "sha256:5ff56c76ce77b9805378a7a73032c17cbdb1a5b84faa1df03c5d3e306e5616df"}, + {file = "pyzmq-26.0.2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:bab697fc1574fee4b81da955678708567c43c813c84c91074e452bda5346c921"}, + {file = "pyzmq-26.0.2-cp38-cp38-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:0c0fed8aa9ba0488ee1cbdaa304deea92d52fab43d373297002cfcc69c0a20c5"}, + {file = "pyzmq-26.0.2-cp38-cp38-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:606b922699fcec472ed814dda4dc3ff7c748254e0b26762a0ba21a726eb1c107"}, + {file = "pyzmq-26.0.2-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:45f0fd82bad4d199fa993fbf0ac586a7ac5879addbe436a35a389df7e0eb4c91"}, + {file = "pyzmq-26.0.2-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:166c5e41045939a52c01e6f374e493d9a6a45dfe677360d3e7026e38c42e8906"}, + {file = "pyzmq-26.0.2-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:d566e859e8b8d5bca08467c093061774924b3d78a5ba290e82735b2569edc84b"}, + {file = "pyzmq-26.0.2-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:264ee0e72b72ca59279dc320deab5ae0fac0d97881aed1875ce4bde2e56ffde0"}, + {file = "pyzmq-26.0.2-cp38-cp38-win32.whl", hash = "sha256:3152bbd3a4744cbdd83dfb210ed701838b8b0c9065cef14671d6d91df12197d0"}, + {file = "pyzmq-26.0.2-cp38-cp38-win_amd64.whl", hash = "sha256:bf77601d75ca692c179154b7e5943c286a4aaffec02c491afe05e60493ce95f2"}, + {file = "pyzmq-26.0.2-cp39-cp39-macosx_10_15_universal2.whl", hash = "sha256:c770a7545b3deca2db185b59175e710a820dd4ed43619f4c02e90b0e227c6252"}, + {file = "pyzmq-26.0.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:d47175f0a380bfd051726bc5c0054036ae4a5d8caf922c62c8a172ccd95c1a2a"}, + {file = "pyzmq-26.0.2-cp39-cp39-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:9bce298c1ce077837e110367c321285dc4246b531cde1abfc27e4a5bbe2bed4d"}, + {file = "pyzmq-26.0.2-cp39-cp39-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:c40b09b7e184d6e3e1be1c8af2cc320c0f9f610d8a5df3dd866e6e6e4e32b235"}, + {file = "pyzmq-26.0.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d420d856bf728713874cefb911398efe69e1577835851dd297a308a78c14c249"}, + {file = "pyzmq-26.0.2-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:d792d3cab987058451e55c70c5926e93e2ceb68ca5a2334863bb903eb860c9cb"}, + {file = "pyzmq-26.0.2-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:83ec17729cf6d3464dab98a11e98294fcd50e6b17eaabd3d841515c23f6dbd3a"}, + {file = "pyzmq-26.0.2-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:47c17d5ebfa88ae90f08960c97b49917098665b8cd8be31f2c24e177bcf37a0f"}, + {file = "pyzmq-26.0.2-cp39-cp39-win32.whl", hash = "sha256:d509685d1cd1d018705a811c5f9d5bc237790936ead6d06f6558b77e16cc7235"}, + {file = "pyzmq-26.0.2-cp39-cp39-win_amd64.whl", hash = "sha256:c7cc8cc009e8f6989a6d86c96f87dae5f5fb07d6c96916cdc7719d546152c7db"}, + {file = "pyzmq-26.0.2-cp39-cp39-win_arm64.whl", hash = "sha256:3ada31cb879cd7532f4a85b501f4255c747d4813ab76b35c49ed510ce4865b45"}, + {file = "pyzmq-26.0.2-pp310-pypy310_pp73-macosx_10_9_x86_64.whl", hash = "sha256:0a6ceaddc830dd3ca86cb8451cf373d1f05215368e11834538c2902ed5205139"}, + {file = "pyzmq-26.0.2-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6a967681463aa7a99eb9a62bb18229b653b45c10ff0947b31cc0837a83dfb86f"}, + {file = "pyzmq-26.0.2-pp310-pypy310_pp73-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6472a73bc115bc40a2076609a90894775abe6faf19a78375675a2f889a613071"}, + {file = "pyzmq-26.0.2-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5d6aea92bcccfe5e5524d3c70a6f16ffdae548390ddad26f4207d55c55a40593"}, + {file = "pyzmq-26.0.2-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:e025f6351e49d48a5aa2f5a09293aa769b0ee7369c25bed551647234b7fa0c75"}, + {file = "pyzmq-26.0.2-pp37-pypy37_pp73-macosx_10_9_x86_64.whl", hash = "sha256:40bd7ebe4dbb37d27f0c56e2a844f360239343a99be422085e13e97da13f73f9"}, + {file = "pyzmq-26.0.2-pp37-pypy37_pp73-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:1dd40d586ad6f53764104df6e01810fe1b4e88fd353774629a5e6fe253813f79"}, + {file = "pyzmq-26.0.2-pp37-pypy37_pp73-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:f2aca15e9ad8c8657b5b3d7ae3d1724dc8c1c1059c06b4b674c3aa36305f4930"}, + {file = "pyzmq-26.0.2-pp37-pypy37_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:450ec234736732eb0ebeffdb95a352450d4592f12c3e087e2a9183386d22c8bf"}, + {file = "pyzmq-26.0.2-pp37-pypy37_pp73-win_amd64.whl", hash = "sha256:f43be2bebbd09360a2f23af83b243dc25ffe7b583ea8c722e6df03e03a55f02f"}, + {file = "pyzmq-26.0.2-pp38-pypy38_pp73-macosx_10_9_x86_64.whl", hash = "sha256:867f55e54aff254940bcec5eec068e7c0ac1e6bf360ab91479394a8bf356b0e6"}, + {file = "pyzmq-26.0.2-pp38-pypy38_pp73-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:b4dbc033c5ad46f8c429bf238c25a889b8c1d86bfe23a74e1031a991cb3f0000"}, + {file = "pyzmq-26.0.2-pp38-pypy38_pp73-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:6e8dd2961462e337e21092ec2da0c69d814dcb1b6e892955a37444a425e9cfb8"}, + {file = "pyzmq-26.0.2-pp38-pypy38_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:35391e72df6c14a09b697c7b94384947c1dd326aca883ff98ff137acdf586c33"}, + {file = "pyzmq-26.0.2-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:1c3d3c92fa54eda94ab369ca5b8d35059987c326ba5e55326eb068862f64b1fc"}, + {file = "pyzmq-26.0.2-pp39-pypy39_pp73-macosx_10_9_x86_64.whl", hash = "sha256:e7aa61a9cc4f0523373e31fc9255bf4567185a099f85ca3598e64de484da3ab2"}, + {file = "pyzmq-26.0.2-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ee53a8191271f144cc20b12c19daa9f1546adc84a2f33839e3338039b55c373c"}, + {file = "pyzmq-26.0.2-pp39-pypy39_pp73-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ac60a980f07fa988983f7bfe6404ef3f1e4303f5288a01713bc1266df6d18783"}, + {file = "pyzmq-26.0.2-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:88896b1b4817d7b2fe1ec7205c4bbe07bf5d92fb249bf2d226ddea8761996068"}, + {file = "pyzmq-26.0.2-pp39-pypy39_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:18dfffe23751edee917764ffa133d5d3fef28dfd1cf3adebef8c90bc854c74c4"}, + {file = "pyzmq-26.0.2-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:6926dd14cfe6967d3322640b6d5c3c3039db71716a5e43cca6e3b474e73e0b36"}, + {file = "pyzmq-26.0.2.tar.gz", hash = "sha256:f0f9bb370449158359bb72a3e12c658327670c0ffe6fbcd1af083152b64f9df0"}, ] [package.dependencies] @@ -4763,13 +4819,13 @@ six = "*" [[package]] name = "referencing" -version = "0.33.0" +version = "0.35.0" description = "JSON Referencing + Python" optional = false python-versions = ">=3.8" files = [ - {file = "referencing-0.33.0-py3-none-any.whl", hash = "sha256:39240f2ecc770258f28b642dd47fd74bc8b02484de54e1882b74b35ebd779bd5"}, - {file = "referencing-0.33.0.tar.gz", hash = "sha256:c775fedf74bc0f9189c2a3be1c12fd03e8c23f4d371dce795df44e06c5b412f7"}, + {file = "referencing-0.35.0-py3-none-any.whl", hash = "sha256:8080727b30e364e5783152903672df9b6b091c926a146a759080b62ca3126cd6"}, + {file = "referencing-0.35.0.tar.gz", hash = "sha256:191e936b0c696d0af17ad7430a3dc68e88bc11be6514f4757dc890f04ab05889"}, ] [package.dependencies] @@ -4799,13 +4855,13 @@ use-chardet-on-py3 = ["chardet (>=3.0.2,<6)"] [[package]] name = "requests-oauthlib" -version = "1.4.0" +version = "2.0.0" description = "OAuthlib authentication support for Requests." optional = false -python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" +python-versions = ">=3.4" files = [ - {file = "requests-oauthlib-1.4.0.tar.gz", hash = "sha256:acee623221e4a39abcbb919312c8ff04bd44e7e417087fb4bd5e2a2f53d5e79a"}, - {file = "requests_oauthlib-1.4.0-py2.py3-none-any.whl", hash = "sha256:7a3130d94a17520169e38db6c8d75f2c974643788465ecc2e4b36d288bf13033"}, + {file = "requests-oauthlib-2.0.0.tar.gz", hash = "sha256:b3dffaebd884d8cd778494369603a9e7b58d29111bf6b41bdc2dcd87203af4e9"}, + {file = "requests_oauthlib-2.0.0-py2.py3-none-any.whl", hash = "sha256:7dd8a5c40426b779b0868c404bdef9768deccf22749cde15852df527e6269b36"}, ] [package.dependencies] @@ -4968,37 +5024,37 @@ files = [ [[package]] name = "scikit-learn" -version = "1.4.1.post1" +version = "1.4.2" description = "A set of python modules for machine learning and data mining" optional = false python-versions = ">=3.9" files = [ - {file = "scikit-learn-1.4.1.post1.tar.gz", hash = "sha256:93d3d496ff1965470f9977d05e5ec3376fb1e63b10e4fda5e39d23c2d8969a30"}, - {file = "scikit_learn-1.4.1.post1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:c540aaf44729ab5cd4bd5e394f2b375e65ceaea9cdd8c195788e70433d91bbc5"}, - {file = "scikit_learn-1.4.1.post1-cp310-cp310-macosx_12_0_arm64.whl", hash = "sha256:4310bff71aa98b45b46cd26fa641309deb73a5d1c0461d181587ad4f30ea3c36"}, - {file = "scikit_learn-1.4.1.post1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9f43dd527dabff5521af2786a2f8de5ba381e182ec7292663508901cf6ceaf6e"}, - {file = "scikit_learn-1.4.1.post1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c02e27d65b0c7dc32f2c5eb601aaf5530b7a02bfbe92438188624524878336f2"}, - {file = "scikit_learn-1.4.1.post1-cp310-cp310-win_amd64.whl", hash = "sha256:629e09f772ad42f657ca60a1a52342eef786218dd20cf1369a3b8d085e55ef8f"}, - {file = "scikit_learn-1.4.1.post1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:6145dfd9605b0b50ae72cdf72b61a2acd87501369a763b0d73d004710ebb76b5"}, - {file = "scikit_learn-1.4.1.post1-cp311-cp311-macosx_12_0_arm64.whl", hash = "sha256:1afed6951bc9d2053c6ee9a518a466cbc9b07c6a3f9d43bfe734192b6125d508"}, - {file = "scikit_learn-1.4.1.post1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ce03506ccf5f96b7e9030fea7eb148999b254c44c10182ac55857bc9b5d4815f"}, - {file = "scikit_learn-1.4.1.post1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4ba516fcdc73d60e7f48cbb0bccb9acbdb21807de3651531208aac73c758e3ab"}, - {file = "scikit_learn-1.4.1.post1-cp311-cp311-win_amd64.whl", hash = "sha256:78cd27b4669513b50db4f683ef41ea35b5dddc797bd2bbd990d49897fd1c8a46"}, - {file = "scikit_learn-1.4.1.post1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:a1e289f33f613cefe6707dead50db31930530dc386b6ccff176c786335a7b01c"}, - {file = "scikit_learn-1.4.1.post1-cp312-cp312-macosx_12_0_arm64.whl", hash = "sha256:0df87de9ce1c0140f2818beef310fb2e2afdc1e66fc9ad587965577f17733649"}, - {file = "scikit_learn-1.4.1.post1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:712c1c69c45b58ef21635360b3d0a680ff7d83ac95b6f9b82cf9294070cda710"}, - {file = "scikit_learn-1.4.1.post1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1754b0c2409d6ed5a3380512d0adcf182a01363c669033a2b55cca429ed86a81"}, - {file = "scikit_learn-1.4.1.post1-cp312-cp312-win_amd64.whl", hash = "sha256:1d491ef66e37f4e812db7e6c8286520c2c3fc61b34bf5e59b67b4ce528de93af"}, - {file = "scikit_learn-1.4.1.post1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:aa0029b78ef59af22cfbd833e8ace8526e4df90212db7ceccbea582ebb5d6794"}, - {file = "scikit_learn-1.4.1.post1-cp39-cp39-macosx_12_0_arm64.whl", hash = "sha256:14e4c88436ac96bf69eb6d746ac76a574c314a23c6961b7d344b38877f20fee1"}, - {file = "scikit_learn-1.4.1.post1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d7cd3a77c32879311f2aa93466d3c288c955ef71d191503cf0677c3340ae8ae0"}, - {file = "scikit_learn-1.4.1.post1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2a3ee19211ded1a52ee37b0a7b373a8bfc66f95353af058a210b692bd4cda0dd"}, - {file = "scikit_learn-1.4.1.post1-cp39-cp39-win_amd64.whl", hash = "sha256:234b6bda70fdcae9e4abbbe028582ce99c280458665a155eed0b820599377d25"}, + {file = "scikit-learn-1.4.2.tar.gz", hash = "sha256:daa1c471d95bad080c6e44b4946c9390a4842adc3082572c20e4f8884e39e959"}, + {file = "scikit_learn-1.4.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:8539a41b3d6d1af82eb629f9c57f37428ff1481c1e34dddb3b9d7af8ede67ac5"}, + {file = "scikit_learn-1.4.2-cp310-cp310-macosx_12_0_arm64.whl", hash = "sha256:68b8404841f944a4a1459b07198fa2edd41a82f189b44f3e1d55c104dbc2e40c"}, + {file = "scikit_learn-1.4.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:81bf5d8bbe87643103334032dd82f7419bc8c8d02a763643a6b9a5c7288c5054"}, + {file = "scikit_learn-1.4.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:36f0ea5d0f693cb247a073d21a4123bdf4172e470e6d163c12b74cbb1536cf38"}, + {file = "scikit_learn-1.4.2-cp310-cp310-win_amd64.whl", hash = "sha256:87440e2e188c87db80ea4023440923dccbd56fbc2d557b18ced00fef79da0727"}, + {file = "scikit_learn-1.4.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:45dee87ac5309bb82e3ea633955030df9bbcb8d2cdb30383c6cd483691c546cc"}, + {file = "scikit_learn-1.4.2-cp311-cp311-macosx_12_0_arm64.whl", hash = "sha256:1d0b25d9c651fd050555aadd57431b53d4cf664e749069da77f3d52c5ad14b3b"}, + {file = "scikit_learn-1.4.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b0203c368058ab92efc6168a1507d388d41469c873e96ec220ca8e74079bf62e"}, + {file = "scikit_learn-1.4.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:44c62f2b124848a28fd695db5bc4da019287abf390bfce602ddc8aa1ec186aae"}, + {file = "scikit_learn-1.4.2-cp311-cp311-win_amd64.whl", hash = "sha256:5cd7b524115499b18b63f0c96f4224eb885564937a0b3477531b2b63ce331904"}, + {file = "scikit_learn-1.4.2-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:90378e1747949f90c8f385898fff35d73193dfcaec3dd75d6b542f90c4e89755"}, + {file = "scikit_learn-1.4.2-cp312-cp312-macosx_12_0_arm64.whl", hash = "sha256:ff4effe5a1d4e8fed260a83a163f7dbf4f6087b54528d8880bab1d1377bd78be"}, + {file = "scikit_learn-1.4.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:671e2f0c3f2c15409dae4f282a3a619601fa824d2c820e5b608d9d775f91780c"}, + {file = "scikit_learn-1.4.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d36d0bc983336bbc1be22f9b686b50c964f593c8a9a913a792442af9bf4f5e68"}, + {file = "scikit_learn-1.4.2-cp312-cp312-win_amd64.whl", hash = "sha256:d762070980c17ba3e9a4a1e043ba0518ce4c55152032f1af0ca6f39b376b5928"}, + {file = "scikit_learn-1.4.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:d9993d5e78a8148b1d0fdf5b15ed92452af5581734129998c26f481c46586d68"}, + {file = "scikit_learn-1.4.2-cp39-cp39-macosx_12_0_arm64.whl", hash = "sha256:426d258fddac674fdf33f3cb2d54d26f49406e2599dbf9a32b4d1696091d4256"}, + {file = "scikit_learn-1.4.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5460a1a5b043ae5ae4596b3126a4ec33ccba1b51e7ca2c5d36dac2169f62ab1d"}, + {file = "scikit_learn-1.4.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:49d64ef6cb8c093d883e5a36c4766548d974898d378e395ba41a806d0e824db8"}, + {file = "scikit_learn-1.4.2-cp39-cp39-win_amd64.whl", hash = "sha256:c97a50b05c194be9146d61fe87dbf8eac62b203d9e87a3ccc6ae9aed2dfaf361"}, ] [package.dependencies] joblib = ">=1.2.0" -numpy = ">=1.19.5,<2.0" +numpy = ">=1.19.5" scipy = ">=1.6.0" threadpoolctl = ">=2.0.0" @@ -5010,45 +5066,45 @@ tests = ["black (>=23.3.0)", "matplotlib (>=3.3.4)", "mypy (>=1.3)", "numpydoc ( [[package]] name = "scipy" -version = "1.12.0" +version = "1.13.0" description = "Fundamental algorithms for scientific computing in Python" optional = false python-versions = ">=3.9" files = [ - {file = "scipy-1.12.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:78e4402e140879387187f7f25d91cc592b3501a2e51dfb320f48dfb73565f10b"}, - {file = "scipy-1.12.0-cp310-cp310-macosx_12_0_arm64.whl", hash = "sha256:f5f00ebaf8de24d14b8449981a2842d404152774c1a1d880c901bf454cb8e2a1"}, - {file = "scipy-1.12.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e53958531a7c695ff66c2e7bb7b79560ffdc562e2051644c5576c39ff8efb563"}, - {file = "scipy-1.12.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5e32847e08da8d895ce09d108a494d9eb78974cf6de23063f93306a3e419960c"}, - {file = "scipy-1.12.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:4c1020cad92772bf44b8e4cdabc1df5d87376cb219742549ef69fc9fd86282dd"}, - {file = "scipy-1.12.0-cp310-cp310-win_amd64.whl", hash = "sha256:75ea2a144096b5e39402e2ff53a36fecfd3b960d786b7efd3c180e29c39e53f2"}, - {file = "scipy-1.12.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:408c68423f9de16cb9e602528be4ce0d6312b05001f3de61fe9ec8b1263cad08"}, - {file = "scipy-1.12.0-cp311-cp311-macosx_12_0_arm64.whl", hash = "sha256:5adfad5dbf0163397beb4aca679187d24aec085343755fcdbdeb32b3679f254c"}, - {file = "scipy-1.12.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c3003652496f6e7c387b1cf63f4bb720951cfa18907e998ea551e6de51a04467"}, - {file = "scipy-1.12.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8b8066bce124ee5531d12a74b617d9ac0ea59245246410e19bca549656d9a40a"}, - {file = "scipy-1.12.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:8bee4993817e204d761dba10dbab0774ba5a8612e57e81319ea04d84945375ba"}, - {file = "scipy-1.12.0-cp311-cp311-win_amd64.whl", hash = "sha256:a24024d45ce9a675c1fb8494e8e5244efea1c7a09c60beb1eeb80373d0fecc70"}, - {file = "scipy-1.12.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:e7e76cc48638228212c747ada851ef355c2bb5e7f939e10952bc504c11f4e372"}, - {file = "scipy-1.12.0-cp312-cp312-macosx_12_0_arm64.whl", hash = "sha256:f7ce148dffcd64ade37b2df9315541f9adad6efcaa86866ee7dd5db0c8f041c3"}, - {file = "scipy-1.12.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9c39f92041f490422924dfdb782527a4abddf4707616e07b021de33467f917bc"}, - {file = "scipy-1.12.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a7ebda398f86e56178c2fa94cad15bf457a218a54a35c2a7b4490b9f9cb2676c"}, - {file = "scipy-1.12.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:95e5c750d55cf518c398a8240571b0e0782c2d5a703250872f36eaf737751338"}, - {file = "scipy-1.12.0-cp312-cp312-win_amd64.whl", hash = "sha256:e646d8571804a304e1da01040d21577685ce8e2db08ac58e543eaca063453e1c"}, - {file = "scipy-1.12.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:913d6e7956c3a671de3b05ccb66b11bc293f56bfdef040583a7221d9e22a2e35"}, - {file = "scipy-1.12.0-cp39-cp39-macosx_12_0_arm64.whl", hash = "sha256:bba1b0c7256ad75401c73e4b3cf09d1f176e9bd4248f0d3112170fb2ec4db067"}, - {file = "scipy-1.12.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:730badef9b827b368f351eacae2e82da414e13cf8bd5051b4bdfd720271a5371"}, - {file = "scipy-1.12.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6546dc2c11a9df6926afcbdd8a3edec28566e4e785b915e849348c6dd9f3f490"}, - {file = "scipy-1.12.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:196ebad3a4882081f62a5bf4aeb7326aa34b110e533aab23e4374fcccb0890dc"}, - {file = "scipy-1.12.0-cp39-cp39-win_amd64.whl", hash = "sha256:b360f1b6b2f742781299514e99ff560d1fe9bd1bff2712894b52abe528d1fd1e"}, - {file = "scipy-1.12.0.tar.gz", hash = "sha256:4bf5abab8a36d20193c698b0f1fc282c1d083c94723902c447e5d2f1780936a3"}, -] - -[package.dependencies] -numpy = ">=1.22.4,<1.29.0" - -[package.extras] -dev = ["click", "cython-lint (>=0.12.2)", "doit (>=0.36.0)", "mypy", "pycodestyle", "pydevtool", "rich-click", "ruff", "types-psutil", "typing_extensions"] -doc = ["jupytext", "matplotlib (>2)", "myst-nb", "numpydoc", "pooch", "pydata-sphinx-theme (==0.9.0)", "sphinx (!=4.1.0)", "sphinx-design (>=0.2.0)"] -test = ["asv", "gmpy2", "hypothesis", "mpmath", "pooch", "pytest", "pytest-cov", "pytest-timeout", "pytest-xdist", "scikit-umfpack", "threadpoolctl"] + {file = "scipy-1.13.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:ba419578ab343a4e0a77c0ef82f088238a93eef141b2b8017e46149776dfad4d"}, + {file = "scipy-1.13.0-cp310-cp310-macosx_12_0_arm64.whl", hash = "sha256:22789b56a999265431c417d462e5b7f2b487e831ca7bef5edeb56efe4c93f86e"}, + {file = "scipy-1.13.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:05f1432ba070e90d42d7fd836462c50bf98bd08bed0aa616c359eed8a04e3922"}, + {file = "scipy-1.13.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b8434f6f3fa49f631fae84afee424e2483289dfc30a47755b4b4e6b07b2633a4"}, + {file = "scipy-1.13.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:dcbb9ea49b0167de4167c40eeee6e167caeef11effb0670b554d10b1e693a8b9"}, + {file = "scipy-1.13.0-cp310-cp310-win_amd64.whl", hash = "sha256:1d2f7bb14c178f8b13ebae93f67e42b0a6b0fc50eba1cd8021c9b6e08e8fb1cd"}, + {file = "scipy-1.13.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:0fbcf8abaf5aa2dc8d6400566c1a727aed338b5fe880cde64907596a89d576fa"}, + {file = "scipy-1.13.0-cp311-cp311-macosx_12_0_arm64.whl", hash = "sha256:5e4a756355522eb60fcd61f8372ac2549073c8788f6114449b37e9e8104f15a5"}, + {file = "scipy-1.13.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b5acd8e1dbd8dbe38d0004b1497019b2dbbc3d70691e65d69615f8a7292865d7"}, + {file = "scipy-1.13.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9ff7dad5d24a8045d836671e082a490848e8639cabb3dbdacb29f943a678683d"}, + {file = "scipy-1.13.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:4dca18c3ffee287ddd3bc8f1dabaf45f5305c5afc9f8ab9cbfab855e70b2df5c"}, + {file = "scipy-1.13.0-cp311-cp311-win_amd64.whl", hash = "sha256:a2f471de4d01200718b2b8927f7d76b5d9bde18047ea0fa8bd15c5ba3f26a1d6"}, + {file = "scipy-1.13.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:d0de696f589681c2802f9090fff730c218f7c51ff49bf252b6a97ec4a5d19e8b"}, + {file = "scipy-1.13.0-cp312-cp312-macosx_12_0_arm64.whl", hash = "sha256:b2a3ff461ec4756b7e8e42e1c681077349a038f0686132d623fa404c0bee2551"}, + {file = "scipy-1.13.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6bf9fe63e7a4bf01d3645b13ff2aa6dea023d38993f42aaac81a18b1bda7a82a"}, + {file = "scipy-1.13.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1e7626dfd91cdea5714f343ce1176b6c4745155d234f1033584154f60ef1ff42"}, + {file = "scipy-1.13.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:109d391d720fcebf2fbe008621952b08e52907cf4c8c7efc7376822151820820"}, + {file = "scipy-1.13.0-cp312-cp312-win_amd64.whl", hash = "sha256:8930ae3ea371d6b91c203b1032b9600d69c568e537b7988a3073dfe4d4774f21"}, + {file = "scipy-1.13.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:5407708195cb38d70fd2d6bb04b1b9dd5c92297d86e9f9daae1576bd9e06f602"}, + {file = "scipy-1.13.0-cp39-cp39-macosx_12_0_arm64.whl", hash = "sha256:ac38c4c92951ac0f729c4c48c9e13eb3675d9986cc0c83943784d7390d540c78"}, + {file = "scipy-1.13.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:09c74543c4fbeb67af6ce457f6a6a28e5d3739a87f62412e4a16e46f164f0ae5"}, + {file = "scipy-1.13.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:28e286bf9ac422d6beb559bc61312c348ca9b0f0dae0d7c5afde7f722d6ea13d"}, + {file = "scipy-1.13.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:33fde20efc380bd23a78a4d26d59fc8704e9b5fd9b08841693eb46716ba13d86"}, + {file = "scipy-1.13.0-cp39-cp39-win_amd64.whl", hash = "sha256:45c08bec71d3546d606989ba6e7daa6f0992918171e2a6f7fbedfa7361c2de1e"}, + {file = "scipy-1.13.0.tar.gz", hash = "sha256:58569af537ea29d3f78e5abd18398459f195546bb3be23d16677fb26616cc11e"}, +] + +[package.dependencies] +numpy = ">=1.22.4,<2.3" + +[package.extras] +dev = ["cython-lint (>=0.12.2)", "doit (>=0.36.0)", "mypy", "pycodestyle", "pydevtool", "rich-click", "ruff", "types-psutil", "typing_extensions"] +doc = ["jupyterlite-pyodide-kernel", "jupyterlite-sphinx (>=0.12.0)", "jupytext", "matplotlib (>=3.5)", "myst-nb", "numpydoc", "pooch", "pydata-sphinx-theme (>=0.15.2)", "sphinx (>=5.0.0)", "sphinx-design (>=0.4.0)"] +test = ["array-api-strict", "asv", "gmpy2", "hypothesis (>=6.30)", "mpmath", "pooch", "pytest", "pytest-cov", "pytest-timeout", "pytest-xdist", "scikit-umfpack", "threadpoolctl"] [[package]] name = "seaborn" @@ -5073,13 +5129,13 @@ stats = ["scipy (>=1.3)", "statsmodels (>=0.10)"] [[package]] name = "send2trash" -version = "1.8.2" +version = "1.8.3" description = "Send file to trash natively under Mac OS X, Windows and Linux" optional = false -python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,>=2.7" +python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,>=2.7" files = [ - {file = "Send2Trash-1.8.2-py3-none-any.whl", hash = "sha256:a384719d99c07ce1eefd6905d2decb6f8b7ed054025bb0e618919f945de4f679"}, - {file = "Send2Trash-1.8.2.tar.gz", hash = "sha256:c132d59fa44b9ca2b1699af5c86f57ce9f4c5eb56629d5d55fbb7a35f84e2312"}, + {file = "Send2Trash-1.8.3-py3-none-any.whl", hash = "sha256:0c31227e0bd08961c7665474a3d1ef7193929fedda4233843689baa056be46c9"}, + {file = "Send2Trash-1.8.3.tar.gz", hash = "sha256:b18e7a3966d99871aefeb00cfbcfdced55ce4871194810fc71f4aa484b953abf"}, ] [package.extras] @@ -5089,18 +5145,18 @@ win32 = ["pywin32"] [[package]] name = "setuptools" -version = "69.2.0" +version = "69.5.1" description = "Easily download, build, install, upgrade, and uninstall Python packages" optional = false python-versions = ">=3.8" files = [ - {file = "setuptools-69.2.0-py3-none-any.whl", hash = "sha256:c21c49fb1042386df081cb5d86759792ab89efca84cf114889191cd09aacc80c"}, - {file = "setuptools-69.2.0.tar.gz", hash = "sha256:0ff4183f8f42cd8fa3acea16c45205521a4ef28f73c6391d8a25e92893134f2e"}, + {file = "setuptools-69.5.1-py3-none-any.whl", hash = "sha256:c636ac361bc47580504644275c9ad802c50415c7522212252c033bd15f301f32"}, + {file = "setuptools-69.5.1.tar.gz", hash = "sha256:6c1fccdac05a97e598fb0ae3bbed5904ccb317337a51139dcd51453611bbb987"}, ] [package.extras] -docs = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "pygments-github-lexers (==0.0.5)", "rst.linker (>=1.9)", "sphinx (<7.2.5)", "sphinx (>=3.5)", "sphinx-favicon", "sphinx-inline-tabs", "sphinx-lint", "sphinx-notfound-page (>=1,<2)", "sphinx-reredirects", "sphinxcontrib-towncrier"] -testing = ["build[virtualenv]", "filelock (>=3.4.0)", "importlib-metadata", "ini2toml[lite] (>=0.9)", "jaraco.develop (>=7.21)", "jaraco.envs (>=2.2)", "jaraco.path (>=3.2.0)", "mypy (==1.9)", "packaging (>=23.2)", "pip (>=19.1)", "pytest (>=6)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=2.2)", "pytest-home (>=0.5)", "pytest-mypy (>=0.9.1)", "pytest-perf", "pytest-ruff (>=0.2.1)", "pytest-timeout", "pytest-xdist (>=3)", "tomli", "tomli-w (>=1.0.0)", "virtualenv (>=13.0.0)", "wheel"] +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-inline-tabs", "sphinx-lint", "sphinx-notfound-page (>=1,<2)", "sphinx-reredirects", "sphinxcontrib-towncrier"] +testing = ["build[virtualenv]", "filelock (>=3.4.0)", "importlib-metadata", "ini2toml[lite] (>=0.9)", "jaraco.develop (>=7.21)", "jaraco.envs (>=2.2)", "jaraco.path (>=3.2.0)", "mypy (==1.9)", "packaging (>=23.2)", "pip (>=19.1)", "pytest (>=6,!=8.1.1)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=2.2)", "pytest-home (>=0.5)", "pytest-mypy", "pytest-perf", "pytest-ruff (>=0.2.1)", "pytest-timeout", "pytest-xdist (>=3)", "tomli", "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.2)", "pytest", "pytest-enabler", "pytest-xdist", "tomli", "virtualenv (>=13.0.0)", "wheel"] [[package]] @@ -5160,20 +5216,20 @@ files = [ [[package]] name = "sphinx" -version = "7.2.6" +version = "7.3.7" description = "Python documentation generator" optional = false python-versions = ">=3.9" files = [ - {file = "sphinx-7.2.6-py3-none-any.whl", hash = "sha256:1e09160a40b956dc623c910118fa636da93bd3ca0b9876a7b3df90f07d691560"}, - {file = "sphinx-7.2.6.tar.gz", hash = "sha256:9a5160e1ea90688d5963ba09a2dcd8bdd526620edbb65c328728f1b2228d5ab5"}, + {file = "sphinx-7.3.7-py3-none-any.whl", hash = "sha256:413f75440be4cacf328f580b4274ada4565fb2187d696a84970c23f77b64d8c3"}, + {file = "sphinx-7.3.7.tar.gz", hash = "sha256:a4a7db75ed37531c05002d56ed6948d4c42f473a36f46e1382b0bd76ca9627bc"}, ] [package.dependencies] -alabaster = ">=0.7,<0.8" +alabaster = ">=0.7.14,<0.8.0" babel = ">=2.9" colorama = {version = ">=0.4.5", markers = "sys_platform == \"win32\""} -docutils = ">=0.18.1,<0.21" +docutils = ">=0.18.1,<0.22" imagesize = ">=1.3" Jinja2 = ">=3.0" packaging = ">=21.0" @@ -5186,11 +5242,12 @@ sphinxcontrib-htmlhelp = ">=2.0.0" sphinxcontrib-jsmath = "*" sphinxcontrib-qthelp = "*" sphinxcontrib-serializinghtml = ">=1.1.9" +tomli = {version = ">=2", markers = "python_version < \"3.11\""} [package.extras] docs = ["sphinxcontrib-websupport"] -lint = ["docutils-stubs", "flake8 (>=3.5.0)", "flake8-simplify", "isort", "mypy (>=0.990)", "ruff", "sphinx-lint", "types-requests"] -test = ["cython (>=3.0)", "filelock", "html5lib", "pytest (>=4.6)", "setuptools (>=67.0)"] +lint = ["flake8 (>=3.5.0)", "importlib_metadata", "mypy (==1.9.0)", "pytest (>=6.0)", "ruff (==0.3.7)", "sphinx-lint", "tomli", "types-docutils", "types-requests"] +test = ["cython (>=3.0)", "defusedxml (>=0.7.1)", "pytest (>=6.0)", "setuptools (>=67.0)"] [[package]] name = "sphinx-autoapi" @@ -5369,60 +5426,60 @@ test = ["pytest"] [[package]] name = "sqlalchemy" -version = "2.0.28" +version = "2.0.29" description = "Database Abstraction Library" optional = false python-versions = ">=3.7" files = [ - {file = "SQLAlchemy-2.0.28-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:e0b148ab0438f72ad21cb004ce3bdaafd28465c4276af66df3b9ecd2037bf252"}, - {file = "SQLAlchemy-2.0.28-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:bbda76961eb8f27e6ad3c84d1dc56d5bc61ba8f02bd20fcf3450bd421c2fcc9c"}, - {file = "SQLAlchemy-2.0.28-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:feea693c452d85ea0015ebe3bb9cd15b6f49acc1a31c28b3c50f4db0f8fb1e71"}, - {file = "SQLAlchemy-2.0.28-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5da98815f82dce0cb31fd1e873a0cb30934971d15b74e0d78cf21f9e1b05953f"}, - {file = "SQLAlchemy-2.0.28-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:4a5adf383c73f2d49ad15ff363a8748319ff84c371eed59ffd0127355d6ea1da"}, - {file = "SQLAlchemy-2.0.28-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:56856b871146bfead25fbcaed098269d90b744eea5cb32a952df00d542cdd368"}, - {file = "SQLAlchemy-2.0.28-cp310-cp310-win32.whl", hash = "sha256:943aa74a11f5806ab68278284a4ddd282d3fb348a0e96db9b42cb81bf731acdc"}, - {file = "SQLAlchemy-2.0.28-cp310-cp310-win_amd64.whl", hash = "sha256:c6c4da4843e0dabde41b8f2e8147438330924114f541949e6318358a56d1875a"}, - {file = "SQLAlchemy-2.0.28-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:46a3d4e7a472bfff2d28db838669fc437964e8af8df8ee1e4548e92710929adc"}, - {file = "SQLAlchemy-2.0.28-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:0d3dd67b5d69794cfe82862c002512683b3db038b99002171f624712fa71aeaa"}, - {file = "SQLAlchemy-2.0.28-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c61e2e41656a673b777e2f0cbbe545323dbe0d32312f590b1bc09da1de6c2a02"}, - {file = "SQLAlchemy-2.0.28-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0315d9125a38026227f559488fe7f7cee1bd2fbc19f9fd637739dc50bb6380b2"}, - {file = "SQLAlchemy-2.0.28-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:af8ce2d31679006e7b747d30a89cd3ac1ec304c3d4c20973f0f4ad58e2d1c4c9"}, - {file = "SQLAlchemy-2.0.28-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:81ba314a08c7ab701e621b7ad079c0c933c58cdef88593c59b90b996e8b58fa5"}, - {file = "SQLAlchemy-2.0.28-cp311-cp311-win32.whl", hash = "sha256:1ee8bd6d68578e517943f5ebff3afbd93fc65f7ef8f23becab9fa8fb315afb1d"}, - {file = "SQLAlchemy-2.0.28-cp311-cp311-win_amd64.whl", hash = "sha256:ad7acbe95bac70e4e687a4dc9ae3f7a2f467aa6597049eeb6d4a662ecd990bb6"}, - {file = "SQLAlchemy-2.0.28-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:d3499008ddec83127ab286c6f6ec82a34f39c9817f020f75eca96155f9765097"}, - {file = "SQLAlchemy-2.0.28-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:9b66fcd38659cab5d29e8de5409cdf91e9986817703e1078b2fdaad731ea66f5"}, - {file = "SQLAlchemy-2.0.28-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bea30da1e76cb1acc5b72e204a920a3a7678d9d52f688f087dc08e54e2754c67"}, - {file = "SQLAlchemy-2.0.28-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:124202b4e0edea7f08a4db8c81cc7859012f90a0d14ba2bf07c099aff6e96462"}, - {file = "SQLAlchemy-2.0.28-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:e23b88c69497a6322b5796c0781400692eca1ae5532821b39ce81a48c395aae9"}, - {file = "SQLAlchemy-2.0.28-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:4b6303bfd78fb3221847723104d152e5972c22367ff66edf09120fcde5ddc2e2"}, - {file = "SQLAlchemy-2.0.28-cp312-cp312-win32.whl", hash = "sha256:a921002be69ac3ab2cf0c3017c4e6a3377f800f1fca7f254c13b5f1a2f10022c"}, - {file = "SQLAlchemy-2.0.28-cp312-cp312-win_amd64.whl", hash = "sha256:b4a2cf92995635b64876dc141af0ef089c6eea7e05898d8d8865e71a326c0385"}, - {file = "SQLAlchemy-2.0.28-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:8e91b5e341f8c7f1e5020db8e5602f3ed045a29f8e27f7f565e0bdee3338f2c7"}, - {file = "SQLAlchemy-2.0.28-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:45c7b78dfc7278329f27be02c44abc0d69fe235495bb8e16ec7ef1b1a17952db"}, - {file = "SQLAlchemy-2.0.28-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3eba73ef2c30695cb7eabcdb33bb3d0b878595737479e152468f3ba97a9c22a4"}, - {file = "SQLAlchemy-2.0.28-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:5df5d1dafb8eee89384fb7a1f79128118bc0ba50ce0db27a40750f6f91aa99d5"}, - {file = "SQLAlchemy-2.0.28-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:2858bbab1681ee5406650202950dc8f00e83b06a198741b7c656e63818633526"}, - {file = "SQLAlchemy-2.0.28-cp37-cp37m-win32.whl", hash = "sha256:9461802f2e965de5cff80c5a13bc945abea7edaa1d29360b485c3d2b56cdb075"}, - {file = "SQLAlchemy-2.0.28-cp37-cp37m-win_amd64.whl", hash = "sha256:a6bec1c010a6d65b3ed88c863d56b9ea5eeefdf62b5e39cafd08c65f5ce5198b"}, - {file = "SQLAlchemy-2.0.28-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:843a882cadebecc655a68bd9a5b8aa39b3c52f4a9a5572a3036fb1bb2ccdc197"}, - {file = "SQLAlchemy-2.0.28-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:dbb990612c36163c6072723523d2be7c3eb1517bbdd63fe50449f56afafd1133"}, - {file = "SQLAlchemy-2.0.28-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bd7e4baf9161d076b9a7e432fce06217b9bd90cfb8f1d543d6e8c4595627edb9"}, - {file = "SQLAlchemy-2.0.28-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e0a5354cb4de9b64bccb6ea33162cb83e03dbefa0d892db88a672f5aad638a75"}, - {file = "SQLAlchemy-2.0.28-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:fffcc8edc508801ed2e6a4e7b0d150a62196fd28b4e16ab9f65192e8186102b6"}, - {file = "SQLAlchemy-2.0.28-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:aca7b6d99a4541b2ebab4494f6c8c2f947e0df4ac859ced575238e1d6ca5716b"}, - {file = "SQLAlchemy-2.0.28-cp38-cp38-win32.whl", hash = "sha256:8c7f10720fc34d14abad5b647bc8202202f4948498927d9f1b4df0fb1cf391b7"}, - {file = "SQLAlchemy-2.0.28-cp38-cp38-win_amd64.whl", hash = "sha256:243feb6882b06a2af68ecf4bec8813d99452a1b62ba2be917ce6283852cf701b"}, - {file = "SQLAlchemy-2.0.28-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:fc4974d3684f28b61b9a90fcb4c41fb340fd4b6a50c04365704a4da5a9603b05"}, - {file = "SQLAlchemy-2.0.28-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:87724e7ed2a936fdda2c05dbd99d395c91ea3c96f029a033a4a20e008dd876bf"}, - {file = "SQLAlchemy-2.0.28-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:68722e6a550f5de2e3cfe9da6afb9a7dd15ef7032afa5651b0f0c6b3adb8815d"}, - {file = "SQLAlchemy-2.0.28-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:328529f7c7f90adcd65aed06a161851f83f475c2f664a898af574893f55d9e53"}, - {file = "SQLAlchemy-2.0.28-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:df40c16a7e8be7413b885c9bf900d402918cc848be08a59b022478804ea076b8"}, - {file = "SQLAlchemy-2.0.28-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:426f2fa71331a64f5132369ede5171c52fd1df1bd9727ce621f38b5b24f48750"}, - {file = "SQLAlchemy-2.0.28-cp39-cp39-win32.whl", hash = "sha256:33157920b233bc542ce497a81a2e1452e685a11834c5763933b440fedd1d8e2d"}, - {file = "SQLAlchemy-2.0.28-cp39-cp39-win_amd64.whl", hash = "sha256:2f60843068e432311c886c5f03c4664acaef507cf716f6c60d5fde7265be9d7b"}, - {file = "SQLAlchemy-2.0.28-py3-none-any.whl", hash = "sha256:78bb7e8da0183a8301352d569900d9d3594c48ac21dc1c2ec6b3121ed8b6c986"}, - {file = "SQLAlchemy-2.0.28.tar.gz", hash = "sha256:dd53b6c4e6d960600fd6532b79ee28e2da489322fcf6648738134587faf767b6"}, + {file = "SQLAlchemy-2.0.29-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:4c142852ae192e9fe5aad5c350ea6befe9db14370b34047e1f0f7cf99e63c63b"}, + {file = "SQLAlchemy-2.0.29-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:99a1e69d4e26f71e750e9ad6fdc8614fbddb67cfe2173a3628a2566034e223c7"}, + {file = "SQLAlchemy-2.0.29-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5ef3fbccb4058355053c51b82fd3501a6e13dd808c8d8cd2561e610c5456013c"}, + {file = "SQLAlchemy-2.0.29-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9d6753305936eddc8ed190e006b7bb33a8f50b9854823485eed3a886857ab8d1"}, + {file = "SQLAlchemy-2.0.29-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:0f3ca96af060a5250a8ad5a63699180bc780c2edf8abf96c58af175921df847a"}, + {file = "SQLAlchemy-2.0.29-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:c4520047006b1d3f0d89e0532978c0688219857eb2fee7c48052560ae76aca1e"}, + {file = "SQLAlchemy-2.0.29-cp310-cp310-win32.whl", hash = "sha256:b2a0e3cf0caac2085ff172c3faacd1e00c376e6884b5bc4dd5b6b84623e29e4f"}, + {file = "SQLAlchemy-2.0.29-cp310-cp310-win_amd64.whl", hash = "sha256:01d10638a37460616708062a40c7b55f73e4d35eaa146781c683e0fa7f6c43fb"}, + {file = "SQLAlchemy-2.0.29-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:308ef9cb41d099099fffc9d35781638986870b29f744382904bf9c7dadd08513"}, + {file = "SQLAlchemy-2.0.29-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:296195df68326a48385e7a96e877bc19aa210e485fa381c5246bc0234c36c78e"}, + {file = "SQLAlchemy-2.0.29-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a13b917b4ffe5a0a31b83d051d60477819ddf18276852ea68037a144a506efb9"}, + {file = "SQLAlchemy-2.0.29-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4f6d971255d9ddbd3189e2e79d743ff4845c07f0633adfd1de3f63d930dbe673"}, + {file = "SQLAlchemy-2.0.29-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:61405ea2d563407d316c63a7b5271ae5d274a2a9fbcd01b0aa5503635699fa1e"}, + {file = "SQLAlchemy-2.0.29-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:de7202ffe4d4a8c1e3cde1c03e01c1a3772c92858837e8f3879b497158e4cb44"}, + {file = "SQLAlchemy-2.0.29-cp311-cp311-win32.whl", hash = "sha256:b5d7ed79df55a731749ce65ec20d666d82b185fa4898430b17cb90c892741520"}, + {file = "SQLAlchemy-2.0.29-cp311-cp311-win_amd64.whl", hash = "sha256:205f5a2b39d7c380cbc3b5dcc8f2762fb5bcb716838e2d26ccbc54330775b003"}, + {file = "SQLAlchemy-2.0.29-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:d96710d834a6fb31e21381c6d7b76ec729bd08c75a25a5184b1089141356171f"}, + {file = "SQLAlchemy-2.0.29-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:52de4736404e53c5c6a91ef2698c01e52333988ebdc218f14c833237a0804f1b"}, + {file = "SQLAlchemy-2.0.29-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5c7b02525ede2a164c5fa5014915ba3591730f2cc831f5be9ff3b7fd3e30958e"}, + {file = "SQLAlchemy-2.0.29-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0dfefdb3e54cd15f5d56fd5ae32f1da2d95d78319c1f6dfb9bcd0eb15d603d5d"}, + {file = "SQLAlchemy-2.0.29-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:a88913000da9205b13f6f195f0813b6ffd8a0c0c2bd58d499e00a30eb508870c"}, + {file = "SQLAlchemy-2.0.29-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:fecd5089c4be1bcc37c35e9aa678938d2888845a134dd016de457b942cf5a758"}, + {file = "SQLAlchemy-2.0.29-cp312-cp312-win32.whl", hash = "sha256:8197d6f7a3d2b468861ebb4c9f998b9df9e358d6e1cf9c2a01061cb9b6cf4e41"}, + {file = "SQLAlchemy-2.0.29-cp312-cp312-win_amd64.whl", hash = "sha256:9b19836ccca0d321e237560e475fd99c3d8655d03da80c845c4da20dda31b6e1"}, + {file = "SQLAlchemy-2.0.29-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:87a1d53a5382cdbbf4b7619f107cc862c1b0a4feb29000922db72e5a66a5ffc0"}, + {file = "SQLAlchemy-2.0.29-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2a0732dffe32333211801b28339d2a0babc1971bc90a983e3035e7b0d6f06b93"}, + {file = "SQLAlchemy-2.0.29-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:90453597a753322d6aa770c5935887ab1fc49cc4c4fdd436901308383d698b4b"}, + {file = "SQLAlchemy-2.0.29-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:ea311d4ee9a8fa67f139c088ae9f905fcf0277d6cd75c310a21a88bf85e130f5"}, + {file = "SQLAlchemy-2.0.29-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:5f20cb0a63a3e0ec4e169aa8890e32b949c8145983afa13a708bc4b0a1f30e03"}, + {file = "SQLAlchemy-2.0.29-cp37-cp37m-win32.whl", hash = "sha256:e5bbe55e8552019c6463709b39634a5fc55e080d0827e2a3a11e18eb73f5cdbd"}, + {file = "SQLAlchemy-2.0.29-cp37-cp37m-win_amd64.whl", hash = "sha256:c2f9c762a2735600654c654bf48dad388b888f8ce387b095806480e6e4ff6907"}, + {file = "SQLAlchemy-2.0.29-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:7e614d7a25a43a9f54fcce4675c12761b248547f3d41b195e8010ca7297c369c"}, + {file = "SQLAlchemy-2.0.29-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:471fcb39c6adf37f820350c28aac4a7df9d3940c6548b624a642852e727ea586"}, + {file = "SQLAlchemy-2.0.29-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:988569c8732f54ad3234cf9c561364221a9e943b78dc7a4aaf35ccc2265f1930"}, + {file = "SQLAlchemy-2.0.29-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dddaae9b81c88083e6437de95c41e86823d150f4ee94bf24e158a4526cbead01"}, + {file = "SQLAlchemy-2.0.29-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:334184d1ab8f4c87f9652b048af3f7abea1c809dfe526fb0435348a6fef3d380"}, + {file = "SQLAlchemy-2.0.29-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:38b624e5cf02a69b113c8047cf7f66b5dfe4a2ca07ff8b8716da4f1b3ae81567"}, + {file = "SQLAlchemy-2.0.29-cp38-cp38-win32.whl", hash = "sha256:bab41acf151cd68bc2b466deae5deeb9e8ae9c50ad113444151ad965d5bf685b"}, + {file = "SQLAlchemy-2.0.29-cp38-cp38-win_amd64.whl", hash = "sha256:52c8011088305476691b8750c60e03b87910a123cfd9ad48576d6414b6ec2a1d"}, + {file = "SQLAlchemy-2.0.29-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:3071ad498896907a5ef756206b9dc750f8e57352113c19272bdfdc429c7bd7de"}, + {file = "SQLAlchemy-2.0.29-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:dba622396a3170974f81bad49aacebd243455ec3cc70615aeaef9e9613b5bca5"}, + {file = "SQLAlchemy-2.0.29-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7b184e3de58009cc0bf32e20f137f1ec75a32470f5fede06c58f6c355ed42a72"}, + {file = "SQLAlchemy-2.0.29-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8c37f1050feb91f3d6c32f864d8e114ff5545a4a7afe56778d76a9aec62638ba"}, + {file = "SQLAlchemy-2.0.29-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:bda7ce59b06d0f09afe22c56714c65c957b1068dee3d5e74d743edec7daba552"}, + {file = "SQLAlchemy-2.0.29-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:25664e18bef6dc45015b08f99c63952a53a0a61f61f2e48a9e70cec27e55f699"}, + {file = "SQLAlchemy-2.0.29-cp39-cp39-win32.whl", hash = "sha256:77d29cb6c34b14af8a484e831ab530c0f7188f8efed1c6a833a2c674bf3c26ec"}, + {file = "SQLAlchemy-2.0.29-cp39-cp39-win_amd64.whl", hash = "sha256:04c487305ab035a9548f573763915189fc0fe0824d9ba28433196f8436f1449c"}, + {file = "SQLAlchemy-2.0.29-py3-none-any.whl", hash = "sha256:dc4ee2d4ee43251905f88637d5281a8d52e916a021384ec10758826f5cbae305"}, + {file = "SQLAlchemy-2.0.29.tar.gz", hash = "sha256:bd9566b8e58cabd700bc367b60e90d9349cd16f0984973f98a9a09f9c64e86f0"}, ] [package.dependencies] @@ -5456,19 +5513,18 @@ sqlcipher = ["sqlcipher3_binary"] [[package]] name = "sqlparse" -version = "0.4.4" +version = "0.5.0" description = "A non-validating SQL parser." optional = false -python-versions = ">=3.5" +python-versions = ">=3.8" files = [ - {file = "sqlparse-0.4.4-py3-none-any.whl", hash = "sha256:5430a4fe2ac7d0f93e66f1efc6e1338a41884b7ddf2a350cedd20ccc4d9d28f3"}, - {file = "sqlparse-0.4.4.tar.gz", hash = "sha256:d446183e84b8349fa3061f0fe7f06ca94ba65b426946ffebe6e3e8295332420c"}, + {file = "sqlparse-0.5.0-py3-none-any.whl", hash = "sha256:c204494cd97479d0e39f28c93d46c0b2d5959c7b9ab904762ea6c7af211c8663"}, + {file = "sqlparse-0.5.0.tar.gz", hash = "sha256:714d0a4932c059d16189f58ef5411ec2287a4360f17cdd0edd2d09d4c5087c93"}, ] [package.extras] -dev = ["build", "flake8"] +dev = ["build", "hatch"] doc = ["sphinx"] -test = ["pytest", "pytest-cov"] [[package]] name = "stack-data" @@ -5607,26 +5663,37 @@ docs = ["myst-parser", "pydata-sphinx-theme", "sphinx"] test = ["pre-commit", "pytest (>=7.0)", "pytest-timeout"] typing = ["mypy (>=1.6,<2.0)", "traitlets (>=5.11.1)"] +[[package]] +name = "texttable" +version = "1.7.0" +description = "module to create simple ASCII tables" +optional = false +python-versions = "*" +files = [ + {file = "texttable-1.7.0-py2.py3-none-any.whl", hash = "sha256:72227d592c82b3d7f672731ae73e4d1f88cd8e2ef5b075a7a7f01a23a3743917"}, + {file = "texttable-1.7.0.tar.gz", hash = "sha256:2d2068fb55115807d3ac77a4ca68fa48803e84ebb0ee2340f858107a36522638"}, +] + [[package]] name = "threadpoolctl" -version = "3.3.0" +version = "3.4.0" description = "threadpoolctl" optional = false python-versions = ">=3.8" files = [ - {file = "threadpoolctl-3.3.0-py3-none-any.whl", hash = "sha256:6155be1f4a39f31a18ea70f94a77e0ccd57dced08122ea61109e7da89883781e"}, - {file = "threadpoolctl-3.3.0.tar.gz", hash = "sha256:5dac632b4fa2d43f42130267929af3ba01399ef4bd1882918e92dbc30365d30c"}, + {file = "threadpoolctl-3.4.0-py3-none-any.whl", hash = "sha256:8f4c689a65b23e5ed825c8436a92b818aac005e0f3715f6a1664d7c7ee29d262"}, + {file = "threadpoolctl-3.4.0.tar.gz", hash = "sha256:f11b491a03661d6dd7ef692dd422ab34185d982466c49c8f98c8f716b5c93196"}, ] [[package]] name = "tinycss2" -version = "1.2.1" +version = "1.3.0" description = "A tiny CSS parser" optional = false -python-versions = ">=3.7" +python-versions = ">=3.8" files = [ - {file = "tinycss2-1.2.1-py3-none-any.whl", hash = "sha256:2b80a96d41e7c3914b8cda8bc7f705a4d9c49275616e886103dd839dfc847847"}, - {file = "tinycss2-1.2.1.tar.gz", hash = "sha256:8cff3a8f066c2ec677c06dbc7b45619804a6938478d9d73c284b29d14ecb0627"}, + {file = "tinycss2-1.3.0-py3-none-any.whl", hash = "sha256:54a8dbdffb334d536851be0226030e9505965bb2f30f21a4a82c55fb2a80fae7"}, + {file = "tinycss2-1.3.0.tar.gz", hash = "sha256:152f9acabd296a8375fbca5b84c961ff95971fcfc32e79550c8df8e29118c54d"}, ] [package.dependencies] @@ -5634,7 +5701,7 @@ webencodings = ">=0.4" [package.extras] doc = ["sphinx", "sphinx_rtd_theme"] -test = ["flake8", "isort", "pytest"] +test = ["pytest", "ruff"] [[package]] name = "tokenize-rt" @@ -5737,13 +5804,13 @@ opt-einsum = ["opt-einsum (>=3.3)"] [[package]] name = "torchmetrics" -version = "1.3.1" +version = "1.3.2" description = "PyTorch native Metrics" optional = false python-versions = ">=3.8" files = [ - {file = "torchmetrics-1.3.1-py3-none-any.whl", hash = "sha256:a44bd1edee629bbf463eb81bfba8300b3785d8b3b8d758bdcafa862b80955b4f"}, - {file = "torchmetrics-1.3.1.tar.gz", hash = "sha256:8d371f7597a1a5eb02d5f2ed59642d6fef09093926997ce91e18b1147cc8defa"}, + {file = "torchmetrics-1.3.2-py3-none-any.whl", hash = "sha256:44ca3a9f86dc050cb3f554836ef291698ea797778457195b4f685fce8e2e64a3"}, + {file = "torchmetrics-1.3.2.tar.gz", hash = "sha256:0a67694a4c4265eeb54cda741eaf5cb1f3a71da74b7e7e6215ad156c9f2379f6"}, ] [package.dependencies] @@ -5753,14 +5820,14 @@ packaging = ">17.1" torch = ">=1.10.0" [package.extras] -all = ["SciencePlots (>=2.0.0)", "ipadic (>=1.0.0)", "matplotlib (>=3.3.0)", "mecab-ko (>=1.0.0)", "mecab-ko-dic (>=1.0.0)", "mecab-python3 (>=1.0.6)", "mypy (==1.8.0)", "nltk (>=3.6)", "piq (<=0.8.0)", "pycocotools (>2.0.0)", "pystoi (>=0.3.0)", "regex (>=2021.9.24)", "scipy (>1.0.0)", "sentencepiece (>=0.1.98)", "torch (==2.2.0)", "torch-fidelity (<=0.4.0)", "torchaudio (>=0.10.0)", "torchvision (>=0.8)", "tqdm (>=4.41.0)", "transformers (>4.4.0)", "transformers (>=4.10.0)", "types-PyYAML", "types-emoji", "types-protobuf", "types-requests", "types-setuptools", "types-six", "types-tabulate"] +all = ["SciencePlots (>=2.0.0)", "ipadic (>=1.0.0)", "matplotlib (>=3.3.0)", "mecab-ko (>=1.0.0)", "mecab-ko-dic (>=1.0.0)", "mecab-python3 (>=1.0.6)", "mypy (==1.8.0)", "nltk (>=3.6)", "piq (<=0.8.0)", "pycocotools (>2.0.0)", "pystoi (>=0.3.0)", "regex (>=2021.9.24)", "scipy (>1.0.0)", "sentencepiece (>=0.2.0)", "torch (==2.2.1)", "torch-fidelity (<=0.4.0)", "torchaudio (>=0.10.0)", "torchvision (>=0.8)", "tqdm (>=4.41.0)", "transformers (>4.4.0)", "transformers (>=4.10.0)", "types-PyYAML", "types-emoji", "types-protobuf", "types-requests", "types-setuptools", "types-six", "types-tabulate"] audio = ["pystoi (>=0.3.0)", "torchaudio (>=0.10.0)"] detection = ["pycocotools (>2.0.0)", "torchvision (>=0.8)"] -dev = ["SciencePlots (>=2.0.0)", "bert-score (==0.3.13)", "dython (<=0.7.5)", "fairlearn", "fast-bss-eval (>=0.1.0)", "faster-coco-eval (>=1.3.3)", "huggingface-hub (<0.21)", "ipadic (>=1.0.0)", "jiwer (>=2.3.0)", "kornia (>=0.6.7)", "lpips (<=0.1.4)", "matplotlib (>=3.3.0)", "mecab-ko (>=1.0.0)", "mecab-ko-dic (>=1.0.0)", "mecab-python3 (>=1.0.6)", "mir-eval (>=0.6)", "monai (==1.3.0)", "mypy (==1.8.0)", "netcal (>1.0.0)", "nltk (>=3.6)", "numpy (<1.25.0)", "pandas (>1.0.0)", "pandas (>=1.4.0)", "piq (<=0.8.0)", "pycocotools (>2.0.0)", "pystoi (>=0.3.0)", "pytorch-msssim (==1.0.0)", "regex (>=2021.9.24)", "rouge-score (>0.1.0)", "sacrebleu (>=2.3.0)", "scikit-image (>=0.19.0)", "scipy (>1.0.0)", "sentencepiece (>=0.1.98)", "sewar (>=0.4.4)", "statsmodels (>0.13.5)", "torch (==2.2.0)", "torch-complex (<=0.4.3)", "torch-fidelity (<=0.4.0)", "torchaudio (>=0.10.0)", "torchvision (>=0.8)", "tqdm (>=4.41.0)", "transformers (>4.4.0)", "transformers (>=4.10.0)", "types-PyYAML", "types-emoji", "types-protobuf", "types-requests", "types-setuptools", "types-six", "types-tabulate"] +dev = ["SciencePlots (>=2.0.0)", "bert-score (==0.3.13)", "dython (<=0.7.5)", "fairlearn", "fast-bss-eval (>=0.1.0)", "faster-coco-eval (>=1.3.3)", "huggingface-hub (<0.22)", "ipadic (>=1.0.0)", "jiwer (>=2.3.0)", "kornia (>=0.6.7)", "lpips (<=0.1.4)", "matplotlib (>=3.3.0)", "mecab-ko (>=1.0.0)", "mecab-ko-dic (>=1.0.0)", "mecab-python3 (>=1.0.6)", "mir-eval (>=0.6)", "monai (==1.3.0)", "mypy (==1.8.0)", "netcal (>1.0.0)", "nltk (>=3.6)", "numpy (<1.25.0)", "pandas (>1.0.0)", "pandas (>=1.4.0)", "piq (<=0.8.0)", "pycocotools (>2.0.0)", "pystoi (>=0.3.0)", "pytorch-msssim (==1.0.0)", "regex (>=2021.9.24)", "rouge-score (>0.1.0)", "sacrebleu (>=2.3.0)", "scikit-image (>=0.19.0)", "scipy (>1.0.0)", "sentencepiece (>=0.2.0)", "sewar (>=0.4.4)", "statsmodels (>0.13.5)", "torch (==2.2.1)", "torch-complex (<=0.4.3)", "torch-fidelity (<=0.4.0)", "torchaudio (>=0.10.0)", "torchvision (>=0.8)", "tqdm (>=4.41.0)", "transformers (>4.4.0)", "transformers (>=4.10.0)", "types-PyYAML", "types-emoji", "types-protobuf", "types-requests", "types-setuptools", "types-six", "types-tabulate"] image = ["scipy (>1.0.0)", "torch-fidelity (<=0.4.0)", "torchvision (>=0.8)"] multimodal = ["piq (<=0.8.0)", "transformers (>=4.10.0)"] -text = ["ipadic (>=1.0.0)", "mecab-ko (>=1.0.0)", "mecab-ko-dic (>=1.0.0)", "mecab-python3 (>=1.0.6)", "nltk (>=3.6)", "regex (>=2021.9.24)", "sentencepiece (>=0.1.98)", "tqdm (>=4.41.0)", "transformers (>4.4.0)"] -typing = ["mypy (==1.8.0)", "torch (==2.2.0)", "types-PyYAML", "types-emoji", "types-protobuf", "types-requests", "types-setuptools", "types-six", "types-tabulate"] +text = ["ipadic (>=1.0.0)", "mecab-ko (>=1.0.0)", "mecab-ko-dic (>=1.0.0)", "mecab-python3 (>=1.0.6)", "nltk (>=3.6)", "regex (>=2021.9.24)", "sentencepiece (>=0.2.0)", "tqdm (>=4.41.0)", "transformers (>4.4.0)"] +typing = ["mypy (==1.8.0)", "torch (==2.2.1)", "types-PyYAML", "types-emoji", "types-protobuf", "types-requests", "types-setuptools", "types-six", "types-tabulate"] visual = ["SciencePlots (>=2.0.0)", "matplotlib (>=3.3.0)"] [[package]] @@ -5805,18 +5872,18 @@ telegram = ["requests"] [[package]] name = "traitlets" -version = "5.14.2" +version = "5.14.3" description = "Traitlets Python configuration system" optional = false python-versions = ">=3.8" files = [ - {file = "traitlets-5.14.2-py3-none-any.whl", hash = "sha256:fcdf85684a772ddeba87db2f398ce00b40ff550d1528c03c14dbf6a02003cd80"}, - {file = "traitlets-5.14.2.tar.gz", hash = "sha256:8cdd83c040dab7d1dee822678e5f5d100b514f7b72b01615b26fc5718916fdf9"}, + {file = "traitlets-5.14.3-py3-none-any.whl", hash = "sha256:b74e89e397b1ed28cc831db7aea759ba6640cb3de13090ca145426688ff1ac4f"}, + {file = "traitlets-5.14.3.tar.gz", hash = "sha256:9ed0579d3502c94b4b3732ac120375cda96f923114522847de4b3bb98b96b6b7"}, ] [package.extras] docs = ["myst-parser", "pydata-sphinx-theme", "sphinx"] -test = ["argcomplete (>=3.0.3)", "mypy (>=1.7.0)", "pre-commit", "pytest (>=7.0,<8.1)", "pytest-mock", "pytest-mypy-testing"] +test = ["argcomplete (>=3.0.3)", "mypy (>=1.7.0)", "pre-commit", "pytest (>=7.0,<8.2)", "pytest-mock", "pytest-mypy-testing"] [[package]] name = "triton" @@ -5857,13 +5924,13 @@ tutorials = ["matplotlib", "pandas", "tabulate"] [[package]] name = "types-python-dateutil" -version = "2.9.0.20240315" +version = "2.9.0.20240316" description = "Typing stubs for python-dateutil" optional = false python-versions = ">=3.8" files = [ - {file = "types-python-dateutil-2.9.0.20240315.tar.gz", hash = "sha256:c1f6310088eb9585da1b9f811765b989ed2e2cdd4203c1a367e944b666507e4e"}, - {file = "types_python_dateutil-2.9.0.20240315-py3-none-any.whl", hash = "sha256:78aa9124f360df90bb6e85eb1a4d06e75425445bf5ecb13774cb0adef7ff3956"}, + {file = "types-python-dateutil-2.9.0.20240316.tar.gz", hash = "sha256:5d2f2e240b86905e40944dd787db6da9263f0deabef1076ddaed797351ec0202"}, + {file = "types_python_dateutil-2.9.0.20240316-py3-none-any.whl", hash = "sha256:6b8cb66d960771ce5ff974e9dd45e38facb81718cc1e208b10b1baccbfdbee3b"}, ] [[package]] @@ -5894,13 +5961,13 @@ typing-extensions = ">=4.5.0" [[package]] name = "typing-extensions" -version = "4.10.0" +version = "4.11.0" description = "Backported and Experimental Type Hints for Python 3.8+" optional = false python-versions = ">=3.8" files = [ - {file = "typing_extensions-4.10.0-py3-none-any.whl", hash = "sha256:69b1a937c3a517342112fb4c6df7e72fc39a38e7891a5730ed4985b5214b5475"}, - {file = "typing_extensions-4.10.0.tar.gz", hash = "sha256:b0abd7c89e8fb96f98db18d86106ff1d90ab692004eb746cf6eda2682f91b3cb"}, + {file = "typing_extensions-4.11.0-py3-none-any.whl", hash = "sha256:c1f94d72897edaf4ce775bb7558d5b79d8126906a14ea5ed1635921406c0387a"}, + {file = "typing_extensions-4.11.0.tar.gz", hash = "sha256:83f085bd5ca59c80295fc2a82ab5dac679cbe02b9f33f7d83af68e241bea51b0"}, ] [[package]] @@ -5951,13 +6018,13 @@ zstd = ["zstandard (>=0.18.0)"] [[package]] name = "virtualenv" -version = "20.25.1" +version = "20.26.0" description = "Virtual Python Environment builder" optional = false python-versions = ">=3.7" files = [ - {file = "virtualenv-20.25.1-py3-none-any.whl", hash = "sha256:961c026ac520bac5f69acb8ea063e8a4f071bcc9457b9c1f28f6b085c511583a"}, - {file = "virtualenv-20.25.1.tar.gz", hash = "sha256:e08e13ecdca7a0bd53798f356d5831434afa5b07b93f0abdf0797b7a06ffe197"}, + {file = "virtualenv-20.26.0-py3-none-any.whl", hash = "sha256:0846377ea76e818daaa3e00a4365c018bc3ac9760cbb3544de542885aad61fb3"}, + {file = "virtualenv-20.26.0.tar.gz", hash = "sha256:ec25a9671a5102c8d2657f62792a27b48f016664c6873f6beed3800008577210"}, ] [package.dependencies] @@ -5966,7 +6033,7 @@ filelock = ">=3.12.2,<4" platformdirs = ">=3.9.1,<5" [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)"] +docs = ["furo (>=2023.7.26)", "proselint (>=0.13)", "sphinx (>=7.1.2,!=7.3)", "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]] @@ -6023,29 +6090,29 @@ files = [ [[package]] name = "websocket-client" -version = "1.7.0" +version = "1.8.0" description = "WebSocket client for Python with low level API options" optional = false python-versions = ">=3.8" files = [ - {file = "websocket-client-1.7.0.tar.gz", hash = "sha256:10e511ea3a8c744631d3bd77e61eb17ed09304c413ad42cf6ddfa4c7787e8fe6"}, - {file = "websocket_client-1.7.0-py3-none-any.whl", hash = "sha256:f4c3d22fec12a2461427a29957ff07d35098ee2d976d3ba244e688b8b4057588"}, + {file = "websocket_client-1.8.0-py3-none-any.whl", hash = "sha256:17b44cc997f5c498e809b22cdf2d9c7a9e71c02c8cc2b6c56e7c2d1239bfa526"}, + {file = "websocket_client-1.8.0.tar.gz", hash = "sha256:3239df9f44da632f96012472805d40a23281a991027ce11d2f45a6f24ac4c3da"}, ] [package.extras] -docs = ["Sphinx (>=6.0)", "sphinx-rtd-theme (>=1.1.0)"] +docs = ["Sphinx (>=6.0)", "myst-parser (>=2.0.0)", "sphinx-rtd-theme (>=1.1.0)"] optional = ["python-socks", "wsaccel"] test = ["websockets"] [[package]] name = "werkzeug" -version = "3.0.1" +version = "3.0.2" description = "The comprehensive WSGI web application library." optional = false python-versions = ">=3.8" files = [ - {file = "werkzeug-3.0.1-py3-none-any.whl", hash = "sha256:90a285dc0e42ad56b34e696398b8122ee4c681833fb35b8334a095d82c56da10"}, - {file = "werkzeug-3.0.1.tar.gz", hash = "sha256:507e811ecea72b18a404947aded4b3390e1db8f826b494d76550ef45bb3b1dcc"}, + {file = "werkzeug-3.0.2-py3-none-any.whl", hash = "sha256:3aac3f5da756f93030740bc235d3e09449efcf65f2f55e3602e1d851b8f48795"}, + {file = "werkzeug-3.0.2.tar.gz", hash = "sha256:e39b645a6ac92822588e7b39a692e7828724ceae0b0d702ef96701f90e70128d"}, ] [package.dependencies] @@ -6282,4 +6349,4 @@ seaborn = ["seaborn"] [metadata] lock-version = "2.0" python-versions = "~3.10" -content-hash = "8e13008902ec45a88f60f5b5771e0be7383f4202b40076c336ba1fd181a3f535" +content-hash = "a6f6723fa1ca43ddf5124def03f3d5ef11569bb1772803c8ebac3b4eab090cb5" diff --git a/pyproject.toml b/pyproject.toml index 808098e..e528ea0 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -24,6 +24,7 @@ tensordict = "^0.1.0" torch = "2.0.0" torchmetrics = "^1.0.3" types-PyYAML = "^6.0.12.2" +igraph ="^0.11.3" [tool.poetry.extras] seaborn = ["seaborn"] diff --git a/src/causica/data_generation/generate_data.py b/src/causica/data_generation/generate_data.py index d236ab1..326cf88 100644 --- a/src/causica/data_generation/generate_data.py +++ b/src/causica/data_generation/generate_data.py @@ -209,8 +209,66 @@ def get_size_dict(variables: VariablesMetadata) -> dict[str, int]: return size_dict +def sample_treatment_given_effects(graph: torch.Tensor, node_names: Sequence[str], effect_variables: list[str]) -> str: + """Sample a treatment from the graph. + + Args: + graph: The adjacency matrix of the graph. + node_names: The names of the nodes in the graph. + effect_variables: pre-specified effect variables. + + Returns: + The treatment variable. + """ + nx_graph = nx.from_numpy_array(graph.numpy(), create_using=nx.DiGraph) + nx_graph = nx.relabel_nodes(nx_graph, dict(enumerate(node_names))) + + common_ancestors = nx.ancestors(nx_graph, effect_variables[0]) + for effect_variable in effect_variables[1:]: + common_ancestors &= nx.ancestors(nx_graph, effect_variable) + if common_ancestors: + treatment = np.random.choice(list(common_ancestors), size=1, replace=False).item() + return treatment + warnings.warn("No common ancestors found for the given effects. Returning random sampled treatment.") + possible_treatments = [node for node in node_names if node not in effect_variables] + treatment = np.random.choice(possible_treatments, size=1, replace=False).item() + return treatment + + +def sample_effects_given_treatment( + graph: torch.Tensor, + node_names: Sequence[str], + treatment_variable: str, + num_effects: int = 1, +) -> list[str]: + """Sample effect variables from the graph given a treatment. + + Args: + graph: The adjacency matrix of the graph. + node_names: The names of the nodes in the graph. + treatment_variable: The treatment variable from which to find effects. + num_effects: The number of effect nodes to sample. + + Returns: + The list of effect variables. + """ + nx_graph = nx.from_numpy_array(graph.numpy(), create_using=nx.DiGraph) + nx_graph = nx.relabel_nodes(nx_graph, dict(enumerate(node_names))) + + descendants = list(nx.descendants(nx_graph, treatment_variable)) + if not descendants: + warnings.warn("No descendants found for the given treatment. Defaulting to random sampling.") + possible_effects = [node for node in node_names if node != treatment_variable] + return np.random.choice(possible_effects, size=num_effects, replace=False).tolist() + + return np.random.choice(descendants, size=num_effects, replace=False).tolist() + + def sample_treatment_and_effect( - graph: torch.Tensor, node_names: Sequence[str], ensure_effect: bool = True, num_effects: int = 1 + graph: torch.Tensor, + node_names: Sequence[str], + ensure_effect: bool = True, + num_effects: int = 1, ) -> tuple[str, list[str]]: """Sample a treatment and effects from a graph. @@ -219,32 +277,26 @@ def sample_treatment_and_effect( node_names: The names of the nodes in the graph. ensure_effect: Whether to ensure that there is a path from the treatment to the effect. num_effects: The number of effect nodes to sample. - Returns: - The treatment and effects. + A tuple containing the treatment variable and a list of effect variables. """ - if ensure_effect: - assert num_effects == 1, "Can only ensure effect for one effect node." + if ensure_effect: nx_graph = nx.from_numpy_array(graph.numpy(), create_using=nx.DiGraph) nx_graph = nx.relabel_nodes(nx_graph, dict(enumerate(node_names))) - - topological_order = nx.topological_sort(nx_graph) - topological_order_with_descendants = [n for n in topological_order if nx.descendants(nx_graph, n)] - - if topological_order_with_descendants: - treatment = np.random.choice(topological_order_with_descendants, size=1, replace=False).item() - # randomly sample a effect not that is a descendant of the treatment - descendants = nx.descendants(nx_graph, treatment) - effects = np.random.choice(list(descendants), size=num_effects, replace=False) - return treatment, effects.tolist() - warnings.warn("No edges found. Defaulting to random sampling.") + nodes_with_outgoing_edges = [n for n in node_names if any(True for _ in nx_graph.successors(n))] + if nodes_with_outgoing_edges: + treatment = np.random.choice(nodes_with_outgoing_edges, size=1, replace=False).item() + descendants = list(nx.descendants(nx_graph, treatment)) + effects = np.random.choice(descendants, size=num_effects, replace=False).tolist() + return treatment, effects + warnings.warn("No nodes with descendants found in the graph. Defaulting to random sampling.") samples = np.random.choice(node_names, size=1 + num_effects, replace=False) treatment = samples[0] - effects = samples[1:] + effects = samples[1:].tolist() - return treatment, effects.tolist() + return treatment, effects def sample_dataset( @@ -254,6 +306,8 @@ def sample_dataset( num_intervention_samples: int = 1000, sample_interventions: bool = False, sample_counterfactuals: bool = False, + treatment_variable: str | None = None, + effect_variables: list[str] | None = None, ) -> CausalDataset: """Sample a new dataset and returns it as a CausalDataset object. @@ -264,7 +318,8 @@ def sample_dataset( num_intervention_samples: The number of interventional samples to sample. sample_interventions: Whether to sample interventions. sample_counterfactuals: Whether to sample counterfactuals. - + treatment_variable: pre-specified treatment + effet_variables: pre-specified effects Returns: A CausalDataset object holding the data, graph and potential interventions and counterfactuals. """ @@ -276,7 +331,19 @@ def sample_dataset( counterfactuals: list[CounterfactualWithEffects] = [] for _ in range(num_interventions): - treatment, effects = sample_treatment_and_effect(sem.graph, sem.node_names) + if treatment_variable and effect_variables: + treatment, effects = treatment_variable, effect_variables + elif treatment_variable: + treatment, effects = treatment_variable, sample_effects_given_treatment( + sem.graph, sem.node_names, treatment_variable + ) + elif effect_variables: + treatment, effects = ( + sample_treatment_given_effects(sem.graph, sem.node_names, effect_variables), + effect_variables, + ) + else: + treatment, effects = sample_treatment_and_effect(sem.graph, sem.node_names) if sample_interventions: interventions.append( diff --git a/src/causica/data_generation/samplers/functional_relationships_sampler.py b/src/causica/data_generation/samplers/functional_relationships_sampler.py index ad024cb..21ce1a0 100644 --- a/src/causica/data_generation/samplers/functional_relationships_sampler.py +++ b/src/causica/data_generation/samplers/functional_relationships_sampler.py @@ -7,6 +7,9 @@ from causica.data_generation.samplers.sampler import Sampler from causica.functional_relationships.functional_relationships import FunctionalRelationships +from causica.functional_relationships.heteroscedastic_rff_functional_relationships import ( + HeteroscedasticRFFFunctionalRelationships, +) from causica.functional_relationships.linear_functional_relationships import LinearFunctionalRelationships from causica.functional_relationships.rff_functional_relationships import RFFFunctionalRelationships @@ -43,8 +46,11 @@ def sample(self) -> FunctionalRelationships: class RFFFunctionalRelationshipsSampler(FunctionalRelationshipsSampler): - """Sample a Non Linear Functional Relationship, by providing two distributions: - a first distribution for the random features, and a second distribution for the linear outer coefficients.""" + """Sample a Non Linear Functional Relationship, by providing two distributions. + + The sampler uses two distributions: + - a first distribution for the random features + - a second distribution for the linear outer coefficients.""" def __init__( self, @@ -79,3 +85,77 @@ def sample(self) -> FunctionalRelationships: initial_output_scales=sample_out, initial_angles=sample_angle, ) + + +class HeteroscedasticRFFFunctionalRelationshipsSampler(FunctionalRelationshipsSampler): + """Sample a Post Non Linear Functional Relationship, by providing two distributions. + + The sampler uses two distributions: + - a first distribution for the random features + - a second distribution for the linear outer coefficients. + The model is of the form: x = log(1+ exp(g(x))) where g is the RFF model.""" + + def __init__( + self, + rf_dist: td.Distribution, + coeff_dist: td.Distribution, + shapes_dict: dict[str, torch.Size], + bias_dist: Optional[td.Distribution] = None, + length_dist: Optional[td.Distribution] | Optional[float] = None, + out_dist: Optional[td.Distribution] | Optional[float] = None, + angle_dist: Optional[td.Distribution] = None, + log_scale: bool = False, + ): + """ + Args: + rf_dist: Distribution for the random features + coeff_dist: Distribution for the linear outer coefficients + shapes_dict: Shapes of the different variables + bias_dist: Distribution for the bias + length_dist: Distribution for the length scales + out_dist: Distribution for the output scales + angle_dist: Distribution for the angles + log_scale: If True, the output is log(log(1+exp(g(x)))), else it is log(1+exp(g(x))) + """ + + super().__init__(shapes_dict) + self.bias_dist = bias_dist + self.length_dist = length_dist + self.out_dist = out_dist + self.angle_dist = angle_dist + self.rf_dist = rf_dist + self.coeff_dist = coeff_dist + self.log_scale = log_scale + + def sample(self) -> HeteroscedasticRFFFunctionalRelationships: + sample_coeff = self.coeff_dist.sample() + sample_rf = self.rf_dist.sample() + sample_bias = self.bias_dist.sample() if self.bias_dist is not None else None + sample_angle = self.angle_dist.sample() if self.angle_dist is not None else None + + if self.length_dist is not None: + if isinstance(self.length_dist, td.Distribution): + sample_length = self.length_dist.sample() + else: + sample_length = self.length_dist * torch.ones_like(sample_rf[0]) + else: + sample_length = None + + if self.out_dist is not None: + if isinstance(self.out_dist, td.Distribution): + sample_out = self.out_dist.sample() + else: + sample_out = self.out_dist * torch.ones_like(sample_rf[0]) + else: + sample_out = None + + return HeteroscedasticRFFFunctionalRelationships( + shapes=self.shapes_dict, + initial_random_features=sample_rf, + initial_coefficients=sample_coeff, + initial_bias=sample_bias, + initial_length_scales=sample_length, + initial_output_scales=sample_out, + initial_angles=sample_angle, + log_scale=self.log_scale, + ) diff --git a/src/causica/data_generation/samplers/noise_dist_sampler.py b/src/causica/data_generation/samplers/noise_dist_sampler.py index 194f89a..d63fb42 100644 --- a/src/causica/data_generation/samplers/noise_dist_sampler.py +++ b/src/causica/data_generation/samplers/noise_dist_sampler.py @@ -45,44 +45,79 @@ def sample(self) -> JointNoiseModule: class UnivariateNormalNoiseModuleSampler(NoiseModuleSampler): """Sample a UnivariateNormalNoiseModule, with standard deviation given by a distribution.""" - def __init__(self, std_dist: td.Distribution, dim: int = 1): + def __init__(self, std_dist: td.Distribution | float, dim: int = 1): + """ + Args: + std_dist: A distribution of standard deviation, or a constant value. + dim: Dimension + """ super().__init__() - self.std_dist = std_dist + if isinstance(std_dist, float): + self.std_dist: td.Distribution | torch.Tensor = torch.tensor(std_dist) + else: + self.std_dist = std_dist self.dim = dim def sample( self, ): - return UnivariateNormalNoiseModule(dim=self.dim, init_log_scale=torch.log(self.std_dist.sample()).item()) + if isinstance(self.std_dist, torch.Tensor): + log_std_sampled = torch.log(self.std_dist) + else: + log_std_sampled = torch.log(self.std_dist.sample()) + return UnivariateNormalNoiseModule(dim=self.dim, init_log_scale=log_std_sampled) class UnivariateLaplaceNoiseModuleSampler(NoiseModuleSampler): """Sample a UnivariateLaplaceNoiseModule, with standard deviation given by a distribution.""" - def __init__(self, std_dist: td.Distribution, dim: int = 1): + def __init__(self, std_dist: td.Distribution | float, dim: int = 1): + """ + Args: + std_dist: A distribution of standard deviation, or a constant value. + dim: Dimension + """ super().__init__() - self.std_dist = std_dist + + if isinstance(std_dist, float): + self.std_dist: td.Distribution | torch.Tensor = torch.tensor(std_dist) + else: + self.std_dist = std_dist self.dim = dim def sample( self, ): - log_std_sampled = torch.log(self.std_dist.sample()) + if isinstance(self.std_dist, float): + log_std_sampled = torch.log(torch.tensor(self.std_dist)) + else: + log_std_sampled = torch.log(self.std_dist.sample()) return UnivariateLaplaceNoiseModule(dim=self.dim, init_log_scale=log_std_sampled) class UnivariateCauchyNoiseModuleSampler(NoiseModuleSampler): """Sample a UnivariateLaplaceNoiseModule, with standard deviation given by a distribution.""" - def __init__(self, std_dist: td.Distribution, dim: int = 1): + def __init__(self, std_dist: td.Distribution | float, dim: int = 1): + """ + Args: + std_dist: A distribution of standard deviation, or a constant value. + dim: Dimension + """ super().__init__() - self.std_dist = std_dist + if isinstance(std_dist, float): + self.std_dist: td.Distribution | torch.Tensor = torch.tensor(std_dist) + else: + self.std_dist = std_dist self.dim = dim def sample( self, ): - log_std_sampled = torch.log(self.std_dist.sample()) + if isinstance(self.std_dist, torch.Tensor): + log_std_sampled = torch.log(self.std_dist) + else: + log_std_sampled = torch.log(self.std_dist.sample()) return UnivariateCauchyNoiseModule(dim=self.dim, init_log_scale=log_std_sampled) diff --git a/src/causica/data_generation/samplers/sem_sampler.py b/src/causica/data_generation/samplers/sem_sampler.py index 420b11b..a97f0ae 100644 --- a/src/causica/data_generation/samplers/sem_sampler.py +++ b/src/causica/data_generation/samplers/sem_sampler.py @@ -1,3 +1,5 @@ +from typing import Optional + import torch from causica.data_generation.samplers.functional_relationships_sampler import FunctionalRelationshipsSampler @@ -15,16 +17,26 @@ def __init__( adjacency_dist: AdjacencyDistribution, joint_noise_module_sampler: JointNoiseModuleSampler, functional_relationships_sampler: FunctionalRelationshipsSampler, + log_functional_rescaling_sampler: Optional[FunctionalRelationshipsSampler] = None, ): self.adjacency_dist = adjacency_dist self.joint_noise_module_sampler = joint_noise_module_sampler self.functional_relationships_sampler = functional_relationships_sampler + self.log_functional_rescaling_sampler = log_functional_rescaling_sampler self.shapes_dict: dict[str, torch.Size] = functional_relationships_sampler.shapes_dict def sample(self): adjacency_matrix = self.adjacency_dist.sample() functional_relationships = self.functional_relationships_sampler.sample() + log_func_rescale = ( + self.log_functional_rescaling_sampler.sample() + if self.log_functional_rescaling_sampler is not None + else None + ) joint_noise_module = self.joint_noise_module_sampler.sample() return DistributionParametersSEM( - graph=adjacency_matrix, noise_dist=joint_noise_module, func=functional_relationships + graph=adjacency_matrix, + noise_dist=joint_noise_module, + func=functional_relationships, + log_func_rescale=log_func_rescale, ) diff --git a/src/causica/datasets/causica_dataset_format/load.py b/src/causica/datasets/causica_dataset_format/load.py index 5e9ddf7..78bfe3c 100644 --- a/src/causica/datasets/causica_dataset_format/load.py +++ b/src/causica/datasets/causica_dataset_format/load.py @@ -110,7 +110,7 @@ def load_data( match data_enum: case (DataEnum.TRAIN | DataEnum.TEST | DataEnum.VALIDATION): arr = np.loadtxt(f, delimiter=",") - categorical_sizes = _get_categorical_sizes(variables_list=variables_metadata.variables) + categorical_sizes = get_categorical_sizes(variables_list=variables_metadata.variables) return convert_one_hot( tensordict_from_variables_metadata(arr, variables_metadata.variables), one_hot_sizes=categorical_sizes, @@ -241,7 +241,7 @@ def _to_intervention( {node_name: first_row[node_name] for node_name in condition_nodes}, batch_size=tuple() ) - categorical_sizes = _get_categorical_sizes(variables_list=variables_list) + categorical_sizes = get_categorical_sizes(variables_list=variables_list) return InterventionData( intervention_values=convert_one_hot( @@ -265,7 +265,7 @@ def _to_counterfactual( ) factual_data = tensordict_from_variables_metadata(base_data, variables_list=variables_list) - categorical_sizes = _get_categorical_sizes(variables_list=variables_list) + categorical_sizes = get_categorical_sizes(variables_list=variables_list) return CounterfactualData( intervention_values=convert_one_hot( @@ -276,7 +276,8 @@ def _to_counterfactual( ) -def _get_categorical_sizes(variables_list: list[Variable]) -> dict[str, int]: +def get_categorical_sizes(variables_list: list[Variable]) -> dict[str, int]: + """Returns the number of categories of each categorical variable.""" categorical_sizes = {} for item in variables_list: if item.type == VariableTypeEnum.CATEGORICAL: @@ -290,15 +291,17 @@ def _get_categorical_sizes(variables_list: list[Variable]) -> dict[str, int]: return categorical_sizes -def tensordict_from_variables_metadata(data: np.ndarray, variables_list: list[Variable]) -> TensorDict: +def tensordict_from_variables_metadata(data: np.ndarray | torch.Tensor, variables_list: list[Variable]) -> TensorDict: """Returns a tensor created by concatenating all values along the last dim.""" - assert data.ndim == 2, "Numpy loading only supported for 2d data" + if data.ndim != 2: + raise ValueError("Creating a TensorDict for tabular data is only supported for 2D data") batch_size = data.shape[0] - # guaranteed to be ordered correctly in python 3.7+ https://docs.python.org/3/library/collections.html#collections.Counter + # Ordered correctly in python 3.7+ https://docs.python.org/3/library/collections.html#collections.Counter sizes = Counter(d.group_name for d in variables_list) # get the dimensions of each key from the variables sum_sizes = sum(sizes.values()) - assert sum_sizes == data.shape[1], f"Variable sizes do not match data shape, got {sum_sizes} and {data.shape}" + if sum_sizes != data.shape[1]: + raise ValueError(f"Variable sizes do not match data shape, got {sum_sizes} and {data.shape}") # NOTE: This assumes that variables in the same group will have the same type. dtypes = {item.group_name: DTYPE_MAP[item.type] for item in variables_list} diff --git a/src/causica/datasets/synthetic_dataset.py b/src/causica/datasets/synthetic_dataset.py index dac2240..5d36384 100644 --- a/src/causica/datasets/synthetic_dataset.py +++ b/src/causica/datasets/synthetic_dataset.py @@ -1,5 +1,3 @@ -from typing import Iterable - import torch from torch.utils.data import Dataset @@ -28,6 +26,8 @@ def __init__( num_sems: int = 0, sample_interventions: bool = False, sample_counterfactuals: bool = False, + treatment_variable: str | None = None, + effect_variables: list[str] | None = None, ): """ Args: @@ -41,6 +41,10 @@ def __init__( num_sems: The number of sems to sample the data from. If 0, each data sample is generated from a new SEM. sample_interventions: Whether to sample interventions. sample_counterfactuals: Whether to sample counterfactuals. + treatment_variable: This specify the name of the nodes to be taken as treatment from the generated sems. + The sem sampler must always generate this treatment. + effet_variables: This specify the names of the nodes to be taken as effects from the generated sems. + The sem sampler must always generate this effect. """ self.sem_sampler = sem_sampler self.sample_dataset_size = torch.Size([sample_dataset_size]) @@ -50,6 +54,8 @@ def __init__( self.num_sems = num_sems self.sample_interventions = sample_interventions self.sample_counterfactuals = sample_counterfactuals + self.treatment_variable = treatment_variable + self.effect_variables = effect_variables self.sems = [sem_sampler.sample() for _ in range(num_sems)] self.td_to_tensor_transform = TensorToTensorDictTransform(self.sem_sampler.shapes_dict) @@ -82,4 +88,6 @@ def _sample(self, index: int = 0) -> CausalDataset: self.num_intervention_samples, self.sample_interventions, self.sample_counterfactuals, + self.treatment_variable, + self.effect_variables, ) diff --git a/src/causica/datasets/tensordict_utils.py b/src/causica/datasets/tensordict_utils.py index 9cd2f13..08e4c57 100644 --- a/src/causica/datasets/tensordict_utils.py +++ b/src/causica/datasets/tensordict_utils.py @@ -1,14 +1,16 @@ -from typing import Iterable, Optional +from typing import Iterable, Optional, TypeVar import pandas as pd import torch from tensordict.tensordict import TensorDict, TensorDictBase +TD = TypeVar("TD", bound=TensorDictBase) + def convert_one_hot( - data: TensorDict, + data: TD, one_hot_sizes: Optional[dict[str, int]] = None, -): +) -> TD: """ Args: data: A tensordict representing the underlying data diff --git a/src/causica/datasets/timeseries_dataset.py b/src/causica/datasets/timeseries_dataset.py new file mode 100644 index 0000000..397a097 --- /dev/null +++ b/src/causica/datasets/timeseries_dataset.py @@ -0,0 +1,261 @@ +from typing import Type, TypeVar + +import fsspec +import numpy as np +import torch +from tensordict import TensorDictBase +from tensordict.utils import DeviceType +from torch.utils.data import Dataset + +from causica.datasets.causica_dataset_format.load import ( + Variable, + VariablesMetadata, + get_categorical_sizes, + tensordict_from_variables_metadata, +) +from causica.datasets.tensordict_utils import convert_one_hot + + +def load_adjacency_matrix(path: str) -> torch.Tensor: + """Load an adjacency matrix from a file path. + + Args: + path: Path to a .csv or .npy file + + Returns: + Loaded adjacency matrix. + """ + if path.endswith(".csv"): + with fsspec.open_files(path, "r") as files: + (file,) = files + return torch.tensor(np.loadtxt(file, dtype=int, delimiter=",")) + elif path.endswith(".npy"): + with fsspec.open_files(path, "rb") as files: + (file,) = files + return torch.tensor(np.load(file)) + else: + raise ValueError("Unsupported file format.") + + +def ensure_adjacency_matrix( + adjacency_matrix: str | np.ndarray | torch.Tensor, +) -> torch.Tensor: + """Ensure that the adjacency matrix is a tensor. + + Args: + adjacency_matrix: Adjacency matrix or path to CSV file with adjacency matrix. + + Returns: + Adjacency matrix as a tensor. + """ + if isinstance(adjacency_matrix, str): + adjacency_matrix = load_adjacency_matrix(adjacency_matrix) + elif isinstance(adjacency_matrix, np.ndarray): + adjacency_matrix = torch.tensor(adjacency_matrix) + return adjacency_matrix + + +def ensure_variables_metadata( + variables_metadata: str | VariablesMetadata, +) -> VariablesMetadata: + """Ensure that the variables metadata is loaded. + + Args: + variables_metadata: Variables metadata or path to JSON file with variables metadata. + + Returns: + Variables metadata. + """ + if isinstance(variables_metadata, str): + with fsspec.open(variables_metadata, "r") as f: + return VariablesMetadata.from_json(f.read()) # type: ignore + return variables_metadata + + +def preprocess_data( + data: str | np.ndarray | torch.Tensor | TensorDictBase, variables_metadata: VariablesMetadata | None = None +) -> TensorDictBase: + """Preprocess and if necessary load and format the data. + + Args: + data: Data or path to a CSV file with data and no header. + variables_metadata: Variables metadata, if provided will inform the names and preprocessing of variables and + only select the ones present here. Otherwise all will be assumed continuous variables and named by their + index. + + Returns: + Data is formatted as if read by `tensordict_from_variables_metadata`. + """ + # First, collapse types so that data is torch.Tensor | TensorDictBase + if isinstance(data, str): + with fsspec.open(data, "r") as f: + data = np.loadtxt(f, delimiter=",") + if isinstance(data, np.ndarray): + data = torch.from_numpy(data) + + # Then, ensure data is a TensorDict with variables following the metadata + if isinstance(data, torch.Tensor): + if variables_metadata is None: + variables_metadata = VariablesMetadata([Variable(f"x{i}", f"x{i}") for i in range(data.shape[1])]) + data = tensordict_from_variables_metadata(data, variables_metadata.variables) + elif variables_metadata is not None: + data = data.select(*(variable.name for variable in variables_metadata.variables)) + else: # No metadata and data is already a TensorDict + return data + + # One-hot encode categorical variables if indicated by variables metadata + categorical_sizes = get_categorical_sizes(variables_list=variables_metadata.variables) + return convert_one_hot(data, one_hot_sizes=categorical_sizes) + + +TD = TypeVar("TD", bound=TensorDictBase) + + +def index_contiguous_chunks(td: TD, index_key: str) -> tuple[TD, dict[int, slice]]: + """Index a TensorDict into contiguous chunks maintaing the relative order by one of its values. + + Args: + td: TensorDict to index. + index_key: Key to the index value in td. The value must be scalar or 1D apart from its batch shape. + + Example: + >> td = TensorDict({"index": torch.tensor([0, 0, 1, 1, 0, 2, 2]), "x": torch.tensor([4, 5, 6, 7, 8, 9, 10])}) + >> index_contiguous_chunks(td, "index") + ({x: torch.tensor([4, 5, 8, 6, 7, 9, 10])}, {0: slice(0, 3), 1: slice(3, 5), 2: slice(5, 7)}) + + Returns: + A contigous copy of the TensorDict without the index value. + A mapping from index to slice which can be used to directly map to the chunk in the TensorDict. + """ + # Get the series index and sort to maintain order within series but make sure that all series are contiguous + td = td.clone(recurse=False) + index = td.pop(index_key) + if index.dim() > 2 or (index.dim() == 2 and index.shape[1] > 1): + raise ValueError("Pivot index must be scalar or only have one dimension across the batch.") + if index.dim() == 2: + index = index.squeeze(1) + + # Order by the index and build contiguous copies + index, order = torch.sort(index, stable=True) + td = td[order].contiguous() + + # Compute timeseries locations + series_boundaries = torch.diff(index, dim=0) + series_locs, *_ = torch.nonzero(series_boundaries, as_tuple=True) + # Add the first and last index to build the ranges + series_ranges = torch.cat( + [ + torch.zeros((1,), dtype=series_locs.dtype), + series_locs + 1, + torch.tensor([len(index)], dtype=series_locs.dtype), + ] + ) + + # Build a mapping to lookup indices quickly + return td, { + index: slice(a.item(), b.item()) for index, (a, b) in enumerate(zip(series_ranges[:-1], series_ranges[1:])) + } + + +CLS = TypeVar("CLS", bound="IndexedTimeseriesDataset") + + +class IndexedTimeseriesDataset(Dataset): + """Represents a timeseries dataset where each series is indexed by a unique identifier and the steps given by order. + + The raw dataset can be provided in the following formats: + - TensorDict: the expected batch size is the total number of steps from all series, and the key / values refer + to individual features. + - Tensor or filepaths: a tensor type or a path to a CSV file with a table. Both with the expected shape + [`total number of steps from all series`, `num features`]. + + The relative order of each series is maintened and the provided dataset must therefore already be in timestep order. + """ + + @torch.no_grad() + def __init__( + self, + series_index_key: str | int, + data: str | np.ndarray | torch.Tensor | TensorDictBase, + adjacency_matrix: str | np.ndarray | torch.Tensor, + variables_metadata: str | VariablesMetadata | None = None, + device: None | DeviceType = None, + ) -> None: + """ + Args: + series_index_key: Name of the variable that contains the series index or the index to the key / column. + data: Data or path to the CSV file with data. + adjacency_matrix: Adjacency matrix or path to CSV file with adjacency matrix. + variables_metadata: Variables metadata or path to JSON file with variables metadata. If not provided and + variable names cannot be inferred from the data, the variables will be named x0, x1, ... in order and + interpreted as continuous variables. + device: Device to move the data to. + """ + super().__init__() + + # Load and fix types of inputs + if variables_metadata is not None: + variables_metadata = ensure_variables_metadata(variables_metadata) + adjacency_matrix = ensure_adjacency_matrix(adjacency_matrix) + data = preprocess_data(data, variables_metadata) + if isinstance(series_index_key, int): + series_index_key = list(data.keys())[series_index_key] + assert isinstance(series_index_key, str) + + data[series_index_key] = data[series_index_key].to(dtype=torch.int64) + if device is not None: + data = data.to(device) + adjacency_matrix = adjacency_matrix.to(device=device) + + self._data, self._series_lookup = index_contiguous_chunks(data, series_index_key) + self._adjacency_matrix = adjacency_matrix + + @classmethod + def from_dense(cls: Type[CLS], data: torch.Tensor, lengths: torch.Tensor, **kwargs) -> CLS: + """Create an indexed timeseries dataset from a dense representation of data with variable lengths. + + With this representation each timeseries i contains the data of data[i, lengths[i]]. + + Args: + data: Dense timeseries data of shape (num_timeseries, max_length, num_dims). + lengths: Length of each timeseries in the data, shape (num_timeseries, ). + **kwargs: Forwarded to `__init__`. + + Returns: + IndexedTimeseriesDataset with the given data. + """ + if data.dim() != 3: + raise ValueError(f"The expected shape of data is (num_timeseries, max_length, num_dims), got {data.shape}") + if lengths.dim() != 1: + raise ValueError(f"The expected shape of lengths is (num_timeseries, ), got {lengths.shape}") + if data.shape[0] != lengths.shape[0]: + raise ValueError( + f"The number of timeseries in data ({data.shape[0]}) does not match the number of entries " + f"in lengths ({lengths.shape[0]})" + ) + + num_timeseries, max_length, num_dims = data.shape + index = torch.arange(num_timeseries)[:, None].expand(num_timeseries, max_length) + + # Create sequence mask of shape (num_timeseries, max_length, num_dims) which select sequences from start to end, + # and where all entries have the same value along the last axis. + mask = torch.arange(max_length)[None, :, None].expand(1, max_length, num_dims) < lengths[:, None, None] + + # Use mask to create sparse data + sparse_data = torch.cat([data[mask].reshape(-1, num_dims), index[mask[:, :, 0]].reshape(-1, 1)], dim=-1) + return cls(series_index_key=num_dims, data=sparse_data, **kwargs) + + def __getitem__(self, idx) -> TensorDictBase: + """Get timeseries by their relative index. + + Args: + idx: Relative index in [0, len(self)] + + Returns: + The full timeseries TensorDict(..., batch_size=`number of steps`). + """ + return self._data[self._series_lookup[idx]] + + def __len__(self) -> int: + """The total number of timeseries in the dataset.""" + return len(self._series_lookup) diff --git a/src/causica/distributions/__init__.py b/src/causica/distributions/__init__.py index af35e7d..5cf5bf4 100644 --- a/src/causica/distributions/__init__.py +++ b/src/causica/distributions/__init__.py @@ -2,12 +2,20 @@ AdjacencyDistribution, ConstrainedAdjacency, ConstrainedAdjacencyDistribution, + EdgesPerNodeErdosRenyiDAGDistribution, ENCOAdjacencyDistribution, ENCOAdjacencyDistributionModule, ErdosRenyiDAGDistribution, ExpertGraphContainer, + GeometricRandomGraphDAGDistribution, GibbsDAGPrior, + RhinoLaggedAdjacencyDistributionModule, + ScaleFreeDAGDistribution, + StochasticBlockModelDAGDistribution, + TemporalAdjacencyDistributionModule, + TemporalConstrainedAdjacencyDistribution, ThreeWayAdjacencyDistribution, + WattsStrogatzDAGDistribution, ) from causica.distributions.distribution_module import DistributionModule from causica.distributions.noise import ( @@ -23,8 +31,15 @@ NoiseModule, SplineNoise, SplineNoiseModule, + UnivariateCauchyNoise, + UnivariateCauchyNoiseModule, + UnivariateLaplaceNoise, + UnivariateLaplaceNoiseModule, + UnivariateLaplaceNoiseRescaled, UnivariateNormalNoise, UnivariateNormalNoiseModule, + UnivariateNormalNoiseRescaled, create_noise_modules, create_spline_dist_params, ) +from causica.distributions.signed_uniform import MultivariateSignedUniform diff --git a/src/causica/distributions/adjacency/__init__.py b/src/causica/distributions/adjacency/__init__.py index 1430d2e..464f247 100644 --- a/src/causica/distributions/adjacency/__init__.py +++ b/src/causica/distributions/adjacency/__init__.py @@ -2,9 +2,24 @@ from causica.distributions.adjacency.constrained_adjacency_distributions import ( ConstrainedAdjacency, ConstrainedAdjacencyDistribution, + LaggedConstrainedAdjacencyDistribution, + TemporalConstrainedAdjacency, + TemporalConstrainedAdjacencyDistribution, ) -from causica.distributions.adjacency.directed_acyclic import ErdosRenyiDAGDistribution +from causica.distributions.adjacency.edges_per_node_erdos_renyi import EdgesPerNodeErdosRenyiDAGDistribution from causica.distributions.adjacency.enco import ENCOAdjacencyDistribution, ENCOAdjacencyDistributionModule +from causica.distributions.adjacency.erdos_renyi import ErdosRenyiDAGDistribution from causica.distributions.adjacency.fixed_adjacency_distribution import FixedAdjacencyDistribution +from causica.distributions.adjacency.geometric_random_graph import GeometricRandomGraphDAGDistribution from causica.distributions.adjacency.gibbs_dag_prior import ExpertGraphContainer, GibbsDAGPrior +from causica.distributions.adjacency.scale_free import ScaleFreeDAGDistribution +from causica.distributions.adjacency.stochastic_block_model import StochasticBlockModelDAGDistribution +from causica.distributions.adjacency.temporal_adjacency_distributions import ( + LaggedAdjacencyDistribution, + RhinoLaggedAdjacencyDistribution, + RhinoLaggedAdjacencyDistributionModule, + TemporalAdjacencyDistribution, + TemporalAdjacencyDistributionModule, +) from causica.distributions.adjacency.three_way import ThreeWayAdjacencyDistribution +from causica.distributions.adjacency.watts_strogatz import WattsStrogatzDAGDistribution diff --git a/src/causica/distributions/adjacency/constrained_adjacency_distributions.py b/src/causica/distributions/adjacency/constrained_adjacency_distributions.py index 48968b9..e50d212 100644 --- a/src/causica/distributions/adjacency/constrained_adjacency_distributions.py +++ b/src/causica/distributions/adjacency/constrained_adjacency_distributions.py @@ -1,10 +1,14 @@ from functools import partial -from typing import Callable, Type +from typing import Callable, Optional, Type, Union import torch import torch.distributions as td from causica.distributions.adjacency.adjacency_distributions import AdjacencyDistribution +from causica.distributions.adjacency.temporal_adjacency_distributions import ( + LaggedAdjacencyDistribution, + TemporalAdjacencyDistribution, +) from causica.distributions.distribution_module import DistributionModule @@ -20,7 +24,10 @@ class ConstrainedAdjacencyDistribution(AdjacencyDistribution): arg_constraints = {"positive_constraints": td.constraints.boolean, "negative_constraints": td.constraints.boolean} def __init__( - self, dist: AdjacencyDistribution, positive_constraints: torch.Tensor, negative_constraints: torch.Tensor + self, + dist: AdjacencyDistribution | LaggedAdjacencyDistribution, + positive_constraints: torch.Tensor, + negative_constraints: torch.Tensor, ): """ Args: @@ -113,6 +120,83 @@ def _apply_constraints(self, G: torch.Tensor) -> torch.Tensor: return 1.0 - (1.0 - G * self.negative_constraints) * (~self.positive_constraints) +class LaggedConstrainedAdjacencyDistribution(LaggedAdjacencyDistribution, ConstrainedAdjacencyDistribution): + """Adjacency distribution that applies hard constraints to a temporal base lagged distribution. + + The expected shape of adjacency matrices in the base distribution is batch_shape + [lag, nodes, ndoes], where + lag = context_length - 1, and refers to edges from past timesteps. This distribution applies constraints and + overrides the graph produced from the base adjacency distribution with + - 0 when the corresponding negative_constraint=0 + - 1 when the corresponding positive constraint=1 + - unmodified: all other elements + + See also: + ConstrainedAdjacencyDistribution: Similar for non-temporal graphs. + + """ + + def __init__( + self, dist: LaggedAdjacencyDistribution, positive_constraints: torch.Tensor, negative_constraints: torch.Tensor + ): + """ + Args: + dist (LaggedAdjacencyDistribution): Base lagged adj distribution, event shape ([lags, nodes, nodes]) matching the postive and negative constraints. + positive_constraints (torch.Tensor): Positive constraints. 1 means edge is present. + negative_constraints (torch.Tensor): Negative constraints. 0 means edge is not present. + """ + if not dist.event_shape == positive_constraints.shape == negative_constraints.shape: + raise ValueError("The constraints must match the event shape of the distribution.") + self.dist = dist + self.positive_constraints = positive_constraints + self.negative_constraints = negative_constraints + super().__init__(num_nodes=self.dist.num_nodes, lags=self.dist.lags) + + +class TemporalConstrainedAdjacencyDistribution(TemporalAdjacencyDistribution): + """Temporal Adjacency distribution that applies hard constraints to a base distribution. + + This relies on the two constrained base distributions: ConstrainedAdjacencyDistribution for instantaneous graph + and LaggedConstrainedAdjacencyDistribution for lagged graph. For detailed explanation of constraint distributions, + see ConstrainedAdjacencyDistribution and LaggedConstrainedAdjacencyDistribution. + + """ + + arg_constraints = {"positive_constraints": td.constraints.boolean, "negative_constraints": td.constraints.boolean} + + def __init__( + self, + dist: TemporalAdjacencyDistribution, + positive_constraints: torch.Tensor, + negative_constraints: torch.Tensor, + validate_args: Optional[bool] = None, + ): + """ + Args: + dist (TemporalAdjacencyDistribution): Base temporal adjacency distribution, event shape matching the postive + and negative constraints. + positive_constraints (torch.Tensor): Positive constraints with shape [context_length, nodes, nodes]. + 1 means edge is present. + negative_constraints (torch.Tensor): Negative constraintswith shape [context_length, nodes, nodes]. + 0 means edge is not present. + """ + self.positive_constraints = positive_constraints + self.negative_constraints = negative_constraints + constrained_inst_dist = ConstrainedAdjacencyDistribution( + dist.inst_dist, positive_constraints[..., -1, :, :], negative_constraints[..., -1, :, :] + ) + if dist.lagged_dist is not None: + constrained_lagged_dist = LaggedConstrainedAdjacencyDistribution( + dist.lagged_dist, positive_constraints[..., :-1, :, :], negative_constraints[..., :-1, :, :] + ) + else: + constrained_lagged_dist = None + super().__init__( + instantaneous_distribution=constrained_inst_dist, + lagged_distribution=constrained_lagged_dist, + validate_args=validate_args, + ) + + def get_graph_constraint(graph_constraint_matrix: torch.Tensor) -> tuple[torch.Tensor, torch.Tensor]: """Converts graph constraint matrix into a positive and negative matrix for easier usage. @@ -128,7 +212,6 @@ def get_graph_constraint(graph_constraint_matrix: torch.Tensor) -> tuple[torch.T assert graph_constraint_matrix.shape[0] == graph_constraint_matrix.shape[1], "Constraint matrix must be square." # Mask self-edges mask = ~torch.eye(graph_constraint_matrix.shape[0], dtype=torch.bool, device=graph_constraint_matrix.device) - positive_constraints = mask * torch.nan_to_num(graph_constraint_matrix, nan=0).to( dtype=torch.bool, non_blocking=True ) @@ -136,32 +219,82 @@ def get_graph_constraint(graph_constraint_matrix: torch.Tensor) -> tuple[torch.T return positive_constraints, negative_constraints +def get_temporal_graph_constraint(temporal_graph_constraint_matrix: torch.Tensor) -> tuple[torch.Tensor, torch.Tensor]: + """Convert temporal graph constraint matrix into a positive and negative constraint matrix for easier usage. + + See also: + get_graph_constraint: Similar for non-temporal graphs. + + Args: + temporal_graph_constraint_matrix: Temporal graph constraints: 0 = no edge, 1 = edge, nan: no constraint. + Should be a 3D tensor with shape [context_length, num_nodes, num_nodes]. + + Returns: + A tuple of (positive_constraints, negative_constraint). See TemporalConstrainedAdjacencyDistribution for their + interpretation. + """ + assert ( + temporal_graph_constraint_matrix.ndim == 3 + ), "Temporal constraint matrix must be 3D, with shape [context_length, num_nodes, num_nodes]." + assert ( + temporal_graph_constraint_matrix.shape[1] == temporal_graph_constraint_matrix.shape[2] + ), "Constraint matrix for each time must be square." + assert ( + temporal_graph_constraint_matrix.shape[0] >= 0 + ), f"context_length in constraint matrix must be non-negative, but got {temporal_graph_constraint_matrix.shape[0]}" + # Mask self-edges in instantaneous graph + mask = ~torch.eye( + temporal_graph_constraint_matrix.shape[1], dtype=torch.bool, device=temporal_graph_constraint_matrix.device + ) # [nodes, nodes] + # Create positive and negative constraints + positive_constraints = torch.nan_to_num(temporal_graph_constraint_matrix, nan=0).to( + dtype=torch.bool, non_blocking=True + ) + positive_constraints[..., -1, :, :] = positive_constraints[..., -1, :, :] * mask + negative_constraints = torch.nan_to_num(temporal_graph_constraint_matrix, nan=1).to( + dtype=torch.bool, non_blocking=True + ) + return positive_constraints, negative_constraints + + def _create_distribution( - dist_class: Type[AdjacencyDistribution], *args, graph_constraint_matrix: torch.Tensor, **kwargs -) -> ConstrainedAdjacencyDistribution: + dist_class: Type[Union[AdjacencyDistribution, TemporalAdjacencyDistribution]], + *args, + graph_constraint_matrix: torch.Tensor, + **kwargs, +) -> Union[ConstrainedAdjacencyDistribution, TemporalConstrainedAdjacencyDistribution]: """Utility function for generating a constrained adjacency distribution with a base distribution. Args: - dist_class (Type[AdjacencyDistribution]): Type of the base adjacency distribution. + dist_class (Type[Union[AdjacencyDistribution, TemporalAdjacencyDistribution]): Type of the base adjacency distribution. graph_constraint_matrix: Graph constraints: 0 = no edge, 1 = edge, nan: no constraint. Must match the event shape of the distribution. Returns: - ConstrainedAdjacencyDistribution: Constrained adjacency distribution. + Union[ConstrainedAdjacencyDistribution, TemporalConstrainedAdjacencyDistribution]: (Temporal) Constrained adjacency distribution. """ - positive_constraints, negative_constraints = get_graph_constraint(graph_constraint_matrix) + positive_constraints, negative_constraints = ( + get_temporal_graph_constraint(graph_constraint_matrix) + if issubclass(dist_class, TemporalAdjacencyDistribution) + else get_graph_constraint(graph_constraint_matrix) + ) dist = dist_class(*args, **kwargs) + if isinstance(dist, TemporalAdjacencyDistribution): + return TemporalConstrainedAdjacencyDistribution( + dist, positive_constraints=positive_constraints, negative_constraints=negative_constraints + ) + return ConstrainedAdjacencyDistribution( dist, positive_constraints=positive_constraints, negative_constraints=negative_constraints ) def constrained_adjacency( - dist_class: Type[AdjacencyDistribution], -) -> Callable[..., ConstrainedAdjacencyDistribution]: - """Utility function that returns a function constructing a constrained adjacency distribution. + dist_class: Type[Union[AdjacencyDistribution, TemporalAdjacencyDistribution]], +) -> Callable[..., Union[ConstrainedAdjacencyDistribution, TemporalConstrainedAdjacencyDistribution]]: + """Utility function that returns a function constructing a (temporal) constrained adjacency distribution. Args: dist_class: Type of the base adjacency distribution. @@ -169,7 +302,7 @@ def constrained_adjacency( shape of the distribution. Returns: - Callable[..., ConstrainedAdjacencyDistribution]: Utility function creating a ConstrainedAdjacencyDistribution. + Utility function creating a (Temporal)ConstrainedAdjacencyDistribution. """ return partial( @@ -203,3 +336,33 @@ def forward(self) -> ConstrainedAdjacencyDistribution: positive_constraints=self.positive_constraints, negative_constraints=self.negative_constraints, ) + + +class TemporalConstrainedAdjacency(DistributionModule[TemporalAdjacencyDistribution]): + """A constrained temporal adjacency distribution module where certain parts edges of in the temporal adjacency matrix are locked.""" + + def __init__( + self, + temporal_adjacency_distribution: DistributionModule[TemporalAdjacencyDistribution], + temporal_graph_constraint_matrix: torch.Tensor, + ): + """ + Args: + temporal_adjacency_distribution: Underlying temporal adjacency distribution module. + temporal_graph_constraint_matrix: Constraint matrix with edges defined according to `get_temporal_graph_constraint`. + """ + + super().__init__() + self.temporal_adjacency_distribution = temporal_adjacency_distribution + positive_constraints, negative_constraints = get_temporal_graph_constraint(temporal_graph_constraint_matrix) + self.positive_constraints: torch.Tensor + self.negative_constraints: torch.Tensor + self.register_buffer("positive_constraints", positive_constraints) + self.register_buffer("negative_constraints", negative_constraints) + + def forward(self) -> TemporalConstrainedAdjacencyDistribution: + return TemporalConstrainedAdjacencyDistribution( + self.temporal_adjacency_distribution(), + positive_constraints=self.positive_constraints, + negative_constraints=self.negative_constraints, + ) diff --git a/src/causica/distributions/adjacency/edges_per_node_erdos_renyi.py b/src/causica/distributions/adjacency/edges_per_node_erdos_renyi.py new file mode 100644 index 0000000..eaaea9d --- /dev/null +++ b/src/causica/distributions/adjacency/edges_per_node_erdos_renyi.py @@ -0,0 +1,93 @@ +import igraph as ig +import numpy as np +import torch + +from causica.distributions.adjacency.adjacency_distributions import AdjacencyDistribution + + +class EdgesPerNodeErdosRenyiDAGDistribution(AdjacencyDistribution): + """ + An adjacency distribution for sampling Directed Acyclic Graphs using Erdos-Renyi. + + It allows to sample DAGs according to the ER distribution with a fixed number of edges per node. + + Re-implementation in pytorch from AVICI: + + https://github.com/larslorch/avici + + """ + + def __init__( + self, + num_nodes: int, + edges_per_node: list[int], + validate_args: bool | None = None, + ): + """ + Args: + num_nodes: the number of nodes in the DAGs to be sampled + edges_per_node: List of the number of edges per node to sample. + """ + assert num_nodes > 0, "Number of nodes in the graph must be greater than 0" + super().__init__(num_nodes=num_nodes, validate_args=validate_args) + self.edges_per_node = edges_per_node + + def _one_sample(self): + """ + Sample a binary adjacency matrix from the underlying distribution. + + Returns: + A tensor of shape (num_nodes, num_nodes) + """ + + get_edges_per_node = np.random.choice(self.edges_per_node).item() + n_edges = get_edges_per_node * self.num_nodes + prob = min(n_edges / ((self.num_nodes * (self.num_nodes - 1)) / 2), 0.99) + + mat = np.random.binomial(n=1, p=prob, size=(self.num_nodes, self.num_nodes)).astype(int) # bernoulli + + # make DAG by zeroing above diagonal; k=-1 indicates that diagonal is zero too + dag = np.tril(mat, k=-1) + + # randomly permute + perm = np.random.permutation(self.num_nodes).tolist() + dag = dag[perm, :][:, perm] + + if not ig.Graph.Weighted_Adjacency(dag.tolist()).is_dag(): + raise ValueError("Sampled graph is not a DAG") + + dag = torch.tensor(dag, dtype=torch.float32) + + return dag + + def relaxed_sample(self, sample_shape: torch.Size = torch.Size(), temperature: float = 0.0) -> torch.Tensor: + raise NotImplementedError + + def sample(self, sample_shape: torch.Size = torch.Size()): + """ + Sample binary adjacency matrices from the underlying distribution. + + Args: + sample_shape: The shape of the samples to be drawn. + + Returns: + A tensor of shape (sample_shape, num_nodes, num_nodes) + """ + num_samples = int(torch.prod(torch.tensor(sample_shape)).item()) + return torch.stack([self._one_sample() for _ in range(num_samples)]).reshape( + sample_shape + (self.num_nodes, self.num_nodes) + ) + + def entropy(self) -> torch.Tensor: + raise NotImplementedError + + @property + def mean(self) -> torch.Tensor: + raise NotImplementedError + + @property + def mode(self) -> torch.Tensor: + raise NotImplementedError + + def log_prob(self, value: torch.Tensor) -> torch.Tensor: + raise NotImplementedError diff --git a/src/causica/distributions/adjacency/directed_acyclic.py b/src/causica/distributions/adjacency/erdos_renyi.py similarity index 98% rename from src/causica/distributions/adjacency/directed_acyclic.py rename to src/causica/distributions/adjacency/erdos_renyi.py index 1e61b52..2473b1f 100644 --- a/src/causica/distributions/adjacency/directed_acyclic.py +++ b/src/causica/distributions/adjacency/erdos_renyi.py @@ -28,6 +28,7 @@ def __init__( num_nodes: the number of nodes in the DAGs to be sampled probs: A tensor of the probability that an edge exists between 2 nodes of shape batch_shape. If None, calculate p from num_edges num_edges: The number of edges to sample. If None, sample from a binomial distribution with p=probs + validate_args: Arguments from AdjacencyDistribution """ assert num_nodes > 0, "Number of nodes in the graph must be greater than 0" diff --git a/src/causica/distributions/adjacency/geometric_random_graph.py b/src/causica/distributions/adjacency/geometric_random_graph.py new file mode 100644 index 0000000..82a5c11 --- /dev/null +++ b/src/causica/distributions/adjacency/geometric_random_graph.py @@ -0,0 +1,82 @@ +import igraph as ig +import numpy as np +import torch + +from causica.distributions.adjacency.adjacency_distributions import AdjacencyDistribution + + +class GeometricRandomGraphDAGDistribution(AdjacencyDistribution): + """ + An adjacency distribution for sampling Directed Acyclic Graphs using GRG. + + Re-implementation in pytorch from AVICI: + + https://github.com/larslorch/avici + + """ + + def __init__( + self, + num_nodes: int, + radius: list[float], + validate_args: bool | None = None, + ): + """ + Args: + num_nodes: the number of nodes in the DAGs to be sampled + radius: List of the radius of the geometric random graph + validate_args: Optional arguments from AdjacencyDistribution + """ + assert num_nodes > 0, "Number of nodes in the graph must be greater than 0" + super().__init__(num_nodes=num_nodes, validate_args=validate_args) + self.radius = radius + + def _one_sample(self): + """ + Sample a binary adjacency matrix from the underlying distribution. + + Returns: + A tensor of shape (num_nodes, num_nodes) + """ + get_radius = np.random.choice(self.radius).item() + perm = np.random.permutation(self.num_nodes).tolist() + graph = ig.Graph.GRG(n=self.num_nodes, radius=get_radius) + graph = np.array(graph.get_adjacency().data).astype(int) + + dag = np.triu(graph, k=1) + dag = dag[perm, :][:, perm] + + if not ig.Graph.Weighted_Adjacency(dag.tolist()).is_dag(): + raise ValueError("Sampled graph is not a DAG") + + dag = torch.tensor(dag, dtype=torch.float32) + return dag + + def relaxed_sample(self, sample_shape: torch.Size = torch.Size(), temperature: float = 0.0) -> torch.Tensor: + raise NotImplementedError + + def sample(self, sample_shape: torch.Size = torch.Size()): + """ + Sample binary adjacency matrices from the underlying distribution. + + Args: + sample_shape: The shape of the samples to be drawn. + """ + num_samples = int(torch.prod(torch.tensor(sample_shape)).item()) + return torch.stack([self._one_sample() for _ in range(num_samples)]).reshape( + sample_shape + (self.num_nodes, self.num_nodes) + ) + + def entropy(self) -> torch.Tensor: + raise NotImplementedError + + @property + def mean(self) -> torch.Tensor: + raise NotImplementedError + + @property + def mode(self) -> torch.Tensor: + raise NotImplementedError + + def log_prob(self, value: torch.Tensor) -> torch.Tensor: + raise NotImplementedError diff --git a/src/causica/distributions/adjacency/gibbs_dag_prior.py b/src/causica/distributions/adjacency/gibbs_dag_prior.py index 4110e40..21a1b2d 100644 --- a/src/causica/distributions/adjacency/gibbs_dag_prior.py +++ b/src/causica/distributions/adjacency/gibbs_dag_prior.py @@ -33,6 +33,9 @@ class GibbsDAGPrior(td.Distribution): A Expert Graph term that represents some known prior belief about the graph. Each term has an associated parameter (lambda) + + The event shape of the prior is either (num_nodes, num_nodes) or (context_length, num_nodes, num_nodes) for temporal + models and is set at initialization. The shape is checked for given matrices. """ arg_constraints: dict = {} @@ -42,22 +45,36 @@ def __init__( num_nodes: int, sparsity_lambda: float, expert_graph_container: Optional[ExpertGraphContainer] = None, - **kwargs + context_length: int | None = None, + **kwargs, ) -> None: """ Args: - num_nodes (int): Number of nodes in the graph - sparsity_lambda (float): Coefficient of sparsity term - dagness_alpha (torch.Tensor): Coefficient of dagness term - dagness_rho (torch.Tensor): Coefficient of squared dagness term - expert_graph_container (ExpertGraphContainer): Dataclass containing prior belief about the real graph + num_nodes: Number of nodes in the graph + sparsity_lambda: Coefficient of sparsity term + dagness_alpha: Coefficient of dagness term + dagness_rho: Coefficient of squared dagness term + expert_graph_container: Dataclass containing prior belief about the real graph + context_length: Optional, specifying the context_length in temporal models """ - super().__init__(torch.Size(), event_shape=torch.Size((num_nodes, num_nodes)), **kwargs) + event_shape = (num_nodes, num_nodes) if context_length is None else (context_length, num_nodes, num_nodes) + super().__init__(torch.Size(), event_shape=torch.Size(event_shape), **kwargs) + + if expert_graph_container is not None: + if expert_graph_container.dag.shape != event_shape: + raise ValueError( + f"Expert graph shape {expert_graph_container.dag.shape} does not match event shape {event_shape}" + ) + if expert_graph_container.mask.shape != event_shape: + raise ValueError( + f"Expert graph mask shape {expert_graph_container.mask.shape} does not match event shape {event_shape}" + ) self._num_nodes = num_nodes self._expert_graph_container = expert_graph_container self._sparsity_lambda = sparsity_lambda + self._context_length = context_length def get_sparsity_term(self, A: torch.Tensor) -> torch.Tensor: """ @@ -65,7 +82,7 @@ def get_sparsity_term(self, A: torch.Tensor) -> torch.Tensor: The term is small when A is sparse. Args: - A (torch.Tensor): Adjacency matrix of shape (input_dim, input_dim). + A (torch.Tensor): Adjacency matrix of shape event_shape. Returns: Sparsity term. @@ -77,13 +94,14 @@ def get_expert_graph_term(self, A: torch.Tensor) -> torch.Tensor: A term that encourages A to be close to given expert graph. Args: - A (torch.Tensor): Adjacency matrix of shape (input_dim, input_dim). + A (torch.Tensor): Adjacency matrix of shape event_shape. Returns: (torch.Tensor): Expert graph term. """ assert isinstance(self._expert_graph_container, ExpertGraphContainer) + assert A.shape[-len(self.event_shape) :] == self.event_shape return ( ( self._expert_graph_container.mask @@ -99,13 +117,13 @@ def log_prob(self, value: torch.Tensor) -> torch.Tensor: under the distribution given by this instance. Args: - value (torch.Tensor): Adjacency matrix of shape (input_dim, input_dim). + value (torch.Tensor): Adjacency matrix of shape event_shape. Returns: (torch.Tensor): The un-normalized log probability of `value`. """ - assert value.shape[-2:] == (self._num_nodes, self._num_nodes) + assert value.shape[-len(self.event_shape) :] == self.event_shape log_prob = -self._sparsity_lambda * self.get_sparsity_term(value) diff --git a/src/causica/distributions/adjacency/scale_free.py b/src/causica/distributions/adjacency/scale_free.py new file mode 100644 index 0000000..243450e --- /dev/null +++ b/src/causica/distributions/adjacency/scale_free.py @@ -0,0 +1,92 @@ +import igraph as ig +import numpy as np +import torch + +from causica.distributions.adjacency.adjacency_distributions import AdjacencyDistribution + + +class ScaleFreeDAGDistribution(AdjacencyDistribution): + """ + An adjacency distribution for sampling Directed Acyclic Graphs using Barabasi Albert. + + Re-implementation in pytorch from AVICI: + + https://github.com/larslorch/avici + + """ + + def __init__( + self, + num_nodes: int, + edges_per_node: list[int], + power: list[float], + in_degree: bool = True, + validate_args: bool | None = None, + ): + """ + Args: + num_nodes: the number of nodes in the DAGs to be sampled + edges_per_node: List of the number of edges per node to sample. + power: List of the power of the scale free distribution. + in_degree: If True, the in-degree of each node is sampled from the power law distribution. + Otherwise, the out-degree is sampled. + validate_args: Optional arguments from AdjacencyDistribution + """ + assert num_nodes > 0, "Number of nodes in the graph must be greater than 0" + super().__init__(num_nodes=num_nodes, validate_args=validate_args) + self.edges_per_node = edges_per_node + self.power = power + self.in_degree = in_degree + + def _one_sample(self): + """ + Sample a binary adjacency matrix from the underlying distribution. + + Returns: + A tensor of shape (num_nodes, num_nodes) + """ + + get_edges_per_node = np.random.choice(self.edges_per_node).item() + get_power = np.random.choice(self.power).item() + perm = np.random.permutation(self.num_nodes).tolist() + graph = ig.Graph.Barabasi( + n=self.num_nodes, m=get_edges_per_node, directed=True, power=get_power + ).permute_vertices(perm) + dag = np.array(graph.get_adjacency().data).astype(int) + if not self.in_degree: + dag = dag.T + + if not ig.Graph.Weighted_Adjacency(dag.tolist()).is_dag(): + raise ValueError("Sampled graph is not a DAG") + + dag = torch.tensor(dag, dtype=torch.float32) + return dag + + def relaxed_sample(self, sample_shape: torch.Size = torch.Size(), temperature: float = 0.0) -> torch.Tensor: + raise NotImplementedError + + def sample(self, sample_shape: torch.Size = torch.Size()): + """ + Sample binary adjacency matrices from the underlying distribution. + + Args: + sample_shape: The shape of the samples to be drawn. + """ + num_samples = int(torch.prod(torch.tensor(sample_shape)).item()) + return torch.stack([self._one_sample() for _ in range(num_samples)]).reshape( + sample_shape + (self.num_nodes, self.num_nodes) + ) + + def entropy(self) -> torch.Tensor: + raise NotImplementedError + + @property + def mean(self) -> torch.Tensor: + raise NotImplementedError + + @property + def mode(self) -> torch.Tensor: + raise NotImplementedError + + def log_prob(self, value: torch.Tensor) -> torch.Tensor: + raise NotImplementedError diff --git a/src/causica/distributions/adjacency/stochastic_block_model.py b/src/causica/distributions/adjacency/stochastic_block_model.py new file mode 100644 index 0000000..05220a1 --- /dev/null +++ b/src/causica/distributions/adjacency/stochastic_block_model.py @@ -0,0 +1,118 @@ +import igraph as ig +import numpy as np +import torch + +from causica.distributions.adjacency.adjacency_distributions import AdjacencyDistribution + + +class StochasticBlockModelDAGDistribution(AdjacencyDistribution): + """ + An adjacency distribution for sampling Directed Acyclic Graphs using SBM. + + Re-implementation in pytorch from AVICI: + + https://github.com/larslorch/avici + + + """ + + def __init__( + self, + num_nodes: int, + edges_per_node: list[int], + num_blocks: list[int], + damping: list[float], + validate_args: bool | None = None, + ): + """ + Args: + num_nodes: the number of nodes in the DAGs to be sampled + edges_per_node: List of the number of edges per node to sample. + num_blocks: List of the number of blocks in the model + damp: List of the damp factor for inter block edges. damp = 1.0 is equivalent to erdos renyi + validate_args: Optional arguments from AdjacencyDistribution + """ + assert num_nodes > 0, "Number of nodes in the graph must be greater than 0" + super().__init__(num_nodes=num_nodes, validate_args=validate_args) + self.edges_per_node = edges_per_node + self.num_blocks = num_blocks + self.damping = damping + + def _one_sample(self): + """ + Sample a binary adjacency matrix from the underlying distribution. + + Returns: + A tensor of shape (num_nodes, num_nodes) + """ + get_edges_per_node = np.random.choice(self.edges_per_node) + get_num_block = np.random.choice(self.num_blocks) + get_damping = np.random.choice(self.damping) + + # sample blocks + splits = np.sort(np.random.choice(self.num_nodes, size=get_num_block - 1, replace=False)) + blocks = np.split(np.random.permutation(self.num_nodes), splits) + block_sizes = np.array([b.shape[0] for b in blocks]) + + # select p s.t. we get requested edges_per_var in expectation + block_edges_sampled = (np.outer(block_sizes, block_sizes) - np.diag(block_sizes)) / 2 + relative_block_probs = np.eye(get_num_block) + get_damping * (1 - np.eye(get_num_block)) + n_edges = get_edges_per_node * self.num_nodes + p = min(0.99, n_edges / np.sum(block_edges_sampled * relative_block_probs)) + + # sample graph + mat_intra = np.random.binomial(n=1, p=p, size=(self.num_nodes, self.num_nodes)).astype(int) # bernoulli + mat_inter = np.random.binomial(n=1, p=get_damping * p, size=(self.num_nodes, self.num_nodes)).astype( + int + ) # bernoulli + + mat = np.zeros((self.num_nodes, self.num_nodes)) + for i, bi in enumerate(blocks): + for j, bj in enumerate(blocks): + mat[np.ix_(bi, bj)] = (mat_intra if i == j else mat_inter)[np.ix_(bi, bj)] + + # make directed + dag = np.triu(mat, k=1) + + # randomly permute + perm = np.random.permutation(self.num_nodes).tolist() + dag = dag[perm, :][:, perm] + + if not ig.Graph.Weighted_Adjacency(dag.tolist()).is_dag(): + raise ValueError("Sampled graph is not a DAG") + + dag = torch.tensor(dag, dtype=torch.float32) + + return dag + + def relaxed_sample(self, sample_shape: torch.Size = torch.Size(), temperature: float = 0.0) -> torch.Tensor: + raise NotImplementedError + + def sample(self, sample_shape: torch.Size = torch.Size()): + """ + Sample binary adjacency matrices from the underlying distribution. + + Args: + sample_shape: The shape of the samples to be drawn. + + Returns: + A tensor of shape (sample_shape, num_nodes, num_nodes) + """ + num_samples = int(torch.prod(torch.tensor(sample_shape)).item()) + return torch.stack([self._one_sample() for _ in range(num_samples)]).reshape( + sample_shape + (self.num_nodes, self.num_nodes) + ) + + def entropy(self) -> torch.Tensor: + raise NotImplementedError + + @property + def mean(self) -> torch.Tensor: + raise NotImplementedError + + @property + def mode(self) -> torch.Tensor: + raise NotImplementedError + + def log_prob(self, value: torch.Tensor) -> torch.Tensor: + raise NotImplementedError diff --git a/src/causica/distributions/adjacency/temporal_adjacency_distributions.py b/src/causica/distributions/adjacency/temporal_adjacency_distributions.py new file mode 100644 index 0000000..735ca1b --- /dev/null +++ b/src/causica/distributions/adjacency/temporal_adjacency_distributions.py @@ -0,0 +1,368 @@ +import abc +from typing import Optional + +import torch +import torch.distributions as td +import torch.nn.functional as F +from torch import nn +from torch.distributions.utils import logits_to_probs + +from causica.distributions.adjacency.adjacency_distributions import AdjacencyDistribution +from causica.distributions.distribution_module import DistributionModule +from causica.distributions.gumbel_binary import gumbel_softmax_binary + + +class LaggedAdjacencyDistribution(AdjacencyDistribution, abc.ABC): + """ + Probability distributions of the lagged adjacency matrix for causal timeseries model. + + The main functionality is similar to `AdjacencyDistribution`. The main differences are that we additional have + `lag` parameters to indicate the window size of the temporal adjacency matrix. E.g. lag=3 + indicates we only consider previous 3 step of observations. `lags` equals to `context_length` - 1. + + Another difference is that we do not disable the diagonals of the lagged adjacency matries and no dagness constraints as well. + + One single sample from this lagged distribution should have shape (lags, num_nodes, num_nodes). lags must be greater than 0. + For multple samples, the shape will be sample_shape + batch_shape + (lags, num_nodes, num_nodes) + """ + + def __init__(self, num_nodes: int, lags: int, validate_args: Optional[bool] = None): + assert lags > 0, "Number of lags must be greater than 0" + self.lags = lags + assert num_nodes > 0, "Number of nodes in the graph must be greater than 0" + self.num_nodes = num_nodes + event_shape = torch.Size((lags, num_nodes, num_nodes)) + + super(AdjacencyDistribution, self).__init__(event_shape=event_shape, validate_args=validate_args) + + +class TemporalAdjacencyDistribution(td.Distribution): + """Probability distributions of the temporal adjacency matrix for causal timeseries model. + + Combines an instantaneous AdjacencyDistribution and temporal LaggedAdjacencyDistribution to form a proper temporal adjacency matrix. + + One single sample from this distribution should have shape (context_length, num_nodes, num_nodes) + For multple samples, the shape will be sample_shape + batch_shape + (context_length, num_nodes, num_nodes) + + For single sample, graph[-1] will be the instantaneous graph and graph[0] will be the last lagged graph (if context_length > 1). + """ + + support = td.constraints.independent(td.constraints.boolean, 1) + arg_constraints = {} + + def __init__( + self, + instantaneous_distribution: AdjacencyDistribution, + lagged_distribution: Optional[LaggedAdjacencyDistribution], + validate_args: Optional[bool] = None, + ): + """ + Args: + instantaneous_distribution: The distribution of the instantaneous adjacency matrix + lagged_distribution: The distribution of the lagged adjacency matrix + validate_args: Whether to validate the input arguments + """ + if lagged_distribution is not None: + self.lags = lagged_distribution.lags + assert ( + instantaneous_distribution.num_nodes == lagged_distribution.num_nodes + ), "Number of nodes in the graph must be the same" + else: + self.lags = 0 + self.context_length = self.lags + 1 + self.num_nodes = instantaneous_distribution.num_nodes + event_shape = torch.Size((self.context_length, self.num_nodes, self.num_nodes)) + self.inst_dist = instantaneous_distribution + self.lagged_dist = lagged_distribution + + super().__init__(event_shape=event_shape, validate_args=validate_args) + + def relaxed_sample(self, sample_shape: torch.Size = torch.Size(), temperature: float = 0.0) -> torch.Tensor: + """Sample a binary temporal adjacency matrix from the relaxed distribution. + + This relies on the relaxed sampling method of both the instantaneous and lagged distributions, so that gradients + can flow through. To obtain hard samples that stops gradients, use the `sample` method. + + Args: + sample_shape: the shape of the samples to return + temperature: The temperature of the relaxed distribution + + Returns: + A tensor of shape sample_shape + batch_shape + (context_length, num_nodes, num_nodes) + """ + # sample inst adj matrix with shape sample_shape + batch_shape + (num_nodes, num_nodes) + inst_adj_matrix = self.inst_dist.relaxed_sample(sample_shape, temperature) + if self.lagged_dist is None: + return inst_adj_matrix.unsqueeze(-3) # sample_shape + batch_shape + (1, num_nodes, num_nodes) + # sample lagged adj matrix with shape sample_shape + batch_shape + (lags, num_nodes, num_nodes) + lagged_adj_matrix = self.lagged_dist.relaxed_sample(sample_shape, temperature) + + return torch.cat( + [lagged_adj_matrix, inst_adj_matrix.unsqueeze(-3)], dim=-3 + ) # sample_shape + batch_shape + (context_length, num_nodes, num_nodes) + + def sample(self, sample_shape: torch.Size = torch.Size()) -> torch.Tensor: + """Sample a binary temporal adjacency matrix from the underlying distribution. + + The gradients will not flow through this method, use `relaxed_sample` instead. + + Args: + sample_shape: the shape of the samples to return + + Returns: + A tensor of shape sample_shape + batch_shape + (context_length, num_nodes, num_nodes) + """ + inst_adj_matrix = self.inst_dist.sample(sample_shape) # sample_shape + batch_shape + (num_nodes, num_nodes) + if self.lagged_dist is None: + return inst_adj_matrix.unsqueeze(-3) # sample_shape + batch_shape + (1, num_nodes, num_nodes) + + lagged_adj_matrix = self.lagged_dist.sample( + sample_shape + ) # sample_shape + batch_shape + (lags, num_nodes, num_nodes) + return torch.cat( + [lagged_adj_matrix, inst_adj_matrix.unsqueeze(-3)], dim=-3 + ) # sample_shape + batch_shape + (context_length, num_nodes, num_nodes) + + def entropy(self) -> torch.Tensor: + """ + Return the entropy of the underlying temporal adjacency distribution. + + Returns: + A tensor of shape batch_shape, with the entropy of the distribution + """ + if self.lagged_dist is None: + return self.inst_dist.entropy() + return self.inst_dist.entropy() + self.lagged_dist.entropy() + + @property + def mean(self) -> torch.Tensor: + """ + Return the mean of the underlying temporal adjaccency distribution. + + This will be a matrix with all entries in the interval [0, 1]. + + Returns: + A tensor of shape batch_shape + (context_length, num_nodes, num_nodes) + """ + if self.lagged_dist is None: + return self.inst_dist.mean.unsqueeze(-3) # batch_shape + (1, num_nodes, num_nodes) + + return torch.cat( + [self.lagged_dist.mean, self.inst_dist.mean.unsqueeze(-3)], dim=-3 + ) # batch_shape + (context_length, num_nodes, num_nodes) + + @property + def mode(self) -> torch.Tensor: + """ + Return the mode of the underlying temporal adjacency distribution. + + This will be an temporal adjacency matrix. + + Returns: + A tensor of shape batch_shape + (context_length, num_nodes, num_nodes) + """ + if self.lagged_dist is None: + return self.inst_dist.mode.unsqueeze(-3) # batch_shape + (1, num_nodes, num_nodes) + + return torch.cat( + [self.lagged_dist.mode, self.inst_dist.mode.unsqueeze(-3)], dim=-3 + ) # batch_shape + (context_length, num_nodes, num_nodes) + + def log_prob(self, value: torch.Tensor) -> torch.Tensor: + """ + Get the log probability of each tensor from the sample space + + Args: + value: a binary matrix of shape value_shape + batch_shape + (context_length, n, n) + Returns: + A tensor of shape value_shape + batch_shape, with the log probabilities of each tensor in the batch. + """ + inst_adj_matrix = value[..., -1, :, :] # value_shape + batch_shape + (num_nodes, num_nodes) + if self.lagged_dist is None: + return self.inst_dist.log_prob(inst_adj_matrix) # value_shape + batch_shape + + lagged_adj_matrix = value[..., :-1, :, :] # value_shape + batch_shape + (lags, num_nodes, num_nodes) + return self.inst_dist.log_prob(inst_adj_matrix) + self.lagged_dist.log_prob(lagged_adj_matrix) + + +class RhinoLaggedAdjacencyDistribution(LaggedAdjacencyDistribution): + """ + This implements the adjacency distribution for lagged adj matrix. + """ + + arg_constraints = {"logits_edge": td.constraints.real} + + def __init__( + self, + logits_edge: torch.Tensor, + lags: int, + validate_args: Optional[bool] = None, + ): + """ + Args: + logits_edge: The logits for the edge existence. The shape is a batch_shape + (lags, n, n) tensor. + lags: The number of lags in the temporal adjacency matrix. It must be positive. + validate_args: Whether to validate the arguments. Passed to the superclass + """ + assert lags == logits_edge.shape[-3], "lags must match the number of lags in logits_edge" + assert lags > 0, "lags must be a positive integer" + num_nodes = logits_edge.shape[-1] + + if validate_args: + assert len(logits_edge.shape) >= 3, "logits_exist must be a 3D tensor with shape (lags, n, n)" + assert logits_edge.shape[-3:] == (lags, num_nodes, num_nodes), "Invalid logits_edge shape" + + self.logits_edge = logits_edge + self.lags = lags + + super().__init__(num_nodes, lags, validate_args=validate_args) + + def _get_independent_bernoulli_logits(self) -> torch.Tensor: + """ + Construct the lagged matrix logit(pᵢⱼ). + The output will have shape [..., lags, node, node] + + """ + # Lagged adjacency logits + return self.logits_edge # [..., lags, node, node] + + @staticmethod + def base_dist(logits: torch.Tensor) -> td.Distribution: + """A matrix of independent Bernoulli distributions.""" + return td.Independent(td.Bernoulli(logits=logits), 3) + + def relaxed_sample(self, sample_shape: torch.Size = torch.Size(), temperature: float = 0.0) -> torch.Tensor: + """ + Sample from a relaxed distribution. We use a Gumbel Softmax. + + Args: + sample_shape: the shape of the samples to return + Returns: + A tensor of shape sample_shape + batch_shape + (lags, num_nodes, num_nodes) + """ + + logits = self._get_independent_bernoulli_logits() # [batch_shape, lags, num_nodes, num_nodes] + expanded_logits = logits.expand(*(sample_shape + logits.shape)) + samples = gumbel_softmax_binary( + logits=expanded_logits, tau=temperature, hard=True + ) # [sample_shape, batch_shape, lags, num_nodes, num_nodes] + + return samples + + def sample(self, sample_shape: torch.Size = torch.Size()) -> torch.Tensor: + """ + Sample from the underyling independent Bernoulli distribution. + + Gradients will not flow through this method, use relaxed_sample instead. + + Args: + sample_shape: the shape of the samples to return + Returns: + A tensor of shape sample_shape + batch_shape + (lags, num_nodes, num_nodes) + """ + logits = self._get_independent_bernoulli_logits() # [batch_shape, lags, num_nodes, num_nodes] + samples = self.base_dist(logits).sample( + sample_shape=sample_shape + ) # [sample_shape, batch_shape, lags, num_nodes, num_nodes] + return samples + + def entropy(self) -> torch.Tensor: + """ + Get the entropy of the independent Bernoulli distribution. + + Returns: + A tensor of shape batch_shape, with the entropy of the distribution + """ + logits = self._get_independent_bernoulli_logits() + # shape of entropy is [batch_shape, lags, num_nodes, num_nodes] + entropy = F.binary_cross_entropy_with_logits(logits, logits_to_probs(logits, is_binary=True), reduction="none") + # Entropy should not consider diagonal elements for instantaneous adj matrix + return torch.sum(entropy, dim=(-3, -2, -1)) # batch_shape + + @property + def mean(self) -> torch.Tensor: + """ + Return the mean of the underlying independent Bernoulli distribution. + + This will be a matrix with all entries in the interval [0, 1]. + + Returns: + A tensor of shape batch_shape + (lags, num_nodes, num_nodes) + """ + logits = self._get_independent_bernoulli_logits() + # shape of mean is [batch_shape, lags, num_nodes, num_nodes] + return self.base_dist(logits).mean + + @property + def mode(self) -> torch.Tensor: + """ + Return the mode of the underlying independent Bernoulli distribution. + + This will be an adjacency matrix. + + Returns: + A tensor of shape batch_shape + (lags, num_nodes, num_nodes) + """ + logits = self._get_independent_bernoulli_logits() + # bernoulli mode can be nan for very small logits, favour sparseness and set to 0 + # shape of mode is [batch_shape, lags, num_nodes, num_nodes] + mode = torch.nan_to_num(self.base_dist(logits).mode, nan=0.0) + + return mode + + def log_prob(self, value: torch.Tensor) -> torch.Tensor: + """ + Get the log probability of each tensor from the sample space + + Args: + value: a binary matrix of shape value_shape + batch_shape + (lags, n, n) + Returns: + A tensor of shape value_shape + batch_shape, with the log probabilities of each tensor in the batch. + """ + + logits = self._get_independent_bernoulli_logits() + full_log_prob = self.base_dist(logits).log_prob(value) # value_shape + batch_shape + + return full_log_prob + + +class RhinoLaggedAdjacencyDistributionModule(DistributionModule[RhinoLaggedAdjacencyDistribution]): + """Represents an `RhinoLaggedAdjacencyDistributionModule` distribution with learnable parameters.""" + + def __init__(self, num_nodes: int, lags: int) -> None: + """ + Args: + num_nodes: The number of nodes in the graph. It must be positive. + lags: The number of lags in the temporal adjacency matrix. It must be positive. + """ + super().__init__() + self.logits_edge = nn.Parameter(torch.zeros(lags, num_nodes, num_nodes), requires_grad=True) + self.lags = lags + + def forward(self) -> RhinoLaggedAdjacencyDistribution: + return RhinoLaggedAdjacencyDistribution(logits_edge=self.logits_edge, lags=self.lags) + + +class TemporalAdjacencyDistributionModule(DistributionModule[TemporalAdjacencyDistribution]): + """Represents an `TemporalAdjacencyDistributionModule` distribution with learnable parameters.""" + + def __init__( + self, + inst_dist_module: DistributionModule[AdjacencyDistribution], + lagged_dist_module: Optional[DistributionModule[LaggedAdjacencyDistribution]], + ) -> None: + """ + Args: + inst_dist_module: The distribution module of the instantaneous adjacency matrix. + lagged_dist_module: The distribution module of the lagged adjacency matrix. If None, we assume only + instantaneous adjacency exists. + """ + super().__init__() + self.inst_dist_module = inst_dist_module + self.lagged_dist_module = lagged_dist_module + + def forward(self) -> TemporalAdjacencyDistribution: + return TemporalAdjacencyDistribution( + instantaneous_distribution=self.inst_dist_module(), + lagged_distribution=self.lagged_dist_module() if self.lagged_dist_module is not None else None, + ) diff --git a/src/causica/distributions/adjacency/watts_strogatz.py b/src/causica/distributions/adjacency/watts_strogatz.py new file mode 100644 index 0000000..7c295f6 --- /dev/null +++ b/src/causica/distributions/adjacency/watts_strogatz.py @@ -0,0 +1,107 @@ +import math + +import igraph as ig +import numpy as np +import torch + +from causica.distributions.adjacency.adjacency_distributions import AdjacencyDistribution + + +class WattsStrogatzDAGDistribution(AdjacencyDistribution): + """ + An adjacency distribution for sampling Directed Acyclic Graphs using Watts Strogatz. + + Re-implementation in pytorch from AVICI: + + https://github.com/larslorch/avici + + """ + + def __init__( + self, + num_nodes: int, + lattice_dim: list[int], + rewire_prob: list[float], + neighbors: list[int], + validate_args: bool | None = None, + ): + """ + Args: + num_nodes: the number of nodes in the DAGs to be sampled + lattice_dim: List of the dimension of the lattice + rewire_prob: List of the rewiring probability + neighbors: List of the number of neighbors used + validate_args: Optional arguments from AdjacencyDistribution + """ + assert num_nodes > 0, "Number of nodes in the graph must be greater than 0" + super().__init__(num_nodes=num_nodes, validate_args=validate_args) + self.lattice_dim = lattice_dim + self.rewire_prob = rewire_prob + self.neighbors = neighbors + + def _one_sample(self): + """ + Sample a binary adjacency matrix from the underlying distribution. + + Returns: + A tensor of shape (num_nodes, num_nodes) + """ + get_lattice_dim = np.random.choice(self.lattice_dim).item() + # choose size s.t. we get at smallest possible n_vars greater than requested n_vars given the dimension of lattice + dim_size = math.ceil(self.num_nodes ** (1.0 / get_lattice_dim)) + get_rewire_prob = np.random.choice(self.rewire_prob).item() + get_neighbors = np.random.choice(self.neighbors).item() + graph = ig.Graph.Watts_Strogatz( + dim=get_lattice_dim, size=dim_size, nei=get_neighbors, p=get_rewire_prob, multiple=False, loops=False + ) + + # drop excessive vertices s.t. we get exactly n_vars + n_excessive = len(graph.vs) - self.num_nodes + assert n_excessive >= 0 + if n_excessive: + graph.delete_vertices(np.random.choice(graph.vs, size=n_excessive, replace=False)) + assert ( + len(graph.vs) == self.num_nodes + ), f"Didn't get requested graph; g.vs: {len(graph.vs)}, n_vars {self.num_nodes}" + + # make directed + dag = np.triu(np.array(graph.get_adjacency().data).astype(int), k=1) + + # randomly permute + perm = np.random.permutation(self.num_nodes).tolist() + dag = dag[perm, :][:, perm] + + if not ig.Graph.Weighted_Adjacency(dag.tolist()).is_dag(): + raise ValueError("Sampled graph is not a DAG") + + dag = torch.tensor(dag, dtype=torch.float32) + return dag + + def relaxed_sample(self, sample_shape: torch.Size = torch.Size(), temperature: float = 0.0) -> torch.Tensor: + raise NotImplementedError + + def sample(self, sample_shape: torch.Size = torch.Size()): + """ + Sample binary adjacency matrices from the underlying distribution. + + Args: + sample_shape: The shape of the samples to be drawn. + """ + num_samples = int(torch.prod(torch.tensor(sample_shape)).item()) + return torch.stack([self._one_sample() for _ in range(num_samples)]).reshape( + sample_shape + (self.num_nodes, self.num_nodes) + ) + + def entropy(self) -> torch.Tensor: + raise NotImplementedError + + @property + def mean(self) -> torch.Tensor: + raise NotImplementedError + + @property + def mode(self) -> torch.Tensor: + raise NotImplementedError + + def log_prob(self, value: torch.Tensor) -> torch.Tensor: + raise NotImplementedError diff --git a/src/causica/distributions/noise/__init__.py b/src/causica/distributions/noise/__init__.py index 42a5c58..f3c036c 100644 --- a/src/causica/distributions/noise/__init__.py +++ b/src/causica/distributions/noise/__init__.py @@ -3,6 +3,18 @@ from causica.distributions.noise.joint import ContinuousNoiseDist, JointNoise, JointNoiseModule, create_noise_modules from causica.distributions.noise.noise import IndependentNoise, Noise, NoiseModule from causica.distributions.noise.spline import SplineNoise, SplineNoiseModule, create_spline_dist_params -from causica.distributions.noise.univariate_cauchy import UnivariateCauchyNoise, UnivariateCauchyNoiseModule -from causica.distributions.noise.univariate_laplace import UnivariateLaplaceNoise, UnivariateLaplaceNoiseModule -from causica.distributions.noise.univariate_normal import UnivariateNormalNoise, UnivariateNormalNoiseModule +from causica.distributions.noise.univariate_cauchy import ( + UnivariateCauchyNoise, + UnivariateCauchyNoiseModule, + UnivariateCauchyNoiseRescaled, +) +from causica.distributions.noise.univariate_laplace import ( + UnivariateLaplaceNoise, + UnivariateLaplaceNoiseModule, + UnivariateLaplaceNoiseRescaled, +) +from causica.distributions.noise.univariate_normal import ( + UnivariateNormalNoise, + UnivariateNormalNoiseModule, + UnivariateNormalNoiseRescaled, +) diff --git a/src/causica/distributions/noise/joint.py b/src/causica/distributions/noise/joint.py index 2399602..c106712 100644 --- a/src/causica/distributions/noise/joint.py +++ b/src/causica/distributions/noise/joint.py @@ -156,11 +156,22 @@ def __init__(self, independent_noise_modules: Mapping[str, NoiseModule[Noise[tor super().__init__() self.noise_modules = nn.ModuleDict(independent_noise_modules) - def forward(self, x: Optional[TensorDict] = None) -> JointNoise: - if x is None: - noise_distributions = {name: noise_module() for name, noise_module in self.noise_modules.items()} - else: + def forward(self, x: Optional[tuple[TensorDict, TensorDict] | TensorDict] = None) -> JointNoise: + """ + Some noise_module allows to access to tuple of two Tensors rather than a single Tensor (e.g. univariate_normal, univariate_laplace, univariate_cauchy) + Note that if a tuple is provided to a noise_module that does not allow tuple of tensor, this forward call will raise an error. + """ + + if isinstance(x, tuple): + x, y = x + noise_distributions = { + name: noise_module((x.get(name), y.get(name))) for name, noise_module in self.noise_modules.items() + } + elif isinstance(x, TensorDict): noise_distributions = {name: noise_module(x.get(name)) for name, noise_module in self.noise_modules.items()} + else: + noise_distributions = {name: noise_module() for name, noise_module in self.noise_modules.items()} + return JointNoise(independent_noise_dists=noise_distributions) def __getitem__(self, selection: Iterable[str]) -> "JointNoiseModule": diff --git a/src/causica/distributions/noise/univariate_cauchy.py b/src/causica/distributions/noise/univariate_cauchy.py index e31fda7..5873787 100644 --- a/src/causica/distributions/noise/univariate_cauchy.py +++ b/src/causica/distributions/noise/univariate_cauchy.py @@ -31,6 +31,34 @@ def noise_to_sample(self, noise: torch.Tensor) -> torch.Tensor: return noise + self.loc +class UnivariateCauchyNoiseRescaled(UnivariateCauchyNoise): + """ + This is a `UnivariateCauchyeNoise` where the scale parameter is used in sample/noise transformations. + """ + + def sample_to_noise(self, samples: torch.Tensor) -> torch.Tensor: + """ + Transform from the sample observations to corresponding noise variables. + + Args: + samples: Tensor of shape sample_shape + batch_shape + event_shape + Returns: + The generated samples with shape sample_shape + batch_shape + event_shape + """ + return (samples - self.loc) / self.scale + + def noise_to_sample(self, noise: torch.Tensor) -> torch.Tensor: + """ + Generate samples using the given exogenous noise. + + Args: + noise: noise variable with shape sample_shape + batch_shape. + Returns: + The generated samples with shape sample_shape + batch_shape + event_shape + """ + return self.scale * noise + self.loc + + class UnivariateCauchyNoiseModule(NoiseModule[IndependentNoise[UnivariateCauchyNoise]]): """Represents a UnivariateCauchyNoise with learnable parameters for independent variables.""" @@ -51,7 +79,30 @@ def __init__(self, dim: int, init_log_scale: float | torch.Tensor = 0.0): self.log_scale = nn.Parameter(init_log_scale) - def forward(self, x: Optional[torch.Tensor] = None) -> IndependentNoise[UnivariateCauchyNoise]: - if x is None: + def forward( + self, x: Optional[tuple[torch.Tensor, torch.Tensor] | torch.Tensor] = None + ) -> IndependentNoise[UnivariateCauchyNoise]: + """ + Generarate Independent noise module for the Cauchy distribution. + + If both loc and log_scale are provided, use them to generate the noise module. + If only loc is provided, use the loc and the learnable log_scale. + If neither loc nor log_scale is provided, use zeros as loc and the learnable log_scale. + + Args: + x: Tuple of loc and log_scale or just the loc. + Returns: + Independent noise module. + """ + + if isinstance(x, tuple): + assert len(x) == 2, "Expected a tuple of length 2." + x, y = x + return IndependentNoise(UnivariateCauchyNoiseRescaled(loc=x, scale=torch.exp(y)), 1) + + if isinstance(x, torch.Tensor): + y = self.log_scale + else: x = torch.zeros_like(self.log_scale) - return IndependentNoise(UnivariateCauchyNoise(loc=x, scale=torch.exp(self.log_scale)), 1) + y = self.log_scale + return IndependentNoise(UnivariateCauchyNoise(loc=x, scale=torch.exp(y)), 1) diff --git a/src/causica/distributions/noise/univariate_laplace.py b/src/causica/distributions/noise/univariate_laplace.py index 8fef992..1aa7f42 100644 --- a/src/causica/distributions/noise/univariate_laplace.py +++ b/src/causica/distributions/noise/univariate_laplace.py @@ -31,6 +31,34 @@ def noise_to_sample(self, noise: torch.Tensor) -> torch.Tensor: return noise + self.loc +class UnivariateLaplaceNoiseRescaled(UnivariateLaplaceNoise): + """ + This is a `UnivariateLaplaceNoise` where the scale parameter is used in sample/noise transformations. + """ + + def sample_to_noise(self, samples: torch.Tensor) -> torch.Tensor: + """ + Transform from the sample observations to corresponding noise variables. + + Args: + samples: Tensor of shape sample_shape + batch_shape + event_shape + Returns: + The generated samples with shape sample_shape + batch_shape + event_shape + """ + return (samples - self.loc) / self.scale + + def noise_to_sample(self, noise: torch.Tensor) -> torch.Tensor: + """ + Generate samples using the given exogenous noise. + + Args: + noise: noise variable with shape sample_shape + batch_shape. + Returns: + The generated samples with shape sample_shape + batch_shape + event_shape + """ + return self.scale * noise + self.loc + + class UnivariateLaplaceNoiseModule(NoiseModule[IndependentNoise[UnivariateLaplaceNoise]]): """Represents a UnivariateLaplaceNoise with learnable parameters for independent variables.""" @@ -51,7 +79,28 @@ def __init__(self, dim: int, init_log_scale: float | torch.Tensor = 0.0): self.log_scale = nn.Parameter(init_log_scale) - def forward(self, x: Optional[torch.Tensor] = None) -> IndependentNoise[UnivariateLaplaceNoise]: - if x is None: + def forward( + self, x: Optional[tuple[torch.Tensor, torch.Tensor] | torch.Tensor] = None + ) -> IndependentNoise[UnivariateLaplaceNoise]: + """ + Generarate Independent noise module for the Laplace distribution. + + If both loc and log_scale are provided, use them to generate the noise module. + If only loc is provided, use the loc and the learnable log_scale. + If neither loc nor log_scale is provided, use zeros as loc and the learnable log_scale. + + Args: + x: Tuple of loc and log_scale or just the loc. + Returns: + Independent noise module. + """ + if isinstance(x, tuple): + x, y = x + return IndependentNoise(UnivariateLaplaceNoiseRescaled(loc=x, scale=torch.exp(y)), 1) + + if isinstance(x, torch.Tensor): + y = self.log_scale + else: x = torch.zeros_like(self.log_scale) - return IndependentNoise(UnivariateLaplaceNoise(loc=x, scale=torch.exp(self.log_scale)), 1) + y = self.log_scale + return IndependentNoise(UnivariateLaplaceNoise(loc=x, scale=torch.exp(y)), 1) diff --git a/src/causica/distributions/noise/univariate_normal.py b/src/causica/distributions/noise/univariate_normal.py index e49598f..55b5c87 100644 --- a/src/causica/distributions/noise/univariate_normal.py +++ b/src/causica/distributions/noise/univariate_normal.py @@ -31,6 +31,34 @@ def noise_to_sample(self, noise: torch.Tensor) -> torch.Tensor: return noise + self.loc +class UnivariateNormalNoiseRescaled(UnivariateNormalNoise): + """ + This is a `UnivariateNormalNoise` where the scale parameter is used in sample/noise transformations. + """ + + def sample_to_noise(self, samples: torch.Tensor) -> torch.Tensor: + """ + Transform from the sample observations to corresponding noise variables. + + Args: + samples: Tensor of shape sample_shape + batch_shape + event_shape + Returns: + The generated samples with shape sample_shape + batch_shape + event_shape + """ + return (samples - self.loc) / self.scale + + def noise_to_sample(self, noise: torch.Tensor) -> torch.Tensor: + """ + Generate samples using the given exogenous noise. + + Args: + noise: noise variable with shape sample_shape + batch_shape. + Returns: + The generated samples with shape sample_shape + batch_shape + event_shape + """ + return self.scale * noise + self.loc + + class UnivariateNormalNoiseModule(NoiseModule[IndependentNoise[UnivariateNormalNoise]]): """Represents a UnivariateNormalNoise with learnable parameters for independent variables.""" @@ -42,7 +70,30 @@ def __init__(self, dim: int, init_log_scale: float = 0.0): super().__init__() self.log_scale = nn.Parameter(torch.full(torch.Size([dim]), init_log_scale)) - def forward(self, x: Optional[torch.Tensor] = None) -> IndependentNoise[UnivariateNormalNoise]: - if x is None: + def forward( + self, x: Optional[tuple[torch.Tensor, torch.Tensor] | torch.Tensor] = None + ) -> IndependentNoise[UnivariateNormalNoise]: + + """ + Generarate Independent noise module for the Gaussian distribution. + + If both loc and log_scale are provided, use them to generate the noise module. + If only loc is provided, use the loc and the learnable log_scale. + If neither loc nor log_scale is provided, use zeros as loc and the learnable log_scale. + + Args: + x: Tuple of loc and log_scale or just the loc. + Returns: + Independent noise module. + """ + + if isinstance(x, tuple): + x, y = x + return IndependentNoise(UnivariateNormalNoiseRescaled(loc=x, scale=torch.exp(y)), 1) + + if isinstance(x, torch.Tensor): + y = self.log_scale + else: x = torch.zeros_like(self.log_scale) - return IndependentNoise(UnivariateNormalNoise(loc=x, scale=torch.exp(self.log_scale)), 1) + y = self.log_scale + return IndependentNoise(UnivariateNormalNoise(loc=x, scale=torch.exp(y)), 1) diff --git a/src/causica/distributions/signed_uniform.py b/src/causica/distributions/signed_uniform.py new file mode 100644 index 0000000..507b613 --- /dev/null +++ b/src/causica/distributions/signed_uniform.py @@ -0,0 +1,51 @@ +import torch +import torch.distributions as td + + +def signed_uniform_1d(low: float, high: float) -> td.Distribution: + """ + Returns a distribution that is a mixture of two uniform distributions. + + The first uniform distribution is defined on the interval [low, high] + and the second is defined on the interval [-high, -low]. + The mixture is defined by a Bernoulli distribution with a probability of 0.5. + + Args: + low: the lower bound of the uniform distribution + high: the upper bound of the uniform distribution + + Returns: + a distribution that is a mixture of two uniform distributions + """ + + weight = torch.tensor([0.5, 0.5]) + mix = td.Categorical(probs=weight) + low_tensor = torch.tensor([low, -high]) + high_tensor = torch.tensor([high, -low]) + comp = td.Uniform(low=low_tensor, high=high_tensor) + return td.MixtureSameFamily(mix, comp) + + +class MultivariateSignedUniform(td.Distribution): + """ + Distribution that is a mixture of two uniform distributions. + + The first uniform distribution is defined on the interval [low, high] + and the second is defined on the interval [-high, -low]. + The mixture is defined by a Bernoulli distribution with a probability of 0.5. + + Args: + low: the lower bound of the uniform distribution + high: the upper bound of the uniform distribution + size: the size of the distribution + """ + + def __init__(self, low: float, high: float, size: torch.Size): + self.one_dim_dist = signed_uniform_1d(low, high) + self.size = size + super().__init__() + + def sample(self, sample_shape=torch.Size()): + sample_shape = sample_shape + self.size + + return self.one_dim_dist.sample(sample_shape) diff --git a/src/causica/functional_relationships/__init__.py b/src/causica/functional_relationships/__init__.py index fe1f470..3828f5b 100644 --- a/src/causica/functional_relationships/__init__.py +++ b/src/causica/functional_relationships/__init__.py @@ -4,5 +4,9 @@ create_do_functional_relationship, ) from causica.functional_relationships.functional_relationships import FunctionalRelationships +from causica.functional_relationships.heteroscedastic_rff_functional_relationships import ( + HeteroscedasticRFFFunctionalRelationships, +) from causica.functional_relationships.linear_functional_relationships import LinearFunctionalRelationships from causica.functional_relationships.rff_functional_relationships import RFFFunctionalRelationships +from causica.functional_relationships.temporal_functional_relationships import TemporalEmbedFunctionalRelationships diff --git a/src/causica/functional_relationships/do_functional_relationships.py b/src/causica/functional_relationships/do_functional_relationships.py index 3b7b580..53a1a10 100644 --- a/src/causica/functional_relationships/do_functional_relationships.py +++ b/src/causica/functional_relationships/do_functional_relationships.py @@ -2,6 +2,7 @@ from tensordict import TensorDict from causica.functional_relationships.functional_relationships import FunctionalRelationships +from causica.functional_relationships.temporal_functional_relationships import TemporalEmbedFunctionalRelationships class DoFunctionalRelationships(FunctionalRelationships): @@ -25,6 +26,8 @@ def __init__(self, func: FunctionalRelationships, do: TensorDict, submatrix: tor raise ValueError("Intervention is only supported for at least vector valued interventions") if len({val.ndim for val in do.values()}) > 1: raise ValueError("Intervention must have the same number of dimensions for all variables") + if isinstance(func, TemporalEmbedFunctionalRelationships): + raise NotImplementedError("Intervention is not yet implemented for temporal functions.") new_shapes = {key: shape for key, shape in func.shapes.items() if key not in do.keys()} super().__init__(new_shapes, batch_shape=do.batch_size + func.batch_shape) @@ -46,6 +49,7 @@ def pad_intervened_graphs(self, graphs: torch.Tensor) -> torch.Tensor: Returns: A tensor of shape batch_shape_g + (func_n, func_n) """ + num_nodes = self.func.tensor_to_td.num_keys target_shape = graphs.shape[:-2] + (num_nodes, num_nodes) @@ -113,11 +117,16 @@ def create_do_functional_relationship( Return: A tuple with the intervened functional relationship and the intervened graph """ + is_temporal = isinstance(func, TemporalEmbedFunctionalRelationships) + if is_temporal: + raise NotImplementedError("Interventions are not yet supported for temporal graphs") + + graph_ndims = 3 if is_temporal else 2 if func.batch_shape == torch.Size((1,)): func.batch_shape = torch.Size() if interventions.ndim > 1: raise ValueError("Interventions must be at most a single batch of interventions") - if graph.ndim > 3: + if graph.ndim > graph_ndims + 1: raise ValueError("Graph must be at most a single batch of graphs") if interventions.batch_dims > 0 and len(func.batch_shape) > 0: raise ValueError("Cannot intervene on a batch of interventions and a batch of functional relationships") @@ -132,11 +141,11 @@ def create_do_functional_relationship( submatrix = graph[..., do_nodes_mask, :][..., :, ~do_nodes_mask] # Expanding graph if interventions or functions are batched - if do_graph.ndim == 2 and interventions.batch_dims > 0 or len(func.batch_shape) > 0: + if do_graph.ndim == graph_ndims and interventions.batch_dims > 0 or len(func.batch_shape) > 0: do_graph = do_graph.unsqueeze(0) submatrix = submatrix.unsqueeze(0) # Expanding interventions if graph is batched and functions are not - if do_graph.ndim == 3 and interventions.batch_dims == 0 and len(func.batch_shape) == 0: + if do_graph.ndim == graph_ndims + 1 and interventions.batch_dims == 0 and len(func.batch_shape) == 0: interventions = interventions.unsqueeze(0) return DoFunctionalRelationships(func, interventions, submatrix), do_graph diff --git a/src/causica/functional_relationships/heteroscedastic_rff_functional_relationships.py b/src/causica/functional_relationships/heteroscedastic_rff_functional_relationships.py new file mode 100644 index 0000000..823aeb9 --- /dev/null +++ b/src/causica/functional_relationships/heteroscedastic_rff_functional_relationships.py @@ -0,0 +1,67 @@ +from typing import Optional + +import torch + +from causica.functional_relationships.rff_functional_relationships import RFFFunctionalRelationships + + +class HeteroscedasticRFFFunctionalRelationships(RFFFunctionalRelationships): + """ + A simple random fourier feature-based functional relationship. + + The formula implemented here is: + x_i = sqrt(2/M) * output_scales_i sum_{i}^{M} alpha_i sin( / length_scale_i) + """ + + def __init__( + self, + shapes: dict[str, torch.Size], + initial_random_features: torch.Tensor, + initial_coefficients: torch.Tensor, + initial_bias: Optional[torch.Tensor] = None, + initial_length_scales: Optional[torch.Tensor] = None, + initial_output_scales: Optional[torch.Tensor] = None, + initial_angles: Optional[torch.Tensor] = None, + trainable: bool = False, + log_scale: bool = False, + ) -> None: + """ + Args: + shapes: Dict of node shapes (how many dimensions a variable has) + Order corresponds to the order in graph(s). + initial_random_features: a tensor containing the random features [num_rf, output_shape] + initial_coefficients: a tensor containing the linear outer coefficients [num_rf,] + initial_bias: Optional, None or a tensor containing the bias [output_shape,] + initial_length_scales: Optional, None or a tensor containing the length scales [output_shape,] + initial_output_scales: Optional, None or a tensor containing the output scales [output_shape,] + initial_angles: Optional, None or a tensor containing the angles [num_rf,] + trainable: whether the coefficient matrix should be learnable + log_scale: whether to apply a log scale to the output + """ + super().__init__( + shapes=shapes, + initial_random_features=initial_random_features, + initial_coefficients=initial_coefficients, + initial_bias=initial_bias, + initial_length_scales=initial_length_scales, + initial_output_scales=initial_output_scales, + initial_angles=initial_angles, + trainable=trainable, + ) + self.log_scale = log_scale + + def non_linear_map(self, samples: torch.Tensor, graph: torch.Tensor) -> torch.Tensor: + """ + Applies the non linear function to a concatenated tensor of samples. + + Args: + samples: tensor of shape batch_shape_x + [n_cols] + graph: tensor of shape batch_shape_g + [n_nodes, n_nodes] + Returns: + tensor of shape batch_shape_x + batch_shape_g + [n_cols] + """ + res = super().non_linear_map(samples, graph) + res = torch.log(1.0 + torch.exp(res)) + if self.log_scale: + res = torch.log(torch.log(1.0 + torch.exp(res))) + return res diff --git a/src/causica/functional_relationships/rff_functional_relationships.py b/src/causica/functional_relationships/rff_functional_relationships.py index d182773..b536b5e 100644 --- a/src/causica/functional_relationships/rff_functional_relationships.py +++ b/src/causica/functional_relationships/rff_functional_relationships.py @@ -10,6 +10,7 @@ class RFFFunctionalRelationships(FunctionalRelationships): """ A simple random fourier feature-based functional relationship. + The formula implemented here is: x_i = sqrt(2/M) * output_scales_i sum_{i}^{M} alpha_i sin( / length_scale_i ) """ diff --git a/src/causica/functional_relationships/temporal_functional_relationships.py b/src/causica/functional_relationships/temporal_functional_relationships.py new file mode 100644 index 0000000..49abe26 --- /dev/null +++ b/src/causica/functional_relationships/temporal_functional_relationships.py @@ -0,0 +1,40 @@ +import torch +from tensordict import TensorDict + +from causica.functional_relationships.functional_relationships import FunctionalRelationships +from causica.nn import TemporalEmbedNN + + +class TemporalEmbedFunctionalRelationships(FunctionalRelationships): + """This is a `FunctionalRelationsips` that wraps the `RhinoEmbedNN` module.""" + + def __init__( + self, + shapes: dict[str, torch.Size], + embedding_size: int, + out_dim_g: int, + num_layers_g: int, + num_layers_zeta: int, + context_length: int, + ) -> None: + """ + Args: + shapes: Dictionary of shapes of the input tensors. + embedding_size: See `TemporalEmbedNN`. + out_dim_g: See `TemporalEmbedNN`. + num_layers_g: See `TemporalEmbedNN`. + num_layers_zeta: See `TemporalEmbedNN`. + context_length: See `TemporalEmbedNN`. + """ + super().__init__(shapes=shapes) + self.nn = TemporalEmbedNN( + group_mask=self.stacked_key_masks, + embedding_size=embedding_size, + out_dim_l=out_dim_g, + num_layers_l=num_layers_g, + num_layers_zeta=num_layers_zeta, + context_length=context_length, + ) + + def forward(self, samples: TensorDict, graphs: torch.Tensor) -> TensorDict: + return self.tensor_to_td(self.nn(self.tensor_to_td.inv(samples), graphs)) diff --git a/src/causica/graph/evaluation_metrics.py b/src/causica/graph/evaluation_metrics.py index a31dd16..7cba340 100644 --- a/src/causica/graph/evaluation_metrics.py +++ b/src/causica/graph/evaluation_metrics.py @@ -48,6 +48,27 @@ def orientation_precision_recall(graph1: torch.Tensor, graph2: torch.Tensor) -> return precision, recall +def orientation_fallout_recall(graph1: torch.Tensor, graph2: torch.Tensor) -> tuple[torch.Tensor, torch.Tensor]: + """Evaluate the precision and recall of edge orientation for two adjacency matrices.""" + vec1 = _to_vector(graph1) + vec2 = _to_vector(graph2) + non_zero_vec1 = vec1 != 0 + zero_vec1 = vec1 == 0 + non_zero_vec2 = vec2 != 0 + + if (non_zero_vec1_sum := non_zero_vec1.sum()) != 0: + recall = ((vec1 == vec2) & non_zero_vec1).sum() / non_zero_vec1_sum + else: + recall = torch.tensor(0.0, device=graph1.device) + + if (zero_vec1_sum := zero_vec1.sum()) != 0: + fallout = (non_zero_vec2 & zero_vec1).sum() / zero_vec1_sum + else: + fallout = torch.tensor(0.0, device=graph1.device) + + return fallout, recall + + def orientation_f1(graph1: torch.Tensor, graph2: torch.Tensor) -> torch.Tensor: """Evaluate the f1 score of edge existence for two adjacency matrices.""" return f1_score(*orientation_precision_recall(graph1, graph2)) diff --git a/src/causica/lightning/data_modules/synthetic_data_module.py b/src/causica/lightning/data_modules/synthetic_data_module.py index 3bcdbd0..a0d4186 100644 --- a/src/causica/lightning/data_modules/synthetic_data_module.py +++ b/src/causica/lightning/data_modules/synthetic_data_module.py @@ -41,6 +41,8 @@ def __init__( batches_per_metaset: int = 1, sample_interventions: bool = False, sample_counterfactuals: bool = False, + treatment_variable: str | None = None, + effect_variables: list[str] | None = None, num_workers: int = 0, pin_memory: bool = True, persistent_workers: bool = True, @@ -57,6 +59,10 @@ def __init__( batches_per_metaset: The number of batches per epoch per dataset sample_interventions: Whether to sample interventions sample_counterfactuals: Whether to sample counterfactuals + treatment_variable: This specify the name of the nodes to be taken as treatment from the generated sems. + The sem sampler must always generate this treatment. + effet_variables: This specify the names of the nodes to be taken as effects from the generated sems. + The sem sampler must always generate this effect. num_workers: The number of workers to use for the dataloader pin_memory: Whether to pin memory for the dataloader persistent_workers: Whether to use persistent workers for the dataloader @@ -81,6 +87,8 @@ def __init__( self.batches_per_metaset = batches_per_metaset self.sample_interventions = sample_interventions self.sample_counterfactuals = sample_counterfactuals + self.treatment_variable = treatment_variable + self.effect_variables = effect_variables self.num_workers = num_workers self.persistent_workers = persistent_workers @@ -125,6 +133,8 @@ def _get_dataset(self, dataset_size: int) -> ConcatDataset: num_sems=self.num_sems, sample_interventions=self.sample_interventions, sample_counterfactuals=self.sample_counterfactuals, + treatment_variable=self.treatment_variable, + effect_variables=self.effect_variables, ) ) diff --git a/src/causica/lightning/data_modules/variable_spec_data.py b/src/causica/lightning/data_modules/variable_spec_data.py index a726b32..282eeae 100644 --- a/src/causica/lightning/data_modules/variable_spec_data.py +++ b/src/causica/lightning/data_modules/variable_spec_data.py @@ -49,6 +49,7 @@ def __init__( exclude_log_normalization: Iterable[str] = tuple(), default_offset: float = 1.0, log_normalize_min_margin: float = 0.0, + fit_normalizer_on_test_sets: bool = False, load_counterfactual: bool = False, load_interventional: bool = False, load_validation: bool = False, @@ -73,6 +74,7 @@ def __init__( exclude_log_normalization: Which variables to exclude from log normalization default_offset: Default offset for log normalization. log_normalize_min_margin: Minimum margin for log normalization. + fit_normalizer_on_test_sets: Whether to normalize all data. If True, normalization will be fitted on all data splits. load_counterfactual: Whether counterfactual data should be loaded load_interventional: Whether interventional data should be loaded load_validation: Whether to load the validation dataset @@ -93,6 +95,7 @@ def __init__( self.load_validation = load_validation self.default_offset = default_offset self.log_normalize_min_margin = log_normalize_min_margin + self.fit_normalizer_on_test_sets = fit_normalizer_on_test_sets self.use_normalizer = standardize or log_normalize self.normalizer: Optional[Normalizer] = None @@ -233,8 +236,14 @@ def prepare_data(self): if self.use_normalizer: # Only applied to continuous variables normalization_variables = {k for k, v in self._variable_types.items() if v == VariableTypeEnum.CONTINUOUS} + normalization_sets = [self._dataset_train, self._dataset_test] + if self.load_validation: + normalization_sets.append(self._dataset_valid) + normalization_data = ( + torch.cat(normalization_sets) if self.fit_normalizer_on_test_sets else self._dataset_train + ) self.normalizer = self.create_normalizer(normalization_variables)( - self._dataset_train.select(*normalization_variables) + normalization_data.select(*normalization_variables) ) self.normalize_data() else: @@ -243,8 +252,16 @@ def prepare_data(self): def normalize_data(self): self._dataset_train = self.normalizer(self._dataset_train) self._dataset_test = self.normalizer(self._dataset_test) + if self._dataset_test.apply(torch.isnan).any(): + raise ValueError( + "NaN values found in the test data after normalization. Consider changing the normalization parameters." + ) if self.load_validation: self._dataset_valid = self.normalizer(self._dataset_valid) + if self._dataset_valid.apply(torch.isnan).any(): + raise ValueError( + "NaN values found in the validation data after normalization. Consider changing the normalization parameters." + ) if self.load_interventional: for intervention in self.interventions: diff --git a/src/causica/nn/__init__.py b/src/causica/nn/__init__.py index 2230faf..336373f 100644 --- a/src/causica/nn/__init__.py +++ b/src/causica/nn/__init__.py @@ -1 +1,3 @@ +from causica.nn import fip from causica.nn.deci_embed_nn import DECIEmbedNN +from causica.nn.temporal_embed_nn import TemporalEmbedNN diff --git a/src/causica/nn/fip/__init__.py b/src/causica/nn/fip/__init__.py new file mode 100644 index 0000000..cce7d77 --- /dev/null +++ b/src/causica/nn/fip/__init__.py @@ -0,0 +1,4 @@ +from causica.nn.fip.attention_layer import MultiHeadAttention +from causica.nn.fip.avici_encoder import AmortizedEncoder +from causica.nn.fip.decoder_layer import AmortizedDecoderLayer, CausalDecoderLayer +from causica.nn.fip.embeddings import Encoding, PositionWiseFeedForward diff --git a/src/causica/nn/fip/attention_layer.py b/src/causica/nn/fip/attention_layer.py new file mode 100644 index 0000000..d509da7 --- /dev/null +++ b/src/causica/nn/fip/attention_layer.py @@ -0,0 +1,252 @@ +import math +from typing import Optional + +import torch +from torch import nn + + +def cost_matrix_lp(x_1: torch.Tensor, y_1: torch.Tensor, power: int = 2) -> torch.Tensor: + """ + Returns the matrix of $|x_1-y_1|^p$. + + Args: + x_1: expected shape: (*, total_nodes, dim_nodes) + y_1: expected shape: (*, batch_size, total_nodes, dim_nodes) + + Returns: + output: expected shape: (*, total_nodes, total_nodes) + """ + x_col = x_1.unsqueeze(-2) + y_lin = y_1.unsqueeze(-3) + return torch.sum((torch.abs(x_col - y_lin)) ** power, -1) + + +def compute_score_attention( + queries: torch.Tensor, + keys: torch.Tensor, + d_k: float, + mask: Optional[torch.Tensor] = None, + cost_type: str = "dot_product", +) -> torch.Tensor: + """ + Compute the cost matrix between queries and keys. + + Args: + queries: expected shape: (*, total_nodes, dim_nodes) + keys: expected shape: (*, total_nodes, dim_nodes) + d_k (float): rescaling factor + mask: expected shape: (*, total_nodes, total_nodes) + cost_type (str): dot_product or l2 + + Returns: + output: expected shape: (*, total_nodes, total_nodes) + """ + + if cost_type == "l2": + attn_scores = cost_matrix_lp(queries, keys, power=2) / math.sqrt(d_k) + elif cost_type == "dot_product": + attn_scores = torch.matmul(queries, keys.transpose(-2, -1)) / math.sqrt(d_k) + else: + raise ValueError(f"cost_type {cost_type} not recognized") + + if mask is not None: + val_masking = -1e4 if attn_scores.dtype == torch.float16 else -1e9 + attn_scores = attn_scores.masked_fill(mask == 0, val_masking) + + return attn_scores + + +def scaled_dot_product_attention( + queries: torch.Tensor, + keys: torch.Tensor, + d_k: float, + mask: Optional[torch.Tensor] = None, + cost_type: str = "dot_product", +) -> torch.Tensor: + + """ + Compute the standard attention matrix + + Args: + queries: expected shape: (*, total_nodes, dim_nodes) + keys: expected shape: (*, total_nodes, dim_nodes) + d_k (float): rescaling factor + mask: expected shape: (*, total_nodes, total_nodes) + cost_type (str): dot_product or l2 + + Returns: + output: expected shape: (*, total_nodes, total_nodes) + + """ + + attn_scores = compute_score_attention(queries, keys, d_k, mask=mask, cost_type=cost_type) + return torch.softmax(attn_scores, dim=-1) + + +def causal_scaled_dot_product_attention( + queries: torch.Tensor, + keys: torch.Tensor, + d_k: float, + mask: Optional[torch.Tensor] = None, + cost_type: str = "dot_product", +) -> torch.Tensor: + r""" + Partial Optimal Transport problem + + Here we are solving: $\min \langle C, P \rangle$ -\espilon H(P)$ s.t. $P \leq 1$ + where $C$ is the cost matrix, $P$ is the transport matrix, and $H$ is the entropy. + + Args: + queries: expected shape: (*, total_nodes, dim_nodes) + keys: expected shape: (*, total_nodes, dim_nodes) + d_k (float): rescaling factor + mask: expected shape: (*, total_nodes, total_nodes) + cost_type (str): dot_product or l2 + + Returns: + output: expected shape: (*, total_nodes, total_nodes) + + """ + attn_scores = compute_score_attention(queries, keys, d_k, mask=mask, cost_type=cost_type) + + max_val = torch.max(attn_scores, dim=-1, keepdim=True)[0] + attn_scores_rescaled = attn_scores - max_val + attn_probs_rescaled = torch.exp(attn_scores_rescaled) + den_rescaled = torch.sum(attn_probs_rescaled, dim=-1, keepdim=True) + den_rescaled = torch.max(den_rescaled, torch.exp(-max_val)) + + return attn_probs_rescaled / den_rescaled + + +class MultiHeadAttention(nn.Module): + r""" + Multi-Head Attention module + + Attention type: attn_type = causal, causal_fixed, diagonal, standard, linear + + In attn_type is "causal_fixed", the attention matrix is fixed + to be the same for all inputs. In that case, the attention matrix is fixed for any inputs. + + If attn_type is "causal", the attention matrix depends on the input. + + """ + + def __init__( + self, + d_model: int, + num_heads: int, + max_seq_length: int, + dim_key: Optional[int] = None, + attn_type: str = "causal", + cost_type: str = "dot_product", + ): + super().__init__() + self.d_model = d_model + self.num_heads = num_heads + self.attn_type = attn_type + self.cost_type = cost_type + + self.dim_latent = num_heads * dim_key if dim_key is not None else d_model + assert self.dim_latent % num_heads == 0, "dim_latent must be divisible by num_heads" + self.d_k = self.dim_latent // num_heads + + self.w_q = nn.Linear(d_model, self.dim_latent) + self.w_k = nn.Linear(d_model, self.dim_latent) + self.w_v = nn.Linear(d_model, self.dim_latent) + self.w_o = nn.Linear(self.dim_latent, d_model) + + if self.attn_type == "standard": + self.attn = scaled_dot_product_attention + elif self.attn_type == "linear": + self.attn = compute_score_attention + else: + self.position: torch.Tensor + self.register_buffer("position", torch.arange(0, max_seq_length, dtype=torch.long)) + self.emb = nn.Embedding(max_seq_length, d_model) + self.attn = causal_scaled_dot_product_attention + + def split_heads(self, inp: torch.Tensor) -> torch.Tensor: + """ + Args: + inp: expected shape (*, max_seq_length, d_model) + + Returns: + out: expected shape: (*, num_heads, max_seq_length, d_model // num_heads) + + """ + all_dims = inp.size() + inp = inp.view(*all_dims[:-1], self.num_heads, self.d_k) + return inp.transpose(-3, -2) + + def combine_heads(self, inp: torch.Tensor) -> torch.Tensor: + """ + Args: + inp: expected shape (*, num_heads, max_seq_length, d_model // num_heads) + + Returns: + out: expected shape: (*, max_seq_length, d_model) + + """ + inp = inp.transpose(-3, -2).contiguous() + all_dims = inp.size() + return inp.view(*all_dims[:-2], self.dim_latent) + + def compute_attn( + self, queries: torch.Tensor, keys: torch.Tensor, mask: Optional[torch.Tensor] = None + ) -> torch.Tensor: + """ + Args: + queries: expected shape: (batch_size, max_seq_length, d_model) + keys: expected shape: (batch_size, max_seq_length, d_model) + + Returns: + output: expected shape: (batch_size, num_heads, max_seq_length, max_seq_length) + + """ + + if self.attn_type == "causal_fixed": + keys = self.emb(self.position).repeat(keys.size(0), 1, 1) + queries = self.emb(self.position).repeat(queries.size(0), 1, 1) + + queries = self.split_heads(self.w_q(queries)) + keys = self.split_heads(self.w_k(keys)) + return self.attn(queries, keys, self.d_k, mask=mask, cost_type=self.cost_type) + + def compute_cost( + self, queries: torch.Tensor, keys: torch.Tensor, mask: Optional[torch.Tensor] = None + ) -> torch.Tensor: + """ + Args: + queries: expected shape: (batch_size, max_seq_length, d_model) + keys: expected shape: (batch_size, max_seq_length, d_model) + + Returns: + output: expected shape: (batch_size, num_heads, max_seq_length, max_seq_length) + + """ + if self.attn_type == "causal_fixed": + keys = self.emb(self.position).repeat(keys.size(0), 1, 1) + queries = self.emb(self.position).repeat(queries.size(0), 1, 1) + + queries = self.split_heads(self.w_q(queries)) + keys = self.split_heads(self.w_k(keys)) + return compute_score_attention(queries, keys, self.d_k, mask=mask, cost_type=self.cost_type) + + def forward( + self, queries: torch.Tensor, keys: torch.Tensor, values: torch.Tensor, mask: Optional[torch.Tensor] = None + ) -> torch.Tensor: + """ + Args: + queries: expected shape: (batch_size, max_seq_length, d_model) + keys: expected shape: (batch_size, max_seq_length, d_model) + values: expected shape: (batch_size, max_seq_length, d_model) + + Returns: + output: expected shape: (batch_size, max_seq_length, max_seq_length) + + """ + values = self.split_heads(self.w_v(values)) + attn_probs = self.compute_attn(queries, keys, mask=mask) + attn_output = attn_probs @ values + + return self.w_o(self.combine_heads(attn_output)) diff --git a/src/causica/nn/fip/avici_encoder.py b/src/causica/nn/fip/avici_encoder.py new file mode 100644 index 0000000..f3eb8d7 --- /dev/null +++ b/src/causica/nn/fip/avici_encoder.py @@ -0,0 +1,89 @@ +import torch +from torch import nn + +from causica.nn.fip.decoder_layer import AmortizedDecoderLayer + + +class AmortizedEncoder(nn.Module): + """ + Causal Dataset Encoder Model. + + Takes a batch of datasets and encodes it using multiple `AmortizedDecoderLayer`s. + + This is a reinterpretation of the AVICI model in PyTorch: + + https://github.com/larslorch/avici + + """ + + def __init__( + self, + d_model: int, + num_heads: int, + dim_key: int, + num_layers: int, + d_ff: int, + dropout: float, + ): + super().__init__() + + self.emb_data = nn.Linear(1, d_model) + self.decoder_layers_nodes = nn.ModuleList( + [ + AmortizedDecoderLayer( + d_model, + num_heads, + dim_key, + d_ff, + dropout, + max_seq_length=2, + attn_type="standard", + cost_type="dot_product", + ) + for _ in range(num_layers) + ] + ) + + self.decoder_layers_samples = nn.ModuleList( + [ + AmortizedDecoderLayer( + d_model, + num_heads, + dim_key, + d_ff, + dropout, + max_seq_length=2, + attn_type="standard", + cost_type="dot_product", + ) + for _ in range(num_layers) + ] + ) + + self.norm = nn.LayerNorm(d_model) + self.num_layers = num_layers + + def forward(self, tgt: torch.Tensor) -> torch.Tensor: + """ + Args: + tgt: expected shape (*, num_samples, max_seq_length) or (*, num_samples, max_seq_length, 2) + + Returns: + output: expected shape: (*, num_samples, max_seq_length, d_model) + + """ + dec_output = tgt.unsqueeze(-1) + + # embed the data + dec_output = self.emb_data(dec_output) + + for k in range(self.num_layers): + dec_output = self.decoder_layers_nodes[k](dec_output, dec_output, dec_output, None) + + dec_output = dec_output.transpose(-2, -3) + dec_output = self.decoder_layers_samples[k](dec_output, dec_output, dec_output, None) + dec_output = dec_output.transpose(-2, -3) + + dec_output = self.norm(dec_output) + + return dec_output diff --git a/src/causica/nn/fip/decoder_layer.py b/src/causica/nn/fip/decoder_layer.py new file mode 100644 index 0000000..4f078d9 --- /dev/null +++ b/src/causica/nn/fip/decoder_layer.py @@ -0,0 +1,124 @@ +from typing import Optional + +import torch +from torch import nn + +from causica.nn.fip.attention_layer import MultiHeadAttention +from causica.nn.fip.embeddings import PositionWiseFeedForward + + +class CausalDecoderLayer(nn.Module): + """ + Causal Decoder Layer. + + It consists of a multi-head self-attention mechanism, followed by a feed-forward network. + It also allows the model to use the causal attention mechanism. + + """ + + def __init__( + self, + d_model: int, + num_heads: int, + dim_key: int, + d_ff: int, + dropout: float, + max_seq_length: int, + attn_type: str = "standard", + cost_type: str = "dot_product", + ): + super().__init__() + + self.self_attn = MultiHeadAttention( + d_model, num_heads, max_seq_length, dim_key=dim_key, attn_type=attn_type, cost_type=cost_type + ) + + self.feed_forward = PositionWiseFeedForward(d_model, d_ff) + self.norm1 = nn.LayerNorm(d_model) + self.norm2 = nn.LayerNorm(d_model) + self.dropout = nn.Dropout(dropout) + + self.max_seq_length = max_seq_length + + def forward( + self, queries: torch.Tensor, keys: torch.Tensor, values: torch.Tensor, mask: Optional[torch.Tensor] + ) -> torch.Tensor: + + """ + Args: + queries: expected shape: (batch_size, max_seq_length, d_model) + keys: expected shape: (batch_size, max_seq_length, d_model) + values: expected shape: (batch_size, max_seq_length, d_model) + mask: expected shape: (batch_size, max_seq_length, max_seq_length) + + Returns: + output: expected shape: (batch_size, max_seq_length, d_model) + + """ + + attn_output = self.self_attn(queries, keys, values, mask) + x_trans = self.norm1(queries + self.dropout(attn_output)) + + ff_output = self.feed_forward(x_trans) + + return self.norm2(x_trans + self.dropout(ff_output)) + + +class AmortizedDecoderLayer(nn.Module): + """ + Amortized Causal Decoder Layer. + + This is a re-implementation of the decoder layer used in the AVICI model in PyTorch: + + https://github.com/larslorch/avici + + """ + + def __init__( + self, + d_model: int, + num_heads: int, + dim_key: int, + d_ff: int, + dropout: float, + max_seq_length: int, + attn_type: str = "standard", + cost_type: str = "dot_product", + ): + super().__init__() + + self.norm_q = nn.LayerNorm(d_model) + self.norm_k = nn.LayerNorm(d_model) + self.norm_v = nn.LayerNorm(d_model) + + self.self_attn = MultiHeadAttention( + d_model, num_heads, max_seq_length, dim_key=dim_key, attn_type=attn_type, cost_type=cost_type + ) + + self.feed_forward = PositionWiseFeedForward(d_model, d_ff) + + self.norm = nn.LayerNorm(d_model) + self.dropout = nn.Dropout(dropout) + + def forward( + self, queries: torch.Tensor, keys: torch.Tensor, values: torch.Tensor, mask: Optional[torch.Tensor] + ) -> torch.Tensor: + + """ + Args: + queries: expected shape: (batch_size, max_seq_length, d_model) + keys: expected shape: (batch_size, max_seq_length, d_model) + values: expected shape: (batch_size, max_seq_length, d_model) + + Returns: + output: expected shape: (batch_size, max_seq_length, d_model) + + """ + queries_in = self.norm_q(queries) + keys_in = self.norm_k(keys) + values_in = self.norm_v(values) + attn_output = self.self_attn(queries_in, keys_in, values_in, mask) + x = queries + self.dropout(attn_output) + x_in = self.norm(x) + ff_output = self.dropout(self.feed_forward(x_in)) + return x + ff_output diff --git a/src/causica/nn/fip/embeddings.py b/src/causica/nn/fip/embeddings.py new file mode 100644 index 0000000..0f70520 --- /dev/null +++ b/src/causica/nn/fip/embeddings.py @@ -0,0 +1,95 @@ +import torch +from torch import nn + + +class PositionalEncodingFixed(nn.Module): + """ + Standard positional encoding. + + """ + + def __init__(self, d_model: int, max_seq_length: int): + super().__init__() + + pe = torch.zeros(max_seq_length, d_model) + position = torch.arange(0, max_seq_length, dtype=torch.float).unsqueeze(1) + div_term = torch.pow(1e5, torch.linspace(0.0, 1.0, d_model // 2 + 1)) + + pe[:, 0::2] = torch.sin(position * div_term) + pe[:, 1::2] = torch.cos(position * div_term) + + self.pe: torch.Tensor + self.register_buffer("pe", pe.unsqueeze(0)) + + def forward(self, inp: torch.Tensor) -> torch.Tensor: + """ + Args: + inp: expected shape: (batch_size, max_seq_length, d_model) + + Returns: + out: expected shape: (batch_size, max_seq_length, d_model) + + """ + + return inp + self.pe[: inp.size(1), :] + + +class Encoding(nn.Module): + """Learnable encoding. + + Either for nodes or for positions. + For the nodes, we multiply (in the hadamard sense) the node value with the positional embedding + + """ + + def __init__(self, d_model: int, max_seq_length: int, encoding_type: str = "node"): + super().__init__() + self.emb = nn.Embedding(max_seq_length, d_model) + + self.position: torch.Tensor + self.register_buffer("position", torch.arange(0, max_seq_length, dtype=torch.long)) + + self.encoding_type = encoding_type + + def forward(self, inp: torch.Tensor) -> torch.Tensor: + """ + Args: + inp: expected shape: (batch_size, max_seq_length, 1) + + Returns: + out: expected shape: (batch_size, max_seq_length, d_model) + + """ + if self.encoding_type == "node": + return inp * self.emb(self.position) + + if self.encoding_type == "position": + return inp + self.emb(self.position) + + raise ValueError("Unknown encoding type") + + +class PositionWiseFeedForward(nn.Module): + def __init__(self, d_model: int, d_ff: int, add_prod: bool = False): + super().__init__() + + self.fc1 = nn.Linear(d_model, d_ff) + self.fc2 = nn.Linear(d_ff, d_model) + self.relu = nn.ReLU() + self.add_prod = add_prod + if add_prod: + self.fc_m = nn.Linear(d_model, d_ff) + + def forward(self, inp: torch.Tensor) -> torch.Tensor: + """ + Args: + inp: expected shape: (batch_size, max_seq_length, d_model) + + Returns: + out: expected shape: (batch_size, max_seq_length, d_model) + + """ + if self.add_prod: + return self.fc2(self.relu(self.fc1(inp)) * self.fc_m(inp)) + + return self.fc2(self.relu(self.fc1(inp))) diff --git a/src/causica/nn/temporal_embed_nn.py b/src/causica/nn/temporal_embed_nn.py new file mode 100644 index 0000000..dfaf5b7 --- /dev/null +++ b/src/causica/nn/temporal_embed_nn.py @@ -0,0 +1,123 @@ +import torch +from torch import nn + +from causica.nn.deci_embed_nn import _generate_fully_connected + + +class TemporalEmbedNN(nn.Module): + """Defines functional relationships for variables in a timeseries. + + Follows the model described in: + Gong, W., Jennings, J., Zhang, C. and Pawlowski, N., 2022. + Rhino: Deep causal temporal relationship learning with history-dependent noise. + ICLR 2023 + arXiv:2210.14706 + + The model is a generalization of the DECI model to include temporal dependencies but represents relationships + between the current timestep and a fixed number of steps backwards. + + For each variable xᵢ we use + ζᵢ(x) = ζ(eᵢ, Σ_{k in pa(i)} l(eₖ, xₖ)), + + where eᵢ is a learned embedding for node i and pa(i) includes the variables from any timestep in the context. + """ + + def __init__( + self, + group_mask: torch.Tensor, + embedding_size: int, + out_dim_l: int, + num_layers_l: int, + num_layers_zeta: int, + context_length: int, + ): + """ + Args: + group_mask: A mask of shape (num_nodes, num_processed_cols) such that group_mask[i, j] = 1. when col j is in + group i. + embedding_size: Size of the embeddings used by each node. Also affects the hidden dimensionality of l and ζ, + which is set to the larger of 4 * concatenated_shape, embedding_size and 64. + out_dim_l: Output dimension of the "inner" NN, l. If none, default is embedding size. + num_layers_l: Number of layers in the "inner" NN, l. + num_layers_zeta: Number of layers in the "outer" NN, ζ. + context_length: The total number of timesteps in the context of the model, including the current timestep. + """ + super().__init__() + self.group_mask = group_mask + num_nodes, concatenated_shape = group_mask.shape + self.context_length = context_length + + # Initialize embeddings uₜⱼ and graph weights Wₜᵢⱼ + # Notice that we store the embeddings and the graph with the time axis in the reversed order from the paper, + # e.g. embedding[lag - t] are the embeddings for the nodes t steps behind, where t=0 corresponds to the + # instantaneous effect. lag, in the paper, is equivalent to context_length - 1, and is the number of steps back + # from the current that's visible to the model. + self.embeddings = nn.parameter.Parameter(0.01 * torch.randn(context_length, num_nodes, embedding_size)) + self.w = torch.nn.Parameter(torch.zeros((context_length, num_nodes, num_nodes)), requires_grad=True) + + # Set NNs sizes + a = max(4 * concatenated_shape, embedding_size, 64) + in_dim_g = embedding_size + concatenated_shape + in_dim_f = embedding_size + out_dim_l + self.l = _generate_fully_connected( + input_dim=in_dim_g, + output_dim=out_dim_l, + hidden_dims=[a] * num_layers_l, + ) + self.zeta = _generate_fully_connected( + input_dim=in_dim_f, + output_dim=concatenated_shape, + hidden_dims=[a] * num_layers_zeta, + ) + + def forward(self, samples: torch.Tensor, graphs: torch.Tensor) -> torch.Tensor: + """Forward of the module. + + Computes non-linear function hᵢ(X, G) using the given adjacency matrix. + + hᵢ(x, G) = ζᵢ(Σₜⱼ Wₜⱼᵢ Gₜⱼᵢ lⱼ(xₜⱼ) + + We also use an embedding u so: + + hᵢ(x, G) = ζ(uₗᵢ, Σₜⱼ Wₜⱼᵢ Gₜⱼᵢ l(xₜⱼ, uₜⱼ)) + + l takes inputs of size batch_shape + (context_length, embedding_size + concatenated_shape) and outputs + batch_shape + (out_dim_g). The input will be appropriately masked to correspond to one variable group. + + ζ takes inputs of size batch_shape + (embedding_size + out_dim_g, ) and outputs batch_shape + + (concatenated_shape, ) the ouptut is then masked to correspond to one variable. + + Args: + samples: tensor of shape batch_shape_x + batch_shape_f + batch_shape_g + [context_length, n_cols] + graph: tensor of shape batch_shape_g + [context_length, n_nodes, n_nodes] + + Returns: + tensor of shape batch_shape_x + batch_shape_f + batch_shape_g + [n_cols] + """ + batch_shape_samples = samples.shape[:-2] + batch_shape_g = graphs.shape[:-3] + if len(batch_shape_g) > 0 and batch_shape_samples[-len(batch_shape_g) :] != batch_shape_g: + raise ValueError( + f"Batch shape of samples and graph must match but got {batch_shape_samples} and {batch_shape_g}" + ) + + # Shape batch_shape_x + batch_shape_f + batch_shape_g + (context_length, num_nodes, concatenated_shape) + masked_samples = torch.einsum("...i,ji->...ji", samples, self.group_mask) + # Shape batch_shape_x + batch_shape_f + batch_shape_g + (context_length, num_nodes, embedding_size) + expanded_embed = self.embeddings.expand(*batch_shape_samples, -1, -1, -1) + + # Shape batch_shape_x + batch_shape_f + batch_shape_g + + # (context_length, num_nodes, concatenated_shape + embedding_size) + x_and_embeddings = torch.cat([masked_samples, expanded_embed], dim=-1) # (concatenate xⱼ and embeddings uⱼ) + # l(uⱼ, xⱼ): Shape batch_shape_x + batch_shape_f + batch_shape_g + (context_length, num_nodes, out_dim_g) + encoded_samples = self.l(x_and_embeddings) + + # Shape batch_shape_samples + batch_shape_g + (num_nodes, out_dim_g) + aggregated_effects = torch.einsum("...lij,...lio->...jo", self.w * graphs, encoded_samples) + + # ζ(uᵢ, Σⱼ Wᵢⱼ Gⱼᵢ l(uⱼ, xⱼ)) + # Shape batch_shape_x + batch_shape_f + batch_shape_g + (num_nodes, concatenated_shape) + decoded_samples = self.zeta(torch.cat([aggregated_effects, expanded_embed[..., -1, :, :]], dim=-1)) + + # Mask and aggregate Shape batch_shape_x + batch_shape_f + batch_shape_g + (concatenated_shape) + return torch.einsum("...ij,ij->...j", decoded_samples, self.group_mask) diff --git a/src/causica/sem/distribution_parameters_sem.py b/src/causica/sem/distribution_parameters_sem.py index 60efc2c..4bcaa70 100644 --- a/src/causica/sem/distribution_parameters_sem.py +++ b/src/causica/sem/distribution_parameters_sem.py @@ -1,3 +1,5 @@ +from typing import Optional + import torch from tensordict import TensorDict @@ -6,6 +8,7 @@ from causica.functional_relationships import ( DoFunctionalRelationships, FunctionalRelationships, + TemporalEmbedFunctionalRelationships, create_do_functional_relationship, ) from causica.sem.structural_equation_model import SEM @@ -25,25 +28,34 @@ def __init__( graph: torch.Tensor, noise_dist: JointNoiseModule, func: FunctionalRelationships, + log_func_rescale: Optional[FunctionalRelationships] = None, ): """ Args: graph: Adjacency Matrix noise_dist: A dictionary of callables for generating distributions from predictions func: The functional relationship that can map from the tensor representation to the to itself. + log_func_rescale: The log of the functional relationship that applies a data-dependent operation on the noise. """ + self.batch_shape_f = func.batch_shape - batch_shape_g = torch.Size(graph.shape[:-2]) + self.batch_shape_g = torch.Size(graph.shape[:-2]) - if len(self.batch_shape_f) == 0 and len(batch_shape_g) > 0: + if isinstance(func, TemporalEmbedFunctionalRelationships): + raise ValueError(f"Functional relationship {func} is a TemporalEmbedFunctionalRelationships.") + + if len(self.batch_shape_f) == 0 and len(self.batch_shape_g) > 0: self.batch_shape_f = torch.Size((1,)) func.batch_shape = self.batch_shape_f super().__init__( - graph=graph, node_names=noise_dist.keys(), batch_shape=torch.Size(self.batch_shape_f + batch_shape_g) + graph=graph, + node_names=noise_dist.keys(), + batch_shape=torch.Size(self.batch_shape_f + self.batch_shape_g), ) self.noise_dist = noise_dist self.func = func + self.log_func_rescale = log_func_rescale def log_prob(self, value: TensorDict) -> torch.Tensor: """ @@ -55,7 +67,13 @@ def log_prob(self, value: TensorDict) -> torch.Tensor: A tensor of shape `sample_shape + batch_shape` of the log probability of each event """ expanded_value = expand_td_with_batch_shape(value, self.batch_shape) - return self.noise_dist(self.func(expanded_value, self.graph)).log_prob(expanded_value) + if self.log_func_rescale is None: + res = self.noise_dist(self.func(expanded_value, self.graph)).log_prob(expanded_value) + else: + res = self.noise_dist( + (self.func(expanded_value, self.graph), self.log_func_rescale(expanded_value, self.graph)) + ).log_prob(expanded_value) + return res def noise_to_sample(self, noise: TensorDict) -> TensorDict: """ @@ -71,7 +89,14 @@ def noise_to_sample(self, noise: TensorDict) -> TensorDict: x = expand_td_with_batch_shape(noise.clone(), self.batch_shape).zero_() expanded_noise = expand_td_with_batch_shape(noise, self.batch_shape) for _ in self.node_names: - x.update_(self.noise_dist(self.func(x, self.graph)).noise_to_sample(expanded_noise)) + if self.log_func_rescale is None: + x.update_(self.noise_dist(self.func(x, self.graph)).noise_to_sample(expanded_noise)) + else: + x.update_( + self.noise_dist((self.func(x, self.graph), self.log_func_rescale(x, self.graph))).noise_to_sample( + expanded_noise + ) + ) if isinstance(self.func, DoFunctionalRelationships): do = self.func.do if do.batch_dims > 0: @@ -91,7 +116,14 @@ def sample_to_noise(self, sample: TensorDict) -> TensorDict: Dictionary of tensors representing the noise shape `sample_shape + batch_shape + noise_shape` """ expanded_sample = expand_td_with_batch_shape(sample, self.batch_shape) - return self.noise_dist(self.func(expanded_sample, self.graph)).sample_to_noise(expanded_sample) + if self.log_func_rescale is None: + res = self.noise_dist(self.func(expanded_sample, self.graph)).sample_to_noise(expanded_sample) + else: + res = self.noise_dist( + (self.func(expanded_sample, self.graph), self.log_func_rescale(expanded_sample, self.graph)) + ).sample_to_noise(sample) + + return res @torch.no_grad() def sample_noise(self, sample_shape: torch.Size = torch.Size()) -> TensorDict: @@ -113,10 +145,16 @@ def do(self, interventions: TensorDict) -> "DistributionParametersSEM": If the interventions have a batch shape, then the returned functions will have the same batch shape to batch the computation across interventions. """ + do_log_func = None if isinstance(self.func, DoFunctionalRelationships): + if self.log_func_rescale is not None: + if not isinstance(self.log_func_rescale, DoFunctionalRelationships): + raise ValueError( + "log_func_rescale must be DoFunctionalRelationships when func is DoFunctionalRelationships" + ) + if set(interventions.keys()) & set(self.func.do.keys()): raise ValueError("Cannot intervene on already intervened nodes") - prev_interventions = self.func.do if interventions.batch_dims == 0 and prev_interventions.batch_dims > 0: interventions = interventions.expand(prev_interventions.batch_size) @@ -126,20 +164,39 @@ def do(self, interventions: TensorDict) -> "DistributionParametersSEM": raise ValueError( f"Interventions batch shape {interventions.batch_size} does not match do batch shape {self.func.do.batch_size}" ) + interventions_updated = interventions.update(prev_interventions) + graph_updated = self.func.pad_intervened_graphs(self.graph) do_func, do_graphs = create_do_functional_relationship( - interventions=interventions.update(prev_interventions), + interventions=interventions_updated, func=self.func.func, - graph=self.func.pad_intervened_graphs(self.graph), + graph=graph_updated, ) + + if self.log_func_rescale is not None: + interventions_replaced = interventions_updated.apply(torch.zeros_like) + do_log_func, _ = create_do_functional_relationship( + interventions=interventions_replaced, + func=self.log_func_rescale.func, + graph=graph_updated, + ) + else: + do_func, do_graphs = create_do_functional_relationship( interventions=interventions, func=self.func, graph=self.graph ) + if self.log_func_rescale is not None: + interventions_replaced = interventions.apply(torch.zeros_like) + do_log_func, _ = create_do_functional_relationship( + interventions=interventions_replaced, func=self.log_func_rescale, graph=self.graph + ) + intervened_node_names = set(interventions.keys()) unintervened_node_names = [name for name in self.node_names if name not in intervened_node_names] # keep order return DistributionParametersSEM( graph=do_graphs, noise_dist=self.noise_dist[unintervened_node_names], func=do_func, + log_func_rescale=do_log_func, ) diff --git a/src/causica/sem/sem_distribution.py b/src/causica/sem/sem_distribution.py index b349b66..3a9c8f8 100644 --- a/src/causica/sem/sem_distribution.py +++ b/src/causica/sem/sem_distribution.py @@ -5,8 +5,9 @@ from causica.distributions.adjacency import AdjacencyDistribution from causica.distributions.distribution_module import DistributionModule from causica.distributions.noise.joint import JointNoiseModule -from causica.functional_relationships.functional_relationships import FunctionalRelationships +from causica.functional_relationships import FunctionalRelationships, TemporalEmbedFunctionalRelationships from causica.sem.distribution_parameters_sem import DistributionParametersSEM +from causica.sem.temporal_distribution_parameters_sem import TemporalDistributionParametersSEM class SEMDistribution(td.Distribution): @@ -40,7 +41,7 @@ def __init__( self._functional_relationships = functional_relationships def _create_sems(self, graphs: torch.Tensor) -> list[DistributionParametersSEM]: - graphs = graphs.reshape(-1, *graphs.shape[-2:]) + graphs = graphs.reshape(-1, *self._adjacency_dist.event_shape) return [ DistributionParametersSEM( graph=graph, @@ -56,9 +57,7 @@ def sample(self, sample_shape: torch.Size = torch.Size()): graphs = graphs[None, ...] return self._create_sems(graphs) - def relaxed_sample( - self, sample_shape: torch.Size = torch.Size(), temperature: float = 0.0 - ) -> list[DistributionParametersSEM]: + def relaxed_sample(self, sample_shape: torch.Size = torch.Size(), temperature: float = 0.0): graphs = self._adjacency_dist.relaxed_sample(sample_shape=sample_shape, temperature=temperature) return self._create_sems(graphs) @@ -67,19 +66,11 @@ def entropy(self) -> torch.Tensor: @property def mean(self) -> DistributionParametersSEM: # type: ignore - return DistributionParametersSEM( - graph=self._adjacency_dist.mean, - noise_dist=self._noise_module, - func=self._functional_relationships, - ) + return self._create_sems(self._adjacency_dist.mean)[0] @property def mode(self) -> DistributionParametersSEM: # type: ignore - return DistributionParametersSEM( - graph=self._adjacency_dist.mode, - noise_dist=self._noise_module, - func=self._functional_relationships, - ) + return self._create_sems(self._adjacency_dist.mode)[0] def log_prob(self, value: DistributionParametersSEM) -> torch.Tensor: # type: ignore return self._adjacency_dist.log_prob(value.graph) @@ -105,3 +96,47 @@ def forward(self) -> SEMDistribution: noise_module=self.noise_module, functional_relationships=self.functional_relationships, ) + + +class TemporalSEMDistribution(SEMDistribution): + def __init__( + self, + adjacency_dist: AdjacencyDistribution, + noise_module: JointNoiseModule, + functional_relationships: TemporalEmbedFunctionalRelationships, + ): + super().__init__(adjacency_dist, noise_module, functional_relationships) + self._functional_relationships: TemporalEmbedFunctionalRelationships = functional_relationships + + def _create_sems(self, graphs: torch.Tensor) -> list[TemporalDistributionParametersSEM]: # type: ignore + graphs = graphs.reshape(-1, *self._adjacency_dist.event_shape) + return [ + TemporalDistributionParametersSEM( + graph=graph, + noise_dist=self._noise_module, + func=self._functional_relationships, + ) + for graph in graphs.unbind(dim=0) + ] + + +class TemporalSEMDistributionModule(DistributionModule[TemporalSEMDistribution]): + """Represents a SEMDistribution with learnable parameters.""" + + def __init__( + self, + adjacency_module: DistributionModule[AdjacencyDistribution], + functional_relationships: TemporalEmbedFunctionalRelationships, + noise_module: JointNoiseModule, + ): + super().__init__() + self.adjacency_module = adjacency_module + self.functional_relationships = functional_relationships + self.noise_module = noise_module + + def forward(self) -> TemporalSEMDistribution: + return TemporalSEMDistribution( + adjacency_dist=self.adjacency_module(), + noise_module=self.noise_module, + functional_relationships=self.functional_relationships, + ) diff --git a/src/causica/sem/structural_equation_model.py b/src/causica/sem/structural_equation_model.py index 7cd7c6a..9503642 100644 --- a/src/causica/sem/structural_equation_model.py +++ b/src/causica/sem/structural_equation_model.py @@ -46,10 +46,10 @@ def __init__( len(node_names), len(node_names), ), "Graph adjacency matrix must have shape [num_nodes, num_nodes] (excluding batch dimensions)" - self.batch_shape_g = torch.Size(graph.shape[:-2]) if batch_shape is None: + self.batch_shape_g = torch.Size(graph.shape[:1]) if graph.ndim > 2 else torch.Size([]) batch_shape = self.batch_shape_g - self.batch_shape_f = batch_shape[: -len(self.batch_shape_g)] + self.batch_shape_f = torch.Size([]) super().__init__(event_shape=event_shape, batch_shape=batch_shape) @property diff --git a/src/causica/sem/temporal_distribution_parameters_sem.py b/src/causica/sem/temporal_distribution_parameters_sem.py new file mode 100644 index 0000000..af770c2 --- /dev/null +++ b/src/causica/sem/temporal_distribution_parameters_sem.py @@ -0,0 +1,182 @@ +import torch +from tensordict import TensorDict, TensorDictBase + +from causica.datasets.tensordict_utils import expand_td_with_batch_shape +from causica.distributions import JointNoiseModule +from causica.functional_relationships import DoFunctionalRelationships, TemporalEmbedFunctionalRelationships +from causica.sem.structural_equation_model import SEM + + +def split_lagged_and_instanteneous_values(td: TensorDictBase) -> tuple[TensorDict, TensorDict]: + """Splits a temporal TensorDict into lagged and instantaneous values. + + Args: + td: The temporal TensorDict with shape (..., context_length, (variable_shape)). The batch size of the TensorDict + should not include the context length, ie. td.batch_size = (...) and not td.batch_size = (..., context_length). + + Returns: + A tuple with the lagged values and the instantaneous values. + """ + orig_batch_size = td.batch_size + new_batch_size = next(td.values()).shape[:-1] + td.batch_size = new_batch_size + + lagged, instantaneous = td[..., :-1], td[..., -1] + + td.batch_size = orig_batch_size + lagged.batch_size = orig_batch_size + instantaneous.batch_size = orig_batch_size + + return lagged, instantaneous + + +def concatenate_lagged_and_instaneous_values( + lagged_values: TensorDictBase, + instantaneous_values: TensorDictBase, +) -> TensorDict: + """Concatenate the lagged and instantaneous values. + + Args: + instantaneous_values: The TensorDict with the instantaneous values with shape (..., (variable_shape)) + lagged_values: The TensorDict with the lagged values with shape (..., context_length, (variable_shape)) + + Returns: + A new TensorDict with the concatenated lagged and instantaneous values. + """ + return TensorDict( + { + k: torch.cat([lagged_values[k], v[..., None, :]], dim=-2) + for k, v in instantaneous_values.items(include_nested=True, leaves_only=True) + }, + batch_size=instantaneous_values.batch_size, + ) + + +class TemporalDistributionParametersSEM(SEM): + """ + A Structural Equation Model where the functional forward pass generates the parameters of the distributions to be sampled. + + This is more general than the simple additive case. + """ + + def __init__( + self, + graph: torch.Tensor, + noise_dist: JointNoiseModule, + func: TemporalEmbedFunctionalRelationships, + ): + """ + Args: + graph: Adjacency Matrix + noise_dist: A dictionary of callables for generating distributions from predictions + func: The functional relationship that can map from the tensor representation to the to itself. + """ + + self.batch_shape_f = func.batch_shape + batch_shape_g = torch.Size(graph.shape[:-3]) + self.context_length = graph.shape[-3] + + if self.context_length != func.nn.context_length: + raise ValueError( + f"Graph context length {self.context_length} does not match functional relationship context length {func.context_length}" + ) + + if len(self.batch_shape_f) == 0 and len(batch_shape_g) > 0: + self.batch_shape_f = torch.Size((1,)) + func.batch_shape = self.batch_shape_f + + super().__init__( + graph=graph, + node_names=noise_dist.keys(), + batch_shape=torch.Size(self.batch_shape_f + batch_shape_g), + ) + self.noise_dist = noise_dist + self.func = func + + def log_prob(self, value: TensorDict) -> torch.Tensor: + """ + Compute the log prob of the observations + + Args: + value: a dictionary of `sample_shape + batch_shape + context_length + [variable_dim]` shaped tensors + Return: + A tensor of shape `sample_shape + batch_shape` of the log probability of each event + """ + if value[value.sorted_keys[0]].shape[-2] != self.context_length: + raise ValueError( + f"Value shape {value[value.sorted_keys[0]].shape} does not match graph shape {self.graph.shape}." + " The last batch dimension of the value should match the context length." + ) + expanded_value = expand_td_with_batch_shape(value, self.batch_shape) + _lagged, test_value = split_lagged_and_instanteneous_values(expanded_value) # pylint: disable=unused-variable + return self.noise_dist(self.func(expanded_value, self.graph)).log_prob(test_value) + + def noise_to_sample(self, noise: TensorDict) -> TensorDict: + """ + For a given noise vector, return the corresponding sample values. + + Args: + noise: Dictionary of tensors representing the noise shape + `sample_shape + batch_shape + context_length + noise_shape`, where the context length dimension contains + the observed history as well as the noise for the current time step. + Return: + Dictionary of samples from the sem corresponding to the noise, shape + `sample_shape + batch_shape + event_shape` + """ + x = expand_td_with_batch_shape(noise.clone(), self.batch_shape).zero_() + history, noise = split_lagged_and_instanteneous_values(noise) + expanded_noise = expand_td_with_batch_shape(noise, self.batch_shape) + for _ in self.node_names: + new_sample = self.noise_dist(self.func(x, self.graph)).noise_to_sample(expanded_noise) + x.update_(concatenate_lagged_and_instaneous_values(history, new_sample)) + x = split_lagged_and_instanteneous_values(x)[-1] + if isinstance(self.func, DoFunctionalRelationships): + do = self.func.do + if do.batch_dims > 0: + do = do[(...,) + (None,) * len(self.batch_shape_g)] + expanded_do = do.expand(*x.batch_size) + x.update(expanded_do, inplace=True) + x = self.func.func.tensor_to_td.order_td(x) + return x + + def sample_to_noise(self, sample: TensorDict) -> TensorDict: + """ + For a given sample get the noise vector. + + Args: + sample: samples from the sem each of shape `sample_shape + batch_shape + context_length + event_shape` + Return: + Dictionary of tensors representing the noise shape `sample_shape + batch_shape + noise_shape` + """ + if sample[sample.sorted_keys[0]].shape[-2] != self.context_length: + raise ValueError( + f"Value shape {sample[sample.sorted_keys[0]].shape} does not match graph shape {self.graph.shape}." + " The last batch dimension of the value should match the context length." + ) + + expanded_sample = expand_td_with_batch_shape(sample, self.batch_shape) + _lagged, test_sample = split_lagged_and_instanteneous_values(expanded_sample) # pylint: disable=unused-variable + return self.noise_dist(self.func(expanded_sample, self.graph)).sample_to_noise(test_sample) + + def do(self, interventions: TensorDict) -> "TemporalDistributionParametersSEM": + """Return the SEM associated with the interventions. + + If the interventions have a batch shape, then the returned functions will have the same batch shape to batch + the computation across interventions. + """ + _ = interventions + raise NotImplementedError("Interventions are not yet supported for temporal graphs") + + @torch.no_grad() + def sample_noise(self, sample_shape: torch.Size = torch.Size()) -> TensorDict: + """ + Sample the noise vector for the distribution. + + Args: + sample_shape: shape of the returned noise samples + Return: + Dictionary of samples from the noise distribution for each node, shape + `sample_shape + batch_shape + event_shape`. + """ + + return self.noise_dist().sample(torch.Size(torch.Size(sample_shape) + self.batch_shape)) diff --git a/test/data_generation/test_generate_data.py b/test/data_generation/test_generate_data.py index 60551e8..9cac906 100644 --- a/test/data_generation/test_generate_data.py +++ b/test/data_generation/test_generate_data.py @@ -13,8 +13,10 @@ plot_dataset, sample_counterfactual, sample_dataset, + sample_effects_given_treatment, sample_intervention, sample_treatment_and_effect, + sample_treatment_given_effects, ) from causica.data_generation.samplers.functional_relationships_sampler import LinearRelationshipsSampler from causica.data_generation.samplers.noise_dist_sampler import ( @@ -288,7 +290,7 @@ def test_sample_treatment_and_effect(): graph = torch.zeros(5, 5) node_names = [f"x_{i}" for i in range(5)] - with pytest.warns(UserWarning, match="No edges found."): + with pytest.warns(UserWarning, match="No nodes with descendants found in the graph."): treatment, effects = sample_treatment_and_effect(graph, node_names) graph[0, 1] = 1 @@ -297,3 +299,33 @@ def test_sample_treatment_and_effect(): treatment, effects = sample_treatment_and_effect(graph, node_names) assert (treatment == "x_0" and effects == ["x_1"]) or (treatment == "x_2" and effects == ["x_3"]) + + +def test_sample_treatment_given_effects(): + graph = torch.zeros(5, 5) + node_names = [f"x_{i}" for i in range(5)] + graph[0, 1] = 1 + graph[2, 3] = 1 + + with pytest.warns(None): + treatment = sample_treatment_given_effects(graph, node_names, effect_variables=["x_3"]) + assert treatment == "x_2" + + with pytest.warns(UserWarning, match="No common ancestors found for the given effects."): + treatment = sample_treatment_given_effects(graph, node_names, effect_variables=["x_2"]) + assert treatment != "x_2" + + +def test_sample_effects_given_treatment(): + graph = torch.zeros(5, 5) + node_names = [f"x_{i}" for i in range(5)] + graph[0, 1] = 1 + graph[2, 3] = 1 + + with pytest.warns(None): + effects = sample_effects_given_treatment(graph, node_names, treatment_variable="x_0") + assert effects == ["x_1"] + + with pytest.warns(UserWarning, match="No descendants found for the given treatment."): + effects = sample_effects_given_treatment(graph, node_names, treatment_variable="x_1") + assert effects != ["x_1"] diff --git a/test/datasets/test_timeseries_dataset.py b/test/datasets/test_timeseries_dataset.py new file mode 100644 index 0000000..001aaf8 --- /dev/null +++ b/test/datasets/test_timeseries_dataset.py @@ -0,0 +1,147 @@ +from dataclasses import replace +from pathlib import Path + +import numpy as np +import pytest +import torch +from tensordict import TensorDict +from torch.utils.data import DataLoader + +from causica.datasets.causica_dataset_format import Variable, VariablesMetadata +from causica.datasets.timeseries_dataset import ( + IndexedTimeseriesDataset, + ensure_adjacency_matrix, + ensure_variables_metadata, + index_contiguous_chunks, + preprocess_data, +) +from causica.distributions.adjacency.erdos_renyi import ErdosRenyiDAGDistribution + + +@pytest.mark.parametrize("data", [torch.randn((10, 3)), np.random.randn(10, 3)]) +def test_preprocess_data_with_tensor(data): + # Check correctness without metadata + tensordict_data = preprocess_data(data) + tensor_data = torch.tensor(data, dtype=torch.float32) # Will be converted when creating a TensorDict with metadata + for i, (key, values) in enumerate(sorted(tensordict_data.items())): + assert i == int(key.lstrip("x")) + torch.testing.assert_close(values.squeeze(1), tensor_data[..., i]) + + # Check correctness with metadata + keys = ["a", "b", "c"] + variables_metadata = VariablesMetadata([Variable(key, key) for key in keys]) + tensordict_data = preprocess_data(data, variables_metadata) + for i, key in enumerate(keys): + torch.testing.assert_close(tensordict_data[key].squeeze(1), tensor_data[..., i]) + + # Check that incorrect metadata raises an error + with pytest.raises(ValueError): + preprocess_data(data, replace(variables_metadata, variables=variables_metadata.variables[:-1])) + + +def test_preprocess_data_with_tensordict(): + data = TensorDict({"a": torch.randn(10, 3), "b": torch.randn(10, 3), "c": torch.randn(10, 3)}, batch_size=10) + tensordict_data = preprocess_data(data) + assert tensordict_data is data + + selected_keys = list(data.keys())[:-1] + variables_metadata = VariablesMetadata([Variable(key, key) for key in selected_keys]) + selected_data = preprocess_data(tensordict_data, variables_metadata) + torch.testing.assert_close(selected_data, data.select(*selected_keys)) + + +@pytest.mark.parametrize("num_dims", [1, 11, 17]) +@pytest.mark.parametrize("num_repetitions", [1, 3, 5]) +def test_index_contiguous_chunks(num_dims, num_repetitions): + td = TensorDict({"value": torch.eye(num_dims), "index": torch.arange(num_dims)}, batch_size=num_dims) + data = torch.cat([td] * num_repetitions) + data["order"] = torch.arange(data.size(0)) # Set a global order to verify that the relative order is intact + + contiguous_data, slices = index_contiguous_chunks(data, "index") + + for dim in range(num_dims): + chunk = contiguous_data[slices[dim]] + expected_value = torch.eye(num_dims)[[dim] * num_repetitions] + + # Verify that the correct value was retrieved + torch.testing.assert_close(chunk["value"], expected_value) + # Verify that the relative order is intact + torch.testing.assert_close(chunk["order"], torch.sort(chunk["order"]).values) + + +@pytest.mark.parametrize("num_nodes", [2, 3, 10]) +def test_ensure_adjacency_matrix(num_nodes: int, tmp_path: Path): + adjacency_matrix = ErdosRenyiDAGDistribution(num_nodes, probs=torch.tensor(0.9)).sample().to(torch.int64) + + file_path_csv = tmp_path / "adjacency_matrix.csv" + np.savetxt(str(file_path_csv), adjacency_matrix.numpy(), delimiter=",") + loaded_adjacency_matrix = ensure_adjacency_matrix(str(file_path_csv)) + torch.testing.assert_close(loaded_adjacency_matrix, adjacency_matrix) + + file_path_npy = tmp_path / "adjacency_matrix.npy" + np.save(str(file_path_npy), adjacency_matrix.numpy()) + loaded_adjacency_matrix = ensure_adjacency_matrix(str(file_path_npy)) + torch.testing.assert_close(loaded_adjacency_matrix, adjacency_matrix) + + with pytest.raises(ValueError): + ensure_adjacency_matrix(str(tmp_path / "adjacency_matrix.txt")) + + torch.testing.assert_close(ensure_adjacency_matrix(adjacency_matrix.numpy()), adjacency_matrix) + torch.testing.assert_close(ensure_adjacency_matrix(adjacency_matrix), adjacency_matrix) + + +def test_ensure_variables_metadata(tmp_path: Path): + keys = ["a", "b", "c"] + variables_metadata = VariablesMetadata([Variable(key, key) for key in keys]) + assert ensure_variables_metadata(variables_metadata) is variables_metadata + + filepath = tmp_path / "variables_metadata.json" + with filepath.open("w") as f: + f.write(variables_metadata.to_json()) # type: ignore # MyPy is unable to find the method from dataclass_json + assert ensure_variables_metadata(str(filepath)) == variables_metadata + + +@pytest.mark.parametrize("num_timeseries", [1, 33, 101]) +@pytest.mark.parametrize("max_length", [1, 5, 77]) +@pytest.mark.parametrize("num_dims", [1, 3, 10]) +def test_indexed_timeseries_dataset(num_timeseries: int, max_length: int, num_dims: int): + # Create dense timeseries with varying length for easier verification + data = torch.randn(num_timeseries, max_length, num_dims) + lengths = torch.randint(1, max_length + 1, (num_timeseries,)) + adjacency_matrix = ErdosRenyiDAGDistribution(num_dims, probs=torch.tensor(0.9)).sample() + timeseries_dataset = IndexedTimeseriesDataset.from_dense(data, lengths, adjacency_matrix=adjacency_matrix) + + # Verify access to each timeseries + assert len(timeseries_dataset) == num_timeseries + for i in range(num_timeseries): + for j in range(num_dims): + # Take feature j from the dense timeseries i, add a dummy dim to match how the behavior of TensorDict + # datasets which generally always have a feature axis even for scalar features. + xj = data[i][: lengths[i], :][..., j, None] + torch.testing.assert_close(timeseries_dataset[i][f"x{j}"], xj) + + +@pytest.mark.parametrize("num_timeseries", [33]) +@pytest.mark.parametrize("max_length", [11]) +@pytest.mark.parametrize("num_dims", [1, 3]) +@pytest.mark.parametrize("batch_size", [1, 5]) +def test_batching_indexed_timeseries_dataset_fixed_length( + num_timeseries: int, max_length: int, num_dims: int, batch_size: int +): + data = torch.randn(num_timeseries, max_length, num_dims) + lengths = torch.full((num_timeseries,), max_length, dtype=torch.int32) + adjacency_matrix = ErdosRenyiDAGDistribution(num_dims, probs=torch.tensor(0.9)).sample() + timeseries_dataset = IndexedTimeseriesDataset.from_dense(data, lengths, adjacency_matrix=adjacency_matrix) + + data_loader = DataLoader( + timeseries_dataset, batch_size=batch_size, shuffle=False, drop_last=False, collate_fn=torch.stack + ) + + count = 0 + for i, batch in enumerate(data_loader): + if i == len(timeseries_dataset) // batch_size: + assert batch.batch_size == (num_timeseries % batch_size, max_length) + else: + assert batch.batch_size == (batch_size, max_length) + count += batch.batch_size[0] + assert count == num_timeseries diff --git a/test/distributions/adjacency/test_adjacency_distributions.py b/test/distributions/adjacency/test_adjacency_distributions.py index faca2da..fffd08d 100644 --- a/test/distributions/adjacency/test_adjacency_distributions.py +++ b/test/distributions/adjacency/test_adjacency_distributions.py @@ -1,5 +1,5 @@ """Module with generic Adjacency Distribution tests.""" -from typing import Type +from typing import Optional, Type, Union import numpy as np import pytest @@ -9,13 +9,22 @@ AdjacencyDistribution, ConstrainedAdjacencyDistribution, ENCOAdjacencyDistribution, + TemporalConstrainedAdjacencyDistribution, ThreeWayAdjacencyDistribution, ) +from causica.distributions.adjacency.temporal_adjacency_distributions import ( + LaggedAdjacencyDistribution, + RhinoLaggedAdjacencyDistribution, + TemporalAdjacencyDistribution, +) def _distribution_factory( - dist_class: Type[AdjacencyDistribution], num_nodes: int, batch_shape: torch.Size -) -> AdjacencyDistribution: + dist_class: Type[Union[AdjacencyDistribution, TemporalAdjacencyDistribution, LaggedAdjacencyDistribution]], + num_nodes: int, + batch_shape: torch.Size, + context_length: Optional[int] = None, +) -> Union[AdjacencyDistribution, TemporalAdjacencyDistribution, LaggedAdjacencyDistribution]: """Create a combined interface for producing Adjacency Distributions (allows us to use `parametrize` over them)""" if dist_class is ConstrainedAdjacencyDistribution: logits = torch.randn(batch_shape + ((num_nodes * (num_nodes - 1)) // 2, 3)) @@ -26,27 +35,75 @@ def _distribution_factory( return ConstrainedAdjacencyDistribution( inner_dist, positive_constraints=positive_constraints, negative_constraints=negative_constraints ) + if dist_class is TemporalConstrainedAdjacencyDistribution: + assert context_length is not None + logits_exists = torch.randn(batch_shape + (context_length, num_nodes, num_nodes)) + logits_orient = torch.randn(batch_shape + ((num_nodes * (num_nodes - 1)) // 2,)) + lagged_dist = ( + RhinoLaggedAdjacencyDistribution(logits_edge=logits_exists[..., :-1, :, :], lags=context_length - 1) + if context_length > 1 + else None + ) + inst_dist = ENCOAdjacencyDistribution(logits_exist=logits_exists[..., -1, :, :], logits_orient=logits_orient) + inner_dist_temporal = TemporalAdjacencyDistribution( + instantaneous_distribution=inst_dist, lagged_distribution=lagged_dist + ) + square_ones = torch.ones(context_length, num_nodes, num_nodes, dtype=torch.bool) + positive_constraints = torch.triu(square_ones, diagonal=1) + negative_constraints = torch.tril(square_ones, diagonal=-1) + return TemporalConstrainedAdjacencyDistribution( + inner_dist_temporal, positive_constraints=positive_constraints, negative_constraints=negative_constraints + ) + if dist_class is ENCOAdjacencyDistribution: length = (num_nodes * (num_nodes - 1)) // 2 return ENCOAdjacencyDistribution( logits_exist=torch.randn(batch_shape + (num_nodes, num_nodes)), logits_orient=torch.randn(batch_shape + (length,)), ) + if dist_class is TemporalAdjacencyDistribution: + assert context_length is not None + logits_exists = torch.randn(batch_shape + (context_length, num_nodes, num_nodes)) + logits_orient = torch.randn(batch_shape + ((num_nodes * (num_nodes - 1)) // 2,)) + lagged_dist = ( + RhinoLaggedAdjacencyDistribution(logits_edge=logits_exists[..., :-1, :, :], lags=context_length - 1) + if context_length > 1 + else None + ) + inst_dist = ENCOAdjacencyDistribution(logits_exist=logits_exists[..., -1, :, :], logits_orient=logits_orient) + return TemporalAdjacencyDistribution(instantaneous_distribution=inst_dist, lagged_distribution=lagged_dist) if dist_class is ThreeWayAdjacencyDistribution: logits = torch.randn(batch_shape + ((num_nodes * (num_nodes - 1)) // 2, 3)) return ThreeWayAdjacencyDistribution(logits=logits) + if dist_class is RhinoLaggedAdjacencyDistribution: + assert context_length is not None + return RhinoLaggedAdjacencyDistribution( + logits_edge=torch.randn(batch_shape + (context_length - 1, num_nodes, num_nodes)), lags=context_length - 1 + ) raise ValueError("Unrecognised Class") -DIST_CLASSES = [ConstrainedAdjacencyDistribution, ENCOAdjacencyDistribution, ThreeWayAdjacencyDistribution] +DIST_CLASSES = [ + ConstrainedAdjacencyDistribution, + ENCOAdjacencyDistribution, + ThreeWayAdjacencyDistribution, +] +TEMPORAL_DIST_CLASSES = [TemporalAdjacencyDistribution, TemporalConstrainedAdjacencyDistribution] + BATCH_SHAPES = [torch.Size(), (2,)] + # pylint: disable=protected-access @pytest.mark.parametrize("dist_class", DIST_CLASSES) @pytest.mark.parametrize("batch_shape", BATCH_SHAPES) -def test_support(dist_class: Type[AdjacencyDistribution], batch_shape: torch.Size): +def test_support( + dist_class: Type[AdjacencyDistribution], + batch_shape: torch.Size, +): """Test that the defined support works as expected. This method will be used to test other features.""" num_nodes = 3 + + # static adj distribution dist = _distribution_factory(dist_class=dist_class, num_nodes=num_nodes, batch_shape=batch_shape) mat = torch.ones((num_nodes, num_nodes)) @@ -60,9 +117,55 @@ def test_support(dist_class: Type[AdjacencyDistribution], batch_shape: torch.Siz @pytest.mark.parametrize("batch_shape", BATCH_SHAPES) -@pytest.mark.parametrize("dist_class", DIST_CLASSES) +def test_support_lagged(batch_shape: torch.Size): + """Test that the defined support works as expected for lagged adjacency distribution. + + This method will be used to test other features. + """ + context_length = 3 + num_nodes = 3 + dist = _distribution_factory( + dist_class=RhinoLaggedAdjacencyDistribution, + num_nodes=num_nodes, + batch_shape=batch_shape, + context_length=context_length, + ) + mat = torch.ones((context_length - 1, num_nodes, num_nodes)) + + # validate sample returns None when there is no error + dist._validate_sample(mat) + + # validate sample throws when the sample is invalid + with pytest.raises(ValueError): + dist._validate_sample(4 * mat) + + +@pytest.mark.parametrize("dist_class", TEMPORAL_DIST_CLASSES) +@pytest.mark.parametrize("batch_shape", BATCH_SHAPES) +def test_support_temporal(dist_class: Type[TemporalAdjacencyDistribution], batch_shape: torch.Size): + """Test that the defined support works as expected for temporal adjacency distribution. + + This method will be used to test other features. + """ + context_length = 3 + num_nodes = 3 + + dist = _distribution_factory( + dist_class=dist_class, num_nodes=num_nodes, batch_shape=batch_shape, context_length=context_length + ) + mat = torch.ones((context_length, num_nodes, num_nodes)) + # validate sample returns None when there is no error + dist._validate_sample(mat) + + # validate sample throws when the sample is invalid + with pytest.raises(ValueError): + dist._validate_sample(4 * mat) + + +@pytest.mark.parametrize("batch_shape", BATCH_SHAPES) @pytest.mark.parametrize("relaxed_sample", [True, False]) @pytest.mark.parametrize(("num_nodes", "sample_shape"), [(3, tuple()), (4, (20,)), (2, (4, 5))]) +@pytest.mark.parametrize("dist_class", DIST_CLASSES) def test_sample_shape( dist_class: Type[AdjacencyDistribution], num_nodes: int, @@ -71,6 +174,7 @@ def test_sample_shape( batch_shape: torch.Size, ): """Test the sample/rsample method returns binary tensors in the support of the correct shape""" + dist = _distribution_factory(dist_class=dist_class, num_nodes=num_nodes, batch_shape=batch_shape) samples = dist.relaxed_sample(sample_shape, temperature=0.1) if relaxed_sample else dist.sample(sample_shape) assert samples.shape == sample_shape + batch_shape + (num_nodes, num_nodes) @@ -78,9 +182,56 @@ def test_sample_shape( assert not np.isnan(samples).any() -@pytest.mark.parametrize("dist_class", DIST_CLASSES) +@pytest.mark.parametrize("batch_shape", BATCH_SHAPES) +@pytest.mark.parametrize("relaxed_sample", [True, False]) +@pytest.mark.parametrize(("num_nodes", "sample_shape"), [(3, tuple()), (4, (20,)), (2, (4, 5))]) +@pytest.mark.parametrize("context_length", [3]) +def test_sample_shape_lagged( + num_nodes: int, sample_shape: torch.Size, relaxed_sample: bool, batch_shape: torch.Size, context_length: int +): + """Test the sample/rsample method returns binary tensors in the support of the correct shape for lagged distribution.""" + + dist = _distribution_factory( + dist_class=RhinoLaggedAdjacencyDistribution, + num_nodes=num_nodes, + batch_shape=batch_shape, + context_length=context_length, + ) + samples = dist.relaxed_sample(sample_shape, temperature=0.1) if relaxed_sample else dist.sample(sample_shape) + assert samples.shape == sample_shape + batch_shape + (context_length - 1, num_nodes, num_nodes) + + dist._validate_sample(samples) + assert not np.isnan(samples).any() + + +@pytest.mark.parametrize("batch_shape", BATCH_SHAPES) +@pytest.mark.parametrize("relaxed_sample", [True, False]) +@pytest.mark.parametrize(("num_nodes", "sample_shape"), [(3, tuple()), (4, (20,)), (2, (4, 5))]) +@pytest.mark.parametrize(("context_length"), [1, 3]) +@pytest.mark.parametrize("dist_class", TEMPORAL_DIST_CLASSES) +def test_sample_shape_temporal( + num_nodes: int, + sample_shape: torch.Size, + relaxed_sample: bool, + batch_shape: torch.Size, + context_length: int, + dist_class: Type[TemporalAdjacencyDistribution], +): + """Test the sample/rsample method returns binary tensors in the support of the correct shape for temporal distribution.""" + + dist = _distribution_factory( + dist_class=dist_class, num_nodes=num_nodes, batch_shape=batch_shape, context_length=context_length + ) + samples = dist.relaxed_sample(sample_shape, temperature=0.1) if relaxed_sample else dist.sample(sample_shape) + assert samples.shape == sample_shape + batch_shape + (context_length, num_nodes, num_nodes) + + dist._validate_sample(samples) + assert not np.isnan(samples).any() + + @pytest.mark.parametrize("relaxed_sample", [True, False]) @pytest.mark.parametrize("sample_shape", [(2000,), (40, 50)]) +@pytest.mark.parametrize("dist_class", DIST_CLASSES) def test_sample_distinct( dist_class: Type[AdjacencyDistribution], sample_shape: torch.Size, @@ -88,42 +239,231 @@ def test_sample_distinct( ): """Test the sample/rsample method returns distinct binary tensors""" num_nodes = 4 + dist = _distribution_factory(dist_class=dist_class, num_nodes=num_nodes, batch_shape=torch.Size()) samples = dist.relaxed_sample(sample_shape, temperature=0.1) if relaxed_sample else dist.sample(sample_shape) - # the samples should be different from each other - assert not np.all(np.isclose(samples, samples.reshape(-1, num_nodes, num_nodes)[0, ...])) + ref_sample = samples.reshape(-1, num_nodes, num_nodes) + # Check that the samples are distinct + assert not np.all(np.isclose(samples, ref_sample[0, ...])) + + +@pytest.mark.parametrize("relaxed_sample", [True, False]) +@pytest.mark.parametrize("sample_shape", [(2000,), (40, 50)]) +@pytest.mark.parametrize("context_length", [3]) +def test_sample_distinct_lagged( + sample_shape: torch.Size, + relaxed_sample: bool, + context_length: int, +): + """Test the sample/rsample method returns distinct binary tensors for lagged distribution.""" + num_nodes = 4 + + dist = _distribution_factory( + dist_class=RhinoLaggedAdjacencyDistribution, + num_nodes=num_nodes, + batch_shape=torch.Size(), + context_length=context_length, + ) + samples = dist.relaxed_sample(sample_shape, temperature=0.1) if relaxed_sample else dist.sample(sample_shape) + ref_sample = samples.reshape(-1, context_length - 1, num_nodes, num_nodes) + + # Check that the samples are distinct + assert not np.all(np.isclose(samples, ref_sample[0, ...])) + + +@pytest.mark.parametrize("relaxed_sample", [True, False]) +@pytest.mark.parametrize("sample_shape", [(2000,), (40, 50)]) +@pytest.mark.parametrize("dist_class", TEMPORAL_DIST_CLASSES) +@pytest.mark.parametrize("context_length", [1, 3]) +def test_sample_distinct_temporal( + dist_class: Type[TemporalAdjacencyDistribution], + sample_shape: torch.Size, + relaxed_sample: bool, + context_length: int, +): + """Test the sample/rsample method returns distinct binary tensors for temporal adjacency distribution.""" + num_nodes = 4 + + dist = _distribution_factory( + dist_class=dist_class, num_nodes=num_nodes, batch_shape=torch.Size(), context_length=context_length + ) + samples = dist.relaxed_sample(sample_shape, temperature=0.1) if relaxed_sample else dist.sample(sample_shape) + ref_sample = samples.reshape(-1, context_length, num_nodes, num_nodes) + + # Check that the samples are distinct + assert not np.all(np.isclose(samples, ref_sample[0, ...])) @pytest.mark.parametrize("batch_shape", BATCH_SHAPES) @pytest.mark.parametrize("dist_class", DIST_CLASSES) -def test_mean(dist_class: Type[AdjacencyDistribution], batch_shape: torch.Size): +def test_mean( + dist_class: Type[AdjacencyDistribution], + batch_shape: torch.Size, +): """Test basic properties of the means of the distributions""" num_nodes = 3 + dist = _distribution_factory(dist_class=dist_class, num_nodes=num_nodes, batch_shape=batch_shape) mean = dist.mean assert mean.shape == batch_shape + (num_nodes, num_nodes) + + assert (mean <= 1.0).all() + assert (mean >= 0.0).all() + + +@pytest.mark.parametrize("batch_shape", BATCH_SHAPES) +@pytest.mark.parametrize("context_length", [3]) +def test_mean_lagged( + batch_shape: torch.Size, + context_length: int, +): + """Test basic properties of the means of the lagged distributions""" + num_nodes = 3 + + dist = _distribution_factory( + dist_class=RhinoLaggedAdjacencyDistribution, + num_nodes=num_nodes, + batch_shape=batch_shape, + context_length=context_length, + ) + mean = dist.mean + assert mean.shape == batch_shape + (context_length - 1, num_nodes, num_nodes) + + assert (mean <= 1.0).all() + assert (mean >= 0.0).all() + + +@pytest.mark.parametrize("batch_shape", BATCH_SHAPES) +@pytest.mark.parametrize("dist_class", TEMPORAL_DIST_CLASSES) +@pytest.mark.parametrize("context_length", [1, 3]) +def test_mean_temporal( + dist_class: Type[TemporalAdjacencyDistribution], + batch_shape: torch.Size, + context_length: int, +): + """Test basic properties of the means of the temporal adjacency distributions""" + num_nodes = 3 + dist = _distribution_factory( + dist_class=dist_class, num_nodes=num_nodes, batch_shape=batch_shape, context_length=context_length + ) + mean = dist.mean + assert mean.shape == batch_shape + (context_length, num_nodes, num_nodes) assert (mean <= 1.0).all() assert (mean >= 0.0).all() @pytest.mark.parametrize("batch_shape", BATCH_SHAPES) @pytest.mark.parametrize("dist_class", DIST_CLASSES) -def test_mode(dist_class: Type[AdjacencyDistribution], batch_shape: torch.Size): +def test_mode( + dist_class: Type[AdjacencyDistribution], + batch_shape: torch.Size, +): """Test basic properties of the modes of the distributions""" num_nodes = 3 + dist = _distribution_factory(dist_class=dist_class, num_nodes=num_nodes, batch_shape=batch_shape) mode = dist.mode - dist._validate_sample(mode) # mode should be in the support assert mode.shape == batch_shape + (num_nodes, num_nodes) +@pytest.mark.parametrize("batch_shape", BATCH_SHAPES) +@pytest.mark.parametrize("context_length", [3]) +def test_mode_lagged( + batch_shape: torch.Size, + context_length: int, +): + """Test basic properties of the modes of the lagged distributions""" + num_nodes = 3 + + dist = _distribution_factory( + dist_class=RhinoLaggedAdjacencyDistribution, + num_nodes=num_nodes, + batch_shape=batch_shape, + context_length=context_length, + ) + mode = dist.mode + assert mode.shape == batch_shape + (context_length - 1, num_nodes, num_nodes) + + +@pytest.mark.parametrize("batch_shape", BATCH_SHAPES) +@pytest.mark.parametrize("dist_class", TEMPORAL_DIST_CLASSES) +@pytest.mark.parametrize("context_length", [1, 3]) +def test_mode_temporal( + dist_class: Type[TemporalAdjacencyDistribution], + batch_shape: torch.Size, + context_length: int, +): + """Test basic properties of the modes of the temporal adjacency distributions""" + num_nodes = 3 + + dist = _distribution_factory( + dist_class=dist_class, num_nodes=num_nodes, batch_shape=batch_shape, context_length=context_length + ) + mode = dist.mode + assert mode.shape == batch_shape + (context_length, num_nodes, num_nodes) + + @pytest.mark.parametrize("batch_shape", BATCH_SHAPES) @pytest.mark.parametrize("sample_shape", [tuple(), (2,), (3,)]) @pytest.mark.parametrize("dist_class", DIST_CLASSES) -def test_log_prob(dist_class: Type[AdjacencyDistribution], batch_shape: torch.Size, sample_shape: torch.Size): +def test_log_prob( + dist_class: Type[AdjacencyDistribution], + batch_shape: torch.Size, + sample_shape: torch.Size, +): """Test basic properties of the log_prob of the distributions""" num_nodes = 4 + dist = _distribution_factory(dist_class=dist_class, num_nodes=num_nodes, batch_shape=batch_shape) - values = torch.randint(0, 2, sample_shape + batch_shape + (num_nodes, num_nodes), dtype=torch.float64) + shapes = sample_shape + batch_shape + (num_nodes, num_nodes) + + values = torch.randint(0, 2, shapes, dtype=torch.float64) + log_probs = dist.log_prob(values) + assert log_probs.shape == sample_shape + batch_shape + + +@pytest.mark.parametrize("batch_shape", BATCH_SHAPES) +@pytest.mark.parametrize("sample_shape", [tuple(), (2,), (3,)]) +@pytest.mark.parametrize("context_length", [3]) +def test_log_prob_lagged( + batch_shape: torch.Size, + sample_shape: torch.Size, + context_length: int, +): + """Test basic properties of the log_prob of the lagged distributions""" + num_nodes = 4 + + dist = _distribution_factory( + dist_class=RhinoLaggedAdjacencyDistribution, + num_nodes=num_nodes, + batch_shape=batch_shape, + context_length=context_length, + ) + shapes = sample_shape + batch_shape + (context_length - 1, num_nodes, num_nodes) + + values = torch.randint(0, 2, shapes, dtype=torch.float64) + log_probs = dist.log_prob(values) + assert log_probs.shape == sample_shape + batch_shape + + +@pytest.mark.parametrize("batch_shape", BATCH_SHAPES) +@pytest.mark.parametrize("sample_shape", [tuple(), (2,), (3,)]) +@pytest.mark.parametrize("dist_class", TEMPORAL_DIST_CLASSES) +@pytest.mark.parametrize("context_length", [1, 3]) +def test_log_prob_temporal( + dist_class: Type[TemporalAdjacencyDistribution], + batch_shape: torch.Size, + sample_shape: torch.Size, + context_length: int, +): + """Test basic properties of the log_prob of the temporal adjacency distributions""" + num_nodes = 4 + + dist = _distribution_factory( + dist_class=dist_class, num_nodes=num_nodes, batch_shape=batch_shape, context_length=context_length + ) + shapes = sample_shape + batch_shape + (context_length, num_nodes, num_nodes) + + values = torch.randint(0, 2, shapes, dtype=torch.float64) log_probs = dist.log_prob(values) assert log_probs.shape == sample_shape + batch_shape diff --git a/test/distributions/adjacency/test_constrained_adjacency_distributions.py b/test/distributions/adjacency/test_constrained_adjacency_distributions.py index 1058e7e..54c67d0 100644 --- a/test/distributions/adjacency/test_constrained_adjacency_distributions.py +++ b/test/distributions/adjacency/test_constrained_adjacency_distributions.py @@ -1,24 +1,98 @@ +from typing import Type + import pytest import torch +from causica.distributions.adjacency.adjacency_distributions import AdjacencyDistribution from causica.distributions.adjacency.constrained_adjacency_distributions import constrained_adjacency +from causica.distributions.adjacency.enco import ENCOAdjacencyDistribution +from causica.distributions.adjacency.temporal_adjacency_distributions import ( + RhinoLaggedAdjacencyDistribution, + TemporalAdjacencyDistribution, +) from causica.distributions.adjacency.three_way import ThreeWayAdjacencyDistribution +DIST_CLASSES = [ + ENCOAdjacencyDistribution, + ThreeWayAdjacencyDistribution, +] + +TEMPORAL_DIST_CLASSES = [TemporalAdjacencyDistribution] + +@pytest.mark.parametrize("dist_class", DIST_CLASSES) @pytest.mark.parametrize("num_nodes", [3, 100, 500]) @pytest.mark.parametrize("force_edge_from_diagonal", [1, 2, 3]) @pytest.mark.parametrize("exist_force_edge", [True, False]) -def test_constrained_adjacency(num_nodes: int, force_edge_from_diagonal: int, exist_force_edge: bool): +def test_constrained_adjacency( + dist_class: Type[AdjacencyDistribution], + num_nodes: int, + force_edge_from_diagonal: int, + exist_force_edge: bool, +): """Tests that a constrained distribution can be set up that forces no edges from the given diagonal. Args: + dist_class: The class of the adjacency distribution to test. num_nodes: Number of nodes represented by the adjacency matrix. force_edge_from_diagonal: Specifies which diagonal of the adjacency matrix to force to 1. """ ones = torch.ones((num_nodes, num_nodes), dtype=torch.bool) * exist_force_edge nans = torch.full((num_nodes, num_nodes), fill_value=torch.nan) + if force_edge_from_diagonal == 0: + ones = ones & ~torch.eye(num_nodes, dtype=torch.bool) + + # Set all edges but one diagonal to nan + constraints = ( + ones + + torch.tril(nans, diagonal=force_edge_from_diagonal - 1) + + torch.triu(nans, diagonal=force_edge_from_diagonal + 1) + ) + + mask = ~torch.isnan(constraints) + + constrained_dist_cls = constrained_adjacency(dist_class) + if dist_class is ThreeWayAdjacencyDistribution: + logits = torch.nn.Parameter(torch.zeros(((num_nodes * (num_nodes - 1)) // 2, 3))) + constrained_dist = constrained_dist_cls(logits=logits, graph_constraint_matrix=constraints) + elif dist_class is ENCOAdjacencyDistribution: + logits_exist = torch.nn.Parameter(torch.zeros((num_nodes, num_nodes))) + logits_orient = torch.nn.Parameter(torch.zeros(((num_nodes * (num_nodes - 1)) // 2,))) + constrained_dist = constrained_dist_cls( + logits_exist=logits_exist, logits_orient=logits_orient, graph_constraint_matrix=constraints + ) + else: + raise ValueError("Unrecognised Class") + + assert torch.allclose(constrained_dist.sample()[mask], constraints[mask]) + + +@pytest.mark.parametrize("dist_class", TEMPORAL_DIST_CLASSES) +@pytest.mark.parametrize("num_nodes", [3, 100, 500]) +@pytest.mark.parametrize("force_edge_from_diagonal", [1, 2, 3]) +@pytest.mark.parametrize("exist_force_edge", [True, False]) +@pytest.mark.parametrize("context_length", [1, 3]) +def test_temporal_constrained_adjacency( + dist_class: Type[TemporalAdjacencyDistribution], + num_nodes: int, + force_edge_from_diagonal: int, + exist_force_edge: bool, + context_length: int, +): + """Tests that a temporal constrained distribution can be set up that forces no edges from the given diagonal. + + Args: + dist_class: The class of the temporal adjacency distribution to test. + num_nodes: Number of nodes represented by the adjacency matrix. + force_edge_from_diagonal: Specifies which diagonal of the adjacency matrix to force to 1. + """ + assert context_length >= 1 + ones = torch.ones((context_length, num_nodes, num_nodes), dtype=torch.bool) * exist_force_edge + nans = torch.full((context_length, num_nodes, num_nodes), fill_value=torch.nan) + if force_edge_from_diagonal == 0: + ones[-1, :, :] = ones[-1, :, :] & ~torch.eye(num_nodes, dtype=torch.bool) # Set all edges but one diagonal to nan constraints = ( ones @@ -28,8 +102,18 @@ def test_constrained_adjacency(num_nodes: int, force_edge_from_diagonal: int, ex mask = ~torch.isnan(constraints) - constrained_dist_cls = constrained_adjacency(ThreeWayAdjacencyDistribution) - logits = torch.nn.Parameter(torch.zeros(((num_nodes * (num_nodes - 1)) // 2, 3))) - constrained_dist = constrained_dist_cls(logits=logits, graph_constraint_matrix=constraints) + constrained_dist_cls = constrained_adjacency(dist_class) + + logits_exist = torch.nn.Parameter(torch.zeros((context_length, num_nodes, num_nodes))) + logits_orient = torch.nn.Parameter(torch.zeros(((num_nodes * (num_nodes - 1)) // 2,))) + lagged_dist = ( + RhinoLaggedAdjacencyDistribution(logits_edge=logits_exist[..., :-1, :, :], lags=context_length - 1) + if context_length > 1 + else None + ) + inst_dist = ENCOAdjacencyDistribution(logits_exist=logits_exist[..., -1, :, :], logits_orient=logits_orient) + constrained_dist = constrained_dist_cls( + instantaneous_distribution=inst_dist, lagged_distribution=lagged_dist, graph_constraint_matrix=constraints + ) assert torch.allclose(constrained_dist.sample()[mask], constraints[mask]) diff --git a/test/distributions/adjacency/test_edges_per_node_erdos_renyi.py b/test/distributions/adjacency/test_edges_per_node_erdos_renyi.py new file mode 100644 index 0000000..dd6bdbd --- /dev/null +++ b/test/distributions/adjacency/test_edges_per_node_erdos_renyi.py @@ -0,0 +1,31 @@ +import networkx as nx +import torch + +from causica.distributions import EdgesPerNodeErdosRenyiDAGDistribution + + +def test_samples_dags(): + """Test that all samples are DAGs""" + edges_per_node = [1, 2] + n = 5 + sample_shape = torch.Size([3, 4]) + dist = EdgesPerNodeErdosRenyiDAGDistribution(num_nodes=n, edges_per_node=edges_per_node) + samples = dist.sample(sample_shape).numpy() + assert samples.shape == torch.Size(sample_shape + (n, n)) + flat_samples = samples.reshape((-1, n, n)) + for dag in flat_samples: + assert nx.is_directed_acyclic_graph(nx.from_numpy_array(dag, create_using=nx.DiGraph)) + + +def test_num_edges(): + n = 8 + edges_per_node = [2] + num_edges_expected = edges_per_node[0] * n + samples = EdgesPerNodeErdosRenyiDAGDistribution(num_nodes=n, edges_per_node=edges_per_node).sample( + torch.Size([100]) + ) + + assert samples.shape == torch.Size([100, 8, 8]) + torch.testing.assert_close( + samples.sum(dim=(-2, -1)).mean(), torch.tensor(num_edges_expected, dtype=torch.float32), atol=2.0, rtol=0.1 + ) diff --git a/test/distributions/adjacency/test_enco.py b/test/distributions/adjacency/test_enco.py index e825540..df2cb1b 100644 --- a/test/distributions/adjacency/test_enco.py +++ b/test/distributions/adjacency/test_enco.py @@ -1,22 +1,64 @@ """Module with ENCO specific tests.""" + import numpy as np +import pytest import torch from torch.distributions.utils import probs_to_logits from causica.distributions.adjacency.enco import ENCOAdjacencyDistribution +from causica.distributions.adjacency.temporal_adjacency_distributions import ( + RhinoLaggedAdjacencyDistribution, + TemporalAdjacencyDistribution, +) def test_enco_entropy(): """Test ENCO entropy is correct for a known distribution.""" eps = 1e-7 + probs_exist = torch.tensor([[0.0, 1.0 - eps], [1.0 - eps, 0.0]]) probs_orient = torch.tensor([0.5]) logits_exist = torch.nn.Parameter(probs_to_logits(probs_exist, is_binary=True), requires_grad=True) logits_orient = torch.nn.Parameter(probs_to_logits(probs_orient, is_binary=True), requires_grad=True) dist = ENCOAdjacencyDistribution(logits_exist=logits_exist, logits_orient=logits_orient) + target_entropy = 2 * np.log(2) entropy = dist.entropy() - np.testing.assert_allclose(entropy.detach(), 2 * np.log(2)) + + np.testing.assert_allclose(entropy.detach(), target_entropy) + entropy.backward() + assert logits_exist.grad is not None + assert logits_orient.grad is not None + np.testing.assert_allclose(logits_exist.grad, np.zeros_like(logits_exist.detach().numpy()), atol=1e-7) + np.testing.assert_allclose(logits_orient.grad, np.zeros_like(logits_orient.detach().numpy()), atol=1e-7) + optim = torch.optim.SGD((logits_orient, logits_exist), lr=1e-2, momentum=0.9) + optim.step() + + +@pytest.mark.parametrize("context_length", [1, 3]) +def test_temporal_enco_entropy(context_length: int): + """Test temporal ENCO entropy is correct for a known distribution.""" + eps = 1e-7 + probs_exist = torch.cat( + [0.5 * (torch.ones(context_length - 1, 2, 2) - eps), torch.tensor([[[0.0, 1.0 - eps], [1.0 - eps, 0.0]]])], + dim=0, + ) + probs_orient = torch.tensor([0.5]) + logits_exist = torch.nn.Parameter(probs_to_logits(probs_exist, is_binary=True), requires_grad=True) + logits_orient = torch.nn.Parameter(probs_to_logits(probs_orient, is_binary=True), requires_grad=True) + inst_dist = ENCOAdjacencyDistribution(logits_exist=logits_exist[..., -1, :, :], logits_orient=logits_orient) + lagged_dist = ( + RhinoLaggedAdjacencyDistribution(logits_edge=logits_exist[..., :-1, :, :], lags=context_length - 1) + if context_length > 1 + else None + ) + dist_temporal = TemporalAdjacencyDistribution(inst_dist, lagged_dist) + target_entropy = (context_length - 1) * 4 * np.log(2) + 2 * np.log(2) + entropy = dist_temporal.entropy() + + np.testing.assert_allclose(entropy.detach(), target_entropy) entropy.backward() + assert logits_exist.grad is not None + assert logits_orient.grad is not None np.testing.assert_allclose(logits_exist.grad, np.zeros_like(logits_exist.detach().numpy()), atol=1e-7) np.testing.assert_allclose(logits_orient.grad, np.zeros_like(logits_orient.detach().numpy()), atol=1e-7) optim = torch.optim.SGD((logits_orient, logits_exist), lr=1e-2, momentum=0.9) @@ -25,6 +67,7 @@ def test_enco_entropy(): def test_enco_backprop(): """Test ENCO backpropagation works.""" + logits_exist = torch.nn.Parameter(torch.zeros((2, 2)), requires_grad=True) logits_orient = torch.nn.Parameter(torch.tensor([1.0]), requires_grad=True) logits_exist_np = np.array(logits_exist.detach().numpy()) @@ -42,7 +85,7 @@ def test_enco_backprop(): # entropy should increase assert (loss.detach().numpy() < loss_init).all() logits_exist_after = logits_exist.detach().numpy() - # diagonal elements should remain the same + # diagonal elements (or diagonal in instantaneous adj for temporal adj) should remain the same np.testing.assert_allclose(np.diagonal(logits_exist_after), np.diagonal(logits_exist_np)) # other edges should be more like to exist (their existence logits increase) assert logits_exist_after[1, 0] > logits_exist_np[1, 0] @@ -52,12 +95,73 @@ def test_enco_backprop(): optim.zero_grad() # test we can backpropagate through relaxed_sample - sample = dist.relaxed_sample(sample_shape=(100,), temperature=0.1) + sample = dist.relaxed_sample(sample_shape=torch.Size((100,)), temperature=0.1) loss = torch.sum((torch.eye(2)[None, ...] - sample) ** 2) loss.backward() # their should be gradients + assert logits_exist.grad is not None + assert logits_orient.grad is not None + assert abs(logits_exist.grad[0, 1]) > 1e-3 - assert abs(logits_orient.grad[0]) > 1e-3 + assert abs(logits_orient.grad[-1]) > 1e-3 + + +@pytest.mark.parametrize("context_length", [1, 3]) +def test_temporal_enco_backprop(context_length: int): + """Test temporal ENCO backpropagation works.""" + + eps = 1e-1 + logits_exist_prob = torch.cat( + [eps * torch.ones(context_length - 1, 2, 2), torch.tensor([[[0.0, 0.4], [0, 0.4]]])], dim=0 + ) + logits_exist = torch.nn.Parameter(probs_to_logits(logits_exist_prob), requires_grad=True) + logits_orient = torch.nn.Parameter(torch.tensor([1.0]), requires_grad=True) + logits_exist_np = np.array(logits_exist.detach().numpy()) + logits_orient_np = np.array(logits_orient.detach().numpy()) + inst_dist = ENCOAdjacencyDistribution(logits_exist=logits_exist[..., -1, :, :], logits_orient=logits_orient) + lagged_dist = ( + RhinoLaggedAdjacencyDistribution(logits_edge=logits_exist[..., :-1, :, :], lags=context_length - 1) + if context_length > 1 + else None + ) + optim = torch.optim.SGD((logits_orient, logits_exist), lr=1e-2, momentum=0.9) + dist = TemporalAdjacencyDistribution(inst_dist, lagged_dist) + + # maximise the entropy + loss = -dist.entropy() + loss_init = loss.detach().numpy() + loss.backward() + optim.step() + loss = -dist.entropy() + + # entropy should increase + assert (loss.detach().numpy() < loss_init).all() + logits_exist_after = logits_exist.detach().numpy() + # diagonal elements (or diagonal in instantaneous adj for temporal adj) should remain the same + np.testing.assert_allclose( + np.diagonal(logits_exist_after[..., -1, :, :], axis1=-1, axis2=-2), + np.diagonal(logits_exist_np[..., -1, :, :], axis1=-1, axis2=-2), + ) + # other edges should be more like to exist (their existence logits increase) + assert np.all(logits_exist_after[:-1, 1, 0] > logits_exist_np[:-1, 1, 0]) + assert np.all(logits_exist_after[:-1, 0, 1] > logits_exist_np[:-1, 0, 1]) + + # the orientation logit should be heading towards 0 so it should decrease from 1 + assert (logits_orient_np > logits_orient.detach().numpy()).all() + optim.zero_grad() + + # test we can backpropagate through relaxed_sample + sample = dist.relaxed_sample(sample_shape=torch.Size((100,)), temperature=0.1) + loss = torch.sum((torch.eye(2)[None, ...] - sample) ** 2) + loss.backward() + # their should be gradients + assert logits_exist.grad is not None + assert logits_orient.grad is not None + + grad_matrix = abs(logits_exist.grad[:-1, 0, 1]) if context_length > 1 else abs(logits_exist.grad[-1, 0, 1]) + assert torch.all(grad_matrix > 1e-3) + + assert abs(logits_orient.grad[-1]) > 1e-3 def test_enco(): @@ -73,3 +177,47 @@ def test_enco(): np.testing.assert_allclose( dist.log_prob(samples), np.array([np.log(0.09 * 0.44), np.log(0.91 * 0.56), np.log(0.91 * 0.44)]), atol=1e-7 ) + + +def test_temporal_enco(): + """Test Temporal ENCO method are correct for a known distribution""" + context_length = 2 + probs_exist = torch.stack([torch.tensor([[0.3, 0.7], [0.2, 0.6]]), torch.tensor([[0.9, 0.3], [0.8, 0.7]])], dim=0) + probs_orient = torch.tensor([0.7]) + logits_exist = probs_to_logits(probs_exist, is_binary=True) + logits_orient = probs_to_logits(probs_orient, is_binary=True) + inst_dist = ENCOAdjacencyDistribution(logits_exist=logits_exist[..., -1, :, :], logits_orient=logits_orient) + lagged_dist = ( + RhinoLaggedAdjacencyDistribution(logits_edge=logits_exist[..., :-1, :, :], lags=context_length - 1) + if context_length > 1 + else None + ) + dist = TemporalAdjacencyDistribution(instantaneous_distribution=inst_dist, lagged_distribution=lagged_dist) + np.testing.assert_allclose(torch.tensor([[[0.3, 0.7], [0.2, 0.6]], [[0.0, 0.09], [0.56, 0.0]]]), dist.mean) + np.testing.assert_allclose( + torch.tensor( + [ + [[0.0, 1.0], [0.0, 1.0]], + [[0.0, 0.0], [1.0, 0.0]], + ] + ), + dist.mode, + ) + + samples = torch.tensor( + [ + [[[0.0, 0.0], [1.0, 0.0]], [[0.0, 1.0], [0.0, 0.0]]], + [[[0.0, 1.0], [1.0, 0.0]], [[0.0, 0.0], [1.0, 0.0]]], + [[[1.0, 0.0], [1.0, 0.0]], [[0.0, 0.0], [0.0, 0.0]]], + ] + ) + np.testing.assert_allclose( + dist.log_prob(samples), + np.array( + [ + np.log(0.09 * 0.44 * 0.7 * 0.3 * 0.2 * 0.4), + np.log(0.91 * 0.56 * 0.7 * 0.7 * 0.2 * 0.4), + np.log(0.91 * 0.44 * 0.3 * 0.3 * 0.2 * 0.4), + ] + ), + ) diff --git a/test/distributions/adjacency/test_directed_acyclic.py b/test/distributions/adjacency/test_erdos_renyi.py similarity index 100% rename from test/distributions/adjacency/test_directed_acyclic.py rename to test/distributions/adjacency/test_erdos_renyi.py diff --git a/test/distributions/adjacency/test_geometric_random_graph.py b/test/distributions/adjacency/test_geometric_random_graph.py new file mode 100644 index 0000000..490dd26 --- /dev/null +++ b/test/distributions/adjacency/test_geometric_random_graph.py @@ -0,0 +1,17 @@ +import networkx as nx +import torch + +from causica.distributions import GeometricRandomGraphDAGDistribution + + +def test_samples_dags(): + """Test that all samples are DAGs""" + radius = [0.1, 0.5] + n = 5 + sample_shape = torch.Size([3, 4]) + dist = GeometricRandomGraphDAGDistribution(num_nodes=n, radius=radius) + samples = dist.sample(sample_shape).numpy() + assert samples.shape == torch.Size(sample_shape + (n, n)) + flat_samples = samples.reshape((-1, n, n)) + for dag in flat_samples: + assert nx.is_directed_acyclic_graph(nx.from_numpy_array(dag, create_using=nx.DiGraph)) diff --git a/test/distributions/adjacency/test_gibbs_dag_prior.py b/test/distributions/adjacency/test_gibbs_dag_prior.py index 85ee3e7..2608e93 100644 --- a/test/distributions/adjacency/test_gibbs_dag_prior.py +++ b/test/distributions/adjacency/test_gibbs_dag_prior.py @@ -36,6 +36,18 @@ def test_get_sparsity_term(): assert gibbs_dag_prior.get_sparsity_term(dense_dag) > gibbs_dag_prior.get_sparsity_term(sparse_dag) +def test_get_sparsity_term_temporal(): + gibbs_dag_prior = GibbsDAGPrior(num_nodes=2, sparsity_lambda=torch.tensor(1), context_length=2) + + empty_dag = torch.zeros(2, 3, 3) + + assert gibbs_dag_prior.get_sparsity_term(empty_dag) == 0 + + dense_dag = torch.ones(2, 3, 3) + + assert gibbs_dag_prior.get_sparsity_term(dense_dag) > gibbs_dag_prior.get_sparsity_term(empty_dag) + + def test_get_expert_graph_term(): mask = torch.Tensor([[0, 0, 0], [0, 0, 0], [0, 0, 0]]) @@ -67,6 +79,40 @@ def test_get_expert_graph_term(): torch.testing.assert_close(gibbs_dag_prior.get_expert_graph_term(A), torch.tensor(0.2)) +def test_get_expert_graph_term_temporal(): + + mask = torch.zeros(2, 3, 3) + dag = torch.stack([torch.Tensor([[0, 0, 1], [0, 0, 0], [0, 0, 0]]), torch.ones(3, 3)]) + confidence = 0.8 + scale = 10 + + expert_graph_container = ExpertGraphContainer(dag, mask, confidence, scale) + + gibbs_dag_prior = GibbsDAGPrior( + num_nodes=3, + sparsity_lambda=torch.tensor(1), + expert_graph_container=expert_graph_container, + context_length=2, + ) + + A = torch.Tensor([[[0, 0, 1], [0, 1, 1], [1, 0, 1]], [[0, 0, 1], [0, 1, 1], [1, 0, 1]]]) + + assert gibbs_dag_prior.get_expert_graph_term(A) == torch.tensor(0) + + mask[0, 0, 2] = 1 + mask[1, 0, 2] = 1 + + expert_graph_container = ExpertGraphContainer(dag, mask, confidence, scale) + gibbs_dag_prior = GibbsDAGPrior( + num_nodes=3, + sparsity_lambda=torch.tensor(1), + expert_graph_container=expert_graph_container, + context_length=2, + ) + + torch.testing.assert_close(gibbs_dag_prior.get_expert_graph_term(A), torch.tensor(0.4)) + + def test_log_prob(): gibbs_dag_prior = GibbsDAGPrior(num_nodes=123, sparsity_lambda=torch.tensor(1)) @@ -87,3 +133,25 @@ def test_log_prob(): A = torch.Tensor([[0, 1], [0, 0]]) torch.testing.assert_close(gibbs_dag_prior.log_prob(A), torch.tensor(-1.0)) + + +def test_log_prob_temporal(): + gibbs_dag_prior = GibbsDAGPrior(num_nodes=3, sparsity_lambda=torch.tensor(1), context_length=3) + + A = torch.Tensor([[0, 0, 1], [0, 0, 1], [0, 0, 0]]) + + with pytest.raises(AssertionError): + gibbs_dag_prior.log_prob(A) + + gibbs_dag_prior = GibbsDAGPrior(num_nodes=2, sparsity_lambda=torch.tensor(1), context_length=2) + + A = torch.Tensor([[[1, 1], [0, 1]], [[1, 1], [0, 1]]]) + + torch.testing.assert_close( + gibbs_dag_prior.log_prob(A), + torch.tensor(-6.0), + ) + + A = torch.Tensor([[[0, 1], [0, 0]], [[0, 1], [0, 0]]]) + + torch.testing.assert_close(gibbs_dag_prior.log_prob(A), torch.tensor(-2.0)) diff --git a/test/distributions/adjacency/test_scale_free.py b/test/distributions/adjacency/test_scale_free.py new file mode 100644 index 0000000..0bb0899 --- /dev/null +++ b/test/distributions/adjacency/test_scale_free.py @@ -0,0 +1,46 @@ +import networkx as nx +import torch + +from causica.distributions import ScaleFreeDAGDistribution + + +def test_samples_dags(): + """Test that all samples are DAGs""" + edges_per_node = [1, 2] + power = [1.0, 3.0] + n = 5 + sample_shape = torch.Size([3, 4]) + dist = ScaleFreeDAGDistribution(num_nodes=n, edges_per_node=edges_per_node, power=power) + samples = dist.sample(sample_shape).numpy() + assert samples.shape == torch.Size(sample_shape + (n, n)) + flat_samples = samples.reshape((-1, n, n)) + for dag in flat_samples: + assert nx.is_directed_acyclic_graph(nx.from_numpy_array(dag, create_using=nx.DiGraph)) + + +def test_num_edges(): + n = 8 + edges_per_node = [2] + power = [1.0] + num_edges_expected = edges_per_node[0] * n + samples = ScaleFreeDAGDistribution(num_nodes=n, edges_per_node=edges_per_node, power=power).sample( + torch.Size([100]) + ) + + assert samples.shape == torch.Size([100, 8, 8]) + torch.testing.assert_close( + samples.sum(dim=(-2, -1)).mean(), torch.tensor(num_edges_expected, dtype=torch.float32), atol=2.0, rtol=0.1 + ) + + +def test_out_degree(): + n = 8 + edges_per_node = [2] + power = [1.0] + in_degree = False + dag = ScaleFreeDAGDistribution( + num_nodes=n, edges_per_node=edges_per_node, power=power, in_degree=in_degree + ).sample() + assert dag.shape == torch.Size([8, 8]) + dag = dag.numpy() + assert nx.is_directed_acyclic_graph(nx.from_numpy_array(dag, create_using=nx.DiGraph)) diff --git a/test/distributions/adjacency/test_stochastic_block_model.py b/test/distributions/adjacency/test_stochastic_block_model.py new file mode 100644 index 0000000..a7de486 --- /dev/null +++ b/test/distributions/adjacency/test_stochastic_block_model.py @@ -0,0 +1,21 @@ +import networkx as nx +import torch + +from causica.distributions import StochasticBlockModelDAGDistribution + + +def test_samples_dags(): + """Test that all samples are DAGs""" + edges_per_node = [1, 2] + n = 5 + num_blocks = [2, 3] + damping = [0.1, 0.2] + sample_shape = torch.Size([3, 4]) + dist = StochasticBlockModelDAGDistribution( + num_nodes=n, edges_per_node=edges_per_node, num_blocks=num_blocks, damping=damping + ) + samples = dist.sample(sample_shape).numpy() + assert samples.shape == torch.Size(sample_shape + (n, n)) + flat_samples = samples.reshape((-1, n, n)) + for dag in flat_samples: + assert nx.is_directed_acyclic_graph(nx.from_numpy_array(dag, create_using=nx.DiGraph)) diff --git a/test/distributions/adjacency/test_watts_strogatz.py b/test/distributions/adjacency/test_watts_strogatz.py new file mode 100644 index 0000000..ef026eb --- /dev/null +++ b/test/distributions/adjacency/test_watts_strogatz.py @@ -0,0 +1,21 @@ +import networkx as nx +import torch + +from causica.distributions import WattsStrogatzDAGDistribution + + +def test_samples_dags(): + """Test that all samples are DAGs""" + n = 5 + lattice_dim = [2, 3] + rewire_prob = [0.1, 0.3] + neighbors = [1, 3] + sample_shape = torch.Size([3, 4]) + dist = WattsStrogatzDAGDistribution( + num_nodes=n, lattice_dim=lattice_dim, rewire_prob=rewire_prob, neighbors=neighbors + ) + samples = dist.sample(sample_shape).numpy() + assert samples.shape == torch.Size(sample_shape + (n, n)) + flat_samples = samples.reshape((-1, n, n)) + for dag in flat_samples: + assert nx.is_directed_acyclic_graph(nx.from_numpy_array(dag, create_using=nx.DiGraph)) diff --git a/test/distributions/noise/test_univariate_cauchy.py b/test/distributions/noise/test_univariate_cauchy.py index 3f9b468..c6519a5 100644 --- a/test/distributions/noise/test_univariate_cauchy.py +++ b/test/distributions/noise/test_univariate_cauchy.py @@ -1,7 +1,7 @@ import pytest import torch -from causica.distributions.noise import UnivariateCauchyNoise +from causica.distributions.noise import UnivariateCauchyNoise, UnivariateCauchyNoiseModule @pytest.mark.parametrize(("batch", "dimension"), [(1, 10), (2, 5)]) @@ -50,3 +50,21 @@ def test_noise_to_sample(): pred = torch.Tensor([[5, 12], [18, 19]]) noise_model = UnivariateCauchyNoise(loc=pred, scale=torch.ones_like(pred)) assert torch.allclose(noise_model.noise_to_sample(noise), torch.Tensor([[10, 17], [20, 25]])) + + +def test_forward_module(): + dim = 2 + init_log_scale = 1.0 + noise_module = UnivariateCauchyNoiseModule(dim, init_log_scale) + + # provide only loc + loc = torch.randn((dim,)) + noise_model = noise_module(loc) + + samples = torch.randn((dim,)) + assert torch.allclose(noise_model.sample_to_noise(samples), samples - loc) + + # provide loc and log_scale + log_scale = torch.randn((dim,)) + noise_model = noise_module((loc, log_scale)) + assert torch.allclose(noise_model.sample_to_noise(samples), (samples - loc) / log_scale.exp()) diff --git a/test/distributions/noise/test_univariate_laplace.py b/test/distributions/noise/test_univariate_laplace.py index 390edbb..6d34df7 100644 --- a/test/distributions/noise/test_univariate_laplace.py +++ b/test/distributions/noise/test_univariate_laplace.py @@ -1,7 +1,7 @@ import pytest import torch -from causica.distributions.noise import UnivariateLaplaceNoise +from causica.distributions.noise import UnivariateLaplaceNoise, UnivariateLaplaceNoiseModule @pytest.mark.parametrize(("batch", "dimension"), [(1, 10), (2, 5)]) @@ -50,3 +50,21 @@ def test_noise_to_sample(): pred = torch.Tensor([[5, 12], [18, 19]]) noise_model = UnivariateLaplaceNoise(loc=pred, scale=torch.ones_like(pred)) assert torch.allclose(noise_model.noise_to_sample(noise), torch.Tensor([[10, 17], [20, 25]])) + + +def test_forward_module(): + dim = 2 + init_log_scale = 1.0 + noise_module = UnivariateLaplaceNoiseModule(dim, init_log_scale) + + # provide only loc + loc = torch.randn((dim,)) + noise_model = noise_module(loc) + + samples = torch.randn((dim,)) + assert torch.allclose(noise_model.sample_to_noise(samples), samples - loc) + + # provide loc and log_scale + log_scale = torch.randn((dim,)) + noise_model = noise_module((loc, log_scale)) + assert torch.allclose(noise_model.sample_to_noise(samples), (samples - loc) / log_scale.exp()) diff --git a/test/distributions/noise/test_univariate_normal.py b/test/distributions/noise/test_univariate_normal.py index 69f6a68..333a423 100644 --- a/test/distributions/noise/test_univariate_normal.py +++ b/test/distributions/noise/test_univariate_normal.py @@ -1,7 +1,7 @@ import pytest import torch -from causica.distributions.noise import UnivariateNormalNoise +from causica.distributions.noise import UnivariateNormalNoise, UnivariateNormalNoiseModule @pytest.mark.parametrize(("batch", "dimension"), [(1, 10), (2, 5)]) @@ -51,3 +51,21 @@ def test_noise_to_sample(): pred = torch.Tensor([[5, 12], [18, 19]]) noise_model = UnivariateNormalNoise(loc=pred, scale=torch.ones_like(pred)) assert torch.allclose(noise_model.noise_to_sample(noise), torch.Tensor([[10, 17], [20, 25]])) + + +def test_forward_module(): + dim = 2 + init_log_scale = 1.0 + noise_module = UnivariateNormalNoiseModule(dim, init_log_scale) + + # provide only loc + loc = torch.randn((dim,)) + noise_model = noise_module(loc) + + samples = torch.randn((dim,)) + assert torch.allclose(noise_model.sample_to_noise(samples), samples - loc) + + # provide loc and log_scale + log_scale = torch.randn((dim,)) + noise_model = noise_module((loc, log_scale)) + assert torch.allclose(noise_model.sample_to_noise(samples), (samples - loc) / log_scale.exp()) diff --git a/test/distributions/test_sem_distribution.py b/test/distributions/test_sem_distribution.py index cd2b09d..38e87b6 100644 --- a/test/distributions/test_sem_distribution.py +++ b/test/distributions/test_sem_distribution.py @@ -8,6 +8,7 @@ from causica.distributions.noise.univariate_normal import UnivariateNormalNoiseModule from causica.functional_relationships import DECIEmbedFunctionalRelationships from causica.functional_relationships.functional_relationships import FunctionalRelationships +from causica.sem.distribution_parameters_sem import DistributionParametersSEM from causica.sem.sem_distribution import SEMDistribution diff --git a/test/integration/test_timeseries_dataset.py b/test/integration/test_timeseries_dataset.py new file mode 100644 index 0000000..2c74e5e --- /dev/null +++ b/test/integration/test_timeseries_dataset.py @@ -0,0 +1,20 @@ +import os + +import torch +from torch.utils.data import DataLoader + +from causica.datasets.causica_dataset_format import CAUSICA_DATASETS_PATH +from causica.datasets.timeseries_dataset import IndexedTimeseriesDataset + + +def test_ecoli_100(): + dataset_root = os.path.join(CAUSICA_DATASETS_PATH, "Ecoli1_100") + data_path = os.path.join(dataset_root, "train.csv") + adj_path = os.path.join(dataset_root, "adj_matrix.npy") + + dataset = IndexedTimeseriesDataset(0, data_path, adj_path) + dataloader = DataLoader(dataset, batch_size=46, collate_fn=torch.stack) + (full_batch,) = list(dataloader) + assert full_batch.batch_size == (46, 21) # The dataset is known to contain 46 timeseries with 21 steps each + assert len(full_batch.keys()) == 100 # And 100 features + assert all(value.shape == (46, 21, 1) for value in full_batch.values()) # Each with 1 dimension per sample and step diff --git a/test/lightning/test_variable_spec_data.py b/test/lightning/test_variable_spec_data.py index 74f5b11..4a7f517 100644 --- a/test/lightning/test_variable_spec_data.py +++ b/test/lightning/test_variable_spec_data.py @@ -64,6 +64,27 @@ } ) +DF2 = pd.DataFrame( + { + "x0_0": { + 0: -10.5569139122962952, + 1: 0.5148046016693115, + }, + "x0_1": { + 0: 0.06510090827941895, + 1: 1.4334404468536377, + }, + "x1_0": { + 0: 0.008327394723892214, + 1: -0.3806004524230957, + }, + "x1_1": { + 0: 0.33135163784027094, + 1: 0.03788989782333375, + }, + } +) + VARIABLES_METADATA = VariablesMetadata( variables=[ @@ -108,9 +129,7 @@ } -@pytest.mark.parametrize("normalize", [True, False]) -def test_variable_spec_data(tmp_path, normalize): - """Test Variable Spec Data Module functionality""" +def _save_tmp_data(tmp_path): variable_spec_path = tmp_path / "variables.json" with variable_spec_path.open("w") as f: json.dump(VARIABLES_METADATA.to_dict(encode_json=True), f) @@ -123,6 +142,10 @@ def test_variable_spec_data(tmp_path, normalize): with test_path.open("w") as f: DF.to_csv(f, index=False, header=False) + valid_path = tmp_path / "val.csv" + with valid_path.open("w") as f: + DF2.to_csv(f, index=False, header=False) + adjacency_path = tmp_path / "adj_matrix.csv" with adjacency_path.open("w") as f: np.savetxt(f, ADJACENCY.tolist(), delimiter=",") @@ -131,12 +154,26 @@ def test_variable_spec_data(tmp_path, normalize): with intervention_path.open("w") as f: json.dump(INTERVENTIONS, f) + +@pytest.mark.parametrize("normalize", [True, False]) +def test_variable_spec_data(tmp_path, normalize): + """Test Variable Spec Data Module functionality""" + _save_tmp_data(tmp_path) + if normalize: data_module = VariableSpecDataModule( - tmp_path, batch_size=2, standardize=True, exclude_standardization=["x1"], load_interventional=True + tmp_path, + batch_size=2, + standardize=True, + exclude_standardization=["x1"], + load_interventional=True, ) else: - data_module = VariableSpecDataModule(tmp_path, batch_size=2, load_interventional=True) + data_module = VariableSpecDataModule( + tmp_path, + batch_size=2, + load_interventional=True, + ) data_module.prepare_data() @@ -161,28 +198,25 @@ def test_variable_spec_data(tmp_path, normalize): torch.testing.assert_close(intervention_b.intervention_data, data_module.normalizer(samples_td)) -@pytest.mark.parametrize("log_normalize", [True, False]) -def test_save_load_variable_spec_data(tmp_path, log_normalize): - """Test saving and loading Variable Spec Data Module from checkpoint""" - variable_spec_path = tmp_path / "variables.json" - with variable_spec_path.open("w") as f: - json.dump(VARIABLES_METADATA.to_dict(encode_json=True), f) +def test_log_normalizer_error(tmp_path): + """Test error when log normalizer is applied to negative values""" + _save_tmp_data(tmp_path) - train_path = tmp_path / "train.csv" - with train_path.open("w") as f: - DF.to_csv(f, index=False, header=False) - - test_path = tmp_path / "test.csv" - with test_path.open("w") as f: - DF.to_csv(f, index=False, header=False) + with pytest.raises(ValueError, match="NaN values found in the validation data after normalization."): + data_module = VariableSpecDataModule( + tmp_path, + batch_size=2, + standardize=False, + log_normalize=True, + load_validation=True, + ) + data_module.prepare_data() - adjacency_path = tmp_path / "adj_matrix.csv" - with adjacency_path.open("w") as f: - np.savetxt(f, ADJACENCY.tolist(), delimiter=",") - intervention_path = tmp_path / "interventions.json" - with intervention_path.open("w") as f: - json.dump(INTERVENTIONS, f) +@pytest.mark.parametrize("log_normalize", [True, False]) +def test_save_load_variable_spec_data(tmp_path, log_normalize): + """Test saving and loading Variable Spec Data Module from checkpoint""" + _save_tmp_data(tmp_path) if log_normalize: data_module = VariableSpecDataModule( diff --git a/test/nn/test_temporal_embed_nn.py b/test/nn/test_temporal_embed_nn.py new file mode 100644 index 0000000..08c0610 --- /dev/null +++ b/test/nn/test_temporal_embed_nn.py @@ -0,0 +1,40 @@ +import pytest +import torch + +from causica.nn import TemporalEmbedNN + +CONTEXT_LENGTH = 7 +PROCESSED_DIM = 6 +NODE_NUM = 4 +GROUP_MASK = torch.tensor( + [ + [1, 0, 0, 0, 0, 0], + [0, 1, 0, 0, 0, 0], + [0, 0, 1, 1, 0, 0], + [0, 0, 0, 0, 1, 1], + ], + dtype=torch.float32, +) +assert GROUP_MASK.shape == (NODE_NUM, PROCESSED_DIM) + + +GRAPH_SHAPES = [tuple(), (5,), (2, 3)] +SAMPLE_SHAPES = [tuple(), (3,), (1, 2)] + + +@pytest.mark.parametrize("graph_shape", GRAPH_SHAPES) +@pytest.mark.parametrize("sample_shape", SAMPLE_SHAPES) +def test_rhino_embed_nn(graph_shape, sample_shape): + graph_tensor = torch.randint(0, 2, (*graph_shape, CONTEXT_LENGTH, NODE_NUM, NODE_NUM), dtype=torch.float32) + sample_tensor = torch.randn((*sample_shape, *graph_shape, CONTEXT_LENGTH, PROCESSED_DIM)) + + embed_nn = TemporalEmbedNN( + group_mask=GROUP_MASK, + embedding_size=32, + out_dim_l=32, + num_layers_l=2, + num_layers_zeta=2, + context_length=CONTEXT_LENGTH, + ) + out = embed_nn(sample_tensor, graph_tensor) + assert out.shape == sample_shape + graph_shape + (PROCESSED_DIM,) diff --git a/test/sem/__init__.py b/test/sem/__init__.py index ea69030..a6c646e 100644 --- a/test/sem/__init__.py +++ b/test/sem/__init__.py @@ -1,7 +1,11 @@ import torch from causica.distributions import JointNoiseModule, Noise, NoiseModule, UnivariateNormalNoiseModule -from causica.functional_relationships import LinearFunctionalRelationships, RFFFunctionalRelationships +from causica.functional_relationships import ( + HeteroscedasticRFFFunctionalRelationships, + LinearFunctionalRelationships, + RFFFunctionalRelationships, +) from causica.sem.distribution_parameters_sem import DistributionParametersSEM @@ -34,3 +38,20 @@ def create_rffgauss_sem( noise_dist = JointNoiseModule(independent_noise_modules) func = RFFFunctionalRelationships(shapes, random_features, coeff_matrix) return DistributionParametersSEM(graph=graph, noise_dist=noise_dist, func=func) + + +def create_heteroscedastic_rffgauss_sem( + shapes: dict[str, torch.Size], + random_features: torch.Tensor, + coeff_matrix: torch.Tensor, + graph: torch.Tensor, + log_scale: float = 0.0, +) -> DistributionParametersSEM: + """Creates a dummy SEM with RFF functional relationships and Gaussian noise.""" + independent_noise_modules = { + name: UnivariateNormalNoiseModule(shape[-1], init_log_scale=log_scale) for name, shape in shapes.items() + } + noise_dist = JointNoiseModule(independent_noise_modules) + func = RFFFunctionalRelationships(shapes, random_features, coeff_matrix) + log_func = HeteroscedasticRFFFunctionalRelationships(shapes, random_features, coeff_matrix, log_scale=True) + return DistributionParametersSEM(graph=graph, noise_dist=noise_dist, func=func, log_func_rescale=log_func) diff --git a/test/sem/test_sem.py b/test/sem/test_sem.py index e116415..10aa12c 100644 --- a/test/sem/test_sem.py +++ b/test/sem/test_sem.py @@ -9,7 +9,7 @@ from causica.functional_relationships import LinearFunctionalRelationships from causica.sem.distribution_parameters_sem import DistributionParametersSEM -from . import create_lingauss_sem, create_rffgauss_sem +from . import create_heteroscedastic_rffgauss_sem, create_lingauss_sem, create_rffgauss_sem @pytest.fixture(name="two_variable_dict") @@ -98,6 +98,44 @@ def test_batched_intervention_2d_graph(graph, intervention_variable, interventio torch.testing.assert_close(do_sample[sampled_variable], non_batch_sample[sampled_variable], atol=1e-5, rtol=1e-4) +@pytest.mark.parametrize("graph", [torch.tensor([[0, 0], [1, 0.0]]), torch.tensor([[0, 1], [0, 0.0]])]) +@pytest.mark.parametrize( + "intervention_variable,intervention_value", + [("x1", [[1.42], [0.42], [1.12]]), ("x2", [[1.42, 0.42], [0.42, 1.42], [0.42, 0.42]])], +) +def test_heteroscedastic_batched_intervention_2d_graph( + graph, intervention_variable, intervention_value, two_variable_dict +): + rff_features = torch.rand((10, 3)) + coef_matrix = torch.rand((10,)) + sem = create_heteroscedastic_rffgauss_sem(two_variable_dict, rff_features, coef_matrix, graph) + variable_names = set(two_variable_dict.keys()) + sampled_variables = variable_names - {intervention_variable} + assert len(sampled_variables) == 1 + sampled_variable = sampled_variables.pop() + intervention_value = torch.tensor(intervention_value) + batched_do_sem = sem.do( + TensorDict( + {intervention_variable: intervention_value}, + batch_size=[ + 3, + ], + ) + ) + noise = batched_do_sem.sample_noise((10,)) + do_sample = batched_do_sem.noise_to_sample(noise) + + non_batch_sample_list = [] + for i, intervention in enumerate(intervention_value): + non_batch_sample_list.append( + sem.do(TensorDict({intervention_variable: intervention}, batch_size=tuple())) + .noise_to_sample(noise[:, i, None]) + .squeeze(1) + ) + non_batch_sample = torch.stack(non_batch_sample_list, dim=1) + torch.testing.assert_close(do_sample[sampled_variable], non_batch_sample[sampled_variable], atol=1e-5, rtol=1e-4) + + def test_batched_intervention_3d_graph(three_variable_dict): graph = torch.zeros(3, 3) graph[0, 1] = graph[0, 2] = graph[1, 2] = 1 diff --git a/test/sem/test_temporal_distribution_parameters_sem.py b/test/sem/test_temporal_distribution_parameters_sem.py new file mode 100644 index 0000000..6d2f2b5 --- /dev/null +++ b/test/sem/test_temporal_distribution_parameters_sem.py @@ -0,0 +1,33 @@ +import torch +from tensordict import TensorDict + +from causica.datasets.tensordict_utils import tensordict_shapes +from causica.sem.temporal_distribution_parameters_sem import split_lagged_and_instanteneous_values + + +def test_split_lagged_and_instanteneous_values(): + td = TensorDict( + { + "a": torch.rand(2, 3, 4), + "b": torch.rand(2, 3, 5), + "c": torch.rand(2, 3, 6), + "d": torch.rand(2, 3, 7), + }, + batch_size=2, + ) + + lagged, instantaneous = split_lagged_and_instanteneous_values(td) + + assert lagged.batch_size == instantaneous.batch_size == torch.Size([2]) + assert tensordict_shapes(lagged) == { + "a": torch.Size([2, 4]), + "b": torch.Size([2, 5]), + "c": torch.Size([2, 6]), + "d": torch.Size([2, 7]), + } + assert tensordict_shapes(instantaneous) == { + "a": torch.Size([4]), + "b": torch.Size([5]), + "c": torch.Size([6]), + "d": torch.Size([7]), + } diff --git a/test/test_dag_constraint.py b/test/test_dag_constraint.py index 19895e1..8ffc0d3 100644 --- a/test/test_dag_constraint.py +++ b/test/test_dag_constraint.py @@ -24,3 +24,11 @@ def test_calculate_dagness(): 2 * np.exp(1) - 2, decimal=5, ) + + stacked_dags = torch.stack([dag, dag_one_cycle, dag_two_cycle]) + dagness = calculate_dagness(stacked_dags) + assert dagness.shape == (3,) + assert dagness[0] == 0 + assert dagness[1] > 0 + assert dagness[2] > 0 + assert dagness[1] < dagness[2]