FeatureRank 0.1.1__tar.gz → 0.1.4__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.1 → featurerank-0.1.4}/FeatureRank/GUI.py +223 -72
  2. {featurerank-0.1.1 → featurerank-0.1.4}/FeatureRank/__init__.py +1 -1
  3. {featurerank-0.1.1 → featurerank-0.1.4/FeatureRank.egg-info}/PKG-INFO +76 -30
  4. featurerank-0.1.4/FeatureRank.egg-info/requires.txt +11 -0
  5. featurerank-0.1.1/Readme.md → featurerank-0.1.4/PKG-INFO +87 -27
  6. featurerank-0.1.1/FeatureRank.egg-info/PKG-INFO → featurerank-0.1.4/README.md +71 -41
  7. {featurerank-0.1.1 → featurerank-0.1.4}/pyproject.toml +5 -3
  8. featurerank-0.1.1/FeatureRank.egg-info/requires.txt +0 -5
  9. {featurerank-0.1.1 → featurerank-0.1.4}/FeatureRank/__main__.py +0 -0
  10. {featurerank-0.1.1 → featurerank-0.1.4}/FeatureRank.egg-info/SOURCES.txt +0 -0
  11. {featurerank-0.1.1 → featurerank-0.1.4}/FeatureRank.egg-info/dependency_links.txt +0 -0
  12. {featurerank-0.1.1 → featurerank-0.1.4}/FeatureRank.egg-info/entry_points.txt +0 -0
  13. {featurerank-0.1.1 → featurerank-0.1.4}/FeatureRank.egg-info/top_level.txt +0 -0
  14. {featurerank-0.1.1 → featurerank-0.1.4}/scripts/EvaluatePaperDimensionReduction.py +0 -0
  15. {featurerank-0.1.1 → featurerank-0.1.4}/scripts/FeatureBlockDatasetTools.py +0 -0
  16. {featurerank-0.1.1 → featurerank-0.1.4}/scripts/FeatureRank.py +0 -0
  17. {featurerank-0.1.1 → featurerank-0.1.4}/scripts/Figure_Scripts/CreateAccuracyBoxplotFromList.py +0 -0
  18. {featurerank-0.1.1 → featurerank-0.1.4}/scripts/Figure_Scripts/CreateClassificationFigure.py +0 -0
  19. {featurerank-0.1.1 → featurerank-0.1.4}/scripts/Figure_Scripts/CreateClusterClassificationSummaryFigure.py +0 -0
  20. {featurerank-0.1.1 → featurerank-0.1.4}/scripts/Figure_Scripts/CreateClusterFigure1.py +0 -0
  21. {featurerank-0.1.1 → featurerank-0.1.4}/scripts/Figure_Scripts/CreateConfusionMatrixPanel.py +0 -0
  22. {featurerank-0.1.1 → featurerank-0.1.4}/scripts/Figure_Scripts/CreatePrecisionRecallPanel.py +0 -0
  23. {featurerank-0.1.1 → featurerank-0.1.4}/scripts/Figure_Scripts/CreateRegressionFigure2.py +0 -0
  24. {featurerank-0.1.1 → featurerank-0.1.4}/scripts/Figure_Scripts/CreateRocPanel.py +0 -0
  25. {featurerank-0.1.1 → featurerank-0.1.4}/scripts/GeneratePaperDimensionReduction.py +0 -0
  26. {featurerank-0.1.1 → featurerank-0.1.4}/scripts/PaperDimensionReductionCommon.py +0 -0
  27. {featurerank-0.1.1 → featurerank-0.1.4}/scripts/RunAutoencoder.py +0 -0
  28. {featurerank-0.1.1 → featurerank-0.1.4}/scripts/RunBlockFeatureSelection.py +0 -0
  29. {featurerank-0.1.1 → featurerank-0.1.4}/scripts/RunDimensionReduction.py +0 -0
  30. {featurerank-0.1.1 → featurerank-0.1.4}/scripts/RunFeatureRankCV.py +0 -0
  31. {featurerank-0.1.1 → featurerank-0.1.4}/scripts/Simple.py +0 -0
  32. {featurerank-0.1.1 → featurerank-0.1.4}/scripts/__init__.py +0 -0
  33. {featurerank-0.1.1 → featurerank-0.1.4}/setup.cfg +0 -0
  34. {featurerank-0.1.1 → featurerank-0.1.4}/src/AutoencoderFeatureSelection.py +0 -0
  35. {featurerank-0.1.1 → featurerank-0.1.4}/src/Classification.py +0 -0
  36. {featurerank-0.1.1 → featurerank-0.1.4}/src/Clustering.py +0 -0
  37. {featurerank-0.1.1 → featurerank-0.1.4}/src/Config.py +0 -0
  38. {featurerank-0.1.1 → featurerank-0.1.4}/src/DataLoader.py +0 -0
  39. {featurerank-0.1.1 → featurerank-0.1.4}/src/DivideCombine.py +0 -0
  40. {featurerank-0.1.1 → featurerank-0.1.4}/src/Experiment.py +0 -0
  41. {featurerank-0.1.1 → featurerank-0.1.4}/src/Models.py +0 -0
  42. {featurerank-0.1.1 → featurerank-0.1.4}/src/OutputPaths.py +0 -0
  43. {featurerank-0.1.1 → featurerank-0.1.4}/src/Preprocessing.py +0 -0
  44. {featurerank-0.1.1 → featurerank-0.1.4}/src/Regression.py +0 -0
  45. {featurerank-0.1.1 → featurerank-0.1.4}/src/Reporting.py +0 -0
  46. {featurerank-0.1.1 → featurerank-0.1.4}/src/Runtime.py +0 -0
  47. {featurerank-0.1.1 → featurerank-0.1.4}/src/Utils.py +0 -0
  48. {featurerank-0.1.1 → featurerank-0.1.4}/src/Workflow.py +0 -0
  49. {featurerank-0.1.1 → featurerank-0.1.4}/src/__init__.py +0 -0
  50. {featurerank-0.1.1 → featurerank-0.1.4}/tests/test_feature_rank_refactor.py +0 -0
@@ -6,8 +6,6 @@ still performed by ``scripts.FeatureRank`` and the existing ``src`` modules.
6
6
 
7
7
  from __future__ import annotations
8
8
 
9
- import contextlib
10
- import io
11
9
  import os
12
10
  from pathlib import Path
13
11
  import queue
@@ -21,6 +19,7 @@ from tkinter import filedialog, messagebox, ttk
21
19
 
22
20
  PERCENT_VALUES = tuple(str(value) for value in range(10, 101, 10))
23
21
  BLOCK_VALUES = ("5", "10", "20")
22
+ NO_DATASET_LABEL = "No Dataset"
24
23
  MODE_LABELS = {
25
24
  "GL": "GL — Global Feature Ranking",
26
25
  "DC": "DC — Divide & Combine Feature Ranking",
@@ -33,22 +32,6 @@ TASK_LABELS = {
33
32
  _WINDOW_OPENED = False
34
33
 
35
34
 
36
- class _QueueWriter(io.TextIOBase):
37
- """Forward workflow output to the GUI queue without changing the workflow."""
38
-
39
- def __init__(self, messages: queue.Queue[tuple[str, object]]) -> None:
40
- super().__init__()
41
- self.messages = messages
42
-
43
- def write(self, text: str) -> int:
44
- if text:
45
- self.messages.put(("log", text))
46
- return len(text)
47
-
48
- def flush(self) -> None:
49
- return None
50
-
51
-
52
35
  def _dataset_choices() -> list[str]:
53
36
  """Find the repository's paired raw datasets for the dropdown."""
54
37
  project_root = Path(__file__).resolve().parent.parent
@@ -58,9 +41,42 @@ def _dataset_choices() -> list[str]:
58
41
  if not directory.exists():
59
42
  continue
60
43
  for data_file in sorted(directory.glob("*_data.csv")):
61
- if data_file.is_file() and data_file.name not in choices:
62
- choices.append(data_file.name)
63
- return choices or ["breast_cancer_data.csv"]
44
+ absolute_path = str(data_file.resolve())
45
+ if data_file.is_file() and absolute_path not in choices:
46
+ choices.append(absolute_path)
47
+ return choices or [NO_DATASET_LABEL]
48
+
49
+
50
+ def _resolve_dataset_path(dataset: str) -> str:
51
+ """Use an absolute path when the selected file exists locally."""
52
+ requested = Path(dataset).expanduser()
53
+ if requested.is_file():
54
+ return str(requested.resolve())
55
+
56
+ project_root = Path(__file__).resolve().parent.parent
57
+ candidates = (
58
+ Path.cwd() / requested,
59
+ Path.cwd() / "data" / "raw" / requested,
60
+ project_root / requested,
61
+ project_root / "data" / "raw" / requested,
62
+ )
63
+ for candidate in candidates:
64
+ if candidate.is_file():
65
+ return str(candidate.resolve())
66
+ return dataset
67
+
68
+
69
+ def _worker_python() -> Path:
70
+ """Find a project virtual-environment interpreter before the system one."""
71
+ project_root = Path(__file__).resolve().parent.parent
72
+ executable_name = "python.exe" if os.name == "nt" else "python"
73
+ candidates = (
74
+ Path.cwd() / ".venv" / ("Scripts" if os.name == "nt" else "bin") / executable_name,
75
+ project_root / ".venv" / ("Scripts" if os.name == "nt" else "bin") / executable_name,
76
+ Path(sys.executable).with_name("python"),
77
+ Path(sys.executable),
78
+ )
79
+ return next((candidate for candidate in candidates if candidate.is_file()), candidates[-1])
64
80
 
65
81
 
66
82
  def _open_folder(path: Path) -> None:
@@ -97,16 +113,27 @@ class FeatureRankGUI:
97
113
  def __init__(self, root: tk.Tk) -> None:
98
114
  self.root = root
99
115
  self.root.title("FeatureRank")
100
- self.root.geometry("780x720")
101
- self.root.minsize(700, 620)
116
+ # Keep the first view compact; the log can still grow when resized.
117
+ self.root.geometry("760x680")
118
+ self.root.minsize(680, 600)
102
119
 
103
120
  self.messages: queue.Queue[tuple[str, object]] = queue.Queue()
104
121
  self.worker: threading.Thread | None = None
105
122
  self.last_output_dir: Path | None = None
106
123
  self.block_count = 10
107
124
  self.blocks_seen = 0
125
+ self.progress_value = 0.0
126
+ self.progress_started_at: float | None = None
127
+ self.progress_job: str | None = None
128
+ self.progress_running = False
129
+
130
+ dataset_paths = _dataset_choices()
131
+ self.dataset_paths = {
132
+ Path(path).name: path for path in dataset_paths if path != NO_DATASET_LABEL
133
+ }
134
+ self.dataset_labels = [NO_DATASET_LABEL, *self.dataset_paths]
108
135
 
109
- self.dataset_var = tk.StringVar(value=_dataset_choices()[0])
136
+ self.dataset_var = tk.StringVar(value=NO_DATASET_LABEL)
110
137
  self.percent_var = tk.StringVar(value="20")
111
138
  self.task_var = tk.StringVar(value="Classification")
112
139
  self.mode_var = tk.StringVar(value=MODE_LABELS["GL"])
@@ -139,16 +166,56 @@ class FeatureRankGUI:
139
166
  style.theme_use("clam")
140
167
  except tk.TclError:
141
168
  pass
142
- style.configure("Title.TLabel", font=("Helvetica", 20, "bold"), foreground="#1f2937")
143
- style.configure("Subtitle.TLabel", font=("Helvetica", 10), foreground="#4b5563")
144
- style.configure("Stage.TLabel", font=("Helvetica", 11, "bold"), foreground="#1f4e79")
145
- style.configure("Run.TButton", font=("Helvetica", 11, "bold"), padding=(18, 8))
169
+ # A restrained palette gives the form a clean surface on all platforms.
170
+ self.root.configure(background="#f4f6f8")
171
+ style.configure("TFrame", background="#f4f6f8")
172
+ style.configure("TLabel", background="#ffffff", foreground="#1f2937")
173
+ style.configure("TLabelframe", background="#ffffff")
174
+ style.configure("TLabelframe.Label", background="#ffffff", foreground="#1f2937")
175
+ style.configure(
176
+ "Title.TLabel",
177
+ background="#f4f6f8",
178
+ font=("Helvetica", 20, "bold"),
179
+ foreground="#1f2937",
180
+ )
181
+ style.configure(
182
+ "Subtitle.TLabel",
183
+ background="#f4f6f8",
184
+ font=("Helvetica", 10),
185
+ foreground="#4b5563",
186
+ )
187
+ style.configure(
188
+ "Stage.TLabel",
189
+ background="#f4f6f8",
190
+ font=("Helvetica", 11, "bold"),
191
+ foreground="#1f4e79",
192
+ )
193
+ style.configure(
194
+ "Run.TButton",
195
+ background="#1f4e79",
196
+ foreground="#ffffff",
197
+ font=("Helvetica", 11, "bold"),
198
+ padding=(16, 6),
199
+ )
200
+ style.map(
201
+ "Run.TButton",
202
+ background=[("active", "#173a5c"), ("disabled", "#b9c3cf")],
203
+ foreground=[("disabled", "#eef2f6")],
204
+ )
205
+ style.configure(
206
+ "Horizontal.TProgressbar",
207
+ background="#1f4e79",
208
+ troughcolor="#e5e7eb",
209
+ bordercolor="#d1d5db",
210
+ lightcolor="#1f4e79",
211
+ darkcolor="#1f4e79",
212
+ )
146
213
 
147
214
  def _build_window(self) -> None:
148
215
  self.root.columnconfigure(0, weight=1)
149
216
  self.root.rowconfigure(3, weight=1)
150
217
 
151
- header = ttk.Frame(self.root, padding=(28, 24, 28, 12))
218
+ header = ttk.Frame(self.root, padding=(24, 18, 24, 8))
152
219
  header.grid(row=0, column=0, sticky="ew")
153
220
  ttk.Label(header, text="FeatureRank", style="Title.TLabel").pack(anchor="w")
154
221
  ttk.Label(
@@ -157,53 +224,55 @@ class FeatureRankGUI:
157
224
  style="Subtitle.TLabel",
158
225
  ).pack(anchor="w", pady=(4, 0))
159
226
 
160
- options = ttk.LabelFrame(self.root, text="Experiment", padding=18)
161
- options.grid(row=1, column=0, padx=28, pady=8, sticky="ew")
227
+ options = ttk.LabelFrame(self.root, text="Experiment", padding=14)
228
+ options.grid(row=1, column=0, padx=24, pady=6, sticky="ew")
162
229
  options.columnconfigure(1, weight=1)
163
230
 
164
- ttk.Label(options, text="Dataset").grid(row=0, column=0, sticky="w", pady=7)
231
+ ttk.Label(options, text="Dataset").grid(row=0, column=0, sticky="w", pady=5)
165
232
  self.dataset_combo = ttk.Combobox(
166
- options, textvariable=self.dataset_var, values=_dataset_choices(), state="readonly"
233
+ options, textvariable=self.dataset_var, values=self.dataset_labels, state="readonly"
167
234
  )
168
- self.dataset_combo.grid(row=0, column=1, sticky="ew", padx=(14, 8), pady=7)
235
+ self.dataset_combo.grid(row=0, column=1, sticky="ew", padx=(10, 6), pady=5)
169
236
  ttk.Button(options, text="Browse…", command=self._browse_dataset).grid(
170
- row=0, column=2, pady=7
237
+ row=0, column=2, pady=5
171
238
  )
172
239
 
173
- ttk.Label(options, text="Task").grid(row=1, column=0, sticky="w", pady=7)
240
+ ttk.Label(options, text="Task").grid(row=1, column=0, sticky="w", pady=5)
174
241
  ttk.Combobox(
175
242
  options,
176
243
  textvariable=self.task_var,
177
244
  values=tuple(TASK_LABELS),
178
245
  state="readonly",
179
- ).grid(row=1, column=1, sticky="ew", padx=(14, 8), pady=7)
246
+ ).grid(row=1, column=1, sticky="ew", padx=(10, 6), pady=5)
180
247
 
181
- ttk.Label(options, text="Feature Percent").grid(row=2, column=0, sticky="w", pady=7)
248
+ ttk.Label(options, text="Feature Percent").grid(row=2, column=0, sticky="w", pady=5)
182
249
  ttk.Combobox(
183
250
  options,
184
251
  textvariable=self.percent_var,
185
252
  values=PERCENT_VALUES,
186
253
  state="readonly",
187
254
  width=12,
188
- ).grid(row=2, column=1, sticky="w", padx=(14, 8), pady=7)
255
+ ).grid(row=2, column=1, sticky="w", padx=(10, 6), pady=5)
189
256
 
190
- ttk.Label(options, text="Mode").grid(row=3, column=0, sticky="w", pady=7)
257
+ ttk.Label(options, text="Mode").grid(row=3, column=0, sticky="w", pady=5)
191
258
  self.mode_combo = ttk.Combobox(
192
259
  options,
193
260
  textvariable=self.mode_var,
194
261
  values=tuple(MODE_LABELS.values()),
195
262
  state="readonly",
196
263
  )
197
- self.mode_combo.grid(row=3, column=1, sticky="ew", padx=(14, 8), pady=7)
264
+ self.mode_combo.grid(row=3, column=1, sticky="ew", padx=(10, 6), pady=5)
198
265
  self.mode_combo.bind("<<ComboboxSelected>>", self._mode_changed)
199
266
 
200
- ttk.Label(options, text="Block Count").grid(row=4, column=0, sticky="w", pady=7)
267
+ ttk.Label(options, text="Block Count").grid(row=4, column=0, sticky="w", pady=5)
201
268
  self.block_combo = ttk.Combobox(
202
269
  options, textvariable=self.block_var, values=BLOCK_VALUES, state="disabled", width=12
203
270
  )
204
- self.block_combo.grid(row=4, column=1, sticky="w", padx=(14, 8), pady=7)
271
+ self.block_combo.grid(row=4, column=1, sticky="w", padx=(10, 6), pady=5)
205
272
 
206
- action_row = ttk.Frame(self.root, padding=(28, 8, 28, 8))
273
+ # Keep the run controls in a shallow toolbar so START is not surrounded
274
+ # by a large empty band.
275
+ action_row = ttk.Frame(self.root, padding=(24, 2, 24, 2))
207
276
  action_row.grid(row=2, column=0, sticky="ew")
208
277
  action_row.columnconfigure(1, weight=1)
209
278
  self.start_button = ttk.Button(
@@ -214,14 +283,14 @@ class FeatureRankGUI:
214
283
  row=0, column=1, sticky="e", padx=(12, 0)
215
284
  )
216
285
 
217
- progress_frame = ttk.Frame(self.root, padding=(28, 0, 28, 12))
286
+ progress_frame = ttk.Frame(self.root, padding=(24, 0, 24, 8))
218
287
  progress_frame.grid(row=3, column=0, sticky="nsew")
219
288
  progress_frame.columnconfigure(0, weight=1)
220
289
  progress_frame.rowconfigure(2, weight=1)
221
290
  self.progress = ttk.Progressbar(progress_frame, mode="determinate", maximum=100)
222
291
  self.progress.grid(row=0, column=0, sticky="ew")
223
292
  ttk.Label(progress_frame, textvariable=self.status_var, style="Subtitle.TLabel").grid(
224
- row=1, column=0, sticky="w", pady=(6, 8)
293
+ row=1, column=0, sticky="w", pady=(5, 6)
225
294
  )
226
295
 
227
296
  log_frame = ttk.LabelFrame(progress_frame, text="Log / Output", padding=8)
@@ -230,7 +299,7 @@ class FeatureRankGUI:
230
299
  log_frame.rowconfigure(0, weight=1)
231
300
  self.log_text = tk.Text(
232
301
  log_frame,
233
- height=13,
302
+ height=8,
234
303
  wrap="word",
235
304
  state="disabled",
236
305
  background="#f8fafc",
@@ -244,19 +313,19 @@ class FeatureRankGUI:
244
313
  scrollbar.grid(row=0, column=1, sticky="ns")
245
314
  self.log_text.configure(yscrollcommand=scrollbar.set)
246
315
 
247
- summary = ttk.LabelFrame(self.root, text="Summary", padding=14)
248
- summary.grid(row=4, column=0, padx=28, pady=(0, 10), sticky="ew")
316
+ summary = ttk.LabelFrame(self.root, text="Summary", padding=12)
317
+ summary.grid(row=4, column=0, padx=24, pady=(0, 8), sticky="ew")
249
318
  summary.columnconfigure(1, weight=1)
250
319
  for row, (key, value) in enumerate(self.summary_vars.items()):
251
320
  ttk.Label(summary, text=key).grid(row=row // 2, column=(row % 2) * 2, sticky="w")
252
321
  ttk.Label(summary, textvariable=value, foreground="#1f4e79").grid(
253
- row=row // 2, column=(row % 2) * 2 + 1, sticky="w", padx=(8, 26), pady=2
322
+ row=row // 2, column=(row % 2) * 2 + 1, sticky="w", padx=(8, 22), pady=2
254
323
  )
255
324
 
256
325
  self.open_button = ttk.Button(
257
326
  self.root, text="Open Results Folder", command=self._open_results, state="disabled"
258
327
  )
259
- self.open_button.grid(row=5, column=0, padx=28, pady=(0, 18), sticky="w")
328
+ self.open_button.grid(row=5, column=0, padx=24, pady=(0, 14), sticky="w")
260
329
 
261
330
  def _browse_dataset(self) -> None:
262
331
  selected = filedialog.askopenfilename(
@@ -264,14 +333,20 @@ class FeatureRankGUI:
264
333
  filetypes=[("CSV files", "*.csv"), ("Text files", "*.txt"), ("All files", "*.*")],
265
334
  )
266
335
  if selected:
267
- self.dataset_var.set(selected)
336
+ # Keep the full path internally, but show only the file name in
337
+ # the form so the selected dataset remains readable.
338
+ self.dataset_paths[Path(selected).name] = selected
339
+ self.dataset_labels = [NO_DATASET_LABEL, *self.dataset_paths]
340
+ self.dataset_combo.configure(values=self.dataset_labels)
341
+ self.dataset_var.set(Path(selected).name)
268
342
 
269
343
  def _mode_changed(self, _event: object = None) -> None:
270
344
  is_dc = self.mode_var.get().startswith("DC")
271
345
  self.block_combo.configure(state="readonly" if is_dc else "disabled")
272
346
 
273
347
  def _start(self) -> None:
274
- dataset = self.dataset_var.get().strip()
348
+ dataset_label = self.dataset_var.get().strip()
349
+ dataset = self.dataset_paths.get(dataset_label, "")
275
350
  if not dataset:
276
351
  messagebox.showerror("FeatureRank", "Please select a dataset.")
277
352
  return
@@ -291,10 +366,13 @@ class FeatureRankGUI:
291
366
  self.last_output_dir = None
292
367
  self.open_button.configure(state="disabled")
293
368
  self.start_button.configure(state="disabled")
294
- self.progress.configure(value=2)
369
+ self.progress_value = 2.0
370
+ self.progress.configure(value=self.progress_value)
295
371
  self.stage_var.set("Loading Dataset")
296
372
  self.status_var.set("The experiment is running. You can follow progress in the log.")
297
373
  self._clear_log()
374
+ self.progress_started_at = time.perf_counter()
375
+ self.progress_running = True
298
376
 
299
377
  self.worker = threading.Thread(
300
378
  target=self._run_experiment,
@@ -302,23 +380,24 @@ class FeatureRankGUI:
302
380
  daemon=True,
303
381
  )
304
382
  self.worker.start()
383
+ self._animate_progress()
305
384
  self.root.after(100, self._poll_messages)
306
385
 
307
386
  def _run_experiment(
308
387
  self, dataset: str, percent: float, task: str, mode: str, block_count: int
309
388
  ) -> None:
310
389
  started_at = time.perf_counter()
311
- writer = _QueueWriter(self.messages)
390
+ output_lines: list[str] = []
312
391
  try:
313
- # These imports intentionally stay inside the worker. Importing
314
- # FeatureRank remains quick, and all computation reuses the CLI
315
- # orchestration and task implementations already in the project.
316
- from scripts.FeatureRank import metric_name_for_task, run_workflow
392
+ # Keep TensorFlow out of the Tk process. On macOS, loading a model
393
+ # from a GUI thread can terminate the native Python process. The
394
+ # existing FeatureRank CLI runs in a separate worker instead; its
395
+ # stdout is streamed back into this window.
317
396
  from src.Config import ExperimentConfig
318
- from src.OutputPaths import metric_runs_path
319
397
 
398
+ dataset_path = _resolve_dataset_path(dataset)
320
399
  config = ExperimentConfig(
321
- dataset_name=dataset,
400
+ dataset_name=dataset_path,
322
401
  task=task,
323
402
  feature_percent=percent,
324
403
  random_state=42,
@@ -328,16 +407,59 @@ class FeatureRankGUI:
328
407
  cluster_k=None,
329
408
  save_details=False,
330
409
  )
331
- metric_path = metric_runs_path(config.task, Path(dataset).stem, percent)
332
- with contextlib.redirect_stdout(writer), contextlib.redirect_stderr(writer):
333
- values, average = run_workflow(
334
- config=config,
335
- mode=mode,
336
- repeat_runs=1,
337
- metric_name=metric_name_for_task(config.task),
338
- metric_path=metric_path,
339
- block_count=block_count,
340
- )
410
+ python_executable = _worker_python()
411
+ command = [
412
+ str(python_executable),
413
+ "-m",
414
+ "scripts.FeatureRank",
415
+ "--dataset-name",
416
+ dataset_path,
417
+ "--task",
418
+ task,
419
+ "--feature-percent",
420
+ str(percent),
421
+ "--random-state",
422
+ "42",
423
+ "--dc" if mode == "dc" else "--global",
424
+ ]
425
+ if mode == "dc":
426
+ command.extend(["--block-count", str(block_count)])
427
+
428
+ worker_environment = os.environ.copy()
429
+ worker_environment["PYTHONUNBUFFERED"] = "1"
430
+ package_root = str(Path(__file__).resolve().parent.parent)
431
+ existing_python_path = worker_environment.get("PYTHONPATH")
432
+ worker_environment["PYTHONPATH"] = os.pathsep.join(
433
+ path for path in (package_root, existing_python_path) if path
434
+ )
435
+ self.messages.put(("log", f"Worker: {python_executable}"))
436
+ process = subprocess.Popen(
437
+ command,
438
+ cwd=Path.cwd(),
439
+ env=worker_environment,
440
+ stdout=subprocess.PIPE,
441
+ stderr=subprocess.STDOUT,
442
+ text=True,
443
+ bufsize=1,
444
+ )
445
+ assert process.stdout is not None
446
+ for line in process.stdout:
447
+ output_lines.append(line.rstrip())
448
+ self.messages.put(("log", line))
449
+ return_code = process.wait()
450
+ if return_code != 0:
451
+ if return_code in {139, -11}:
452
+ raise RuntimeError(
453
+ "TensorFlow Anaconda ortaminda baslatilamadi (native crash). "
454
+ "Anaconda TensorFlow paketini guncelleyin veya uyumlu bir "
455
+ "Python yorumlayicisi kullanin."
456
+ )
457
+ recent_output = "\n".join(line for line in output_lines[-12:] if line.strip())
458
+ details = f"\n\nSon worker çıktısı:\n{recent_output}" if recent_output else ""
459
+ raise RuntimeError(f"FeatureRank worker stopped with code {return_code}.{details}")
460
+
461
+ average = 0.0
462
+ values: list[float] = []
341
463
  summary = self._read_summary(
342
464
  config,
343
465
  mode,
@@ -476,11 +598,31 @@ class FeatureRankGUI:
476
598
  elif "metrik" in lower or "metric" in lower or "tamamlandi" in lower:
477
599
  self._set_progress("Calculating Results", 95)
478
600
 
601
+ def _animate_progress(self) -> None:
602
+ """Advance the bar smoothly while the worker is still running.
603
+
604
+ Workflow messages move the bar to meaningful stages. This timer fills
605
+ the gaps between those messages based on elapsed time, then waits near
606
+ 90% until the worker reports completion instead of pretending to know
607
+ the exact duration of every dataset.
608
+ """
609
+ if not self.progress_running or self.progress_started_at is None:
610
+ return
611
+ elapsed = time.perf_counter() - self.progress_started_at
612
+ time_based_value = min(90.0, 2.0 + elapsed * 1.2)
613
+ if time_based_value > self.progress_value:
614
+ self.progress_value = time_based_value
615
+ self.progress.configure(value=self.progress_value)
616
+ self.status_var.set(f"The experiment is running. Elapsed time: {elapsed:.0f} s.")
617
+ self.progress_job = self.root.after(200, self._animate_progress)
618
+
479
619
  def _set_progress(self, stage: str, value: int) -> None:
480
620
  self.stage_var.set(stage)
481
- self.progress.configure(value=min(value, 99))
621
+ self.progress_value = max(self.progress_value, float(min(value, 99)))
622
+ self.progress.configure(value=self.progress_value)
482
623
 
483
624
  def _completed(self, summary: dict[str, str]) -> None:
625
+ self._stop_progress_animation()
484
626
  for key, value in summary.items():
485
627
  self.summary_vars[key].set(value)
486
628
  output = summary.get("Output Directory", "N/A")
@@ -493,13 +635,22 @@ class FeatureRankGUI:
493
635
  self.start_button.configure(state="normal")
494
636
 
495
637
  def _failed(self, error: str) -> None:
638
+ self._stop_progress_animation()
496
639
  self.progress.configure(value=0)
640
+ self.progress_value = 0.0
497
641
  self.stage_var.set("Failed")
498
642
  self.status_var.set("The experiment could not be completed.")
499
643
  self._append_log(f"ERROR: {error}")
500
644
  self.start_button.configure(state="normal")
501
645
  messagebox.showerror("FeatureRank", error)
502
646
 
647
+ def _stop_progress_animation(self) -> None:
648
+ self.progress_running = False
649
+ self.progress_started_at = None
650
+ if self.progress_job is not None:
651
+ self.root.after_cancel(self.progress_job)
652
+ self.progress_job = None
653
+
503
654
  def _clear_log(self) -> None:
504
655
  self.log_text.configure(state="normal")
505
656
  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.1"
14
+ __version__ = "0.1.4"
15
15
 
16
16
 
17
17
  def _run_cli(arguments: list[str] | None = None) -> None: