ThreeWToolkit 1.0.3__tar.gz → 3.1.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {threewtoolkit-1.0.3 → threewtoolkit-3.1.0}/PKG-INFO +19 -10
- {threewtoolkit-1.0.3 → threewtoolkit-3.1.0}/README.md +15 -14
- {threewtoolkit-1.0.3 → threewtoolkit-3.1.0}/pyproject.toml +17 -8
- threewtoolkit-3.1.0/tests/test_clustering_models.py +265 -0
- threewtoolkit-3.1.0/tests/test_clustering_plots.py +456 -0
- threewtoolkit-3.1.0/tests/test_dataset.py +155 -0
- threewtoolkit-3.1.0/tests/test_distance_computer.py +119 -0
- threewtoolkit-3.1.0/tests/test_instance_quality_filter.py +167 -0
- {threewtoolkit-1.0.3 → threewtoolkit-3.1.0}/tests/test_report_generation.py +5 -4
- threewtoolkit-3.1.0/tests/test_time_series_resampler.py +138 -0
- {threewtoolkit-1.0.3 → threewtoolkit-3.1.0}/toolkit/ThreeWToolkit/README.md +6 -3
- threewtoolkit-3.1.0/toolkit/ThreeWToolkit/assessment/__init__.py +12 -0
- {threewtoolkit-1.0.3 → threewtoolkit-3.1.0}/toolkit/ThreeWToolkit/assessment/assessment_visualizations.py +36 -1
- threewtoolkit-3.1.0/toolkit/ThreeWToolkit/assessment/model_assess.py +380 -0
- threewtoolkit-3.1.0/toolkit/ThreeWToolkit/clustering/__init__.py +26 -0
- threewtoolkit-3.1.0/toolkit/ThreeWToolkit/clustering/_consensus.py +196 -0
- threewtoolkit-3.1.0/toolkit/ThreeWToolkit/clustering/_distances.py +95 -0
- threewtoolkit-3.1.0/toolkit/ThreeWToolkit/clustering/_divisive.py +101 -0
- threewtoolkit-3.1.0/toolkit/ThreeWToolkit/clustering/_hierarchical.py +89 -0
- threewtoolkit-3.1.0/toolkit/ThreeWToolkit/clustering/_normalization.py +51 -0
- threewtoolkit-3.1.0/toolkit/ThreeWToolkit/clustering/_quality.py +87 -0
- threewtoolkit-3.1.0/toolkit/ThreeWToolkit/clustering/_resampling.py +52 -0
- threewtoolkit-3.1.0/toolkit/ThreeWToolkit/clustering/_utils.py +45 -0
- {threewtoolkit-1.0.3 → threewtoolkit-3.1.0}/toolkit/ThreeWToolkit/constants.py +7 -0
- threewtoolkit-3.1.0/toolkit/ThreeWToolkit/core/__init__.py +84 -0
- threewtoolkit-3.1.0/toolkit/ThreeWToolkit/core/base_assessment.py +47 -0
- threewtoolkit-3.1.0/toolkit/ThreeWToolkit/core/base_assessment_visualization.py +16 -0
- threewtoolkit-3.1.0/toolkit/ThreeWToolkit/core/base_clustering.py +175 -0
- threewtoolkit-3.1.0/toolkit/ThreeWToolkit/core/base_dataset.py +47 -0
- threewtoolkit-3.1.0/toolkit/ThreeWToolkit/core/base_feature_extractor.py +29 -0
- threewtoolkit-3.1.0/toolkit/ThreeWToolkit/core/base_instantiable.py +15 -0
- threewtoolkit-3.1.0/toolkit/ThreeWToolkit/core/base_models.py +59 -0
- threewtoolkit-3.1.0/toolkit/ThreeWToolkit/core/base_pipeline.py +37 -0
- threewtoolkit-3.1.0/toolkit/ThreeWToolkit/core/base_prediction_strategies.py +48 -0
- threewtoolkit-3.1.0/toolkit/ThreeWToolkit/core/base_preprocessing.py +38 -0
- threewtoolkit-3.1.0/toolkit/ThreeWToolkit/core/base_trainer.py +420 -0
- threewtoolkit-3.1.0/toolkit/ThreeWToolkit/core/base_transform.py +44 -0
- {threewtoolkit-1.0.3/toolkit/ThreeWToolkit/data_visualization → threewtoolkit-3.1.0/toolkit/ThreeWToolkit/core}/base_visualizer.py +1 -1
- threewtoolkit-3.1.0/toolkit/ThreeWToolkit/core/dataset_outputs.py +29 -0
- threewtoolkit-3.1.0/toolkit/ThreeWToolkit/core/enums.py +109 -0
- {threewtoolkit-1.0.3 → threewtoolkit-3.1.0}/toolkit/ThreeWToolkit/data_visualization/__init__.py +16 -2
- threewtoolkit-3.1.0/toolkit/ThreeWToolkit/data_visualization/clustering_plots.py +502 -0
- {threewtoolkit-1.0.3 → threewtoolkit-3.1.0}/toolkit/ThreeWToolkit/data_visualization/correlation_heatmap.py +1 -1
- {threewtoolkit-1.0.3 → threewtoolkit-3.1.0}/toolkit/ThreeWToolkit/data_visualization/plot_fft.py +1 -1
- {threewtoolkit-1.0.3 → threewtoolkit-3.1.0}/toolkit/ThreeWToolkit/data_visualization/plot_multiple_series.py +14 -3
- {threewtoolkit-1.0.3 → threewtoolkit-3.1.0}/toolkit/ThreeWToolkit/data_visualization/plot_series.py +1 -1
- {threewtoolkit-1.0.3 → threewtoolkit-3.1.0}/toolkit/ThreeWToolkit/data_visualization/seasonal_decomposition.py +1 -1
- {threewtoolkit-1.0.3 → threewtoolkit-3.1.0}/toolkit/ThreeWToolkit/data_visualization/three_w_chart.py +26 -8
- {threewtoolkit-1.0.3 → threewtoolkit-3.1.0}/toolkit/ThreeWToolkit/data_visualization/wavelet_spectrogram.py +1 -1
- threewtoolkit-3.1.0/toolkit/ThreeWToolkit/dataset/__init__.py +17 -0
- threewtoolkit-3.1.0/toolkit/ThreeWToolkit/dataset/parquet_dataset.py +406 -0
- threewtoolkit-3.1.0/toolkit/ThreeWToolkit/dataset/subset_dataset.py +75 -0
- threewtoolkit-3.1.0/toolkit/ThreeWToolkit/dataset/transform_dataset.py +98 -0
- threewtoolkit-3.1.0/toolkit/ThreeWToolkit/dataset/transformed_dataset.py +45 -0
- threewtoolkit-3.1.0/toolkit/ThreeWToolkit/feature_extraction/__init__.py +25 -0
- threewtoolkit-3.1.0/toolkit/ThreeWToolkit/feature_extraction/adapters.py +122 -0
- threewtoolkit-3.1.0/toolkit/ThreeWToolkit/feature_extraction/exponential_statistics.py +184 -0
- threewtoolkit-3.1.0/toolkit/ThreeWToolkit/feature_extraction/statistical.py +109 -0
- threewtoolkit-3.1.0/toolkit/ThreeWToolkit/feature_extraction/wavelet.py +148 -0
- threewtoolkit-3.1.0/toolkit/ThreeWToolkit/feature_extraction/windowing.py +205 -0
- threewtoolkit-3.1.0/toolkit/ThreeWToolkit/logging_config.py +39 -0
- {threewtoolkit-1.0.3 → threewtoolkit-3.1.0}/toolkit/ThreeWToolkit/metrics/_classification.py +26 -19
- {threewtoolkit-1.0.3 → threewtoolkit-3.1.0}/toolkit/ThreeWToolkit/metrics/_regression.py +2 -3
- {threewtoolkit-1.0.3 → threewtoolkit-3.1.0}/toolkit/ThreeWToolkit/models/__init__.py +2 -7
- threewtoolkit-3.1.0/toolkit/ThreeWToolkit/models/mlp.py +91 -0
- threewtoolkit-3.1.0/toolkit/ThreeWToolkit/models/sklearn_models.py +140 -0
- threewtoolkit-3.1.0/toolkit/ThreeWToolkit/models/torch_models.py +103 -0
- threewtoolkit-3.1.0/toolkit/ThreeWToolkit/pipeline.py +377 -0
- threewtoolkit-3.1.0/toolkit/ThreeWToolkit/preprocessing/__init__.py +27 -0
- threewtoolkit-3.1.0/toolkit/ThreeWToolkit/preprocessing/adapters.py +67 -0
- threewtoolkit-3.1.0/toolkit/ThreeWToolkit/preprocessing/clean_signals.py +218 -0
- threewtoolkit-3.1.0/toolkit/ThreeWToolkit/preprocessing/fill_labels.py +80 -0
- threewtoolkit-3.1.0/toolkit/ThreeWToolkit/preprocessing/impute_missing.py +164 -0
- threewtoolkit-3.1.0/toolkit/ThreeWToolkit/preprocessing/normalize.py +160 -0
- threewtoolkit-3.1.0/toolkit/ThreeWToolkit/preprocessing/remap.py +78 -0
- threewtoolkit-3.1.0/toolkit/ThreeWToolkit/preprocessing/rename_column.py +61 -0
- threewtoolkit-3.1.0/toolkit/ThreeWToolkit/reports/__init__.py +3 -0
- {threewtoolkit-1.0.3 → threewtoolkit-3.1.0}/toolkit/ThreeWToolkit/reports/report_generation.py +35 -118
- threewtoolkit-3.1.0/toolkit/ThreeWToolkit/trainer/__init__.py +9 -0
- threewtoolkit-3.1.0/toolkit/ThreeWToolkit/trainer/sklearn_trainer.py +212 -0
- threewtoolkit-3.1.0/toolkit/ThreeWToolkit/trainer/torch_trainer.py +382 -0
- threewtoolkit-3.1.0/toolkit/ThreeWToolkit/utils/__init__.py +13 -0
- threewtoolkit-3.1.0/toolkit/ThreeWToolkit/utils/data_splitter.py +172 -0
- threewtoolkit-3.1.0/toolkit/ThreeWToolkit/utils/data_utils.py +132 -0
- {threewtoolkit-1.0.3 → threewtoolkit-3.1.0}/toolkit/ThreeWToolkit/utils/downloader.py +21 -21
- threewtoolkit-3.1.0/toolkit/ThreeWToolkit/utils/model_recorder.py +122 -0
- {threewtoolkit-1.0.3 → threewtoolkit-3.1.0}/toolkit/ThreeWToolkit/utils/template_manager.py +5 -2
- {threewtoolkit-1.0.3 → threewtoolkit-3.1.0}/toolkit/ThreeWToolkit/utils/trainer_logger.py +6 -3
- {threewtoolkit-1.0.3 → threewtoolkit-3.1.0}/toolkit/ThreeWToolkit.egg-info/PKG-INFO +19 -10
- {threewtoolkit-1.0.3 → threewtoolkit-3.1.0}/toolkit/ThreeWToolkit.egg-info/SOURCES.txt +45 -33
- {threewtoolkit-1.0.3 → threewtoolkit-3.1.0}/toolkit/ThreeWToolkit.egg-info/requires.txt +12 -6
- {threewtoolkit-1.0.3 → threewtoolkit-3.1.0}/toolkit/ThreeWToolkit.egg-info/top_level.txt +1 -0
- threewtoolkit-1.0.3/tests/test_assessment_visualization.py +0 -159
- threewtoolkit-1.0.3/tests/test_data_loader.py +0 -168
- threewtoolkit-1.0.3/tests/test_dataset.py +0 -166
- threewtoolkit-1.0.3/tests/test_downloader.py +0 -92
- threewtoolkit-1.0.3/tests/test_example.py +0 -5
- threewtoolkit-1.0.3/tests/test_extract_exponential_statistical_features.py +0 -562
- threewtoolkit-1.0.3/tests/test_extract_statistical_features.py +0 -512
- threewtoolkit-1.0.3/tests/test_extract_wavelet_features.py +0 -286
- threewtoolkit-1.0.3/tests/test_load_model.py +0 -180
- threewtoolkit-1.0.3/tests/test_metrics.py +0 -743
- threewtoolkit-1.0.3/tests/test_mlp.py +0 -241
- threewtoolkit-1.0.3/tests/test_model_assessment.py +0 -1047
- threewtoolkit-1.0.3/tests/test_pipeline.py +0 -691
- threewtoolkit-1.0.3/tests/test_plot_correlation_heatmap.py +0 -87
- threewtoolkit-1.0.3/tests/test_plot_multiple_series.py +0 -73
- threewtoolkit-1.0.3/tests/test_plot_series.py +0 -123
- threewtoolkit-1.0.3/tests/test_preprocessing.py +0 -429
- threewtoolkit-1.0.3/tests/test_save_best_model.py +0 -111
- threewtoolkit-1.0.3/tests/test_sklearn_models.py +0 -200
- threewtoolkit-1.0.3/tests/test_template_manager.py +0 -256
- threewtoolkit-1.0.3/tests/test_trainer.py +0 -575
- threewtoolkit-1.0.3/tests/test_trainer_logger.py +0 -81
- threewtoolkit-1.0.3/toolkit/ThreeWToolkit/assessment/__init__.py +0 -3
- threewtoolkit-1.0.3/toolkit/ThreeWToolkit/assessment/model_assess.py +0 -702
- threewtoolkit-1.0.3/toolkit/ThreeWToolkit/core/__init__.py +0 -0
- threewtoolkit-1.0.3/toolkit/ThreeWToolkit/core/base_assessment.py +0 -109
- threewtoolkit-1.0.3/toolkit/ThreeWToolkit/core/base_assessment_visualization.py +0 -25
- threewtoolkit-1.0.3/toolkit/ThreeWToolkit/core/base_dataset.py +0 -90
- threewtoolkit-1.0.3/toolkit/ThreeWToolkit/core/base_feature_extractor.py +0 -179
- threewtoolkit-1.0.3/toolkit/ThreeWToolkit/core/base_metrics.py +0 -224
- threewtoolkit-1.0.3/toolkit/ThreeWToolkit/core/base_model_trainer.py +0 -11
- threewtoolkit-1.0.3/toolkit/ThreeWToolkit/core/base_models.py +0 -40
- threewtoolkit-1.0.3/toolkit/ThreeWToolkit/core/base_preprocessing.py +0 -152
- threewtoolkit-1.0.3/toolkit/ThreeWToolkit/core/base_step.py +0 -66
- threewtoolkit-1.0.3/toolkit/ThreeWToolkit/core/base_time_series_holdout.py +0 -35
- threewtoolkit-1.0.3/toolkit/ThreeWToolkit/core/enums.py +0 -49
- threewtoolkit-1.0.3/toolkit/ThreeWToolkit/data_loader/__init__.py +0 -5
- threewtoolkit-1.0.3/toolkit/ThreeWToolkit/dataset/__init__.py +0 -6
- threewtoolkit-1.0.3/toolkit/ThreeWToolkit/dataset/parquet_dataset.py +0 -333
- threewtoolkit-1.0.3/toolkit/ThreeWToolkit/feature_extraction/__init__.py +0 -10
- threewtoolkit-1.0.3/toolkit/ThreeWToolkit/feature_extraction/extract_exponential_statistics_features.py +0 -359
- threewtoolkit-1.0.3/toolkit/ThreeWToolkit/feature_extraction/extract_statistical_features.py +0 -297
- threewtoolkit-1.0.3/toolkit/ThreeWToolkit/feature_extraction/extract_wavelet_features.py +0 -309
- threewtoolkit-1.0.3/toolkit/ThreeWToolkit/models/mlp.py +0 -691
- threewtoolkit-1.0.3/toolkit/ThreeWToolkit/models/sklearn_models.py +0 -185
- threewtoolkit-1.0.3/toolkit/ThreeWToolkit/pipeline.py +0 -592
- threewtoolkit-1.0.3/toolkit/ThreeWToolkit/preprocessing/__init__.py +0 -13
- threewtoolkit-1.0.3/toolkit/ThreeWToolkit/preprocessing/_data_processing.py +0 -492
- threewtoolkit-1.0.3/toolkit/ThreeWToolkit/reports/__init__.py +0 -0
- threewtoolkit-1.0.3/toolkit/ThreeWToolkit/trainer/trainer.py +0 -831
- threewtoolkit-1.0.3/toolkit/ThreeWToolkit/utils/__init__.py +0 -3
- threewtoolkit-1.0.3/toolkit/ThreeWToolkit/utils/data_utils.py +0 -397
- threewtoolkit-1.0.3/toolkit/ThreeWToolkit/utils/model_recorder.py +0 -97
- {threewtoolkit-1.0.3 → threewtoolkit-3.1.0}/LICENSE.md +0 -0
- {threewtoolkit-1.0.3 → threewtoolkit-3.1.0}/setup.cfg +0 -0
- {threewtoolkit-1.0.3 → threewtoolkit-3.1.0}/toolkit/ThreeWToolkit/__init__.py +0 -0
- {threewtoolkit-1.0.3 → threewtoolkit-3.1.0}/toolkit/ThreeWToolkit/data_visualization/plot_utils.py +0 -0
- {threewtoolkit-1.0.3 → threewtoolkit-3.1.0}/toolkit/ThreeWToolkit/data_visualization/plots.py +0 -0
- {threewtoolkit-1.0.3/toolkit/ThreeWToolkit/data_loader → threewtoolkit-3.1.0/toolkit/ThreeWToolkit/dataset}/csv_data_loader.py +0 -0
- {threewtoolkit-1.0.3 → threewtoolkit-3.1.0}/toolkit/ThreeWToolkit/metrics/__init__.py +0 -0
- {threewtoolkit-1.0.3 → threewtoolkit-3.1.0}/toolkit/ThreeWToolkit/utils/general_utils.py +0 -0
- {threewtoolkit-1.0.3 → threewtoolkit-3.1.0}/toolkit/ThreeWToolkit.egg-info/dependency_links.txt +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: ThreeWToolkit
|
|
3
|
-
Version: 1.0
|
|
3
|
+
Version: 3.1.0
|
|
4
4
|
Summary: A modular and open-source AI toolkit for time-series processing, aimed at fault detection and classification in oil well operation
|
|
5
5
|
Author-email: Ricardo Emanuel Vaz Vargas <ricardo.vargas@petrobras.com.br>
|
|
6
6
|
License: Apache-2.0
|
|
@@ -26,6 +26,7 @@ Requires-Dist: ipykernel>=6.29.5
|
|
|
26
26
|
Requires-Dist: ipywidgets==8.1.7
|
|
27
27
|
Requires-Dist: jupyter-client==8.6.3
|
|
28
28
|
Requires-Dist: jupyter-core==5.8.1
|
|
29
|
+
Requires-Dist: tornado<7,>=6.5.5
|
|
29
30
|
Requires-Dist: Pillow>=11.0.0
|
|
30
31
|
Requires-Dist: scikit-image>=0.25.2
|
|
31
32
|
Requires-Dist: torch>=2.7.0
|
|
@@ -38,17 +39,22 @@ Requires-Dist: PyWavelets>=1.8.0
|
|
|
38
39
|
Requires-Dist: pyarrow>=20.0.0
|
|
39
40
|
Requires-Dist: pylatex>=1.4.2
|
|
40
41
|
Requires-Dist: statsmodels>=0.14.5
|
|
41
|
-
Requires-Dist: requests
|
|
42
|
+
Requires-Dist: requests<3,>=2.31
|
|
43
|
+
Requires-Dist: black[jupyter]==26.5.0
|
|
44
|
+
Requires-Dist: nbconvert
|
|
45
|
+
Requires-Dist: nbformat
|
|
46
|
+
Requires-Dist: dtaidistance>=2.3
|
|
47
|
+
Requires-Dist: joblib>=1.0
|
|
42
48
|
Provides-Extra: dev
|
|
43
|
-
Requires-Dist:
|
|
44
|
-
Requires-Dist:
|
|
45
|
-
Requires-Dist:
|
|
49
|
+
Requires-Dist: mypy>=1.19.1; extra == "dev"
|
|
50
|
+
Requires-Dist: black==26.5.0; extra == "dev"
|
|
51
|
+
Requires-Dist: ruff>=0.14.13; extra == "dev"
|
|
52
|
+
Requires-Dist: pytest>=9.0.2; extra == "dev"
|
|
53
|
+
Requires-Dist: pytest-cov>=7.0.0; extra == "dev"
|
|
46
54
|
Requires-Dist: jupyter; extra == "dev"
|
|
47
|
-
Requires-Dist: ruff>=0.12.2; extra == "dev"
|
|
48
55
|
Requires-Dist: uv; extra == "dev"
|
|
49
56
|
Requires-Dist: types-requests>=2.32.4.20250611; extra == "dev"
|
|
50
57
|
Requires-Dist: coverage>=7.8.1; extra == "dev"
|
|
51
|
-
Requires-Dist: pytest>=8.3.5; extra == "dev"
|
|
52
58
|
Requires-Dist: pytest-mock>=3.14.1; extra == "dev"
|
|
53
59
|
Provides-Extra: docs
|
|
54
60
|
Requires-Dist: mkdocs; extra == "docs"
|
|
@@ -59,6 +65,7 @@ Dynamic: license-file
|
|
|
59
65
|
[![Apache 2.0][apache-shield]][apache]
|
|
60
66
|
[![Code style][black-shield]][black]
|
|
61
67
|
[![Versioning][semver-shield]][semver]
|
|
68
|
+
[![Coverage Status][coveralls-shield]][coveralls]
|
|
62
69
|
|
|
63
70
|
[apache]: https://opensource.org/licenses/Apache-2.0
|
|
64
71
|
[apache-shield]: https://img.shields.io/badge/License-Apache_2.0-blue.svg
|
|
@@ -66,6 +73,8 @@ Dynamic: license-file
|
|
|
66
73
|
[black-shield]: https://img.shields.io/badge/code%20style-black-000000.svg
|
|
67
74
|
[semver]: https://semver.org
|
|
68
75
|
[semver-shield]: https://img.shields.io/badge/semver-2.0.1-blue
|
|
76
|
+
[coveralls]: https://coveralls.io/github/rafaelpadilla/3W?branch=dev
|
|
77
|
+
[coveralls-shield]: https://coveralls.io/repos/github/rafaelpadilla/3W/badge.svg?branch=dev
|
|
69
78
|
|
|
70
79
|
<h1>
|
|
71
80
|
<img src="https://raw.githubusercontent.com/petrobras/3W/main/images/3w_logo.png" width="45" style="vertical-align: middle; margin-right: 10px;" />
|
|
@@ -118,7 +127,7 @@ To better understand how the system is organized, we can divide all classes into
|
|
|
118
127
|
## 📊 3W Dataset <a id="dataset"></a>
|
|
119
128
|
The **3W dataset** serves as a **reference dataset** for this project and is hosted on [Figshare](https://figshare.com/projects/3W_Dataset/251195). However, the toolkit is not limited to the 3W dataset and can be adapted for other datasets as well.
|
|
120
129
|
|
|
121
|
-
Further details on the 3W dataset’s structure, preprocessing, and usage are available in the [3W_DATASET_STRUCTURE.md](
|
|
130
|
+
Further details on the 3W dataset’s structure, preprocessing, and usage are available in the [3W_DATASET_STRUCTURE.md](../../3W_DATASET_STRUCTURE.md) file.
|
|
122
131
|
|
|
123
132
|
|
|
124
133
|
## ⚙️ Installation & Setup <a id="installation"></a>
|
|
@@ -161,11 +170,11 @@ conda activate 3W
|
|
|
161
170
|
## 🪐 Jupyter Notebooks & Examples <a id="notebooks"></a>
|
|
162
171
|
A curated set of ready-to-use jupyter notebooks that demonstrate how to use the toolkit to common fault detection tasks using the 3W dataset. These examples accelerate onboarding and reproducibility.
|
|
163
172
|
|
|
164
|
-
The set of notebooks can be found in the [notebooks folder](../
|
|
173
|
+
The set of notebooks can be found in the [notebooks folder](../demos/)
|
|
165
174
|
|
|
166
175
|
|
|
167
176
|
## 🤝 Contributing <a id="contributing"></a>
|
|
168
177
|
|
|
169
|
-
We welcome contributions to help us improve and expand the functionality of the 3W toolkit. To ensure a smooth collaboration process, please follow our contrubuting guidelines [here](
|
|
178
|
+
We welcome contributions to help us improve and expand the functionality of the 3W toolkit. To ensure a smooth collaboration process, please follow our contrubuting guidelines [here](../../CONTRIBUTING.md).
|
|
170
179
|
|
|
171
180
|
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
[![CC BY 4.0][cc-by-shield]][cc-by]
|
|
4
4
|
[![Code style][black-shield]][black]
|
|
5
5
|
[![Versioning][semver-shield]][semver]
|
|
6
|
+
[![Coverage Status][coveralls-shield]][coveralls]
|
|
6
7
|
|
|
7
8
|
[apache]: https://opensource.org/licenses/Apache-2.0
|
|
8
9
|
[apache-shield]: https://img.shields.io/badge/License-Apache_2.0-blue.svg
|
|
@@ -12,6 +13,8 @@
|
|
|
12
13
|
[black-shield]: https://img.shields.io/badge/code%20style-black-000000.svg
|
|
13
14
|
[semver]: https://semver.org
|
|
14
15
|
[semver-shield]: https://img.shields.io/badge/semver-2.0.0-blue
|
|
16
|
+
[coveralls]: https://coveralls.io/github/rafaelpadilla/3W?branch=dev
|
|
17
|
+
[coveralls-shield]: https://coveralls.io/repos/github/rafaelpadilla/3W/badge.svg?branch=dev
|
|
15
18
|
|
|
16
19
|
# Table of Content
|
|
17
20
|
|
|
@@ -87,22 +90,24 @@ It is also very important to know, participate and follow the discussions. See t
|
|
|
87
90
|
|
|
88
91
|
## Licenses
|
|
89
92
|
|
|
90
|
-
All the code of this project is licensed under the [Apache 2.0 License]
|
|
93
|
+
All the code of this project is licensed under the [Apache 2.0 License](https://opensource.org/licenses/Apache-2.0) and all 3W Dataset's data files (Parquet files saved in subdirectories of the [dataset](dataset) directory) are licensed under the [Creative Commons Attribution 4.0 International License](http://creativecommons.org/licenses/by/4.0/).
|
|
91
94
|
|
|
92
95
|
## Versioning
|
|
93
96
|
|
|
94
97
|
In the 3W Project, three types of versions will be managed as follows.
|
|
95
98
|
|
|
96
|
-
* Version of the 3W Toolkit: specified in the [
|
|
99
|
+
* Version of the 3W Toolkit: specified in the [pyproject.toml](pyproject.toml) file;
|
|
97
100
|
* Version of the 3W Dataset: specified in the [dataset.ini](dataset/dataset.ini) file;
|
|
98
101
|
* Version of the 3W Project: specified with tags in the git repository;
|
|
99
|
-
* We will exclusively use the semantic versioning defined in https://semver.org;
|
|
102
|
+
* We will exclusively use the semantic versioning defined in [https://semver.org](https://semver.org);
|
|
100
103
|
* Versions will always be updated manually;
|
|
101
104
|
* Versioning of the 3W Toolkit and 3W Dataset are completely independent of each other;
|
|
102
105
|
* The version of the 3W Project will be updated whenever, and only when, there is a new commit in the `main` branch of the repository, regardless of the updated resource: 3W Toolkit, 3W Dataset, 3W Project's documentation, example of use, etc;
|
|
103
106
|
* We will only use annotated tags and for each tag there will be a release in the remote repository (GitHub);
|
|
104
107
|
* Content for each release will be automatically generated with functionality provided by GitHub.
|
|
105
108
|
|
|
109
|
+
For detailed versioning rules, including SemVer guidelines for the 3W Toolkit, see [VERSIONING.md](VERSIONING.md).
|
|
110
|
+
|
|
106
111
|
## Questions
|
|
107
112
|
|
|
108
113
|
See the discussions section. If you don't get clarification, please open discussions to ask your questions so we can answer them.
|
|
@@ -117,7 +122,7 @@ The 3W Dataset consists of multiple Parquet files saved in subdirectories of the
|
|
|
117
122
|
|
|
118
123
|
## Overview
|
|
119
124
|
|
|
120
|
-
A 3W Dataset's general presentation with some quantities and statistics is available in [this](
|
|
125
|
+
A 3W Dataset's general presentation with some quantities and statistics is available in [this](dataset/demos/_basic/main.ipynb) Jupyter Notebook.
|
|
121
126
|
|
|
122
127
|
# 3W Toolkit
|
|
123
128
|
|
|
@@ -133,23 +138,19 @@ It is important to note that there are arbitrary choices in this toolkit, but th
|
|
|
133
138
|
|
|
134
139
|
The 3W Toolkit is implemented in sub-modules as discribed [here](3W_TOOLKIT_STRUCTURE.md).
|
|
135
140
|
|
|
136
|
-
## Incorporated
|
|
137
|
-
|
|
138
|
-
Specific problems will be incorporated into this project gradually. At this point, we can work on:
|
|
141
|
+
## Incorporated Benchmarks
|
|
139
142
|
|
|
140
|
-
|
|
143
|
+
Benchmarks for specific tasks will be gradually incorporated into this project. They will be located in the `3W/benchmarks/` folder.
|
|
141
144
|
|
|
142
|
-
All
|
|
145
|
+
We encourage the community to contribute their own benchmarks. All specifications are detailed in the [Contributing Guide](CONTRIBUTING.md).
|
|
143
146
|
|
|
144
147
|
## Examples of Use
|
|
145
148
|
|
|
146
149
|
The list below with examples of how to use the 3W Toolkit will be incremented throughout its development.
|
|
147
150
|
|
|
148
151
|
* 3W Dataset's overviews:
|
|
149
|
-
* [Baseline](
|
|
150
|
-
* [André Machado's overview](
|
|
151
|
-
* Binary classifier of Spurious Closure of DHSV:
|
|
152
|
-
* [Baseline](problems/01_binary_classifier_of_spurious_closure_of_dhsv/_baseline/main.ipynb)
|
|
152
|
+
* [Baseline](dataset/demos/_basic/main.ipynb)
|
|
153
|
+
* [André Machado's overview](dataset/demos/AndreMachado/main.ipynb)
|
|
153
154
|
|
|
154
155
|
For a contribution of yours to be listed here, follow the instructions detailed in the [CONTRIBUTING GUIDE](CONTRIBUTING.md).
|
|
155
156
|
|
|
@@ -157,7 +158,7 @@ For a contribution of yours to be listed here, follow the instructions detailed
|
|
|
157
158
|
|
|
158
159
|
For all results generated by the 3W Toolkit to be consistent, we recommend you create and use a virtual environment with the packages versions specified in the [environment.yml](environment.yml), which was generated with [conda](https://docs.conda.io). Our current recommendation is to use the conda distributed by [Miniforge](https://conda-forge.org/download/). Download and install Miniforge according to the official instructions. Open a prompt on your operating system (Windows, Linux or MacOS). Make sure the current directory is the directory where you have the 3W. Run the following commands as needed:
|
|
159
160
|
|
|
160
|
-
* To create a virtual environment from our [environment.yml](environment.yml):
|
|
161
|
+
* To create a virtual environment from our [environment.yml](environment.yml):
|
|
161
162
|
```
|
|
162
163
|
$ conda env create -f environment.yml
|
|
163
164
|
```
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "ThreeWToolkit"
|
|
3
|
-
version = "1.0
|
|
3
|
+
version = "3.1.0"
|
|
4
4
|
description = "A modular and open-source AI toolkit for time-series processing, aimed at fault detection and classification in oil well operation"
|
|
5
5
|
readme = { file = "toolkit/ThreeWToolkit/README.md", content-type = "text/markdown" }
|
|
6
6
|
license = { text = "Apache-2.0" }
|
|
@@ -31,6 +31,7 @@ dependencies = [
|
|
|
31
31
|
"ipywidgets==8.1.7",
|
|
32
32
|
"jupyter-client==8.6.3",
|
|
33
33
|
"jupyter-core==5.8.1",
|
|
34
|
+
"tornado>=6.5.5,<7",
|
|
34
35
|
"Pillow>=11.0.0",
|
|
35
36
|
"scikit-image>=0.25.2",
|
|
36
37
|
"torch>=2.7.0",
|
|
@@ -43,7 +44,12 @@ dependencies = [
|
|
|
43
44
|
"pyarrow>=20.0.0",
|
|
44
45
|
"pylatex>=1.4.2",
|
|
45
46
|
"statsmodels>=0.14.5",
|
|
46
|
-
"requests
|
|
47
|
+
"requests>=2.31,<3",
|
|
48
|
+
"black[jupyter]==26.5.0",
|
|
49
|
+
"nbconvert",
|
|
50
|
+
"nbformat",
|
|
51
|
+
"dtaidistance>=2.3",
|
|
52
|
+
"joblib>=1.0",
|
|
47
53
|
]
|
|
48
54
|
|
|
49
55
|
[project.urls]
|
|
@@ -56,15 +62,15 @@ Changelog = "https://github.com/petrobras/3W/releases"
|
|
|
56
62
|
|
|
57
63
|
[project.optional-dependencies]
|
|
58
64
|
dev = [
|
|
59
|
-
"
|
|
60
|
-
"
|
|
61
|
-
"
|
|
65
|
+
"mypy>=1.19.1",
|
|
66
|
+
"black==26.5.0",
|
|
67
|
+
"ruff>=0.14.13",
|
|
68
|
+
"pytest>=9.0.2",
|
|
69
|
+
"pytest-cov>=7.0.0",
|
|
62
70
|
"jupyter",
|
|
63
|
-
"ruff>=0.12.2",
|
|
64
71
|
"uv",
|
|
65
72
|
"types-requests>=2.32.4.20250611",
|
|
66
73
|
"coverage>=7.8.1",
|
|
67
|
-
"pytest>=8.3.5",
|
|
68
74
|
"pytest-mock>=3.14.1",
|
|
69
75
|
]
|
|
70
76
|
docs = [
|
|
@@ -98,8 +104,11 @@ line-length = 88
|
|
|
98
104
|
[tool.ruff.format]
|
|
99
105
|
quote-style = "double"
|
|
100
106
|
|
|
107
|
+
[tool.black]
|
|
108
|
+
line-length = 88
|
|
109
|
+
target-version = ["py310"]
|
|
110
|
+
|
|
101
111
|
[tool.mypy]
|
|
102
112
|
python_version = "3.10"
|
|
103
113
|
mypy_path = "toolkit/"
|
|
104
114
|
ignore_missing_imports = true
|
|
105
|
-
|
|
@@ -0,0 +1,265 @@
|
|
|
1
|
+
import pytest
|
|
2
|
+
import numpy as np
|
|
3
|
+
from pydantic import ValidationError
|
|
4
|
+
|
|
5
|
+
from ThreeWToolkit.core.base_clustering import (
|
|
6
|
+
HierarchicalClusteringConfig,
|
|
7
|
+
DivisiveClusteringConfig,
|
|
8
|
+
MultivariateConsensusConfig,
|
|
9
|
+
)
|
|
10
|
+
from ThreeWToolkit.core.enums import LinkageMethodEnum
|
|
11
|
+
from ThreeWToolkit.clustering import (
|
|
12
|
+
HierarchicalClusterer,
|
|
13
|
+
DivisiveRanker,
|
|
14
|
+
MultivariateConsensus,
|
|
15
|
+
)
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class TestHierarchicalClusterer:
|
|
19
|
+
"""Test suite for HierarchicalClusterer estimator."""
|
|
20
|
+
|
|
21
|
+
@pytest.fixture
|
|
22
|
+
def distance_matrix(self):
|
|
23
|
+
"""Two tight pairs: (0,1) and (2,3)."""
|
|
24
|
+
return np.array(
|
|
25
|
+
[
|
|
26
|
+
[0.0, 1.0, 5.0, 6.0],
|
|
27
|
+
[1.0, 0.0, 4.0, 5.0],
|
|
28
|
+
[5.0, 4.0, 0.0, 2.0],
|
|
29
|
+
[6.0, 5.0, 2.0, 0.0],
|
|
30
|
+
]
|
|
31
|
+
)
|
|
32
|
+
|
|
33
|
+
@pytest.fixture
|
|
34
|
+
def config(self):
|
|
35
|
+
return HierarchicalClusteringConfig(
|
|
36
|
+
linkage_method=LinkageMethodEnum.AVERAGE,
|
|
37
|
+
default_threshold=0.5,
|
|
38
|
+
)
|
|
39
|
+
|
|
40
|
+
@pytest.fixture
|
|
41
|
+
def fitted_clusterer(self, config, distance_matrix):
|
|
42
|
+
clusterer = HierarchicalClusterer(config)
|
|
43
|
+
clusterer.fit(distance_matrix)
|
|
44
|
+
return clusterer
|
|
45
|
+
|
|
46
|
+
def test_fit_returns_self(self, config, distance_matrix):
|
|
47
|
+
clusterer = HierarchicalClusterer(config)
|
|
48
|
+
result = clusterer.fit(distance_matrix)
|
|
49
|
+
assert result is clusterer
|
|
50
|
+
|
|
51
|
+
def test_linkage_matrix_stored(self, fitted_clusterer):
|
|
52
|
+
assert fitted_clusterer.linkage_matrix_ is not None
|
|
53
|
+
assert fitted_clusterer.linkage_matrix_.ndim == 2
|
|
54
|
+
assert fitted_clusterer.linkage_matrix_.shape[1] == 4
|
|
55
|
+
|
|
56
|
+
def test_distance_matrix_normalized(self, fitted_clusterer):
|
|
57
|
+
assert fitted_clusterer.distance_matrix_normalized_ is not None
|
|
58
|
+
assert fitted_clusterer.distance_matrix_normalized_.max() <= 1.0
|
|
59
|
+
|
|
60
|
+
def test_clusters_at_low_threshold(self, fitted_clusterer):
|
|
61
|
+
"""At a very low threshold, each instance is its own cluster."""
|
|
62
|
+
labels = fitted_clusterer.get_clusters_at_threshold(0.01)
|
|
63
|
+
assert len(labels) == 4
|
|
64
|
+
assert len(set(labels)) == 4
|
|
65
|
+
|
|
66
|
+
def test_clusters_at_high_threshold(self, fitted_clusterer):
|
|
67
|
+
"""At a high threshold, all instances merge into one cluster."""
|
|
68
|
+
labels = fitted_clusterer.get_clusters_at_threshold(1.0)
|
|
69
|
+
assert len(labels) == 4
|
|
70
|
+
assert len(set(labels)) == 1
|
|
71
|
+
|
|
72
|
+
def test_find_main_cluster_indices(self, fitted_clusterer):
|
|
73
|
+
indices = fitted_clusterer.find_main_cluster_indices(1.0)
|
|
74
|
+
assert sorted(indices) == [0, 1, 2, 3]
|
|
75
|
+
|
|
76
|
+
def test_raises_before_fit(self, config):
|
|
77
|
+
clusterer = HierarchicalClusterer(config)
|
|
78
|
+
with pytest.raises(RuntimeError, match="must be fitted"):
|
|
79
|
+
clusterer.get_clusters_at_threshold(0.5)
|
|
80
|
+
|
|
81
|
+
def test_zero_distance_matrix(self, config):
|
|
82
|
+
"""All-zero distance matrix should not crash."""
|
|
83
|
+
zeros = np.zeros((3, 3))
|
|
84
|
+
clusterer = HierarchicalClusterer(config)
|
|
85
|
+
clusterer.fit(zeros)
|
|
86
|
+
labels = clusterer.get_clusters_at_threshold(0.5)
|
|
87
|
+
assert len(labels) == 3
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
class TestDivisiveRanker:
|
|
91
|
+
"""Test suite for DivisiveRanker estimator."""
|
|
92
|
+
|
|
93
|
+
@pytest.fixture
|
|
94
|
+
def distance_matrix(self):
|
|
95
|
+
"""Instance 2 is the outlier (highest total distance)."""
|
|
96
|
+
return np.array(
|
|
97
|
+
[
|
|
98
|
+
[0.0, 1.0, 10.0],
|
|
99
|
+
[1.0, 0.0, 9.0],
|
|
100
|
+
[10.0, 9.0, 0.0],
|
|
101
|
+
]
|
|
102
|
+
)
|
|
103
|
+
|
|
104
|
+
@pytest.fixture
|
|
105
|
+
def config(self):
|
|
106
|
+
return DivisiveClusteringConfig()
|
|
107
|
+
|
|
108
|
+
@pytest.fixture
|
|
109
|
+
def fitted_ranker(self, config, distance_matrix):
|
|
110
|
+
ranker = DivisiveRanker(config)
|
|
111
|
+
ranker.fit(distance_matrix)
|
|
112
|
+
return ranker
|
|
113
|
+
|
|
114
|
+
def test_fit_returns_self(self, config, distance_matrix):
|
|
115
|
+
ranker = DivisiveRanker(config)
|
|
116
|
+
result = ranker.fit(distance_matrix)
|
|
117
|
+
assert result is ranker
|
|
118
|
+
|
|
119
|
+
def test_ranking_length_matches_instances(self, fitted_ranker):
|
|
120
|
+
ranking = fitted_ranker.get_ranked_indices()
|
|
121
|
+
assert len(ranking) == 3
|
|
122
|
+
|
|
123
|
+
def test_ranking_contains_all_indices(self, fitted_ranker):
|
|
124
|
+
ranking = fitted_ranker.get_ranked_indices()
|
|
125
|
+
assert sorted(ranking) == [0, 1, 2]
|
|
126
|
+
|
|
127
|
+
def test_outlier_eliminated_first(self, fitted_ranker):
|
|
128
|
+
"""Instance 2 has the largest distance sum and should be eliminated first."""
|
|
129
|
+
ranking = fitted_ranker.get_ranked_indices()
|
|
130
|
+
assert ranking[0] == 2
|
|
131
|
+
|
|
132
|
+
def test_elimination_distances_length(self, fitted_ranker):
|
|
133
|
+
assert len(fitted_ranker.elimination_distances_) == 3
|
|
134
|
+
|
|
135
|
+
def test_last_elimination_distance_is_zero(self, fitted_ranker):
|
|
136
|
+
assert fitted_ranker.elimination_distances_[-1] == 0.0
|
|
137
|
+
|
|
138
|
+
def test_raises_before_fit(self, config):
|
|
139
|
+
ranker = DivisiveRanker(config)
|
|
140
|
+
with pytest.raises(RuntimeError, match="must be fitted"):
|
|
141
|
+
ranker.get_ranked_indices()
|
|
142
|
+
|
|
143
|
+
def test_raises_on_non_square_matrix(self, config):
|
|
144
|
+
ranker = DivisiveRanker(config)
|
|
145
|
+
with pytest.raises(ValueError, match="2D square"):
|
|
146
|
+
ranker.fit(np.array([[1.0, 2.0]]))
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
class TestMultivariateConsensus:
|
|
150
|
+
"""Test suite for MultivariateConsensus estimator."""
|
|
151
|
+
|
|
152
|
+
@pytest.fixture
|
|
153
|
+
def config(self):
|
|
154
|
+
return MultivariateConsensusConfig(
|
|
155
|
+
min_threshold=0.1,
|
|
156
|
+
max_threshold=1.0,
|
|
157
|
+
threshold_step=0.1,
|
|
158
|
+
)
|
|
159
|
+
|
|
160
|
+
@pytest.fixture
|
|
161
|
+
def fitted_hierarchical_models(self):
|
|
162
|
+
"""Build two fitted HierarchicalClusterers for two variables."""
|
|
163
|
+
cfg = HierarchicalClusteringConfig(
|
|
164
|
+
linkage_method=LinkageMethodEnum.AVERAGE,
|
|
165
|
+
default_threshold=0.5,
|
|
166
|
+
)
|
|
167
|
+
# Variable A: instances 0,1 are close; instance 2 is far
|
|
168
|
+
dm_a = np.array(
|
|
169
|
+
[
|
|
170
|
+
[0.0, 1.0, 10.0],
|
|
171
|
+
[1.0, 0.0, 9.0],
|
|
172
|
+
[10.0, 9.0, 0.0],
|
|
173
|
+
]
|
|
174
|
+
)
|
|
175
|
+
model_a = HierarchicalClusterer(cfg)
|
|
176
|
+
model_a.fit(dm_a)
|
|
177
|
+
|
|
178
|
+
# Variable B: similar structure
|
|
179
|
+
dm_b = np.array(
|
|
180
|
+
[
|
|
181
|
+
[0.0, 2.0, 8.0],
|
|
182
|
+
[2.0, 0.0, 7.0],
|
|
183
|
+
[8.0, 7.0, 0.0],
|
|
184
|
+
]
|
|
185
|
+
)
|
|
186
|
+
model_b = HierarchicalClusterer(cfg)
|
|
187
|
+
model_b.fit(dm_b)
|
|
188
|
+
|
|
189
|
+
return {"var_A": model_a, "var_B": model_b}
|
|
190
|
+
|
|
191
|
+
def test_fit_returns_self(self, config, fitted_hierarchical_models):
|
|
192
|
+
consensus = MultivariateConsensus(config)
|
|
193
|
+
result = consensus.fit(fitted_hierarchical_models)
|
|
194
|
+
assert result is consensus
|
|
195
|
+
|
|
196
|
+
def test_selection_mask_shape(self, config, fitted_hierarchical_models):
|
|
197
|
+
consensus = MultivariateConsensus(config)
|
|
198
|
+
consensus.fit(fitted_hierarchical_models)
|
|
199
|
+
|
|
200
|
+
n_thresholds = len(consensus.thresholds_analyzed_)
|
|
201
|
+
assert consensus.selection_mask_.shape[0] == n_thresholds
|
|
202
|
+
assert consensus.selection_mask_.shape[1] > 0
|
|
203
|
+
|
|
204
|
+
def test_common_counts_populated(self, config, fitted_hierarchical_models):
|
|
205
|
+
consensus = MultivariateConsensus(config)
|
|
206
|
+
consensus.fit(fitted_hierarchical_models)
|
|
207
|
+
|
|
208
|
+
assert len(consensus.common_counts_) == len(consensus.thresholds_analyzed_)
|
|
209
|
+
|
|
210
|
+
def test_high_threshold_selects_all(self, config, fitted_hierarchical_models):
|
|
211
|
+
consensus = MultivariateConsensus(config)
|
|
212
|
+
consensus.fit(fitted_hierarchical_models)
|
|
213
|
+
|
|
214
|
+
selected = consensus.get_selected_indices_at_threshold(1.0)
|
|
215
|
+
assert len(selected) == 3
|
|
216
|
+
|
|
217
|
+
def test_low_threshold_selects_fewer(self, config, fitted_hierarchical_models):
|
|
218
|
+
consensus = MultivariateConsensus(config)
|
|
219
|
+
consensus.fit(fitted_hierarchical_models)
|
|
220
|
+
|
|
221
|
+
selected_low = consensus.get_selected_indices_at_threshold(0.1)
|
|
222
|
+
selected_high = consensus.get_selected_indices_at_threshold(1.0)
|
|
223
|
+
assert len(selected_low) <= len(selected_high)
|
|
224
|
+
|
|
225
|
+
def test_raises_before_fit(self, config):
|
|
226
|
+
consensus = MultivariateConsensus(config)
|
|
227
|
+
with pytest.raises(RuntimeError, match="not fitted"):
|
|
228
|
+
consensus.get_selected_indices_at_threshold(0.5)
|
|
229
|
+
|
|
230
|
+
def test_with_valid_indices_map(self, config, fitted_hierarchical_models):
|
|
231
|
+
valid_indices = {
|
|
232
|
+
"var_A": [0, 1, 2],
|
|
233
|
+
"var_B": [0, 1, 2],
|
|
234
|
+
}
|
|
235
|
+
consensus = MultivariateConsensus(config)
|
|
236
|
+
consensus.fit(
|
|
237
|
+
fitted_hierarchical_models,
|
|
238
|
+
valid_indices_map=valid_indices,
|
|
239
|
+
)
|
|
240
|
+
|
|
241
|
+
assert consensus._is_fitted
|
|
242
|
+
|
|
243
|
+
|
|
244
|
+
class TestMultivariateConsensusConfig:
|
|
245
|
+
"""Test suite for MultivariateConsensusConfig validation."""
|
|
246
|
+
|
|
247
|
+
def test_default_values(self):
|
|
248
|
+
config = MultivariateConsensusConfig()
|
|
249
|
+
assert config.min_threshold == 0.1
|
|
250
|
+
assert config.max_threshold == 1.0
|
|
251
|
+
assert config.threshold_step == 0.1
|
|
252
|
+
|
|
253
|
+
def test_max_must_be_greater_than_min(self):
|
|
254
|
+
with pytest.raises(ValidationError, match="max_threshold"):
|
|
255
|
+
MultivariateConsensusConfig(
|
|
256
|
+
min_threshold=0.5,
|
|
257
|
+
max_threshold=0.3,
|
|
258
|
+
)
|
|
259
|
+
|
|
260
|
+
def test_equal_min_max_rejected(self):
|
|
261
|
+
with pytest.raises(ValidationError, match="max_threshold"):
|
|
262
|
+
MultivariateConsensusConfig(
|
|
263
|
+
min_threshold=0.5,
|
|
264
|
+
max_threshold=0.5,
|
|
265
|
+
)
|