FeatureRank 0.1.2__tar.gz → 0.1.5__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 (50) hide show
  1. {featurerank-0.1.2 → featurerank-0.1.5}/FeatureRank/GUI.py +245 -43
  2. {featurerank-0.1.2 → featurerank-0.1.5}/FeatureRank/__init__.py +1 -1
  3. {featurerank-0.1.2 → featurerank-0.1.5/FeatureRank.egg-info}/PKG-INFO +77 -30
  4. featurerank-0.1.5/FeatureRank.egg-info/requires.txt +11 -0
  5. featurerank-0.1.2/README.md → featurerank-0.1.5/PKG-INFO +89 -27
  6. featurerank-0.1.2/FeatureRank.egg-info/PKG-INFO → featurerank-0.1.5/README.md +73 -42
  7. {featurerank-0.1.2 → featurerank-0.1.5}/pyproject.toml +4 -3
  8. featurerank-0.1.2/FeatureRank.egg-info/requires.txt +0 -6
  9. {featurerank-0.1.2 → featurerank-0.1.5}/FeatureRank/__main__.py +0 -0
  10. {featurerank-0.1.2 → featurerank-0.1.5}/FeatureRank.egg-info/SOURCES.txt +0 -0
  11. {featurerank-0.1.2 → featurerank-0.1.5}/FeatureRank.egg-info/dependency_links.txt +0 -0
  12. {featurerank-0.1.2 → featurerank-0.1.5}/FeatureRank.egg-info/entry_points.txt +0 -0
  13. {featurerank-0.1.2 → featurerank-0.1.5}/FeatureRank.egg-info/top_level.txt +0 -0
  14. {featurerank-0.1.2 → featurerank-0.1.5}/scripts/EvaluatePaperDimensionReduction.py +0 -0
  15. {featurerank-0.1.2 → featurerank-0.1.5}/scripts/FeatureBlockDatasetTools.py +0 -0
  16. {featurerank-0.1.2 → featurerank-0.1.5}/scripts/FeatureRank.py +0 -0
  17. {featurerank-0.1.2 → featurerank-0.1.5}/scripts/Figure_Scripts/CreateAccuracyBoxplotFromList.py +0 -0
  18. {featurerank-0.1.2 → featurerank-0.1.5}/scripts/Figure_Scripts/CreateClassificationFigure.py +0 -0
  19. {featurerank-0.1.2 → featurerank-0.1.5}/scripts/Figure_Scripts/CreateClusterClassificationSummaryFigure.py +0 -0
  20. {featurerank-0.1.2 → featurerank-0.1.5}/scripts/Figure_Scripts/CreateClusterFigure1.py +0 -0
  21. {featurerank-0.1.2 → featurerank-0.1.5}/scripts/Figure_Scripts/CreateConfusionMatrixPanel.py +0 -0
  22. {featurerank-0.1.2 → featurerank-0.1.5}/scripts/Figure_Scripts/CreatePrecisionRecallPanel.py +0 -0
  23. {featurerank-0.1.2 → featurerank-0.1.5}/scripts/Figure_Scripts/CreateRegressionFigure2.py +0 -0
  24. {featurerank-0.1.2 → featurerank-0.1.5}/scripts/Figure_Scripts/CreateRocPanel.py +0 -0
  25. {featurerank-0.1.2 → featurerank-0.1.5}/scripts/GeneratePaperDimensionReduction.py +0 -0
  26. {featurerank-0.1.2 → featurerank-0.1.5}/scripts/PaperDimensionReductionCommon.py +0 -0
  27. {featurerank-0.1.2 → featurerank-0.1.5}/scripts/RunAutoencoder.py +0 -0
  28. {featurerank-0.1.2 → featurerank-0.1.5}/scripts/RunBlockFeatureSelection.py +0 -0
  29. {featurerank-0.1.2 → featurerank-0.1.5}/scripts/RunDimensionReduction.py +0 -0
  30. {featurerank-0.1.2 → featurerank-0.1.5}/scripts/RunFeatureRankCV.py +0 -0
  31. {featurerank-0.1.2 → featurerank-0.1.5}/scripts/Simple.py +0 -0
  32. {featurerank-0.1.2 → featurerank-0.1.5}/scripts/__init__.py +0 -0
  33. {featurerank-0.1.2 → featurerank-0.1.5}/setup.cfg +0 -0
  34. {featurerank-0.1.2 → featurerank-0.1.5}/src/AutoencoderFeatureSelection.py +0 -0
  35. {featurerank-0.1.2 → featurerank-0.1.5}/src/Classification.py +0 -0
  36. {featurerank-0.1.2 → featurerank-0.1.5}/src/Clustering.py +0 -0
  37. {featurerank-0.1.2 → featurerank-0.1.5}/src/Config.py +0 -0
  38. {featurerank-0.1.2 → featurerank-0.1.5}/src/DataLoader.py +0 -0
  39. {featurerank-0.1.2 → featurerank-0.1.5}/src/DivideCombine.py +0 -0
  40. {featurerank-0.1.2 → featurerank-0.1.5}/src/Experiment.py +0 -0
  41. {featurerank-0.1.2 → featurerank-0.1.5}/src/Models.py +0 -0
  42. {featurerank-0.1.2 → featurerank-0.1.5}/src/OutputPaths.py +0 -0
  43. {featurerank-0.1.2 → featurerank-0.1.5}/src/Preprocessing.py +0 -0
  44. {featurerank-0.1.2 → featurerank-0.1.5}/src/Regression.py +0 -0
  45. {featurerank-0.1.2 → featurerank-0.1.5}/src/Reporting.py +0 -0
  46. {featurerank-0.1.2 → featurerank-0.1.5}/src/Runtime.py +0 -0
  47. {featurerank-0.1.2 → featurerank-0.1.5}/src/Utils.py +0 -0
  48. {featurerank-0.1.2 → featurerank-0.1.5}/src/Workflow.py +0 -0
  49. {featurerank-0.1.2 → featurerank-0.1.5}/src/__init__.py +0 -0
  50. {featurerank-0.1.2 → featurerank-0.1.5}/tests/test_feature_rank_refactor.py +0 -0
@@ -9,6 +9,7 @@ from __future__ import annotations
9
9
  import os
10
10
  from pathlib import Path
11
11
  import queue
12
+ import shutil
12
13
  import subprocess
13
14
  import sys
14
15
  import threading
@@ -19,6 +20,7 @@ from tkinter import filedialog, messagebox, ttk
19
20
 
20
21
  PERCENT_VALUES = tuple(str(value) for value in range(10, 101, 10))
21
22
  BLOCK_VALUES = ("5", "10", "20")
23
+ NO_DATASET_LABEL = "No Dataset"
22
24
  MODE_LABELS = {
23
25
  "GL": "GL — Global Feature Ranking",
24
26
  "DC": "DC — Divide & Combine Feature Ranking",
@@ -40,9 +42,99 @@ def _dataset_choices() -> list[str]:
40
42
  if not directory.exists():
41
43
  continue
42
44
  for data_file in sorted(directory.glob("*_data.csv")):
43
- if data_file.is_file() and data_file.name not in choices:
44
- choices.append(data_file.name)
45
- return choices or ["breast_cancer_data.csv"]
45
+ absolute_path = str(data_file.resolve())
46
+ if data_file.is_file() and absolute_path not in choices:
47
+ choices.append(absolute_path)
48
+ return choices or [NO_DATASET_LABEL]
49
+
50
+
51
+ def _resolve_dataset_path(dataset: str) -> str:
52
+ """Use an absolute path when the selected file exists locally."""
53
+ requested = Path(dataset).expanduser()
54
+ if requested.is_file():
55
+ return str(requested.resolve())
56
+
57
+ project_root = Path(__file__).resolve().parent.parent
58
+ candidates = (
59
+ Path.cwd() / requested,
60
+ Path.cwd() / "data" / "raw" / requested,
61
+ project_root / requested,
62
+ project_root / "data" / "raw" / requested,
63
+ )
64
+ for candidate in candidates:
65
+ if candidate.is_file():
66
+ return str(candidate.resolve())
67
+ return dataset
68
+
69
+
70
+ def _supported_python(path: Path) -> bool:
71
+ """Return whether an interpreter can run the TensorFlow worker."""
72
+ try:
73
+ result = subprocess.run(
74
+ [str(path), "-c", "import sys; print(f'{sys.version_info[0]}.{sys.version_info[1]}')"],
75
+ stdin=subprocess.DEVNULL,
76
+ stdout=subprocess.PIPE,
77
+ stderr=subprocess.DEVNULL,
78
+ text=True,
79
+ timeout=5,
80
+ check=False,
81
+ )
82
+ except (OSError, subprocess.TimeoutExpired):
83
+ return False
84
+ try:
85
+ major, minor = (int(part) for part in result.stdout.strip().split(".", maxsplit=1))
86
+ except (ValueError, TypeError):
87
+ return False
88
+ return (3, 10) <= (major, minor) < (3, 14)
89
+
90
+
91
+ def _discover_compatible_python() -> list[Path]:
92
+ """Find an installed Python 3.13–3.10 for a Python 3.14 GUI process."""
93
+ candidates: list[Path] = []
94
+ versions = ("3.13", "3.12", "3.11", "3.10")
95
+ if os.name == "nt":
96
+ launcher = shutil.which("py")
97
+ if launcher:
98
+ for version in versions:
99
+ try:
100
+ result = subprocess.run(
101
+ [launcher, f"-{version}", "-c", "import sys; print(sys.executable)"],
102
+ stdin=subprocess.DEVNULL,
103
+ stdout=subprocess.PIPE,
104
+ stderr=subprocess.DEVNULL,
105
+ text=True,
106
+ timeout=5,
107
+ check=False,
108
+ )
109
+ except (OSError, subprocess.TimeoutExpired):
110
+ continue
111
+ executable = result.stdout.strip()
112
+ if executable:
113
+ candidates.append(Path(executable))
114
+ else:
115
+ for version in versions:
116
+ executable = shutil.which(f"python{version}")
117
+ if executable:
118
+ candidates.append(Path(executable))
119
+ return candidates
120
+
121
+
122
+ def _worker_python() -> Path | None:
123
+ """Find a compatible interpreter before falling back to the current one."""
124
+ project_root = Path(__file__).resolve().parent.parent
125
+ executable_name = "python.exe" if os.name == "nt" else "python"
126
+ candidates = [
127
+ Path.cwd() / ".venv" / ("Scripts" if os.name == "nt" else "bin") / executable_name,
128
+ project_root / ".venv" / ("Scripts" if os.name == "nt" else "bin") / executable_name,
129
+ Path(sys.executable).with_name("python"),
130
+ Path(sys.executable),
131
+ ]
132
+ if sys.version_info >= (3, 14):
133
+ candidates.extend(_discover_compatible_python())
134
+ for candidate in candidates:
135
+ if candidate.is_file() and _supported_python(candidate):
136
+ return candidate
137
+ return None
46
138
 
47
139
 
48
140
  def _open_folder(path: Path) -> None:
@@ -79,16 +171,27 @@ class FeatureRankGUI:
79
171
  def __init__(self, root: tk.Tk) -> None:
80
172
  self.root = root
81
173
  self.root.title("FeatureRank")
82
- self.root.geometry("780x720")
83
- self.root.minsize(700, 620)
174
+ # Keep the first view compact; the log can still grow when resized.
175
+ self.root.geometry("760x680")
176
+ self.root.minsize(680, 600)
84
177
 
85
178
  self.messages: queue.Queue[tuple[str, object]] = queue.Queue()
86
179
  self.worker: threading.Thread | None = None
87
180
  self.last_output_dir: Path | None = None
88
181
  self.block_count = 10
89
182
  self.blocks_seen = 0
183
+ self.progress_value = 0.0
184
+ self.progress_started_at: float | None = None
185
+ self.progress_job: str | None = None
186
+ self.progress_running = False
187
+
188
+ dataset_paths = _dataset_choices()
189
+ self.dataset_paths = {
190
+ Path(path).name: path for path in dataset_paths if path != NO_DATASET_LABEL
191
+ }
192
+ self.dataset_labels = [NO_DATASET_LABEL, *self.dataset_paths]
90
193
 
91
- self.dataset_var = tk.StringVar(value=_dataset_choices()[0])
194
+ self.dataset_var = tk.StringVar(value=NO_DATASET_LABEL)
92
195
  self.percent_var = tk.StringVar(value="20")
93
196
  self.task_var = tk.StringVar(value="Classification")
94
197
  self.mode_var = tk.StringVar(value=MODE_LABELS["GL"])
@@ -121,16 +224,56 @@ class FeatureRankGUI:
121
224
  style.theme_use("clam")
122
225
  except tk.TclError:
123
226
  pass
124
- style.configure("Title.TLabel", font=("Helvetica", 20, "bold"), foreground="#1f2937")
125
- style.configure("Subtitle.TLabel", font=("Helvetica", 10), foreground="#4b5563")
126
- style.configure("Stage.TLabel", font=("Helvetica", 11, "bold"), foreground="#1f4e79")
127
- style.configure("Run.TButton", font=("Helvetica", 11, "bold"), padding=(18, 8))
227
+ # A restrained palette gives the form a clean surface on all platforms.
228
+ self.root.configure(background="#f4f6f8")
229
+ style.configure("TFrame", background="#f4f6f8")
230
+ style.configure("TLabel", background="#ffffff", foreground="#1f2937")
231
+ style.configure("TLabelframe", background="#ffffff")
232
+ style.configure("TLabelframe.Label", background="#ffffff", foreground="#1f2937")
233
+ style.configure(
234
+ "Title.TLabel",
235
+ background="#f4f6f8",
236
+ font=("Helvetica", 20, "bold"),
237
+ foreground="#1f2937",
238
+ )
239
+ style.configure(
240
+ "Subtitle.TLabel",
241
+ background="#f4f6f8",
242
+ font=("Helvetica", 10),
243
+ foreground="#4b5563",
244
+ )
245
+ style.configure(
246
+ "Stage.TLabel",
247
+ background="#f4f6f8",
248
+ font=("Helvetica", 11, "bold"),
249
+ foreground="#1f4e79",
250
+ )
251
+ style.configure(
252
+ "Run.TButton",
253
+ background="#1f4e79",
254
+ foreground="#ffffff",
255
+ font=("Helvetica", 11, "bold"),
256
+ padding=(16, 6),
257
+ )
258
+ style.map(
259
+ "Run.TButton",
260
+ background=[("active", "#173a5c"), ("disabled", "#b9c3cf")],
261
+ foreground=[("disabled", "#eef2f6")],
262
+ )
263
+ style.configure(
264
+ "Horizontal.TProgressbar",
265
+ background="#1f4e79",
266
+ troughcolor="#e5e7eb",
267
+ bordercolor="#d1d5db",
268
+ lightcolor="#1f4e79",
269
+ darkcolor="#1f4e79",
270
+ )
128
271
 
129
272
  def _build_window(self) -> None:
130
273
  self.root.columnconfigure(0, weight=1)
131
274
  self.root.rowconfigure(3, weight=1)
132
275
 
133
- header = ttk.Frame(self.root, padding=(28, 24, 28, 12))
276
+ header = ttk.Frame(self.root, padding=(24, 18, 24, 8))
134
277
  header.grid(row=0, column=0, sticky="ew")
135
278
  ttk.Label(header, text="FeatureRank", style="Title.TLabel").pack(anchor="w")
136
279
  ttk.Label(
@@ -139,53 +282,55 @@ class FeatureRankGUI:
139
282
  style="Subtitle.TLabel",
140
283
  ).pack(anchor="w", pady=(4, 0))
141
284
 
142
- options = ttk.LabelFrame(self.root, text="Experiment", padding=18)
143
- options.grid(row=1, column=0, padx=28, pady=8, sticky="ew")
285
+ options = ttk.LabelFrame(self.root, text="Experiment", padding=14)
286
+ options.grid(row=1, column=0, padx=24, pady=6, sticky="ew")
144
287
  options.columnconfigure(1, weight=1)
145
288
 
146
- ttk.Label(options, text="Dataset").grid(row=0, column=0, sticky="w", pady=7)
289
+ ttk.Label(options, text="Dataset").grid(row=0, column=0, sticky="w", pady=5)
147
290
  self.dataset_combo = ttk.Combobox(
148
- options, textvariable=self.dataset_var, values=_dataset_choices(), state="readonly"
291
+ options, textvariable=self.dataset_var, values=self.dataset_labels, state="readonly"
149
292
  )
150
- self.dataset_combo.grid(row=0, column=1, sticky="ew", padx=(14, 8), pady=7)
293
+ self.dataset_combo.grid(row=0, column=1, sticky="ew", padx=(10, 6), pady=5)
151
294
  ttk.Button(options, text="Browse…", command=self._browse_dataset).grid(
152
- row=0, column=2, pady=7
295
+ row=0, column=2, pady=5
153
296
  )
154
297
 
155
- ttk.Label(options, text="Task").grid(row=1, column=0, sticky="w", pady=7)
298
+ ttk.Label(options, text="Task").grid(row=1, column=0, sticky="w", pady=5)
156
299
  ttk.Combobox(
157
300
  options,
158
301
  textvariable=self.task_var,
159
302
  values=tuple(TASK_LABELS),
160
303
  state="readonly",
161
- ).grid(row=1, column=1, sticky="ew", padx=(14, 8), pady=7)
304
+ ).grid(row=1, column=1, sticky="ew", padx=(10, 6), pady=5)
162
305
 
163
- ttk.Label(options, text="Feature Percent").grid(row=2, column=0, sticky="w", pady=7)
306
+ ttk.Label(options, text="Feature Percent").grid(row=2, column=0, sticky="w", pady=5)
164
307
  ttk.Combobox(
165
308
  options,
166
309
  textvariable=self.percent_var,
167
310
  values=PERCENT_VALUES,
168
311
  state="readonly",
169
312
  width=12,
170
- ).grid(row=2, column=1, sticky="w", padx=(14, 8), pady=7)
313
+ ).grid(row=2, column=1, sticky="w", padx=(10, 6), pady=5)
171
314
 
172
- ttk.Label(options, text="Mode").grid(row=3, column=0, sticky="w", pady=7)
315
+ ttk.Label(options, text="Mode").grid(row=3, column=0, sticky="w", pady=5)
173
316
  self.mode_combo = ttk.Combobox(
174
317
  options,
175
318
  textvariable=self.mode_var,
176
319
  values=tuple(MODE_LABELS.values()),
177
320
  state="readonly",
178
321
  )
179
- self.mode_combo.grid(row=3, column=1, sticky="ew", padx=(14, 8), pady=7)
322
+ self.mode_combo.grid(row=3, column=1, sticky="ew", padx=(10, 6), pady=5)
180
323
  self.mode_combo.bind("<<ComboboxSelected>>", self._mode_changed)
181
324
 
182
- ttk.Label(options, text="Block Count").grid(row=4, column=0, sticky="w", pady=7)
325
+ ttk.Label(options, text="Block Count").grid(row=4, column=0, sticky="w", pady=5)
183
326
  self.block_combo = ttk.Combobox(
184
327
  options, textvariable=self.block_var, values=BLOCK_VALUES, state="disabled", width=12
185
328
  )
186
- self.block_combo.grid(row=4, column=1, sticky="w", padx=(14, 8), pady=7)
329
+ self.block_combo.grid(row=4, column=1, sticky="w", padx=(10, 6), pady=5)
187
330
 
188
- action_row = ttk.Frame(self.root, padding=(28, 8, 28, 8))
331
+ # Keep the run controls in a shallow toolbar so START is not surrounded
332
+ # by a large empty band.
333
+ action_row = ttk.Frame(self.root, padding=(24, 2, 24, 2))
189
334
  action_row.grid(row=2, column=0, sticky="ew")
190
335
  action_row.columnconfigure(1, weight=1)
191
336
  self.start_button = ttk.Button(
@@ -196,14 +341,14 @@ class FeatureRankGUI:
196
341
  row=0, column=1, sticky="e", padx=(12, 0)
197
342
  )
198
343
 
199
- progress_frame = ttk.Frame(self.root, padding=(28, 0, 28, 12))
344
+ progress_frame = ttk.Frame(self.root, padding=(24, 0, 24, 8))
200
345
  progress_frame.grid(row=3, column=0, sticky="nsew")
201
346
  progress_frame.columnconfigure(0, weight=1)
202
347
  progress_frame.rowconfigure(2, weight=1)
203
348
  self.progress = ttk.Progressbar(progress_frame, mode="determinate", maximum=100)
204
349
  self.progress.grid(row=0, column=0, sticky="ew")
205
350
  ttk.Label(progress_frame, textvariable=self.status_var, style="Subtitle.TLabel").grid(
206
- row=1, column=0, sticky="w", pady=(6, 8)
351
+ row=1, column=0, sticky="w", pady=(5, 6)
207
352
  )
208
353
 
209
354
  log_frame = ttk.LabelFrame(progress_frame, text="Log / Output", padding=8)
@@ -212,7 +357,7 @@ class FeatureRankGUI:
212
357
  log_frame.rowconfigure(0, weight=1)
213
358
  self.log_text = tk.Text(
214
359
  log_frame,
215
- height=13,
360
+ height=8,
216
361
  wrap="word",
217
362
  state="disabled",
218
363
  background="#f8fafc",
@@ -226,19 +371,19 @@ class FeatureRankGUI:
226
371
  scrollbar.grid(row=0, column=1, sticky="ns")
227
372
  self.log_text.configure(yscrollcommand=scrollbar.set)
228
373
 
229
- summary = ttk.LabelFrame(self.root, text="Summary", padding=14)
230
- summary.grid(row=4, column=0, padx=28, pady=(0, 10), sticky="ew")
374
+ summary = ttk.LabelFrame(self.root, text="Summary", padding=12)
375
+ summary.grid(row=4, column=0, padx=24, pady=(0, 8), sticky="ew")
231
376
  summary.columnconfigure(1, weight=1)
232
377
  for row, (key, value) in enumerate(self.summary_vars.items()):
233
378
  ttk.Label(summary, text=key).grid(row=row // 2, column=(row % 2) * 2, sticky="w")
234
379
  ttk.Label(summary, textvariable=value, foreground="#1f4e79").grid(
235
- row=row // 2, column=(row % 2) * 2 + 1, sticky="w", padx=(8, 26), pady=2
380
+ row=row // 2, column=(row % 2) * 2 + 1, sticky="w", padx=(8, 22), pady=2
236
381
  )
237
382
 
238
383
  self.open_button = ttk.Button(
239
384
  self.root, text="Open Results Folder", command=self._open_results, state="disabled"
240
385
  )
241
- self.open_button.grid(row=5, column=0, padx=28, pady=(0, 18), sticky="w")
386
+ self.open_button.grid(row=5, column=0, padx=24, pady=(0, 14), sticky="w")
242
387
 
243
388
  def _browse_dataset(self) -> None:
244
389
  selected = filedialog.askopenfilename(
@@ -246,14 +391,20 @@ class FeatureRankGUI:
246
391
  filetypes=[("CSV files", "*.csv"), ("Text files", "*.txt"), ("All files", "*.*")],
247
392
  )
248
393
  if selected:
249
- self.dataset_var.set(selected)
394
+ # Keep the full path internally, but show only the file name in
395
+ # the form so the selected dataset remains readable.
396
+ self.dataset_paths[Path(selected).name] = selected
397
+ self.dataset_labels = [NO_DATASET_LABEL, *self.dataset_paths]
398
+ self.dataset_combo.configure(values=self.dataset_labels)
399
+ self.dataset_var.set(Path(selected).name)
250
400
 
251
401
  def _mode_changed(self, _event: object = None) -> None:
252
402
  is_dc = self.mode_var.get().startswith("DC")
253
403
  self.block_combo.configure(state="readonly" if is_dc else "disabled")
254
404
 
255
405
  def _start(self) -> None:
256
- dataset = self.dataset_var.get().strip()
406
+ dataset_label = self.dataset_var.get().strip()
407
+ dataset = self.dataset_paths.get(dataset_label, "")
257
408
  if not dataset:
258
409
  messagebox.showerror("FeatureRank", "Please select a dataset.")
259
410
  return
@@ -273,10 +424,13 @@ class FeatureRankGUI:
273
424
  self.last_output_dir = None
274
425
  self.open_button.configure(state="disabled")
275
426
  self.start_button.configure(state="disabled")
276
- self.progress.configure(value=2)
427
+ self.progress_value = 2.0
428
+ self.progress.configure(value=self.progress_value)
277
429
  self.stage_var.set("Loading Dataset")
278
430
  self.status_var.set("The experiment is running. You can follow progress in the log.")
279
431
  self._clear_log()
432
+ self.progress_started_at = time.perf_counter()
433
+ self.progress_running = True
280
434
 
281
435
  self.worker = threading.Thread(
282
436
  target=self._run_experiment,
@@ -284,12 +438,14 @@ class FeatureRankGUI:
284
438
  daemon=True,
285
439
  )
286
440
  self.worker.start()
441
+ self._animate_progress()
287
442
  self.root.after(100, self._poll_messages)
288
443
 
289
444
  def _run_experiment(
290
445
  self, dataset: str, percent: float, task: str, mode: str, block_count: int
291
446
  ) -> None:
292
447
  started_at = time.perf_counter()
448
+ output_lines: list[str] = []
293
449
  try:
294
450
  # Keep TensorFlow out of the Tk process. On macOS, loading a model
295
451
  # from a GUI thread can terminate the native Python process. The
@@ -297,8 +453,9 @@ class FeatureRankGUI:
297
453
  # stdout is streamed back into this window.
298
454
  from src.Config import ExperimentConfig
299
455
 
456
+ dataset_path = _resolve_dataset_path(dataset)
300
457
  config = ExperimentConfig(
301
- dataset_name=dataset,
458
+ dataset_name=dataset_path,
302
459
  task=task,
303
460
  feature_percent=percent,
304
461
  random_state=42,
@@ -308,15 +465,19 @@ class FeatureRankGUI:
308
465
  cluster_k=None,
309
466
  save_details=False,
310
467
  )
311
- python_executable = Path(sys.executable).with_name("python")
312
- if not python_executable.exists():
313
- python_executable = Path(sys.executable)
468
+ python_executable = _worker_python()
469
+ if python_executable is None:
470
+ raise RuntimeError(
471
+ "Python 3.14 GUI acildi, ancak TensorFlow worker'i icin Python 3.13, "
472
+ "3.12, 3.11 veya 3.10 bulunamadi. FeatureRank'i calistirmak icin "
473
+ "desteklenen bir Python surumu kurun."
474
+ )
314
475
  command = [
315
476
  str(python_executable),
316
477
  "-m",
317
478
  "scripts.FeatureRank",
318
479
  "--dataset-name",
319
- dataset,
480
+ dataset_path,
320
481
  "--task",
321
482
  task,
322
483
  "--feature-percent",
@@ -328,9 +489,18 @@ class FeatureRankGUI:
328
489
  if mode == "dc":
329
490
  command.extend(["--block-count", str(block_count)])
330
491
 
492
+ worker_environment = os.environ.copy()
493
+ worker_environment["PYTHONUNBUFFERED"] = "1"
494
+ package_root = str(Path(__file__).resolve().parent.parent)
495
+ existing_python_path = worker_environment.get("PYTHONPATH")
496
+ worker_environment["PYTHONPATH"] = os.pathsep.join(
497
+ path for path in (package_root, existing_python_path) if path
498
+ )
499
+ self.messages.put(("log", f"Worker: {python_executable}"))
331
500
  process = subprocess.Popen(
332
501
  command,
333
502
  cwd=Path.cwd(),
503
+ env=worker_environment,
334
504
  stdout=subprocess.PIPE,
335
505
  stderr=subprocess.STDOUT,
336
506
  text=True,
@@ -338,6 +508,7 @@ class FeatureRankGUI:
338
508
  )
339
509
  assert process.stdout is not None
340
510
  for line in process.stdout:
511
+ output_lines.append(line.rstrip())
341
512
  self.messages.put(("log", line))
342
513
  return_code = process.wait()
343
514
  if return_code != 0:
@@ -347,7 +518,9 @@ class FeatureRankGUI:
347
518
  "Anaconda TensorFlow paketini guncelleyin veya uyumlu bir "
348
519
  "Python yorumlayicisi kullanin."
349
520
  )
350
- raise RuntimeError(f"FeatureRank worker stopped with code {return_code}.")
521
+ recent_output = "\n".join(line for line in output_lines[-12:] if line.strip())
522
+ details = f"\n\nSon worker çıktısı:\n{recent_output}" if recent_output else ""
523
+ raise RuntimeError(f"FeatureRank worker stopped with code {return_code}.{details}")
351
524
 
352
525
  average = 0.0
353
526
  values: list[float] = []
@@ -489,11 +662,31 @@ class FeatureRankGUI:
489
662
  elif "metrik" in lower or "metric" in lower or "tamamlandi" in lower:
490
663
  self._set_progress("Calculating Results", 95)
491
664
 
665
+ def _animate_progress(self) -> None:
666
+ """Advance the bar smoothly while the worker is still running.
667
+
668
+ Workflow messages move the bar to meaningful stages. This timer fills
669
+ the gaps between those messages based on elapsed time, then waits near
670
+ 90% until the worker reports completion instead of pretending to know
671
+ the exact duration of every dataset.
672
+ """
673
+ if not self.progress_running or self.progress_started_at is None:
674
+ return
675
+ elapsed = time.perf_counter() - self.progress_started_at
676
+ time_based_value = min(90.0, 2.0 + elapsed * 1.2)
677
+ if time_based_value > self.progress_value:
678
+ self.progress_value = time_based_value
679
+ self.progress.configure(value=self.progress_value)
680
+ self.status_var.set(f"The experiment is running. Elapsed time: {elapsed:.0f} s.")
681
+ self.progress_job = self.root.after(200, self._animate_progress)
682
+
492
683
  def _set_progress(self, stage: str, value: int) -> None:
493
684
  self.stage_var.set(stage)
494
- self.progress.configure(value=min(value, 99))
685
+ self.progress_value = max(self.progress_value, float(min(value, 99)))
686
+ self.progress.configure(value=self.progress_value)
495
687
 
496
688
  def _completed(self, summary: dict[str, str]) -> None:
689
+ self._stop_progress_animation()
497
690
  for key, value in summary.items():
498
691
  self.summary_vars[key].set(value)
499
692
  output = summary.get("Output Directory", "N/A")
@@ -506,13 +699,22 @@ class FeatureRankGUI:
506
699
  self.start_button.configure(state="normal")
507
700
 
508
701
  def _failed(self, error: str) -> None:
702
+ self._stop_progress_animation()
509
703
  self.progress.configure(value=0)
704
+ self.progress_value = 0.0
510
705
  self.stage_var.set("Failed")
511
706
  self.status_var.set("The experiment could not be completed.")
512
707
  self._append_log(f"ERROR: {error}")
513
708
  self.start_button.configure(state="normal")
514
709
  messagebox.showerror("FeatureRank", error)
515
710
 
711
+ def _stop_progress_animation(self) -> None:
712
+ self.progress_running = False
713
+ self.progress_started_at = None
714
+ if self.progress_job is not None:
715
+ self.root.after_cancel(self.progress_job)
716
+ self.progress_job = None
717
+
516
718
  def _clear_log(self) -> None:
517
719
  self.log_text.configure(state="normal")
518
720
  self.log_text.delete("1.0", tk.END)
@@ -11,7 +11,7 @@ import sys
11
11
 
12
12
  from .GUI import Launch
13
13
 
14
- __version__ = "0.1.2"
14
+ __version__ = "0.1.5"
15
15
 
16
16
 
17
17
  def _run_cli(arguments: list[str] | None = None) -> None: