PyIAML 1.0.0__py3-none-any.whl

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.
Files changed (279) hide show
  1. iaml/__init__.py +56 -0
  2. iaml/actionable.py +11 -0
  3. iaml/actionables/__init__.py +21 -0
  4. iaml/actionables/boosting/__init__.py +4 -0
  5. iaml/actionables/boosting/act_adaboost.py +59 -0
  6. iaml/actionables/cleaning/__init__.py +26 -0
  7. iaml/actionables/cleaning/act_categorical_imputer.py +124 -0
  8. iaml/actionables/cleaning/act_count_vectorizer.py +204 -0
  9. iaml/actionables/cleaning/act_drop_categorical_column.py +51 -0
  10. iaml/actionables/cleaning/act_drop_date_column.py +48 -0
  11. iaml/actionables/cleaning/act_drop_high_cardinality_categorical.py +337 -0
  12. iaml/actionables/cleaning/act_drop_numerical_column.py +75 -0
  13. iaml/actionables/cleaning/act_drop_textual_column.py +51 -0
  14. iaml/actionables/cleaning/act_encode_target_column.py +56 -0
  15. iaml/actionables/cleaning/act_frequency_encoder.py +127 -0
  16. iaml/actionables/cleaning/act_hashing_vectorizer.py +186 -0
  17. iaml/actionables/cleaning/act_knn_imputer.py +152 -0
  18. iaml/actionables/cleaning/act_mean_column.py +79 -0
  19. iaml/actionables/cleaning/act_mice.py +464 -0
  20. iaml/actionables/cleaning/act_missing_count_feature.py +109 -0
  21. iaml/actionables/cleaning/act_missing_indicator.py +124 -0
  22. iaml/actionables/cleaning/act_onehot.py +65 -0
  23. iaml/actionables/cleaning/act_ordinal_encoder.py +177 -0
  24. iaml/actionables/cleaning/act_rare_category_grouper.py +173 -0
  25. iaml/actionables/cleaning/act_simple_imputer.py +109 -0
  26. iaml/actionables/cleaning/act_split_date.py +68 -0
  27. iaml/actionables/cleaning/act_target_encoder.py +274 -0
  28. iaml/actionables/cleaning/act_text_normalizer.py +241 -0
  29. iaml/actionables/cleaning/act_tf_idf.py +80 -0
  30. iaml/actionables/cleaning/act_word2vec.py +150 -0
  31. iaml/actionables/features_precleaning/__init__.py +12 -0
  32. iaml/actionables/features_precleaning/act_coerce_numeric_strings.py +194 -0
  33. iaml/actionables/features_precleaning/act_date_converter.py +99 -0
  34. iaml/actionables/features_precleaning/act_drop_bad_quality_rows.py +77 -0
  35. iaml/actionables/features_precleaning/act_drop_duplicate_rows.py +131 -0
  36. iaml/actionables/features_precleaning/act_drop_high_missing_columns.py +94 -0
  37. iaml/actionables/features_precleaning/act_drop_id_like_columns.py +294 -0
  38. iaml/actionables/features_precleaning/act_normalize_column_names.py +157 -0
  39. iaml/actionables/features_precleaning/act_sentinel_to_na_n.py +270 -0
  40. iaml/actionables/features_precleaning/act_trim_space.py +79 -0
  41. iaml/actionables/features_preprocessing/__init__.py +18 -0
  42. iaml/actionables/features_preprocessing/act_cyclical_date_encoding.py +212 -0
  43. iaml/actionables/features_preprocessing/act_fast_ica.py +161 -0
  44. iaml/actionables/features_preprocessing/act_feature_agglomeration.py +90 -0
  45. iaml/actionables/features_preprocessing/act_k_bins_discretizer.py +207 -0
  46. iaml/actionables/features_preprocessing/act_k_means_features.py +296 -0
  47. iaml/actionables/features_preprocessing/act_kernel_pca.py +143 -0
  48. iaml/actionables/features_preprocessing/act_log_transformer.py +122 -0
  49. iaml/actionables/features_preprocessing/act_nystroem.py +100 -0
  50. iaml/actionables/features_preprocessing/act_pca.py +77 -0
  51. iaml/actionables/features_preprocessing/act_polynomial_features.py +86 -0
  52. iaml/actionables/features_preprocessing/act_power_transformer.py +106 -0
  53. iaml/actionables/features_preprocessing/act_quantile_transformer.py +114 -0
  54. iaml/actionables/features_preprocessing/act_rbf_sampler.py +88 -0
  55. iaml/actionables/features_preprocessing/act_select_percentile.py +112 -0
  56. iaml/actionables/features_preprocessing/act_sparse_random_projection.py +157 -0
  57. iaml/actionables/features_preprocessing/act_truncated_svd.py +137 -0
  58. iaml/actionables/features_selection/__init__.py +8 -0
  59. iaml/actionables/features_selection/act_permutation_importance_selector.py +421 -0
  60. iaml/actionables/features_selection/act_remove_high_correlated_column.py +70 -0
  61. iaml/actionables/features_selection/act_remove_low_variance_column.py +74 -0
  62. iaml/actionables/features_selection/act_rfe.py +214 -0
  63. iaml/actionables/features_selection/act_select_from_model.py +325 -0
  64. iaml/actionables/features_selection/act_select_k_best.py +181 -0
  65. iaml/actionables/features_selection/act_vif_selector.py +130 -0
  66. iaml/actionables/imbalance/__init__.py +10 -0
  67. iaml/actionables/imbalance/act_adasyn.py +150 -0
  68. iaml/actionables/imbalance/act_borderline_smote.py +171 -0
  69. iaml/actionables/imbalance/act_near_miss.py +158 -0
  70. iaml/actionables/imbalance/act_random_over_sampling.py +60 -0
  71. iaml/actionables/imbalance/act_random_under_sampler.py +135 -0
  72. iaml/actionables/imbalance/act_smote.py +162 -0
  73. iaml/actionables/imbalance/act_smote_tomek.py +182 -0
  74. iaml/actionables/imbalance/act_smoteenn.py +193 -0
  75. iaml/actionables/imbalance/act_tomek_links.py +138 -0
  76. iaml/actionables/normalize/__init__.py +6 -0
  77. iaml/actionables/normalize/act_max_abs_scaler.py +78 -0
  78. iaml/actionables/normalize/act_minmax_scaler.py +56 -0
  79. iaml/actionables/normalize/act_normalizer.py +95 -0
  80. iaml/actionables/normalize/act_robust_scaler.py +111 -0
  81. iaml/actionables/normalize/act_standard_scaler.py +55 -0
  82. iaml/actionables/predictors/__init__.py +6 -0
  83. iaml/actionables/predictors/_xgboost.py +16 -0
  84. iaml/actionables/predictors/classifier/__init__.py +26 -0
  85. iaml/actionables/predictors/classifier/act_bagging_classifier.py +113 -0
  86. iaml/actionables/predictors/classifier/act_bernoulli_nb.py +89 -0
  87. iaml/actionables/predictors/classifier/act_catboost_classifier.py +135 -0
  88. iaml/actionables/predictors/classifier/act_complement_nb.py +106 -0
  89. iaml/actionables/predictors/classifier/act_decision_tree_classifier.py +117 -0
  90. iaml/actionables/predictors/classifier/act_extra_trees_classifier.py +115 -0
  91. iaml/actionables/predictors/classifier/act_gaussian_nb.py +53 -0
  92. iaml/actionables/predictors/classifier/act_hist_gradient_boosting_classifier.py +144 -0
  93. iaml/actionables/predictors/classifier/act_knn.py +86 -0
  94. iaml/actionables/predictors/classifier/act_light_gbm_classifier.py +211 -0
  95. iaml/actionables/predictors/classifier/act_linear_discriminant_analysis.py +63 -0
  96. iaml/actionables/predictors/classifier/act_linear_svc.py +134 -0
  97. iaml/actionables/predictors/classifier/act_logistic_regression.py +92 -0
  98. iaml/actionables/predictors/classifier/act_mlp_classifier.py +107 -0
  99. iaml/actionables/predictors/classifier/act_multinomial_nb.py +76 -0
  100. iaml/actionables/predictors/classifier/act_passive_aggressive_classifier.py +141 -0
  101. iaml/actionables/predictors/classifier/act_quadratic_discriminant_analysis.py +72 -0
  102. iaml/actionables/predictors/classifier/act_randomforest.py +113 -0
  103. iaml/actionables/predictors/classifier/act_ridge_classifier.py +116 -0
  104. iaml/actionables/predictors/classifier/act_sgd_classifier.py +149 -0
  105. iaml/actionables/predictors/classifier/act_svm_svc.py +88 -0
  106. iaml/actionables/predictors/classifier/act_xgboost.py +111 -0
  107. iaml/actionables/predictors/regressor/__init__.py +27 -0
  108. iaml/actionables/predictors/regressor/act_ada_boost_regressor.py +75 -0
  109. iaml/actionables/predictors/regressor/act_ard_regression.py +95 -0
  110. iaml/actionables/predictors/regressor/act_catboost_regressor.py +134 -0
  111. iaml/actionables/predictors/regressor/act_decision_tree_regressor.py +111 -0
  112. iaml/actionables/predictors/regressor/act_elastic_net_regressor.py +109 -0
  113. iaml/actionables/predictors/regressor/act_extra_trees_regressor.py +113 -0
  114. iaml/actionables/predictors/regressor/act_gaussian_process_regressor.py +55 -0
  115. iaml/actionables/predictors/regressor/act_gboost_regressor.py +95 -0
  116. iaml/actionables/predictors/regressor/act_hist_gradient_boosting_regressor.py +105 -0
  117. iaml/actionables/predictors/regressor/act_huber_regressor.py +101 -0
  118. iaml/actionables/predictors/regressor/act_knn_regressor.py +86 -0
  119. iaml/actionables/predictors/regressor/act_lasso_regressor.py +103 -0
  120. iaml/actionables/predictors/regressor/act_light_gbm_regressor.py +201 -0
  121. iaml/actionables/predictors/regressor/act_linear_regression.py +43 -0
  122. iaml/actionables/predictors/regressor/act_mlp_regressor.py +104 -0
  123. iaml/actionables/predictors/regressor/act_poisson_regressor.py +111 -0
  124. iaml/actionables/predictors/regressor/act_quantile_regressor.py +87 -0
  125. iaml/actionables/predictors/regressor/act_randomforest_regressor.py +116 -0
  126. iaml/actionables/predictors/regressor/act_ransac_regressor.py +106 -0
  127. iaml/actionables/predictors/regressor/act_ridge_regressor.py +107 -0
  128. iaml/actionables/predictors/regressor/act_sgd_regressor.py +106 -0
  129. iaml/actionables/predictors/regressor/act_svm_svr.py +81 -0
  130. iaml/actionables/predictors/regressor/act_xgboost_regressor.py +97 -0
  131. iaml/actionables/predictors/survival/__init__.py +12 -0
  132. iaml/actionables/predictors/survival/act_aalen_additive_model.py +83 -0
  133. iaml/actionables/predictors/survival/act_cox.py +110 -0
  134. iaml/actionables/predictors/survival/act_coxnet_survival_analysis.py +134 -0
  135. iaml/actionables/predictors/survival/act_extra_survival_trees.py +101 -0
  136. iaml/actionables/predictors/survival/act_fast_survival_svm.py +102 -0
  137. iaml/actionables/predictors/survival/act_gradient_boosting_survival_analysis.py +93 -0
  138. iaml/actionables/predictors/survival/act_random_survival_forest.py +91 -0
  139. iaml/actionables/predictors/survival/act_survival_component_wise_gboost.py +80 -0
  140. iaml/actionables/predictors/survival/act_survival_tree.py +120 -0
  141. iaml/actionables/predictors/survival/act_survival_xgboost.py +9 -0
  142. iaml/actionables/predictors/survival/act_weibull_aft.py +230 -0
  143. iaml/cache.py +61 -0
  144. iaml/cache_keys.py +57 -0
  145. iaml/candidate.py +736 -0
  146. iaml/core_dispatcher.py +125 -0
  147. iaml/data_type.py +11 -0
  148. iaml/dataset.py +506 -0
  149. iaml/decorators/__init__.py +3 -0
  150. iaml/decorators/all.py +4 -0
  151. iaml/decorators/is_step.py +45 -0
  152. iaml/decorators/runner.py +100 -0
  153. iaml/explanation.py +112 -0
  154. iaml/iaml.py +1072 -0
  155. iaml/iaml_pipeline.py +600 -0
  156. iaml/logger.py +138 -0
  157. iaml/meta_explorer_step.py +62 -0
  158. iaml/meta_ordered_step.py +28 -0
  159. iaml/meta_partial_explorer_step.py +34 -0
  160. iaml/meta_singleton.py +24 -0
  161. iaml/metastep.py +211 -0
  162. iaml/metric.py +111 -0
  163. iaml/metric_plot.py +82 -0
  164. iaml/metrics/__init__.py +21 -0
  165. iaml/metrics/_classification.py +28 -0
  166. iaml/metrics/_survival_times.py +22 -0
  167. iaml/metrics/accuracy_metric.py +59 -0
  168. iaml/metrics/balanced_accuracy_metric.py +67 -0
  169. iaml/metrics/brier_score.py +90 -0
  170. iaml/metrics/classification_error_metric.py +66 -0
  171. iaml/metrics/concordance_index_ipcw.py +84 -0
  172. iaml/metrics/concordance_index_metric.py +67 -0
  173. iaml/metrics/cumulative_dynamic_auc.py +119 -0
  174. iaml/metrics/f1_score_metric.py +71 -0
  175. iaml/metrics/integrated_brier_score.py +98 -0
  176. iaml/metrics/integrated_brier_score_loss.py +41 -0
  177. iaml/metrics/mean_absolute_error_metric.py +46 -0
  178. iaml/metrics/mean_squared_error_metric.py +46 -0
  179. iaml/metrics/mean_squared_log_error_metric.py +49 -0
  180. iaml/metrics/median_absolute_error_metric.py +48 -0
  181. iaml/metrics/precision_metric.py +63 -0
  182. iaml/metrics/r2_score_metric.py +45 -0
  183. iaml/metrics/recall_metric.py +65 -0
  184. iaml/metrics/roc_auc_metric.py +50 -0
  185. iaml/metrics/specificity_metric.py +44 -0
  186. iaml/metrics/specificity_multiclass_metric.py +55 -0
  187. iaml/metrics/specificity_multilabel_metric.py +60 -0
  188. iaml/optimizers/__init__.py +5 -0
  189. iaml/optimizers/bayesian_optimizer.py +193 -0
  190. iaml/optimizers/genetic_optimizer.py +284 -0
  191. iaml/optimizers/optimizer.py +31 -0
  192. iaml/optimizers/random_optimizer.py +101 -0
  193. iaml/plot.py +138 -0
  194. iaml/plots/__init__.py +32 -0
  195. iaml/plots/bar_plot.py +141 -0
  196. iaml/plots/box_plot.py +166 -0
  197. iaml/plots/class_prediction_error_plot.py +37 -0
  198. iaml/plots/classification_report_plot.py +35 -0
  199. iaml/plots/confusion_matrix_plot.py +34 -0
  200. iaml/plots/correlation_heatmap_plot.py +201 -0
  201. iaml/plots/cumulative_hazard_plot.py +72 -0
  202. iaml/plots/density_plot.py +210 -0
  203. iaml/plots/histogram_plot.py +179 -0
  204. iaml/plots/kaplan_meier_comparison_plot.py +89 -0
  205. iaml/plots/line_plot.py +70 -0
  206. iaml/plots/missingness_heatmap_plot.py +203 -0
  207. iaml/plots/outlier_plot.py +217 -0
  208. iaml/plots/pair_plot.py +228 -0
  209. iaml/plots/precision_recall_curve_plot.py +86 -0
  210. iaml/plots/prediction_error_plot.py +34 -0
  211. iaml/plots/qq_plot.py +220 -0
  212. iaml/plots/residual_plot.py +38 -0
  213. iaml/plots/roc_dynamique_curve_plot.py +79 -0
  214. iaml/plots/rocauc_plot.py +96 -0
  215. iaml/plots/shap_plot.py +187 -0
  216. iaml/plots/target_distribution_plot.py +241 -0
  217. iaml/plots/violin_plot.py +206 -0
  218. iaml/predictor.py +139 -0
  219. iaml/reference.py +65 -0
  220. iaml/shared_cache.py +90 -0
  221. iaml/sklearn_preprocessor.py +74 -0
  222. iaml/splitters/__init__.py +3 -0
  223. iaml/splitters/kfold_splitter.py +32 -0
  224. iaml/splitters/random_splitter.py +26 -0
  225. iaml/stack.py +39 -0
  226. iaml/statistic.py +66 -0
  227. iaml/statistics/__init__.py +77 -0
  228. iaml/statistics/anova_statistic.py +80 -0
  229. iaml/statistics/cardinality_ratio_statistic.py +63 -0
  230. iaml/statistics/category_cooccurrence_statistic.py +79 -0
  231. iaml/statistics/chi_square_statistic.py +81 -0
  232. iaml/statistics/coef_variation_statistic.py +72 -0
  233. iaml/statistics/correlation_with_target.py +105 -0
  234. iaml/statistics/count.py +72 -0
  235. iaml/statistics/data_type_summary_statistic.py +74 -0
  236. iaml/statistics/duplicate_row_statistic.py +56 -0
  237. iaml/statistics/effect_size_statistic.py +129 -0
  238. iaml/statistics/entropy_statistic.py +69 -0
  239. iaml/statistics/event_rate_statistic.py +52 -0
  240. iaml/statistics/grouped_mean_statistic.py +60 -0
  241. iaml/statistics/iqr_statistic.py +66 -0
  242. iaml/statistics/kurtosis.py +50 -0
  243. iaml/statistics/mad_statistic.py +66 -0
  244. iaml/statistics/mean.py +61 -0
  245. iaml/statistics/median_statistic.py +61 -0
  246. iaml/statistics/minmax.py +60 -0
  247. iaml/statistics/missing_rate_statistic.py +62 -0
  248. iaml/statistics/mode.py +47 -0
  249. iaml/statistics/most_frequent_ratio.py +81 -0
  250. iaml/statistics/outlier_count_iqr_statistic.py +76 -0
  251. iaml/statistics/quantile.py +59 -0
  252. iaml/statistics/range.py +53 -0
  253. iaml/statistics/rare_category_rate.py +92 -0
  254. iaml/statistics/skewness.py +53 -0
  255. iaml/statistics/stdev.py +50 -0
  256. iaml/statistics/summary_table_statistic.py +60 -0
  257. iaml/statistics/time_by_group_statistic.py +83 -0
  258. iaml/statistics/time_summary_statistic.py +56 -0
  259. iaml/statistics/top_k_value_counts.py +68 -0
  260. iaml/statistics/unique_count_statistic.py +57 -0
  261. iaml/statistics/value_counts.py +63 -0
  262. iaml/statistics/variance.py +51 -0
  263. iaml/statistics/violin.py +63 -0
  264. iaml/step.py +600 -0
  265. iaml/step_cache.py +87 -0
  266. iaml/step_wrapper.py +79 -0
  267. iaml/timed_pool_executor.py +492 -0
  268. iaml/type_of_target.py +68 -0
  269. iaml/void_step.py +101 -0
  270. iaml/worker_manager.py +169 -0
  271. iaml/wrapper/__init__.py +4 -0
  272. iaml/wrapper/wrap_basic_gridsearch.py +68 -0
  273. iaml/wrapper/wrap_genetic_gridsearch.py +293 -0
  274. iaml/wrapper/wrap_iterative_gridsearch.py +399 -0
  275. pyiaml-1.0.0.dist-info/METADATA +802 -0
  276. pyiaml-1.0.0.dist-info/RECORD +279 -0
  277. pyiaml-1.0.0.dist-info/WHEEL +5 -0
  278. pyiaml-1.0.0.dist-info/licenses/LICENSE +674 -0
  279. pyiaml-1.0.0.dist-info/top_level.txt +1 -0
iaml/step_cache.py ADDED
@@ -0,0 +1,87 @@
1
+ """Shared in-process cache for step outputs."""
2
+ from __future__ import annotations
3
+
4
+ from collections import OrderedDict
5
+ from threading import RLock
6
+ from typing import Any
7
+ from weakref import ReferenceType, ref
8
+
9
+ from .meta_singleton import MetaSingleton
10
+
11
+
12
+ class StepCache(metaclass=MetaSingleton):
13
+ """LRU cache shared across deep-copied steps, validating input identity."""
14
+ def __init__(self, max_size: int = 1000) -> None:
15
+ self._max_size = max_size
16
+ self._data: OrderedDict[tuple, tuple[ReferenceType | None, Any]] = OrderedDict()
17
+ self._by_step: dict[str, set[tuple]] = {}
18
+ self._lock = RLock()
19
+
20
+ def get(self, key: tuple, input_candidate: Any) -> Any | None:
21
+ """Return an output only while its original input still matches by identity."""
22
+ with self._lock:
23
+ entry = self._data.get(key)
24
+ if entry is None:
25
+ return None
26
+ input_ref, value = entry
27
+ original_input = input_ref() if input_ref is not None else None
28
+ if original_input is not input_candidate or (
29
+ input_ref is not None and original_input is None
30
+ ):
31
+ self._remove(key)
32
+ return None
33
+ if value is None:
34
+ return None
35
+ self._data.move_to_end(key)
36
+ return value
37
+
38
+ def put(self, key: tuple, value: Any, step_cache_id: str, input_candidate: Any) -> None:
39
+ """Cache an output without keeping its input candidate alive."""
40
+ with self._lock:
41
+ input_ref = ref(input_candidate) if input_candidate is not None else None
42
+ if key in self._data:
43
+ self._data.move_to_end(key)
44
+ else:
45
+ self._by_step.setdefault(step_cache_id, set()).add(key)
46
+ self._data[key] = (input_ref, value)
47
+ self._evict()
48
+
49
+ def _remove(self, key: tuple) -> None:
50
+ """Remove an entry and its step index while holding the cache lock."""
51
+ self._data.pop(key, None)
52
+ step_cache_id = key[0]
53
+ keys = self._by_step.get(step_cache_id)
54
+ if keys is not None:
55
+ keys.discard(key)
56
+ if not keys:
57
+ self._by_step.pop(step_cache_id, None)
58
+
59
+ def _evict(self) -> None:
60
+ while len(self._data) > self._max_size:
61
+ self._remove(next(iter(self._data)))
62
+
63
+ def clear(self, step_cache_id: str) -> None:
64
+ with self._lock:
65
+ keys = self._by_step.pop(step_cache_id, None)
66
+ if not keys:
67
+ return
68
+ for key in keys:
69
+ self._data.pop(key, None)
70
+
71
+ def values_for_step(self, step_cache_id: str) -> list[Any]:
72
+ with self._lock:
73
+ if step_cache_id not in self._by_step:
74
+ return []
75
+ return [
76
+ value for key, (_, value) in self._data.items()
77
+ if key[0] == step_cache_id
78
+ ]
79
+
80
+ def size_for_step(self, step_cache_id: str) -> int:
81
+ with self._lock:
82
+ keys = self._by_step.get(step_cache_id)
83
+ return len(keys) if keys else 0
84
+
85
+ def total_size(self) -> int:
86
+ with self._lock:
87
+ return len(self._data)
iaml/step_wrapper.py ADDED
@@ -0,0 +1,79 @@
1
+ """StepWrapper is a direct child of Step and will wrap and execute another step.
2
+ Wrap with StepWrapper is useless, use children classes
3
+ """
4
+ from .step import Step
5
+ from .decorators.all import is_step, runner
6
+ from .candidate import Candidate
7
+ from .dataset import Dataset
8
+
9
+
10
+ @is_step('wrapper')
11
+ class StepWrapper(Step):
12
+ """StepWrapper is a direct child of Step and will wrap and execute another step.
13
+ Wrap with StepWrapper is useless, use children classes
14
+ """
15
+ _usage = "Use when you need a minimal wrapper around exactly one child step for delegation. Applicable to any dataset and single-step pipelines. Avoid when Actionable or MetaStep provides the intended behavior."
16
+
17
+ def __init__(self, step: Step, **kwargs): # pylint: disable=unused-argument
18
+ # is_step forwards subclass constructor options through every parent.
19
+ self.step: Step = step
20
+ """The step to wrap"""
21
+
22
+ @classmethod
23
+ def from_pipeline(cls, pipeline: dict, *args, **kwargs) -> Step:
24
+ """Load any kind of StepWrapper. The step must have exactly one child
25
+
26
+ :param dict pipeline: JSON pipeline.
27
+ :raise TypeError: invalid pipeline: StepWrapper must have exactly one child.
28
+ :return: Loaded step.
29
+ """
30
+ if 'children' not in pipeline or len(pipeline['children']) != 1:
31
+ raise TypeError('invalid pipeline: StepWrapper must have exactly one child')
32
+
33
+ child = Step.from_pipeline(pipeline['children'][0])
34
+ step = super().from_pipeline(pipeline, child, *args, **kwargs)
35
+
36
+ return step
37
+
38
+ def configure_parents(self, *parents) -> None:
39
+ self.step.configure_parents(*parents)
40
+ super().configure_parents(*parents)
41
+
42
+ def wrap(self, step: Step) -> None:
43
+ """Set wrapped step
44
+
45
+ :param Step step: Step to wrap.
46
+ :raise ValueError: Step must be an occurrence of step (or inherited classes).
47
+ """
48
+ if Step in step.__class__.__mro__:
49
+ self.step = step
50
+ else:
51
+ raise ValueError("Step must be an occurrence of step (or inherited classes)")
52
+
53
+ def suitable(self, dataset: Dataset) -> bool:
54
+ return self.step.suitable(dataset)
55
+
56
+ def all_configurations(self) -> list[dict]:
57
+ to_return = Step.all_configurations(self)
58
+ to_return = to_return + self.step.all_configurations()
59
+
60
+ return to_return
61
+
62
+ def json_pipeline(self) -> dict:
63
+ return {
64
+ **super().json_pipeline(),
65
+ 'children': [self.step.json_pipeline()]
66
+ }
67
+
68
+ def all_steps(self) -> list[Step]:
69
+ return [self.step, *self.step.all_steps()]
70
+
71
+ @runner
72
+ def run(self, candidate: Candidate) -> Candidate:
73
+ return self.step.run(candidate)
74
+
75
+ def count_steps(self) -> int:
76
+ return 1 + self.step.count_steps()
77
+
78
+ def priorize(self, candidate: Candidate=None) -> float:
79
+ return self.step.priorize(candidate)
@@ -0,0 +1,492 @@
1
+ """TimedPoolExecutor will run *max_workers* new process and will send them actions
2
+ to run.
3
+ Compare to ProcessPoolExecutor, this one allow us to kill process quickly after timeout
4
+ """
5
+ import random
6
+ import signal
7
+ import time
8
+ import traceback
9
+ import warnings
10
+ import threading
11
+ import queue
12
+ import pickle
13
+ import multiprocess
14
+ import multiprocess.managers
15
+ import multiprocess.process
16
+
17
+ from .logger import Logger
18
+ from .core_dispatcher import CoreDispatcher
19
+ from .shared_cache import start_cache_manager
20
+ from .cache import Cache
21
+
22
+
23
+ class TerminatedError(RuntimeError):
24
+ """Custom RuntimeError
25
+ Raised when we try to run a job in a stopped executor
26
+ """
27
+
28
+
29
+ def process_daemon(
30
+ to_run_queue: multiprocess.Queue,
31
+ queue: multiprocess.Queue,
32
+ error_queue: multiprocess.Queue,
33
+ finally_queue: multiprocess.Queue,
34
+ shared_cache) -> None:
35
+ """Will be run by TimedPoolExecutor -> Daemon process able to handle actions
36
+
37
+ :param multiprocess.Queue to_run_queue: List of action to run
38
+ :param multiprocess.Queue queue: Queue used to send result
39
+ :param multiprocess.Queue error_queue: Queue used to raise errors
40
+ :param multiprocess.Queue finally_queue: Queue used for every run (success or fail).
41
+ Used to count number of ran actions
42
+ """
43
+ result = None
44
+
45
+ Cache().configure(shared_cache)
46
+
47
+ time.sleep(random.random()) # Weird thing to un-sync the threads
48
+
49
+ with warnings.catch_warnings():
50
+ warnings.filterwarnings("ignore")
51
+ while True:
52
+ value = to_run_queue.get()
53
+
54
+ method, args, kwargs, callback_id = value
55
+ try:
56
+ result = method(*args, **kwargs)
57
+ queue.put((result, callback_id))
58
+ except Exception: # pylint: disable=broad-exception-caught
59
+ error_queue.put((traceback.format_exc(), callback_id))
60
+ finally:
61
+ finally_queue.put(1)
62
+
63
+
64
+ class TimedPoolExecutor: # pylint: disable=too-many-instance-attributes
65
+ """TimedPoolExecutor will run *max_workers* new process and will send them actions
66
+ to run.
67
+ Compare to ProcessPoolExecutor, this one allow us to kill process quickly after timeout
68
+
69
+ :param int, optional max_workers: Maximum number of parallel workers
70
+ :param callable, optional callback: Function call when the worker is done
71
+ :param bool, optional sliding_stages: Wait for all workers to end, or not
72
+ :param bool, optional debug: Are we in debug mode ?
73
+ """
74
+ def __init__(
75
+ self,
76
+ max_workers: int = None,
77
+ callback: callable = None,
78
+ sliding_stages: bool = True,
79
+ debug: bool = False) -> None:
80
+ """Initialize a TimedPoolExecutor
81
+ """
82
+ self.max_workers: int = min(max_workers, multiprocess.cpu_count())
83
+ """Maximum number of workers allowed to work in parallel"""
84
+
85
+ self.debug: bool = debug
86
+ """If true, task will be done without using any process. Easier to debug"""
87
+
88
+ self.stop_flag: bool = False
89
+ """Used to stop thread"""
90
+
91
+ self.sliding_stages: bool = sliding_stages
92
+ """If True, don't wait for all workers to end, leaving empty cpu cores"""
93
+
94
+ # Daemon THREAD (& not Process)
95
+ self.main_daemon: threading.Thread = None
96
+ """Main runnng thread with a infinite loop to catch results of sub process"""
97
+
98
+ self.daemons_collectors: list[threading.Thread] = None
99
+ """List of running daemons"""
100
+
101
+ self._mp_capable: bool = True
102
+ """Flag indicating whether multiprocessing primitives are available."""
103
+ self._mp_fallback: bool = False
104
+ """Flag indicating whether we already fell back to sequential execution."""
105
+
106
+ try:
107
+ self.manager: multiprocess.Manager = multiprocess.Manager()
108
+ except Exception as exc: # pylint: disable=broad-except
109
+ warnings.warn(f"TimedPoolExecutor fallback to sequential mode (manager start failed: {exc!r})")
110
+ self.manager = None
111
+ self._mp_capable = False
112
+
113
+ if self._mp_capable:
114
+ self.to_run_queue = self.manager.Queue()
115
+ self.error_queue = self.manager.Queue()
116
+ self.result_queue = self.manager.Queue()
117
+ self.finally_queue = self.manager.Queue()
118
+ else:
119
+ self.to_run_queue = queue.Queue()
120
+ self.error_queue = queue.Queue()
121
+ self.result_queue = queue.Queue()
122
+ self.finally_queue = queue.Queue()
123
+ # Queues used to exchange data with subprocesses
124
+
125
+ self.cache_manager: multiprocess.managers.BaseManager | None = None
126
+ """Keep a strong reference to the shared cache manager process"""
127
+
128
+ self.shared_cache = None
129
+ """Proxy object used by workers to talk to the shared cache"""
130
+
131
+ self.callbacks: list[callable] = [callback]
132
+ """Method to call after each run"""
133
+
134
+ self.results: list = []
135
+ """List of all result since last reset"""
136
+
137
+ self.submit_count: int = 0
138
+ """Count -> Help TimedPoolExecutor to know if everything is finished"""
139
+
140
+ self.finished_run: int = 0
141
+ """Count -> Help TimedPoolExecutor to know if everything is finished"""
142
+
143
+ self.process: list[multiprocess.Process] = []
144
+ """List of sub processes"""
145
+
146
+ if not self._mp_capable:
147
+ self.debug = True
148
+ self.max_workers = 1
149
+ self.cache_manager = None
150
+ self.shared_cache = None
151
+ Cache().configure(None)
152
+ else:
153
+ try:
154
+ self.cache_manager, self.shared_cache = start_cache_manager(max_cache_size=500)
155
+ Cache().configure(self.shared_cache)
156
+ except OSError as exc:
157
+ warnings.warn(f"Shared cache disabled (start_cache_manager failed: {exc!r})")
158
+ self.cache_manager = None
159
+ self.shared_cache = None
160
+ Cache().configure(None)
161
+
162
+ # Create and start sub process (will only wait until first submit)
163
+ for _ in range(max_workers):
164
+ self.process.append(
165
+ multiprocess.Process( # pylint: disable=not-callable
166
+ target=process_daemon,
167
+ args=[self.to_run_queue,
168
+ self.result_queue,
169
+ self.error_queue,
170
+ self.finally_queue,
171
+ self.shared_cache
172
+ ]
173
+ )
174
+ )
175
+ self.process[-1].start()
176
+
177
+ CoreDispatcher().affiliate(
178
+ [process.pid for process in self.process],
179
+ core_number=self.max_workers)
180
+
181
+ self.__run_daemon() # Run the daemon THREAD
182
+
183
+ if threading.current_thread() is threading.main_thread():
184
+ signal.signal(signal.SIGINT, lambda *_: self.shutdown())
185
+ signal.signal(signal.SIGTERM, lambda *_: self.shutdown())
186
+
187
+ def __del__(self):
188
+ """When delete -> TimedPoolExecutor kill all these daemons
189
+ """
190
+ self.shutdown()
191
+
192
+ def shutdown(self) -> None:
193
+ """Shutdown TimedPoolExecutor : Kill subprocess and thread
194
+ """
195
+ self.stop_flag = True # Main daemon thread will kill process
196
+ if self.main_daemon:
197
+ self.main_daemon.join()
198
+
199
+ def __collect_results(self) -> None:
200
+ """Collect results from queues and run callback
201
+ """
202
+ while True:
203
+ result, callback_id = self.result_queue.get()
204
+
205
+ if isinstance(result, str) and result == 'stop':
206
+ break
207
+
208
+ if callback_id is not None and callable(self.callbacks[callback_id]):
209
+ self.callbacks[callback_id](result)
210
+
211
+ Logger().info(str(result))
212
+ self.results.append(result)
213
+
214
+ def __collect_finally(self) -> None:
215
+ while True:
216
+ item = self.finally_queue.get()
217
+ if item == "stop":
218
+ break
219
+ self.finished_run += 1
220
+
221
+ def __print_errors(self) -> None:
222
+ """Collect and print error from error_queue"""
223
+ while True:
224
+ item = self.error_queue.get()
225
+
226
+ if item is None:
227
+ continue
228
+
229
+ error, callback_id = item
230
+
231
+ if error == 'stop':
232
+ break
233
+
234
+ Logger().error("Error in a subprocess : ", error)
235
+ if callback_id is not None and callback_id < len(self.callbacks):
236
+ callback = self.callbacks[callback_id]
237
+ if callable(callback):
238
+ callback(None)
239
+
240
+ def __keep_running(self) -> None:
241
+ """Daemon THREAD process. Infinite loop to catch results & errors"""
242
+ while True:
243
+ if self.stop_flag:
244
+ self.error_queue.put(("stop", None)) # Gentilly ask thread to stop
245
+ self.result_queue.put(("stop", None)) # Gentilly ask thread to stop
246
+ self.finally_queue.put("stop") # Gentilly ask thread to stop
247
+
248
+ for process in self.process:
249
+ process.kill()
250
+
251
+ # empty task queue
252
+ while not self.to_run_queue.empty():
253
+ self.to_run_queue.get()
254
+
255
+ if self.manager is not None:
256
+ self.manager.shutdown()
257
+ if self.cache_manager is not None:
258
+ self.cache_manager.shutdown()
259
+ self.cache_manager = None
260
+
261
+ self.shared_cache = None
262
+ Cache().configure(None)
263
+
264
+ break
265
+
266
+ Logger().print_queue()
267
+ time.sleep(0.5)
268
+
269
+ def __run_daemon(self) -> None:
270
+ """Start the daemon THREAD"""
271
+ self.stop_flag = False
272
+ if not self.main_daemon or not self.main_daemon.is_alive():
273
+ self.main_daemon = threading.Thread(target=self.__keep_running)
274
+ self.main_daemon.start()
275
+
276
+ self.__run_collectors()
277
+
278
+ def __run_collectors(self) -> None:
279
+ """Start collector daemons"""
280
+ self.daemons_collectors = [
281
+ threading.Thread(target=self.__print_errors),
282
+ threading.Thread(target=self.__collect_results),
283
+ threading.Thread(target=self.__collect_finally)]
284
+
285
+ for collector in self.daemons_collectors:
286
+ collector.start()
287
+
288
+ def _drain_queue(self, target_queue: queue.Queue | multiprocess.managers.BaseProxy) -> None:
289
+ """Clear queued tasks without blocking."""
290
+ while True:
291
+ try:
292
+ target_queue.get_nowait()
293
+ except Exception:
294
+ break
295
+
296
+ def _terminate_workers(self) -> None:
297
+ """Stop all worker processes immediately."""
298
+ for process in self.process:
299
+ try:
300
+ if process.is_alive():
301
+ process.kill()
302
+ except Exception:
303
+ try:
304
+ process.terminate()
305
+ except Exception:
306
+ pass
307
+ try:
308
+ process.join(timeout=0.2)
309
+ except Exception:
310
+ pass
311
+ self.process = []
312
+
313
+ def _restart_workers(self) -> None:
314
+ """Restart worker processes after a timeout cancellation."""
315
+ if not self._mp_capable:
316
+ return
317
+
318
+ for _ in range(self.max_workers):
319
+ self.process.append(
320
+ multiprocess.Process( # pylint: disable=not-callable
321
+ target=process_daemon,
322
+ args=[self.to_run_queue,
323
+ self.result_queue,
324
+ self.error_queue,
325
+ self.finally_queue,
326
+ self.shared_cache
327
+ ]
328
+ )
329
+ )
330
+ self.process[-1].start()
331
+
332
+ CoreDispatcher().affiliate(
333
+ [process.pid for process in self.process],
334
+ core_number=self.max_workers)
335
+
336
+ def submit(self, target: callable, *args, deadline: float | None = None, **kwargs) -> bool:
337
+ """Submit a task, waiting for space when a deadline is specified.
338
+
339
+ :param callable target: Method to run
340
+ :param Tuple, optional args: parameters passed to the callable
341
+ :param float, optional deadline: Absolute time from ``time.monotonic()``.
342
+ :param Dict, optional kwargs: parameters passed to the callable
343
+ :return: False if the deadline expires before submission, otherwise True.
344
+ """
345
+ while True:
346
+ if self.stop_flag:
347
+ raise TerminatedError("Job submission failed: Executor is currently \
348
+ shutdown and cannot accept new tasks.")
349
+
350
+ remaining = float("inf") if deadline is None else deadline - time.monotonic()
351
+ if remaining <= 0:
352
+ return False
353
+
354
+ # Keep at most one waiting task per worker in addition to those running.
355
+ # Large candidate objects otherwise make both submission and cancellation
356
+ # spend most of the training budget serializing an unbounded backlog.
357
+ if deadline is None or self.submit_count - self.finished_run < self.max_workers * 2:
358
+ break
359
+ time.sleep(min(0.05, remaining))
360
+
361
+ callback_id = len(self.callbacks) - 1
362
+
363
+ if self.debug:
364
+ result = target(*args, **kwargs)
365
+ if callback_id is not None and callable(self.callbacks[callback_id]):
366
+ self.callbacks[callback_id](result)
367
+ Logger().info(str(result))
368
+ self.results.append(result)
369
+ self.submit_count += 1
370
+ self.finished_run += 1
371
+ return True
372
+
373
+ try:
374
+ self.to_run_queue.put((target, args, kwargs, callback_id))
375
+ self.submit_count += 1
376
+ except pickle.PicklingError as exc:
377
+ if deadline is not None and time.monotonic() >= deadline:
378
+ return False
379
+ if not self._mp_fallback:
380
+ warnings.warn(
381
+ f"TimedPoolExecutor fallback to sequential mode (pickle failed: {exc!r})"
382
+ )
383
+ self._mp_fallback = True
384
+ self.debug = True
385
+ result = target(*args, **kwargs)
386
+ if callback_id is not None and callable(self.callbacks[callback_id]):
387
+ self.callbacks[callback_id](result)
388
+ Logger().info(str(result))
389
+ self.results.append(result)
390
+ self.submit_count += 1
391
+ self.finished_run += 1
392
+
393
+ return True
394
+
395
+ def __finished(self) -> bool:
396
+ """Are all the submitted tasks finished?
397
+
398
+ :return: True if all tasks are finished
399
+ """
400
+ return self.finished_run >= self.submit_count
401
+
402
+ def reset(self):
403
+ """Reset all queues, callback, results, etc.
404
+ Allow to reuse this instance of TimedPoolExecutor without restarting subProcess
405
+ """
406
+ if not self.sliding_stages:
407
+ self.callbacks = [self.callbacks[-1]]
408
+ self.submit_count = 0
409
+ self.finished_run = 0
410
+
411
+ self.results = []
412
+
413
+ def __join_collectors(self):
414
+ """Join collector thread.
415
+ Stop and start thread, used when we want to sync with thread to collect all data
416
+ """
417
+
418
+ # Stop and join collector
419
+ self.error_queue.put(("stop", None))
420
+ self.result_queue.put(("stop", None))
421
+ self.finally_queue.put("stop")
422
+
423
+ for collector in self.daemons_collectors:
424
+ collector.join()
425
+
426
+ # Restart collectors
427
+ self.__run_collectors()
428
+
429
+ def set_callback(self, callback: callable) -> None:
430
+ """Set the method call to when a task finish
431
+
432
+ :param callable callback: callback method
433
+ """
434
+ self.callbacks.append(callback)
435
+
436
+ def join(self, timeout: float | None, reset: bool = True) -> list:
437
+ """Wait until all the task are finished or timeout is reach
438
+ If timeout is reach -> Remaining tasks will be kill without sending results
439
+
440
+ :param float timeout: Maximum seconds to wait; None waits without a timeout.
441
+ :param bool, optional reset: Reset the instance after join(). Defaults to True.
442
+
443
+ :return: All finished task results
444
+ """
445
+ start_time = time.monotonic()
446
+ def remain_time():
447
+ if timeout is None:
448
+ return float("inf")
449
+ return max(0.0, timeout - (time.monotonic() - start_time))
450
+
451
+ def slide():
452
+ try:
453
+ is_empty = self.to_run_queue.empty()
454
+ except BrokenPipeError:
455
+ is_empty = True
456
+
457
+ return self.sliding_stages \
458
+ and (
459
+ is_empty # submit queue is empty
460
+ and (
461
+ self.submit_count - self.finished_run <= self.max_workers/2
462
+ # At least half of the worker is free
463
+ )
464
+ and self.results # We have got at least one result
465
+ )
466
+
467
+ while not self.__finished():
468
+ remaining = remain_time()
469
+ if remaining <= 0 or slide():
470
+ break
471
+ time.sleep(min(0.05, remaining))
472
+
473
+ timed_out = not self.__finished() and remain_time() == 0
474
+ if timed_out:
475
+ if self._mp_capable and self.process:
476
+ self._terminate_workers()
477
+ self._drain_queue(self.to_run_queue)
478
+
479
+ # Join collector thread, just to be sure we have collected all data
480
+ self.__join_collectors()
481
+
482
+ if timed_out:
483
+ self.submit_count = self.finished_run
484
+ if self._mp_capable:
485
+ self._restart_workers()
486
+
487
+ results = self.results # Save before reset!
488
+
489
+ if reset:
490
+ self.reset()
491
+
492
+ return results