From 3f05e193d1a492956486bddca28cf392de3158b4 Mon Sep 17 00:00:00 2001 From: Mathias Leys Date: Thu, 6 Jun 2024 12:46:40 +0200 Subject: [PATCH 01/28] Restructure models & add prophet boilerplate --- nada_ai/linear_model/__init__.py | 1 + .../{nn/models.py => linear_model/linear_regression.py} | 2 +- nada_ai/nn/__init__.py | 1 - nada_ai/time_series/__init__.py | 1 + nada_ai/time_series/prophet.py | 7 +++++++ pyproject.toml | 1 + 6 files changed, 11 insertions(+), 2 deletions(-) create mode 100644 nada_ai/linear_model/__init__.py rename nada_ai/{nn/models.py => linear_model/linear_regression.py} (96%) create mode 100644 nada_ai/time_series/__init__.py create mode 100644 nada_ai/time_series/prophet.py diff --git a/nada_ai/linear_model/__init__.py b/nada_ai/linear_model/__init__.py new file mode 100644 index 0000000..da620d7 --- /dev/null +++ b/nada_ai/linear_model/__init__.py @@ -0,0 +1 @@ +from .linear_regression import LinearRegression \ No newline at end of file diff --git a/nada_ai/nn/models.py b/nada_ai/linear_model/linear_regression.py similarity index 96% rename from nada_ai/nn/models.py rename to nada_ai/linear_model/linear_regression.py index a74d69f..828e9f7 100644 --- a/nada_ai/nn/models.py +++ b/nada_ai/linear_model/linear_regression.py @@ -1,4 +1,4 @@ -"""Model implementations""" +"""Linear regression implementation""" import nada_algebra as na from nada_ai.nn.module import Module diff --git a/nada_ai/nn/__init__.py b/nada_ai/nn/__init__.py index c7176d8..9924b14 100644 --- a/nada_ai/nn/__init__.py +++ b/nada_ai/nn/__init__.py @@ -2,4 +2,3 @@ from .parameter import Parameter from .layers import * from .activations import * -from .models import * diff --git a/nada_ai/time_series/__init__.py b/nada_ai/time_series/__init__.py new file mode 100644 index 0000000..74a3000 --- /dev/null +++ b/nada_ai/time_series/__init__.py @@ -0,0 +1 @@ +from .prophet import Prophet \ No newline at end of file diff --git a/nada_ai/time_series/prophet.py b/nada_ai/time_series/prophet.py new file mode 100644 index 0000000..98e2277 --- /dev/null +++ b/nada_ai/time_series/prophet.py @@ -0,0 +1,7 @@ +"""Facebook Prophet implementation""" + +from nada_ai.nn.module import Module +from nada_ai.nn.parameter import Parameter + +class Prophet(Module): + ... diff --git a/pyproject.toml b/pyproject.toml index 048d218..9fb5917 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -8,6 +8,7 @@ readme = "README.md" [tool.poetry.dependencies] python = "^3.10" numpy = "^1.26.4" +pandas = "^2.2.2" nada-dsl = "^0.2.1" py-nillion-client = "^0.2.1" nada-algebra = "^0.3.2" From 0217be87e6dd1739d37332a6477b3d4c26739be3 Mon Sep 17 00:00:00 2001 From: Mathias Leys Date: Fri, 7 Jun 2024 12:56:17 +0200 Subject: [PATCH 02/28] Baseline prophet skeleton --- nada_ai/linear_model/__init__.py | 2 +- nada_ai/time_series/__init__.py | 2 +- nada_ai/time_series/helpers.py | 48 +++++ nada_ai/time_series/prophet.py | 202 +++++++++++++++++++++- tests/nada-tests/nada-project.toml | 4 + tests/nada-tests/src/linear_regression.py | 2 +- tests/nada-tests/src/prophet.py | 26 +++ 7 files changed, 282 insertions(+), 4 deletions(-) create mode 100644 nada_ai/time_series/helpers.py create mode 100644 tests/nada-tests/src/prophet.py diff --git a/nada_ai/linear_model/__init__.py b/nada_ai/linear_model/__init__.py index da620d7..5303cce 100644 --- a/nada_ai/linear_model/__init__.py +++ b/nada_ai/linear_model/__init__.py @@ -1 +1 @@ -from .linear_regression import LinearRegression \ No newline at end of file +from .linear_regression import LinearRegression diff --git a/nada_ai/time_series/__init__.py b/nada_ai/time_series/__init__.py index 74a3000..2f2ffd5 100644 --- a/nada_ai/time_series/__init__.py +++ b/nada_ai/time_series/__init__.py @@ -1 +1 @@ -from .prophet import Prophet \ No newline at end of file +from .prophet import Prophet diff --git a/nada_ai/time_series/helpers.py b/nada_ai/time_series/helpers.py new file mode 100644 index 0000000..0a4f13e --- /dev/null +++ b/nada_ai/time_series/helpers.py @@ -0,0 +1,48 @@ +"""Time series helper functions""" + +from collections import OrderedDict +import pandas as pd +import numpy as np +import nada_algebra as na + + +def fourier_series( + dates: pd.Series, + period: int | float, + series_order: int, +) -> np.ndarray: + x_T = dates * np.pi * 2 + fourier_components = np.empty((dates.shape[0], 2 * series_order)) + for i in range(series_order): + c = x_T * (i + 1) / period + fourier_components[:, 2 * i] = np.sin(c) + fourier_components[:, (2 * i) + 1] = np.cos(c) + return fourier_components + + +def decode_component_matrix(component_matrix: na.NadaArray): + additive_terns, multiplicative_terms, yearly, weekly, daily = component_matrix.inner + return { + "additive_terms": additive_terns, + "multiplicative_terms": multiplicative_terms, + "yearly": yearly, + "weekly": weekly, + "daily": daily, + } + + +def decode_seasonality_matrix(seasonality_matrix: na.NadaArray): + period_names = ["yearly", "yearly", "weekly", "weekly", "daily", "daily"] + modes = ["additive", "multiplicative"] * 3 + + result = {} + for row, period_name, mode in zip(seasonality_matrix.inner, period_names, modes): + period, fourier_order, prior_scale = row + result[f"{period_name}_{mode}"] = { + "period": period, + "fourier_order": fourier_order, + "prior_scale": prior_scale, + "mode": mode, + } + + return OrderedDict(result) diff --git a/nada_ai/time_series/prophet.py b/nada_ai/time_series/prophet.py index 98e2277..f8f3dc9 100644 --- a/nada_ai/time_series/prophet.py +++ b/nada_ai/time_series/prophet.py @@ -1,7 +1,207 @@ """Facebook Prophet implementation""" +from typing import override +import pandas as pd +import nada_algebra as na from nada_ai.nn.module import Module from nada_ai.nn.parameter import Parameter +from nada_ai.time_series.helpers import ( + fourier_series, + decode_seasonality_matrix, + decode_component_matrix, +) + class Prophet(Module): - ... + """Prophet forecasting implementation""" + + def __init__( + self, + growth: str = "linear", + n_changepoints: int = 25, # TODO: somehow enforce a fixed number of changepoints in case n_changepoints < len(sequence) + num_seasonality_features: int = 1, # TODO: somehow enforce a fixed number of seasonalities + ) -> None: + if growth not in {"linear"}: + raise NotImplementedError(f"Growth mode `{growth}` is not supported") + + self.growth = growth + + M = 1 # NOTE: MAP estimation is assumed, so M=1 guaranteed + + self.k = Parameter((M, 1)) + self.m = Parameter((M, 1)) + self.delta = Parameter((M, n_changepoints)) + self.beta = Parameter((M, num_seasonality_features)) + self.changepoints_t = Parameter(n_changepoints) + + self.y_scale = Parameter(1) + + self.seasonality_matrix = Parameter((6, 3)) + self.component_matrix = Parameter(5) + + self.seasonalities = decode_seasonality_matrix(self.seasonality_matrix) + self.component_modes = decode_component_matrix(self.component_matrix) + + def predict_seasonal_comps(self, df): + seasonal_features, _, component_cols, _ = self.make_all_seasonality_features(df) + + X = seasonal_features.values + data = {} + for component in component_cols.columns: + component_values = component_cols[component].values + beta_c = self.beta * component_values + + comp = X @ beta_c.T + + comp = ( + comp + * self.component_modes[ + component.replace("_additive", "").replace("_multiplicative", "") + ] + ) + data[component] = comp.mean(axis=1) + + return pd.DataFrame(data) + + def make_all_seasonality_features(self, df): + seasonal_features = [] + prior_scales = [] + modes = {"additive": [], "multiplicative": []} + + for name, props in self.seasonalities.items(): + features = fourier_series(df["ds"], props["period"], props["fourier_order"]) + columns = [ + "{}_delim_{}".format(name, i + 1) for i in range(features.shape[1]) + ] + features = pd.DataFrame(features, columns=columns) + + seasonal_features.append(features) + prior_scales.extend([props["prior_scale"]] * features.shape[1]) + modes[props["mode"]].append(name) + + if len(seasonal_features) == 0: + seasonal_features.append(pd.DataFrame({"zeros": na.zeros(df.shape[0])})) + prior_scales.append(1.0) + + seasonal_features = pd.concat(seasonal_features, axis=1) + component_cols, modes = self.regressor_column_matrix(seasonal_features, modes) + return seasonal_features, prior_scales, component_cols, modes + + def regressor_column_matrix(self, seasonal_features, modes): + components = pd.DataFrame( + { + "col": na.arange(seasonal_features.shape[1]), + "component": [x.split("_delim_")[0] for x in seasonal_features.columns], + } + ) + + for mode in ["additive", "multiplicative"]: + components = self.add_group_component( + components, mode + "_terms", modes[mode] + ) + modes[mode].append(mode + "_terms") + + component_cols = pd.crosstab( + components["col"], + components["component"], + ).sort_index(level="col") + + for name in ["additive_terms", "multiplicative_terms"]: + if name not in component_cols: + component_cols[name] = 0 + + component_cols.drop("zeros", axis=1, inplace=True, errors="ignore") + + return component_cols, modes + + def add_group_component(self, components, name, group): + new_comp = components[components["component"].isin(set(group))].copy() + group_cols = new_comp["col"].unique() + if len(group_cols) > 0: + new_comp = pd.DataFrame({"col": group_cols, "component": name}) + components = pd.concat([components, new_comp]) + return components + + def predict_trend(self, df: pd.DataFrame) -> pd.Series: + # NOTE: this indexing is possible because M=1 guaranteed + # If this were not the case, we should take the arithmetic mean + [[k]] = self.k + [[m]] = self.m + [delta] = self.delta + + t = df["t"].to_numpy() + if self.growth == "linear": + mask = self.changepoints_t[None, :] <= t[..., None] + deltas_t = delta * mask + k_t = deltas_t.sum(axis=1) + k + m_t = (-self.changepoints_t * deltas_t).sum(axis=1) + m + trend = k_t * t + m_t + else: + raise NotImplementedError(self.growth + " is not supported") + + return trend * self.y_scale + df["floor"] + + def predict(self, df: pd.DataFrame) -> na.NadaArray: + df["trend"] = self.predict_trend(df) + seasonal_comps = self.predict_seasonal_comps(df) + + result = pd.concat((df[["ds", "trend"]], seasonal_comps), axis=1) + + return (result["trend"] * (result["multiplicative_terms"] + 1)) + result[ + "additive_terms" + ] + + @override + def __call__( + self, + dates: na.NadaArray, + floor: na.NadaArray, + t: na.NadaArray, + trend: na.NadaArray, + ) -> na.NadaArray: + """ + Forward pass. + Note: requires multiple input arrays due special nature of forecasting. + + Args: + dates (na.NadaArray): Array of timestamp values. + floor (na.NadaArray): Array of floor values. + t (na.NadaArray): Array of t values. + trend (na.NadaArray): Array of trend values. + + Returns: + na.NadaArray: Forecasted values. + """ + return self.forward(dates=dates, floor=floor, t=t, trend=trend) + + @override + def forward( + self, + dates: na.NadaArray, + floor: na.NadaArray, + t: na.NadaArray, + trend: na.NadaArray, + ) -> na.NadaArray: + """ + Forward pass. + Note: requires multiple input arrays due special nature of forecasting. + + Args: + dates (na.NadaArray): Array of timestamp values. + floor (na.NadaArray): Array of floor values. + t (na.NadaArray): Array of t values. + trend (na.NadaArray): Array of trend values. + + Returns: + na.NadaArray: Forecasted values. + """ + return self.predict( + pd.DataFrame( + { + "ds": dates.inner, + "floor": floor.inner, + "t": t.inner, + "trend": trend.inner, + } + ) + ) diff --git a/tests/nada-tests/nada-project.toml b/tests/nada-tests/nada-project.toml index f79b2da..f8c9e6a 100644 --- a/tests/nada-tests/nada-project.toml +++ b/tests/nada-tests/nada-project.toml @@ -41,3 +41,7 @@ prime_size = 128 [[programs]] path = "src/end-to-end.py" prime_size = 128 + +[[programs]] +path = "src/prophet.py" +prime_size = 128 diff --git a/tests/nada-tests/src/linear_regression.py b/tests/nada-tests/src/linear_regression.py index bf3b2cf..b347c7d 100644 --- a/tests/nada-tests/src/linear_regression.py +++ b/tests/nada-tests/src/linear_regression.py @@ -1,6 +1,6 @@ from nada_dsl import * import nada_algebra as na -from nada_ai.nn import LinearRegression +from nada_ai.linear_model import LinearRegression def nada_main(): diff --git a/tests/nada-tests/src/prophet.py b/tests/nada-tests/src/prophet.py new file mode 100644 index 0000000..6c4686d --- /dev/null +++ b/tests/nada-tests/src/prophet.py @@ -0,0 +1,26 @@ +import pytest +from nada_dsl import * +import nada_algebra as na +from nada_ai.time_series import Prophet + + +def nada_main(): + party = Party("party") + + dates = na.array([4], party, "dates", SecretInteger) + floor = na.array([4], party, "floor", SecretInteger) + t = na.array([4], party, "t", na.SecretRational) + trend = na.array([4], party, "floor", na.SecretRational) + + prophet = Prophet( + growth="linear", + n_changepoints=2, + num_seasonality_features=1, + ) + + with pytest.raises(NotImplementedError): + Prophet(growth="to_the_moon") + + result = prophet(dates, floor, t, trend) + + return result.output(party, "forecast") From 65b433ed1f9efbd2c5d68bc72876e7e1d26383d9 Mon Sep 17 00:00:00 2001 From: Mathias Leys Date: Tue, 11 Jun 2024 12:41:26 +0100 Subject: [PATCH 03/28] Implement prophet model --- nada_ai/__init__.py | 2 +- nada_ai/client.py | 27 +++- nada_ai/time_series/helpers.py | 46 ++---- nada_ai/time_series/prophet.py | 273 +++++++++++++++++++-------------- 4 files changed, 192 insertions(+), 156 deletions(-) diff --git a/nada_ai/__init__.py b/nada_ai/__init__.py index 0cbc867..de41c0d 100644 --- a/nada_ai/__init__.py +++ b/nada_ai/__init__.py @@ -1 +1 @@ -from nada_ai.client import StateClient, TorchClient, SklearnClient +from nada_ai.client import StateClient, TorchClient, SklearnClient, ProphetClient diff --git a/nada_ai/client.py b/nada_ai/client.py index 35147c6..50eff14 100644 --- a/nada_ai/client.py +++ b/nada_ai/client.py @@ -12,6 +12,7 @@ LogisticRegression, LogisticRegressionCV, ) +import prophet import torch from torch import nn import sklearn @@ -101,9 +102,9 @@ def __ensure_numpy(self, array_like: Any) -> np.ndarray: return np.array(array_like) if isinstance(array_like, (float, int, np.floating)): return np.array([array_like]) - raise TypeError( - "Could not convert type `%s` to NumPy array" % type(array_like).__name__ - ) + + error_msg = f"Could not convert `{type(array_like).__name__}` to NumPy array" + raise TypeError(error_msg) class StateClient(ModelClient): @@ -153,3 +154,23 @@ def __init__(self, model: sklearn.base.BaseEstimator) -> None: ) self.state_dict = state_dict + + +class ProphetClient(ModelClient): + """ModelClient for Prophet models""" + + def __init__(self, model) -> None: + """ + Client initialization. + + Args: + model (prophet.forecaster.Prophet): Prophet model. + """ + self.state_dict = { + "k": model.params["k"], + "m": model.params["m"], + "delta": model.params["delta"], + "beta": model.params["beta"], + "changepoints_t": model.changepoints_t, + "y_scale": model.y_scale, + } diff --git a/nada_ai/time_series/helpers.py b/nada_ai/time_series/helpers.py index 0a4f13e..9a753b3 100644 --- a/nada_ai/time_series/helpers.py +++ b/nada_ai/time_series/helpers.py @@ -1,48 +1,28 @@ """Time series helper functions""" -from collections import OrderedDict -import pandas as pd import numpy as np -import nada_algebra as na def fourier_series( - dates: pd.Series, + dates: np.ndarray, period: int | float, series_order: int, ) -> np.ndarray: - x_T = dates * np.pi * 2 + """ + Generates (plain-text) Fourier series. + + Args: + dates (np.ndarray): Array of timestamp values. + period (int | float): Fourier period. + series_order (int): Order of Fourier series. + + Returns: + np.ndarray: Generates Fourier series. + """ + x_T = dates * 2 * np.pi fourier_components = np.empty((dates.shape[0], 2 * series_order)) for i in range(series_order): c = x_T * (i + 1) / period fourier_components[:, 2 * i] = np.sin(c) fourier_components[:, (2 * i) + 1] = np.cos(c) return fourier_components - - -def decode_component_matrix(component_matrix: na.NadaArray): - additive_terns, multiplicative_terms, yearly, weekly, daily = component_matrix.inner - return { - "additive_terms": additive_terns, - "multiplicative_terms": multiplicative_terms, - "yearly": yearly, - "weekly": weekly, - "daily": daily, - } - - -def decode_seasonality_matrix(seasonality_matrix: na.NadaArray): - period_names = ["yearly", "yearly", "weekly", "weekly", "daily", "daily"] - modes = ["additive", "multiplicative"] * 3 - - result = {} - for row, period_name, mode in zip(seasonality_matrix.inner, period_names, modes): - period, fourier_order, prior_scale = row - result[f"{period_name}_{mode}"] = { - "period": period, - "fourier_order": fourier_order, - "prior_scale": prior_scale, - "mode": mode, - } - - return OrderedDict(result) diff --git a/nada_ai/time_series/prophet.py b/nada_ai/time_series/prophet.py index f8f3dc9..1f2ed71 100644 --- a/nada_ai/time_series/prophet.py +++ b/nada_ai/time_series/prophet.py @@ -1,15 +1,12 @@ """Facebook Prophet implementation""" -from typing import override -import pandas as pd +import numpy as np +from typing import Dict, Tuple, override + import nada_algebra as na from nada_ai.nn.module import Module from nada_ai.nn.parameter import Parameter -from nada_ai.time_series.helpers import ( - fourier_series, - decode_seasonality_matrix, - decode_component_matrix, -) +from nada_ai.time_series.helpers import fourier_series class Prophet(Module): @@ -17,191 +14,229 @@ class Prophet(Module): def __init__( self, + n_changepoints: int, growth: str = "linear", - n_changepoints: int = 25, # TODO: somehow enforce a fixed number of changepoints in case n_changepoints < len(sequence) - num_seasonality_features: int = 1, # TODO: somehow enforce a fixed number of seasonalities + yearly_seasonality: bool = True, + weekly_seasonality: bool = True, + daily_seasonality: bool = False, + seasonality_mode: str = "additive", ) -> None: - if growth not in {"linear"}: - raise NotImplementedError(f"Growth mode `{growth}` is not supported") + """ + Prophet model initialization. + Args: + n_changepoints (int): Number of changepoints. + growth (str, optional): Forecasting growth mode. Defaults to "linear". + yearly_seasonality (bool, optional): Whether or not to include a yearly + seasonality term. Defaults to True. + weekly_seasonality (bool, optional): Whether or not to include a weekly + seasonality term. Defaults to True. + daily_seasonality (bool, optional): Whether or not to include a daily + seasonality term. Defaults to False. + seasonality_mode (str, optional): Seasonality mode. Defaults to 'additive'. + """ self.growth = growth + self.seasonalities = { + "additive": {}, + "multiplicative": {}, + } + if yearly_seasonality: + self.seasonalities[seasonality_mode]["yearly"] = { + "period": 365.25, + "fourier_order": 10, + } + if weekly_seasonality: + self.seasonalities[seasonality_mode]["weekly"] = { + "period": 7, + "fourier_order": 3, + } + if daily_seasonality: + self.seasonalities[seasonality_mode]["daily"] = { + "period": 1, + "fourier_order": 4, + } + + num_fourier = 0 + for _, mode_seasonality in self.seasonalities.items(): + for _, period_seasonality in mode_seasonality.items(): + # NOTE: times two because there is always a term for both sin and cos + num_fourier += period_seasonality["fourier_order"] * 2 + M = 1 # NOTE: MAP estimation is assumed, so M=1 guaranteed self.k = Parameter((M, 1)) self.m = Parameter((M, 1)) + self.beta = ( + Parameter((M, num_fourier)) + if num_fourier != 0 + else na.NadaArray(np.array([None])) + ) self.delta = Parameter((M, n_changepoints)) - self.beta = Parameter((M, num_seasonality_features)) self.changepoints_t = Parameter(n_changepoints) - self.y_scale = Parameter(1) - self.seasonality_matrix = Parameter((6, 3)) - self.component_matrix = Parameter(5) + def predict_seasonal_comps( + self, dates: np.ndarray + ) -> Tuple[na.NadaArray, na.NadaArray]: + """ + Predicts seasonal components. - self.seasonalities = decode_seasonality_matrix(self.seasonality_matrix) - self.component_modes = decode_component_matrix(self.component_matrix) + Args: + dates (np.ndarray): Array of timestamp values. - def predict_seasonal_comps(self, df): - seasonal_features, _, component_cols, _ = self.make_all_seasonality_features(df) + Returns: + Tuple[na.NadaArray, na.NadaArray]: Additive and multiplicative + seasonal components. + """ + [beta] = self.beta - X = seasonal_features.values - data = {} - for component in component_cols.columns: - component_values = component_cols[component].values - beta_c = self.beta * component_values + seasonal_components = {} + for mode in ["additive", "multiplicative"]: + seasonal_features = self.make_seasonality_features( + dates, self.seasonalities[mode] + ) - comp = X @ beta_c.T + components = [] + for _, features in seasonal_features.items(): + if features is None: + continue - comp = ( - comp - * self.component_modes[ - component.replace("_additive", "").replace("_multiplicative", "") - ] - ) - data[component] = comp.mean(axis=1) - - return pd.DataFrame(data) - - def make_all_seasonality_features(self, df): - seasonal_features = [] - prior_scales = [] - modes = {"additive": [], "multiplicative": []} - - for name, props in self.seasonalities.items(): - features = fourier_series(df["ds"], props["period"], props["fourier_order"]) - columns = [ - "{}_delim_{}".format(name, i + 1) for i in range(features.shape[1]) - ] - features = pd.DataFrame(features, columns=columns) - - seasonal_features.append(features) - prior_scales.extend([props["prior_scale"]] * features.shape[1]) - modes[props["mode"]].append(name) - - if len(seasonal_features) == 0: - seasonal_features.append(pd.DataFrame({"zeros": na.zeros(df.shape[0])})) - prior_scales.append(1.0) - - seasonal_features = pd.concat(seasonal_features, axis=1) - component_cols, modes = self.regressor_column_matrix(seasonal_features, modes) - return seasonal_features, prior_scales, component_cols, modes - - def regressor_column_matrix(self, seasonal_features, modes): - components = pd.DataFrame( - { - "col": na.arange(seasonal_features.shape[1]), - "component": [x.split("_delim_")[0] for x in seasonal_features.columns], - } + comp = (features @ beta.T) * self.y_scale + components.append(comp) + + if len(components) == 0: + seasonal_components[mode] = na.zeros(dates.shape, na.Rational) + else: + seasonal_components[mode] = na.NadaArray(np.array(components)) + + additive_component = (seasonal_components["additive"] * self.y_scale).sum( + axis=0 ) + multiplicative_component = ( + -seasonal_components["multiplicative"] + na.rational(1) + ).prod(axis=0) - for mode in ["additive", "multiplicative"]: - components = self.add_group_component( - components, mode + "_terms", modes[mode] - ) - modes[mode].append(mode + "_terms") + return additive_component, multiplicative_component - component_cols = pd.crosstab( - components["col"], - components["component"], - ).sort_index(level="col") + def make_seasonality_features( + self, dates: np.ndarray, seasonalities: Dict[str, Dict[str, int | float]] + ) -> Dict[str, na.NadaArray]: + """ + Generates seasonality features per seasonal component. - for name in ["additive_terms", "multiplicative_terms"]: - if name not in component_cols: - component_cols[name] = 0 + Args: + dates (np.ndarray): Array of timestamp values. + seasonalities (Dict[str, Dict[str, int | float]]): Seasonality config. - component_cols.drop("zeros", axis=1, inplace=True, errors="ignore") + Returns: + Dict[str, na.NadaArray]: Generated seasonality features. + """ + features = {} + for name, props in seasonalities.items(): + period, fourier_order = props["period"], props["fourier_order"] + if fourier_order == 0: + feats = None + else: + feats = fourier_series(dates, period, fourier_order) + features[name] = na.frompyfunc(na.rational, 1, 1)(feats) + return features + + def predict_trend(self, floor: na.NadaArray, t: na.NadaArray) -> na.NadaArray: + """ + Predicts trend values. - return component_cols, modes + Args: + floor (na.NadaArray): Array of floor values. + t (na.NadaArray): Array of t values. - def add_group_component(self, components, name, group): - new_comp = components[components["component"].isin(set(group))].copy() - group_cols = new_comp["col"].unique() - if len(group_cols) > 0: - new_comp = pd.DataFrame({"col": group_cols, "component": name}) - components = pd.concat([components, new_comp]) - return components + Raises: + NotImplementedError: Raised when unsupported growth mode is provided. - def predict_trend(self, df: pd.DataFrame) -> pd.Series: - # NOTE: this indexing is possible because M=1 guaranteed + Returns: + na.NadaArray: Predicted trend. + """ + # NOTE: this indexing is possible because M=1 is guaranteed # If this were not the case, we should take the arithmetic mean [[k]] = self.k [[m]] = self.m [delta] = self.delta - t = df["t"].to_numpy() if self.growth == "linear": - mask = self.changepoints_t[None, :] <= t[..., None] + mask = na.frompyfunc( + lambda a, b: (a <= b).if_else(na.rational(1), na.rational(0)), 2, 1 + )(self.changepoints_t[None, :], t[..., None]) deltas_t = delta * mask k_t = deltas_t.sum(axis=1) + k m_t = (-self.changepoints_t * deltas_t).sum(axis=1) + m trend = k_t * t + m_t + elif self.growth == "flat": + trend = na.ones_like(t, na.rational) * m else: raise NotImplementedError(self.growth + " is not supported") - return trend * self.y_scale + df["floor"] + return trend * self.y_scale + floor - def predict(self, df: pd.DataFrame) -> na.NadaArray: - df["trend"] = self.predict_trend(df) - seasonal_comps = self.predict_seasonal_comps(df) + def predict( + self, + dates: np.ndarray, + floor: na.NadaArray, + t: na.NadaArray, + ) -> na.NadaArray: + """ + Generates time series forecasts. - result = pd.concat((df[["ds", "trend"]], seasonal_comps), axis=1) + Args: + dates (np.ndarray): Array of timestamp values. + floor (na.NadaArray): Array of floor values. + t (na.NadaArray): Array of t values. - return (result["trend"] * (result["multiplicative_terms"] + 1)) + result[ - "additive_terms" - ] + Returns: + na.NadaArray: Forecasted values. + """ + trend = self.predict_trend(floor, t) + additive_comps, multiplicative_comps = self.predict_seasonal_comps(dates) + yhat = trend * (multiplicative_comps + na.rational(1)) + additive_comps + return yhat @override def __call__( self, - dates: na.NadaArray, + dates: np.ndarray, floor: na.NadaArray, t: na.NadaArray, - trend: na.NadaArray, ) -> na.NadaArray: """ Forward pass. - Note: requires multiple input arrays due special nature of forecasting. + Note: requires multiple input arrays due to special nature of forecasting. Args: - dates (na.NadaArray): Array of timestamp values. + dates (np.ndarray): Array of timestamp values. floor (na.NadaArray): Array of floor values. t (na.NadaArray): Array of t values. - trend (na.NadaArray): Array of trend values. Returns: na.NadaArray: Forecasted values. """ - return self.forward(dates=dates, floor=floor, t=t, trend=trend) + return self.predict(dates=dates, floor=floor, t=t) @override def forward( self, - dates: na.NadaArray, + dates: np.ndarray, floor: na.NadaArray, t: na.NadaArray, - trend: na.NadaArray, ) -> na.NadaArray: """ Forward pass. - Note: requires multiple input arrays due special nature of forecasting. + Note: requires multiple input arrays due to special nature of forecasting. Args: - dates (na.NadaArray): Array of timestamp values. + dates (np.ndarray): Array of timestamp values. floor (na.NadaArray): Array of floor values. t (na.NadaArray): Array of t values. - trend (na.NadaArray): Array of trend values. Returns: na.NadaArray: Forecasted values. """ - return self.predict( - pd.DataFrame( - { - "ds": dates.inner, - "floor": floor.inner, - "t": t.inner, - "trend": trend.inner, - } - ) - ) + return self.predict(dates=dates, floor=floor, t=t) From e76f593005d75361ae9c88281bbfe5cd4aa165e2 Mon Sep 17 00:00:00 2001 From: Mathias Leys Date: Tue, 11 Jun 2024 12:41:46 +0100 Subject: [PATCH 04/28] Update module & parameter logic --- nada_ai/nn/layers.py | 12 ++++-------- nada_ai/nn/module.py | 10 +++++----- nada_ai/nn/parameter.py | 3 +-- pyproject.toml | 6 +++--- tests/nada-tests/src/prophet.py | 19 ++++++++++--------- 5 files changed, 23 insertions(+), 27 deletions(-) diff --git a/nada_ai/nn/layers.py b/nada_ai/nn/layers.py index 5d87fdd..28f32e1 100644 --- a/nada_ai/nn/layers.py +++ b/nada_ai/nn/layers.py @@ -1,13 +1,13 @@ """NN layers logic""" -from typing import Iterable, Union +from typing import Sequence, Union import numpy as np import nada_algebra as na from nada_ai.nn.module import Module from nada_ai.nn.parameter import Parameter from nada_dsl import Integer -_ShapeLike = Union[int, Iterable[int]] +_ShapeLike = Union[int, Sequence[int]] class Linear(Module): @@ -193,7 +193,7 @@ def forward(self, x: na.NadaArray) -> na.NadaArray: unbatched = True batch_size, channels, input_height, input_width = x.shape - is_rational = x.is_rational + pool_type = na.rational if x.is_rational else Integer if any(pad > 0 for pad in self.padding): x = na.pad( @@ -225,11 +225,7 @@ def forward(self, x: na.NadaArray) -> na.NadaArray: end_w = start_w + self.kernel_size[1] pool_region = x[b, c, start_h:end_h, start_w:end_w] - - if is_rational: - pool_size = na.rational(pool_region.size) - else: - pool_size = Integer(pool_region.size) + pool_size = pool_type(pool_region.size) output_tensor[b, c, i, j] = na.sum(pool_region) / pool_size diff --git a/nada_ai/nn/module.py b/nada_ai/nn/module.py index e248dbb..0a7465e 100644 --- a/nada_ai/nn/module.py +++ b/nada_ai/nn/module.py @@ -27,7 +27,7 @@ class Module(ABC): """Generic neural network module""" @abstractmethod - def forward(self, x: na.NadaArray, *args, **kwargs) -> na.NadaArray: + def forward(self, x: na.NadaArray) -> na.NadaArray: """ Forward pass. @@ -39,7 +39,7 @@ def forward(self, x: na.NadaArray, *args, **kwargs) -> na.NadaArray: """ ... - def __call__(self, x: na.NadaArray, *args, **kwargs) -> na.NadaArray: + def __call__(self, x: na.NadaArray) -> na.NadaArray: """ Proxy for forward pass. @@ -49,7 +49,7 @@ def __call__(self, x: na.NadaArray, *args, **kwargs) -> na.NadaArray: Returns: na.NadaArray: Output array. """ - return self.forward(x, *args, **kwargs) + return self.forward(x) def __named_parameters(self, prefix: str) -> Iterator[Tuple[str, Parameter]]: """ @@ -122,6 +122,6 @@ def load_state_from_network( raise NotImplementedError("Loading non-rational state is not supported") for param_name, param in self.named_parameters(): - state_name = f"{name}_{param_name}" - param_state = na.array(param.shape, party, state_name, nada_type) + param_state_name = f"{name}_{param_name}" + param_state = na.array(param.shape, party, param_state_name, nada_type) param.load_state(param_state) diff --git a/nada_ai/nn/parameter.py b/nada_ai/nn/parameter.py index b0c2df8..1d3a7ff 100644 --- a/nada_ai/nn/parameter.py +++ b/nada_ai/nn/parameter.py @@ -19,8 +19,7 @@ def __init__(self, shape: _ShapeLike) -> None: Args: shape (_ShapeLike, optional): Parameter array shape. """ - zeros = na.zeros(shape) - super().__init__(inner=zeros.inner) + super().__init__(inner=np.empty(shape)) def numel(self) -> int: """ diff --git a/pyproject.toml b/pyproject.toml index 9fb5917..5589266 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -8,7 +8,9 @@ readme = "README.md" [tool.poetry.dependencies] python = "^3.10" numpy = "^1.26.4" -pandas = "^2.2.2" +torch = "^2.3.0" +scikit-learn = "^1.4.2" +prophet = "^1.1.5" nada-dsl = "^0.2.1" py-nillion-client = "^0.2.1" nada-algebra = "^0.3.2" @@ -17,8 +19,6 @@ nada-algebra = "^0.3.2" [tool.poetry.group.dev.dependencies] pytest = "^8.2.0" black = "^24.4.2" -torch = "^2.3.0" -scikit-learn = "^1.4.2" [build-system] requires = ["poetry-core"] diff --git a/tests/nada-tests/src/prophet.py b/tests/nada-tests/src/prophet.py index 6c4686d..b39a54c 100644 --- a/tests/nada-tests/src/prophet.py +++ b/tests/nada-tests/src/prophet.py @@ -1,5 +1,5 @@ -import pytest from nada_dsl import * +import numpy as np import nada_algebra as na from nada_ai.time_series import Prophet @@ -7,20 +7,21 @@ def nada_main(): party = Party("party") - dates = na.array([4], party, "dates", SecretInteger) - floor = na.array([4], party, "floor", SecretInteger) + dates = np.linspace(1, 10, 4) + floor = na.array([4], party, "floor", na.SecretRational) t = na.array([4], party, "t", na.SecretRational) - trend = na.array([4], party, "floor", na.SecretRational) prophet = Prophet( - growth="linear", n_changepoints=2, - num_seasonality_features=1, + growth="linear", + yearly_seasonality=False, + weekly_seasonality=True, + daily_seasonality=False, + seasonality_mode="additive", ) - with pytest.raises(NotImplementedError): - Prophet(growth="to_the_moon") + prophet.load_state_from_network("my_prophet", party, na.SecretRational) - result = prophet(dates, floor, t, trend) + result = prophet(dates, floor, t) return result.output(party, "forecast") From bc5eee6ed633cc164f3592affcc027de1a919aea Mon Sep 17 00:00:00 2001 From: Mathias Leys Date: Tue, 11 Jun 2024 12:42:18 +0100 Subject: [PATCH 05/28] Update tests --- tests/nada-tests/tests/prophet.yaml | 56 +++++++++++++++++++++++++++++ 1 file changed, 56 insertions(+) create mode 100644 tests/nada-tests/tests/prophet.yaml diff --git a/tests/nada-tests/tests/prophet.yaml b/tests/nada-tests/tests/prophet.yaml new file mode 100644 index 0000000..b5d6047 --- /dev/null +++ b/tests/nada-tests/tests/prophet.yaml @@ -0,0 +1,56 @@ +--- +program: prophet +inputs: + secrets: + my_prophet_m_0_0: + SecretInteger: "3" + my_prophet_beta_0_4: + SecretInteger: "3" + my_prophet_beta_0_5: + SecretInteger: "3" + my_prophet_changepoints_t_1: + SecretInteger: "3" + my_prophet_delta_0_1: + SecretInteger: "3" + my_prophet_y_scale_0: + SecretInteger: "3" + floor_2: + SecretInteger: "3" + my_prophet_delta_0_0: + SecretInteger: "3" + floor_3: + SecretInteger: "3" + t_2: + SecretInteger: "3" + floor_0: + SecretInteger: "3" + my_prophet_changepoints_t_0: + SecretInteger: "3" + my_prophet_beta_0_1: + SecretInteger: "3" + t_1: + SecretInteger: "3" + my_prophet_beta_0_3: + SecretInteger: "3" + my_prophet_k_0_0: + SecretInteger: "3" + my_prophet_beta_0_2: + SecretInteger: "3" + floor_1: + SecretInteger: "3" + t_0: + SecretInteger: "3" + t_3: + SecretInteger: "3" + my_prophet_beta_0_0: + SecretInteger: "3" + public_variables: {} +expected_outputs: + forecast_2: + SecretInteger: "6" + forecast_0: + SecretInteger: "6" + forecast_3: + SecretInteger: "6" + forecast_1: + SecretInteger: "5" From 688e0b05b379194a72995ce8bb1d58812f5e6468 Mon Sep 17 00:00:00 2001 From: Mathias Leys Date: Tue, 11 Jun 2024 18:11:20 +0100 Subject: [PATCH 06/28] Update poetry --- poetry.lock | 650 ++++++++++++++++++++++++++++++++++++++++++++++++- pyproject.toml | 1 + 2 files changed, 643 insertions(+), 8 deletions(-) diff --git a/poetry.lock b/poetry.lock index e09e6aa..701b24c 100644 --- a/poetry.lock +++ b/poetry.lock @@ -78,6 +78,28 @@ files = [ [package.dependencies] colorama = {version = "*", markers = "platform_system == \"Windows\""} +[[package]] +name = "cmdstanpy" +version = "1.2.3" +description = "Python interface to CmdStan" +optional = false +python-versions = ">=3.8" +files = [ + {file = "cmdstanpy-1.2.3-py3-none-any.whl", hash = "sha256:9cd927496462dff5f9f8075fac13f22b370834de93d18387f204bd93418fc08a"}, + {file = "cmdstanpy-1.2.3.tar.gz", hash = "sha256:4491265f11d2bc8efa3ba0628e36d673ab995f30b168c06ee8e9c297a7ce3569"}, +] + +[package.dependencies] +numpy = ">=1.21" +pandas = "*" +stanio = ">=0.4.0,<2.0.0" +tqdm = "*" + +[package.extras] +all = ["xarray"] +docs = ["ipykernel", "ipython", "ipywidgets", "matplotlib", "nbsphinx", "pydata-sphinx-theme (<0.9)", "sphinx (>5,<6)", "sphinx-copybutton", "xarray"] +test = ["flake8", "mypy", "pylint", "pytest", "pytest-cov", "pytest-order", "xarray"] + [[package]] name = "colorama" version = "0.4.6" @@ -89,6 +111,84 @@ files = [ {file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"}, ] +[[package]] +name = "contourpy" +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.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.8.0)", "types-Pillow"] +test = ["Pillow", "contourpy[test-no-images]", "matplotlib"] +test-no-images = ["pytest", "pytest-cov", "pytest-xdist", "wurlitzer"] + +[[package]] +name = "cycler" +version = "0.12.1" +description = "Composable style cycles" +optional = false +python-versions = ">=3.8" +files = [ + {file = "cycler-0.12.1-py3-none-any.whl", hash = "sha256:85cef7cff222d8644161529808465972e51340599459b8ac3ccbac5a854e0d30"}, + {file = "cycler-0.12.1.tar.gz", hash = "sha256:88bb128f02ba341da8ef447245a9e138fae777f6a23943da4540077d3601eb1c"}, +] + +[package.extras] +docs = ["ipython", "matplotlib", "numpydoc", "sphinx"] +tests = ["pytest", "pytest-cov", "pytest-xdist"] + [[package]] name = "exceptiongroup" version = "1.2.1" @@ -119,6 +219,71 @@ docs = ["furo (>=2023.9.10)", "sphinx (>=7.2.6)", "sphinx-autodoc-typehints (>=1 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 = "fonttools" +version = "4.53.0" +description = "Tools to manipulate font files" +optional = false +python-versions = ">=3.8" +files = [ + {file = "fonttools-4.53.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:52a6e0a7a0bf611c19bc8ec8f7592bdae79c8296c70eb05917fd831354699b20"}, + {file = "fonttools-4.53.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:099634631b9dd271d4a835d2b2a9e042ccc94ecdf7e2dd9f7f34f7daf333358d"}, + {file = "fonttools-4.53.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e40013572bfb843d6794a3ce076c29ef4efd15937ab833f520117f8eccc84fd6"}, + {file = "fonttools-4.53.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:715b41c3e231f7334cbe79dfc698213dcb7211520ec7a3bc2ba20c8515e8a3b5"}, + {file = "fonttools-4.53.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:74ae2441731a05b44d5988d3ac2cf784d3ee0a535dbed257cbfff4be8bb49eb9"}, + {file = "fonttools-4.53.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:95db0c6581a54b47c30860d013977b8a14febc206c8b5ff562f9fe32738a8aca"}, + {file = "fonttools-4.53.0-cp310-cp310-win32.whl", hash = "sha256:9cd7a6beec6495d1dffb1033d50a3f82dfece23e9eb3c20cd3c2444d27514068"}, + {file = "fonttools-4.53.0-cp310-cp310-win_amd64.whl", hash = "sha256:daaef7390e632283051e3cf3e16aff2b68b247e99aea916f64e578c0449c9c68"}, + {file = "fonttools-4.53.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:a209d2e624ba492df4f3bfad5996d1f76f03069c6133c60cd04f9a9e715595ec"}, + {file = "fonttools-4.53.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:4f520d9ac5b938e6494f58a25c77564beca7d0199ecf726e1bd3d56872c59749"}, + {file = "fonttools-4.53.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:eceef49f457253000e6a2d0f7bd08ff4e9fe96ec4ffce2dbcb32e34d9c1b8161"}, + {file = "fonttools-4.53.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fa1f3e34373aa16045484b4d9d352d4c6b5f9f77ac77a178252ccbc851e8b2ee"}, + {file = "fonttools-4.53.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:28d072169fe8275fb1a0d35e3233f6df36a7e8474e56cb790a7258ad822b6fd6"}, + {file = "fonttools-4.53.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:4a2a6ba400d386e904fd05db81f73bee0008af37799a7586deaa4aef8cd5971e"}, + {file = "fonttools-4.53.0-cp311-cp311-win32.whl", hash = "sha256:bb7273789f69b565d88e97e9e1da602b4ee7ba733caf35a6c2affd4334d4f005"}, + {file = "fonttools-4.53.0-cp311-cp311-win_amd64.whl", hash = "sha256:9fe9096a60113e1d755e9e6bda15ef7e03391ee0554d22829aa506cdf946f796"}, + {file = "fonttools-4.53.0-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:d8f191a17369bd53a5557a5ee4bab91d5330ca3aefcdf17fab9a497b0e7cff7a"}, + {file = "fonttools-4.53.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:93156dd7f90ae0a1b0e8871032a07ef3178f553f0c70c386025a808f3a63b1f4"}, + {file = "fonttools-4.53.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bff98816cb144fb7b85e4b5ba3888a33b56ecef075b0e95b95bcd0a5fbf20f06"}, + {file = "fonttools-4.53.0-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:973d030180eca8255b1bce6ffc09ef38a05dcec0e8320cc9b7bcaa65346f341d"}, + {file = "fonttools-4.53.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:c4ee5a24e281fbd8261c6ab29faa7fd9a87a12e8c0eed485b705236c65999109"}, + {file = "fonttools-4.53.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:bd5bc124fae781a4422f61b98d1d7faa47985f663a64770b78f13d2c072410c2"}, + {file = "fonttools-4.53.0-cp312-cp312-win32.whl", hash = "sha256:a239afa1126b6a619130909c8404070e2b473dd2b7fc4aacacd2e763f8597fea"}, + {file = "fonttools-4.53.0-cp312-cp312-win_amd64.whl", hash = "sha256:45b4afb069039f0366a43a5d454bc54eea942bfb66b3fc3e9a2c07ef4d617380"}, + {file = "fonttools-4.53.0-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:93bc9e5aaa06ff928d751dc6be889ff3e7d2aa393ab873bc7f6396a99f6fbb12"}, + {file = "fonttools-4.53.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:2367d47816cc9783a28645bc1dac07f8ffc93e0f015e8c9fc674a5b76a6da6e4"}, + {file = "fonttools-4.53.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:907fa0b662dd8fc1d7c661b90782ce81afb510fc4b7aa6ae7304d6c094b27bce"}, + {file = "fonttools-4.53.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3e0ad3c6ea4bd6a289d958a1eb922767233f00982cf0fe42b177657c86c80a8f"}, + {file = "fonttools-4.53.0-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:73121a9b7ff93ada888aaee3985a88495489cc027894458cb1a736660bdfb206"}, + {file = "fonttools-4.53.0-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:ee595d7ba9bba130b2bec555a40aafa60c26ce68ed0cf509983e0f12d88674fd"}, + {file = "fonttools-4.53.0-cp38-cp38-win32.whl", hash = "sha256:fca66d9ff2ac89b03f5aa17e0b21a97c21f3491c46b583bb131eb32c7bab33af"}, + {file = "fonttools-4.53.0-cp38-cp38-win_amd64.whl", hash = "sha256:31f0e3147375002aae30696dd1dc596636abbd22fca09d2e730ecde0baad1d6b"}, + {file = "fonttools-4.53.0-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:7d6166192dcd925c78a91d599b48960e0a46fe565391c79fe6de481ac44d20ac"}, + {file = "fonttools-4.53.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:ef50ec31649fbc3acf6afd261ed89d09eb909b97cc289d80476166df8438524d"}, + {file = "fonttools-4.53.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7f193f060391a455920d61684a70017ef5284ccbe6023bb056e15e5ac3de11d1"}, + {file = "fonttools-4.53.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ba9f09ff17f947392a855e3455a846f9855f6cf6bec33e9a427d3c1d254c712f"}, + {file = "fonttools-4.53.0-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:0c555e039d268445172b909b1b6bdcba42ada1cf4a60e367d68702e3f87e5f64"}, + {file = "fonttools-4.53.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:5a4788036201c908079e89ae3f5399b33bf45b9ea4514913f4dbbe4fac08efe0"}, + {file = "fonttools-4.53.0-cp39-cp39-win32.whl", hash = "sha256:d1a24f51a3305362b94681120c508758a88f207fa0a681c16b5a4172e9e6c7a9"}, + {file = "fonttools-4.53.0-cp39-cp39-win_amd64.whl", hash = "sha256:1e677bfb2b4bd0e5e99e0f7283e65e47a9814b0486cb64a41adf9ef110e078f2"}, + {file = "fonttools-4.53.0-py3-none-any.whl", hash = "sha256:6b4f04b1fbc01a3569d63359f2227c89ab294550de277fd09d8fca6185669fa4"}, + {file = "fonttools-4.53.0.tar.gz", hash = "sha256:c93ed66d32de1559b6fc348838c7572d5c0ac1e4a258e76763a5caddd8944002"}, +] + +[package.extras] +all = ["brotli (>=1.0.1)", "brotlicffi (>=0.8.0)", "fs (>=2.2.0,<3)", "lxml (>=4.0)", "lz4 (>=1.7.4.2)", "matplotlib", "munkres", "pycairo", "scipy", "skia-pathops (>=0.5.0)", "sympy", "uharfbuzz (>=0.23.0)", "unicodedata2 (>=15.1.0)", "xattr", "zopfli (>=0.1.4)"] +graphite = ["lz4 (>=1.7.4.2)"] +interpolatable = ["munkres", "pycairo", "scipy"] +lxml = ["lxml (>=4.0)"] +pathops = ["skia-pathops (>=0.5.0)"] +plot = ["matplotlib"] +repacker = ["uharfbuzz (>=0.23.0)"] +symfont = ["sympy"] +type1 = ["xattr"] +ufo = ["fs (>=2.2.0,<3)"] +unicode = ["unicodedata2 (>=15.1.0)"] +woff = ["brotli (>=1.0.1)", "brotlicffi (>=0.8.0)", "zopfli (>=0.1.4)"] + [[package]] name = "fsspec" version = "2024.6.0" @@ -158,6 +323,35 @@ test-downstream = ["aiobotocore (>=2.5.4,<3.0.0)", "dask-expr", "dask[dataframe, test-full = ["adlfs", "aiohttp (!=4.0.0a0,!=4.0.0a1)", "cloudpickle", "dask", "distributed", "dropbox", "dropboxdrivefs", "fastparquet", "fusepy", "gcsfs", "jinja2", "kerchunk", "libarchive-c", "lz4", "notebook", "numpy", "ocifs", "pandas", "panel", "paramiko", "pyarrow", "pyarrow (>=1)", "pyftpdlib", "pygit2", "pytest", "pytest-asyncio (!=0.22.0)", "pytest-benchmark", "pytest-cov", "pytest-mock", "pytest-recording", "pytest-rerunfailures", "python-snappy", "requests", "smbprotocol", "tqdm", "urllib3", "zarr", "zstandard"] tqdm = ["tqdm"] +[[package]] +name = "holidays" +version = "0.50" +description = "Generate and work with holidays in Python" +optional = false +python-versions = ">=3.8" +files = [ + {file = "holidays-0.50-py3-none-any.whl", hash = "sha256:7f17ce3a29bc77194ab90eaa320a8cc71823f6beb8a034c0d892cc20114278c3"}, + {file = "holidays-0.50.tar.gz", hash = "sha256:f9b7de6c98bafefc5d19087dbb721926b31670133704f414032c22b309648e8a"}, +] + +[package.dependencies] +python-dateutil = "*" + +[[package]] +name = "importlib-resources" +version = "6.4.0" +description = "Read resources from Python packages" +optional = false +python-versions = ">=3.8" +files = [ + {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.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" version = "2.0.0" @@ -211,6 +405,119 @@ files = [ {file = "joblib-1.4.2.tar.gz", hash = "sha256:2382c5816b2636fbd20a09e0f4e9dad4736765fdfb7dca582943b9c1366b3f0e"}, ] +[[package]] +name = "kiwisolver" +version = "1.4.5" +description = "A fast implementation of the Cassowary constraint solver" +optional = false +python-versions = ">=3.7" +files = [ + {file = "kiwisolver-1.4.5-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:05703cf211d585109fcd72207a31bb170a0f22144d68298dc5e61b3c946518af"}, + {file = "kiwisolver-1.4.5-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:146d14bebb7f1dc4d5fbf74f8a6cb15ac42baadee8912eb84ac0b3b2a3dc6ac3"}, + {file = "kiwisolver-1.4.5-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:6ef7afcd2d281494c0a9101d5c571970708ad911d028137cd558f02b851c08b4"}, + {file = "kiwisolver-1.4.5-cp310-cp310-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:9eaa8b117dc8337728e834b9c6e2611f10c79e38f65157c4c38e9400286f5cb1"}, + {file = "kiwisolver-1.4.5-cp310-cp310-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:ec20916e7b4cbfb1f12380e46486ec4bcbaa91a9c448b97023fde0d5bbf9e4ff"}, + {file = "kiwisolver-1.4.5-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:39b42c68602539407884cf70d6a480a469b93b81b7701378ba5e2328660c847a"}, + {file = "kiwisolver-1.4.5-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:aa12042de0171fad672b6c59df69106d20d5596e4f87b5e8f76df757a7c399aa"}, + {file = "kiwisolver-1.4.5-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2a40773c71d7ccdd3798f6489aaac9eee213d566850a9533f8d26332d626b82c"}, + {file = "kiwisolver-1.4.5-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:19df6e621f6d8b4b9c4d45f40a66839294ff2bb235e64d2178f7522d9170ac5b"}, + {file = "kiwisolver-1.4.5-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:83d78376d0d4fd884e2c114d0621624b73d2aba4e2788182d286309ebdeed770"}, + {file = "kiwisolver-1.4.5-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:e391b1f0a8a5a10ab3b9bb6afcfd74f2175f24f8975fb87ecae700d1503cdee0"}, + {file = "kiwisolver-1.4.5-cp310-cp310-musllinux_1_1_s390x.whl", hash = "sha256:852542f9481f4a62dbb5dd99e8ab7aedfeb8fb6342349a181d4036877410f525"}, + {file = "kiwisolver-1.4.5-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:59edc41b24031bc25108e210c0def6f6c2191210492a972d585a06ff246bb79b"}, + {file = "kiwisolver-1.4.5-cp310-cp310-win32.whl", hash = "sha256:a6aa6315319a052b4ee378aa171959c898a6183f15c1e541821c5c59beaa0238"}, + {file = "kiwisolver-1.4.5-cp310-cp310-win_amd64.whl", hash = "sha256:d0ef46024e6a3d79c01ff13801cb19d0cad7fd859b15037aec74315540acc276"}, + {file = "kiwisolver-1.4.5-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:11863aa14a51fd6ec28688d76f1735f8f69ab1fabf388851a595d0721af042f5"}, + {file = "kiwisolver-1.4.5-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:8ab3919a9997ab7ef2fbbed0cc99bb28d3c13e6d4b1ad36e97e482558a91be90"}, + {file = "kiwisolver-1.4.5-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:fcc700eadbbccbf6bc1bcb9dbe0786b4b1cb91ca0dcda336eef5c2beed37b797"}, + {file = "kiwisolver-1.4.5-cp311-cp311-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:dfdd7c0b105af050eb3d64997809dc21da247cf44e63dc73ff0fd20b96be55a9"}, + {file = "kiwisolver-1.4.5-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:76c6a5964640638cdeaa0c359382e5703e9293030fe730018ca06bc2010c4437"}, + {file = "kiwisolver-1.4.5-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:bbea0db94288e29afcc4c28afbf3a7ccaf2d7e027489c449cf7e8f83c6346eb9"}, + {file = "kiwisolver-1.4.5-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:ceec1a6bc6cab1d6ff5d06592a91a692f90ec7505d6463a88a52cc0eb58545da"}, + {file = "kiwisolver-1.4.5-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:040c1aebeda72197ef477a906782b5ab0d387642e93bda547336b8957c61022e"}, + {file = "kiwisolver-1.4.5-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:f91de7223d4c7b793867797bacd1ee53bfe7359bd70d27b7b58a04efbb9436c8"}, + {file = "kiwisolver-1.4.5-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:faae4860798c31530dd184046a900e652c95513796ef51a12bc086710c2eec4d"}, + {file = "kiwisolver-1.4.5-cp311-cp311-musllinux_1_1_ppc64le.whl", hash = "sha256:b0157420efcb803e71d1b28e2c287518b8808b7cf1ab8af36718fd0a2c453eb0"}, + {file = "kiwisolver-1.4.5-cp311-cp311-musllinux_1_1_s390x.whl", hash = "sha256:06f54715b7737c2fecdbf140d1afb11a33d59508a47bf11bb38ecf21dc9ab79f"}, + {file = "kiwisolver-1.4.5-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:fdb7adb641a0d13bdcd4ef48e062363d8a9ad4a182ac7647ec88f695e719ae9f"}, + {file = "kiwisolver-1.4.5-cp311-cp311-win32.whl", hash = "sha256:bb86433b1cfe686da83ce32a9d3a8dd308e85c76b60896d58f082136f10bffac"}, + {file = "kiwisolver-1.4.5-cp311-cp311-win_amd64.whl", hash = "sha256:6c08e1312a9cf1074d17b17728d3dfce2a5125b2d791527f33ffbe805200a355"}, + {file = "kiwisolver-1.4.5-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:32d5cf40c4f7c7b3ca500f8985eb3fb3a7dfc023215e876f207956b5ea26632a"}, + {file = "kiwisolver-1.4.5-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:f846c260f483d1fd217fe5ed7c173fb109efa6b1fc8381c8b7552c5781756192"}, + {file = "kiwisolver-1.4.5-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:5ff5cf3571589b6d13bfbfd6bcd7a3f659e42f96b5fd1c4830c4cf21d4f5ef45"}, + {file = "kiwisolver-1.4.5-cp312-cp312-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7269d9e5f1084a653d575c7ec012ff57f0c042258bf5db0954bf551c158466e7"}, + {file = "kiwisolver-1.4.5-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:da802a19d6e15dffe4b0c24b38b3af68e6c1a68e6e1d8f30148c83864f3881db"}, + {file = "kiwisolver-1.4.5-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3aba7311af82e335dd1e36ffff68aaca609ca6290c2cb6d821a39aa075d8e3ff"}, + {file = "kiwisolver-1.4.5-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:763773d53f07244148ccac5b084da5adb90bfaee39c197554f01b286cf869228"}, + {file = "kiwisolver-1.4.5-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2270953c0d8cdab5d422bee7d2007f043473f9d2999631c86a223c9db56cbd16"}, + {file = "kiwisolver-1.4.5-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:d099e745a512f7e3bbe7249ca835f4d357c586d78d79ae8f1dcd4d8adeb9bda9"}, + {file = "kiwisolver-1.4.5-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:74db36e14a7d1ce0986fa104f7d5637aea5c82ca6326ed0ec5694280942d1162"}, + {file = "kiwisolver-1.4.5-cp312-cp312-musllinux_1_1_ppc64le.whl", hash = "sha256:7e5bab140c309cb3a6ce373a9e71eb7e4873c70c2dda01df6820474f9889d6d4"}, + {file = "kiwisolver-1.4.5-cp312-cp312-musllinux_1_1_s390x.whl", hash = "sha256:0f114aa76dc1b8f636d077979c0ac22e7cd8f3493abbab152f20eb8d3cda71f3"}, + {file = "kiwisolver-1.4.5-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:88a2df29d4724b9237fc0c6eaf2a1adae0cdc0b3e9f4d8e7dc54b16812d2d81a"}, + {file = "kiwisolver-1.4.5-cp312-cp312-win32.whl", hash = "sha256:72d40b33e834371fd330fb1472ca19d9b8327acb79a5821d4008391db8e29f20"}, + {file = "kiwisolver-1.4.5-cp312-cp312-win_amd64.whl", hash = "sha256:2c5674c4e74d939b9d91dda0fae10597ac7521768fec9e399c70a1f27e2ea2d9"}, + {file = "kiwisolver-1.4.5-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:3a2b053a0ab7a3960c98725cfb0bf5b48ba82f64ec95fe06f1d06c99b552e130"}, + {file = "kiwisolver-1.4.5-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3cd32d6c13807e5c66a7cbb79f90b553642f296ae4518a60d8d76243b0ad2898"}, + {file = "kiwisolver-1.4.5-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:59ec7b7c7e1a61061850d53aaf8e93db63dce0c936db1fda2658b70e4a1be709"}, + {file = "kiwisolver-1.4.5-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:da4cfb373035def307905d05041c1d06d8936452fe89d464743ae7fb8371078b"}, + {file = "kiwisolver-1.4.5-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:2400873bccc260b6ae184b2b8a4fec0e4082d30648eadb7c3d9a13405d861e89"}, + {file = "kiwisolver-1.4.5-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:1b04139c4236a0f3aff534479b58f6f849a8b351e1314826c2d230849ed48985"}, + {file = "kiwisolver-1.4.5-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:4e66e81a5779b65ac21764c295087de82235597a2293d18d943f8e9e32746265"}, + {file = "kiwisolver-1.4.5-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:7931d8f1f67c4be9ba1dd9c451fb0eeca1a25b89e4d3f89e828fe12a519b782a"}, + {file = "kiwisolver-1.4.5-cp37-cp37m-musllinux_1_1_ppc64le.whl", hash = "sha256:b3f7e75f3015df442238cca659f8baa5f42ce2a8582727981cbfa15fee0ee205"}, + {file = "kiwisolver-1.4.5-cp37-cp37m-musllinux_1_1_s390x.whl", hash = "sha256:bbf1d63eef84b2e8c89011b7f2235b1e0bf7dacc11cac9431fc6468e99ac77fb"}, + {file = "kiwisolver-1.4.5-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:4c380469bd3f970ef677bf2bcba2b6b0b4d5c75e7a020fb863ef75084efad66f"}, + {file = "kiwisolver-1.4.5-cp37-cp37m-win32.whl", hash = "sha256:9408acf3270c4b6baad483865191e3e582b638b1654a007c62e3efe96f09a9a3"}, + {file = "kiwisolver-1.4.5-cp37-cp37m-win_amd64.whl", hash = "sha256:5b94529f9b2591b7af5f3e0e730a4e0a41ea174af35a4fd067775f9bdfeee01a"}, + {file = "kiwisolver-1.4.5-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:11c7de8f692fc99816e8ac50d1d1aef4f75126eefc33ac79aac02c099fd3db71"}, + {file = "kiwisolver-1.4.5-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:53abb58632235cd154176ced1ae8f0d29a6657aa1aa9decf50b899b755bc2b93"}, + {file = "kiwisolver-1.4.5-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:88b9f257ca61b838b6f8094a62418421f87ac2a1069f7e896c36a7d86b5d4c29"}, + {file = "kiwisolver-1.4.5-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3195782b26fc03aa9c6913d5bad5aeb864bdc372924c093b0f1cebad603dd712"}, + {file = "kiwisolver-1.4.5-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:fc579bf0f502e54926519451b920e875f433aceb4624a3646b3252b5caa9e0b6"}, + {file = "kiwisolver-1.4.5-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5a580c91d686376f0f7c295357595c5a026e6cbc3d77b7c36e290201e7c11ecb"}, + {file = "kiwisolver-1.4.5-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:cfe6ab8da05c01ba6fbea630377b5da2cd9bcbc6338510116b01c1bc939a2c18"}, + {file = "kiwisolver-1.4.5-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:d2e5a98f0ec99beb3c10e13b387f8db39106d53993f498b295f0c914328b1333"}, + {file = "kiwisolver-1.4.5-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:a51a263952b1429e429ff236d2f5a21c5125437861baeed77f5e1cc2d2c7c6da"}, + {file = "kiwisolver-1.4.5-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:3edd2fa14e68c9be82c5b16689e8d63d89fe927e56debd6e1dbce7a26a17f81b"}, + {file = "kiwisolver-1.4.5-cp38-cp38-musllinux_1_1_ppc64le.whl", hash = "sha256:74d1b44c6cfc897df648cc9fdaa09bc3e7679926e6f96df05775d4fb3946571c"}, + {file = "kiwisolver-1.4.5-cp38-cp38-musllinux_1_1_s390x.whl", hash = "sha256:76d9289ed3f7501012e05abb8358bbb129149dbd173f1f57a1bf1c22d19ab7cc"}, + {file = "kiwisolver-1.4.5-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:92dea1ffe3714fa8eb6a314d2b3c773208d865a0e0d35e713ec54eea08a66250"}, + {file = "kiwisolver-1.4.5-cp38-cp38-win32.whl", hash = "sha256:5c90ae8c8d32e472be041e76f9d2f2dbff4d0b0be8bd4041770eddb18cf49a4e"}, + {file = "kiwisolver-1.4.5-cp38-cp38-win_amd64.whl", hash = "sha256:c7940c1dc63eb37a67721b10d703247552416f719c4188c54e04334321351ced"}, + {file = "kiwisolver-1.4.5-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:9407b6a5f0d675e8a827ad8742e1d6b49d9c1a1da5d952a67d50ef5f4170b18d"}, + {file = "kiwisolver-1.4.5-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:15568384086b6df3c65353820a4473575dbad192e35010f622c6ce3eebd57af9"}, + {file = "kiwisolver-1.4.5-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:0dc9db8e79f0036e8173c466d21ef18e1befc02de8bf8aa8dc0813a6dc8a7046"}, + {file = "kiwisolver-1.4.5-cp39-cp39-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:cdc8a402aaee9a798b50d8b827d7ecf75edc5fb35ea0f91f213ff927c15f4ff0"}, + {file = "kiwisolver-1.4.5-cp39-cp39-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:6c3bd3cde54cafb87d74d8db50b909705c62b17c2099b8f2e25b461882e544ff"}, + {file = "kiwisolver-1.4.5-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:955e8513d07a283056b1396e9a57ceddbd272d9252c14f154d450d227606eb54"}, + {file = "kiwisolver-1.4.5-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:346f5343b9e3f00b8db8ba359350eb124b98c99efd0b408728ac6ebf38173958"}, + {file = "kiwisolver-1.4.5-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b9098e0049e88c6a24ff64545cdfc50807818ba6c1b739cae221bbbcbc58aad3"}, + {file = "kiwisolver-1.4.5-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:00bd361b903dc4bbf4eb165f24d1acbee754fce22ded24c3d56eec268658a5cf"}, + {file = "kiwisolver-1.4.5-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:7b8b454bac16428b22560d0a1cf0a09875339cab69df61d7805bf48919415901"}, + {file = "kiwisolver-1.4.5-cp39-cp39-musllinux_1_1_ppc64le.whl", hash = "sha256:f1d072c2eb0ad60d4c183f3fb44ac6f73fb7a8f16a2694a91f988275cbf352f9"}, + {file = "kiwisolver-1.4.5-cp39-cp39-musllinux_1_1_s390x.whl", hash = "sha256:31a82d498054cac9f6d0b53d02bb85811185bcb477d4b60144f915f3b3126342"}, + {file = "kiwisolver-1.4.5-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:6512cb89e334e4700febbffaaa52761b65b4f5a3cf33f960213d5656cea36a77"}, + {file = "kiwisolver-1.4.5-cp39-cp39-win32.whl", hash = "sha256:9db8ea4c388fdb0f780fe91346fd438657ea602d58348753d9fb265ce1bca67f"}, + {file = "kiwisolver-1.4.5-cp39-cp39-win_amd64.whl", hash = "sha256:59415f46a37f7f2efeec758353dd2eae1b07640d8ca0f0c42548ec4125492635"}, + {file = "kiwisolver-1.4.5-pp37-pypy37_pp73-macosx_10_9_x86_64.whl", hash = "sha256:5c7b3b3a728dc6faf3fc372ef24f21d1e3cee2ac3e9596691d746e5a536de920"}, + {file = "kiwisolver-1.4.5-pp37-pypy37_pp73-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:620ced262a86244e2be10a676b646f29c34537d0d9cc8eb26c08f53d98013390"}, + {file = "kiwisolver-1.4.5-pp37-pypy37_pp73-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:378a214a1e3bbf5ac4a8708304318b4f890da88c9e6a07699c4ae7174c09a68d"}, + {file = "kiwisolver-1.4.5-pp37-pypy37_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:aaf7be1207676ac608a50cd08f102f6742dbfc70e8d60c4db1c6897f62f71523"}, + {file = "kiwisolver-1.4.5-pp37-pypy37_pp73-win_amd64.whl", hash = "sha256:ba55dce0a9b8ff59495ddd050a0225d58bd0983d09f87cfe2b6aec4f2c1234e4"}, + {file = "kiwisolver-1.4.5-pp38-pypy38_pp73-macosx_10_9_x86_64.whl", hash = "sha256:fd32ea360bcbb92d28933fc05ed09bffcb1704ba3fc7942e81db0fd4f81a7892"}, + {file = "kiwisolver-1.4.5-pp38-pypy38_pp73-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:5e7139af55d1688f8b960ee9ad5adafc4ac17c1c473fe07133ac092310d76544"}, + {file = "kiwisolver-1.4.5-pp38-pypy38_pp73-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:dced8146011d2bc2e883f9bd68618b8247387f4bbec46d7392b3c3b032640126"}, + {file = "kiwisolver-1.4.5-pp38-pypy38_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c9bf3325c47b11b2e51bca0824ea217c7cd84491d8ac4eefd1e409705ef092bd"}, + {file = "kiwisolver-1.4.5-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:5794cf59533bc3f1b1c821f7206a3617999db9fbefc345360aafe2e067514929"}, + {file = "kiwisolver-1.4.5-pp39-pypy39_pp73-macosx_10_9_x86_64.whl", hash = "sha256:e368f200bbc2e4f905b8e71eb38b3c04333bddaa6a2464a6355487b02bb7fb09"}, + {file = "kiwisolver-1.4.5-pp39-pypy39_pp73-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e5d706eba36b4c4d5bc6c6377bb6568098765e990cfc21ee16d13963fab7b3e7"}, + {file = "kiwisolver-1.4.5-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:85267bd1aa8880a9c88a8cb71e18d3d64d2751a790e6ca6c27b8ccc724bcd5ad"}, + {file = "kiwisolver-1.4.5-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:210ef2c3a1f03272649aff1ef992df2e724748918c4bc2d5a90352849eb40bea"}, + {file = "kiwisolver-1.4.5-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:11d011a7574eb3b82bcc9c1a1d35c1d7075677fdd15de527d91b46bd35e935ee"}, + {file = "kiwisolver-1.4.5.tar.gz", hash = "sha256:e57e563a57fb22a142da34f38acc2fc1a5c864bc29ca1517a88abc963e60d6ec"}, +] + [[package]] name = "markupsafe" version = "2.1.5" @@ -280,6 +587,58 @@ files = [ {file = "MarkupSafe-2.1.5.tar.gz", hash = "sha256:d283d37a890ba4c1ae73ffadf8046435c76e7bc2247bbb63c00bd1a709c6544b"}, ] +[[package]] +name = "matplotlib" +version = "3.9.0" +description = "Python plotting package" +optional = false +python-versions = ">=3.9" +files = [ + {file = "matplotlib-3.9.0-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:2bcee1dffaf60fe7656183ac2190bd630842ff87b3153afb3e384d966b57fe56"}, + {file = "matplotlib-3.9.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:3f988bafb0fa39d1074ddd5bacd958c853e11def40800c5824556eb630f94d3b"}, + {file = "matplotlib-3.9.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fe428e191ea016bb278758c8ee82a8129c51d81d8c4bc0846c09e7e8e9057241"}, + {file = "matplotlib-3.9.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:eaf3978060a106fab40c328778b148f590e27f6fa3cd15a19d6892575bce387d"}, + {file = "matplotlib-3.9.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:2e7f03e5cbbfacdd48c8ea394d365d91ee8f3cae7e6ec611409927b5ed997ee4"}, + {file = "matplotlib-3.9.0-cp310-cp310-win_amd64.whl", hash = "sha256:13beb4840317d45ffd4183a778685e215939be7b08616f431c7795276e067463"}, + {file = "matplotlib-3.9.0-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:063af8587fceeac13b0936c42a2b6c732c2ab1c98d38abc3337e430e1ff75e38"}, + {file = "matplotlib-3.9.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:9a2fa6d899e17ddca6d6526cf6e7ba677738bf2a6a9590d702c277204a7c6152"}, + {file = "matplotlib-3.9.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:550cdda3adbd596078cca7d13ed50b77879104e2e46392dcd7c75259d8f00e85"}, + {file = "matplotlib-3.9.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:76cce0f31b351e3551d1f3779420cf8f6ec0d4a8cf9c0237a3b549fd28eb4abb"}, + {file = "matplotlib-3.9.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:c53aeb514ccbbcbab55a27f912d79ea30ab21ee0531ee2c09f13800efb272674"}, + {file = "matplotlib-3.9.0-cp311-cp311-win_amd64.whl", hash = "sha256:a5be985db2596d761cdf0c2eaf52396f26e6a64ab46bd8cd810c48972349d1be"}, + {file = "matplotlib-3.9.0-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:c79f3a585f1368da6049318bdf1f85568d8d04b2e89fc24b7e02cc9b62017382"}, + {file = "matplotlib-3.9.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:bdd1ecbe268eb3e7653e04f451635f0fb0f77f07fd070242b44c076c9106da84"}, + {file = "matplotlib-3.9.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d38e85a1a6d732f645f1403ce5e6727fd9418cd4574521d5803d3d94911038e5"}, + {file = "matplotlib-3.9.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0a490715b3b9984fa609116481b22178348c1a220a4499cda79132000a79b4db"}, + {file = "matplotlib-3.9.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:8146ce83cbc5dc71c223a74a1996d446cd35cfb6a04b683e1446b7e6c73603b7"}, + {file = "matplotlib-3.9.0-cp312-cp312-win_amd64.whl", hash = "sha256:d91a4ffc587bacf5c4ce4ecfe4bcd23a4b675e76315f2866e588686cc97fccdf"}, + {file = "matplotlib-3.9.0-cp39-cp39-macosx_10_12_x86_64.whl", hash = "sha256:616fabf4981a3b3c5a15cd95eba359c8489c4e20e03717aea42866d8d0465956"}, + {file = "matplotlib-3.9.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:cd53c79fd02f1c1808d2cfc87dd3cf4dbc63c5244a58ee7944497107469c8d8a"}, + {file = "matplotlib-3.9.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:06a478f0d67636554fa78558cfbcd7b9dba85b51f5c3b5a0c9be49010cf5f321"}, + {file = "matplotlib-3.9.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:81c40af649d19c85f8073e25e5806926986806fa6d54be506fbf02aef47d5a89"}, + {file = "matplotlib-3.9.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:52146fc3bd7813cc784562cb93a15788be0b2875c4655e2cc6ea646bfa30344b"}, + {file = "matplotlib-3.9.0-cp39-cp39-win_amd64.whl", hash = "sha256:0fc51eaa5262553868461c083d9adadb11a6017315f3a757fc45ec6ec5f02888"}, + {file = "matplotlib-3.9.0-pp39-pypy39_pp73-macosx_10_12_x86_64.whl", hash = "sha256:bd4f2831168afac55b881db82a7730992aa41c4f007f1913465fb182d6fb20c0"}, + {file = "matplotlib-3.9.0-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:290d304e59be2b33ef5c2d768d0237f5bd132986bdcc66f80bc9bcc300066a03"}, + {file = "matplotlib-3.9.0-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7ff2e239c26be4f24bfa45860c20ffccd118d270c5b5d081fa4ea409b5469fcd"}, + {file = "matplotlib-3.9.0-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:af4001b7cae70f7eaacfb063db605280058246de590fa7874f00f62259f2df7e"}, + {file = "matplotlib-3.9.0.tar.gz", hash = "sha256:e6d29ea6c19e34b30fb7d88b7081f869a03014f66fe06d62cc77d5a6ea88ed7a"}, +] + +[package.dependencies] +contourpy = ">=1.0.1" +cycler = ">=0.10" +fonttools = ">=4.22.0" +kiwisolver = ">=1.3.1" +numpy = ">=1.23" +packaging = ">=20.0" +pillow = ">=8" +pyparsing = ">=2.3.1" +python-dateutil = ">=2.7" + +[package.extras] +dev = ["meson-python (>=0.13.1)", "numpy (>=1.25)", "pybind11 (>=2.6)", "setuptools (>=64)", "setuptools_scm (>=7)"] + [[package]] name = "mkl" version = "2021.4.0" @@ -569,15 +928,88 @@ files = [ [[package]] name = "packaging" -version = "24.0" +version = "24.1" description = "Core utilities for Python packages" optional = false -python-versions = ">=3.7" +python-versions = ">=3.8" files = [ - {file = "packaging-24.0-py3-none-any.whl", hash = "sha256:2ddfb553fdf02fb784c234c7ba6ccc288296ceabec964ad2eae3777778130bc5"}, - {file = "packaging-24.0.tar.gz", hash = "sha256:eb82c5e3e56209074766e6885bb04b8c38a0c015d0a30036ebe7ece34c9989e9"}, + {file = "packaging-24.1-py3-none-any.whl", hash = "sha256:5b8f2217dbdbd2f7f384c41c628544e6d52f2d0f53c6d0c3ea61aa5d1d7ff124"}, + {file = "packaging-24.1.tar.gz", hash = "sha256:026ed72c8ed3fcce5bf8950572258698927fd1dbda10a5e981cdf0ac37f4f002"}, ] +[[package]] +name = "pandas" +version = "2.2.2" +description = "Powerful data structures for data analysis, time series, and statistics" +optional = false +python-versions = ">=3.9" +files = [ + {file = "pandas-2.2.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:90c6fca2acf139569e74e8781709dccb6fe25940488755716d1d354d6bc58bce"}, + {file = "pandas-2.2.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:c7adfc142dac335d8c1e0dcbd37eb8617eac386596eb9e1a1b77791cf2498238"}, + {file = "pandas-2.2.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4abfe0be0d7221be4f12552995e58723c7422c80a659da13ca382697de830c08"}, + {file = "pandas-2.2.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8635c16bf3d99040fdf3ca3db669a7250ddf49c55dc4aa8fe0ae0fa8d6dcc1f0"}, + {file = "pandas-2.2.2-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:40ae1dffb3967a52203105a077415a86044a2bea011b5f321c6aa64b379a3f51"}, + {file = "pandas-2.2.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:8e5a0b00e1e56a842f922e7fae8ae4077aee4af0acb5ae3622bd4b4c30aedf99"}, + {file = "pandas-2.2.2-cp310-cp310-win_amd64.whl", hash = "sha256:ddf818e4e6c7c6f4f7c8a12709696d193976b591cc7dc50588d3d1a6b5dc8772"}, + {file = "pandas-2.2.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:696039430f7a562b74fa45f540aca068ea85fa34c244d0deee539cb6d70aa288"}, + {file = "pandas-2.2.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:8e90497254aacacbc4ea6ae5e7a8cd75629d6ad2b30025a4a8b09aa4faf55151"}, + {file = "pandas-2.2.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:58b84b91b0b9f4bafac2a0ac55002280c094dfc6402402332c0913a59654ab2b"}, + {file = "pandas-2.2.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6d2123dc9ad6a814bcdea0f099885276b31b24f7edf40f6cdbc0912672e22eee"}, + {file = "pandas-2.2.2-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:2925720037f06e89af896c70bca73459d7e6a4be96f9de79e2d440bd499fe0db"}, + {file = "pandas-2.2.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:0cace394b6ea70c01ca1595f839cf193df35d1575986e484ad35c4aeae7266c1"}, + {file = "pandas-2.2.2-cp311-cp311-win_amd64.whl", hash = "sha256:873d13d177501a28b2756375d59816c365e42ed8417b41665f346289adc68d24"}, + {file = "pandas-2.2.2-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:9dfde2a0ddef507a631dc9dc4af6a9489d5e2e740e226ad426a05cabfbd7c8ef"}, + {file = "pandas-2.2.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:e9b79011ff7a0f4b1d6da6a61aa1aa604fb312d6647de5bad20013682d1429ce"}, + {file = "pandas-2.2.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1cb51fe389360f3b5a4d57dbd2848a5f033350336ca3b340d1c53a1fad33bcad"}, + {file = "pandas-2.2.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:eee3a87076c0756de40b05c5e9a6069c035ba43e8dd71c379e68cab2c20f16ad"}, + {file = "pandas-2.2.2-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:3e374f59e440d4ab45ca2fffde54b81ac3834cf5ae2cdfa69c90bc03bde04d76"}, + {file = "pandas-2.2.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:43498c0bdb43d55cb162cdc8c06fac328ccb5d2eabe3cadeb3529ae6f0517c32"}, + {file = "pandas-2.2.2-cp312-cp312-win_amd64.whl", hash = "sha256:d187d355ecec3629624fccb01d104da7d7f391db0311145817525281e2804d23"}, + {file = "pandas-2.2.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:0ca6377b8fca51815f382bd0b697a0814c8bda55115678cbc94c30aacbb6eff2"}, + {file = "pandas-2.2.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:9057e6aa78a584bc93a13f0a9bf7e753a5e9770a30b4d758b8d5f2a62a9433cd"}, + {file = "pandas-2.2.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:001910ad31abc7bf06f49dcc903755d2f7f3a9186c0c040b827e522e9cef0863"}, + {file = "pandas-2.2.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:66b479b0bd07204e37583c191535505410daa8df638fd8e75ae1b383851fe921"}, + {file = "pandas-2.2.2-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:a77e9d1c386196879aa5eb712e77461aaee433e54c68cf253053a73b7e49c33a"}, + {file = "pandas-2.2.2-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:92fd6b027924a7e178ac202cfbe25e53368db90d56872d20ffae94b96c7acc57"}, + {file = "pandas-2.2.2-cp39-cp39-win_amd64.whl", hash = "sha256:640cef9aa381b60e296db324337a554aeeb883ead99dc8f6c18e81a93942f5f4"}, + {file = "pandas-2.2.2.tar.gz", hash = "sha256:9e79019aba43cb4fda9e4d983f8e88ca0373adbb697ae9c6c43093218de28b54"}, +] + +[package.dependencies] +numpy = [ + {version = ">=1.22.4", markers = "python_version < \"3.11\""}, + {version = ">=1.23.2", markers = "python_version == \"3.11\""}, + {version = ">=1.26.0", markers = "python_version >= \"3.12\""}, +] +python-dateutil = ">=2.8.2" +pytz = ">=2020.1" +tzdata = ">=2022.7" + +[package.extras] +all = ["PyQt5 (>=5.15.9)", "SQLAlchemy (>=2.0.0)", "adbc-driver-postgresql (>=0.8.0)", "adbc-driver-sqlite (>=0.8.0)", "beautifulsoup4 (>=4.11.2)", "bottleneck (>=1.3.6)", "dataframe-api-compat (>=0.1.7)", "fastparquet (>=2022.12.0)", "fsspec (>=2022.11.0)", "gcsfs (>=2022.11.0)", "html5lib (>=1.1)", "hypothesis (>=6.46.1)", "jinja2 (>=3.1.2)", "lxml (>=4.9.2)", "matplotlib (>=3.6.3)", "numba (>=0.56.4)", "numexpr (>=2.8.4)", "odfpy (>=1.4.1)", "openpyxl (>=3.1.0)", "pandas-gbq (>=0.19.0)", "psycopg2 (>=2.9.6)", "pyarrow (>=10.0.1)", "pymysql (>=1.0.2)", "pyreadstat (>=1.2.0)", "pytest (>=7.3.2)", "pytest-xdist (>=2.2.0)", "python-calamine (>=0.1.7)", "pyxlsb (>=1.0.10)", "qtpy (>=2.3.0)", "s3fs (>=2022.11.0)", "scipy (>=1.10.0)", "tables (>=3.8.0)", "tabulate (>=0.9.0)", "xarray (>=2022.12.0)", "xlrd (>=2.0.1)", "xlsxwriter (>=3.0.5)", "zstandard (>=0.19.0)"] +aws = ["s3fs (>=2022.11.0)"] +clipboard = ["PyQt5 (>=5.15.9)", "qtpy (>=2.3.0)"] +compression = ["zstandard (>=0.19.0)"] +computation = ["scipy (>=1.10.0)", "xarray (>=2022.12.0)"] +consortium-standard = ["dataframe-api-compat (>=0.1.7)"] +excel = ["odfpy (>=1.4.1)", "openpyxl (>=3.1.0)", "python-calamine (>=0.1.7)", "pyxlsb (>=1.0.10)", "xlrd (>=2.0.1)", "xlsxwriter (>=3.0.5)"] +feather = ["pyarrow (>=10.0.1)"] +fss = ["fsspec (>=2022.11.0)"] +gcp = ["gcsfs (>=2022.11.0)", "pandas-gbq (>=0.19.0)"] +hdf5 = ["tables (>=3.8.0)"] +html = ["beautifulsoup4 (>=4.11.2)", "html5lib (>=1.1)", "lxml (>=4.9.2)"] +mysql = ["SQLAlchemy (>=2.0.0)", "pymysql (>=1.0.2)"] +output-formatting = ["jinja2 (>=3.1.2)", "tabulate (>=0.9.0)"] +parquet = ["pyarrow (>=10.0.1)"] +performance = ["bottleneck (>=1.3.6)", "numba (>=0.56.4)", "numexpr (>=2.8.4)"] +plot = ["matplotlib (>=3.6.3)"] +postgresql = ["SQLAlchemy (>=2.0.0)", "adbc-driver-postgresql (>=0.8.0)", "psycopg2 (>=2.9.6)"] +pyarrow = ["pyarrow (>=10.0.1)"] +spss = ["pyreadstat (>=1.2.0)"] +sql-other = ["SQLAlchemy (>=2.0.0)", "adbc-driver-postgresql (>=0.8.0)", "adbc-driver-sqlite (>=0.8.0)"] +test = ["hypothesis (>=6.46.1)", "pytest (>=7.3.2)", "pytest-xdist (>=2.2.0)"] +xml = ["lxml (>=4.9.2)"] + [[package]] name = "parsial" version = "0.1.0" @@ -607,6 +1039,92 @@ files = [ {file = "pathspec-0.12.1.tar.gz", hash = "sha256:a482d51503a1ab33b1c67a6c3813a26953dbdc71c31dacaef9a838c4e29f5712"}, ] +[[package]] +name = "pillow" +version = "10.3.0" +description = "Python Imaging Library (Fork)" +optional = false +python-versions = ">=3.8" +files = [ + {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] +docs = ["furo", "olefile", "sphinx (>=2.4)", "sphinx-copybutton", "sphinx-inline-tabs", "sphinx-removed-in", "sphinxext-opengraph"] +fpx = ["olefile"] +mic = ["olefile"] +tests = ["check-manifest", "coverage", "defusedxml", "markdown2", "olefile", "packaging", "pyroma", "pytest", "pytest-cov", "pytest-timeout"] +typing = ["typing-extensions"] +xmp = ["defusedxml"] + [[package]] name = "platformdirs" version = "4.2.2" @@ -638,6 +1156,34 @@ files = [ dev = ["pre-commit", "tox"] testing = ["pytest", "pytest-benchmark"] +[[package]] +name = "prophet" +version = "1.1.5" +description = "Automatic Forecasting Procedure" +optional = false +python-versions = ">=3.7" +files = [ + {file = "prophet-1.1.5-py3-none-macosx_10_9_x86_64.whl", hash = "sha256:1c522a76ce49264ad140bbebca7d1c7cdc9ebfe718cdcaaf2c289db96e9e03bd"}, + {file = "prophet-1.1.5-py3-none-macosx_11_0_arm64.whl", hash = "sha256:24e7ce972f95068af7074dbad5846f83485257366b023af44e86716fb438bb0d"}, + {file = "prophet-1.1.5-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cd5991a86dc0a86e09da550fb072ade8714dc17741da0231907d972429476a54"}, + {file = "prophet-1.1.5-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7761d127a41d3434b83d16f27b41cdc71052c7d98c0d6013a227a0bc55347da9"}, + {file = "prophet-1.1.5-py3-none-win_amd64.whl", hash = "sha256:337d9cf7728b8d3ef18a8304aa90c079f206aead15d9f0beff87cad8050553ff"}, + {file = "prophet-1.1.5.tar.gz", hash = "sha256:80973c0b8a22d835bfa9d6665a78ebc63115135eaef0f73b46ee14e9bad3ca1a"}, +] + +[package.dependencies] +cmdstanpy = ">=1.0.4" +holidays = ">=0.25" +importlib-resources = "*" +matplotlib = ">=2.0.0" +numpy = ">=1.15.4" +pandas = ">=1.0.4" +tqdm = ">=4.36.1" + +[package.extras] +dev = ["jupyterlab", "nbconvert", "plotly", "pytest", "setuptools (>=64)", "wheel"] +parallel = ["dask[dataframe]", "distributed"] + [[package]] name = "py-nillion-client" version = "0.2.1" @@ -654,6 +1200,20 @@ files = [ [package.extras] dev = ["maturin (==1.4.0)", "maturin[zig] (==1.4.0)", "pytest (==8.0.0)", "pyyaml (==6.0.1)"] +[[package]] +name = "pyparsing" +version = "3.1.2" +description = "pyparsing module - Classes and methods to define and execute parsing grammars" +optional = false +python-versions = ">=3.6.8" +files = [ + {file = "pyparsing-3.1.2-py3-none-any.whl", hash = "sha256:f9db75911801ed778fe61bb643079ff86601aca99fcae6345aa67292038fb742"}, + {file = "pyparsing-3.1.2.tar.gz", hash = "sha256:a1bac0ce561155ecc3ed78ca94d3c9378656ad4c94c1270de543f621420f94ad"}, +] + +[package.extras] +diagrams = ["jinja2", "railroad-diagrams"] + [[package]] name = "pytest" version = "8.2.2" @@ -676,6 +1236,31 @@ tomli = {version = ">=1", markers = "python_version < \"3.11\""} [package.extras] dev = ["argcomplete", "attrs (>=19.2)", "hypothesis (>=3.56)", "mock", "pygments (>=2.7.2)", "requests", "setuptools", "xmlschema"] +[[package]] +name = "python-dateutil" +version = "2.9.0.post0" +description = "Extensions to the standard Python datetime module" +optional = false +python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,>=2.7" +files = [ + {file = "python-dateutil-2.9.0.post0.tar.gz", hash = "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3"}, + {file = "python_dateutil-2.9.0.post0-py2.py3-none-any.whl", hash = "sha256:a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427"}, +] + +[package.dependencies] +six = ">=1.5" + +[[package]] +name = "pytz" +version = "2024.1" +description = "World timezone definitions, modern and historical" +optional = false +python-versions = "*" +files = [ + {file = "pytz-2024.1-py2.py3-none-any.whl", hash = "sha256:328171f4e3623139da4983451950b28e95ac706e13f3f2630a879749e7a8b319"}, + {file = "pytz-2024.1.tar.gz", hash = "sha256:2a29735ea9c18baf14b448846bde5a48030ed267578472d8955cd0e7443a9812"}, +] + [[package]] name = "richreports" version = "0.2.0" @@ -792,6 +1377,24 @@ files = [ {file = "six-1.16.0.tar.gz", hash = "sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926"}, ] +[[package]] +name = "stanio" +version = "0.5.0" +description = "Utilities for preparing Stan inputs and processing Stan outputs" +optional = false +python-versions = ">=3.8" +files = [ + {file = "stanio-0.5.0-py3-none-any.whl", hash = "sha256:412f8ede127968be51e074be6a6ca1e66d5a8ed50805e2fed89b76868e7332ae"}, + {file = "stanio-0.5.0.tar.gz", hash = "sha256:dee429314d965f390181afa8ff7ff8144446eeb58cc2547ccc108ba73e6744e2"}, +] + +[package.dependencies] +numpy = "*" + +[package.extras] +test = ["pandas", "pytest", "pytest-cov"] +ujson = ["ujson (>=5.5.0)"] + [[package]] name = "sympy" version = "1.12.1" @@ -895,6 +1498,26 @@ typing-extensions = ">=4.8.0" opt-einsum = ["opt-einsum (>=3.3)"] optree = ["optree (>=0.9.1)"] +[[package]] +name = "tqdm" +version = "4.66.4" +description = "Fast, Extensible Progress Meter" +optional = false +python-versions = ">=3.7" +files = [ + {file = "tqdm-4.66.4-py3-none-any.whl", hash = "sha256:b75ca56b413b030bc3f00af51fd2c1a1a5eac6a0c1cca83cbb37a5c52abce644"}, + {file = "tqdm-4.66.4.tar.gz", hash = "sha256:e4d936c9de8727928f3be6079590e97d9abfe8d39a590be678eb5919ffc186bb"}, +] + +[package.dependencies] +colorama = {version = "*", markers = "platform_system == \"Windows\""} + +[package.extras] +dev = ["pytest (>=6)", "pytest-cov", "pytest-timeout", "pytest-xdist"] +notebook = ["ipywidgets (>=6)"] +slack = ["slack-sdk"] +telegram = ["requests"] + [[package]] name = "triton" version = "2.3.1" @@ -920,16 +1543,27 @@ tutorials = ["matplotlib", "pandas", "tabulate", "torch"] [[package]] name = "typing-extensions" -version = "4.12.1" +version = "4.12.2" description = "Backported and Experimental Type Hints for Python 3.8+" optional = false python-versions = ">=3.8" files = [ - {file = "typing_extensions-4.12.1-py3-none-any.whl", hash = "sha256:6024b58b69089e5a89c347397254e35f1bf02a907728ec7fee9bf0fe837d203a"}, - {file = "typing_extensions-4.12.1.tar.gz", hash = "sha256:915f5e35ff76f56588223f15fdd5938f9a1cf9195c0de25130c627e4d597f6d1"}, + {file = "typing_extensions-4.12.2-py3-none-any.whl", hash = "sha256:04e5ca0351e0f3f85c6853954072df659d0d13fac324d0072316b67d7794700d"}, + {file = "typing_extensions-4.12.2.tar.gz", hash = "sha256:1a7ead55c7e559dd4dee8856e3a88b41225abfe1ce8df57b7c13915fe121ffb8"}, +] + +[[package]] +name = "tzdata" +version = "2024.1" +description = "Provider of IANA time zone data" +optional = false +python-versions = ">=2" +files = [ + {file = "tzdata-2024.1-py2.py3-none-any.whl", hash = "sha256:9068bc196136463f5245e51efda838afa15aaeca9903f49050dfa2679db4d252"}, + {file = "tzdata-2024.1.tar.gz", hash = "sha256:2674120f8d891909751c38abcdfd386ac0a5a1127954fbc332af6b5ceae07efd"}, ] [metadata] lock-version = "2.0" python-versions = "^3.10" -content-hash = "6e39c59ae056502f6a0512ca45e3f7502694d1c91518b37d223f390baf51e712" +content-hash = "5fbd199d20ff748b0f4b8b98c3b1b6967824e9bcf2b87300b112f37572412d87" diff --git a/pyproject.toml b/pyproject.toml index 5589266..47efe38 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -19,6 +19,7 @@ nada-algebra = "^0.3.2" [tool.poetry.group.dev.dependencies] pytest = "^8.2.0" black = "^24.4.2" +pandas = "^2.2.2" [build-system] requires = ["poetry-core"] From a771eed0cbcb6702bca398a7f29b8aadbb3de4a7 Mon Sep 17 00:00:00 2001 From: Mathias Leys Date: Tue, 11 Jun 2024 18:11:49 +0100 Subject: [PATCH 07/28] Update examples --- examples/README.md | 1 + examples/complex_model/network/compute.py | 8 +- examples/neural_net/network/compute.py | 8 +- examples/time_series/nada-project.toml | 7 + examples/time_series/network/compute.py | 189 +++++++++++++++++ .../network/helpers/nillion_client_helper.py | 12 ++ .../network/helpers/nillion_keypath_helper.py | 10 + .../helpers/nillion_payments_helper.py | 12 ++ examples/time_series/src/main.py | 35 ++++ examples/time_series/tests/time_series.yaml | 192 ++++++++++++++++++ 10 files changed, 472 insertions(+), 2 deletions(-) create mode 100644 examples/time_series/nada-project.toml create mode 100644 examples/time_series/network/compute.py create mode 100644 examples/time_series/network/helpers/nillion_client_helper.py create mode 100644 examples/time_series/network/helpers/nillion_keypath_helper.py create mode 100644 examples/time_series/network/helpers/nillion_payments_helper.py create mode 100644 examples/time_series/src/main.py create mode 100644 examples/time_series/tests/time_series.yaml diff --git a/examples/README.md b/examples/README.md index 1ace6fc..74161c1 100644 --- a/examples/README.md +++ b/examples/README.md @@ -5,6 +5,7 @@ The following are the currently available examples: - [Linear Regression](./linear_regression): shows how to run a linear regression using Nada AI - [Neural Net](./neural_net): shows how to build & run a simple Feed-Forward Neural Net (both linear layers & activations) using Nada AI - [Complex Model](./complex_model): shows how to build more intricate model architectures using Nada AI. Contains convolutions, pooling operations, linear layers and activations +- [Time Series](./time_series): shows how to run a Facebook Prophet time series forecasting model using Nada AI The Nada program source code is stored in `src/main.py`. diff --git a/examples/complex_model/network/compute.py b/examples/complex_model/network/compute.py index 8e53180..61f126d 100644 --- a/examples/complex_model/network/compute.py +++ b/examples/complex_model/network/compute.py @@ -189,7 +189,13 @@ def forward(self, x: torch.Tensor) -> torch.Tensor: ) # Sort & rescale the obtained results by the quantization scale (here: 16) - outputs = [result[1] / 2**16 for result in sorted(result.items())] + outputs = outputs = [ + result[1] / 2**16 + for result in sorted( + result.items(), + key=lambda x: int(x[0].replace("my_output", "").replace("_", "")), + ) + ] print(f"🖥️ The result is {outputs}") diff --git a/examples/neural_net/network/compute.py b/examples/neural_net/network/compute.py index 677682a..0d5c771 100644 --- a/examples/neural_net/network/compute.py +++ b/examples/neural_net/network/compute.py @@ -167,7 +167,13 @@ def forward(self, x: na.NadaArray) -> na.NadaArray: ) # Sort & rescale the obtained results by the quantization scale (here: 16) - outputs = [result[1] / 2**16 for result in sorted(result.items())] + outputs = [ + result[1] / 2**16 + for result in sorted( + result.items(), + key=lambda x: int(x[0].replace("my_output", "").replace("_", "")), + ) + ] print(f"🖥️ The result is {outputs}") diff --git a/examples/time_series/nada-project.toml b/examples/time_series/nada-project.toml new file mode 100644 index 0000000..15f098a --- /dev/null +++ b/examples/time_series/nada-project.toml @@ -0,0 +1,7 @@ +name = "time_series" +version = "0.1.0" +authors = [""] + +[[programs]] +path = "src/main.py" +prime_size = 128 diff --git a/examples/time_series/network/compute.py b/examples/time_series/network/compute.py new file mode 100644 index 0000000..ef1c293 --- /dev/null +++ b/examples/time_series/network/compute.py @@ -0,0 +1,189 @@ +import asyncio +import py_nillion_client as nillion +import os +import sys +import time +import numpy as np +import nada_algebra as na +import pandas as pd +from nada_ai import ProphetClient +from prophet import Prophet +from dotenv import load_dotenv + +# Add the parent directory to the system path to import modules from it +sys.path.append(os.path.abspath(os.path.join(os.path.dirname(__file__), "../.."))) + +# Import helper functions for creating nillion client and getting keys +from neural_net.network.helpers.nillion_client_helper import create_nillion_client +from neural_net.network.helpers.nillion_keypath_helper import ( + getUserKeyFromFile, + getNodeKeyFromFile, +) +import nada_algebra.client as na_client + +# Load environment variables from a .env file +load_dotenv() + + +# Decorator function to measure and log the execution time of asynchronous functions +def async_timer(file_path): + def decorator(func): + async def wrapper(*args, **kwargs): + start_time = time.time() + result = await func(*args, **kwargs) + end_time = time.time() + elapsed_time = end_time - start_time + + # Log the execution time to a file + with open(file_path, "a") as file: + file.write(f"{elapsed_time:.6f},\n") + return result + + return wrapper + + return decorator + + +# Asynchronous function to store a program on the nillion client +@async_timer("bench/store_program.txt") +async def store_program(client, user_id, cluster_id, program_name, program_mir_path): + action_id = await client.store_program(cluster_id, program_name, program_mir_path) + program_id = f"{user_id}/{program_name}" + print("Stored program. action_id:", action_id) + print("Stored program_id:", program_id) + return program_id + + +# Asynchronous function to store secrets on the nillion client +@async_timer("bench/store_secrets.txt") +async def store_secrets(client, cluster_id, program_id, party_id, party_name, secrets): + secret_bindings = nillion.ProgramBindings(program_id) + secret_bindings.add_input_party(party_name, party_id) + + # Store the secret for the specified party + store_id = await client.store_secrets(cluster_id, secret_bindings, secrets, None) + return store_id + + +# Asynchronous function to perform computation on the nillion client +@async_timer("bench/compute.txt") +async def compute( + client, cluster_id, compute_bindings, store_ids, computation_time_secrets +): + compute_id = await client.compute( + cluster_id, + compute_bindings, + store_ids, + computation_time_secrets, + nillion.PublicVariables({}), + ) + + # Monitor and print the computation result + print(f"The computation was sent to the network. compute_id: {compute_id}") + while True: + compute_event = await client.next_compute_event() + if isinstance(compute_event, nillion.ComputeFinishedEvent): + print(f"✅ Compute complete for compute_id {compute_event.uuid}") + return compute_event.result.value + + +# Main asynchronous function to coordinate the process +async def main(): + cluster_id = os.getenv("NILLION_CLUSTER_ID") + userkey = getUserKeyFromFile(os.getenv("NILLION_USERKEY_PATH_PARTY_1")) + nodekey = getNodeKeyFromFile(os.getenv("NILLION_NODEKEY_PATH_PARTY_1")) + client = create_nillion_client(userkey, nodekey) + party_id = client.party_id + user_id = client.user_id + party_names = na_client.parties(2) + program_name = "main" + program_mir_path = f"./target/{program_name}.nada.bin" + + if not os.path.exists("bench"): + os.mkdir("bench") + + na.set_log_scale(50) + + # Store the program + program_id = await store_program( + client, user_id, cluster_id, program_name, program_mir_path + ) + + # Train prophet model + model = Prophet() + + ds = pd.date_range("2024-05-01", "2024-05-17").tolist() + y = np.arange(1, 18).tolist() + + fit_model = model.fit(df=pd.DataFrame({"ds": ds, "y": y})) + + print("Model params are:", fit_model.params) + print("Number of detected changepoints:", fit_model.n_changepoints) + + # Create and store model secrets via ModelClient + model_client = ProphetClient(fit_model) + model_secrets = nillion.Secrets( + model_client.export_state_as_secrets("my_prophet", na.SecretRational) + ) + + model_store_id = await store_secrets( + client, cluster_id, program_id, party_id, party_names[0], model_secrets + ) + + # Store inputs to perform inference for + future_df = fit_model.make_future_dataframe(periods=3) + inference_ds = fit_model.setup_dataframe(future_df.copy()) + + my_input = {} + my_input.update( + na_client.array(inference_ds["floor"].to_numpy(), "floor", na.SecretRational) + ) + my_input.update( + na_client.array(inference_ds["t"].to_numpy(), "t", na.SecretRational) + ) + + input_secrets = nillion.Secrets(my_input) + + data_store_id = await store_secrets( + client, cluster_id, program_id, party_id, party_names[1], input_secrets + ) + + # Set up the compute bindings for the parties + compute_bindings = nillion.ProgramBindings(program_id) + [ + compute_bindings.add_input_party(party_name, party_id) + for party_name in party_names + ] + compute_bindings.add_output_party(party_names[1], party_id) + + print(f"Computing using program {program_id}") + print(f"Use secret store_id: {model_store_id} {data_store_id}") + + # Perform the computation and return the result + result = await compute( + client, + cluster_id, + compute_bindings, + [model_store_id, data_store_id], + nillion.Secrets({}), + ) + + # Sort & rescale the obtained results by the quantization scale + outputs = [ + result[1] / 2**50 + for result in sorted( + result.items(), + key=lambda x: int(x[0].replace("my_output", "").replace("_", "")), + ) + ] + + print(f"🖥️ The result is {outputs}") + + expected = fit_model.predict(inference_ds)["yhat"].to_numpy() + print(f"🖥️ VS expected plain-text result {expected}") + return result + + +# Run the main function if the script is executed directly +if __name__ == "__main__": + asyncio.run(main()) diff --git a/examples/time_series/network/helpers/nillion_client_helper.py b/examples/time_series/network/helpers/nillion_client_helper.py new file mode 100644 index 0000000..5247aee --- /dev/null +++ b/examples/time_series/network/helpers/nillion_client_helper.py @@ -0,0 +1,12 @@ +import os +import py_nillion_client as nillion +from helpers.nillion_payments_helper import create_payments_config + + +def create_nillion_client(userkey, nodekey): + bootnodes = [os.getenv("NILLION_BOOTNODE_MULTIADDRESS")] + payments_config = create_payments_config() + + return nillion.NillionClient( + nodekey, bootnodes, nillion.ConnectionMode.relay(), userkey, payments_config + ) diff --git a/examples/time_series/network/helpers/nillion_keypath_helper.py b/examples/time_series/network/helpers/nillion_keypath_helper.py new file mode 100644 index 0000000..4a45413 --- /dev/null +++ b/examples/time_series/network/helpers/nillion_keypath_helper.py @@ -0,0 +1,10 @@ +import os +import py_nillion_client as nillion + + +def getUserKeyFromFile(userkey_filepath): + return nillion.UserKey.from_file(userkey_filepath) + + +def getNodeKeyFromFile(nodekey_filepath): + return nillion.NodeKey.from_file(nodekey_filepath) diff --git a/examples/time_series/network/helpers/nillion_payments_helper.py b/examples/time_series/network/helpers/nillion_payments_helper.py new file mode 100644 index 0000000..f68b33c --- /dev/null +++ b/examples/time_series/network/helpers/nillion_payments_helper.py @@ -0,0 +1,12 @@ +import os +import py_nillion_client as nillion + + +def create_payments_config(): + return nillion.PaymentsConfig( + os.getenv("NILLION_BLOCKCHAIN_RPC_ENDPOINT"), + os.getenv("NILLION_WALLET_PRIVATE_KEY"), + int(os.getenv("NILLION_CHAIN_ID")), + os.getenv("NILLION_PAYMENTS_SC_ADDRESS"), + os.getenv("NILLION_BLINDING_FACTORS_MANAGER_SC_ADDRESS"), + ) diff --git a/examples/time_series/src/main.py b/examples/time_series/src/main.py new file mode 100644 index 0000000..b0347c4 --- /dev/null +++ b/examples/time_series/src/main.py @@ -0,0 +1,35 @@ +import nada_algebra as na +import numpy as np +from nada_ai.time_series import Prophet + + +def nada_main(): + na.set_log_scale(50) + + # Step 1: We use Nada Algebra wrapper to create "Party0" and "Party1" + parties = na.parties(2) + + # Step 2: Instantiate model object + my_prophet = Prophet( + n_changepoints=12, # NOTE: this is a learned hyperparameter + yearly_seasonality=False, + weekly_seasonality=True, + daily_seasonality=False, + ) + + # Step 3: Load model weights from Nillion network by passing model name (acts as ID) + # In this examples Party0 provides the model and Party1 runs inference + my_prophet.load_state_from_network("my_prophet", parties[0], na.SecretRational) + + # Step 4: Load input data to be used for inference (provided by Party1) + dates = np.arange(np.datetime64("2024-05-01"), np.datetime64("2024-05-21")) + + floor = na.array((20,), parties[1], "floor", na.SecretRational) + t = na.array((20,), parties[1], "t", na.SecretRational) + + # Step 5: Compute inference + # Note: completely equivalent to `my_model.forward(...)` or `model.predict(...)` + result = my_prophet(dates, floor, t) + + # Step 6: We can use result.output() to produce the output for Party1 and variable name "my_output" + return result.output(parties[1], "my_output") diff --git a/examples/time_series/tests/time_series.yaml b/examples/time_series/tests/time_series.yaml new file mode 100644 index 0000000..7497d2b --- /dev/null +++ b/examples/time_series/tests/time_series.yaml @@ -0,0 +1,192 @@ +--- +program: main +inputs: + secrets: + my_prophet_changepoints_t_8: + SecretInteger: "3" + my_prophet_changepoints_t_11: + SecretInteger: "3" + floor_17: + SecretInteger: "3" + floor_4: + SecretInteger: "3" + my_prophet_beta_0_3: + SecretInteger: "3" + t_7: + SecretInteger: "3" + t_3: + SecretInteger: "3" + my_prophet_beta_0_0: + SecretInteger: "3" + t_1: + SecretInteger: "3" + floor_5: + SecretInteger: "3" + my_prophet_changepoints_t_6: + SecretInteger: "3" + floor_16: + SecretInteger: "3" + my_prophet_y_scale_0: + SecretInteger: "3" + floor_2: + SecretInteger: "3" + floor_19: + SecretInteger: "3" + my_prophet_beta_0_5: + SecretInteger: "3" + my_prophet_changepoints_t_7: + SecretInteger: "3" + my_prophet_delta_0_5: + SecretInteger: "3" + t_8: + SecretInteger: "3" + floor_18: + SecretInteger: "3" + my_prophet_beta_0_2: + SecretInteger: "3" + floor_10: + SecretInteger: "3" + t_12: + SecretInteger: "3" + my_prophet_delta_0_4: + SecretInteger: "3" + t_14: + SecretInteger: "3" + t_16: + SecretInteger: "3" + t_2: + SecretInteger: "3" + t_5: + SecretInteger: "3" + floor_11: + SecretInteger: "3" + my_prophet_delta_0_2: + SecretInteger: "3" + my_prophet_delta_0_7: + SecretInteger: "3" + my_prophet_k_0_0: + SecretInteger: "3" + my_prophet_changepoints_t_1: + SecretInteger: "3" + t_13: + SecretInteger: "3" + t_18: + SecretInteger: "3" + my_prophet_delta_0_0: + SecretInteger: "3" + my_prophet_changepoints_t_9: + SecretInteger: "3" + floor_13: + SecretInteger: "3" + t_15: + SecretInteger: "3" + t_17: + SecretInteger: "3" + my_prophet_delta_0_6: + SecretInteger: "3" + t_10: + SecretInteger: "3" + my_prophet_changepoints_t_10: + SecretInteger: "3" + my_prophet_beta_0_4: + SecretInteger: "3" + floor_3: + SecretInteger: "3" + my_prophet_changepoints_t_2: + SecretInteger: "3" + t_9: + SecretInteger: "3" + floor_9: + SecretInteger: "3" + t_11: + SecretInteger: "3" + my_prophet_delta_0_3: + SecretInteger: "3" + my_prophet_delta_0_10: + SecretInteger: "3" + my_prophet_changepoints_t_3: + SecretInteger: "3" + floor_1: + SecretInteger: "3" + my_prophet_delta_0_8: + SecretInteger: "3" + t_19: + SecretInteger: "3" + floor_7: + SecretInteger: "3" + floor_0: + SecretInteger: "3" + floor_14: + SecretInteger: "3" + floor_8: + SecretInteger: "3" + my_prophet_changepoints_t_4: + SecretInteger: "3" + floor_12: + SecretInteger: "3" + my_prophet_changepoints_t_0: + SecretInteger: "3" + my_prophet_delta_0_9: + SecretInteger: "3" + my_prophet_m_0_0: + SecretInteger: "3" + my_prophet_changepoints_t_5: + SecretInteger: "3" + t_6: + SecretInteger: "3" + my_prophet_beta_0_1: + SecretInteger: "3" + t_0: + SecretInteger: "3" + floor_15: + SecretInteger: "3" + t_4: + SecretInteger: "3" + my_prophet_delta_0_11: + SecretInteger: "3" + my_prophet_delta_0_1: + SecretInteger: "3" + floor_6: + SecretInteger: "3" + public_variables: {} +expected_outputs: + my_output_1: + SecretInteger: "3" + my_output_16: + SecretInteger: "3" + my_output_17: + SecretInteger: "3" + my_output_14: + SecretInteger: "3" + my_output_19: + SecretInteger: "3" + my_output_3: + SecretInteger: "3" + my_output_2: + SecretInteger: "3" + my_output_0: + SecretInteger: "3" + my_output_11: + SecretInteger: "3" + my_output_10: + SecretInteger: "3" + my_output_5: + SecretInteger: "3" + my_output_6: + SecretInteger: "3" + my_output_7: + SecretInteger: "3" + my_output_12: + SecretInteger: "3" + my_output_9: + SecretInteger: "3" + my_output_4: + SecretInteger: "3" + my_output_8: + SecretInteger: "3" + my_output_15: + SecretInteger: "3" + my_output_18: + SecretInteger: "3" + my_output_13: + SecretInteger: "3" From eee449a5a513bca541c2ac521d28b54d448ecb5e Mon Sep 17 00:00:00 2001 From: Mathias Leys Date: Tue, 11 Jun 2024 18:12:06 +0100 Subject: [PATCH 08/28] Update tests --- tests/test_all_nada.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/tests/test_all_nada.py b/tests/test_all_nada.py index 7db2bbb..a20fcf9 100644 --- a/tests/test_all_nada.py +++ b/tests/test_all_nada.py @@ -13,9 +13,15 @@ "pool", "linear_regression", "end-to-end", + "prophet", ] -EXAMPLES = [] +EXAMPLES = [ + "complex_model", + "linear_regression", + "neural_net", + # "time_series", +] TESTS = [("tests/nada-tests/", test) for test in TESTS] + [ ("examples/" + test, test) for test in EXAMPLES From 50e08539b2c1c076ef6ed4e3dcba86b38cfacbe5 Mon Sep 17 00:00:00 2001 From: Mathias Leys Date: Tue, 11 Jun 2024 18:12:14 +0100 Subject: [PATCH 09/28] Fix prophet bug --- nada_ai/time_series/prophet.py | 44 ++++++++++++++++++++++++++++------ 1 file changed, 37 insertions(+), 7 deletions(-) diff --git a/nada_ai/time_series/prophet.py b/nada_ai/time_series/prophet.py index 1f2ed71..8b43169 100644 --- a/nada_ai/time_series/prophet.py +++ b/nada_ai/time_series/prophet.py @@ -102,7 +102,7 @@ def predict_seasonal_comps( if features is None: continue - comp = (features @ beta.T) * self.y_scale + comp = features @ beta.T components.append(comp) if len(components) == 0: @@ -110,12 +110,13 @@ def predict_seasonal_comps( else: seasonal_components[mode] = na.NadaArray(np.array(components)) - additive_component = (seasonal_components["additive"] * self.y_scale).sum( - axis=0 + additive_component = seasonal_components["additive"] * self.y_scale + additive_component = additive_component.sum(axis=0) + + multiplicative_component = -seasonal_components["multiplicative"] + na.rational( + 1 ) - multiplicative_component = ( - -seasonal_components["multiplicative"] + na.rational(1) - ).prod(axis=0) + multiplicative_component = multiplicative_component.prod(axis=0) return additive_component, multiplicative_component @@ -180,7 +181,9 @@ def predict_trend(self, floor: na.NadaArray, t: na.NadaArray) -> na.NadaArray: def predict( self, dates: np.ndarray, + # TODO: often all zero - opportunity to compress floor: na.NadaArray, + # TODO: can be deterministically generated from len(horizon) t: na.NadaArray, ) -> na.NadaArray: """ @@ -194,11 +197,38 @@ def predict( Returns: na.NadaArray: Forecasted values. """ + assert len(dates) == len( + floor + ), "Provided Prophet inputs must be equally sized." + assert len(floor) == len(t), "Provided Prophet inputs must be equally sized." + + dates = self.ensure_numeric_dates(dates) trend = self.predict_trend(floor, t) additive_comps, multiplicative_comps = self.predict_seasonal_comps(dates) - yhat = trend * (multiplicative_comps + na.rational(1)) + additive_comps + yhat = trend * multiplicative_comps + additive_comps return yhat + def ensure_numeric_dates(self, dates: np.ndarray) -> np.ndarray: + """ + Ensures an array of dates is of the correct data format. + + Args: + dates (np.ndarray): Data array. + + Raises: + TypeError: Raised when dates array of incompatible type is passed. + + Returns: + np.ndarray: Standardized dates. + """ + if isinstance(dates.dtype, np.floating): + return dates + if np.issubdtype(dates.dtype, np.datetime64): + return dates.astype(np.float64) + raise TypeError( + f"Could not convert dates array of type `{dates}` to a NumPy array of numerics." + ) + @override def __call__( self, From 8e43b3031c236ca02e24faa2d2bbe9bea3b5d1a7 Mon Sep 17 00:00:00 2001 From: Mathias Leys Date: Tue, 11 Jun 2024 18:22:54 +0100 Subject: [PATCH 10/28] Fix type checking --- nada_ai/time_series/prophet.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/nada_ai/time_series/prophet.py b/nada_ai/time_series/prophet.py index 8b43169..38d4e54 100644 --- a/nada_ai/time_series/prophet.py +++ b/nada_ai/time_series/prophet.py @@ -221,7 +221,7 @@ def ensure_numeric_dates(self, dates: np.ndarray) -> np.ndarray: Returns: np.ndarray: Standardized dates. """ - if isinstance(dates.dtype, np.floating): + if np.issubdtype(dates.dtype, (np.integer, np.floating)): return dates if np.issubdtype(dates.dtype, np.datetime64): return dates.astype(np.float64) From 1ef414595622c8775ad6beb8a1292c292af46dcb Mon Sep 17 00:00:00 2001 From: Mathias Leys Date: Tue, 11 Jun 2024 18:48:55 +0100 Subject: [PATCH 11/28] Refactoring --- examples/README.md | 2 +- examples/complex_model/nada-project.toml | 2 +- .../complex_model/src/{main.py => complex_model.py} | 0 examples/complex_model/tests/complex_model.yaml | 2 +- examples/linear_regression/nada-project.toml | 2 +- .../src/{main.py => linear_regression.py} | 0 .../linear_regression/tests/linear_regression.yaml | 2 +- examples/neural_net/nada-project.toml | 4 ++-- examples/neural_net/src/{main.py => neural_net.py} | 0 examples/neural_net/tests/neural_net.yaml | 2 +- examples/time_series/nada-project.toml | 2 +- examples/time_series/src/{main.py => time_series.py} | 0 examples/time_series/tests/time_series.yaml | 2 +- nada_ai/client.py | 2 +- nada_ai/time_series/prophet.py | 10 +++++----- tests/python-tests/test_nn.py | 10 ---------- 16 files changed, 16 insertions(+), 26 deletions(-) rename examples/complex_model/src/{main.py => complex_model.py} (100%) rename examples/linear_regression/src/{main.py => linear_regression.py} (100%) rename examples/neural_net/src/{main.py => neural_net.py} (100%) rename examples/time_series/src/{main.py => time_series.py} (100%) diff --git a/examples/README.md b/examples/README.md index 74161c1..39f1b1d 100644 --- a/examples/README.md +++ b/examples/README.md @@ -7,6 +7,6 @@ The following are the currently available examples: - [Complex Model](./complex_model): shows how to build more intricate model architectures using Nada AI. Contains convolutions, pooling operations, linear layers and activations - [Time Series](./time_series): shows how to run a Facebook Prophet time series forecasting model using Nada AI -The Nada program source code is stored in `src/main.py`. +The Nada program source code is stored in `src/.py`. In order to follow the end-to-end example, head to `network/compute.py`. You can run it by simply running `nada build` to build the Nada program followed by `python network/compute.py`. diff --git a/examples/complex_model/nada-project.toml b/examples/complex_model/nada-project.toml index aaa7360..0c95e19 100644 --- a/examples/complex_model/nada-project.toml +++ b/examples/complex_model/nada-project.toml @@ -3,5 +3,5 @@ version = "0.1.0" authors = [""] [[programs]] -path = "src/main.py" +path = "src/complex_model.py" prime_size = 128 diff --git a/examples/complex_model/src/main.py b/examples/complex_model/src/complex_model.py similarity index 100% rename from examples/complex_model/src/main.py rename to examples/complex_model/src/complex_model.py diff --git a/examples/complex_model/tests/complex_model.yaml b/examples/complex_model/tests/complex_model.yaml index 43e1974..9a30a2c 100644 --- a/examples/complex_model/tests/complex_model.yaml +++ b/examples/complex_model/tests/complex_model.yaml @@ -1,5 +1,5 @@ --- -program: main +program: complex_model inputs: secrets: # We assume all values were originally floats, scaled & rounded by a factor of 2**16 diff --git a/examples/linear_regression/nada-project.toml b/examples/linear_regression/nada-project.toml index c64354d..3b69c79 100644 --- a/examples/linear_regression/nada-project.toml +++ b/examples/linear_regression/nada-project.toml @@ -3,5 +3,5 @@ version = "0.1.0" authors = [""] [[programs]] -path = "src/main.py" +path = "src/linear_regression.py" prime_size = 128 diff --git a/examples/linear_regression/src/main.py b/examples/linear_regression/src/linear_regression.py similarity index 100% rename from examples/linear_regression/src/main.py rename to examples/linear_regression/src/linear_regression.py diff --git a/examples/linear_regression/tests/linear_regression.yaml b/examples/linear_regression/tests/linear_regression.yaml index 6e20d1b..cf8c05d 100644 --- a/examples/linear_regression/tests/linear_regression.yaml +++ b/examples/linear_regression/tests/linear_regression.yaml @@ -1,5 +1,5 @@ --- -program: main +program: linear_regression inputs: secrets: # We assume all values were originally floats, scaled & rounded by a factor of 2**16 diff --git a/examples/neural_net/nada-project.toml b/examples/neural_net/nada-project.toml index aaa7360..1ac31f6 100644 --- a/examples/neural_net/nada-project.toml +++ b/examples/neural_net/nada-project.toml @@ -1,7 +1,7 @@ -name = "complex_model" +name = "neural_net" version = "0.1.0" authors = [""] [[programs]] -path = "src/main.py" +path = "src/neural_net.py" prime_size = 128 diff --git a/examples/neural_net/src/main.py b/examples/neural_net/src/neural_net.py similarity index 100% rename from examples/neural_net/src/main.py rename to examples/neural_net/src/neural_net.py diff --git a/examples/neural_net/tests/neural_net.yaml b/examples/neural_net/tests/neural_net.yaml index 7fec66a..bb15100 100644 --- a/examples/neural_net/tests/neural_net.yaml +++ b/examples/neural_net/tests/neural_net.yaml @@ -1,5 +1,5 @@ --- -program: main +program: neural_net inputs: secrets: # We assume all values were originally floats, scaled & rounded by a factor of 2**16 diff --git a/examples/time_series/nada-project.toml b/examples/time_series/nada-project.toml index 15f098a..b8f6a35 100644 --- a/examples/time_series/nada-project.toml +++ b/examples/time_series/nada-project.toml @@ -3,5 +3,5 @@ version = "0.1.0" authors = [""] [[programs]] -path = "src/main.py" +path = "src/time_series.py" prime_size = 128 diff --git a/examples/time_series/src/main.py b/examples/time_series/src/time_series.py similarity index 100% rename from examples/time_series/src/main.py rename to examples/time_series/src/time_series.py diff --git a/examples/time_series/tests/time_series.yaml b/examples/time_series/tests/time_series.yaml index 7497d2b..b0e2b98 100644 --- a/examples/time_series/tests/time_series.yaml +++ b/examples/time_series/tests/time_series.yaml @@ -1,5 +1,5 @@ --- -program: main +program: time_series inputs: secrets: my_prophet_changepoints_t_8: diff --git a/nada_ai/client.py b/nada_ai/client.py index 50eff14..a7bc824 100644 --- a/nada_ai/client.py +++ b/nada_ai/client.py @@ -159,7 +159,7 @@ def __init__(self, model: sklearn.base.BaseEstimator) -> None: class ProphetClient(ModelClient): """ModelClient for Prophet models""" - def __init__(self, model) -> None: + def __init__(self, model: "prophet.forecaster.Prophet") -> None: """ Client initialization. diff --git a/nada_ai/time_series/prophet.py b/nada_ai/time_series/prophet.py index 38d4e54..7110a46 100644 --- a/nada_ai/time_series/prophet.py +++ b/nada_ai/time_series/prophet.py @@ -197,10 +197,8 @@ def predict( Returns: na.NadaArray: Forecasted values. """ - assert len(dates) == len( - floor - ), "Provided Prophet inputs must be equally sized." - assert len(floor) == len(t), "Provided Prophet inputs must be equally sized." + assert len(dates) == len(floor), "Prophet inputs must be equally sized." + assert len(floor) == len(t), "Prophet inputs must be equally sized." dates = self.ensure_numeric_dates(dates) trend = self.predict_trend(floor, t) @@ -221,7 +219,9 @@ def ensure_numeric_dates(self, dates: np.ndarray) -> np.ndarray: Returns: np.ndarray: Standardized dates. """ - if np.issubdtype(dates.dtype, (np.integer, np.floating)): + if np.issubdtype(dates.dtype, np.integer) or np.issubdtype( + dates.dtype, np.floating + ): return dates if np.issubdtype(dates.dtype, np.datetime64): return dates.astype(np.float64) diff --git a/tests/python-tests/test_nn.py b/tests/python-tests/test_nn.py index 92e056b..af18282 100644 --- a/tests/python-tests/test_nn.py +++ b/tests/python-tests/test_nn.py @@ -79,8 +79,6 @@ def forward(x: na.NadaArray) -> na.NadaArray: ... def test_parameters_5(self): param = Parameter((2, 3)) - assert param[0][0] == Integer(0) - alphas = na.alphas((2, 3), alpha=Integer(42)) param.load_state(alphas) @@ -96,9 +94,6 @@ def forward(x: na.NadaArray) -> na.NadaArray: ... mod = TestModule() - for _, param in mod.named_parameters(): - assert param[0][0] == Integer(0) - alphas = na.alphas((2, 3), alpha=Integer(42)) for _, param in mod.named_parameters(): @@ -124,9 +119,6 @@ def forward(x: na.NadaArray) -> na.NadaArray: ... mod = TestModule2() - for _, param in mod.named_parameters(): - assert param[0][0] == Integer(0) - alphas = na.alphas((2, 3), alpha=Integer(42)) for _, param in mod.named_parameters(): @@ -138,8 +130,6 @@ def forward(x: na.NadaArray) -> na.NadaArray: ... def test_parameters_8(self): param = Parameter((2, 3)) - assert param[0][0] == Integer(0) - alphas = na.alphas((3, 3), alpha=Integer(42)) with pytest.raises(MismatchedShapesException): From 5b73d516848b82255b41e277d3915ab2cdf36758 Mon Sep 17 00:00:00 2001 From: Mathias Leys Date: Tue, 11 Jun 2024 22:30:29 +0100 Subject: [PATCH 12/28] Fix multiplicative seasonality --- nada_ai/time_series/prophet.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/nada_ai/time_series/prophet.py b/nada_ai/time_series/prophet.py index 7110a46..0cf7cf6 100644 --- a/nada_ai/time_series/prophet.py +++ b/nada_ai/time_series/prophet.py @@ -113,7 +113,7 @@ def predict_seasonal_comps( additive_component = seasonal_components["additive"] * self.y_scale additive_component = additive_component.sum(axis=0) - multiplicative_component = -seasonal_components["multiplicative"] + na.rational( + multiplicative_component = seasonal_components["multiplicative"] + na.rational( 1 ) multiplicative_component = multiplicative_component.prod(axis=0) From 56add95ebd505ddb9aba2e02fe8ec459eabf67c9 Mon Sep 17 00:00:00 2001 From: Mathias Leys Date: Tue, 11 Jun 2024 22:32:04 +0100 Subject: [PATCH 13/28] fix flat trend --- nada_ai/time_series/prophet.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/nada_ai/time_series/prophet.py b/nada_ai/time_series/prophet.py index 0cf7cf6..7c5317c 100644 --- a/nada_ai/time_series/prophet.py +++ b/nada_ai/time_series/prophet.py @@ -172,7 +172,7 @@ def predict_trend(self, floor: na.NadaArray, t: na.NadaArray) -> na.NadaArray: m_t = (-self.changepoints_t * deltas_t).sum(axis=1) + m trend = k_t * t + m_t elif self.growth == "flat": - trend = na.ones_like(t, na.rational) * m + trend = na.ones_like(t, na.Rational) * m else: raise NotImplementedError(self.growth + " is not supported") From 83d72068175066e31288da4366ba2d75da4aa8c0 Mon Sep 17 00:00:00 2001 From: Mathias Leys Date: Tue, 11 Jun 2024 22:35:56 +0100 Subject: [PATCH 14/28] Variable naming --- nada_ai/time_series/prophet.py | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/nada_ai/time_series/prophet.py b/nada_ai/time_series/prophet.py index 7c5317c..58e5389 100644 --- a/nada_ai/time_series/prophet.py +++ b/nada_ai/time_series/prophet.py @@ -110,15 +110,13 @@ def predict_seasonal_comps( else: seasonal_components[mode] = na.NadaArray(np.array(components)) - additive_component = seasonal_components["additive"] * self.y_scale - additive_component = additive_component.sum(axis=0) + add_component = seasonal_components["additive"] * self.y_scale + add_component = add_component.sum(axis=0) - multiplicative_component = seasonal_components["multiplicative"] + na.rational( - 1 - ) - multiplicative_component = multiplicative_component.prod(axis=0) + mult_component = seasonal_components["multiplicative"] + na.rational(1) + mult_component = mult_component.prod(axis=0) - return additive_component, multiplicative_component + return add_component, mult_component def make_seasonality_features( self, dates: np.ndarray, seasonalities: Dict[str, Dict[str, int | float]] From 6726a7aac937ab419e1bb9a89b8968ec488bcf6a Mon Sep 17 00:00:00 2001 From: Mathias Leys Date: Wed, 12 Jun 2024 09:39:45 +0100 Subject: [PATCH 15/28] Refactor fourier --- nada_ai/time_series/prophet.py | 20 +++++++++++++++----- 1 file changed, 15 insertions(+), 5 deletions(-) diff --git a/nada_ai/time_series/prophet.py b/nada_ai/time_series/prophet.py index 58e5389..1c95963 100644 --- a/nada_ai/time_series/prophet.py +++ b/nada_ai/time_series/prophet.py @@ -57,11 +57,7 @@ def __init__( "fourier_order": 4, } - num_fourier = 0 - for _, mode_seasonality in self.seasonalities.items(): - for _, period_seasonality in mode_seasonality.items(): - # NOTE: times two because there is always a term for both sin and cos - num_fourier += period_seasonality["fourier_order"] * 2 + num_fourier = self._num_fourier_terms() M = 1 # NOTE: MAP estimation is assumed, so M=1 guaranteed @@ -76,6 +72,20 @@ def __init__( self.changepoints_t = Parameter(n_changepoints) self.y_scale = Parameter(1) + def _num_fourier_terms(self) -> int: + """ + Calculates the number of Fourier terms. + + Returns: + int: Number of Fourier terms. + """ + num_fourier = 0 + for _, mode_seasonality in self.seasonalities.items(): + for _, period_seasonality in mode_seasonality.items(): + # NOTE: times two because there is always a term for both sin and cos + num_fourier += period_seasonality["fourier_order"] * 2 + return num_fourier + def predict_seasonal_comps( self, dates: np.ndarray ) -> Tuple[na.NadaArray, na.NadaArray]: From efab6815d2704a9d8a0313def071b28a51174c3d Mon Sep 17 00:00:00 2001 From: Mathias Leys Date: Wed, 12 Jun 2024 10:00:43 +0100 Subject: [PATCH 16/28] Fix formatting --- nada_ai/time_series/prophet.py | 24 +++++++++++++----------- 1 file changed, 13 insertions(+), 11 deletions(-) diff --git a/nada_ai/time_series/prophet.py b/nada_ai/time_series/prophet.py index 1c95963..b0ad5ec 100644 --- a/nada_ai/time_series/prophet.py +++ b/nada_ai/time_series/prophet.py @@ -115,10 +115,11 @@ def predict_seasonal_comps( comp = features @ beta.T components.append(comp) - if len(components) == 0: - seasonal_components[mode] = na.zeros(dates.shape, na.Rational) - else: - seasonal_components[mode] = na.NadaArray(np.array(components)) + seasonal_components[mode] = ( + na.NadaArray(np.array(components)) + if len(components) != 0 + else na.zeros(dates.shape, na.Rational) + ) add_component = seasonal_components["additive"] * self.y_scale add_component = add_component.sum(axis=0) @@ -144,11 +145,11 @@ def make_seasonality_features( features = {} for name, props in seasonalities.items(): period, fourier_order = props["period"], props["fourier_order"] - if fourier_order == 0: - feats = None - else: + feats = None + if fourier_order != 0: feats = fourier_series(dates, period, fourier_order) - features[name] = na.frompyfunc(na.rational, 1, 1)(feats) + feats = na.frompyfunc(na.rational, 1, 1)(feats) + features[name] = feats return features def predict_trend(self, floor: na.NadaArray, t: na.NadaArray) -> na.NadaArray: @@ -172,9 +173,10 @@ def predict_trend(self, floor: na.NadaArray, t: na.NadaArray) -> na.NadaArray: [delta] = self.delta if self.growth == "linear": - mask = na.frompyfunc( - lambda a, b: (a <= b).if_else(na.rational(1), na.rational(0)), 2, 1 - )(self.changepoints_t[None, :], t[..., None]) + le_fn = lambda a, b: (a <= b).if_else(na.rational(1), na.rational(0)) + mask = na.frompyfunc(le_fn, 2, 1)( + self.changepoints_t[None, :], t[..., None] + ) deltas_t = delta * mask k_t = deltas_t.sum(axis=1) + k m_t = (-self.changepoints_t * deltas_t).sum(axis=1) + m From c60da035f8ac7bd0b23f3fd7d2cc072b487ff77a Mon Sep 17 00:00:00 2001 From: Mathias Leys Date: Wed, 12 Jun 2024 10:03:30 +0100 Subject: [PATCH 17/28] Fix formatting --- nada_ai/time_series/prophet.py | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/nada_ai/time_series/prophet.py b/nada_ai/time_series/prophet.py index b0ad5ec..991a93c 100644 --- a/nada_ai/time_series/prophet.py +++ b/nada_ai/time_series/prophet.py @@ -173,10 +173,9 @@ def predict_trend(self, floor: na.NadaArray, t: na.NadaArray) -> na.NadaArray: [delta] = self.delta if self.growth == "linear": - le_fn = lambda a, b: (a <= b).if_else(na.rational(1), na.rational(0)) - mask = na.frompyfunc(le_fn, 2, 1)( - self.changepoints_t[None, :], t[..., None] - ) + less_than_fn = lambda a, b: (a <= b).if_else(na.rational(1), na.rational(0)) + less_than_vectorized = na.frompyfunc(less_than_fn, 2, 1) + mask = less_than_vectorized(self.changepoints_t[None, :], t[..., None]) deltas_t = delta * mask k_t = deltas_t.sum(axis=1) + k m_t = (-self.changepoints_t * deltas_t).sum(axis=1) + m From 7610e740adfd0607cfd14010bdd47e7ff8a0816b Mon Sep 17 00:00:00 2001 From: Mathias Leys Date: Wed, 12 Jun 2024 10:04:08 +0100 Subject: [PATCH 18/28] Fix formatting --- nada_ai/time_series/prophet.py | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/nada_ai/time_series/prophet.py b/nada_ai/time_series/prophet.py index 991a93c..e881346 100644 --- a/nada_ai/time_series/prophet.py +++ b/nada_ai/time_series/prophet.py @@ -228,11 +228,10 @@ def ensure_numeric_dates(self, dates: np.ndarray) -> np.ndarray: Returns: np.ndarray: Standardized dates. """ - if np.issubdtype(dates.dtype, np.integer) or np.issubdtype( - dates.dtype, np.floating - ): + dtype = dates.dtype + if np.issubdtype(dtype, np.integer) or np.issubdtype(dtype, np.floating): return dates - if np.issubdtype(dates.dtype, np.datetime64): + if np.issubdtype(dtype, np.datetime64): return dates.astype(np.float64) raise TypeError( f"Could not convert dates array of type `{dates}` to a NumPy array of numerics." From 868c9d4247eeff1ebaf62c75c5569d4de82476aa Mon Sep 17 00:00:00 2001 From: Mathias Leys Date: Wed, 12 Jun 2024 10:06:03 +0100 Subject: [PATCH 19/28] Fix formatting --- nada_ai/time_series/prophet.py | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/nada_ai/time_series/prophet.py b/nada_ai/time_series/prophet.py index e881346..ddd86a8 100644 --- a/nada_ai/time_series/prophet.py +++ b/nada_ai/time_series/prophet.py @@ -206,8 +206,8 @@ def predict( Returns: na.NadaArray: Forecasted values. """ - assert len(dates) == len(floor), "Prophet inputs must be equally sized." - assert len(floor) == len(t), "Prophet inputs must be equally sized." + assert dates.shape == floor.shape, "Prophet inputs must be equally sized." + assert floor.shape == t.shape, "Prophet inputs must be equally sized." dates = self.ensure_numeric_dates(dates) trend = self.predict_trend(floor, t) @@ -233,9 +233,8 @@ def ensure_numeric_dates(self, dates: np.ndarray) -> np.ndarray: return dates if np.issubdtype(dtype, np.datetime64): return dates.astype(np.float64) - raise TypeError( - f"Could not convert dates array of type `{dates}` to a NumPy array of numerics." - ) + error_msg = f"Could not convert dates of type `{dates}` to a numeric array." + raise TypeError(error_msg) @override def __call__( From 8cc6489ac8613bc99a219218e5f457572a1f2dfd Mon Sep 17 00:00:00 2001 From: Mathias Leys Date: Wed, 12 Jun 2024 15:11:33 +0100 Subject: [PATCH 20/28] Bump nada-algebra version --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 47efe38..326c0ca 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -13,7 +13,7 @@ scikit-learn = "^1.4.2" prophet = "^1.1.5" nada-dsl = "^0.2.1" py-nillion-client = "^0.2.1" -nada-algebra = "^0.3.2" +nada-algebra = "^0.3.3" [tool.poetry.group.dev.dependencies] From d7c205be5a3acd6d8eb3ac63fc0bfe18876a0dc9 Mon Sep 17 00:00:00 2001 From: Mathias Leys Date: Wed, 12 Jun 2024 15:29:46 +0100 Subject: [PATCH 21/28] Update poetry --- poetry.lock | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/poetry.lock b/poetry.lock index 701b24c..0833d43 100644 --- a/poetry.lock +++ b/poetry.lock @@ -687,13 +687,13 @@ files = [ [[package]] name = "nada-algebra" -version = "0.3.2" +version = "0.3.3" description = "Nada-Algebra is a Python library designed for algebraic operations on NumPy-like array objects on top of Nada DSL and Nillion Network." optional = false python-versions = "<4.0,>=3.10" files = [ - {file = "nada_algebra-0.3.2-py3-none-any.whl", hash = "sha256:d83e7c71854bb6230ad51009a273d2eb9c4f4c4117a5808f8cd485f1f9a83d6f"}, - {file = "nada_algebra-0.3.2.tar.gz", hash = "sha256:1aed21b53ad7e764dccd27aec2d8fd25faaef8159429f81a7341499f5554d09e"}, + {file = "nada_algebra-0.3.3-py3-none-any.whl", hash = "sha256:d5590a7e5a535df96bcc4fbe58dc8e5200bfe620368a6248f3bdf473f5f7e0da"}, + {file = "nada_algebra-0.3.3.tar.gz", hash = "sha256:99314f2d3fd17160436ded762d4ff8b6c4e879fcc60468d497768d6161cf3a98"}, ] [package.dependencies] @@ -1566,4 +1566,4 @@ files = [ [metadata] lock-version = "2.0" python-versions = "^3.10" -content-hash = "5fbd199d20ff748b0f4b8b98c3b1b6967824e9bcf2b87300b112f37572412d87" +content-hash = "5b547da890f17807c8b1eb405e22fde1942bb4498c70aa2b01282e35f7b38d1a" From 4a3ce9e925648c3f4f32c226a7e222da8466510f Mon Sep 17 00:00:00 2001 From: Mathias Leys Date: Wed, 12 Jun 2024 15:29:55 +0100 Subject: [PATCH 22/28] Update examples --- examples/time_series/network/compute.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/time_series/network/compute.py b/examples/time_series/network/compute.py index ef1c293..177932b 100644 --- a/examples/time_series/network/compute.py +++ b/examples/time_series/network/compute.py @@ -6,7 +6,7 @@ import numpy as np import nada_algebra as na import pandas as pd -from nada_ai import ProphetClient +from nada_ai.client import ProphetClient from prophet import Prophet from dotenv import load_dotenv From b1b64f078f7798a404e9be5dcdb907750f6fbd67 Mon Sep 17 00:00:00 2001 From: Mathias Leys Date: Wed, 12 Jun 2024 15:30:04 +0100 Subject: [PATCH 23/28] Update tests --- tests/python-tests/test_model_client.py | 61 ++++++++++++++++++++++++- 1 file changed, 60 insertions(+), 1 deletion(-) diff --git a/tests/python-tests/test_model_client.py b/tests/python-tests/test_model_client.py index ce9f19b..b65ac82 100644 --- a/tests/python-tests/test_model_client.py +++ b/tests/python-tests/test_model_client.py @@ -3,11 +3,13 @@ import pytest import torch import numpy as np +import pandas as pd import nada_algebra as na from sklearn.linear_model import LinearRegression, LogisticRegression from torch import nn -from nada_ai.client import ModelClient, TorchClient, SklearnClient +from prophet import Prophet +from nada_ai.client import ModelClient, TorchClient, SklearnClient, ProphetClient import py_nillion_client as nillion @@ -247,3 +249,60 @@ def forward(self, x: torch.Tensor) -> torch.Tensor: for expected_layer in expected_layers: assert expected_layer in secrets.keys() + + def test_prophet_1(self): + model = Prophet() + + ds = pd.date_range("2024-05-01", "2024-05-17").tolist() + y = np.arange(1, 18).tolist() + + fit_model = model.fit(df=pd.DataFrame({"ds": ds, "y": y})) + + # Avoid zero secrets - just for testing purposes + for param_name in fit_model.params: + fit_model.params[param_name] = fit_model.params[param_name] + 1 + + model_client = ProphetClient(fit_model) + + secrets = model_client.export_state_as_secrets("test_model", na.SecretRational) + + expected_layers = [ + "test_model_k_0_0", + "test_model_m_0_0", + "test_model_delta_0_0", + "test_model_delta_0_1", + "test_model_delta_0_2", + "test_model_delta_0_3", + "test_model_delta_0_4", + "test_model_delta_0_5", + "test_model_delta_0_6", + "test_model_delta_0_7", + "test_model_delta_0_8", + "test_model_delta_0_9", + "test_model_delta_0_10", + "test_model_delta_0_11", + "test_model_beta_0_0", + "test_model_beta_0_1", + "test_model_beta_0_2", + "test_model_beta_0_3", + "test_model_beta_0_4", + "test_model_beta_0_5", + "test_model_changepoints_t_0", + "test_model_changepoints_t_1", + "test_model_changepoints_t_2", + "test_model_changepoints_t_3", + "test_model_changepoints_t_4", + "test_model_changepoints_t_5", + "test_model_changepoints_t_6", + "test_model_changepoints_t_7", + "test_model_changepoints_t_8", + "test_model_changepoints_t_9", + "test_model_changepoints_t_10", + "test_model_changepoints_t_11", + "test_model_y_scale_0", + ] + + assert len(secrets) == len(expected_layers) + + for expected_layer in expected_layers: + assert expected_layer in secrets.keys() From 56ca1b851b9336d196e85d0e81daebe07f88e52e Mon Sep 17 00:00:00 2001 From: Mathias Leys Date: Wed, 12 Jun 2024 15:34:10 +0100 Subject: [PATCH 24/28] Fix compatibility with legacy python versions --- nada_ai/time_series/prophet.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/nada_ai/time_series/prophet.py b/nada_ai/time_series/prophet.py index ddd86a8..367a75b 100644 --- a/nada_ai/time_series/prophet.py +++ b/nada_ai/time_series/prophet.py @@ -1,7 +1,7 @@ """Facebook Prophet implementation""" import numpy as np -from typing import Dict, Tuple, override +from typing import Dict, Tuple import nada_algebra as na from nada_ai.nn.module import Module @@ -236,7 +236,6 @@ def ensure_numeric_dates(self, dates: np.ndarray) -> np.ndarray: error_msg = f"Could not convert dates of type `{dates}` to a numeric array." raise TypeError(error_msg) - @override def __call__( self, dates: np.ndarray, @@ -257,7 +256,6 @@ def __call__( """ return self.predict(dates=dates, floor=floor, t=t) - @override def forward( self, dates: np.ndarray, From 19115084218657dda76592919ff2555dfef3d0ae Mon Sep 17 00:00:00 2001 From: Mathias Leys Date: Wed, 12 Jun 2024 15:36:22 +0100 Subject: [PATCH 25/28] Fix example tests --- examples/complex_model/target/.gitignore | 5 +++++ examples/linear_regression/target/.gitignore | 5 +++++ examples/neural_net/target/.gitignore | 5 +++++ examples/time_series/target/.gitignore | 5 +++++ 4 files changed, 20 insertions(+) create mode 100644 examples/complex_model/target/.gitignore create mode 100644 examples/linear_regression/target/.gitignore create mode 100644 examples/neural_net/target/.gitignore create mode 100644 examples/time_series/target/.gitignore diff --git a/examples/complex_model/target/.gitignore b/examples/complex_model/target/.gitignore new file mode 100644 index 0000000..5291a74 --- /dev/null +++ b/examples/complex_model/target/.gitignore @@ -0,0 +1,5 @@ +# This directory is kept purposely, so that no compilation errors arise. +# Ignore everything in this directory +* +# Except this file +!.gitignore \ No newline at end of file diff --git a/examples/linear_regression/target/.gitignore b/examples/linear_regression/target/.gitignore new file mode 100644 index 0000000..5291a74 --- /dev/null +++ b/examples/linear_regression/target/.gitignore @@ -0,0 +1,5 @@ +# This directory is kept purposely, so that no compilation errors arise. +# Ignore everything in this directory +* +# Except this file +!.gitignore \ No newline at end of file diff --git a/examples/neural_net/target/.gitignore b/examples/neural_net/target/.gitignore new file mode 100644 index 0000000..5291a74 --- /dev/null +++ b/examples/neural_net/target/.gitignore @@ -0,0 +1,5 @@ +# This directory is kept purposely, so that no compilation errors arise. +# Ignore everything in this directory +* +# Except this file +!.gitignore \ No newline at end of file diff --git a/examples/time_series/target/.gitignore b/examples/time_series/target/.gitignore new file mode 100644 index 0000000..5291a74 --- /dev/null +++ b/examples/time_series/target/.gitignore @@ -0,0 +1,5 @@ +# This directory is kept purposely, so that no compilation errors arise. +# Ignore everything in this directory +* +# Except this file +!.gitignore \ No newline at end of file From 074cde13d779cffba2f82819d1763e755dad63a6 Mon Sep 17 00:00:00 2001 From: Mathias Leys Date: Wed, 12 Jun 2024 16:24:55 +0100 Subject: [PATCH 26/28] Fix rescaling --- examples/complex_model/network/compute.py | 4 ++-- examples/linear_regression/network/compute.py | 4 ++-- examples/neural_net/network/compute.py | 6 +++--- examples/time_series/network/compute.py | 2 +- 4 files changed, 8 insertions(+), 8 deletions(-) diff --git a/examples/complex_model/network/compute.py b/examples/complex_model/network/compute.py index 61f126d..ccc4bdc 100644 --- a/examples/complex_model/network/compute.py +++ b/examples/complex_model/network/compute.py @@ -188,9 +188,9 @@ def forward(self, x: torch.Tensor) -> torch.Tensor: nillion.Secrets({}), ) - # Sort & rescale the obtained results by the quantization scale (here: 16) + # Sort & rescale the obtained results by the quantization scale outputs = outputs = [ - result[1] / 2**16 + na_client.float_from_rational(result[1]) for result in sorted( result.items(), key=lambda x: int(x[0].replace("my_output", "").replace("_", "")), diff --git a/examples/linear_regression/network/compute.py b/examples/linear_regression/network/compute.py index b69889c..1bd5aef 100644 --- a/examples/linear_regression/network/compute.py +++ b/examples/linear_regression/network/compute.py @@ -162,8 +162,8 @@ async def main(): [model_store_id, data_store_id], nillion.Secrets({}), ) - # Rescale the obtained result by the quantization scale (here: 16) - outputs = [result["my_output_0"] / 2**16] + # Rescale the obtained result by the quantization scale + outputs = [na_client.float_from_rational(result["my_output_0"])] print(f"🖥️ The result is {outputs}") expected = fit_model.predict(np.ones((NUM_FEATS,)).reshape(1, -1)) diff --git a/examples/neural_net/network/compute.py b/examples/neural_net/network/compute.py index 0d5c771..9999bf9 100644 --- a/examples/neural_net/network/compute.py +++ b/examples/neural_net/network/compute.py @@ -5,7 +5,7 @@ import numpy as np import time import nada_algebra as na -from nada_ai import TorchClient +from nada_ai.client import TorchClient import torch from dotenv import load_dotenv @@ -166,9 +166,9 @@ def forward(self, x: na.NadaArray) -> na.NadaArray: nillion.Secrets({}), ) - # Sort & rescale the obtained results by the quantization scale (here: 16) + # Sort & rescale the obtained results by the quantization scale outputs = [ - result[1] / 2**16 + na_client.float_from_rational(result[1]) for result in sorted( result.items(), key=lambda x: int(x[0].replace("my_output", "").replace("_", "")), diff --git a/examples/time_series/network/compute.py b/examples/time_series/network/compute.py index 177932b..5f5fafb 100644 --- a/examples/time_series/network/compute.py +++ b/examples/time_series/network/compute.py @@ -170,7 +170,7 @@ async def main(): # Sort & rescale the obtained results by the quantization scale outputs = [ - result[1] / 2**50 + na_client.float_from_rational(result[1]) for result in sorted( result.items(), key=lambda x: int(x[0].replace("my_output", "").replace("_", "")), From 38304207086f1d358d0c91b13e7769b4c4064d52 Mon Sep 17 00:00:00 2001 From: Mathias Leys Date: Wed, 12 Jun 2024 16:25:13 +0100 Subject: [PATCH 27/28] Bump nada-algebra version --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 326c0ca..b762363 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -13,7 +13,7 @@ scikit-learn = "^1.4.2" prophet = "^1.1.5" nada-dsl = "^0.2.1" py-nillion-client = "^0.2.1" -nada-algebra = "^0.3.3" +nada-algebra = "^0.3.4" [tool.poetry.group.dev.dependencies] From 295a3ce1c0896b5848e7de5b1f4e1cbd909e71b8 Mon Sep 17 00:00:00 2001 From: Mathias Leys Date: Wed, 12 Jun 2024 16:38:44 +0100 Subject: [PATCH 28/28] Update poetry --- poetry.lock | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/poetry.lock b/poetry.lock index 0833d43..5b99ba5 100644 --- a/poetry.lock +++ b/poetry.lock @@ -687,13 +687,13 @@ files = [ [[package]] name = "nada-algebra" -version = "0.3.3" +version = "0.3.4" description = "Nada-Algebra is a Python library designed for algebraic operations on NumPy-like array objects on top of Nada DSL and Nillion Network." optional = false python-versions = "<4.0,>=3.10" files = [ - {file = "nada_algebra-0.3.3-py3-none-any.whl", hash = "sha256:d5590a7e5a535df96bcc4fbe58dc8e5200bfe620368a6248f3bdf473f5f7e0da"}, - {file = "nada_algebra-0.3.3.tar.gz", hash = "sha256:99314f2d3fd17160436ded762d4ff8b6c4e879fcc60468d497768d6161cf3a98"}, + {file = "nada_algebra-0.3.4-py3-none-any.whl", hash = "sha256:b9ac86f35b5ed29b12439c9aacf929287a6eafcbd5075ff51f05b25354794117"}, + {file = "nada_algebra-0.3.4.tar.gz", hash = "sha256:8fe57e4b2efd6abbda59660aad053fc1c1d13dedfa0e7109d9ecb11a173ca334"}, ] [package.dependencies] @@ -1566,4 +1566,4 @@ files = [ [metadata] lock-version = "2.0" python-versions = "^3.10" -content-hash = "5b547da890f17807c8b1eb405e22fde1942bb4498c70aa2b01282e35f7b38d1a" +content-hash = "352c0a87c207737336e42778f8d9a574bbbda4f8f94b06883aa2fc20a137ad1a"