diff --git a/.gitignore b/.gitignore index 61c7ae1..41cf80f 100644 --- a/.gitignore +++ b/.gitignore @@ -1,7 +1,14 @@ -/tmp/ -/test/.pytest_cache/ -*.json -*.pyc -*.idea/ -/build/ -/venv/ +**/tmp/ +**/.pytest_cache/ +*.json +*.pyc +*.idea/ +**/build/ +**/venv/ +**/tmp_* +**/tmp-* +**/test/ +**/*.egg-info/ +/testing/test_data/* +/testing/test_output +*.dat \ No newline at end of file diff --git a/Dockerfile b/Dockerfile index 2ca3cfa..23b2666 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,13 +1,11 @@ -FROM python:3.9.15-slim - -ADD src /src -ADD test /test -ADD rest /rest -ADD ephemerality.py / -ADD requirements.txt / -ADD _version.py / -ADD setup.py / - -RUN pip install --no-cache-dir --upgrade -r requirements.txt - -CMD ["uvicorn", "rest.api:app", "--host", "0.0.0.0", "--port", "8080"] +FROM python:3.10-slim + +ADD dist/ephemerality-2.0.0-py3-none-any.whl /dist/ +ADD LICENSE / +ADD README.md / + +RUN pip install --no-cache-dir /dist/ephemerality-2.0.0-py3-none-any.whl + +WORKDIR /usr/local/lib/python3.10/site-packages/ephemerality + +ENTRYPOINT ["uvicorn", "rest.runner:app", "--host", "0.0.0.0", "--port", "8080"] diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..2e80ea4 --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2023 Barcelona Supercomputing Center - Centro Nacional de Supercomputación (BSC-CNS) + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. \ No newline at end of file diff --git a/README.md b/README.md index e7ad269..a039031 100644 --- a/README.md +++ b/README.md @@ -1,165 +1,309 @@ -# Ephemerality metric -In [[1]](#1) we formalized the ephemerality metrics used to estimate the healthiness of online discussions. It shows how -'ephemeral' topics are, that is whether the discussions are more or less uniformly active or only revolve around one or -several peaks of activity. - -We defined 3 versions of ephemerality: original, filtered, and sorted. Let us suppose we have a discussion that we can divide in $N$ bins of equal time length and for each bin we can calculate activity in that time period (e.g. number of tweets, watches, visits etc.). Let $t$ denote a normalized vector of frequency corresponding to this discussion, $t_i$ corresponds to normalized activity in during time bin $i$. Let $\alpha\in\left[0, 1\right)$ denote a parameter showing which portion of activity we consider to be the "core" activity. Then we can define ephemerality as a normalized portion of $t$ that contains the remaining $1-\alpha$ activity. We can interpret this defenition in three slightly different ways depending on what we consider to be the core activity: - -1. **Original ephemerality**. We calculate core activity as the minimal portion of $t$ starting from the beginning of the vector that contains at least $\alpha$ of the total activity (which is 1 in case of normalized $t$). Then the ephemerality formula can be computed as follows: - -$$ -\varepsilon_{orig}\left(t_i\right) = 1 - \frac1\alpha \frac{\arg\min_{m\in [1,N]}: \left( \sum_{i=1\dots m} t_i \right) \ge \alpha}{N} -$$ - -2. **Filtered ephemerality**. We calculate core activity the minimal *central* portion of $t$ that contains at least $\alpha$ of the total activity. For that we exclude portions of $t$ from the beginning and the end of $t$, so that the sum of each of these portions is as close to $\frac{1-\alpha}{2}$ as possible without reaching it: - -$$ -\varepsilon_{filt}\left(t_i\right) = 1 - \frac1\alpha \frac{\arg\min_{m\in [1,N]}: \left( \sum_{i=1\dots m} t_i - \max_{p\in [1,N]}: \left( \sum_{j=1\dots p} t_j \right) < \frac{1-\alpha}{2} \right) \ge \alpha}{N} -$$ - -3. **Sorted ephemerality**. Finally, we can define the core activity as the minimal number of time bins that cover $\alpha$ portion of the activity. For that we sort $t$ components in descending order (denoted as $\widehat{t}$) and then apply the formula of original ephemerality: - -$$ -\varepsilon_{sort}\left(t_i\right) = 1 - \frac1\alpha \frac{\arg\min_{m\in [1,N]}: \left( \sum_{i=1\dots m} \widehat{t}_i \right) \ge \alpha}{N} -$$ - -## Requirements -The code was tested to work with Python 3.8.6 and Numpy 1.21.5, but is expected to also run on their older versions. - -## How to run the experiments -The code can be run directly via the calculate_ephemerality.py script or via a Docker container built with the provided -Dockerfile. - -### Input -The script/container expect the following input arguments: - -* **Frequency vector file**. `[-i PATH, --input PATH]` _Optional_. Path to a file containing one or several arrays of -numbers in csv format (one array per line), representing temporal frequency vectors. They do not need to be normalized: -if they are not --- they will be normalized automatically. -* **Frequency vector**. _Optional_. If input file is not provided, a frequency vector is expected as a positional -argument (either comma- or space-separated). -* **Output file**. `[-o PATH, --output PATH]` _Optional_. If it is provided, the results will be written into this file -in JSON format. -* **Threshold**. `[-t FLOAT, -threshold FLOAT]` _Optional_. Threshold value for ephemerality computations. Defaults -to 0.8. -* **Print**. `[-p, --print]`. _Optional_. If output file is provided, forces the results to still be printed to stdout. - -### Output -If no output file specified or `-p` option is used, results are printed to STDOUT in [ -$\varepsilon_{orig}$ ␣ -span( $\varepsilon_{orig}$ ) ␣ -$\varepsilon_{filt}$ ␣ -span( $\varepsilon_{filt}$ ) ␣ -$\varepsilon_{sort}$ ␣ -span( $\varepsilon_{sort}$ ) -] format, one line per each line of input file (or a single line for command line input). - -If the output file was specified among the input arguments, the results will be written into that file in JSON format as -a list of dictionaries, one per input line: - -``` -[ - { - "ephemerality_original": FLOAT, - "ephemerality_original_span": INT, - "ephemerality_filtered": FLOAT, - "ephemerality_filtered_span": INT, - "ephemerality_sorted": FLOAT, - "ephemerality_sorted_span": INT - }, - ... -] -``` - -### Example - -Input file `test_input.csv`: -``` -0.0,0.0,0.0,0.2,0.55,0.0,0.15,0.1,0.0,0.0 -0,1,1.,0.0,.0 -``` - -#### Python execution: - -Input 1: - -``` -python ephemerality.py -i tmp/test_input.csv -t 0.8 --output tmp/test_output.json -P -``` - -Output 1: -``` -0.1250000000000001 7 0.5 4 0.625 3 -0.2500000000000001 3 0.5 2 0.5 2 -``` - -`test_output.json` content: -``` -[ - { - "ephemerality_original": 0.1250000000000001, - "ephemerality_original_span": 7, - "ephemerality_filtered": 0.5, - "ephemerality_filtered_span": 4, - "ephemerality_sorted": 0.625, - "ephemerality_sorted_span": 3 - }, - { - "ephemerality_original": 0.2500000000000001, - "ephemerality_original_span": 3, - "ephemerality_filtered": 0.5, - "ephemerality_filtered_span": 2, - "ephemerality_sorted": 0.5, - "ephemerality_sorted_span": 2 - } -] -``` - -Input 2: - -``` -python ephemerality.py 0.0 0.0 0.0 0.2 0.55 0.0 0.15 0.1 0.0 0.0 -t 0.5 -``` - -Output 2: -``` -0.0 5 0.8 1 0.8 1 -``` - -#### Docker execution -``` -docker run -a STDOUT -v [PATH_TO_FOLDER]/tmp/:/tmp/ ephemerality:1.0.0 -i /tmp/test_input.csv -o /tmp/test_output.json -t 0.5 -p -``` - -Output: -``` -0.0 5 0.8 1 0.8 1 -0.19999999999999996 2 0.6 1 0.6 1 -``` - -`test_output.json` content: -``` -[ - { - "ephemerality_original": 0.0, - "ephemerality_original_span": 5, - "ephemerality_filtered": 0.8, - "ephemerality_filtered_span": 1, - "ephemerality_sorted": 0.8, - "ephemerality_sorted_span": 1 - }, - { - "ephemerality_original": 0.19999999999999996, - "ephemerality_original_span": 2, - "ephemerality_filtered": 0.6, - "ephemerality_filtered_span": 1, - "ephemerality_sorted": 0.6, - "ephemerality_sorted_span": 1 - } -] -``` - - -## References -[1] -Gnatyshak, D., Garcia-Gasulla, D., Alvarez-Napagao, S., Arjona, J., & Venturini, T. (2022). Healthy Twitter discussions? Time will tell. arXiv preprint arXiv:2203.11261 +# Ephemerality metric +Ephemerality metrics are used to estimate the healthiness of various (online) activities, e.g. online discussions. +It shows how 'ephemeral' these activities are, that is whether they are more or less uniform +over the period of interest or are only clustered around one or several short time periods. + +Ephemerality formula is defined as follows: + +$$ +\varepsilon_\alpha^{core}=\left(1 - \frac1\alpha \cdot \frac{core\ length}{period\ length}\right)^+ +$$ + +Essentially, **ephemerality** is a portion of the time period occupied by non‑core activity. The core activity can be +defined in different ways and is parametrized by $\alpha$ — the minimal percentage of total activity it needs to +contain. + +We defined 4 versions of computing these core periods: + +1. **Left $\alpha$-core**. Continuous time span from **the beginning of the period** to the point when $\alpha$% of +total activity volume is contained within. Measures how fast activity becomes negligible after the start of the period. +Best suited for the types of activity with well-defined start time, e.g. reactions to posts, videos, news, etc. + +2. **Right $\alpha$-core**. Continuous time span from **the end of the period** to the point when $\alpha$% of total +activity volume is contained within. Measures how far in the past the meaningful past of activity spans. Best suited for +when you want to analyze activity within a specific time frame (for instance, up until the current date). + +3. **Middle $\alpha$-core**. Continuous time span from **in the middle of the period** that contains $\alpha$% of total +activity volume. Computed by cutting out at most (but as close to) $\frac{1-\alpha}2$% of activity volume from the +beginning and the end of the period. Best suited for activities with no identifiable start and end times within the +period of interest, e.g. discussions of certain topics onj social media. + +4. **Sorted $\alpha$-core**. **Minimal number of time bins** that together contain $\alpha$% of total activity volume +(left $\alpha$‑core for activity vectors sorted in descending order). Measures what portion of time is occupied by the +most prominent activity. Works well in combination with other cores to check whether all of the activity was centered +around one or more short periods of time. + +## Requirements +The code was tested to work with Python 3.10, Numpy 1.24.2, and pydantic 2.7.1, but is expected to also run with their +older versions. +FastAPI ^0.110.2 and uvicorn ^0.22.0 are also needed to run the ephemerality computation web-service. +Matplotlib ^3.8.4 is needed for visualization of computed activity cores. + +## How to run +Ephemerality package can be run as a standalone Python module, or used inside regular Python scripts. + +### Standalone + +The code can be run directly as a module `python3 -m ephemerality [args]`. There are two modes provided: a +command line-based computation and a RESTful service. In case of the latter, there is an option to use a Docker +container, either from Docker Hub (`hpaibsc/ephemerality:latest`) or built with the provided Dockerfile. + +To run the computation from the command lineuse `cmd` argument: + +```shell +python3 -m ephemerality cmd [activity] [-h] [-i INPUT_FILE] [-r] [-o OUTPUT_FILE.json] [-c CORE_TYPES] [-t THRESHOLD] [--plot] +``` + +In case of activity vector of moderate size, you can enter it either as a sequence of numbers directly, or as a +comma/space separated string. Alternatively, you can save the activity vector(s) in a *.csv file (a vector per line) or +*.json file. In case of the latter, please use the following format for each input (you can have a single dictionary or +a list of ones): + +```NumPy +JSON/Python input body format + +Attributes +---------- +input_sequence : Sequence[str | float | int] + Input sequence of either activity over time bins, or timestamps to be aggregated into an activity vector. +input_type : Literal['activity', 'a', 'timestamps', 't', 'datetime', 'd'], default='a' + Format of the input sequence. +threshold : float, default=.8 + Ratio of the activity considered core. +time_format : str, optional, default="%Y-%m-%dT%H:%M:%S.%fZ" + If input type is datetime, specifies the datetime format used (refer to `strptime` format guidelines). +timezone : float, optional, default=0. + If input type is datetime, specifies the offset in hours from the UTC time. Should be within [-24, +24] range. +range : tuple[str | float | int, str | float | int], optional + If input type is timestamp or datetime, specifies the time range of the activity vector. + Defaults to (min, max) of the input timestamps. +granularity : Literal['week', 'day', 'hour'] or str, optional, default='day' + If input type is timestamp or datetime, specifies the size of time bins when converting to activity vector. + Can be specified as \'{}d\' or \'{}h\' (e.g. '2d' or '7.5h') for a custom time bin size in days or hours. +reference_name : str, optional + Will be added to the output for easier matching between inputs and outputs (besides identical sorting). +``` + +The module uses uvicorn package to run a REST service. To start it use `api` argument: + +```shell +python3 -m ephemerality api [-h] [--host HOST] [--port PORT] ... +``` + +Any additional arguments will be passed as arguments to the uvicorn call. + +The web-service initialized this way accepts GET requests of the following format: + +```http +http://{url}/ephemerality/{api_version}/all?core_types=lmrs&include_input=False +``` + +* `api_version` is optional and is used for backward compatibility, it defaults to the latest API version. +* `core_types` is a string specifying the core types for which ephemerality needs to be computed. `l` for the left +core, `m` for the middle core, `r` for the right core, `s` for the sorted core, or any combination of thereof. +It defaults to all core types, `lmrs`. +* `include_input` is a boolean value specifying whether the input should be also included in the output for each +computation. +* `input_data` is a body argument in JSON format. It should contain a list of JSON dictionaries of the input type +specified above. + +The output is in JSON format. It is a list of dictionaries, with one entry per each input: + +```json +[ + { + "input": ..., + "output": { + "eph_left_core": 1.0, + "eph_middle_core": 1.0, + "eph_right_core": 1.0, + "eph_sorted_core": 1.0, + "len_left_core": 0, + "len_middle_core": 0, + "len_right_core": 0, + "len_sorted_core": 0 + } + }, + ... +] +``` + +`input` value depends on the provided input parameters: +* If `include_input` was set to True, it will be a copy of the corresponding input dictionary. +* Otherwise it will be a string: + - `reference_name` if it was provided; + - or a zero-base counter number of the corresponding input. + +#### Docker container + +Finally, you can also use the docker container to run the aforementioned REST service. You can either get the image +from Docker Hub: + +```shell +docker pull hpaibsc/ephemerality:latest +``` + +Or build it from the source using the provided Dockerfile. + +The web-service will be available at `http://0.0.0.0:8080` inside of the container. + +### Python + +Finally, it is possible to just import the ephemerality computation function from the module: + +```Python +from ephemerality import compute_ephemerality + +activity = [0., 0., 0., .2, .55, 0., .15, .1, 0., 0.] +threshold = 0.8 +compute_ephemerality(activity, threshold) +``` + +Output: +```pycon +EphemeralitySet( + eph_left_core=0.1250000000000001, + eph_middle_core=0.5, + eph_right_core=0.2500000000000001, + eph_sorted_core=0.625, + len_left_core=7, + len_middle_core=4, + len_right_core=6, + len_sorted_core=3) +``` + +`compute_ephemerality` function has the following signature: + +```NumPy +Compute ephemerality values for given activity vector. + +This function computes ephemerality for a numeric vector using all current definitions of actiovity cores. +Alpha (desired non-ephemeral core length) can be specified with ``threshold parameter. In case not all cores are +needed, the required types can be specified in ``types``. + +Parameters +---------- +activity_vector : Sequence[float | int] + A sequence of activity values. Time bins corresponding to each value is assumed to be of equal length. Does not + need to be normalised. +threshold : float, default=0.8 + Desired non-ephemeral core length as fraction. Ephemerality is equal to 1.0 if the core length is at least ``threshold`` of + the ``activity_vector`` length. +types : str, default='lmrs' + Activity cores to be computed. A sequence of characters corresponding to core types. + 'l' for left core, 'm' for middle core, 'r' for right core, 's' for sorted core. Multiples of the same character + will be ignored. +plot : bool, default=False + Set to True to display the activity over time plot with core periods highlighted. + +Returns +------- +EphemeralitySet + Container for the computed core lengths and ephemerality values +``` + +EphemeralitySet is a simple container of the following format: + +```NumPy +Container for ephemerality and core size values. + +This class is a simple pydantic BaseModel used to store computed core lengths and corresponding ephemerality values +by core type. Values that were not computed default to None. + +Attributes +---------- +len_left_core : int, optional + Length of the left core in time bins +len_middle_core : int, optional + Length of the middle core in time bins +len_right_core : int, optional + Length of the right core in time bins +len_sorted_core : int, optional + Length of the sorted core in time bins + +eph_left_core: float, optional + Ephemerality value for the left core +eph_middle_core: float, optional + Ephemerality value for the middle core +eph_right_core: float, optional + Ephemerality value for the right core +eph_sorted_core: float, optional + Ephemerality value for the sorted core +``` + +## Examples + +Below are several examples of activity vectors and corresponding ephemerality computation results, demonstrating +how this module can be used. All input activity vectors represent one year of activity with one day granularity. +Threshold of $\alpha=0.8$ was used for all of the examples. + +### Example 1 + +![Activity vector 1](images/example_1.png) + +This vector represents a typical reaction activity to a post of any kind (e.g. text post, video, etc.). Most of the +activity is concentrated at the beginning of the observation period and quickly goes down noise afterwards. Assuming +we picked the start of the vector well (i.e. the time of posting), we will obtain the following ephemerality +computation results: + +![Results 1](images/results_1.png) + +Here you can see that for the selected period ephemerality values for all cores except for the right core are high. +Essentially, ephemerality value of 0.75 in this case signifies that about a quarter of the period corresponded to +non-core activity. + +The exception of the right core (which is computed from the right end of the activity vector) makes sense, as to +accumulate the 80% of activity we need to go to almost the beginning of the vector. This can be interpreted as the +fact, that looking into the past from the last (e.g. current) data this activity goes essentially full period deep. +That is, this activity existed for quite a while in the past, and did not appear recently. + +### Example 2 + +![Activity vector 2](images/example_2.png) + +This activity vector has a few smaller but more gradual active periods and a big peak by its end. This produces the +following results: + +![Results 2](images/results_2.png) + +Left and middle core ephemeralities in this case are pretty low, as it takes almost all the period to accumulate the +80% of activity. The final peak was not enough to offset the previous, more gradual, activity. + +In case of the right core, the last two peaks did contain the required amount of activity, so the ephemerality value +is significant. + +Finally, as the majority of the activity is concentrated in the three peaks, the sorted ephemerality value is rather +high (albeit not close to 1, thanks to the widths of the first two peaks). + +### Example 3 + +![Activity vector 3](images/example_3.png) + +In this example the vector contains 4 intense and abrupt peaks of activity among the general noise, a clear indicator +of the forced activity injections. However, these peaks are spread throughout the observation period, with one being +close to the beginning of the vector, and another one to its end. + +![Results 3](images/results_3.png) + +Non-sorted cores in this case do cover most of the observation period due to the spread of the peaks. However, the +sorted ephemerality value is very high, signifying that most of this activity were contained within a small number of +actual days. + +Here we should note that it is important to look at ephemerality for all the core types. From the current results we +know that there are several well-spread short peaks of activity. If the middle core ephemerality was also high, that +would have signified that there is only one peak or a close cluster of peaks in the middle of the period. If either +the left or right core ephemerality were also high, that would have meant that this peak is closer to the left or the +right end of the vector respectively. + +### Example 4 + +![Activity vector 4](images/example_4.png) + +In the final example, the activity is locally chaotic but more or less uniform over the whole period. There is a small +evolution of the trend here, but nothing unexpected. + +![Results 4](images/results_4.png) + +And this uniformity is confirmed by the resulting ephemerality values which are close to 0 (it would have been 0 in +case of the same amount of activity each day). Here the slightly higher value of the sorted core ephemerality +corresponds to the general variance of the activity: the lower the local fluctuations are, the closer to 0 it will be. diff --git a/_version.py b/_version.py deleted file mode 100644 index d538f87..0000000 --- a/_version.py +++ /dev/null @@ -1 +0,0 @@ -__version__ = "1.0.0" \ No newline at end of file diff --git a/dist/ephemerality-2.0.0-py3-none-any.whl b/dist/ephemerality-2.0.0-py3-none-any.whl new file mode 100644 index 0000000..fd4fbfe Binary files /dev/null and b/dist/ephemerality-2.0.0-py3-none-any.whl differ diff --git a/dist/ephemerality-2.0.0.tar.gz b/dist/ephemerality-2.0.0.tar.gz new file mode 100644 index 0000000..c30b371 Binary files /dev/null and b/dist/ephemerality-2.0.0.tar.gz differ diff --git a/ephemerality.py b/ephemerality.py deleted file mode 100644 index a1d050a..0000000 --- a/ephemerality.py +++ /dev/null @@ -1,89 +0,0 @@ -from _version import __version__ -import sys -import json -import argparse -import numpy as np -from src import compute_ephemerality - - -HELP_INFO = "" - - -def init_argparse() -> argparse.ArgumentParser: - parser = argparse.ArgumentParser( - usage="%(prog)s [FREQUENCY_VECTOR] [-h] [-v] [-i INPUT_FILE] [-o OUTPUT_FILE.json] [-t THRESHOLD]...", - description="Calculate ephemerality for a given vector of frequencies." - ) - parser.add_argument( - "-v", "--version", action="version", - version=f"{parser.prog} version {__version__}" - ) - parser.add_argument( - "-p", "--print", action="store_true", - help="If output file is provided, forces the results to still be printed to stdout." - ) - parser.add_argument( - "-i", "--input", action="store", - help="Path to the input csv file. If not specified, will use the command line arguments " - "(delimited either by commas or spaces)." - ) - parser.add_argument( - "-o", "--output", action="store", - help="Path to the output json file. If not specified, will output ephemerality values to stdout in the" - " following format separated by a space: \"EPH_ORIG EPH_ORIG_SPAN EPH_FILT EPH_FILT_SPAN EPH_SORT " - "EPH_SORT_SPAN\"" - ) - parser.add_argument( - "-t", "--threshold", action="store", default=0.8, - help="Threshold value for ephemerality computations. Defaults to 0.8." - ) - parser.add_argument( - 'frequencies', - help='frequency vector (if the input file is not specified)', - nargs='*' - ) - return parser - - -def print_ephemeralities(ephemerality_list: list[dict]): - for ephemeralities in ephemerality_list: - print(f"{ephemeralities['ephemerality_original']} {ephemeralities['ephemerality_original_span']} " - f"{ephemeralities['ephemerality_filtered']} {ephemeralities['ephemerality_filtered_span']} " - f"{ephemeralities['ephemerality_sorted']} {ephemeralities['ephemerality_sorted_span']}") - - -if __name__ == '__main__': - parser = init_argparse() - args = parser.parse_args() - - frequency_vectors = list() - - if args.input: - with open(args.input, 'r') as f: - for line in f.readlines(): - if line.strip(): - frequency_vectors.append(np.array(line.split(','), dtype=float)) - else: - if len(args.frequencies) > 1: - frequency_vectors.append(np.array(args.frequencies, dtype=float)) - elif len(args.frequencies) == 1: - if ' ' in args.frequencies[0]: - frequency_vectors.append(np.array(args.frequencies[0].split(' '), dtype=float)) - else: - frequency_vectors.append(np.array(args.frequencies[0].split(','), dtype=float)) - else: - sys.exit('No input provided!') - - threshold = float(args.threshold) - - ephemerality_list = list() - for frequency_vector in frequency_vectors: - ephemerality_list.append(compute_ephemerality(frequency_vector=frequency_vector, threshold=threshold).dict()) - - if args.output: - with open(args.output, 'w+') as f: - json.dump(ephemerality_list, f, indent=2) - if args.print: - print_ephemeralities(ephemerality_list) - else: - print_ephemeralities(ephemerality_list) diff --git a/ephemerality/__init__.py b/ephemerality/__init__.py new file mode 100644 index 0000000..d682f98 --- /dev/null +++ b/ephemerality/__init__.py @@ -0,0 +1,5 @@ +from .src import EphemeralitySet, InputData +from .src import compute_ephemerality + + +__all__ = ['compute_ephemerality', 'InputData', 'EphemeralitySet'] diff --git a/ephemerality/__main__.py b/ephemerality/__main__.py new file mode 100644 index 0000000..a2da2c2 --- /dev/null +++ b/ephemerality/__main__.py @@ -0,0 +1,36 @@ +from argparse import ArgumentParser +import importlib.metadata + +from ephemerality.scripts import init_cmd_parser, init_api_argparse + +PROG = "python3 -m ephemerality" + + +def init_parser() -> ArgumentParser: + parser = ArgumentParser( + prog=PROG, + usage="%(prog)s [-h] [-v] {cmd,api} ...", + description="Runs ephemerality computation module in one of the available mode." + ) + parser.add_argument( + "--version", action="version", + version=f'ephemerality {importlib.metadata.version("ephemerality")}' + ) + + subparsers = parser.add_subparsers( + prog=PROG, + help="Use \"cmd\" to run the module once from a command line.\n" + "Use \"api\" to start a REST web service offering ephemerality computation on request." + ) + cmd_parser = subparsers.add_parser("cmd") + api_parser = subparsers.add_parser("api") + + init_cmd_parser(cmd_parser) + init_api_argparse(api_parser) + + return parser + + +parser = init_parser() +args = parser.parse_args() +args.func(args) diff --git a/ephemerality/rest/__init__.py b/ephemerality/rest/__init__.py new file mode 100644 index 0000000..eeb424f --- /dev/null +++ b/ephemerality/rest/__init__.py @@ -0,0 +1,14 @@ +import ephemerality.rest.api_versions as api_versions +from ephemerality.rest.api_versions import AbstractRestApi +from ephemerality.rest.api import router +from ephemerality.src import InputData + +__all__ = [ + 'InputData', + 'router', + 'AbstractRestApi' +] + + +API_VERSION_DICT: dict[str, AbstractRestApi] = {api.version(): api for api in api_versions.__all__ if api.version()} +DEFAULT_API: AbstractRestApi = API_VERSION_DICT[max(API_VERSION_DICT.keys())] diff --git a/ephemerality/rest/api.py b/ephemerality/rest/api.py new file mode 100644 index 0000000..820efbb --- /dev/null +++ b/ephemerality/rest/api.py @@ -0,0 +1,92 @@ +from typing import Annotated, Any, Union + +import ephemerality.rest as rest +from ephemerality.src import InputData, process_input +from fastapi import APIRouter, status, Query, Response +from fastapi.responses import JSONResponse + +router = APIRouter() + + +def run_computations( + input_data: list[InputData], + core_types: str, + api: rest.AbstractRestApi, + include_input: bool = False) -> Union[list[dict[str, Any] | dict[str, dict[str, Any]]], None]: + output = [] + noname_counter = 0 + for test_case in input_data: + case_input = process_input(input_remote_data=test_case)[0] + case_output = api.get_ephemerality( + input_vector=case_input.activity, + threshold=case_input.threshold, + types=core_types + ).dict(exclude_none=True) + + if include_input: + output.append({ + "input": test_case.dict(), + "output": case_output + }) + else: + if test_case.reference_name: + input_name = test_case.reference_name + else: + input_name = str(noname_counter) + noname_counter += 1 + + output.append({ + "input": input_name, + "output": case_output + }) + return output + + +def process_request( + input_data: list[InputData], + api_version: str, + core_types: str, + include_input: bool +) -> Response: + if api_version not in rest.API_VERSION_DICT: + raise ValueError(f'Unrecognized API version: {api_version}!') + else: + api = rest.API_VERSION_DICT[api_version] + + output = run_computations(input_data=input_data, core_types=core_types, api=api, include_input=include_input) + return JSONResponse(content=output) + + +@router.get("/ephemerality/all", status_code=status.HTTP_200_OK) +async def compute_all_ephemeralities_default_version( + input_data: list[InputData], + core_types: Annotated[ + str, Query(min_length=1, max_length=4, regex="^[lmrs]+$") + ] = "lmrs", + include_input: bool = False +) -> Response: + default_version = rest.DEFAULT_API.version() + return process_request( + input_data=input_data, + core_types=core_types, + api_version=default_version, + include_input=include_input + ) + + +@router.get("/ephemerality/{api_version}/all", status_code=status.HTTP_200_OK) +async def compute_all_ephemeralities( + input_data: list[InputData], + api_version: str, + core_types: Annotated[ + str, Query(min_length=1, max_length=4, regex="^[lmrs]+$") + ] = "lmrs", + include_input: bool = False +) -> Response: + + return process_request( + input_data=input_data, + core_types=core_types, + api_version=api_version, + include_input=include_input, + ) diff --git a/ephemerality/rest/api_versions/__init__.py b/ephemerality/rest/api_versions/__init__.py new file mode 100644 index 0000000..b886d2d --- /dev/null +++ b/ephemerality/rest/api_versions/__init__.py @@ -0,0 +1,11 @@ +from ephemerality.rest.api_versions.api_template import AbstractRestApi +from ephemerality.rest.api_versions.api11 import RestAPI11 + + +__all__ = [ + AbstractRestApi, + RestAPI11 +] + + + diff --git a/ephemerality/rest/api_versions/api11.py b/ephemerality/rest/api_versions/api11.py new file mode 100644 index 0000000..f7785d6 --- /dev/null +++ b/ephemerality/rest/api_versions/api11.py @@ -0,0 +1,20 @@ +from typing import Sequence, Annotated + +from fastapi import Query + +from ephemerality.rest.api_versions.api_template import AbstractRestApi +from ephemerality.src import compute_ephemerality, EphemeralitySet + + +class RestAPI11(AbstractRestApi): + @staticmethod + def version() -> str: + return "1.1" + + @staticmethod + def get_ephemerality( + input_vector: Sequence[float], + threshold: Annotated[float, Query(gt=0., le=1.)], + types: Annotated[str, Query(Query(min_length=1, max_length=4, regex="^[lmrs]+$"))] + ) -> EphemeralitySet: + return compute_ephemerality(activity_vector=input_vector, threshold=threshold, types=types) diff --git a/ephemerality/rest/api_versions/api_template.py b/ephemerality/rest/api_versions/api_template.py new file mode 100644 index 0000000..8c95d06 --- /dev/null +++ b/ephemerality/rest/api_versions/api_template.py @@ -0,0 +1,19 @@ +from abc import ABC, abstractmethod +from typing import Annotated, Sequence +from fastapi import Query +from ephemerality.src import EphemeralitySet + + +class AbstractRestApi(ABC): + @staticmethod + @abstractmethod + def version() -> str | None: + return None + + @staticmethod + @abstractmethod + def get_ephemerality(input_vector: Sequence[float], + threshold: Annotated[float, Query(gt=0., le=1.)], + types: Annotated[str, Query(Query(min_length=1, max_length=4, regex="^[lmrs]+$"))] + ) -> EphemeralitySet: + raise NotImplementedError diff --git a/ephemerality/rest/runner.py b/ephemerality/rest/runner.py new file mode 100644 index 0000000..5076734 --- /dev/null +++ b/ephemerality/rest/runner.py @@ -0,0 +1,6 @@ +from fastapi import FastAPI +from ephemerality.rest import router + + +app = FastAPI() +app.include_router(router) diff --git a/ephemerality/scripts/__init__.py b/ephemerality/scripts/__init__.py new file mode 100644 index 0000000..03c7596 --- /dev/null +++ b/ephemerality/scripts/__init__.py @@ -0,0 +1,4 @@ +from ephemerality.scripts.ephemerality_api import init_api_argparse +from ephemerality.scripts.ephemerality_cmd import init_cmd_parser + +__all__ = ['init_cmd_parser', 'init_api_argparse'] diff --git a/ephemerality/scripts/ephemerality_api.py b/ephemerality/scripts/ephemerality_api.py new file mode 100644 index 0000000..5821cc2 --- /dev/null +++ b/ephemerality/scripts/ephemerality_api.py @@ -0,0 +1,36 @@ +import argparse +from argparse import ArgumentParser, Namespace +from subprocess import call + + +def init_api_argparse(parser: ArgumentParser) -> ArgumentParser: + parser.usage = "%(prog)s [-h] [--host HOST] [--port PORT] ..." + parser.description = ("Start a REST web service to compute ephemerality computations on requests. Any additional " + "arguments will be passed to the uvicorn service initialisation call.") + parser.add_argument( + "--host", action="store", default="127.0.0.1", + help="Bind socket to this host. Defaults to \"127.0.0.1\"." + ) + parser.add_argument( + "--port", action="store", type=int, default=8080, + help="Bind to a socket with this port. Defaults to 8080." + ) + parser.add_argument( + "uvicorn_args", nargs=argparse.REMAINDER, + help="Arguments to be passed to uvicorn." + ) + parser.set_defaults( + func=exec_start_service_call + ) + return parser + + +def start_service(host: str = "127.0.0.1", port: int = 8080, uvicorn_args: list | None = None) -> None: + call_cmd = ['uvicorn', 'ephemerality.rest.runner:app', '--host', host, '--port', str(port)] + if uvicorn_args: + call_cmd.extend(uvicorn_args) + call(call_cmd) + + +def exec_start_service_call(input_args: Namespace) -> None: + start_service(host=input_args.host, port=input_args.port, uvicorn_args=input_args.uvicorn_args) diff --git a/ephemerality/scripts/ephemerality_cmd.py b/ephemerality/scripts/ephemerality_cmd.py new file mode 100644 index 0000000..9fde6bf --- /dev/null +++ b/ephemerality/scripts/ephemerality_cmd.py @@ -0,0 +1,118 @@ +import json +import sys +from argparse import ArgumentParser, Namespace +from pathlib import Path + +import numpy as np +from ephemerality.src import compute_ephemerality, process_input, ProcessedData + + +def init_cmd_parser(parser: ArgumentParser) -> ArgumentParser: + parser.usage = "%(prog)s [activity] [-h] [-i INPUT_FILE] [-r] [-o OUTPUT_FILE.json] [-c CORE_TYPES] [-t THRESHOLD] [--plot]..." + parser.description = "Calculate ephemerality for a given activity vector or a set of timestamps." + parser.add_argument( + "-p", "--print", action="store_true", + help="If an output file is specified, forces the results to still be printed to stdout." + ) + parser.add_argument( + "-i", "--input", action="store", + help="Path to either a JSON or CSV file with input data, or to the folder with files. If not specified, " + "will read the activity vector from the command line (as numbers delimited by either commas or spaces)." + ) + parser.add_argument( + "-r", "--recursive", action="store_true", + help="Used with a folder-type input to specify to also process files in the full subfolder tree. " + "Defaults to False." + ) + parser.add_argument( + "-o", "--output", action="store", + help="Path to an output JSON file. If not specified, will output ephemerality values to stdout in JSON format." + ) + parser.add_argument( + "--output_indent", action="store", type=int, default=-1, + help="Sets the indentation level of the output (either a JSON file or STDOUT) in terms of number of spaces per " + "level. If negative, will output results as a single line. Defaults to -1." + ) + parser.add_argument( + "-c", "--core_types", action="store", type=str, default="lmrs", + help="Specify core types to be computed. \"l\" for left core, \"m\" for middle core, \"r\" for right core, " + "\"s\" for sorted core, or any combination of thereof. Default to \"lmrs\" for all 4 core types. " + ) + parser.add_argument( + "-t", "--threshold", action="store", type=float, default=0.8, + help="Threshold value for ephemerality computations in case of CSV input. Defaults to 0.8." + ) + parser.add_argument( + "--plot", action="store_true", + help="Visualize requested core types on the activity vector plot." + ) + parser.add_argument( + 'activity', + help='Activity vector (if the input file is not specified)', + nargs='*' + ) + parser.set_defaults( + func=exec_cmd_compute_call + ) + return parser + + +def exec_cmd_compute_call(input_args: Namespace) -> None: + if input_args.input: + path = Path(input_args.input) + if path.is_dir(): + input_cases = process_input(input_folder=input_args.input, recursive=input_args.recursive) + elif path.is_file(): + input_cases = process_input(input_file=input_args.input, threshold=float(input_args.threshold)) + else: + raise ValueError("Unknown input file format!") + else: + input_cases: list[ProcessedData] = [] + if len(input_args.activity) > 1: + input_cases.append( + ProcessedData( + name="cmd-input", + activity=np.array(input_args.activity, dtype=float), + threshold=float(input_args.threshold))) + elif len(input_args.activity) == 1: + if ' ' in input_args.activity[0]: + input_cases.append( + ProcessedData( + name="cmd-input", + activity=np.array(input_args.activity[0].split(' '), dtype=float), + threshold=float(input_args.threshold))) + elif ',' in input_args.activity[0]: + input_cases.append( + ProcessedData( + name="cmd-input", + activity=np.array(input_args.activity[0].split(','), dtype=float), + threshold=float(input_args.threshold))) + else: + input_cases.append( + ProcessedData( + name="cmd-input", + activity=np.array([input_args.activity[0]], dtype=float), + threshold=float(input_args.threshold))) + else: + sys.exit('No input provided!') + + results = {} + + for input_case in input_cases: + results[input_case.name] = compute_ephemerality(activity_vector=input_case.activity, + threshold=input_case.threshold, + types=input_args.core_types, + plot=input_args.plot).dict() + if len(results) == 1: + results = results.popitem()[1] + + output_indent = input_args.output_indent if input_args.output_indent >= 0 else None + if input_args.output: + with open(input_args.output, 'w') as f: + json.dump(results, f, indent=output_indent, sort_keys=True) + if input_args.print: + print(json.dumps(results, indent=output_indent, sort_keys=True)) + else: + return None + else: + print(json.dumps(results, indent=output_indent, sort_keys=True)) diff --git a/ephemerality/src/__init__.py b/ephemerality/src/__init__.py new file mode 100644 index 0000000..04e58b0 --- /dev/null +++ b/ephemerality/src/__init__.py @@ -0,0 +1,5 @@ +from .data_processing import process_input, InputData, ProcessedData +from .ephemerality_computation import compute_ephemerality +from .utils import EphemeralitySet + +__all__ = ['compute_ephemerality', 'EphemeralitySet', 'process_input', 'InputData', 'ProcessedData'] diff --git a/ephemerality/src/data_processing.py b/ephemerality/src/data_processing.py new file mode 100644 index 0000000..c87bf13 --- /dev/null +++ b/ephemerality/src/data_processing.py @@ -0,0 +1,220 @@ +import json +import warnings +from dataclasses import dataclass +from datetime import datetime, timezone, timedelta +from pathlib import Path +from typing import Sequence, Literal + +import numpy as np +from numpy.typing import NDArray +from pydantic import BaseModel + +SECONDS_WEEK = 604800. +SECONDS_DAY = 86400. +SECONDS_HOUR = 3600. + + +class InputData(BaseModel): + """ + GET request body format + + Attributes + ---------- + input_sequence : Sequence[str | float | int] + Input sequence of either activity over time bins, or timestamps to be aggregated into an activity vector. + input_type : Literal['activity', 'a', 'timestamps', 't', 'datetime', 'd'], default='a' + Format of the input sequence. + threshold : float, default=.8 + Ratio of the activity considered core. + time_format : str, optional, default="%Y-%m-%dT%H:%M:%S.%fZ" + If input type is datetime, specifies the datetime format used (refer to `strptime` format guidelines). + timezone : float, optional, default=0. + If input type is datetime, specifies the offset in hours from the UTC time. Should be within [-24, +24] range. + range : tuple[str | float | int, str | float | int], optional + If input type is timestamp or datetime, specifies the time range of the activity vector. + Defaults to (min, max) of the input timestamps. + granularity : Literal['week', 'day', 'hour'] or str, optional, default='day' + If input type is timestamp or datetime, specifies the size of time bins when converting to activity vector. + Can be specified as \'{}d\' or \'{}h\' (e.g. '2d' or '7.5h') for a custom time bin size in days or hours. + reference_name : str, optional + Will be added to the output for easier matching between inputs and outputs (besides identical sorting). + """ + input_sequence: Sequence[str | float | int] + input_type: Literal['activity', 'a', 'timestamps', 't', 'datetime', 'd'] = 'a' + threshold: float = .8 + time_format: str = "%Y-%m-%dT%H:%M:%S.%fZ" + timezone: float = 0. + range: None | tuple[str | float | int, str | float | int] = None + granularity: Literal['week', 'day', 'hour'] | str = 'day' + reference_name: str = "" + + def __init__(self, **kwargs): + kwargs['input_sequence'] = [str(item) for item in kwargs['input_sequence']] + if 'range' in kwargs and kwargs['range'] is not None: + kwargs['range'] = (str(kwargs['range'][0]), str(kwargs['range'][1])) + super().__init__(**kwargs) + + +@dataclass +class ProcessedData: + name: str + activity: NDArray[float] + threshold: float = 0.8 + + +def process_input( + input_folder: str | Path | None = None, + recursive: bool = True, + input_file: str | Path | None = None, + input_remote_data: InputData | None = None, + input_dict: dict | None = None, + input_seq: Sequence[float | int | str] | None = None, + threshold: float = 0.8) -> list[ProcessedData]: + output = [] + + if input_folder: + output.extend(process_folder(path=Path(input_folder), recursive=recursive, threshold=threshold)) + + if input_file: + output.extend(process_file(path=Path(input_file), threshold=threshold)) + + if input_remote_data: + output.append(process_formatted_data(input_remote_data)) + + if input_dict: + output.append(process_formatted_data(InputData(**input_dict))) + + if input_seq: + if threshold is None: + raise ValueError('Threshold value is not defined!') + output.append(ProcessedData(name="sequence", activity=np.ndarray(input_seq, dtype=float), threshold=threshold)) + + return output + + +def process_folder(path: Path, recursive: bool = True, threshold: float | None = None) -> list[ProcessedData]: + output = [] + for file in path.iterdir(): + if file.is_file(): + output.extend(process_file(path=file, threshold=threshold)) + elif file.is_dir() and recursive: + output.extend(process_folder(path=file, recursive=recursive, threshold=threshold)) + return output + + +def process_file(path: Path, threshold: float | None = None) -> list[ProcessedData]: + if path.suffix == '.json': + return process_json(path) + elif path.suffix == '.csv': + return [ProcessedData(name=f"{str(path.resolve())}[{i}]", activity=sequence, threshold=threshold) + for i, sequence in enumerate(process_csv(path))] + else: + return [] + + +def process_json(path: Path) -> list[ProcessedData]: + with open(path, 'r') as f: + input_object = json.load(f) + + if isinstance(input_object, dict): + input_object = [input_object] + + output = [] + for i, input_case in enumerate(input_object): + input_case = InputData(**input_case) + try: + case_output = process_formatted_data(input_case) + if not case_output.name: + case_output.name = f"{str(path.resolve())}[{i}]" + output.append(case_output) + except ValueError: + warnings.warn( + f'\"input_type\" is not one of [\"activity\", \"a\", \"timestamps\", \"t\", \"datetime\", \"d\"]!' + f' Ignoring file \"{str(path.resolve())}\"!') + + return output + + +def process_formatted_data(input_data: InputData) -> ProcessedData: + if input_data.input_type == 'activity' or input_data.input_type == 'a': + return ProcessedData( + name=input_data.reference_name, + activity=np.array(input_data.input_sequence, dtype=float), + threshold=input_data.threshold + ) + elif input_data.input_type == 'timestamps' or input_data.input_type == 't': + return ProcessedData( + name=input_data.reference_name, + activity=timestamps_to_activity(np.array(input_data.input_sequence, dtype=float), + input_data.range, + input_data.granularity), + threshold=input_data.threshold + ) + elif input_data.input_type == 'datetime' or input_data.input_type == 'd': + timestamps = [datetime.strptime(time_point, input_data.time_format).replace( + tzinfo=timezone(timedelta(hours=input_data.timezone))).timestamp() + for time_point in input_data.input_sequence] + if input_data.range is not None: + ts_range = ( + datetime.strptime(input_data.range[0], input_data.time_format).replace( + tzinfo=timezone(timedelta(hours=input_data.timezone))).timestamp(), + datetime.strptime(input_data.range[1], input_data.time_format).replace( + tzinfo=timezone(timedelta(hours=input_data.timezone))).timestamp() + ) + else: + ts_range = None + return ProcessedData( + name=input_data.reference_name, + activity=timestamps_to_activity(np.array(timestamps, dtype=float), ts_range, input_data.granularity), + threshold=input_data.threshold + ) + else: + raise ValueError("Wrong \"input_type\" value!") + + +def process_csv(path: Path) -> list[NDArray[float]]: + output = [] + with open(path, 'r') as f: + for line in f: + if line: + output.append(np.fromstring(line.strip(), dtype=float, sep=',')) + return output + + +def timestamps_to_activity(timestamps: Sequence[float | int | str], + ts_range: None | tuple[float | int | str, float | int | str] = None, + granularity: str = 'day') -> NDArray[float]: + if not isinstance(timestamps, np.ndarray) or timestamps.dtype != float: + timestamps = np.array(timestamps, dtype=float) + if ts_range is None: + ts_range = (np.min(timestamps), np.max(timestamps)) + if granularity == 'week': + bin_width = SECONDS_WEEK + elif granularity == 'day': + bin_width = SECONDS_DAY + elif granularity == 'hour': + bin_width = SECONDS_HOUR + elif granularity[-1] == 'd' and _is_float(granularity[:-1]): + bin_width = float(granularity[:-1]) * SECONDS_DAY + elif granularity[-1] == 'h' and _is_float(granularity[:-1]): + bin_width = float(granularity[:-1]) * SECONDS_HOUR + else: + raise ValueError(f"Invalid granularity value: {granularity}!") + + # print(f"\n\nDEBUG: ts_range[0]: {ts_range[0]}, ts_range[1]: {ts_range[1]}, bin_width: {bin_width}\n\n") + + bins = np.arange(ts_range[0], ts_range[1], bin_width) + if not np.isclose(bins[-1], ts_range[1]): + bins = np.append(bins, ts_range[1]) + + activity, _ = np.histogram(np.array(timestamps, dtype=float), bins=bins) + activity.dtype = float + return activity + + +def _is_float(num: str) -> bool: + try: + float(num) + except ValueError: + return False + return True diff --git a/ephemerality/src/ephemerality_computation.py b/ephemerality/src/ephemerality_computation.py new file mode 100644 index 0000000..f8bbb09 --- /dev/null +++ b/ephemerality/src/ephemerality_computation.py @@ -0,0 +1,352 @@ +from typing import Sequence + +import numpy as np +from numpy.typing import NDArray +from matplotlib import pyplot as plt +from matplotlib.axes import Axes +from matplotlib.figure import Figure +from .utils import EphemeralitySet + + +def _check_threshold(threshold: float) -> bool: + if threshold <= 0.: + raise ValueError('Threshold value must be greater than 0!') + + if threshold > 1.: + raise ValueError('Threshold value must be less or equal to 1!') + + return True + + +def _ephemerality_raise_error(threshold: float) -> None: + if _check_threshold(threshold): + raise ValueError('Input activity vector has not been internally normalized (problematic data format?)!') + + +def _normalize_activity_vector(activity_vector: Sequence[float]) -> NDArray: + activity_vector = np.array(activity_vector, dtype=np.float64) + + if sum(activity_vector) != 1.: + activity_vector /= np.sum(activity_vector) + + return activity_vector + + +def _str_to_core_types(type_str: str) -> tuple[set[str], int]: + types = set() + for c in type_str.lower(): + match c: + case 'l': + types.add('l') + case 'm': + types.add('m') + case 'r': + types.add('r') + case 's': + types.add('s') + case _: + raise ValueError(f'Unrecognized core type: {c}') + n_types = len(types) + return types, n_types + + +def _init_fig(core_types: set[str]) -> tuple[Figure, list[Axes]]: + fig = plt.figure() + n_cores = len(core_types) + axes = list() + match n_cores: + case 1: + gs = fig.add_gridspec(nrows=1, ncols=1) + grid = [gs[0, 0]] + case 2: + gs = fig.add_gridspec(nrows=1, ncols=2) + grid = [gs[0, 0], gs[0, 1]] + case 3: + gs = fig.add_gridspec(nrows=2, ncols=1) + gs_0 = gs[0].subgridspec(nrows=1, ncols=2) + gs_1 = gs[1].subgridspec(nrows=1, ncols=3, width_ratios=[1, 2, 1]) + grid = [gs_0[0, 0], gs_0[0, 1], gs_1[0, 1]] + case 4: + gs = fig.add_gridspec(nrows=2, ncols=2) + grid = [gs[0, 0], gs[0, 1], gs[1, 0], gs[1, 1]] + for i in range(n_cores): + axes.append(fig.add_subplot(grid[i])) + + return fig, axes + + +def _annotate_ax(ax: Axes, x_len: int, core_type: str, core_length: int, ephemerality: float, threshold: float) -> Axes: + ax.set_xlim((0, x_len)) + ax.set_xlabel('Time') + ax.set_ylabel('Normalized activity') + ax.set_title(rf'{core_type} core (length {core_length}), $\varepsilon_{{{threshold}}}^{core_type[0].lower()}={np.round(ephemerality, 3)}$') + return ax + + +def compute_left_core_length(activity_vector: NDArray, threshold: float, axes: Axes | None = None) -> int: + """ + Compute the length of the left core of the activity vector. + + Parameters + ---------- + activity_vector : numpy.typing.NDArray + A normalized sequence of activity values. Time bins corresponding to each value is assumed to be of equal + length. + threshold : float + Ratio of activity that is considered to comprise its core part. + axes : Optional[Axes] + If provided, will plot the histogram of the activity vector and color its core part on it. + + Returns + ------- + int + The length of the left core period of the activity vector. + """ + current_sum = 0 + for i, freq in enumerate(activity_vector): + current_sum = current_sum + freq + if np.isclose(current_sum, threshold) or current_sum > threshold: + if axes is not None: + axes.stairs(activity_vector[:(i + 1)], np.arange(i + 2), fill=True, color='C1') + axes.stairs(activity_vector, np.arange(len(activity_vector) + 1), fill=False, color='C0') + return i + 1 + + _ephemerality_raise_error(threshold) + + +def compute_right_core_length(activity_vector: NDArray, threshold: float, axes: Axes | None = None) -> int: + """ + Compute the length of the right core of the activity vector. + + Parameters + ---------- + activity_vector : numpy.typing.NDArray + A normalized sequence of activity values. Time bins corresponding to each value is assumed to be of equal + length. + threshold : float + Ratio of activity that is considered to comprise its core part. + axes : Optional[Axes] + If provided, will plot the histogram of the activity vector and color its core part on it. + + Returns + ------- + int + The length of the right core period of the activity vector. + """ + current_sum = 0 + for i, freq in enumerate(activity_vector[::-1]): + current_sum = current_sum + freq + if np.isclose(current_sum, threshold) or current_sum > threshold: + if axes is not None: + bound = len(activity_vector) - (i + 1) + axes.stairs(activity_vector[bound:], np.arange(bound, len(activity_vector) + 1), fill=True, color='C1') + axes.stairs(activity_vector, np.arange(len(activity_vector) + 1), fill=False, color='C0') + return i + 1 + + _ephemerality_raise_error(threshold) + + +def compute_middle_core_length(activity_vector: NDArray, threshold: float, axes: Axes | None = None) -> int: + """ + Compute the length of the middle core of the activity vector. + + Parameters + ---------- + activity_vector : numpy.typing.NDArray + A normalized sequence of activity values. Time bins corresponding to each value is assumed to be of equal + length. + threshold : float + Ratio of activity that is considered to comprise its core part. + axes : Optional[Axes] + If provided, will plot the histogram of the activity vector and color its core part on it. + + Returns + ------- + int + The length of the middle core period of the activity vector. + """ + lower_threshold = (1. - threshold) / 2 + + current_left_sum = 0 + start_index = -1 + for i, freq in enumerate(activity_vector): + current_left_sum += freq + if current_left_sum > lower_threshold and not np.isclose(current_left_sum, lower_threshold): + start_index = i + break + + current_sum = 0 + for j, freq in enumerate(activity_vector[start_index:]): + current_sum += freq + if np.isclose(current_sum, threshold) or current_sum > threshold: + if axes is not None: + axes.stairs(activity_vector[start_index:(start_index + j + 1)], np.arange(start_index, start_index + j + 2), fill=True, color='C1') + axes.stairs(activity_vector, np.arange(len(activity_vector) + 1), fill=False, color='C0') + return j + 1 + + _ephemerality_raise_error(threshold) + + +def compute_sorted_core_length(activity_vector: np.array, threshold: float, axes: Axes | None = None) -> int: + """ + Compute the length of the sorted core of the activity vector. + + Parameters + ---------- + activity_vector : numpy.typing.NDArray + A normalized sequence of activity values. Time bins corresponding to each value is assumed to be of equal + length. + threshold : float + Ratio of activity that is considered to comprise its core part. + axes : Optional[Axes] + If provided, will plot the histogram of the activity vector and color its core part on it. + + Returns + ------- + int + The length of the sorted core period of the activity vector. + """ + indices = np.argsort(activity_vector)[::-1] + freq_descending_order = activity_vector[indices] + + current_sum = 0 + for i, freq in enumerate(freq_descending_order): + current_sum += freq + if np.isclose(current_sum, threshold) or current_sum > threshold: + if axes is not None: + core = np.zeros((len(activity_vector),)) + core[indices[:(i + 1)]] = activity_vector[indices[:(i + 1)]] + axes.stairs(core, np.arange(len(activity_vector) + 1), fill=True, color='C1') + + axes.stairs(activity_vector, np.arange(len(activity_vector) + 1), fill=False, color='C0') + return i + 1 + + _ephemerality_raise_error(threshold) + + +def _compute_ephemerality_from_core(core_length: int, range_length: int, threshold: float) -> float: + return max(0., 1 - (core_length / range_length) / threshold) + + +def _plot(axes: list[Axes]) -> None: + fig = plt.figure() + n_cores = len(axes) + + +def compute_ephemerality( + activity_vector: Sequence[float | int], + threshold: float = 0.8, + types: str = 'lmrs', + plot: bool = False) -> EphemeralitySet: + """Compute ephemerality values for given activity vector. + + This function computes ephemerality for a numeric vector using all current definitions of actiovity cores. + Alpha (desired non-ephemeral core length) can be specified with ``threshold parameter. In case not all cores are + needed, the required types can be specified in ``types``. + + Parameters + ---------- + activity_vector : Sequence[float | int] + A sequence of activity values. Time bins corresponding to each value is assumed to be of equal length. Does not + need to be normalised. + threshold : float, default=0.8 + Desired non-ephemeral core length as fraction. Ephemerality is equal to 1.0 if the core length is at least ``threshold`` of + the ``activity_vector`` length. + types : str, default='lmrs' + Activity cores to be computed. A sequence of characters corresponding to core types. + 'l' for left core, 'm' for middle core, 'r' for right core, 's' for sorted core. Multiples of the same character + will be ignored. + plot : bool, default=False + Set to True to display the activity over time plot with core periods highlighted. + + Returns + ------- + EphemeralitySet + Container for the computed core lengths and ephemerality values + """ + _check_threshold(threshold) + + if np.sum(activity_vector) == 0.: + raise ZeroDivisionError("Activity vector's sum is 0!") + types, n_cores = _str_to_core_types(types) + + activity_vector = _normalize_activity_vector(activity_vector) + len_range = len(activity_vector) + + if plot: + fig, axes = _init_fig(types) + else: + axes = None + subplot_types = list() + + core_ind = 0 + if 'l' in types: + ax = axes[core_ind] if plot else None + subplot_types.append('l') + length_left_core = compute_left_core_length(activity_vector, threshold, axes=ax) + ephemerality_left_core = _compute_ephemerality_from_core(length_left_core, len_range, threshold) + core_ind += 1 + else: + length_left_core = None + ephemerality_left_core = None + + if 'm' in types: + ax = axes[core_ind] if plot else None + subplot_types.append('m') + length_middle_core = compute_middle_core_length(activity_vector, threshold, axes=ax) + ephemerality_middle_core = _compute_ephemerality_from_core(length_middle_core, len_range, threshold) + core_ind += 1 + else: + length_middle_core = None + ephemerality_middle_core = None + + if 'r' in types: + ax = axes[core_ind] if plot else None + subplot_types.append('r') + length_right_core = compute_right_core_length(activity_vector, threshold, axes=ax) + ephemerality_right_core = _compute_ephemerality_from_core(length_right_core, len_range, threshold) + core_ind += 1 + else: + length_right_core = None + ephemerality_right_core = None + + if 's' in types: + ax = axes[core_ind] if plot else None + subplot_types.append('s') + length_sorted_core = compute_sorted_core_length(activity_vector, threshold, axes=ax) + ephemerality_sorted_core = _compute_ephemerality_from_core(length_sorted_core, len_range, threshold) + core_ind += 1 + else: + length_sorted_core = None + ephemerality_sorted_core = None + + if n_cores == 0: + raise ValueError('No valid ephemerality cores requested!') + + ephemeralities = EphemeralitySet( + len_left_core=length_left_core, + len_middle_core=length_middle_core, + len_right_core=length_right_core, + len_sorted_core=length_sorted_core, + + eph_left_core=ephemerality_left_core, + eph_middle_core=ephemerality_middle_core, + eph_right_core=ephemerality_right_core, + eph_sorted_core=ephemerality_sorted_core + ) + + if plot: + for i, subplot_type in enumerate(subplot_types): + match subplot_type: + case 'l': + _annotate_ax(axes[i], len_range, 'Left', ephemeralities.len_left_core, ephemeralities.eph_left_core, threshold) + case 'm': + _annotate_ax(axes[i], len_range, 'Middle', ephemeralities.len_middle_core, ephemeralities.eph_middle_core, threshold) + case 'r': + _annotate_ax(axes[i], len_range, 'Right', ephemeralities.len_right_core, ephemeralities.eph_right_core, threshold) + case 's': + _annotate_ax(axes[i], len_range, 'Sorted', ephemeralities.len_sorted_core, ephemeralities.eph_sorted_core, threshold) + fig.tight_layout() + fig.show() + + return ephemeralities diff --git a/ephemerality/src/utils.py b/ephemerality/src/utils.py new file mode 100644 index 0000000..5063891 --- /dev/null +++ b/ephemerality/src/utils.py @@ -0,0 +1,73 @@ +import numpy as np +from pydantic import BaseModel + + +class EphemeralitySet(BaseModel): + """ + Container for ephemerality and core size values. + + This class is a simple pydantic BaseModel used to store computed core lengths and corresponding ephemerality values + by core type. Values that were not computed default to None. + + Attributes + ---------- + len_left_core : int, optional + Length of the left core in time bins + len_middle_core : int, optional + Length of the middle core in time bins + len_right_core : int, optional + Length of the right core in time bins + len_sorted_core : int, optional + Length of the sorted core in time bins + + eph_left_core: float, optional + Ephemerality value for the left core + eph_middle_core: float, optional + Ephemerality value for the middle core + eph_right_core: float, optional + Ephemerality value for the right core + eph_sorted_core: float, optional + Ephemerality value for the sorted core + """ + len_left_core: int | None = None + len_middle_core: int | None = None + len_right_core: int | None = None + len_sorted_core: int | None = None + + eph_left_core: float | None = None + eph_middle_core: float | None = None + eph_right_core: float | None = None + eph_sorted_core: float | None = None + + def __eq__(self, other) -> bool: + if isinstance(other, EphemeralitySet): + if \ + self.len_left_core != other.len_left_core or \ + self.len_middle_core != other.len_middle_core or \ + self.len_right_core != other.len_right_core or \ + self.len_sorted_core != other.len_sorted_core or \ + not self._cmp_float_none(self.eph_left_core, other.eph_left_core) or \ + not self._cmp_float_none(self.eph_middle_core, other.eph_middle_core) or \ + not self._cmp_float_none(self.eph_right_core, other.eph_right_core) or \ + not self._cmp_float_none(self.eph_sorted_core, other.eph_sorted_core): + return False + else: + return True + else: + return False + + @staticmethod + def _cmp_float_none(value1: float | None, value2: float | None) -> bool: + if value1 is None and value2 is None: + return True + elif value1 is None or value2 is None: + return False + else: + return np.isclose(value1, value2) + + def __str__(self) -> str: + values = ', '.join([f'{pair[0]}={pair[1]}' for pair in sorted(list(self.dict(exclude_none=True).items()), key=lambda x: x[0])]) + return f'{self.__class__.__name__}({values})' + + def __repr__(self) -> str: + return str(self) diff --git a/images/example_1.png b/images/example_1.png new file mode 100644 index 0000000..53d5596 Binary files /dev/null and b/images/example_1.png differ diff --git a/images/example_2.png b/images/example_2.png new file mode 100644 index 0000000..369081a Binary files /dev/null and b/images/example_2.png differ diff --git a/images/example_3.png b/images/example_3.png new file mode 100644 index 0000000..0aef215 Binary files /dev/null and b/images/example_3.png differ diff --git a/images/example_4.png b/images/example_4.png new file mode 100644 index 0000000..84149f7 Binary files /dev/null and b/images/example_4.png differ diff --git a/images/results_1.png b/images/results_1.png new file mode 100644 index 0000000..1afbb7a Binary files /dev/null and b/images/results_1.png differ diff --git a/images/results_2.png b/images/results_2.png new file mode 100644 index 0000000..d07fef7 Binary files /dev/null and b/images/results_2.png differ diff --git a/images/results_3.png b/images/results_3.png new file mode 100644 index 0000000..8db530c Binary files /dev/null and b/images/results_3.png differ diff --git a/images/results_4.png b/images/results_4.png new file mode 100644 index 0000000..c8e4238 Binary files /dev/null and b/images/results_4.png differ diff --git a/poetry.lock b/poetry.lock new file mode 100644 index 0000000..d0a8124 --- /dev/null +++ b/poetry.lock @@ -0,0 +1,771 @@ +# This file is automatically @generated by Poetry 1.8.2 and should not be changed by hand. + +[[package]] +name = "annotated-types" +version = "0.6.0" +description = "Reusable constraint types to use with typing.Annotated" +optional = false +python-versions = ">=3.8" +files = [ + {file = "annotated_types-0.6.0-py3-none-any.whl", hash = "sha256:0641064de18ba7a25dee8f96403ebc39113d0cb953a01429249d5c7564666a43"}, + {file = "annotated_types-0.6.0.tar.gz", hash = "sha256:563339e807e53ffd9c267e99fc6d9ea23eb8443c08f112651963e24e22f84a5d"}, +] + +[[package]] +name = "anyio" +version = "4.3.0" +description = "High level compatibility layer for multiple asynchronous event loop implementations" +optional = false +python-versions = ">=3.8" +files = [ + {file = "anyio-4.3.0-py3-none-any.whl", hash = "sha256:048e05d0f6caeed70d731f3db756d35dcc1f35747c8c403364a8332c630441b8"}, + {file = "anyio-4.3.0.tar.gz", hash = "sha256:f75253795a87df48568485fd18cdd2a3fa5c4f7c5be8e5e36637733fce06fed6"}, +] + +[package.dependencies] +exceptiongroup = {version = ">=1.0.2", markers = "python_version < \"3.11\""} +idna = ">=2.8" +sniffio = ">=1.1" +typing-extensions = {version = ">=4.1", markers = "python_version < \"3.11\""} + +[package.extras] +doc = ["Sphinx (>=7)", "packaging", "sphinx-autodoc-typehints (>=1.2.0)", "sphinx-rtd-theme"] +test = ["anyio[trio]", "coverage[toml] (>=7)", "exceptiongroup (>=1.2.0)", "hypothesis (>=4.0)", "psutil (>=5.9)", "pytest (>=7.0)", "pytest-mock (>=3.6.1)", "trustme", "uvloop (>=0.17)"] +trio = ["trio (>=0.23)"] + +[[package]] +name = "click" +version = "8.1.7" +description = "Composable command line interface toolkit" +optional = false +python-versions = ">=3.7" +files = [ + {file = "click-8.1.7-py3-none-any.whl", hash = "sha256:ae74fb96c20a0277a1d615f1e4d73c8414f5a98db8b799a7931d1582f3390c28"}, + {file = "click-8.1.7.tar.gz", hash = "sha256:ca9853ad459e787e2192211578cc907e7594e294c7ccc834310722b41b9ca6de"}, +] + +[package.dependencies] +colorama = {version = "*", markers = "platform_system == \"Windows\""} + +[[package]] +name = "colorama" +version = "0.4.6" +description = "Cross-platform colored terminal text." +optional = false +python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7" +files = [ + {file = "colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6"}, + {file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"}, +] + +[[package]] +name = "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" +description = "Backport of PEP 654 (exception groups)" +optional = false +python-versions = ">=3.7" +files = [ + {file = "exceptiongroup-1.2.1-py3-none-any.whl", hash = "sha256:5258b9ed329c5bbdd31a309f53cbfb0b155341807f6ff7606a1e801a891b29ad"}, + {file = "exceptiongroup-1.2.1.tar.gz", hash = "sha256:a4785e48b045528f5bfe627b6ad554ff32def154f42372786903b7abcfe1aa16"}, +] + +[package.extras] +test = ["pytest (>=6)"] + +[[package]] +name = "fastapi" +version = "0.110.3" +description = "FastAPI framework, high performance, easy to learn, fast to code, ready for production" +optional = false +python-versions = ">=3.8" +files = [ + {file = "fastapi-0.110.3-py3-none-any.whl", hash = "sha256:fd7600612f755e4050beb74001310b5a7e1796d149c2ee363124abdfa0289d32"}, + {file = "fastapi-0.110.3.tar.gz", hash = "sha256:555700b0159379e94fdbfc6bb66a0f1c43f4cf7060f25239af3d84b63a656626"}, +] + +[package.dependencies] +pydantic = ">=1.7.4,<1.8 || >1.8,<1.8.1 || >1.8.1,<2.0.0 || >2.0.0,<2.0.1 || >2.0.1,<2.1.0 || >2.1.0,<3.0.0" +starlette = ">=0.37.2,<0.38.0" +typing-extensions = ">=4.8.0" + +[package.extras] +all = ["email_validator (>=2.0.0)", "httpx (>=0.23.0)", "itsdangerous (>=1.1.0)", "jinja2 (>=2.11.2)", "orjson (>=3.2.1)", "pydantic-extra-types (>=2.0.0)", "pydantic-settings (>=2.0.0)", "python-multipart (>=0.0.7)", "pyyaml (>=5.3.1)", "ujson (>=4.0.1,!=4.0.2,!=4.1.0,!=4.2.0,!=4.3.0,!=5.0.0,!=5.1.0)", "uvicorn[standard] (>=0.12.0)"] + +[[package]] +name = "fonttools" +version = "4.51.0" +description = "Tools to manipulate font files" +optional = false +python-versions = ">=3.8" +files = [ + {file = "fonttools-4.51.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:84d7751f4468dd8cdd03ddada18b8b0857a5beec80bce9f435742abc9a851a74"}, + {file = "fonttools-4.51.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:8b4850fa2ef2cfbc1d1f689bc159ef0f45d8d83298c1425838095bf53ef46308"}, + {file = "fonttools-4.51.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b5b48a1121117047d82695d276c2af2ee3a24ffe0f502ed581acc2673ecf1037"}, + {file = "fonttools-4.51.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:180194c7fe60c989bb627d7ed5011f2bef1c4d36ecf3ec64daec8302f1ae0716"}, + {file = "fonttools-4.51.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:96a48e137c36be55e68845fc4284533bda2980f8d6f835e26bca79d7e2006438"}, + {file = "fonttools-4.51.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:806e7912c32a657fa39d2d6eb1d3012d35f841387c8fc6cf349ed70b7c340039"}, + {file = "fonttools-4.51.0-cp310-cp310-win32.whl", hash = "sha256:32b17504696f605e9e960647c5f64b35704782a502cc26a37b800b4d69ff3c77"}, + {file = "fonttools-4.51.0-cp310-cp310-win_amd64.whl", hash = "sha256:c7e91abdfae1b5c9e3a543f48ce96013f9a08c6c9668f1e6be0beabf0a569c1b"}, + {file = "fonttools-4.51.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:a8feca65bab31479d795b0d16c9a9852902e3a3c0630678efb0b2b7941ea9c74"}, + {file = "fonttools-4.51.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:8ac27f436e8af7779f0bb4d5425aa3535270494d3bc5459ed27de3f03151e4c2"}, + {file = "fonttools-4.51.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0e19bd9e9964a09cd2433a4b100ca7f34e34731e0758e13ba9a1ed6e5468cc0f"}, + {file = "fonttools-4.51.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b2b92381f37b39ba2fc98c3a45a9d6383bfc9916a87d66ccb6553f7bdd129097"}, + {file = "fonttools-4.51.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:5f6bc991d1610f5c3bbe997b0233cbc234b8e82fa99fc0b2932dc1ca5e5afec0"}, + {file = "fonttools-4.51.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:9696fe9f3f0c32e9a321d5268208a7cc9205a52f99b89479d1b035ed54c923f1"}, + {file = "fonttools-4.51.0-cp311-cp311-win32.whl", hash = "sha256:3bee3f3bd9fa1d5ee616ccfd13b27ca605c2b4270e45715bd2883e9504735034"}, + {file = "fonttools-4.51.0-cp311-cp311-win_amd64.whl", hash = "sha256:0f08c901d3866a8905363619e3741c33f0a83a680d92a9f0e575985c2634fcc1"}, + {file = "fonttools-4.51.0-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:4060acc2bfa2d8e98117828a238889f13b6f69d59f4f2d5857eece5277b829ba"}, + {file = "fonttools-4.51.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:1250e818b5f8a679ad79660855528120a8f0288f8f30ec88b83db51515411fcc"}, + {file = "fonttools-4.51.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:76f1777d8b3386479ffb4a282e74318e730014d86ce60f016908d9801af9ca2a"}, + {file = "fonttools-4.51.0-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8b5ad456813d93b9c4b7ee55302208db2b45324315129d85275c01f5cb7e61a2"}, + {file = "fonttools-4.51.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:68b3fb7775a923be73e739f92f7e8a72725fd333eab24834041365d2278c3671"}, + {file = "fonttools-4.51.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:8e2f1a4499e3b5ee82c19b5ee57f0294673125c65b0a1ff3764ea1f9db2f9ef5"}, + {file = "fonttools-4.51.0-cp312-cp312-win32.whl", hash = "sha256:278e50f6b003c6aed19bae2242b364e575bcb16304b53f2b64f6551b9c000e15"}, + {file = "fonttools-4.51.0-cp312-cp312-win_amd64.whl", hash = "sha256:b3c61423f22165541b9403ee39874dcae84cd57a9078b82e1dce8cb06b07fa2e"}, + {file = "fonttools-4.51.0-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:1621ee57da887c17312acc4b0e7ac30d3a4fb0fec6174b2e3754a74c26bbed1e"}, + {file = "fonttools-4.51.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:e9d9298be7a05bb4801f558522adbe2feea1b0b103d5294ebf24a92dd49b78e5"}, + {file = "fonttools-4.51.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ee1af4be1c5afe4c96ca23badd368d8dc75f611887fb0c0dac9f71ee5d6f110e"}, + {file = "fonttools-4.51.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c18b49adc721a7d0b8dfe7c3130c89b8704baf599fb396396d07d4aa69b824a1"}, + {file = "fonttools-4.51.0-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:de7c29bdbdd35811f14493ffd2534b88f0ce1b9065316433b22d63ca1cd21f14"}, + {file = "fonttools-4.51.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:cadf4e12a608ef1d13e039864f484c8a968840afa0258b0b843a0556497ea9ed"}, + {file = "fonttools-4.51.0-cp38-cp38-win32.whl", hash = "sha256:aefa011207ed36cd280babfaa8510b8176f1a77261833e895a9d96e57e44802f"}, + {file = "fonttools-4.51.0-cp38-cp38-win_amd64.whl", hash = "sha256:865a58b6e60b0938874af0968cd0553bcd88e0b2cb6e588727117bd099eef836"}, + {file = "fonttools-4.51.0-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:60a3409c9112aec02d5fb546f557bca6efa773dcb32ac147c6baf5f742e6258b"}, + {file = "fonttools-4.51.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:f7e89853d8bea103c8e3514b9f9dc86b5b4120afb4583b57eb10dfa5afbe0936"}, + {file = "fonttools-4.51.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:56fc244f2585d6c00b9bcc59e6593e646cf095a96fe68d62cd4da53dd1287b55"}, + {file = "fonttools-4.51.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0d145976194a5242fdd22df18a1b451481a88071feadf251221af110ca8f00ce"}, + {file = "fonttools-4.51.0-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:c5b8cab0c137ca229433570151b5c1fc6af212680b58b15abd797dcdd9dd5051"}, + {file = "fonttools-4.51.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:54dcf21a2f2d06ded676e3c3f9f74b2bafded3a8ff12f0983160b13e9f2fb4a7"}, + {file = "fonttools-4.51.0-cp39-cp39-win32.whl", hash = "sha256:0118ef998a0699a96c7b28457f15546815015a2710a1b23a7bf6c1be60c01636"}, + {file = "fonttools-4.51.0-cp39-cp39-win_amd64.whl", hash = "sha256:599bdb75e220241cedc6faebfafedd7670335d2e29620d207dd0378a4e9ccc5a"}, + {file = "fonttools-4.51.0-py3-none-any.whl", hash = "sha256:15c94eeef6b095831067f72c825eb0e2d48bb4cea0647c1b05c981ecba2bf39f"}, + {file = "fonttools-4.51.0.tar.gz", hash = "sha256:dc0673361331566d7a663d7ce0f6fdcbfbdc1f59c6e3ed1165ad7202ca183c68"}, +] + +[package.extras] +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 = "h11" +version = "0.14.0" +description = "A pure-Python, bring-your-own-I/O implementation of HTTP/1.1" +optional = false +python-versions = ">=3.7" +files = [ + {file = "h11-0.14.0-py3-none-any.whl", hash = "sha256:e3fe4ac4b851c468cc8363d500db52c2ead036020723024a109d37346efaa761"}, + {file = "h11-0.14.0.tar.gz", hash = "sha256:8f19fbbe99e72420ff35c00b27a34cb9937e902a8b810e2c88300c6f0a3b699d"}, +] + +[[package]] +name = "idna" +version = "3.7" +description = "Internationalized Domain Names in Applications (IDNA)" +optional = false +python-versions = ">=3.5" +files = [ + {file = "idna-3.7-py3-none-any.whl", hash = "sha256:82fee1fc78add43492d3a1898bfa6d8a904cc97d8427f683ed8e798d07761aa0"}, + {file = "idna-3.7.tar.gz", hash = "sha256:028ff3aadf0609c1fd278d8ea3089299412a7a8b9bd005dd08b9f8285bcb5cfc"}, +] + +[[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 = "matplotlib" +version = "3.8.4" +description = "Python plotting package" +optional = false +python-versions = ">=3.9" +files = [ + {file = "matplotlib-3.8.4-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:abc9d838f93583650c35eca41cfcec65b2e7cb50fd486da6f0c49b5e1ed23014"}, + {file = "matplotlib-3.8.4-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:8f65c9f002d281a6e904976007b2d46a1ee2bcea3a68a8c12dda24709ddc9106"}, + {file = "matplotlib-3.8.4-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ce1edd9f5383b504dbc26eeea404ed0a00656c526638129028b758fd43fc5f10"}, + {file = "matplotlib-3.8.4-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ecd79298550cba13a43c340581a3ec9c707bd895a6a061a78fa2524660482fc0"}, + {file = "matplotlib-3.8.4-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:90df07db7b599fe7035d2f74ab7e438b656528c68ba6bb59b7dc46af39ee48ef"}, + {file = "matplotlib-3.8.4-cp310-cp310-win_amd64.whl", hash = "sha256:ac24233e8f2939ac4fd2919eed1e9c0871eac8057666070e94cbf0b33dd9c338"}, + {file = "matplotlib-3.8.4-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:72f9322712e4562e792b2961971891b9fbbb0e525011e09ea0d1f416c4645661"}, + {file = "matplotlib-3.8.4-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:232ce322bfd020a434caaffbd9a95333f7c2491e59cfc014041d95e38ab90d1c"}, + {file = "matplotlib-3.8.4-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6addbd5b488aedb7f9bc19f91cd87ea476206f45d7116fcfe3d31416702a82fa"}, + {file = "matplotlib-3.8.4-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cc4ccdc64e3039fc303defd119658148f2349239871db72cd74e2eeaa9b80b71"}, + {file = "matplotlib-3.8.4-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:b7a2a253d3b36d90c8993b4620183b55665a429da8357a4f621e78cd48b2b30b"}, + {file = "matplotlib-3.8.4-cp311-cp311-win_amd64.whl", hash = "sha256:8080d5081a86e690d7688ffa542532e87f224c38a6ed71f8fbed34dd1d9fedae"}, + {file = "matplotlib-3.8.4-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:6485ac1f2e84676cff22e693eaa4fbed50ef5dc37173ce1f023daef4687df616"}, + {file = "matplotlib-3.8.4-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:c89ee9314ef48c72fe92ce55c4e95f2f39d70208f9f1d9db4e64079420d8d732"}, + {file = "matplotlib-3.8.4-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:50bac6e4d77e4262c4340d7a985c30912054745ec99756ce213bfbc3cb3808eb"}, + {file = "matplotlib-3.8.4-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f51c4c869d4b60d769f7b4406eec39596648d9d70246428745a681c327a8ad30"}, + {file = "matplotlib-3.8.4-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:b12ba985837e4899b762b81f5b2845bd1a28f4fdd1a126d9ace64e9c4eb2fb25"}, + {file = "matplotlib-3.8.4-cp312-cp312-win_amd64.whl", hash = "sha256:7a6769f58ce51791b4cb8b4d7642489df347697cd3e23d88266aaaee93b41d9a"}, + {file = "matplotlib-3.8.4-cp39-cp39-macosx_10_12_x86_64.whl", hash = "sha256:843cbde2f0946dadd8c5c11c6d91847abd18ec76859dc319362a0964493f0ba6"}, + {file = "matplotlib-3.8.4-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:1c13f041a7178f9780fb61cc3a2b10423d5e125480e4be51beaf62b172413b67"}, + {file = "matplotlib-3.8.4-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fb44f53af0a62dc80bba4443d9b27f2fde6acfdac281d95bc872dc148a6509cc"}, + {file = "matplotlib-3.8.4-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:606e3b90897554c989b1e38a258c626d46c873523de432b1462f295db13de6f9"}, + {file = "matplotlib-3.8.4-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:9bb0189011785ea794ee827b68777db3ca3f93f3e339ea4d920315a0e5a78d54"}, + {file = "matplotlib-3.8.4-cp39-cp39-win_amd64.whl", hash = "sha256:6209e5c9aaccc056e63b547a8152661324404dd92340a6e479b3a7f24b42a5d0"}, + {file = "matplotlib-3.8.4-pp39-pypy39_pp73-macosx_10_12_x86_64.whl", hash = "sha256:c7064120a59ce6f64103c9cefba8ffe6fba87f2c61d67c401186423c9a20fd35"}, + {file = "matplotlib-3.8.4-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a0e47eda4eb2614300fc7bb4657fced3e83d6334d03da2173b09e447418d499f"}, + {file = "matplotlib-3.8.4-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:493e9f6aa5819156b58fce42b296ea31969f2aab71c5b680b4ea7a3cb5c07d94"}, + {file = "matplotlib-3.8.4.tar.gz", hash = "sha256:8aac397d5e9ec158960e31c381c5ffc52ddd52bd9a47717e2a694038167dffea"}, +] + +[package.dependencies] +contourpy = ">=1.0.1" +cycler = ">=0.10" +fonttools = ">=4.22.0" +kiwisolver = ">=1.3.1" +numpy = ">=1.21" +packaging = ">=20.0" +pillow = ">=8" +pyparsing = ">=2.3.1" +python-dateutil = ">=2.7" + +[[package]] +name = "numpy" +version = "1.26.4" +description = "Fundamental package for array computing in Python" +optional = false +python-versions = ">=3.9" +files = [ + {file = "numpy-1.26.4-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:9ff0f4f29c51e2803569d7a51c2304de5554655a60c5d776e35b4a41413830d0"}, + {file = "numpy-1.26.4-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:2e4ee3380d6de9c9ec04745830fd9e2eccb3e6cf790d39d7b98ffd19b0dd754a"}, + {file = "numpy-1.26.4-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d209d8969599b27ad20994c8e41936ee0964e6da07478d6c35016bc386b66ad4"}, + {file = "numpy-1.26.4-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ffa75af20b44f8dba823498024771d5ac50620e6915abac414251bd971b4529f"}, + {file = "numpy-1.26.4-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:62b8e4b1e28009ef2846b4c7852046736bab361f7aeadeb6a5b89ebec3c7055a"}, + {file = "numpy-1.26.4-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:a4abb4f9001ad2858e7ac189089c42178fcce737e4169dc61321660f1a96c7d2"}, + {file = "numpy-1.26.4-cp310-cp310-win32.whl", hash = "sha256:bfe25acf8b437eb2a8b2d49d443800a5f18508cd811fea3181723922a8a82b07"}, + {file = "numpy-1.26.4-cp310-cp310-win_amd64.whl", hash = "sha256:b97fe8060236edf3662adfc2c633f56a08ae30560c56310562cb4f95500022d5"}, + {file = "numpy-1.26.4-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:4c66707fabe114439db9068ee468c26bbdf909cac0fb58686a42a24de1760c71"}, + {file = "numpy-1.26.4-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:edd8b5fe47dab091176d21bb6de568acdd906d1887a4584a15a9a96a1dca06ef"}, + {file = "numpy-1.26.4-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7ab55401287bfec946ced39700c053796e7cc0e3acbef09993a9ad2adba6ca6e"}, + {file = "numpy-1.26.4-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:666dbfb6ec68962c033a450943ded891bed2d54e6755e35e5835d63f4f6931d5"}, + {file = "numpy-1.26.4-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:96ff0b2ad353d8f990b63294c8986f1ec3cb19d749234014f4e7eb0112ceba5a"}, + {file = "numpy-1.26.4-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:60dedbb91afcbfdc9bc0b1f3f402804070deed7392c23eb7a7f07fa857868e8a"}, + {file = "numpy-1.26.4-cp311-cp311-win32.whl", hash = "sha256:1af303d6b2210eb850fcf03064d364652b7120803a0b872f5211f5234b399f20"}, + {file = "numpy-1.26.4-cp311-cp311-win_amd64.whl", hash = "sha256:cd25bcecc4974d09257ffcd1f098ee778f7834c3ad767fe5db785be9a4aa9cb2"}, + {file = "numpy-1.26.4-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:b3ce300f3644fb06443ee2222c2201dd3a89ea6040541412b8fa189341847218"}, + {file = "numpy-1.26.4-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:03a8c78d01d9781b28a6989f6fa1bb2c4f2d51201cf99d3dd875df6fbd96b23b"}, + {file = "numpy-1.26.4-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9fad7dcb1aac3c7f0584a5a8133e3a43eeb2fe127f47e3632d43d677c66c102b"}, + {file = "numpy-1.26.4-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:675d61ffbfa78604709862923189bad94014bef562cc35cf61d3a07bba02a7ed"}, + {file = "numpy-1.26.4-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:ab47dbe5cc8210f55aa58e4805fe224dac469cde56b9f731a4c098b91917159a"}, + {file = "numpy-1.26.4-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:1dda2e7b4ec9dd512f84935c5f126c8bd8b9f2fc001e9f54af255e8c5f16b0e0"}, + {file = "numpy-1.26.4-cp312-cp312-win32.whl", hash = "sha256:50193e430acfc1346175fcbdaa28ffec49947a06918b7b92130744e81e640110"}, + {file = "numpy-1.26.4-cp312-cp312-win_amd64.whl", hash = "sha256:08beddf13648eb95f8d867350f6a018a4be2e5ad54c8d8caed89ebca558b2818"}, + {file = "numpy-1.26.4-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:7349ab0fa0c429c82442a27a9673fc802ffdb7c7775fad780226cb234965e53c"}, + {file = "numpy-1.26.4-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:52b8b60467cd7dd1e9ed082188b4e6bb35aa5cdd01777621a1658910745b90be"}, + {file = "numpy-1.26.4-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d5241e0a80d808d70546c697135da2c613f30e28251ff8307eb72ba696945764"}, + {file = "numpy-1.26.4-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f870204a840a60da0b12273ef34f7051e98c3b5961b61b0c2c1be6dfd64fbcd3"}, + {file = "numpy-1.26.4-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:679b0076f67ecc0138fd2ede3a8fd196dddc2ad3254069bcb9faf9a79b1cebcd"}, + {file = "numpy-1.26.4-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:47711010ad8555514b434df65f7d7b076bb8261df1ca9bb78f53d3b2db02e95c"}, + {file = "numpy-1.26.4-cp39-cp39-win32.whl", hash = "sha256:a354325ee03388678242a4d7ebcd08b5c727033fcff3b2f536aea978e15ee9e6"}, + {file = "numpy-1.26.4-cp39-cp39-win_amd64.whl", hash = "sha256:3373d5d70a5fe74a2c1bb6d2cfd9609ecf686d47a2d7b1d37a8f3b6bf6003aea"}, + {file = "numpy-1.26.4-pp39-pypy39_pp73-macosx_10_9_x86_64.whl", hash = "sha256:afedb719a9dcfc7eaf2287b839d8198e06dcd4cb5d276a3df279231138e83d30"}, + {file = "numpy-1.26.4-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:95a7476c59002f2f6c590b9b7b998306fba6a5aa646b1e22ddfeaf8f78c3a29c"}, + {file = "numpy-1.26.4-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:7e50d0a0cc3189f9cb0aeb3a6a6af18c16f59f004b866cd2be1c14b36134a4a0"}, + {file = "numpy-1.26.4.tar.gz", hash = "sha256:2a02aba9ed12e4ac4eb3ea9421c420301a0c6460d9830d74a9df87efa4912010"}, +] + +[[package]] +name = "packaging" +version = "24.0" +description = "Core utilities for Python packages" +optional = false +python-versions = ">=3.7" +files = [ + {file = "packaging-24.0-py3-none-any.whl", hash = "sha256:2ddfb553fdf02fb784c234c7ba6ccc288296ceabec964ad2eae3777778130bc5"}, + {file = "packaging-24.0.tar.gz", hash = "sha256:eb82c5e3e56209074766e6885bb04b8c38a0c015d0a30036ebe7ece34c9989e9"}, +] + +[[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 = "pydantic" +version = "2.7.1" +description = "Data validation using Python type hints" +optional = false +python-versions = ">=3.8" +files = [ + {file = "pydantic-2.7.1-py3-none-any.whl", hash = "sha256:e029badca45266732a9a79898a15ae2e8b14840b1eabbb25844be28f0b33f3d5"}, + {file = "pydantic-2.7.1.tar.gz", hash = "sha256:e9dbb5eada8abe4d9ae5f46b9939aead650cd2b68f249bb3a8139dbe125803cc"}, +] + +[package.dependencies] +annotated-types = ">=0.4.0" +pydantic-core = "2.18.2" +typing-extensions = ">=4.6.1" + +[package.extras] +email = ["email-validator (>=2.0.0)"] + +[[package]] +name = "pydantic-core" +version = "2.18.2" +description = "Core functionality for Pydantic validation and serialization" +optional = false +python-versions = ">=3.8" +files = [ + {file = "pydantic_core-2.18.2-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:9e08e867b306f525802df7cd16c44ff5ebbe747ff0ca6cf3fde7f36c05a59a81"}, + {file = "pydantic_core-2.18.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:f0a21cbaa69900cbe1a2e7cad2aa74ac3cf21b10c3efb0fa0b80305274c0e8a2"}, + {file = "pydantic_core-2.18.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0680b1f1f11fda801397de52c36ce38ef1c1dc841a0927a94f226dea29c3ae3d"}, + {file = "pydantic_core-2.18.2-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:95b9d5e72481d3780ba3442eac863eae92ae43a5f3adb5b4d0a1de89d42bb250"}, + {file = "pydantic_core-2.18.2-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c4fcf5cd9c4b655ad666ca332b9a081112cd7a58a8b5a6ca7a3104bc950f2038"}, + {file = "pydantic_core-2.18.2-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9b5155ff768083cb1d62f3e143b49a8a3432e6789a3abee8acd005c3c7af1c74"}, + {file = "pydantic_core-2.18.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:553ef617b6836fc7e4df130bb851e32fe357ce36336d897fd6646d6058d980af"}, + {file = "pydantic_core-2.18.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:b89ed9eb7d616ef5714e5590e6cf7f23b02d0d539767d33561e3675d6f9e3857"}, + {file = "pydantic_core-2.18.2-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:75f7e9488238e920ab6204399ded280dc4c307d034f3924cd7f90a38b1829563"}, + {file = "pydantic_core-2.18.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:ef26c9e94a8c04a1b2924149a9cb081836913818e55681722d7f29af88fe7b38"}, + {file = "pydantic_core-2.18.2-cp310-none-win32.whl", hash = "sha256:182245ff6b0039e82b6bb585ed55a64d7c81c560715d1bad0cbad6dfa07b4027"}, + {file = "pydantic_core-2.18.2-cp310-none-win_amd64.whl", hash = "sha256:e23ec367a948b6d812301afc1b13f8094ab7b2c280af66ef450efc357d2ae543"}, + {file = "pydantic_core-2.18.2-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:219da3f096d50a157f33645a1cf31c0ad1fe829a92181dd1311022f986e5fbe3"}, + {file = "pydantic_core-2.18.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:cc1cfd88a64e012b74e94cd00bbe0f9c6df57049c97f02bb07d39e9c852e19a4"}, + {file = "pydantic_core-2.18.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:05b7133a6e6aeb8df37d6f413f7705a37ab4031597f64ab56384c94d98fa0e90"}, + {file = "pydantic_core-2.18.2-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:224c421235f6102e8737032483f43c1a8cfb1d2f45740c44166219599358c2cd"}, + {file = "pydantic_core-2.18.2-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b14d82cdb934e99dda6d9d60dc84a24379820176cc4a0d123f88df319ae9c150"}, + {file = "pydantic_core-2.18.2-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2728b01246a3bba6de144f9e3115b532ee44bd6cf39795194fb75491824a1413"}, + {file = "pydantic_core-2.18.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:470b94480bb5ee929f5acba6995251ada5e059a5ef3e0dfc63cca287283ebfa6"}, + {file = "pydantic_core-2.18.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:997abc4df705d1295a42f95b4eec4950a37ad8ae46d913caeee117b6b198811c"}, + {file = "pydantic_core-2.18.2-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:75250dbc5290e3f1a0f4618db35e51a165186f9034eff158f3d490b3fed9f8a0"}, + {file = "pydantic_core-2.18.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:4456f2dca97c425231d7315737d45239b2b51a50dc2b6f0c2bb181fce6207664"}, + {file = "pydantic_core-2.18.2-cp311-none-win32.whl", hash = "sha256:269322dcc3d8bdb69f054681edff86276b2ff972447863cf34c8b860f5188e2e"}, + {file = "pydantic_core-2.18.2-cp311-none-win_amd64.whl", hash = "sha256:800d60565aec896f25bc3cfa56d2277d52d5182af08162f7954f938c06dc4ee3"}, + {file = "pydantic_core-2.18.2-cp311-none-win_arm64.whl", hash = "sha256:1404c69d6a676245199767ba4f633cce5f4ad4181f9d0ccb0577e1f66cf4c46d"}, + {file = "pydantic_core-2.18.2-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:fb2bd7be70c0fe4dfd32c951bc813d9fe6ebcbfdd15a07527796c8204bd36242"}, + {file = "pydantic_core-2.18.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:6132dd3bd52838acddca05a72aafb6eab6536aa145e923bb50f45e78b7251043"}, + {file = "pydantic_core-2.18.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d7d904828195733c183d20a54230c0df0eb46ec746ea1a666730787353e87182"}, + {file = "pydantic_core-2.18.2-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:c9bd70772c720142be1020eac55f8143a34ec9f82d75a8e7a07852023e46617f"}, + {file = "pydantic_core-2.18.2-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:2b8ed04b3582771764538f7ee7001b02e1170223cf9b75dff0bc698fadb00cf3"}, + {file = "pydantic_core-2.18.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e6dac87ddb34aaec85f873d737e9d06a3555a1cc1a8e0c44b7f8d5daeb89d86f"}, + {file = "pydantic_core-2.18.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7ca4ae5a27ad7a4ee5170aebce1574b375de390bc01284f87b18d43a3984df72"}, + {file = "pydantic_core-2.18.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:886eec03591b7cf058467a70a87733b35f44707bd86cf64a615584fd72488b7c"}, + {file = "pydantic_core-2.18.2-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:ca7b0c1f1c983e064caa85f3792dd2fe3526b3505378874afa84baf662e12241"}, + {file = "pydantic_core-2.18.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:4b4356d3538c3649337df4074e81b85f0616b79731fe22dd11b99499b2ebbdf3"}, + {file = "pydantic_core-2.18.2-cp312-none-win32.whl", hash = "sha256:8b172601454f2d7701121bbec3425dd71efcb787a027edf49724c9cefc14c038"}, + {file = "pydantic_core-2.18.2-cp312-none-win_amd64.whl", hash = "sha256:b1bd7e47b1558ea872bd16c8502c414f9e90dcf12f1395129d7bb42a09a95438"}, + {file = "pydantic_core-2.18.2-cp312-none-win_arm64.whl", hash = "sha256:98758d627ff397e752bc339272c14c98199c613f922d4a384ddc07526c86a2ec"}, + {file = "pydantic_core-2.18.2-cp38-cp38-macosx_10_12_x86_64.whl", hash = "sha256:9fdad8e35f278b2c3eb77cbdc5c0a49dada440657bf738d6905ce106dc1de439"}, + {file = "pydantic_core-2.18.2-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:1d90c3265ae107f91a4f279f4d6f6f1d4907ac76c6868b27dc7fb33688cfb347"}, + {file = "pydantic_core-2.18.2-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:390193c770399861d8df9670fb0d1874f330c79caaca4642332df7c682bf6b91"}, + {file = "pydantic_core-2.18.2-cp38-cp38-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:82d5d4d78e4448683cb467897fe24e2b74bb7b973a541ea1dcfec1d3cbce39fb"}, + {file = "pydantic_core-2.18.2-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4774f3184d2ef3e14e8693194f661dea5a4d6ca4e3dc8e39786d33a94865cefd"}, + {file = "pydantic_core-2.18.2-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d4d938ec0adf5167cb335acb25a4ee69a8107e4984f8fbd2e897021d9e4ca21b"}, + {file = "pydantic_core-2.18.2-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e0e8b1be28239fc64a88a8189d1df7fad8be8c1ae47fcc33e43d4be15f99cc70"}, + {file = "pydantic_core-2.18.2-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:868649da93e5a3d5eacc2b5b3b9235c98ccdbfd443832f31e075f54419e1b96b"}, + {file = "pydantic_core-2.18.2-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:78363590ef93d5d226ba21a90a03ea89a20738ee5b7da83d771d283fd8a56761"}, + {file = "pydantic_core-2.18.2-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:852e966fbd035a6468fc0a3496589b45e2208ec7ca95c26470a54daed82a0788"}, + {file = "pydantic_core-2.18.2-cp38-none-win32.whl", hash = "sha256:6a46e22a707e7ad4484ac9ee9f290f9d501df45954184e23fc29408dfad61350"}, + {file = "pydantic_core-2.18.2-cp38-none-win_amd64.whl", hash = "sha256:d91cb5ea8b11607cc757675051f61b3d93f15eca3cefb3e6c704a5d6e8440f4e"}, + {file = "pydantic_core-2.18.2-cp39-cp39-macosx_10_12_x86_64.whl", hash = "sha256:ae0a8a797a5e56c053610fa7be147993fe50960fa43609ff2a9552b0e07013e8"}, + {file = "pydantic_core-2.18.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:042473b6280246b1dbf530559246f6842b56119c2926d1e52b631bdc46075f2a"}, + {file = "pydantic_core-2.18.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1a388a77e629b9ec814c1b1e6b3b595fe521d2cdc625fcca26fbc2d44c816804"}, + {file = "pydantic_core-2.18.2-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:e25add29b8f3b233ae90ccef2d902d0ae0432eb0d45370fe315d1a5cf231004b"}, + {file = "pydantic_core-2.18.2-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f459a5ce8434614dfd39bbebf1041952ae01da6bed9855008cb33b875cb024c0"}, + {file = "pydantic_core-2.18.2-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:eff2de745698eb46eeb51193a9f41d67d834d50e424aef27df2fcdee1b153845"}, + {file = "pydantic_core-2.18.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a8309f67285bdfe65c372ea3722b7a5642680f3dba538566340a9d36e920b5f0"}, + {file = "pydantic_core-2.18.2-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:f93a8a2e3938ff656a7c1bc57193b1319960ac015b6e87d76c76bf14fe0244b4"}, + {file = "pydantic_core-2.18.2-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:22057013c8c1e272eb8d0eebc796701167d8377441ec894a8fed1af64a0bf399"}, + {file = "pydantic_core-2.18.2-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:cfeecd1ac6cc1fb2692c3d5110781c965aabd4ec5d32799773ca7b1456ac636b"}, + {file = "pydantic_core-2.18.2-cp39-none-win32.whl", hash = "sha256:0d69b4c2f6bb3e130dba60d34c0845ba31b69babdd3f78f7c0c8fae5021a253e"}, + {file = "pydantic_core-2.18.2-cp39-none-win_amd64.whl", hash = "sha256:d9319e499827271b09b4e411905b24a426b8fb69464dfa1696258f53a3334641"}, + {file = "pydantic_core-2.18.2-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:a1874c6dd4113308bd0eb568418e6114b252afe44319ead2b4081e9b9521fe75"}, + {file = "pydantic_core-2.18.2-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:ccdd111c03bfd3666bd2472b674c6899550e09e9f298954cfc896ab92b5b0e6d"}, + {file = "pydantic_core-2.18.2-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e18609ceaa6eed63753037fc06ebb16041d17d28199ae5aba0052c51449650a9"}, + {file = "pydantic_core-2.18.2-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6e5c584d357c4e2baf0ff7baf44f4994be121e16a2c88918a5817331fc7599d7"}, + {file = "pydantic_core-2.18.2-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:43f0f463cf89ace478de71a318b1b4f05ebc456a9b9300d027b4b57c1a2064fb"}, + {file = "pydantic_core-2.18.2-pp310-pypy310_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:e1b395e58b10b73b07b7cf740d728dd4ff9365ac46c18751bf8b3d8cca8f625a"}, + {file = "pydantic_core-2.18.2-pp310-pypy310_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:0098300eebb1c837271d3d1a2cd2911e7c11b396eac9661655ee524a7f10587b"}, + {file = "pydantic_core-2.18.2-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:36789b70d613fbac0a25bb07ab3d9dba4d2e38af609c020cf4d888d165ee0bf3"}, + {file = "pydantic_core-2.18.2-pp39-pypy39_pp73-macosx_10_12_x86_64.whl", hash = "sha256:3f9a801e7c8f1ef8718da265bba008fa121243dfe37c1cea17840b0944dfd72c"}, + {file = "pydantic_core-2.18.2-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:3a6515ebc6e69d85502b4951d89131ca4e036078ea35533bb76327f8424531ce"}, + {file = "pydantic_core-2.18.2-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:20aca1e2298c56ececfd8ed159ae4dde2df0781988c97ef77d5c16ff4bd5b400"}, + {file = "pydantic_core-2.18.2-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:223ee893d77a310a0391dca6df00f70bbc2f36a71a895cecd9a0e762dc37b349"}, + {file = "pydantic_core-2.18.2-pp39-pypy39_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:2334ce8c673ee93a1d6a65bd90327588387ba073c17e61bf19b4fd97d688d63c"}, + {file = "pydantic_core-2.18.2-pp39-pypy39_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:cbca948f2d14b09d20268cda7b0367723d79063f26c4ffc523af9042cad95592"}, + {file = "pydantic_core-2.18.2-pp39-pypy39_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:b3ef08e20ec49e02d5c6717a91bb5af9b20f1805583cb0adfe9ba2c6b505b5ae"}, + {file = "pydantic_core-2.18.2-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:c6fdc8627910eed0c01aed6a390a252fe3ea6d472ee70fdde56273f198938374"}, + {file = "pydantic_core-2.18.2.tar.gz", hash = "sha256:2e29d20810dfc3043ee13ac7d9e25105799817683348823f305ab3f349b9386e"}, +] + +[package.dependencies] +typing-extensions = ">=4.6.0,<4.7.0 || >4.7.0" + +[[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 = "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 = "six" +version = "1.16.0" +description = "Python 2 and 3 compatibility utilities" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*" +files = [ + {file = "six-1.16.0-py2.py3-none-any.whl", hash = "sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254"}, + {file = "six-1.16.0.tar.gz", hash = "sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926"}, +] + +[[package]] +name = "sniffio" +version = "1.3.1" +description = "Sniff out which async library your code is running under" +optional = false +python-versions = ">=3.7" +files = [ + {file = "sniffio-1.3.1-py3-none-any.whl", hash = "sha256:2f6da418d1f1e0fddd844478f41680e794e6051915791a034ff65e5f100525a2"}, + {file = "sniffio-1.3.1.tar.gz", hash = "sha256:f4324edc670a0f49750a81b895f35c3adb843cca46f0530f79fc1babb23789dc"}, +] + +[[package]] +name = "starlette" +version = "0.37.2" +description = "The little ASGI library that shines." +optional = false +python-versions = ">=3.8" +files = [ + {file = "starlette-0.37.2-py3-none-any.whl", hash = "sha256:6fe59f29268538e5d0d182f2791a479a0c64638e6935d1c6989e63fb2699c6ee"}, + {file = "starlette-0.37.2.tar.gz", hash = "sha256:9af890290133b79fc3db55474ade20f6220a364a0402e0b556e7cd5e1e093823"}, +] + +[package.dependencies] +anyio = ">=3.4.0,<5" + +[package.extras] +full = ["httpx (>=0.22.0)", "itsdangerous", "jinja2", "python-multipart (>=0.0.7)", "pyyaml"] + +[[package]] +name = "typing-extensions" +version = "4.11.0" +description = "Backported and Experimental Type Hints for Python 3.8+" +optional = false +python-versions = ">=3.8" +files = [ + {file = "typing_extensions-4.11.0-py3-none-any.whl", hash = "sha256:c1f94d72897edaf4ce775bb7558d5b79d8126906a14ea5ed1635921406c0387a"}, + {file = "typing_extensions-4.11.0.tar.gz", hash = "sha256:83f085bd5ca59c80295fc2a82ab5dac679cbe02b9f33f7d83af68e241bea51b0"}, +] + +[[package]] +name = "uvicorn" +version = "0.22.0" +description = "The lightning-fast ASGI server." +optional = false +python-versions = ">=3.7" +files = [ + {file = "uvicorn-0.22.0-py3-none-any.whl", hash = "sha256:e9434d3bbf05f310e762147f769c9f21235ee118ba2d2bf1155a7196448bd996"}, + {file = "uvicorn-0.22.0.tar.gz", hash = "sha256:79277ae03db57ce7d9aa0567830bbb51d7a612f54d6e1e3e92da3ef24c2c8ed8"}, +] + +[package.dependencies] +click = ">=7.0" +h11 = ">=0.8" + +[package.extras] +standard = ["colorama (>=0.4)", "httptools (>=0.5.0)", "python-dotenv (>=0.13)", "pyyaml (>=5.1)", "uvloop (>=0.14.0,!=0.15.0,!=0.15.1)", "watchfiles (>=0.13)", "websockets (>=10.4)"] + +[metadata] +lock-version = "2.0" +python-versions = "^3.10" +content-hash = "c67ad07886d909cfac5f81e7822fd7ddae85b5f1b268b604aa531e24c752baeb" diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..2b9fa9e --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,18 @@ +[tool.poetry] +name = "ephemerality" +version = "2.0.0" +description = "Module for computing ephemerality metrics for temporal activity vectors." +authors = ["Dmitry Gnatyshak "] +readme = "README.md" + +[tool.poetry.dependencies] +python = "^3.10" +numpy = "^1.24.2" +pydantic = "^2.7.1" +matplotlib = "^3.8.4" +fastapi = "^0.110.2" +uvicorn = "^0.22.0" + +[build-system] +requires = ["poetry-core"] +build-backend = "poetry.core.masonry.api" diff --git a/requirements.txt b/requirements.txt deleted file mode 100644 index 8cef061..0000000 --- a/requirements.txt +++ /dev/null @@ -1,5 +0,0 @@ -numpy==1.24.1 -fastapi~=0.89.1 -setuptools==66.0.0 -pydantic==1.10.4 -uvicorn~=0.20.0 diff --git a/rest/__init__.py b/rest/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/rest/api.py b/rest/api.py deleted file mode 100644 index d537a85..0000000 --- a/rest/api.py +++ /dev/null @@ -1,48 +0,0 @@ -from fastapi import FastAPI, status -from pydantic import BaseModel -import rest.api11 as api11 -from src import EphemeralitySet - - -app = FastAPI() - - -class InputData(BaseModel): - input_vector: list[float] - threshold: float - - -@app.post("/ephemerality/{api_version}/all", status_code=status.HTTP_200_OK) -async def get_all_ephemeralities(api_version: str, input_data: InputData) -> EphemeralitySet: - if api_version == '1.1': - return api11.get_all_ephemeralities(input_vector=input_data.input_vector, threshold=input_data.threshold) - else: - raise ValueError(f'Unrecognized API version: {api_version}!') - -@app.post("/ephemerality/{api_version}/left", status_code=status.HTTP_200_OK) -async def get_left_core_ephemeralities(api_version: str, input_data: InputData) -> EphemeralitySet: - if api_version == '1.1': - return api11.get_left_core_ephemerality(input_vector=input_data['input_vector'], threshold=input_data['threshold']) - else: - raise ValueError(f'Unrecognized API version: {api_version}!') - -@app.post("/ephemerality/{api_version}/middle", status_code=status.HTTP_200_OK) -async def get_middle_core_ephemeralities(api_version: str, input_data: InputData) -> EphemeralitySet: - if api_version == '1.1': - return api11.get_middle_core_ephemerality(input_vector=input_data['input_vector'], threshold=input_data['threshold']) - else: - raise ValueError(f'Unrecognized API version: {api_version}!') - -@app.post("/ephemerality/{api_version}/right", status_code=status.HTTP_200_OK) -async def get_right_core_ephemeralities(api_version: str, input_data: InputData) -> EphemeralitySet: - if api_version == '1.1': - return api11.get_right_core_ephemerality(input_vector=input_data['input_vector'], threshold=input_data['threshold']) - else: - raise ValueError(f'Unrecognized API version: {api_version}!') - -@app.post("/ephemerality/{api_version}/sorted", status_code=status.HTTP_200_OK) -async def get_sorted_core_ephemeralities(api_version: str, input_data: InputData) -> EphemeralitySet: - if api_version == '1.1': - return api11.get_sorted_core_ephemerality(input_vector=input_data['input_vector'], threshold=input_data['threshold']) - else: - raise ValueError(f'Unrecognized API version: {api_version}!') \ No newline at end of file diff --git a/rest/api11.py b/rest/api11.py deleted file mode 100644 index b386156..0000000 --- a/rest/api11.py +++ /dev/null @@ -1,18 +0,0 @@ -from typing import Sequence -from src import compute_ephemerality, EphemeralitySet - - -def get_all_ephemeralities(input_vector: Sequence[float], threshold: float) -> EphemeralitySet: - return compute_ephemerality(frequency_vector=input_vector, threshold=threshold, types='all') - -def get_left_core_ephemerality(input_vector: Sequence[float], threshold: float) -> EphemeralitySet: - return compute_ephemerality(frequency_vector=input_vector, threshold=threshold, types='left') - -def get_middle_core_ephemerality(input_vector: Sequence[float], threshold: float) -> EphemeralitySet: - return compute_ephemerality(frequency_vector=input_vector, threshold=threshold, types='middle') - -def get_right_core_ephemerality(input_vector: Sequence[float], threshold: float) -> EphemeralitySet: - return compute_ephemerality(frequency_vector=input_vector, threshold=threshold, types='right') - -def get_sorted_core_ephemerality(input_vector: Sequence[float], threshold: float) -> EphemeralitySet: - return compute_ephemerality(frequency_vector=input_vector, threshold=threshold, types='sorted') \ No newline at end of file diff --git a/setup.py b/setup.py deleted file mode 100644 index 7a31bb1..0000000 --- a/setup.py +++ /dev/null @@ -1,30 +0,0 @@ -import os -from setuptools import setup -import re - -VERSION_FILE = "_version.py" -VERSION_REGEX = r"^__version__ = ['\"]([^'\"]*)['\"]" - - -def read(file_name): - return open(os.path.join(os.path.dirname(__file__), file_name)).read() - - -version_lines = open(VERSION_FILE, 'r').read() -match = re.search(VERSION_REGEX, version_lines, re.M) -if match: - version = match.group(1) -else: - raise RuntimeError("Unable to find version string in %s." % (VERSION_FILE,)) - -setup( - name='ephemerality', - version=version, - packages=['src', 'test'], - url='https://github.com/HPAI-BSC/ephemerality', - license='MIT', - author='HPAI BSC', - author_email='dmitry.gnatyshak@bsc.es', - description='Module for computing ephemerality metrics of temporal arrays.', - long_description=read('README.md') -) diff --git a/src/__init__.py b/src/__init__.py deleted file mode 100644 index 5fae7b4..0000000 --- a/src/__init__.py +++ /dev/null @@ -1,3 +0,0 @@ -from src.ephemerality_computation import compute_ephemerality, EphemeralitySet - -__all__ = ['compute_ephemerality', 'EphemeralitySet'] diff --git a/src/ephemerality_computation.py b/src/ephemerality_computation.py deleted file mode 100644 index 2486067..0000000 --- a/src/ephemerality_computation.py +++ /dev/null @@ -1,174 +0,0 @@ -import numpy as np -from typing import Sequence -from pydantic import BaseModel -import warnings - - -class EphemeralitySet(BaseModel): - """Class to contain ephemerality values by subtypes""" - left_core: float = None - middle_core: float = None - right_core: float = None - sorted_core: float = None - - -def _normalize_frequency_vector(frequency_vector: Sequence[float]) -> np.array: - frequency_vector = np.array(frequency_vector) - - if sum(frequency_vector) != 1.: - frequency_vector /= np.sum(frequency_vector) - - return frequency_vector - - -def _ephemerality_raise_error(threshold: float): - if 0. < threshold <= 1: - raise ValueError('Input frequency vector has not been internally normalized!') - else: - raise ValueError('Threshold value is not within (0, 1] range!') - - -def compute_left_core_length(frequency_vector: np.array, threshold: float) -> int: - current_sum = 0 - for i, freq in enumerate(frequency_vector): - current_sum = current_sum + freq - if np.isclose(current_sum, threshold) or current_sum > threshold: - return i + 1 - - _ephemerality_raise_error(threshold) - - -def compute_right_core_length(frequency_vector: np.array, threshold: float) -> int: - current_sum = 0 - for i, freq in enumerate(frequency_vector[::-1]): - current_sum = current_sum + freq - if np.isclose(current_sum, threshold) or current_sum > threshold: - return i + 1 - - _ephemerality_raise_error(threshold) - - -def compute_middle_core_length(frequency_vector: np.array, threshold: float) -> int: - lower_threshold = (1. - threshold) / 2 - - current_presum = 0 - start_index = -1 - for i, freq in enumerate(frequency_vector): - current_presum += freq - if current_presum > lower_threshold and not np.isclose(current_presum, lower_threshold): - start_index = i - break - - current_sum = 0 - for j, freq in enumerate(frequency_vector[start_index:]): - current_sum += freq - if np.isclose(current_sum, threshold) or current_sum > threshold: - return j + 1 - - _ephemerality_raise_error(threshold) - - -def compute_sorted_core_length(frequency_vector: np.array, threshold: float) -> int: - freq_descending_order = np.sort(frequency_vector)[::-1] - - current_sum = 0 - for i, freq in enumerate(freq_descending_order): - current_sum += freq - if np.isclose(current_sum, threshold) or current_sum > threshold: - return i + 1 - - _ephemerality_raise_error(threshold) - - -def _compute_ephemerality_from_core(core_length: int, range_length: int, threshold: float): - return 1 - (core_length / range_length) / threshold - - -def _check_threshold(threshold: float): - if threshold <= 0.: - raise ValueError('Threshold value must be greater than 0!') - - if threshold > 1.: - raise ValueError('Threshold value must be less or equal to 1!') - - -def compute_ephemerality( - frequency_vector: Sequence[float], - threshold: float = 0.8, - types: str = 'all') -> EphemeralitySet: - - _check_threshold(threshold) - - if np.isclose(np.sum(frequency_vector), 0.): - return EphemeralitySet( - left_core=1., - middle_core=1., - right_core=1., - sorted_core=1. - ) - - frequency_vector = _normalize_frequency_vector(frequency_vector) - range_length = len(frequency_vector) - - if types == 'all' or types == 'left': - left_core_length = compute_left_core_length(frequency_vector, threshold) - ephemerality_left_core = _compute_ephemerality_from_core(left_core_length, range_length, threshold) - if ephemerality_left_core < 0. and not np.isclose(ephemerality_left_core, 0.): - warnings.warn(f'Original ephemerality value is less than 0 ({ephemerality_left_core}) and is going to be rounded up! ' - f'This is indicative of the edge case in which ephemerality span is greater than ' - f'[threshold * input_vector_length], i.e. most of the frequency mass lies in a few vector ' - f'elements at the end of the frequency vector. Original ephemerality in this case should be ' - f'considered to be equal to 0. However, please double check the input vector!', - RuntimeWarning) - ephemerality_left_core = 0. - else: - ephemerality_left_core = None - - if types == 'all' or types == 'middle': - middle_core_length = compute_middle_core_length(frequency_vector, threshold) - ephemerality_middle_core = _compute_ephemerality_from_core(middle_core_length, range_length, threshold) - if ephemerality_middle_core < 0. and not np.isclose(ephemerality_middle_core, 0.): - warnings.warn(f'Filtered ephemerality value is less than 0 ({ephemerality_middle_core}) and is going to be rounded up! ' - f'This is indicative of the edge case in which ephemerality span is greater than ' - f'[threshold * input_vector_length], i.e. most of the frequency mass lies in a few elements ' - f'at the beginning and the end of the frequency vector. Filtered ephemerality in this case should ' - f'be considered to be equal to 0. However, please double check the input vector!', - RuntimeWarning) - ephemerality_middle_core = 0. - else: - ephemerality_middle_core = None - - if types == 'all' or types == 'right': - right_core_length = compute_right_core_length(frequency_vector, threshold) - ephemerality_right_core = _compute_ephemerality_from_core(right_core_length, range_length, threshold) - if ephemerality_right_core < 0. and not np.isclose(ephemerality_right_core, 0.): - warnings.warn(f'Original ephemerality value is less than 0 ({ephemerality_right_core}) and is going to be rounded up! ' - f'This is indicative of the edge case in which ephemerality span is greater than ' - f'[threshold * input_vector_length], i.e. most of the frequency mass lies in a few vector ' - f'elements at the end of the frequency vector. Original ephemerality in this case should be ' - f'considered to be equal to 0. However, please double check the input vector!', - RuntimeWarning) - ephemerality_right_core = 0. - else: - ephemerality_right_core = None - - if types == 'all' or types == 'sorted': - sorted_core_length = compute_sorted_core_length(frequency_vector, threshold) - ephemerality_sorted_core = _compute_ephemerality_from_core(sorted_core_length, range_length, threshold) - if ephemerality_sorted_core < 0. and not np.isclose(ephemerality_sorted_core, 0.): - warnings.warn(f'Sorted ephemerality value is less than 0 ({ephemerality_sorted_core}) and is going to be rounded up! ' - f'This is indicative of the rare edge case of very short and mostly uniform frequency vector (so ' - f'that ephemerality span is greater than [threshold * input_vector_length]). ' - f'Sorted ephemerality in this case should be considered to be equal to 0. ' - f'However, please double check the input vector!', - RuntimeWarning) - ephemerality_sorted_core = 0. - else: - ephemerality_sorted_core = None - - ephemeralities = EphemeralitySet(left_core=ephemerality_left_core, - middle_core=ephemerality_middle_core, - right_core=ephemerality_right_core, - sorted_core=ephemerality_sorted_core) - - return ephemeralities diff --git a/test/__init__.py b/test/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/test/ephemerality_test.py b/test/ephemerality_test.py deleted file mode 100644 index 9bfd3b8..0000000 --- a/test/ephemerality_test.py +++ /dev/null @@ -1,510 +0,0 @@ -import warnings -from unittest import TestCase - -from typing import Sequence -import numpy as np -from dataclasses import dataclass -import re - -from src import compute_ephemerality - - -@dataclass -class EphemeralityTestCase: - input_vector: Sequence[float] - threshold: float - expected_output: dict - warnings: tuple[bool, bool, bool] - - -class TestComputeEphemerality(TestCase): - _warning_messages = [ - re.compile( - r'Original ephemerality value is less than 0 [(]-[0-9]*[.][0-9]*[)] and is going to be rounded up! ' - r'This is indicative of the edge case in which ephemerality span is greater than ' - r'\[threshold [*] input_vector_length], i[.]e[.] most of the frequency mass lies in a few vector ' - r'elements at the end of the frequency vector[.] Original ephemerality in this case should be ' - r'considered to be equal to 0[.] However, please double check the input vector!' - ), - - re.compile( - r'Filtered ephemerality value is less than 0 [(]-[0-9]*[.][0-9]*[)] and is going to be rounded up! ' - r'This is indicative of the edge case in which ephemerality span is greater than ' - r'\[threshold [*] input_vector_length], i[.]e[.] most of the frequency mass lies in a few elements ' - r'at the beginning and the end of the frequency vector[.] Filtered ephemerality in this case should ' - r'be considered to be equal to 0[.] However, please double check the input vector!' - ), - - re.compile( - r'Sorted ephemerality value is less than 0 [(]-[0-9]*[.][0-9]*[)] and is going to be rounded up! ' - r'This is indicative of the rare edge case of very short and mostly uniform frequency vector [(]so ' - r'that ephemerality span is greater than \[threshold [*] input_vector_length][)][.] ' - r'Sorted ephemerality in this case should be considered to be equal to 0[.] ' - r'However, please double check the input vector!' - ) - ] - - _test_cases = [ - EphemeralityTestCase( - input_vector=[1.], - threshold=0.8, - expected_output={ - 'ephemerality_original': 0., 'ephemerality_original_span': 1, - 'ephemerality_filtered': 0., 'ephemerality_filtered_span': 1, - 'ephemerality_sorted': 0., 'ephemerality_sorted_span': 1 - }, - warnings=(True, True, True) - ), - EphemeralityTestCase( - input_vector=[1.], - threshold=0.3, - expected_output={ - 'ephemerality_original': 0., 'ephemerality_original_span': 1, - 'ephemerality_filtered': 0., 'ephemerality_filtered_span': 1, - 'ephemerality_sorted': 0., 'ephemerality_sorted_span': 1 - }, - warnings=(True, True, True) - ), - EphemeralityTestCase( - input_vector=[1., 0.], - threshold=0.8, - expected_output={ - 'ephemerality_original': 0.375, 'ephemerality_original_span': 1, - 'ephemerality_filtered': 0.375, 'ephemerality_filtered_span': 1, - 'ephemerality_sorted': 0.375, 'ephemerality_sorted_span': 1 - }, - warnings=(False, False, False) - ), - EphemeralityTestCase( - input_vector=[1., 0.], - threshold=0.3, - expected_output={ - 'ephemerality_original': 0., 'ephemerality_original_span': 1, - 'ephemerality_filtered': 0., 'ephemerality_filtered_span': 1, - 'ephemerality_sorted': 0., 'ephemerality_sorted_span': 1 - }, - warnings=(True, True, True) - ), - EphemeralityTestCase( - input_vector=[0., 1.], - threshold=0.8, - expected_output={ - 'ephemerality_original': 0., 'ephemerality_original_span': 2, - 'ephemerality_filtered': 0.375, 'ephemerality_filtered_span': 1, - 'ephemerality_sorted': 0.375, 'ephemerality_sorted_span': 1 - }, - warnings=(True, False, False) - ), - EphemeralityTestCase( - input_vector=[0., 1.], - threshold=0.3, - expected_output={ - 'ephemerality_original': 0., 'ephemerality_original_span': 2, - 'ephemerality_filtered': 0., 'ephemerality_filtered_span': 1, - 'ephemerality_sorted': 0., 'ephemerality_sorted_span': 1 - }, - warnings=(True, True, True) - ), - EphemeralityTestCase( - input_vector=[.5, .5], - threshold=0.8, - expected_output={ - 'ephemerality_original': 0., 'ephemerality_original_span': 2, - 'ephemerality_filtered': 0., 'ephemerality_filtered_span': 2, - 'ephemerality_sorted': 0., 'ephemerality_sorted_span': 2 - }, - warnings=(True, True, True) - ), - EphemeralityTestCase( - input_vector=[.5, .5], - threshold=0.3, - expected_output={ - 'ephemerality_original': 0., 'ephemerality_original_span': 1, - 'ephemerality_filtered': 0., 'ephemerality_filtered_span': 1, - 'ephemerality_sorted': 0., 'ephemerality_sorted_span': 1 - }, - warnings=(True, True, True) - ), - EphemeralityTestCase( - input_vector=[0.7, .3], - threshold=0.8, - expected_output={ - 'ephemerality_original': 0., 'ephemerality_original_span': 2, - 'ephemerality_filtered': 0., 'ephemerality_filtered_span': 2, - 'ephemerality_sorted': 0., 'ephemerality_sorted_span': 2 - }, - warnings=(True, True, True) - ), - EphemeralityTestCase( - input_vector=[0.7, .3], - threshold=0.3, - expected_output={ - 'ephemerality_original': 0., 'ephemerality_original_span': 1, - 'ephemerality_filtered': 0., 'ephemerality_filtered_span': 1, - 'ephemerality_sorted': 0., 'ephemerality_sorted_span': 1 - }, - warnings=(True, True, True) - ), - EphemeralityTestCase( - input_vector=[1., 0., 0., 0.], - threshold=0.8, - expected_output={ - 'ephemerality_original': 0.6875, 'ephemerality_original_span': 1, - 'ephemerality_filtered': 0.6875, 'ephemerality_filtered_span': 1, - 'ephemerality_sorted': 0.6875, 'ephemerality_sorted_span': 1 - }, - warnings=(False, False, False) - ), - EphemeralityTestCase( - input_vector=[1., 0., 0., 0.], - threshold=0.3, - expected_output={ - 'ephemerality_original': 1 / 6, 'ephemerality_original_span': 1, - 'ephemerality_filtered': 1 / 6, 'ephemerality_filtered_span': 1, - 'ephemerality_sorted': 1 / 6, 'ephemerality_sorted_span': 1 - }, - warnings=(False, False, False) - ), - EphemeralityTestCase( - input_vector=[0., 0., 0., 1.], - threshold=0.8, - expected_output={ - 'ephemerality_original': 0., 'ephemerality_original_span': 4, - 'ephemerality_filtered': 0.6875, 'ephemerality_filtered_span': 1, - 'ephemerality_sorted': 0.6875, 'ephemerality_sorted_span': 1 - }, - warnings=(True, False, False) - ), - EphemeralityTestCase( - input_vector=[0., 0., 0., 1.], - threshold=0.3, - expected_output={ - 'ephemerality_original': 0., 'ephemerality_original_span': 4, - 'ephemerality_filtered': 1 / 6, 'ephemerality_filtered_span': 1, - 'ephemerality_sorted': 1 / 6, 'ephemerality_sorted_span': 1 - }, - warnings=(True, False, False) - ), - EphemeralityTestCase( - input_vector=[0., 1., 0., 1.], - threshold=0.8, - expected_output={ - 'ephemerality_original': 0., 'ephemerality_original_span': 4, - 'ephemerality_filtered': 0.0625, 'ephemerality_filtered_span': 3, - 'ephemerality_sorted': 0.375, 'ephemerality_sorted_span': 2 - }, - warnings=(True, False, False) - ), - EphemeralityTestCase( - input_vector=[0., 1., 0., 1.], - threshold=0.3, - expected_output={ - 'ephemerality_original': 0., 'ephemerality_original_span': 2, - 'ephemerality_filtered': 1 / 6, 'ephemerality_filtered_span': 1, - 'ephemerality_sorted': 1 / 6, 'ephemerality_sorted_span': 1 - }, - warnings=(True, False, False) - ), - EphemeralityTestCase( - input_vector=[1., 1., 1., 1.], - threshold=0.8, - expected_output={ - 'ephemerality_original': 0., 'ephemerality_original_span': 4, - 'ephemerality_filtered': 0., 'ephemerality_filtered_span': 4, - 'ephemerality_sorted': 0., 'ephemerality_sorted_span': 4 - }, - warnings=(True, True, True) - ), - EphemeralityTestCase( - input_vector=[1., 1., 1., 1.], - threshold=0.3, - expected_output={ - 'ephemerality_original': 0., 'ephemerality_original_span': 2, - 'ephemerality_filtered': 0., 'ephemerality_filtered_span': 2, - 'ephemerality_sorted': 0., 'ephemerality_sorted_span': 2 - }, - warnings=(True, True, True) - ), - EphemeralityTestCase( - input_vector=[1., 1., 0., 0.], - threshold=0.8, - expected_output={ - 'ephemerality_original': 0.375, 'ephemerality_original_span': 2, - 'ephemerality_filtered': 0.375, 'ephemerality_filtered_span': 2, - 'ephemerality_sorted': 0.375, 'ephemerality_sorted_span': 2 - }, - warnings=(False, False, False) - ), - EphemeralityTestCase( - input_vector=[1., 1., 0., 0.], - threshold=0.3, - expected_output={ - 'ephemerality_original': 1 / 6, 'ephemerality_original_span': 1, - 'ephemerality_filtered': 1 / 6, 'ephemerality_filtered_span': 1, - 'ephemerality_sorted': 1 / 6, 'ephemerality_sorted_span': 1 - }, - warnings=(False, False, False) - ), - EphemeralityTestCase( - input_vector=[1., 0., 0., 0., 0., 0., 0., 0., 0., 0.], - threshold=0.8, - expected_output={ - 'ephemerality_original': 0.875, 'ephemerality_original_span': 1, - 'ephemerality_filtered': 0.875, 'ephemerality_filtered_span': 1, - 'ephemerality_sorted': 0.875, 'ephemerality_sorted_span': 1 - }, - warnings=(False, False, False) - ), - EphemeralityTestCase( - input_vector=[1., 0., 0., 0., 0., 0., 0., 0., 0., 0.], - threshold=0.3, - expected_output={ - 'ephemerality_original': 2 / 3, 'ephemerality_original_span': 1, - 'ephemerality_filtered': 2 / 3, 'ephemerality_filtered_span': 1, - 'ephemerality_sorted': 2 / 3, 'ephemerality_sorted_span': 1 - }, - warnings=(False, False, False) - ), - EphemeralityTestCase( - input_vector=[0., 0., 0., 0., 1., 0., 0., 0., 0., 0.], - threshold=0.8, - expected_output={ - 'ephemerality_original': 0.375, 'ephemerality_original_span': 5, - 'ephemerality_filtered': 0.875, 'ephemerality_filtered_span': 1, - 'ephemerality_sorted': 0.875, 'ephemerality_sorted_span': 1 - }, - warnings=(False, False, False) - ), - EphemeralityTestCase( - input_vector=[0., 0., 0., 0., 1., 0., 0., 0., 0., 0.], - threshold=0.3, - expected_output={ - 'ephemerality_original': 0., 'ephemerality_original_span': 5, - 'ephemerality_filtered': 2 / 3, 'ephemerality_filtered_span': 1, - 'ephemerality_sorted': 2 / 3, 'ephemerality_sorted_span': 1 - }, - warnings=(True, False, False) - ), - EphemeralityTestCase( - input_vector=[0., 0., 1., 0., 0., 0., 0., 0., 0., 0.], - threshold=0.8, - expected_output={ - 'ephemerality_original': 0.625, 'ephemerality_original_span': 3, - 'ephemerality_filtered': 0.875, 'ephemerality_filtered_span': 1, - 'ephemerality_sorted': 0.875, 'ephemerality_sorted_span': 1 - }, - warnings=(False, False, False) - ), - EphemeralityTestCase( - input_vector=[0., 0., 1., 0., 0., 0., 0., 0., 0., 0.], - threshold=0.3, - expected_output={ - 'ephemerality_original': 0., 'ephemerality_original_span': 3, - 'ephemerality_filtered': 2 / 3, 'ephemerality_filtered_span': 1, - 'ephemerality_sorted': 2 / 3, 'ephemerality_sorted_span': 1 - }, - warnings=(False, False, False) - ), - EphemeralityTestCase( - input_vector=[0., 0., 0., 1., 0., 0., 0., 0., 0., 0.], - threshold=0.8, - expected_output={ - 'ephemerality_original': 0.5, 'ephemerality_original_span': 4, - 'ephemerality_filtered': 0.875, 'ephemerality_filtered_span': 1, - 'ephemerality_sorted': 0.875, 'ephemerality_sorted_span': 1 - }, - warnings=(False, False, False) - ), - EphemeralityTestCase( - input_vector=[0., 0., 0., 1., 0., 0., 0., 0., 0., 0.], - threshold=0.3, - expected_output={ - 'ephemerality_original': 0., 'ephemerality_original_span': 4, - 'ephemerality_filtered': 2 / 3, 'ephemerality_filtered_span': 1, - 'ephemerality_sorted': 2 / 3, 'ephemerality_sorted_span': 1 - }, - warnings=(True, False, False) - ), - EphemeralityTestCase( - input_vector=[0., 0., 0., 0., 0., 0., 0., 1., 0., 0.], - threshold=0.8, - expected_output={ - 'ephemerality_original': 0., 'ephemerality_original_span': 8, - 'ephemerality_filtered': 0.875, 'ephemerality_filtered_span': 1, - 'ephemerality_sorted': 0.875, 'ephemerality_sorted_span': 1 - }, - warnings=(False, False, False) - ), - EphemeralityTestCase( - input_vector=[0., 0., 0., 0., 0., 0., 0., 1., 0., 0.], - threshold=0.3, - expected_output={ - 'ephemerality_original': 0., 'ephemerality_original_span': 8, - 'ephemerality_filtered': 2 / 3, 'ephemerality_filtered_span': 1, - 'ephemerality_sorted': 2 / 3, 'ephemerality_sorted_span': 1 - }, - warnings=(True, False, False) - ), - EphemeralityTestCase( - input_vector=[0., 0., 0., 0., 0., 0., 0., 0., 1., 0.], - threshold=0.8, - expected_output={ - 'ephemerality_original': 0., 'ephemerality_original_span': 9, - 'ephemerality_filtered': 0.875, 'ephemerality_filtered_span': 1, - 'ephemerality_sorted': 0.875, 'ephemerality_sorted_span': 1 - }, - warnings=(True, False, False) - ), - EphemeralityTestCase( - input_vector=[0., 0., 0., 0., 0., 0., 0., 0., 1., 0.], - threshold=0.3, - expected_output={ - 'ephemerality_original': 0., 'ephemerality_original_span': 9, - 'ephemerality_filtered': 2 / 3, 'ephemerality_filtered_span': 1, - 'ephemerality_sorted': 2 / 3, 'ephemerality_sorted_span': 1 - }, - warnings=(True, False, False) - ), - EphemeralityTestCase( - input_vector=[0., 0., 0., 0., 0., 0., 0., 0., 0., 1.], - threshold=0.8, - expected_output={ - 'ephemerality_original': 0., 'ephemerality_original_span': 10, - 'ephemerality_filtered': 0.875, 'ephemerality_filtered_span': 1, - 'ephemerality_sorted': 0.875, 'ephemerality_sorted_span': 1 - }, - warnings=(True, False, False) - ), - EphemeralityTestCase( - input_vector=[0., 0., 0., 0., 0., 0., 0., 0., 0., 1.], - threshold=0.3, - expected_output={ - 'ephemerality_original': 0., 'ephemerality_original_span': 10, - 'ephemerality_filtered': 2 / 3, 'ephemerality_filtered_span': 1, - 'ephemerality_sorted': 2 / 3, 'ephemerality_sorted_span': 1 - }, - warnings=(True, False, False) - ), - EphemeralityTestCase( - input_vector=[.1, .1, .1, .1, .1, .1, .1, .1, .1, .1], - threshold=0.8, - expected_output={ - 'ephemerality_original': 0., 'ephemerality_original_span': 8, - 'ephemerality_filtered': 0., 'ephemerality_filtered_span': 8, - 'ephemerality_sorted': 0., 'ephemerality_sorted_span': 8 - }, - warnings=(False, False, False) - ), - EphemeralityTestCase( - input_vector=[.1, .1, .1, .1, .1, .1, .1, .1, .1, .1], - threshold=0.3, - expected_output={ - 'ephemerality_original': 0., 'ephemerality_original_span': 3, - 'ephemerality_filtered': 0., 'ephemerality_filtered_span': 3, - 'ephemerality_sorted': 0., 'ephemerality_sorted_span': 3 - }, - warnings=(False, False, False) - ), - EphemeralityTestCase( - input_vector=[0., 0., 0., .2, .55, 0., .15, .1, 0., 0.], - threshold=0.8, - expected_output={ - 'ephemerality_original': 0.125, 'ephemerality_original_span': 7, - 'ephemerality_filtered': 0.5, 'ephemerality_filtered_span': 4, - 'ephemerality_sorted': 0.625, 'ephemerality_sorted_span': 3 - }, - warnings=(False, False, False) - ), - EphemeralityTestCase( - input_vector=[0., 0., 0., .2, .55, 0., .15, .1, 0., 0.], - threshold=0.3, - expected_output={ - 'ephemerality_original': 0., 'ephemerality_original_span': 5, - 'ephemerality_filtered': 2 / 3, 'ephemerality_filtered_span': 1, - 'ephemerality_sorted': 2 / 3, 'ephemerality_sorted_span': 1 - }, - warnings=(True, False, False) - ), - EphemeralityTestCase( - input_vector=np.eye(1, 10000, k=5000).flatten(), - threshold=0.8, - expected_output={ - 'ephemerality_original': 0.375, 'ephemerality_original_span': 5000, - 'ephemerality_filtered': 0.999875, 'ephemerality_filtered_span': 1, - 'ephemerality_sorted': 0.999875, 'ephemerality_sorted_span': 1 - }, - warnings=(False, False, False) - ), - EphemeralityTestCase( - input_vector=np.eye(1, 10000, k=5000).flatten(), - threshold=0.3, - expected_output={ - 'ephemerality_original': 0., 'ephemerality_original_span': 5000, - 'ephemerality_filtered': 2999 / 3000, 'ephemerality_filtered_span': 1, - 'ephemerality_sorted': 2999 / 3000, 'ephemerality_sorted_span': 1 - }, - warnings=(True, False, False) - ), - EphemeralityTestCase( - input_vector=np.ones((10000,)), - threshold=0.8, - expected_output={ - 'ephemerality_original': 0., 'ephemerality_original_span': 8000, - 'ephemerality_filtered': 0., 'ephemerality_filtered_span': 8000, - 'ephemerality_sorted': 0., 'ephemerality_sorted_span': 8000 - }, - warnings=(False, False, False) - ), - EphemeralityTestCase( - input_vector=np.ones((10000,)), - threshold=0.3, - expected_output={ - 'ephemerality_original': 0., 'ephemerality_original_span': 3000, - 'ephemerality_filtered': 0., 'ephemerality_filtered_span': 3000, - 'ephemerality_sorted': 0., 'ephemerality_sorted_span': 3000 - }, - warnings=(False, False, False) - ) - ] - - def add_test_case(self, - input_vector: Sequence[float], - threshold: float, - expected_output: dict, - warnings: tuple[bool, bool, bool]): - self._test_cases.append(EphemeralityTestCase( - input_vector=input_vector, - threshold=threshold, - expected_output=expected_output, - warnings=warnings - )) - - def clear(self): - self._test_cases = list() - - @staticmethod - def round_ephemeralities(ephemeralities: dict, precision: int=8): - np.round_(ephemeralities['ephemerality_original'], precision) - np.round_(ephemeralities['ephemerality_filtered'], precision) - np.round_(ephemeralities['ephemerality_sorted'], precision) - - def test_compute_ephemeralities(self): - for i, test_case in enumerate(self._test_cases): - print(f'\nRunning test case {i}: {test_case.input_vector}, threshold {test_case.threshold}...') - with warnings.catch_warnings(record=True) as warns: - warnings.simplefilter('always', category=RuntimeWarning) - - actual_output = compute_ephemerality(frequency_vector=test_case.input_vector, - threshold=test_case.threshold) - - self.assertEqual(self.round_ephemeralities(test_case.expected_output), - self.round_ephemeralities(actual_output)) - - warn_messages = "" - for warn in warns: - warn_messages += str(warn.message) - - actual_warnings = tuple((TestComputeEphemerality._warning_messages[i].search(warn_messages) is not None - for i in range(3))) - - self.assertEqual(test_case.warnings, actual_warnings) diff --git a/testing/__init__.py b/testing/__init__.py new file mode 100644 index 0000000..2afb534 --- /dev/null +++ b/testing/__init__.py @@ -0,0 +1,4 @@ +from testing.src import generate_data, generate_test_case, clear_data + + +__all__ = ['generate_data', 'generate_test_case', 'clear_data'] diff --git a/testing/run_data_generator.py b/testing/run_data_generator.py new file mode 100644 index 0000000..bae22fb --- /dev/null +++ b/testing/run_data_generator.py @@ -0,0 +1,61 @@ +#!/usr/bin/env python3 + +from argparse import ArgumentParser +import sys +from pathlib import Path + +from testing.src import generate_data + + +def init_parser() -> ArgumentParser: + parser = ArgumentParser( + usage=f"{Path(sys.executable).stem} %(prog)s [-o OUTPUT_FOLDER][-g] [-d DATA_TYPE] [--data_range START END] " + "[-n MAX_TEST_SIZE] [-m CASES_PER_BATCH] [-s SEED] ...", + description="Generate data for tests." + ) + parser.add_argument( + "-o", "--output_folder", action="store", default="./test_data/", + help="Path to the folder to store generated test cases. Defaults to \"./test_data/\"." + ) + parser.add_argument( + "-d", "--data_type", action="store", choices=["activity", "a", "timestamps", "t", "datetime", "d"], default="a", + help="Type of the generated data. Defaults to \"a\"." + ) + parser.add_argument( + "--data_range", action="store", type=float, nargs=2, default=None, + help="Value range for timestamps or datetime data types in UNIX timestamp in seconds. " + "Passed as 2 integer numbers. Defaults to (0, 31536000)." + ) + parser.add_argument( + "-n", "--max_size", action="store", type=int, default=6, + help="Maximal size (in power 10) of test size batches. Defaults to 6." + ) + parser.add_argument( + "-m", "--cases_per_batch", action="store", type=int, default=20, + help="Number of test cases in each size batch. Defaults to 20." + ) + parser.add_argument( + "-s", "--seed", action="store", type=int, default=2023, + help="Value of the seed to be used for test case generation. Defaults to 2023." + ) + return parser + + +if __name__ == '__main__': + parser = init_parser() + args = parser.parse_args() + if args.data_range is None: + args.data_range = (0, 31536000) + + if args.max_size <= 0: + raise ValueError("\"max_size\" value should be positive!") + if args.cases_per_batch <= 0: + raise ValueError("\"cases_per_batch\" value should be positive!") + + generate_data( + max_size=args.max_size, + inputs_per_n=args.cases_per_batch, + data_type=args.data_type, + data_range=args.data_range, + seed=args.seed, + save_dir=args.output_folder) diff --git a/testing/run_unit_tests.py b/testing/run_unit_tests.py new file mode 100644 index 0000000..c5856ac --- /dev/null +++ b/testing/run_unit_tests.py @@ -0,0 +1,7 @@ +#!/usr/bin/env python3 + +from testing.src import test_ephemerality + + +if __name__ == "__main__": + test_ephemerality() diff --git a/testing/src/__init__.py b/testing/src/__init__.py new file mode 100644 index 0000000..f5367ba --- /dev/null +++ b/testing/src/__init__.py @@ -0,0 +1,5 @@ +from testing.src.data_generator import generate_test_case, generate_data, clear_data +from testing.src.test_ephemerality import test_ephemerality + + +__all__ = ['generate_test_case', 'generate_data', 'clear_data', 'test_ephemerality'] diff --git a/testing/src/data_generator.py b/testing/src/data_generator.py new file mode 100644 index 0000000..85ab98e --- /dev/null +++ b/testing/src/data_generator.py @@ -0,0 +1,93 @@ +import json +import numpy as np +from pathlib import Path +import shutil +from datetime import datetime + + +def generate_test_case( + size: int, + data_type: str, + data_range: tuple[float, float] | None = None, + seed: None | int = None, + activity_length: None | int = None +) -> tuple[float, list[float | str]]: + + if activity_length is None: + activity_length = size + activity = np.zeros((activity_length,)) + rng = np.random.default_rng(seed) + threshold = float(rng.uniform(low=0.1, high=0.9, size=None)) + activity[0] = rng.normal(scale=10) + for i in range(1, activity_length): + activity[i] = activity[i - 1] + rng.normal() + activity -= np.mean(activity) + activity = activity.clip(min=0) + + if data_type == "activity" or data_type == "a": + activity /= np.sum(activity) + return threshold, list(activity) + + activity_granule_length = int(np.ceil((data_range[1] - data_range[0]) / activity_length)) + activity = activity.repeat(activity_granule_length)[:(data_range[1] - data_range[0])] + activity /= np.sum(activity) + + timestamps = rng.choice( + a=np.arange(data_range[0], data_range[1]).astype(int), + size=size, + p=activity + ) + timestamps.sort() + + if data_type == "timestamps" or data_type == "t": + return threshold, list(timestamps.astype(str)) + + return threshold, [datetime.fromtimestamp(ts).strftime("%Y-%m-%dT%H:%M:%S.%fZ").replace("000000Z", "000Z") + for ts in timestamps] + + +def generate_data( + max_size: int = 10, + inputs_per_n: int = 100, + data_type: str = "a", + data_range: tuple[float, float] | None = None, + seed: int = 2023, + save_dir: str = "./test_data/" +) -> None: + if save_dir and save_dir[-1] != '/': + save_dir += '/' + + for n in range(1, max_size + 1): + dir_n = Path(f"{save_dir}{n}") + dir_n.mkdir(parents=True, exist_ok=True) + + for i in range(inputs_per_n): + size = 10 ** n + test_case = generate_test_case( + size=size, + data_type=data_type, + data_range=data_range, + seed=seed + i, + activity_length=None if data_type == "activity" or data_type == "a" else int((data_range[1] - data_range[0]) / 1000) + ) + test_data = [{ + "threshold": test_case[0], + "input_sequence": test_case[1], + "input_type": data_type, + "range": [str(data_range[0]), str(data_range[1])], + "reference_name": f"{data_type}_{size}_{i}" + }] + + with open(f"{dir_n}/{i}.json", "w") as f: + json.dump(test_data, f) + + +def clear_data(folder: str) -> None: + for file in Path(folder).iterdir(): + try: + if file.is_file() or file.is_symlink(): + file.unlink() + elif file.is_dir(): + shutil.rmtree(file) + except Exception as ex: + print(f'Failed to delete {file.resolve()}. Reason: {ex}') diff --git a/testing/src/test_ephemerality.py b/testing/src/test_ephemerality.py new file mode 100644 index 0000000..cca1727 --- /dev/null +++ b/testing/src/test_ephemerality.py @@ -0,0 +1,768 @@ +import json +import time +from unittest import TestCase, TextTestRunner +import numpy as np +from typing import Sequence, Iterable, Literal +from typing_extensions import Self +from itertools import chain, combinations +import subprocess +import sys +import requests +from testing.src.test_utils import EphemeralityTestCase +from ephemerality import compute_ephemerality, EphemeralitySet, InputData + + +DEFAULT_TEST_CASES = [ + EphemeralityTestCase( + input_sequence=[1.], + threshold=0.8, + expected_output=EphemeralitySet( + len_left_core=1, + len_middle_core=1, + len_right_core=1, + len_sorted_core=1, + eph_left_core=0., + eph_middle_core=0., + eph_right_core=0., + eph_sorted_core=0. + ) + ), + EphemeralityTestCase( + input_sequence=[1.], + threshold=0.3, + expected_output=EphemeralitySet( + len_left_core=1, + len_middle_core=1, + len_right_core=1, + len_sorted_core=1, + eph_left_core=0., + eph_middle_core=0., + eph_right_core=0., + eph_sorted_core=0. + ) + ), + EphemeralityTestCase( + input_sequence=[1., 0.], + threshold=0.8, + expected_output=EphemeralitySet( + len_left_core=1, + len_middle_core=1, + len_right_core=2, + len_sorted_core=1, + eph_left_core=0.375, + eph_middle_core=0.375, + eph_right_core=0., + eph_sorted_core=0.375 + ) + ), + EphemeralityTestCase( + input_sequence=[1., 0.], + threshold=0.3, + expected_output=EphemeralitySet( + len_left_core=1, + len_middle_core=1, + len_right_core=2, + len_sorted_core=1, + eph_left_core=0., + eph_middle_core=0., + eph_right_core=0., + eph_sorted_core=0. + ) + ), + EphemeralityTestCase( + input_sequence=[0., 1.], + threshold=0.8, + expected_output=EphemeralitySet( + len_left_core=2, + len_middle_core=1, + len_right_core=1, + len_sorted_core=1, + eph_left_core=0., + eph_middle_core=0.375, + eph_right_core=0.375, + eph_sorted_core=0.375 + ) + ), + EphemeralityTestCase( + input_sequence=[0., 1.], + threshold=0.3, + expected_output=EphemeralitySet( + len_left_core=2, + len_middle_core=1, + len_right_core=1, + len_sorted_core=1, + eph_left_core=0., + eph_middle_core=0., + eph_right_core=0., + eph_sorted_core=0. + ) + ), + EphemeralityTestCase( + input_sequence=[.5, .5], + threshold=0.8, + expected_output=EphemeralitySet( + len_left_core=2, + len_middle_core=2, + len_right_core=2, + len_sorted_core=2, + eph_left_core=0., + eph_middle_core=0., + eph_right_core=0., + eph_sorted_core=0. + ) + ), + EphemeralityTestCase( + input_sequence=[.5, .5], + threshold=0.3, + expected_output=EphemeralitySet( + len_left_core=1, + len_middle_core=1, + len_right_core=1, + len_sorted_core=1, + eph_left_core=0., + eph_middle_core=0., + eph_right_core=0., + eph_sorted_core=0. + ) + ), + EphemeralityTestCase( + input_sequence=[0.7, .3], + threshold=0.8, + expected_output=EphemeralitySet( + len_left_core=2, + len_middle_core=2, + len_right_core=2, + len_sorted_core=2, + eph_left_core=0., + eph_middle_core=0., + eph_right_core=0., + eph_sorted_core=0. + ) + ), + EphemeralityTestCase( + input_sequence=[0.7, .3], + threshold=0.3, + expected_output=EphemeralitySet( + len_left_core=1, + len_middle_core=1, + len_right_core=1, + len_sorted_core=1, + eph_left_core=0., + eph_middle_core=0., + eph_right_core=0., + eph_sorted_core=0. + ) + ), + EphemeralityTestCase( + input_sequence=[1., 0., 0., 0.], + threshold=0.8, + expected_output=EphemeralitySet( + len_left_core=1, + len_middle_core=1, + len_right_core=4, + len_sorted_core=1, + eph_left_core=0.6875, + eph_middle_core=0.6875, + eph_right_core=0., + eph_sorted_core=0.6875 + ) + ), + EphemeralityTestCase( + input_sequence=[1., 0., 0., 0.], + threshold=0.3, + expected_output=EphemeralitySet( + len_left_core=1, + len_middle_core=1, + len_right_core=4, + len_sorted_core=1, + eph_left_core=1 / 6, + eph_middle_core=1 / 6, + eph_right_core=0., + eph_sorted_core=1 / 6 + ) + ), + EphemeralityTestCase( + input_sequence=[0., 0., 0., 1.], + threshold=0.8, + expected_output=EphemeralitySet( + len_left_core=4, + len_middle_core=1, + len_right_core=1, + len_sorted_core=1, + eph_left_core=0., + eph_middle_core=0.6875, + eph_right_core=0.6875, + eph_sorted_core=0.6875 + ) + ), + EphemeralityTestCase( + input_sequence=[0., 0., 0., 1.], + threshold=0.3, + expected_output=EphemeralitySet( + len_left_core=4, + len_middle_core=1, + len_right_core=1, + len_sorted_core=1, + eph_left_core=0., + eph_middle_core=1 / 6, + eph_right_core=1 / 6, + eph_sorted_core=1 / 6 + ) + ), + EphemeralityTestCase( + input_sequence=[0., 1., 0., 1.], + threshold=0.8, + expected_output=EphemeralitySet( + len_left_core=4, + len_middle_core=3, + len_right_core=3, + len_sorted_core=2, + eph_left_core=0., + eph_middle_core=0.0625, + eph_right_core=0.0625, + eph_sorted_core=0.375 + ) + ), + EphemeralityTestCase( + input_sequence=[0., 1., 0., 1.], + threshold=0.3, + expected_output=EphemeralitySet( + len_left_core=2, + len_middle_core=1, + len_right_core=1, + len_sorted_core=1, + eph_left_core=0., + eph_middle_core=1 / 6, + eph_right_core=1 / 6, + eph_sorted_core=1 / 6 + ) + ), + EphemeralityTestCase( + input_sequence=[1., 1., 1., 1.], + threshold=0.8, + expected_output=EphemeralitySet( + len_left_core=4, + len_middle_core=4, + len_right_core=4, + len_sorted_core=4, + eph_left_core=0., + eph_middle_core=0., + eph_right_core=0., + eph_sorted_core=0. + ) + ), + EphemeralityTestCase( + input_sequence=[1., 1., 1., 1.], + threshold=0.3, + expected_output=EphemeralitySet( + len_left_core=2, + len_middle_core=2, + len_right_core=2, + len_sorted_core=2, + eph_left_core=0., + eph_middle_core=0., + eph_right_core=0., + eph_sorted_core=0. + ) + ), + EphemeralityTestCase( + input_sequence=[1., 1., 0., 0.], + threshold=0.8, + expected_output=EphemeralitySet( + len_left_core=2, + len_middle_core=2, + len_right_core=4, + len_sorted_core=2, + eph_left_core=0.375, + eph_middle_core=0.375, + eph_right_core=0., + eph_sorted_core=0.375 + ) + ), + EphemeralityTestCase( + input_sequence=[1., 1., 0., 0.], + threshold=0.3, + expected_output=EphemeralitySet( + len_left_core=1, + len_middle_core=1, + len_right_core=3, + len_sorted_core=1, + eph_left_core=1 / 6, + eph_middle_core=1 / 6, + eph_right_core=0, + eph_sorted_core=1 / 6 + ) + ), + EphemeralityTestCase( + input_sequence=[1., 0., 0., 0., 0., 0., 0., 0., 0., 0.], + threshold=0.8, + expected_output=EphemeralitySet( + len_left_core=1, + len_middle_core=1, + len_right_core=10, + len_sorted_core=1, + eph_left_core=0.875, + eph_middle_core=0.875, + eph_right_core=0., + eph_sorted_core=0.875 + ) + ), + EphemeralityTestCase( + input_sequence=[1., 0., 0., 0., 0., 0., 0., 0., 0., 0.], + threshold=0.3, + expected_output=EphemeralitySet( + len_left_core=1, + len_middle_core=1, + len_right_core=10, + len_sorted_core=1, + eph_left_core=2 / 3, + eph_middle_core=2 / 3, + eph_right_core=0., + eph_sorted_core=2 / 3 + ) + ), + EphemeralityTestCase( + input_sequence=[0., 0., 0., 0., 1., 0., 0., 0., 0., 0.], + threshold=0.8, + expected_output=EphemeralitySet( + len_left_core=5, + len_middle_core=1, + len_right_core=6, + len_sorted_core=1, + eph_left_core=0.375, + eph_middle_core=0.875, + eph_right_core=0.25, + eph_sorted_core=0.875 + ) + ), + EphemeralityTestCase( + input_sequence=[0., 0., 0., 0., 1., 0., 0., 0., 0., 0.], + threshold=0.3, + expected_output=EphemeralitySet( + len_left_core=5, + len_middle_core=1, + len_right_core=6, + len_sorted_core=1, + eph_left_core=0., + eph_middle_core=2 / 3, + eph_right_core=0., + eph_sorted_core=2 / 3 + ) + ), + EphemeralityTestCase( + input_sequence=[0., 0., 1., 0., 0., 0., 0., 0., 0., 0.], + threshold=0.8, + expected_output=EphemeralitySet( + len_left_core=3, + len_middle_core=1, + len_right_core=8, + len_sorted_core=1, + eph_left_core=0.625, + eph_middle_core=0.875, + eph_right_core=0., + eph_sorted_core=0.875 + ) + ), + EphemeralityTestCase( + input_sequence=[0., 0., 1., 0., 0., 0., 0., 0., 0., 0.], + threshold=0.3, + expected_output=EphemeralitySet( + len_left_core=3, + len_middle_core=1, + len_right_core=8, + len_sorted_core=1, + eph_left_core=0., + eph_middle_core=2 / 3, + eph_right_core=0., + eph_sorted_core=2 / 3 + ) + ), + EphemeralityTestCase( + input_sequence=[0., 0., 0., 1., 0., 0., 0., 0., 0., 0.], + threshold=0.8, + expected_output=EphemeralitySet( + len_left_core=4, + len_middle_core=1, + len_right_core=7, + len_sorted_core=1, + eph_left_core=0.5, + eph_middle_core=0.875, + eph_right_core=0.125, + eph_sorted_core=0.875 + ) + ), + EphemeralityTestCase( + input_sequence=[0., 0., 0., 1., 0., 0., 0., 0., 0., 0.], + threshold=0.3, + expected_output=EphemeralitySet( + len_left_core=4, + len_middle_core=1, + len_right_core=7, + len_sorted_core=1, + eph_left_core=0., + eph_middle_core=2 / 3, + eph_right_core=0., + eph_sorted_core=2 / 3 + ) + ), + EphemeralityTestCase( + input_sequence=[0., 0., 0., 0., 0., 0., 0., 1., 0., 0.], + threshold=0.8, + expected_output=EphemeralitySet( + len_left_core=8, + len_middle_core=1, + len_right_core=3, + len_sorted_core=1, + eph_left_core=0., + eph_middle_core=0.875, + eph_right_core=0.625, + eph_sorted_core=0.875 + ) + ), + EphemeralityTestCase( + input_sequence=[0., 0., 0., 0., 0., 0., 0., 1., 0., 0.], + threshold=0.3, + expected_output=EphemeralitySet( + len_left_core=8, + len_middle_core=1, + len_right_core=3, + len_sorted_core=1, + eph_left_core=0., + eph_middle_core=2 / 3, + eph_right_core=0., + eph_sorted_core=2 / 3 + ) + ), + EphemeralityTestCase( + input_sequence=[0., 0., 0., 0., 0., 0., 0., 0., 1., 0.], + threshold=0.8, + expected_output=EphemeralitySet( + len_left_core=9, + len_middle_core=1, + len_right_core=2, + len_sorted_core=1, + eph_left_core=0., + eph_middle_core=0.875, + eph_right_core=0.75, + eph_sorted_core=0.875 + ) + ), + EphemeralityTestCase( + input_sequence=[0., 0., 0., 0., 0., 0., 0., 0., 1., 0.], + threshold=0.3, + expected_output=EphemeralitySet( + len_left_core=9, + len_middle_core=1, + len_right_core=2, + len_sorted_core=1, + eph_left_core=0., + eph_middle_core=2 / 3, + eph_right_core=1 / 3, + eph_sorted_core=2 / 3 + ) + ), + EphemeralityTestCase( + input_sequence=[0., 0., 0., 0., 0., 0., 0., 0., 0., 1.], + threshold=0.8, + expected_output=EphemeralitySet( + len_left_core=10, + len_middle_core=1, + len_right_core=1, + len_sorted_core=1, + eph_left_core=0., + eph_middle_core=0.875, + eph_right_core=0.875, + eph_sorted_core=0.875 + ) + ), + EphemeralityTestCase( + input_sequence=[0., 0., 0., 0., 0., 0., 0., 0., 0., 1.], + threshold=0.3, + expected_output=EphemeralitySet( + len_left_core=10, + len_middle_core=1, + len_right_core=1, + len_sorted_core=1, + eph_left_core=0., + eph_middle_core=2 / 3, + eph_right_core=2 / 3, + eph_sorted_core=2 / 3 + ) + ), + EphemeralityTestCase( + input_sequence=[.1, .1, .1, .1, .1, .1, .1, .1, .1, .1], + threshold=0.8, + expected_output=EphemeralitySet( + len_left_core=8, + len_middle_core=8, + len_right_core=8, + len_sorted_core=8, + eph_left_core=0., + eph_middle_core=0., + eph_right_core=0., + eph_sorted_core=0. + ) + ), + EphemeralityTestCase( + input_sequence=[.1, .1, .1, .1, .1, .1, .1, .1, .1, .1], + threshold=0.3, + expected_output=EphemeralitySet( + len_left_core=3, + len_middle_core=3, + len_right_core=3, + len_sorted_core=3, + eph_left_core=0., + eph_middle_core=0., + eph_right_core=0., + eph_sorted_core=0. + ) + ), + EphemeralityTestCase( + input_sequence=[0., 0., 0., .2, .55, 0., .15, .1, 0., 0.], + threshold=0.8, + expected_output=EphemeralitySet( + len_left_core=7, + len_middle_core=4, + len_right_core=6, + len_sorted_core=3, + eph_left_core=0.125, + eph_middle_core=0.5, + eph_right_core=0.25, + eph_sorted_core=0.625 + ) + ), + EphemeralityTestCase( + input_sequence=[0., 0., 0., .2, .55, 0., .15, .1, 0., 0.], + threshold=0.3, + expected_output=EphemeralitySet( + len_left_core=5, + len_middle_core=1, + len_right_core=6, + len_sorted_core=1, + eph_left_core=0., + eph_middle_core=2 / 3, + eph_right_core=0., + eph_sorted_core=2 / 3 + ) + ), + EphemeralityTestCase( + input_sequence=np.eye(1, 10000, k=5000).flatten(), + threshold=0.8, + expected_output=EphemeralitySet( + len_left_core=5001, + len_middle_core=1, + len_right_core=5000, + len_sorted_core=1, + eph_left_core=0.374875, + eph_middle_core=0.999875, + eph_right_core=0.375, + eph_sorted_core=0.999875 + ) + ), + EphemeralityTestCase( + input_sequence=np.eye(1, 10000, k=5000).flatten(), + threshold=0.3, + expected_output=EphemeralitySet( + len_left_core=5001, + len_middle_core=1, + len_right_core=5000, + len_sorted_core=1, + eph_left_core=0., + eph_middle_core=2999 / 3000, + eph_right_core=0., + eph_sorted_core=2999 / 3000 + ) + ), + EphemeralityTestCase( + input_sequence=np.ones((10000,)), + threshold=0.8, + expected_output=EphemeralitySet( + len_left_core=8000, + len_middle_core=8000, + len_right_core=8000, + len_sorted_core=8000, + eph_left_core=0., + eph_middle_core=0., + eph_right_core=0., + eph_sorted_core=0. + ) + ), + EphemeralityTestCase( + input_sequence=np.ones((10000,)), + threshold=0.3, + expected_output=EphemeralitySet( + len_left_core=3000, + len_middle_core=3000, + len_right_core=3000, + len_sorted_core=3000, + eph_left_core=0., + eph_middle_core=0., + eph_right_core=0., + eph_sorted_core=0. + ) + ), + EphemeralityTestCase( + input_sequence=np.pad(np.zeros((9996,)), pad_width=(2, 2), constant_values=(1., 1.)), + threshold=0.8, + expected_output=EphemeralitySet( + len_left_core=10000, + len_middle_core=10000, + len_right_core=10000, + len_sorted_core=4, + eph_left_core=0., + eph_middle_core=0., + eph_right_core=0., + eph_sorted_core=0.9995 + ) + ), + EphemeralityTestCase( + input_sequence=np.pad(np.zeros((9996,)), pad_width=(2, 2), constant_values=(1., 1.)), + threshold=0.3, + expected_output=EphemeralitySet( + len_left_core=2, + len_middle_core=9998, + len_right_core=2, + len_sorted_core=2, + eph_left_core=1499 / 1500, + eph_middle_core=0., + eph_right_core=1499 / 1500, + eph_sorted_core=1499 / 1500 + ) + ), + EphemeralityTestCase( + input_sequence=np.pad(np.eye(1, 9999, k=4999).flatten(), pad_width=(1, 1), constant_values=(1., 1.)), + threshold=0.8, + expected_output=EphemeralitySet( + len_left_core=10001, + len_middle_core=10001, + len_right_core=10001, + len_sorted_core=3, + eph_left_core=0., + eph_middle_core=0., + eph_right_core=0., + eph_sorted_core=39989 / 40004 + ) + ), + EphemeralityTestCase( + input_sequence=np.pad(np.eye(1, 9999, k=4999).flatten(), pad_width=(1, 1), constant_values=(1., 1.)), + threshold=0.3, + expected_output=EphemeralitySet( + len_left_core=1, + len_middle_core=1, + len_right_core=1, + len_sorted_core=1, + eph_left_core=29993 / 30003, + eph_middle_core=29993 / 30003, + eph_right_core=29993 / 30003, + eph_sorted_core=29993 / 30003 + ) + ) +] + + +class PartialCoresIterator: + _core_types = ('l', 'm', 'r', 's') + + def __init__(self, expected_output_full: EphemeralitySet) -> None: + self._expected_output_full = expected_output_full + self._chain = None + + def __iter__(self) -> Self: + self._chain = chain.from_iterable(combinations(self._core_types, r) for r in range(1, len(self._core_types) + 1)) + return self + + def __next__(self) -> tuple[str, EphemeralitySet]: + subset = next(self._chain) + return ''.join(subset), self._eph_subset(subset) + + def _eph_subset(self, types: Iterable[str]) -> EphemeralitySet: + eph_set = EphemeralitySet() + for core_type in types: + match core_type: + case 'l': + eph_set.len_left_core = self._expected_output_full.len_left_core + eph_set.eph_left_core = self._expected_output_full.eph_left_core + case 'm': + eph_set.len_middle_core = self._expected_output_full.len_middle_core + eph_set.eph_middle_core = self._expected_output_full.eph_middle_core + case 'r': + eph_set.len_right_core = self._expected_output_full.len_right_core + eph_set.eph_right_core = self._expected_output_full.eph_right_core + case 's': + eph_set.len_sorted_core = self._expected_output_full.len_sorted_core + eph_set.eph_sorted_core = self._expected_output_full.eph_sorted_core + case _: + raise ValueError('Invalid core type!') + return eph_set + + def __len__(self): + return len(self._chain) + + +class TestComputeEphemerality(TestCase): + test_cases: Sequence[EphemeralityTestCase] = DEFAULT_TEST_CASES + + def test_compute_ephemeralities(self): + print('IN-SCRIPT-BASED TEST') + self.run_test('python') + + def test_cmd(self): + print('CMD-BASED TEST') + self.run_test('cmd') + + def test_api(self): + print('API-BASED TEST') + rest_server = subprocess.Popen( + [f'{sys.executable}', '-m', 'ephemerality', 'api'], stdout=subprocess.DEVNULL, stderr=subprocess.STDOUT + ) + time.sleep(5) + self.run_test('api') + rest_server.terminate() + rest_server.wait() + + def run_test(self, run_type: Literal['python', 'cmd', 'api'] = 'python'): + for i, test_case in enumerate(self.test_cases): + if run_type == 'cmd' and len(test_case.input_sequence) > 100: + continue + with self.subTest(): + print(f'Running test case {i}: {test_case.input_sequence}, threshold {test_case.threshold}...') + + partial_output_iterator = PartialCoresIterator(test_case.expected_output) + for core_types, expected_output in partial_output_iterator: + print(f'\tCore subset \"{core_types}\"...') + + match run_type: + case 'cmd': + actual_output = subprocess.check_output([ + f'{sys.executable}', '-m', 'ephemerality', 'cmd', # f'{Path(root_dir) / "ephemerality"}' + " ".join([str(freq) for freq in test_case.input_sequence]), + '-t', str(test_case.threshold), + '-c', core_types + ]).decode('utf-8') + actual_output = EphemeralitySet(**json.loads(actual_output)) + case 'python': + actual_output = compute_ephemerality(activity_vector=test_case.input_sequence, + threshold=test_case.threshold, + types=core_types) + case 'api': + request_data = InputData(input_sequence=test_case.input_sequence, threshold=test_case.threshold) + response = requests.get(f'http://127.0.0.1:8080/ephemerality/all?core_types={core_types}', json=[request_data.model_dump()]) + actual_output = EphemeralitySet(**json.loads(response.content)[0]['output']) + + try: + self.assertEqual(expected_output, actual_output) + except AssertionError as ex: + print(f"\tAssertion error while processing test case {i}.{core_types}: {test_case.input_sequence}, " + f"threshold {test_case.threshold}...") + print(f"\t\tExpected output: {test_case.expected_output}\n\t\tActual output: {actual_output}") + raise ex + + +def test_ephemerality(test_cases: list[EphemeralityTestCase] | None = None) -> None: + if test_cases is None: + test_cases = DEFAULT_TEST_CASES + test = TestComputeEphemerality('test_compute_ephemeralities') + test.test_cases = test_cases + + runner = TextTestRunner() + runner.run(test) diff --git a/testing/src/test_utils.py b/testing/src/test_utils.py new file mode 100644 index 0000000..4d5b00f --- /dev/null +++ b/testing/src/test_utils.py @@ -0,0 +1,11 @@ +from typing import Sequence +from dataclasses import dataclass + +from ephemerality import EphemeralitySet + + +@dataclass +class EphemeralityTestCase: + input_sequence: Sequence[float] + threshold: float + expected_output: EphemeralitySet