robert 2.1.0__tar.gz → 2.1.1__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.
Files changed (168) hide show
  1. {robert-2.1.0 → robert-2.1.1}/PKG-INFO +4 -2
  2. robert-2.1.1/robert/__init__.py +9 -0
  3. robert-2.1.1/robert/api.py +645 -0
  4. {robert-2.1.0 → robert-2.1.1}/robert/argument_parser.py +9 -3
  5. robert-2.1.1/robert/gui_easyrob/easyrob.py +32 -0
  6. robert-2.1.1/robert/gui_easyrob/easyrob_launcher.py +68 -0
  7. robert-2.1.1/robert/gui_easyrob/main/__init__.py +9 -0
  8. robert-2.1.1/robert/gui_easyrob/main/window.py +3534 -0
  9. robert-2.1.1/robert/gui_easyrob/tabs/__init__.py +21 -0
  10. robert-2.1.1/robert/gui_easyrob/tabs/advanced_options.py +334 -0
  11. robert-2.1.1/robert/gui_easyrob/tabs/aqme.py +1105 -0
  12. robert-2.1.1/robert/gui_easyrob/tabs/images.py +271 -0
  13. robert-2.1.1/robert/gui_easyrob/tabs/molssi.py +338 -0
  14. robert-2.1.1/robert/gui_easyrob/tabs/predictions.py +411 -0
  15. robert-2.1.1/robert/gui_easyrob/tabs/results.py +477 -0
  16. robert-2.1.1/robert/gui_easyrob/tutorials/chemdraw.md +115 -0
  17. robert-2.1.1/robert/gui_easyrob/tutorials/csv.md +212 -0
  18. robert-2.1.1/robert/gui_easyrob/tutorials/descriptors.md +66 -0
  19. robert-2.1.1/robert/gui_easyrob/tutorials/overview.md +527 -0
  20. robert-2.1.1/robert/gui_easyrob/tutorials/predictions.md +68 -0
  21. robert-2.1.1/robert/gui_easyrob/tutorials/tutorial_images/chemdraw/chemdraw_1.png +0 -0
  22. robert-2.1.1/robert/gui_easyrob/tutorials/tutorial_images/chemdraw/chemdraw_10.png +0 -0
  23. robert-2.1.1/robert/gui_easyrob/tutorials/tutorial_images/chemdraw/chemdraw_11.png +0 -0
  24. robert-2.1.1/robert/gui_easyrob/tutorials/tutorial_images/chemdraw/chemdraw_12.png +0 -0
  25. robert-2.1.1/robert/gui_easyrob/tutorials/tutorial_images/chemdraw/chemdraw_13.png +0 -0
  26. robert-2.1.1/robert/gui_easyrob/tutorials/tutorial_images/chemdraw/chemdraw_2.png +0 -0
  27. robert-2.1.1/robert/gui_easyrob/tutorials/tutorial_images/chemdraw/chemdraw_3.png +0 -0
  28. robert-2.1.1/robert/gui_easyrob/tutorials/tutorial_images/chemdraw/chemdraw_4.png +0 -0
  29. robert-2.1.1/robert/gui_easyrob/tutorials/tutorial_images/chemdraw/chemdraw_5.png +0 -0
  30. robert-2.1.1/robert/gui_easyrob/tutorials/tutorial_images/chemdraw/chemdraw_6.png +0 -0
  31. robert-2.1.1/robert/gui_easyrob/tutorials/tutorial_images/chemdraw/chemdraw_7.png +0 -0
  32. robert-2.1.1/robert/gui_easyrob/tutorials/tutorial_images/chemdraw/chemdraw_8.png +0 -0
  33. robert-2.1.1/robert/gui_easyrob/tutorials/tutorial_images/chemdraw/chemdraw_9.png +0 -0
  34. robert-2.1.1/robert/gui_easyrob/tutorials/tutorial_images/csv/csv_1.png +0 -0
  35. robert-2.1.1/robert/gui_easyrob/tutorials/tutorial_images/csv/csv_1_1.png +0 -0
  36. robert-2.1.1/robert/gui_easyrob/tutorials/tutorial_images/csv/csv_1_2.png +0 -0
  37. robert-2.1.1/robert/gui_easyrob/tutorials/tutorial_images/csv/csv_1_3.png +0 -0
  38. robert-2.1.1/robert/gui_easyrob/tutorials/tutorial_images/csv/csv_1_4.png +0 -0
  39. robert-2.1.1/robert/gui_easyrob/tutorials/tutorial_images/csv/csv_1_5.png +0 -0
  40. robert-2.1.1/robert/gui_easyrob/tutorials/tutorial_images/csv/csv_2.png +0 -0
  41. robert-2.1.1/robert/gui_easyrob/tutorials/tutorial_images/csv/csv_3.png +0 -0
  42. robert-2.1.1/robert/gui_easyrob/tutorials/tutorial_images/csv/csv_4.png +0 -0
  43. robert-2.1.1/robert/gui_easyrob/tutorials/tutorial_images/csv/csv_5.png +0 -0
  44. robert-2.1.1/robert/gui_easyrob/tutorials/tutorial_images/csv/csv_6.png +0 -0
  45. robert-2.1.1/robert/gui_easyrob/tutorials/tutorial_images/csv/csv_7.png +0 -0
  46. robert-2.1.1/robert/gui_easyrob/tutorials/tutorial_images/csv/csv_8.png +0 -0
  47. robert-2.1.1/robert/gui_easyrob/tutorials/tutorial_images/csv/csv_9.png +0 -0
  48. robert-2.1.1/robert/gui_easyrob/tutorials/tutorial_images/descriptors/descriptors_1.png +0 -0
  49. robert-2.1.1/robert/gui_easyrob/tutorials/tutorial_images/descriptors/descriptors_2.png +0 -0
  50. robert-2.1.1/robert/gui_easyrob/tutorials/tutorial_images/descriptors/descriptors_3.png +0 -0
  51. robert-2.1.1/robert/gui_easyrob/tutorials/tutorial_images/descriptors/descriptors_4.png +0 -0
  52. robert-2.1.1/robert/gui_easyrob/tutorials/tutorial_images/descriptors/descriptors_5.png +0 -0
  53. robert-2.1.1/robert/gui_easyrob/tutorials/tutorial_images/descriptors/descriptors_6.png +0 -0
  54. robert-2.1.1/robert/gui_easyrob/tutorials/tutorial_images/overview/overview_1.png +0 -0
  55. robert-2.1.1/robert/gui_easyrob/tutorials/tutorial_images/overview/overview_1_1.png +0 -0
  56. robert-2.1.1/robert/gui_easyrob/tutorials/tutorial_images/overview/overview_1_2.png +0 -0
  57. robert-2.1.1/robert/gui_easyrob/tutorials/tutorial_images/overview/overview_1_3.png +0 -0
  58. robert-2.1.1/robert/gui_easyrob/tutorials/tutorial_images/overview/overview_1_4.png +0 -0
  59. robert-2.1.1/robert/gui_easyrob/tutorials/tutorial_images/overview/overview_1_5.png +0 -0
  60. robert-2.1.1/robert/gui_easyrob/tutorials/tutorial_images/overview/overview_1_6.png +0 -0
  61. robert-2.1.1/robert/gui_easyrob/tutorials/tutorial_images/overview/overview_1_7.png +0 -0
  62. robert-2.1.1/robert/gui_easyrob/tutorials/tutorial_images/overview/overview_1_8.png +0 -0
  63. robert-2.1.1/robert/gui_easyrob/tutorials/tutorial_images/overview/overview_2.png +0 -0
  64. robert-2.1.1/robert/gui_easyrob/tutorials/tutorial_images/overview/overview_3.png +0 -0
  65. robert-2.1.1/robert/gui_easyrob/tutorials/tutorial_images/overview/overview_4.png +0 -0
  66. robert-2.1.1/robert/gui_easyrob/tutorials/tutorial_images/overview/overview_5.png +0 -0
  67. robert-2.1.1/robert/gui_easyrob/tutorials/tutorial_images/overview/overview_5_1.png +0 -0
  68. robert-2.1.1/robert/gui_easyrob/tutorials/tutorial_images/overview/overview_5_2.png +0 -0
  69. robert-2.1.1/robert/gui_easyrob/tutorials/tutorial_images/overview/overview_5_3.png +0 -0
  70. robert-2.1.1/robert/gui_easyrob/tutorials/tutorial_images/predictions/predictions_1.png +0 -0
  71. robert-2.1.1/robert/gui_easyrob/tutorials/tutorial_images/predictions/predictions_1_1.png +0 -0
  72. robert-2.1.1/robert/gui_easyrob/tutorials/tutorial_images/predictions/predictions_2.png +0 -0
  73. robert-2.1.1/robert/gui_easyrob/tutorials/tutorial_images/predictions/predictions_3.png +0 -0
  74. robert-2.1.1/robert/gui_easyrob/tutorials/tutorial_images/predictions/predictions_4.png +0 -0
  75. robert-2.1.1/robert/gui_easyrob/tutorials/tutorial_images/predictions/predictions_5.png +0 -0
  76. robert-2.1.1/robert/gui_easyrob/tutorials/tutorial_images/predictions/predictions_6.png +0 -0
  77. robert-2.1.1/robert/gui_easyrob/utils/__init__.py +0 -0
  78. robert-2.1.1/robert/gui_easyrob/utils/aqme_utils.py +163 -0
  79. robert-2.1.1/robert/gui_easyrob/utils/molssi_utils.py +424 -0
  80. robert-2.1.1/robert/gui_easyrob/utils/predictions_utils.py +1074 -0
  81. robert-2.1.1/robert/gui_easyrob/utils/utils_gui.py +392 -0
  82. robert-2.1.1/robert/gui_easyrob/version.py +63 -0
  83. robert-2.1.1/robert/icons/documentation_icon.png +0 -0
  84. robert-2.1.1/robert/icons/youtube_icon.ico +0 -0
  85. {robert-2.1.0 → robert-2.1.1}/robert/predict_utils.py +43 -11
  86. {robert-2.1.0 → robert-2.1.1}/robert/report.py +1 -0
  87. {robert-2.1.0 → robert-2.1.1}/robert/utils.py +152 -20
  88. {robert-2.1.0 → robert-2.1.1}/robert.egg-info/PKG-INFO +4 -2
  89. robert-2.1.1/robert.egg-info/SOURCES.txt +162 -0
  90. robert-2.1.1/robert.egg-info/entry_points.txt +2 -0
  91. {robert-2.1.0 → robert-2.1.1}/robert.egg-info/requires.txt +2 -0
  92. {robert-2.1.0 → robert-2.1.1}/setup.py +26 -7
  93. robert-2.1.1/tests/test_1curate.py +444 -0
  94. robert-2.1.1/tests/test_2generate.py +449 -0
  95. robert-2.1.1/tests/test_3verify.py +134 -0
  96. robert-2.1.1/tests/test_4predict.py +180 -0
  97. robert-2.1.1/tests/test_5aqme_n_full.py +478 -0
  98. robert-2.1.1/tests/test_7api.py +221 -0
  99. robert-2.1.1/tests/test_easyrob.py +2085 -0
  100. robert-2.1.0/robert.egg-info/SOURCES.txt +0 -77
  101. robert-2.1.0/tests/test_1curate.py +0 -371
  102. robert-2.1.0/tests/test_2generate.py +0 -291
  103. robert-2.1.0/tests/test_3verify.py +0 -116
  104. robert-2.1.0/tests/test_4predict.py +0 -171
  105. robert-2.1.0/tests/test_5aqme_n_full.py +0 -387
  106. {robert-2.1.0 → robert-2.1.1}/LICENSE +0 -0
  107. {robert-2.1.0 → robert-2.1.1}/README.md +0 -0
  108. {robert-2.1.0 → robert-2.1.1}/robert/__main__.py +0 -0
  109. {robert-2.1.0 → robert-2.1.1}/robert/aqme.py +0 -0
  110. {robert-2.1.0 → robert-2.1.1}/robert/curate.py +0 -0
  111. {robert-2.1.0 → robert-2.1.1}/robert/evaluate.py +0 -0
  112. {robert-2.1.0 → robert-2.1.1}/robert/generate.py +0 -0
  113. {robert-2.1.0 → robert-2.1.1}/robert/generate_utils.py +0 -0
  114. {robert-2.1.0/robert → robert-2.1.1/robert/gui_easyrob}/__init__.py +0 -0
  115. {robert-2.1.0 → robert-2.1.1}/robert/icons/Robert_icon.png +0 -0
  116. {robert-2.1.0 → robert-2.1.1}/robert/icons/Robert_logo.jpg +0 -0
  117. {robert-2.1.0 → robert-2.1.1}/robert/icons/Robert_logo_transparent.png +0 -0
  118. {robert-2.1.0 → robert-2.1.1}/robert/icons/info_icon.png +0 -0
  119. {robert-2.1.0 → robert-2.1.1}/robert/icons/pdf_icon.png +0 -0
  120. {robert-2.1.0 → robert-2.1.1}/robert/icons/play_icon.png +0 -0
  121. {robert-2.1.0 → robert-2.1.1}/robert/icons/stop_icon.png +0 -0
  122. {robert-2.1.0 → robert-2.1.1}/robert/model_params/ADAB_params.yaml +0 -0
  123. {robert-2.1.0 → robert-2.1.1}/robert/model_params/GB_params.yaml +0 -0
  124. {robert-2.1.0 → robert-2.1.1}/robert/model_params/GP_params.yaml +0 -0
  125. {robert-2.1.0 → robert-2.1.1}/robert/model_params/MVL_params.yaml +0 -0
  126. {robert-2.1.0 → robert-2.1.1}/robert/model_params/NN_params.yaml +0 -0
  127. {robert-2.1.0 → robert-2.1.1}/robert/model_params/RF_params.yaml +0 -0
  128. {robert-2.1.0 → robert-2.1.1}/robert/model_params/VR_params.yaml +0 -0
  129. {robert-2.1.0 → robert-2.1.1}/robert/predict.py +0 -0
  130. {robert-2.1.0 → robert-2.1.1}/robert/report/Robert_icon.ico +0 -0
  131. {robert-2.1.0 → robert-2.1.1}/robert/report/Robert_logo.jpg +0 -0
  132. {robert-2.1.0 → robert-2.1.1}/robert/report/Robert_logo1.jpg +0 -0
  133. {robert-2.1.0 → robert-2.1.1}/robert/report/abbrev.png +0 -0
  134. {robert-2.1.0 → robert-2.1.1}/robert/report/adv_anal.png +0 -0
  135. {robert-2.1.0 → robert-2.1.1}/robert/report/features.png +0 -0
  136. {robert-2.1.0 → robert-2.1.1}/robert/report/generate.png +0 -0
  137. {robert-2.1.0 → robert-2.1.1}/robert/report/outliers.png +0 -0
  138. {robert-2.1.0 → robert-2.1.1}/robert/report/pred.png +0 -0
  139. {robert-2.1.0 → robert-2.1.1}/robert/report/repro.png +0 -0
  140. {robert-2.1.0 → robert-2.1.1}/robert/report/score.png +0 -0
  141. {robert-2.1.0 → robert-2.1.1}/robert/report/score_0.jpg +0 -0
  142. {robert-2.1.0 → robert-2.1.1}/robert/report/score_1.jpg +0 -0
  143. {robert-2.1.0 → robert-2.1.1}/robert/report/score_10.jpg +0 -0
  144. {robert-2.1.0 → robert-2.1.1}/robert/report/score_2.jpg +0 -0
  145. {robert-2.1.0 → robert-2.1.1}/robert/report/score_3.jpg +0 -0
  146. {robert-2.1.0 → robert-2.1.1}/robert/report/score_4.jpg +0 -0
  147. {robert-2.1.0 → robert-2.1.1}/robert/report/score_5.jpg +0 -0
  148. {robert-2.1.0 → robert-2.1.1}/robert/report/score_6.jpg +0 -0
  149. {robert-2.1.0 → robert-2.1.1}/robert/report/score_7.jpg +0 -0
  150. {robert-2.1.0 → robert-2.1.1}/robert/report/score_8.jpg +0 -0
  151. {robert-2.1.0 → robert-2.1.1}/robert/report/score_9.jpg +0 -0
  152. {robert-2.1.0 → robert-2.1.1}/robert/report/score_w_1_0.jpg +0 -0
  153. {robert-2.1.0 → robert-2.1.1}/robert/report/score_w_1_1.jpg +0 -0
  154. {robert-2.1.0 → robert-2.1.1}/robert/report/score_w_2_0.jpg +0 -0
  155. {robert-2.1.0 → robert-2.1.1}/robert/report/score_w_2_1.jpg +0 -0
  156. {robert-2.1.0 → robert-2.1.1}/robert/report/score_w_2_2.jpg +0 -0
  157. {robert-2.1.0 → robert-2.1.1}/robert/report/score_w_3_0.jpg +0 -0
  158. {robert-2.1.0 → robert-2.1.1}/robert/report/score_w_3_1.jpg +0 -0
  159. {robert-2.1.0 → robert-2.1.1}/robert/report/score_w_3_2.jpg +0 -0
  160. {robert-2.1.0 → robert-2.1.1}/robert/report/score_w_3_3.jpg +0 -0
  161. {robert-2.1.0 → robert-2.1.1}/robert/report/transpa.png +0 -0
  162. {robert-2.1.0 → robert-2.1.1}/robert/report/y_distrib.png +0 -0
  163. {robert-2.1.0 → robert-2.1.1}/robert/report_utils.py +0 -0
  164. {robert-2.1.0 → robert-2.1.1}/robert/robert.py +0 -0
  165. {robert-2.1.0 → robert-2.1.1}/robert/verify.py +0 -0
  166. {robert-2.1.0 → robert-2.1.1}/robert.egg-info/dependency_links.txt +0 -0
  167. {robert-2.1.0 → robert-2.1.1}/robert.egg-info/top_level.txt +0 -0
  168. {robert-2.1.0 → robert-2.1.1}/setup.cfg +0 -0
@@ -1,9 +1,9 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: robert
3
- Version: 2.1.0
3
+ Version: 2.1.1
4
4
  Summary: Refiner and Optimizer of a Bunch of Existing Regression Tools
5
5
  Home-page: https://github.com/jvalegre/robert
6
- Download-URL: https://github.com/jvalegre/robert/archive/refs/tags/2.1.0.tar.gz
6
+ Download-URL: https://github.com/jvalegre/robert/archive/refs/tags/2.1.1.tar.gz
7
7
  Author: Juan V. Alegre Requena, David Dalmau
8
8
  Author-email: jv.alegre@csic.es
9
9
  License: MIT
@@ -33,6 +33,8 @@ Requires-Dist: shap==0.49.1
33
33
  Requires-Dist: weasyprint==63.1
34
34
  Requires-Dist: PySide6==6.9.2
35
35
  Requires-Dist: PyMuPDF
36
+ Requires-Dist: pdfplumber
37
+ Requires-Dist: openpyxl
36
38
  Requires-Dist: ansi2html
37
39
  Requires-Dist: rdkit==2025.9.1
38
40
  Requires-Dist: psutil==7.0.0
@@ -0,0 +1,9 @@
1
+ __all__ = ("RobertModel",)
2
+
3
+
4
+ def __getattr__(name: str):
5
+ if name == "RobertModel":
6
+ from robert.api import RobertModel
7
+
8
+ return RobertModel
9
+ raise AttributeError(f"module {__name__!r} has no attribute {name!r}")
@@ -0,0 +1,645 @@
1
+ """
2
+ Sklearn-style facade over the ROBERT CLI pipeline.
3
+
4
+ ``RobertModel`` runs CURATE through PREDICT (optional REPORT). Point
5
+ predictions, uncertainty columns, and matplotlib handling are documented in
6
+ ``docs/API/robert.api.rst``.
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ import glob
12
+ import os
13
+ import tempfile
14
+ import uuid
15
+ import warnings
16
+ from contextlib import contextmanager
17
+ from pathlib import Path
18
+ from typing import Any, Literal, Optional, Tuple, Union
19
+
20
+ import matplotlib
21
+ import matplotlib.pyplot as plt
22
+ import numpy as np
23
+ import pandas as pd
24
+ from sklearn.base import BaseEstimator
25
+ from sklearn.metrics import accuracy_score, r2_score
26
+
27
+ from robert.argument_parser import options_add, var_dict
28
+ from robert.curate import curate
29
+ from robert.generate import generate
30
+ from robert.predict import predict as predict_module
31
+ from robert.report import report as report_module
32
+ from robert.utils import load_params
33
+ from robert.verify import verify
34
+
35
+ _NAME_COL = "__robert_name__"
36
+ _DEFAULT_Y = "__robert_y__"
37
+
38
+
39
+ def _filter_robert_kwargs(kwargs: dict) -> dict:
40
+ """Keep only keys accepted by ``set_options`` / ``load_variables``."""
41
+ return {k: v for k, v in kwargs.items() if k in var_dict}
42
+
43
+
44
+ @contextmanager
45
+ def _chdir(path: Path):
46
+ prev = os.getcwd()
47
+ os.chdir(path)
48
+ try:
49
+ yield
50
+ finally:
51
+ os.chdir(prev)
52
+
53
+
54
+ @contextmanager
55
+ def _noninteractive_mpl():
56
+ """Use Agg during ROBERT steps; restore the previous backend afterward."""
57
+ prev = matplotlib.get_backend()
58
+ was_interactive = matplotlib.is_interactive()
59
+ matplotlib.use("Agg", force=True)
60
+ plt.ioff()
61
+ try:
62
+ yield
63
+ finally:
64
+ matplotlib.use(prev, force=True)
65
+ if was_interactive:
66
+ plt.ion()
67
+
68
+
69
+ class _ParamsAdapter:
70
+ """Minimal ``self`` for :func:`robert.utils.load_params` / ``model_adjust_params``."""
71
+
72
+ __slots__ = ("args",)
73
+
74
+ def __init__(self, seed: int, type_: str):
75
+ self.args = options_add()
76
+ self.args.seed = seed
77
+ self.args.type = type_
78
+
79
+
80
+ def _suffix_title(filter_mode: str) -> str:
81
+ if filter_mode == "pfi":
82
+ return "PFI"
83
+ if filter_mode == "no_pfi":
84
+ return "No_PFI"
85
+ raise ValueError("filter_mode must be 'pfi' or 'no_pfi'")
86
+
87
+
88
+ def _find_params_csv(best_subdir: Path) -> Path:
89
+ if not best_subdir.is_dir():
90
+ raise FileNotFoundError(str(best_subdir))
91
+ csvs = sorted(p for p in best_subdir.glob("*.csv") if not p.name.endswith("_db.csv"))
92
+ if len(csvs) != 1:
93
+ raise RuntimeError(
94
+ "Expected exactly one parameter CSV in "
95
+ f"{best_subdir}, found: {[p.name for p in csvs]}"
96
+ )
97
+ return csvs[0]
98
+
99
+
100
+ def _resolve_prediction_id_column(
101
+ result_df: pd.DataFrame, names_key: str, model_names: str
102
+ ) -> str:
103
+ """
104
+ Column in the PREDICT external CSV used to align rows to the API input order.
105
+
106
+ Prefer the API names column, then the name stored in GENERATE params, then a
107
+ single case-insensitive match. Avoid assuming ``result_df.columns[0]`` is the id
108
+ column (column order may change).
109
+ """
110
+ cols = list(result_df.columns)
111
+ if names_key in result_df.columns:
112
+ return names_key
113
+ if model_names and model_names in result_df.columns:
114
+ return model_names
115
+ candidates = [c for c in (names_key, model_names) if c]
116
+ for cand in candidates:
117
+ cf = cand.casefold()
118
+ matches = [c for c in cols if str(c).casefold() == cf]
119
+ if len(matches) == 1:
120
+ return str(matches[0])
121
+ if len(matches) > 1:
122
+ raise RuntimeError(
123
+ f"Ambiguous id column for predictions: {matches!r} all match {cand!r} "
124
+ f"(case-insensitive). Columns in CSV: {cols!r}"
125
+ )
126
+ raise RuntimeError(
127
+ f"Could not find row id column for predictions (tried {names_key!r} and "
128
+ f"{model_names!r}). Columns in CSV: {cols!r}"
129
+ )
130
+
131
+
132
+ def _resolve_predict_csv(workdir: Path, pred_stem: str, model_code: str, suffix: str) -> str:
133
+ """Path to PREDICT output CSV for the external set (exact file, else sorted glob)."""
134
+ csv_dir = workdir / "PREDICT" / "csv_test"
135
+ exact = csv_dir / f"{pred_stem}_{model_code}_{suffix}.csv"
136
+ if exact.is_file():
137
+ return str(exact)
138
+ pattern = str(csv_dir / f"{pred_stem}_{model_code}_{suffix}.csv")
139
+ matches = sorted(glob.glob(pattern))
140
+ if not matches:
141
+ alt_pat = str(csv_dir / f"*{pred_stem}*{suffix}.csv")
142
+ matches = sorted(glob.glob(alt_pat))
143
+ if not matches:
144
+ raise RuntimeError(f"No prediction CSV found matching {pattern!r}")
145
+ if len(matches) > 1:
146
+ warnings.warn(
147
+ "Multiple prediction CSVs matched; using the first after lexicographic sort: "
148
+ f"{matches[0]!r}",
149
+ UserWarning,
150
+ stacklevel=3,
151
+ )
152
+ return matches[0]
153
+
154
+
155
+ class RobertModel(BaseEstimator):
156
+ """
157
+ High-level ROBERT workflow with ``fit``, ``predict``, and ``score``.
158
+
159
+ Subclasses :class:`~sklearn.base.BaseEstimator` for ``get_params`` and
160
+ ``set_params``. Preprocessing (CURATE encoding, ROBERT-internal
161
+ ``StandardScaler`` on the design matrix) runs inside ROBERT; do not stack a
162
+ separate ``StandardScaler`` on the same raw descriptors. Refitting with the same
163
+ ``workdir`` leaves prior outputs on disk. ``fit`` and ``predict`` change the
164
+ process working directory and are not safe to run concurrently on multiple
165
+ instances in one process.
166
+
167
+ :param problem_type: ``"reg"`` or ``"clas"``.
168
+ :param filter_mode: ``"pfi"`` or ``"no_pfi"``; which ``GENERATE/Best_model``
169
+ variant ``predict`` loads.
170
+ :param workdir: Directory for ROBERT outputs, or ``None`` for a managed temp dir.
171
+ :param names: Column in ``X`` used as row identifiers (CURATE ``names``), or ``None``.
172
+ :param report: If ``True``, run REPORT after PREDICT during ``fit``.
173
+ :param seed: Random seed forwarded to ROBERT, or ``None`` for package default.
174
+ :param y_column: If ``fit(X)`` is called with ``y is None``, name of the target column
175
+ in ``X`` (DataFrame only).
176
+ :param kwargs: Additional ROBERT options (keys in ``robert.argument_parser.var_dict``),
177
+ e.g. ``model``, ``n_iter``. Regression uncertainty tuning includes
178
+ ``conformal_enable``, ``conformal_calib_frac``, and ``conformal_coverage``.
179
+ """
180
+
181
+ def __init__(
182
+ self,
183
+ problem_type: Literal["reg", "clas"] = "reg",
184
+ filter_mode: Literal["pfi", "no_pfi"] = "pfi",
185
+ workdir: Optional[Union[str, Path]] = None,
186
+ names: Optional[str] = None,
187
+ report: bool = False,
188
+ seed: Optional[int] = None,
189
+ y_column: Optional[str] = None,
190
+ **kwargs: Any,
191
+ ) -> None:
192
+ super().__init__()
193
+ kwargs = dict(kwargs)
194
+ if "type" in kwargs:
195
+ warnings.warn(
196
+ "Passing 'type' to RobertModel is deprecated; use 'problem_type' instead.",
197
+ DeprecationWarning,
198
+ stacklevel=2,
199
+ )
200
+ if problem_type != "reg":
201
+ raise ValueError("Pass only one of 'problem_type' or deprecated 'type'.")
202
+ problem_type = kwargs.pop("type") # type: ignore[assignment]
203
+ if "filter" in kwargs:
204
+ warnings.warn(
205
+ "Passing 'filter' to RobertModel is deprecated; use 'filter_mode' instead.",
206
+ DeprecationWarning,
207
+ stacklevel=2,
208
+ )
209
+ if filter_mode != "pfi":
210
+ raise ValueError("Pass only one of 'filter_mode' or deprecated 'filter'.")
211
+ filter_mode = kwargs.pop("filter") # type: ignore[assignment]
212
+
213
+ self.problem_type = problem_type
214
+ self.filter_mode = filter_mode
215
+ self._user_names_col = names
216
+ self.run_report = report
217
+ self.y_column_init = y_column
218
+ self._workdir_raw: Optional[Union[str, Path]] = workdir
219
+ self._seed_explicit = seed
220
+
221
+ raw_kw = _filter_robert_kwargs(kwargs)
222
+ if seed is not None:
223
+ raw_kw["seed"] = seed
224
+ raw_kw["type"] = problem_type
225
+ self._rob_kwargs = raw_kw
226
+
227
+ self._managed_temp = workdir is None
228
+ self.workdir_: Optional[Path] = (
229
+ Path(workdir).resolve() if workdir is not None else None
230
+ )
231
+ self._tempdir_obj: Optional[tempfile.TemporaryDirectory[str]] = None
232
+
233
+ self.is_fitted_ = False
234
+ self.y_col_: str = ""
235
+ self.X_columns_: list[str] = []
236
+ self.model_data_: dict[str, Any] = {}
237
+ self.names_col_: str = ""
238
+ self.n_features_in_: int = 0
239
+ self.feature_names_in_: Optional[np.ndarray] = None
240
+ self._fit_input_was_dataframe = False
241
+
242
+ def get_params(self, deep: bool = True) -> dict[str, Any]:
243
+ """Estimator and ROBERT option names from ``__init__`` (no nested estimators)."""
244
+ _ = deep
245
+ out: dict[str, Any] = {
246
+ "problem_type": self.problem_type,
247
+ "filter_mode": self.filter_mode,
248
+ "workdir": self._workdir_raw,
249
+ "names": self._user_names_col,
250
+ "report": self.run_report,
251
+ "seed": self._seed_explicit,
252
+ "y_column": self.y_column_init,
253
+ }
254
+ for k, v in self._rob_kwargs.items():
255
+ if k == "type":
256
+ continue
257
+ out[k] = v
258
+ return out
259
+
260
+ def set_params(self, **params: Any) -> "RobertModel":
261
+ """Update ``RobertModel`` fields and ROBERT keys accepted by ``var_dict``."""
262
+ if not params:
263
+ return self
264
+ special = {
265
+ "problem_type",
266
+ "filter_mode",
267
+ "workdir",
268
+ "names",
269
+ "report",
270
+ "seed",
271
+ "y_column",
272
+ }
273
+ for key in list(params):
274
+ if key not in special and key not in var_dict:
275
+ raise ValueError(
276
+ f"RobertModel got invalid parameter {key!r}. "
277
+ "Use problem_type, filter_mode, workdir, names, report, seed, y_column, "
278
+ "or a key from robert.argument_parser.var_dict."
279
+ )
280
+ for key in list(params):
281
+ if key not in special:
282
+ continue
283
+ val = params.pop(key)
284
+ if key == "problem_type":
285
+ self.problem_type = val
286
+ self._rob_kwargs["type"] = val
287
+ elif key == "filter_mode":
288
+ self.filter_mode = val
289
+ elif key == "workdir":
290
+ self._workdir_raw = val
291
+ self.workdir_ = Path(val).resolve() if val is not None else None
292
+ self._managed_temp = val is None
293
+ self._tempdir_obj = None
294
+ elif key == "names":
295
+ self._user_names_col = val
296
+ elif key == "report":
297
+ self.run_report = val
298
+ elif key == "seed":
299
+ self._seed_explicit = val
300
+ if val is not None:
301
+ self._rob_kwargs["seed"] = val
302
+ else:
303
+ self._rob_kwargs.pop("seed", None)
304
+ elif key == "y_column":
305
+ self.y_column_init = val
306
+ for k, v in params.items():
307
+ self._rob_kwargs[k] = v
308
+ if k == "type":
309
+ self.problem_type = v # noqa: PLW2901
310
+ return self
311
+
312
+ def __enter__(self) -> "RobertModel":
313
+ self._ensure_workdir()
314
+ return self
315
+
316
+ def __exit__(self, *exc: Any) -> Literal[False]:
317
+ self.cleanup()
318
+ return False
319
+
320
+ def cleanup(self) -> None:
321
+ """Remove managed temporary workdir, if any. Safe to call multiple times."""
322
+ if self._tempdir_obj is not None:
323
+ self._tempdir_obj.cleanup()
324
+ self._tempdir_obj = None
325
+ self.workdir_ = None
326
+ self._managed_temp = self._workdir_raw is None
327
+
328
+ def _ensure_workdir(self) -> Path:
329
+ if self.workdir_ is not None:
330
+ self.workdir_.mkdir(parents=True, exist_ok=True)
331
+ return self.workdir_
332
+ if self._tempdir_obj is None:
333
+ self._tempdir_obj = tempfile.TemporaryDirectory(prefix="robert_api_")
334
+ self.workdir_ = Path(self._tempdir_obj.name).resolve()
335
+ self._managed_temp = True
336
+ assert self.workdir_ is not None
337
+ self.workdir_.mkdir(parents=True, exist_ok=True)
338
+ return self.workdir_
339
+
340
+ def _coerce_xy(
341
+ self,
342
+ X: Union[pd.DataFrame, np.ndarray],
343
+ y: Optional[Union[pd.Series, np.ndarray, list]],
344
+ names: Optional[str],
345
+ ) -> Tuple[pd.DataFrame, pd.Series, str]:
346
+ if isinstance(X, np.ndarray):
347
+ if X.ndim != 2:
348
+ raise ValueError("X must be 2-D")
349
+ X_df = pd.DataFrame(X, columns=[f"x{i}" for i in range(X.shape[1])])
350
+ elif isinstance(X, pd.DataFrame):
351
+ X_df = X.copy()
352
+ else:
353
+ raise TypeError("X must be a pandas DataFrame or numpy.ndarray")
354
+
355
+ names_col = names or self._user_names_col
356
+ if names_col is not None and names_col not in X_df.columns:
357
+ raise ValueError(f"names column {names_col!r} not found in X")
358
+
359
+ if y is None:
360
+ ykey = self.y_column_init
361
+ if ykey is None or ykey not in X_df.columns:
362
+ raise ValueError(
363
+ "y is None but no valid y_column in X; pass y or set y_column="
364
+ )
365
+ y_series = X_df[ykey].copy()
366
+ X_df = X_df.drop(columns=[ykey])
367
+ else:
368
+ y_series = pd.Series(np.asarray(y).ravel(), index=X_df.index)
369
+ if isinstance(y, pd.Series) and y.name is not None and str(y.name).strip():
370
+ y_col_guess = str(y.name)
371
+ else:
372
+ y_col_guess = self.y_column_init or _DEFAULT_Y
373
+ y_series.name = y_col_guess
374
+
375
+ if names_col is None:
376
+ X_df = X_df.copy()
377
+ X_df.insert(0, _NAME_COL, X_df.index.astype(str))
378
+ names_col = _NAME_COL
379
+
380
+ y_col = str(y_series.name) if y_series.name is not None else _DEFAULT_Y
381
+ y_series.name = y_col
382
+ return X_df, y_series, names_col
383
+
384
+ def _build_train_frame(self, X_df: pd.DataFrame, y_series: pd.Series) -> pd.DataFrame:
385
+ out = X_df.copy()
386
+ out[y_series.name] = y_series.values
387
+ return out
388
+
389
+ def _read_model_snapshot(self, workdir: Path) -> dict[str, Any]:
390
+ sub = "PFI" if self.filter_mode == "pfi" else "No_PFI"
391
+ folder = workdir / "GENERATE" / "Best_model" / sub
392
+ params_path = _find_params_csv(folder)
393
+ seed = int(self._rob_kwargs.get("seed", var_dict["seed"]))
394
+ adapter = _ParamsAdapter(seed, self.problem_type)
395
+ return load_params(adapter, str(params_path))
396
+
397
+ def fit(
398
+ self,
399
+ X: Union[pd.DataFrame, np.ndarray],
400
+ y: Optional[Union[pd.Series, np.ndarray, list]] = None,
401
+ names: Optional[str] = None,
402
+ **fit_params: Any,
403
+ ) -> "RobertModel":
404
+ _ = fit_params
405
+ self._fit_input_was_dataframe = isinstance(X, pd.DataFrame)
406
+ workdir = self._ensure_workdir()
407
+ X_df, y_series, names_col = self._coerce_xy(X, y, names)
408
+ train_csv = workdir / "_robert_train.csv"
409
+ train_rel = train_csv.name
410
+ y_col = str(y_series.name)
411
+
412
+ train_frame = self._build_train_frame(X_df, y_series)
413
+ train_frame.to_csv(train_csv, index=False)
414
+
415
+ base = dict(self._rob_kwargs)
416
+ base["command_line"] = False
417
+ base["csv_test"] = ""
418
+
419
+ with _noninteractive_mpl(), _chdir(workdir):
420
+ curate(
421
+ csv_name=train_rel,
422
+ y=y_col,
423
+ names=names_col,
424
+ **base,
425
+ )
426
+ generate(
427
+ csv_name="",
428
+ y="",
429
+ discard=[],
430
+ **base,
431
+ )
432
+ verify(**base)
433
+ predict_module(**base)
434
+ if self.run_report:
435
+ report_module(**base)
436
+
437
+ best_sub = workdir / "GENERATE" / "Best_model" / (
438
+ "PFI" if self.filter_mode == "pfi" else "No_PFI"
439
+ )
440
+ if self.filter_mode == "pfi" and not best_sub.is_dir():
441
+ raise RuntimeError(
442
+ "PFI model folder missing after fit. "
443
+ "Try filter_mode='no_pfi' or enable pfi_filter in ROBERT options."
444
+ )
445
+ if self.filter_mode == "no_pfi" and not best_sub.is_dir():
446
+ raise RuntimeError("No_PFI model folder missing after fit.")
447
+
448
+ model_data = self._read_model_snapshot(workdir)
449
+ self.model_data_ = model_data
450
+ self.y_col_ = str(model_data["y"])
451
+ descs = list(model_data["X_descriptors"])
452
+ self.X_columns_ = descs
453
+ self.n_features_in_ = len(descs)
454
+ if self._fit_input_was_dataframe:
455
+ self.feature_names_in_ = np.asarray(descs, dtype=object)
456
+ else:
457
+ self.feature_names_in_ = None
458
+ model_names = str(model_data.get("names") or "")
459
+ if model_names != names_col:
460
+ if model_names and names_col and model_names.casefold() == names_col.casefold():
461
+ warnings.warn(
462
+ f"Names column casing normalized to saved model column {model_names!r} "
463
+ f"(was {names_col!r}).",
464
+ UserWarning,
465
+ stacklevel=2,
466
+ )
467
+ names_col = model_names
468
+ else:
469
+ raise RuntimeError(
470
+ "Names column mismatch between training data "
471
+ f"({names_col!r}) and GENERATE params ({model_names!r}). "
472
+ "Refit with a clean workdir or align the names column with the model."
473
+ )
474
+ self.names_col_ = names_col
475
+ self.is_fitted_ = True
476
+ return self
477
+
478
+ def best_model_info(self) -> dict[str, Any]:
479
+ if not self.is_fitted_:
480
+ raise RuntimeError("Call fit before best_model_info.")
481
+ md = self.model_data_
482
+ return {
483
+ "model": md["model"],
484
+ "params": md["params"],
485
+ "descriptors": md["X_descriptors"],
486
+ "filter_mode": self.filter_mode,
487
+ "y": md["y"],
488
+ }
489
+
490
+ def predict(
491
+ self,
492
+ X: Union[pd.DataFrame, np.ndarray],
493
+ return_std: bool = False,
494
+ return_uncertainty: Literal[False, "cv_sd", "conformal", "both"] = False,
495
+ ) -> Union[
496
+ np.ndarray,
497
+ Tuple[np.ndarray, np.ndarray],
498
+ Tuple[np.ndarray, np.ndarray, np.ndarray],
499
+ ]:
500
+ if not self.is_fitted_:
501
+ raise RuntimeError("Call fit before predict.")
502
+ workdir = self.workdir_
503
+ assert workdir is not None
504
+
505
+ if return_uncertainty is not False:
506
+ umode: Literal[False, "cv_sd", "conformal", "both"] = return_uncertainty
507
+ if return_std:
508
+ warnings.warn(
509
+ "return_uncertainty is set; return_std is ignored.",
510
+ UserWarning,
511
+ stacklevel=2,
512
+ )
513
+ elif return_std:
514
+ umode = "cv_sd"
515
+ else:
516
+ umode = False
517
+
518
+ descriptors = list(self.model_data_["X_descriptors"])
519
+ y_target = str(self.model_data_["y"])
520
+ names_key = self.names_col_
521
+
522
+ if isinstance(X, np.ndarray):
523
+ if X.ndim != 2:
524
+ raise ValueError("X must be 2-D")
525
+ if X.shape[1] != len(descriptors):
526
+ raise ValueError(
527
+ f"X has {X.shape[1]} columns but model expects "
528
+ f"{len(descriptors)} descriptors"
529
+ )
530
+ X_df = pd.DataFrame(X, columns=descriptors, copy=False)
531
+ else:
532
+ X_df = X.copy()
533
+ missing = [c for c in descriptors if c not in X_df.columns]
534
+ if missing:
535
+ tail = "..." if len(missing) > 10 else ""
536
+ raise ValueError(
537
+ f"Missing descriptor columns: {missing[:10]!r}{tail}"
538
+ )
539
+
540
+ pred_id = uuid.uuid4().hex[:12]
541
+ pred_name = f"_robert_predict_{pred_id}.csv"
542
+ pred_path = workdir / pred_name
543
+
544
+ if names_key and names_key in X_df.columns:
545
+ X_out = X_df[[names_key] + descriptors].copy()
546
+ else:
547
+ X_out = X_df[descriptors].copy()
548
+ X_out.insert(0, names_key or _NAME_COL, X_out.index.astype(str))
549
+
550
+ order_keys = X_out.iloc[:, 0].astype(str)
551
+ if order_keys.duplicated().any():
552
+ raise ValueError(
553
+ "Duplicate values in the names column; cannot align predictions to rows."
554
+ )
555
+ X_out.to_csv(pred_path, index=False)
556
+
557
+ base = dict(self._rob_kwargs)
558
+ base["command_line"] = False
559
+ base["csv_test"] = pred_name
560
+ base["params_dir"] = "GENERATE/Best_model"
561
+ base["names"] = self.names_col_
562
+
563
+ with _noninteractive_mpl(), _chdir(workdir):
564
+ predict_module(**base)
565
+
566
+ suffix = _suffix_title(self.filter_mode)
567
+ model_code = str(self.model_data_["model"])
568
+ pred_stem = Path(pred_name).stem
569
+ csv_path = _resolve_predict_csv(workdir, pred_stem, model_code, suffix)
570
+
571
+ result_df = pd.read_csv(csv_path, encoding="utf-8")
572
+ pred_col = f"{y_target}_pred"
573
+ sd_col = f"{y_target}_pred_sd"
574
+ hw_col = f"{y_target}_pred_conformal_hw"
575
+ if pred_col not in result_df.columns:
576
+ raise RuntimeError(f"Column {pred_col!r} missing in {csv_path}")
577
+
578
+ model_names = str(self.model_data_.get("names") or "")
579
+ name_col_result = _resolve_prediction_id_column(
580
+ result_df, names_key, model_names
581
+ )
582
+ aligned = result_df.set_index(
583
+ result_df[name_col_result].astype(str), drop=False
584
+ )
585
+ try:
586
+ ordered = aligned.reindex(order_keys.values)
587
+ except ValueError as err:
588
+ raise RuntimeError(
589
+ "Could not align predictions to input row names; duplicate names?"
590
+ ) from err
591
+ if ordered[pred_col].isna().any():
592
+ missing_n = int(ordered[pred_col].isna().sum())
593
+ raise RuntimeError(
594
+ f"Prediction CSV row count or names mismatch ({missing_n} missing rows)."
595
+ )
596
+
597
+ y_pred = ordered[pred_col].to_numpy()
598
+ try:
599
+ os.remove(pred_path)
600
+ except OSError:
601
+ pass
602
+
603
+ if umode is False:
604
+ return y_pred
605
+
606
+ if umode in ("cv_sd", "both"):
607
+ if sd_col not in result_df.columns:
608
+ raise RuntimeError(f"Column {sd_col!r} missing in {csv_path}")
609
+ y_sd = ordered[sd_col].to_numpy(dtype=float)
610
+ if umode in ("conformal", "both"):
611
+ if self.problem_type != "reg":
612
+ raise ValueError(
613
+ "return_uncertainty='conformal' or 'both' is only supported "
614
+ "for problem_type='reg'."
615
+ )
616
+ if hw_col not in result_df.columns:
617
+ raise RuntimeError(f"Column {hw_col!r} missing in {csv_path}")
618
+ y_hw = ordered[hw_col].to_numpy(dtype=float)
619
+ if np.all(np.isnan(y_hw)):
620
+ raise RuntimeError(
621
+ f"Column {hw_col!r} has no finite values; disable conformal "
622
+ "or use a larger training set."
623
+ )
624
+
625
+ if umode == "cv_sd":
626
+ return y_pred, y_sd
627
+ if umode == "conformal":
628
+ return y_pred, y_hw
629
+ return y_pred, y_sd, y_hw
630
+
631
+ def score(
632
+ self,
633
+ X: Union[pd.DataFrame, np.ndarray],
634
+ y: Union[pd.Series, np.ndarray, list],
635
+ ) -> float:
636
+ y_true = np.asarray(y).ravel()
637
+ y_hat = np.asarray(self.predict(X, return_std=False)).ravel()
638
+ if self.problem_type == "reg":
639
+ return float(r2_score(y_true, y_hat))
640
+ return float(
641
+ accuracy_score(
642
+ np.round(y_true).astype(int),
643
+ np.round(y_hat).astype(int),
644
+ )
645
+ )