Advertisement
pepsimanfire

mlw_qpAdm+Rotations_1.3.py

May 28th, 2025
36
0
Never
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
text 53.05 KB | Source Code | 0 0
  1. import tkinter as tk
  2. from tkinter import filedialog, messagebox, scrolledtext
  3. import subprocess
  4. import tempfile
  5. import os
  6. import re
  7. from datetime import datetime
  8. from itertools import combinations
  9. import shutil
  10. import platform
  11.  
  12. population_history = []
  13. history_index = -1
  14.  
  15. # Add the clear_output function
  16. def clear_output():
  17. output_text.delete('1.0', tk.END)
  18.  
  19. def save_to_history(field, old_value, new_value):
  20. global population_history, history_index
  21. if history_index < len(population_history) - 1:
  22. population_history = population_history[:history_index + 1]
  23. population_history.append({
  24. 'field': field,
  25. 'old_value': old_value,
  26. 'new_value': ','.join(new_value) if isinstance(new_value, list) else new_value
  27. })
  28. history_index = len(population_history) - 1
  29.  
  30. def undo_population_addition():
  31. global history_index
  32. if history_index >= 0:
  33. entry = population_history[history_index]
  34. field, old_value = entry['field'], entry['old_value']
  35. field_map = {
  36. 'target': target_entry,
  37. 'left': left_entry,
  38. 'right': right_entry,
  39. 'fixed_right': fixed_right_entry,
  40. 'fixed_left': fixed_left_entry,
  41. 'rotation_pool': rotation_pool_entry
  42. }
  43. if field in field_map:
  44. field_map[field].delete(0, tk.END)
  45. field_map[field].insert(0, old_value)
  46. status_label.config(text=f"Undo: reverted {field}")
  47. history_index -= 1
  48. else:
  49. status_label.config(text="Nothing to undo")
  50.  
  51. def redo_population_addition():
  52. global history_index
  53. if history_index < len(population_history) - 1:
  54. history_index += 1
  55. entry = population_history[history_index]
  56. field, new_value = entry['field'], entry['new_value']
  57. field_map = {
  58. 'target': target_entry,
  59. 'left': left_entry,
  60. 'right': right_entry,
  61. 'fixed_right': fixed_right_entry,
  62. 'fixed_left': fixed_left_entry,
  63. 'rotation_pool': rotation_pool_entry
  64. }
  65. if field in field_map:
  66. field_map[field].delete(0, tk.END)
  67. field_map[field].insert(0, new_value)
  68. status_label.config(text=f"Redo: reapplied {field}")
  69. else:
  70. status_label.config(text="Nothing to redo")
  71.  
  72. def add_to_target():
  73. pops = get_selected_populations()
  74. if pops:
  75. current = target_entry.get().strip()
  76. current_pops = [p.strip() for p in re.split(r'[,\s]+', current) if p.strip()]
  77. new_pops = [p for p in pops if p not in current_pops]
  78. if new_pops:
  79. save_to_history("target", current, current_pops + new_pops)
  80. target_entry.delete(0, tk.END)
  81. target_entry.insert(0, ','.join(current_pops + new_pops))
  82.  
  83. def add_to_left():
  84. pops = get_selected_populations()
  85. if pops:
  86. current = left_entry.get().strip()
  87. current_pops = [p.strip() for p in re.split(r'[,\s]+', current) if p.strip()]
  88. new_pops = [p for p in pops if p not in current_pops]
  89. if new_pops:
  90. save_to_history("left", current, current_pops + new_pops)
  91. left_entry.delete(0, tk.END)
  92. left_entry.insert(0, ','.join(current_pops + new_pops))
  93.  
  94. def add_to_right():
  95. pops = get_selected_populations()
  96. if pops:
  97. current = right_entry.get().strip()
  98. current_pops = [p.strip() for p in re.split(r'[,\s]+', current) if p.strip()]
  99. new_pops = [p for p in pops if p not in current_pops]
  100. if new_pops:
  101. save_to_history("right", current, current_pops + new_pops)
  102. right_entry.delete(0, tk.END)
  103. right_entry.insert(0, ','.join(current_pops + new_pops))
  104.  
  105. def add_to_fixed_right():
  106. pops = get_selected_populations()
  107. if pops:
  108. current = fixed_right_entry.get().strip()
  109. current_pops = [p.strip() for p in re.split(r'[,\s]+', current) if p.strip()]
  110. new_pops = [p for p in pops if p not in current_pops]
  111. if new_pops:
  112. save_to_history("fixed_right", current, current_pops + new_pops)
  113. fixed_right_entry.delete(0, tk.END)
  114. fixed_right_entry.insert(0, ','.join(current_pops + new_pops))
  115.  
  116. def add_to_fixed_left():
  117. pops = get_selected_populations()
  118. if pops:
  119. current = fixed_left_entry.get().strip()
  120. current_pops = [p.strip() for p in re.split(r'[,\s]+', current) if p.strip()]
  121. new_pops = [p for p in pops if p not in current_pops]
  122. if new_pops:
  123. save_to_history("fixed_left", current, current_pops + new_pops)
  124. fixed_left_entry.delete(0, tk.END)
  125. fixed_left_entry.insert(0, ','.join(current_pops + new_pops))
  126.  
  127. def add_to_rotation_pool():
  128. pops = get_selected_populations()
  129. if pops:
  130. current = rotation_pool_entry.get().strip()
  131. current_pops = [p.strip() for p in re.split(r'[,\s]+', current) if p.strip()]
  132. new_pops = [p for p in pops if p not in current_pops]
  133. if new_pops:
  134. save_to_history("rotation_pool", current, current_pops + new_pops)
  135. rotation_pool_entry.delete(0, tk.END)
  136. rotation_pool_entry.insert(0, ','.join(current_pops + new_pops))
  137.  
  138. # FORMAT POPS FUNCTION
  139. def format_pops(input_str):
  140. clean_input = input_str.replace('"', '').replace("'", '').replace(',', ' ')
  141. pops = [p.strip() for p in clean_input.split() if p.strip()]
  142. return ','.join(f'"{p}"' for p in pops)
  143.  
  144. # GET R LIBRARY PATHS
  145. def get_r_library_paths(rscript_path='Rscript'):
  146. try:
  147. result = subprocess.run(
  148. [rscript_path, '-e', 'cat(.libPaths())'],
  149. stdout=subprocess.PIPE,
  150. stderr=subprocess.PIPE,
  151. text=True
  152. )
  153. paths_raw = result.stdout.strip()
  154. paths = re.findall(r'\/[^\s"]+', paths_raw)
  155. return paths
  156. except Exception as e:
  157. return []
  158.  
  159. # CHECK IF PACKAGE EXISTS IN ANY OF THE R LIBRARY PATHS
  160. def check_package_in_paths(package_name, paths):
  161. for path in paths:
  162. if os.path.isdir(os.path.join(path, package_name)):
  163. return path
  164. return None
  165.  
  166. # Persistent variable to store modified R code
  167. custom_r_code = None
  168.  
  169. def get_rscript_path(r_folder):
  170. if os.name == 'nt':
  171. # Windows paths
  172. paths_to_try = [
  173. os.path.join(r_folder, 'bin', 'x64', 'Rscript.exe'),
  174. os.path.join(r_folder, 'bin', 'Rscript.exe')
  175. ]
  176. else:
  177. # Linux/Mac paths
  178. if r_folder:
  179. paths_to_try = [os.path.join(r_folder, 'bin', 'Rscript')]
  180. else:
  181. # Auto-detect from PATH or common locations
  182. rscript_path = shutil.which('Rscript')
  183. if rscript_path:
  184. return rscript_path
  185. paths_to_try = [
  186. '/Library/Frameworks/R.framework/Versions/Current/Resources/bin/Rscript',
  187. '/usr/local/bin/Rscript',
  188. '/usr/bin/Rscript',
  189. '/opt/R/bin/Rscript',
  190. '/opt/homebrew/bin/Rscript' # For Homebrew installations
  191. ]
  192. # Check all possible paths
  193. for path in paths_to_try:
  194. if os.path.isfile(path):
  195. return path
  196. return None
  197.  
  198. # MAIN RUN FUNCTION
  199. def run_qpadm():
  200. target_pops_raw = target_entry.get()
  201. left_pops_raw = left_entry.get()
  202. right_pops_raw = right_entry.get()
  203. dataset_prefix = prefix_entry.get()
  204.  
  205. if not (target_pops_raw and left_pops_raw and right_pops_raw and dataset_prefix):
  206. messagebox.showerror("Missing info", "Please fill in all fields.")
  207. return
  208.  
  209. # Determine Rscript path based on OS
  210. if platform.system() == "Windows":
  211. r_folder = r_folder_entry.get().strip()
  212. if not r_folder:
  213. messagebox.showerror("Missing R folder", "Please specify the R installation folder.")
  214. return
  215. rscript_path = os.path.join(r_folder, 'bin', 'x64', 'Rscript.exe')
  216. if not os.path.isfile(rscript_path):
  217. rscript_path_alt = os.path.join(r_folder, 'bin', 'Rscript.exe')
  218. if os.path.isfile(rscript_path_alt):
  219. rscript_path = rscript_path_alt
  220. else:
  221. messagebox.showerror("Rscript not found", "Rscript.exe not found in specified folder.")
  222. return
  223. else:
  224. rscript_path = 'Rscript'
  225. if not shutil.which(rscript_path):
  226. messagebox.showerror("Rscript not found", "Install R or ensure 'Rscript' is in your PATH.")
  227. return
  228.  
  229. target_pops = format_pops(target_pops_raw)
  230. left_pops = format_pops(left_pops_raw)
  231. right_pops = format_pops(right_pops_raw)
  232.  
  233. r_lib_paths = get_r_library_paths(rscript_path)
  234. package_path = check_package_in_paths("admixtools", r_lib_paths)
  235. lib_path_code = f'.libPaths("{package_path}")\n' if package_path else ''
  236.  
  237. r_code = f"""
  238. options(warn = -1) # <-- ADD THIS LINE TO SUPPRESS WARNINGS
  239. options(lubridate.verbose = FALSE) # Disable lubridate system checks
  240. Sys.setenv(TZ = "UTC") # Set default timezone to avoid system checks
  241. Sys.setenv(TZDIR = "/usr/share/zoneinfo") # Explicit path to zoneinfo
  242. {lib_path_code}
  243. suppressPackageStartupMessages(library(admixtools)) # <-- MODIFIED
  244. suppressPackageStartupMessages(library(tidyverse)) # <-- MODIFIED
  245. library(admixtools)
  246. library(tidyverse)
  247.  
  248. prefix = "{dataset_prefix}"
  249. target = c({target_pops})
  250. left = c({left_pops})
  251. right = c({right_pops})
  252.  
  253. results = qpadm(prefix, left, right, target, allsnps = TRUE)
  254.  
  255. cat("\\nRESULTS_WEIGHTS\\n")
  256. print(results$weights)
  257. cat("\\nRESULTS_POPDROP\\n")
  258. print(results$popdrop)
  259. cat("\\nRESULTS_SUMMARY\\n")
  260. cat("Blocks:", results$summary$nblocks, "SNPs:", results$summary$nsnps, "\\n")
  261. """
  262.  
  263. with tempfile.NamedTemporaryFile(delete=False, suffix=".R") as r_script:
  264. r_script.write(r_code.encode('utf-8'))
  265. r_script_path = r_script.name
  266.  
  267. try:
  268. process = subprocess.Popen(
  269. [rscript_path, '--quiet', r_script_path],
  270. stdout=subprocess.PIPE,
  271. stderr=subprocess.STDOUT,
  272. text=True,
  273. encoding='utf-8'
  274. )
  275.  
  276. timestamp = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
  277. output_text.insert(tk.END, f"\n---\nDone by pepsimanfire - Run started at {timestamp}\n")
  278. output_text.insert(tk.END, f"Target: {target_pops_raw}\nLeft: {left_pops_raw}\nRight: {right_pops_raw}\n\n")
  279. output_text.see(tk.END)
  280. output_text.update()
  281.  
  282. full_output = []
  283. weights_output = ""
  284. popdrop_output = ""
  285. summary_output = ""
  286. capturing_weights = False
  287. capturing_popdrop = False
  288. capturing_summary = False
  289.  
  290. snps_count = None
  291. blocks_total = None
  292.  
  293. for raw_line in process.stdout:
  294. line = raw_line.strip()
  295. full_output.append(raw_line)
  296.  
  297. if "RESULTS_WEIGHTS" in line:
  298. capturing_weights = True
  299. capturing_popdrop = capturing_summary = False
  300. continue
  301. elif "RESULTS_POPDROP" in line:
  302. capturing_popdrop = True
  303. capturing_weights = capturing_summary = False
  304. continue
  305. elif "RESULTS_SUMMARY" in line:
  306. capturing_summary = True
  307. capturing_weights = capturing_popdrop = False
  308. continue
  309.  
  310. if capturing_weights:
  311. weights_output += line + "\n"
  312. continue
  313. elif capturing_popdrop:
  314. popdrop_output += line + "\n"
  315. continue
  316. elif capturing_summary:
  317. if re.match(r'^Blocks:\s*SNPs:\s*$', line):
  318. continue
  319. summary_output += line + "\n"
  320. continue
  321.  
  322. snps_match = re.search(r'Computing block lengths for (\d+) SNPs', line)
  323. if snps_match:
  324. snps_count = snps_match.group(1)
  325.  
  326. block_match = re.search(r'Computing .* block (\d+) out of (\d+)', line)
  327. if block_match:
  328. block_num, total_blocks = block_match.groups()
  329. blocks_total = total_blocks
  330. status_label.config(text=f"Computing block {block_num} of {total_blocks}...")
  331. status_label.update()
  332.  
  333. if "Error" in line or "error" in line:
  334. output_text.insert(tk.END, f"āŒ {line}\n")
  335. output_text.see(tk.END)
  336. output_text.update()
  337.  
  338. process.wait()
  339.  
  340. if process.returncode != 0:
  341. error_message = '\n'.join(full_output)
  342. output_text.insert(tk.END, f"\nāŒ qpAdm failed with exit code {process.returncode}:\n{error_message}\n")
  343. output_text.see(tk.END)
  344. else:
  345. output_text.insert(tk.END, "Weights:\n" + weights_output)
  346. output_text.insert(tk.END, "Popdrop:\n" + popdrop_output)
  347. if summary_output:
  348. output_text.insert(tk.END, summary_output)
  349. if blocks_total and snps_count:
  350. output_text.insert(tk.END, f"Total: {blocks_total} Blocks, {snps_count} SNPs\n")
  351.  
  352. output_text.see(tk.END)
  353. output_text.update()
  354. status_label.config(text="qpAdm completed." if process.returncode == 0 else "qpAdm failed!")
  355.  
  356. except Exception as e:
  357. messagebox.showerror("Error running R", str(e))
  358.  
  359. finally:
  360. os.remove(r_script_path)
  361.  
  362. def edit_and_run_r_code():
  363. global custom_r_code
  364. dataset_prefix = prefix_entry.get().strip()
  365.  
  366. # Determine Rscript path
  367. if platform.system() == "Windows":
  368. r_folder = r_folder_entry.get().strip()
  369. if not r_folder:
  370. messagebox.showerror("Missing R folder", "Please specify the R installation folder.")
  371. return
  372. rscript_path = os.path.join(r_folder, 'bin', 'x64', 'Rscript.exe')
  373. if not os.path.isfile(rscript_path):
  374. messagebox.showerror("Rscript not found", f"Rscript.exe not found at:\n{rscript_path}")
  375. return
  376. else:
  377. rscript_path = 'Rscript'
  378. if not shutil.which(rscript_path):
  379. messagebox.showerror("Rscript not found", "Install R or ensure 'Rscript' is in PATH.")
  380. return
  381.  
  382. # Default R code template
  383. r_lib_paths = get_r_library_paths(rscript_path)
  384. package_path = check_package_in_paths("admixtools", r_lib_paths)
  385. lib_path_code = f'.libPaths("{package_path}")\n' if package_path else ''
  386.  
  387. default_r_code = f"""
  388. {lib_path_code}
  389. library(admixtools)
  390. library(tidyverse)
  391.  
  392. # You must edit these:
  393. prefix = "{dataset_prefix}" # <- If your dataset name is john_merged and your folder is john/dataset, put it like this: john/dataset/john_merged
  394. target = c("POP1") # <- Replace with your actual target populations
  395. left = c("LEFT1", "LEFT2") # <- Replace with your left populations
  396. right = c("RIGHT1", "RIGHT2") # <- Replace with your right populations
  397.  
  398. results = qpadm(prefix, left, right, target, allsnps = TRUE)
  399.  
  400. cat("\\nRESULTS_WEIGHTS\\n")
  401. print(results$weights)
  402. cat("\\nRESULTS_POPDROP\\n")
  403. print(results$popdrop)
  404. cat("\\nRESULTS_SUMMARY\\n")
  405. cat("Blocks:", results$summary$nblocks, "SNPs:", results$summary$nsnps, "\\n")
  406. """.strip()
  407.  
  408. if custom_r_code is None:
  409. custom_r_code = default_r_code # first-time setup
  410.  
  411. # --- Create the popup editor window ---
  412. editor_win = tk.Toplevel(root)
  413. editor_win.attributes('-topmost', True)
  414. editor_win.title("Edit R Code Manually")
  415.  
  416. def enforce_always_on_top():
  417. try:
  418. editor_win.attributes("-topmost", True)
  419. except tk.TclError:
  420. return # Window likely closed
  421. editor_win.after(1000, enforce_always_on_top)
  422.  
  423. enforce_always_on_top() # Start the loop
  424.  
  425. # Text editor
  426. text_editor = tk.Text(editor_win, wrap=tk.NONE, width=100, height=30, undo=True)
  427. text_editor.insert('1.0', custom_r_code)
  428. text_editor.pack(fill=tk.BOTH, expand=True)
  429. text_editor.bind('<Control-z>', lambda e: output_text.edit_undo())
  430.  
  431. # --- Button actions ---
  432. def run_edited_r_code():
  433. global custom_r_code
  434. edited_code = text_editor.get("1.0", tk.END)
  435. custom_r_code = edited_code # Save persistently
  436.  
  437. with tempfile.NamedTemporaryFile(delete=False, suffix=".R") as temp_r_file:
  438. temp_r_file.write(edited_code.encode('utf-8'))
  439. r_script_path = temp_r_file.name
  440.  
  441. try:
  442. process = subprocess.Popen(
  443. [rscript_path, r_script_path],
  444. stdout=subprocess.PIPE,
  445. stderr=subprocess.STDOUT,
  446. text=True,
  447. encoding='utf-8'
  448. )
  449. timestamp = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
  450. output_text.insert(tk.END, f"\n---\n[Done by pepsimanfire - Custom R Code Output - {timestamp}]\n")
  451.  
  452. full_output = []
  453. for line in process.stdout:
  454. output_text.insert(tk.END, line)
  455. full_output.append(line)
  456. output_text.see(tk.END)
  457.  
  458. process.wait()
  459.  
  460. if process.returncode != 0:
  461. error_message = ''.join(full_output)
  462. output_text.insert(tk.END, f"\nāŒ Custom R code failed (exit code {process.returncode}):\n{error_message}\n")
  463. output_text.see(tk.END)
  464. status_label.config(text="Custom R code failed!")
  465. else:
  466. status_label.config(text="Custom R code executed.")
  467.  
  468. except Exception as e:
  469. messagebox.showerror("Execution Error", str(e))
  470. finally:
  471. os.remove(r_script_path)
  472.  
  473. def restore_original_code():
  474. nonlocal text_editor
  475. global custom_r_code
  476. text_editor.delete("1.0", tk.END)
  477. text_editor.insert("1.0", default_r_code)
  478. status_label.config(text="Original code restored.")
  479.  
  480. def on_editor_close():
  481. current_code = text_editor.get("1.0", tk.END)
  482. # Always save the edited code before closing
  483. global custom_r_code
  484. custom_r_code = current_code
  485. editor_win.destroy()
  486.  
  487. editor_win.protocol("WM_DELETE_WINDOW", on_editor_close)
  488.  
  489. # --- Buttons Frame ---
  490. button_frame = tk.Frame(editor_win)
  491. button_frame.pack(fill=tk.X, pady=5)
  492.  
  493. restore_btn = tk.Button(button_frame, text="Restore Original Code", command=restore_original_code, bg="orange")
  494. restore_btn.pack(side=tk.LEFT, padx=5)
  495.  
  496. run_button = tk.Button(editor_win, text="Run This Code", command=run_edited_r_code, bg="lightgreen")
  497. run_button.pack(pady=5)
  498.  
  499. # --- ROTATION FUNCTIONS ---
  500. def run_rotation():
  501. # Determine Rscript path
  502. if platform.system() == "Windows":
  503. r_folder = r_folder_entry.get().strip()
  504. if not r_folder:
  505. messagebox.showerror("Error", "Please specify the R installation folder")
  506. return
  507. rscript_path = os.path.join(r_folder, 'bin', 'Rscript.exe')
  508. if not os.path.isfile(rscript_path):
  509. rscript_path = os.path.join(r_folder, 'bin', 'x64', 'Rscript.exe')
  510. if not os.path.isfile(rscript_path):
  511. messagebox.showerror("Error", "Rscript.exe not found.")
  512. return
  513. else:
  514. rscript_path = 'Rscript'
  515. if not shutil.which(rscript_path):
  516. messagebox.showerror("Error", "Install R or ensure 'Rscript' is in PATH.")
  517. return
  518.  
  519. # --- Get Inputs ---
  520. target_pops = target_entry.get().strip()
  521. fixed_left = fixed_left_entry.get().strip()
  522. fixed_right = fixed_right_entry.get().strip()
  523. rotation_pool = rotation_pool_entry.get().strip()
  524. model_min = int(model_min_entry.get())
  525. model_max = int(model_max_entry.get())
  526. prefix = prefix_entry.get().strip()
  527. rotation_mode = rotation_mode_entry.get().strip().lower()[0] if rotation_mode_entry.get().strip() else 'd'
  528.  
  529. # Format populations
  530. def format_pop_list(pop_str):
  531. if not pop_str:
  532. return []
  533. pops = [p.strip().strip('"') for p in pop_str.split(',')]
  534. return [f'"{p}"' for p in pops if p]
  535.  
  536. fixed_left_pops = format_pop_list(fixed_left)
  537. fixed_right_pops = format_pop_list(fixed_right)
  538. rotation_pool_pops = format_pop_list(rotation_pool)
  539.  
  540. # --- Generate all models based on rotation mode ---
  541. all_models = []
  542.  
  543. if rotation_mode == "r":
  544. # RIGHT-ONLY: Rotation pool only added to right (left stays fixed)
  545. for size in range(model_min, model_max + 1):
  546. for combo in combinations(rotation_pool_pops, size):
  547. right = fixed_right_pops + list(combo)
  548. left = fixed_left_pops # Left remains unchanged
  549. if not (set(left) & set(right)): # Skip if overlap
  550. all_models.append((left, right))
  551.  
  552. elif rotation_mode == "l":
  553. # LEFT-ONLY: Rotation pool only added to left (right stays fixed)
  554. for size in range(model_min, model_max + 1):
  555. for combo in combinations(rotation_pool_pops, size):
  556. left = fixed_left_pops + list(combo)
  557. right = fixed_right_pops # Right remains unchanged
  558. if not (set(left) & set(right)): # Skip if overlap
  559. all_models.append((left, right))
  560. else:
  561. # DEFAULT: Original behavior - rotate both sides
  562. for size in range(model_min, model_max + 1):
  563. for combo in combinations(rotation_pool_pops, size):
  564. rotating_left = list(combo)
  565. rotating_right = [p for p in rotation_pool_pops if p not in rotating_left]
  566. left = fixed_left_pops + rotating_left
  567. right = fixed_right_pops + rotating_right
  568. if not (set(left) & set(right)): # Skip if overlap
  569. all_models.append((left, right))
  570.  
  571. # Remove duplicates
  572. unique_models = []
  573. seen = set()
  574. for left, right in all_models:
  575. key = (tuple(sorted(left)), tuple(sorted(right)))
  576. if key not in seen:
  577. seen.add(key)
  578. unique_models.append((left, right))
  579.  
  580. # Print all models first
  581. timestamp = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
  582. output_text.insert(tk.END, f"\n---\n[Rotation Analysis - {timestamp}]\n")
  583. output_text.insert(tk.END, f"Target: {target_pops}\n")
  584. output_text.insert(tk.END, f"Fixed Left: {fixed_left_pops}\n")
  585. output_text.insert(tk.END, f"Fixed Right: {fixed_right_pops}\n")
  586. output_text.insert(tk.END, f"Rotation Pool: {rotation_pool_pops}\n")
  587. output_text.insert(tk.END, f"Rotation Mode: {'Right-only' if rotation_mode == 'r' else 'Left-only' if rotation_mode == 'l' else 'Default'}\n")
  588. output_text.insert(tk.END, f"Model Size Range: {model_min}-{model_max}\n")
  589. output_text.insert(tk.END, f"Total Models: {len(unique_models)}\n\n")
  590. output_text.see(tk.END)
  591. output_text.update()
  592.  
  593. # --- Run Models ---
  594. total_models = len(unique_models)
  595. for idx, (left, right) in enumerate(unique_models):
  596. current_model = idx + 1
  597. timestamp = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
  598. output_text.insert(tk.END, f"\n---\n[Model {current_model}/{total_models} - {timestamp}]\n")
  599. output_text.insert(tk.END, f"Left: {', '.join(left)}\n")
  600. output_text.insert(tk.END, f"Right: {', '.join(right)}\n")
  601. output_text.update()
  602.  
  603. # Format populations for R
  604. formatted_left = ','.join(left)
  605. formatted_right = ','.join(right)
  606. formatted_target = f'"{target_pops}"'
  607.  
  608. r_lib_paths = get_r_library_paths(rscript_path)
  609. package_path = check_package_in_paths("admixtools", r_lib_paths)
  610. lib_path_code = f'.libPaths("{package_path}")\n' if package_path else ''
  611.  
  612.  
  613. # Generate R code with same output format as run_qpadm()
  614. r_code = f"""
  615. options(warn = -1) # <-- ADD THIS LINE TO SUPPRESS WARNINGS
  616. options(lubridate.verbose = FALSE) # Disable lubridate system checks
  617. Sys.setenv(TZ = "UTC") # Set default timezone to avoid system checks
  618. Sys.setenv(TZDIR = "/usr/share/zoneinfo") # Explicit path to zoneinfo
  619. {lib_path_code}
  620. suppressPackageStartupMessages(library(admixtools)) # <-- MODIFIED
  621. suppressPackageStartupMessages(library(tidyverse)) # <-- MODIFIED
  622. library(admixtools)
  623. library(tidyverse)
  624.  
  625. prefix <- "{prefix}"
  626. target <- c({formatted_target})
  627. left <- c({formatted_left})
  628. right <- c({formatted_right})
  629.  
  630. tryCatch({{
  631. results <- qpadm(prefix, left, right, target, allsnps = TRUE)
  632.  
  633. cat("\\nRESULTS_WEIGHTS\\n")
  634. print(results$weights)
  635. cat("\\nRESULTS_POPDROP\\n")
  636. print(results$popdrop)
  637. }}, error = function(e) {{
  638. cat("ERROR:", e$message, "\\n")
  639. }})
  640. """
  641.  
  642. with tempfile.NamedTemporaryFile(delete=False, suffix=".R", mode='w') as r_script:
  643. r_script.write(r_code)
  644. r_script_path = r_script.name
  645.  
  646. try:
  647. process = subprocess.Popen(
  648. [rscript_path, '--quiet', r_script_path],
  649. stdout=subprocess.PIPE,
  650. stderr=subprocess.STDOUT,
  651. text=True,
  652. encoding='utf-8'
  653. )
  654.  
  655. # Parse output the same way as run_qpadm()
  656. full_output = []
  657. weights_output = ""
  658. popdrop_output = ""
  659. summary_output = ""
  660. capturing_weights = False
  661. capturing_popdrop = False
  662. capturing_summary = False
  663.  
  664. for raw_line in process.stdout:
  665. line = raw_line.strip()
  666. full_output.append(raw_line)
  667.  
  668. if "RESULTS_WEIGHTS" in line:
  669. capturing_weights = True
  670. capturing_popdrop = capturing_summary = False
  671. continue
  672. elif "RESULTS_POPDROP" in line:
  673. capturing_popdrop = True
  674. capturing_weights = capturing_summary = False
  675. continue
  676. elif "RESULTS_SUMMARY" in line:
  677. capturing_summary = True
  678. capturing_weights = capturing_popdrop = False
  679. continue
  680.  
  681. if capturing_weights:
  682. weights_output += line + "\n"
  683. elif capturing_popdrop:
  684. popdrop_output += line + "\n"
  685. elif capturing_summary:
  686. summary_output += line + "\n"
  687.  
  688. # Show progress for long-running models
  689. block_match = re.search(r'Computing .* block (\d+) out of (\d+)', line)
  690. if block_match:
  691. block_num, total_blocks = block_match.groups()
  692. status_label.config(text=f"Model {current_model}/{total_models} - Block {block_num}/{total_blocks}")
  693. status_label.update()
  694.  
  695. process.wait()
  696.  
  697. # Display results or errors
  698. if process.returncode != 0:
  699. error_message = '\n'.join(full_output)
  700. output_text.insert(tk.END, f"āŒ Model {current_model} failed (exit code {process.returncode}):\n{error_message}\n")
  701. else:
  702. if weights_output:
  703. output_text.insert(tk.END, "Weights:\n" + weights_output + "\n")
  704. if popdrop_output:
  705. output_text.insert(tk.END, "Popdrop:\n" + popdrop_output + "\n")
  706. if summary_output:
  707. output_text.insert(tk.END, "Summary:\n" + summary_output + "\n")
  708.  
  709. output_text.see(tk.END)
  710. output_text.update()
  711.  
  712. except Exception as e:
  713. output_text.insert(tk.END, f"āŒ Model {current_model} failed: {str(e)}\n")
  714. output_text.see(tk.END)
  715.  
  716. finally:
  717. if os.path.exists(r_script_path):
  718. os.remove(r_script_path)
  719.  
  720. status_label.config(text="Rotation analysis completed!")
  721.  
  722. # --- MAIN WINDOW SETUP ---
  723. root = tk.Tk()
  724. # Adjust scaling factor for Linux
  725. if platform.system() == "Linux":
  726. root.tk.call('tk', 'scaling', 1.0) # Linux scaling adjustment
  727. elif platform.system() == "Darwin": # macOS detection
  728. root.tk.call('tk', 'scaling', 1.0) # Example: Higher scaling for macOS (Retina displays)
  729. root.title("qpAdm Runner")
  730.  
  731. # --- Scrollable Window Setup ---
  732. main_frame = tk.Frame(root)
  733. main_frame.pack(fill=tk.BOTH, expand=1)
  734.  
  735. canvas = tk.Canvas(main_frame)
  736. scrollbar = tk.Scrollbar(main_frame, orient=tk.VERTICAL, command=canvas.yview)
  737. scrollable_frame = tk.Frame(canvas)
  738.  
  739. scrollable_frame.bind(
  740. "<Configure>",
  741. lambda e: canvas.configure(
  742. scrollregion=canvas.bbox("all")
  743. )
  744. )
  745.  
  746. canvas.create_window((0, 0), window=scrollable_frame, anchor="nw")
  747. canvas.configure(yscrollcommand=scrollbar.set)
  748.  
  749. canvas.pack(side=tk.LEFT, fill=tk.BOTH, expand=1)
  750. scrollbar.pack(side=tk.RIGHT, fill=tk.Y)
  751.  
  752. # --- GUI ELEMENTS ---
  753. tk.Label(scrollable_frame, text="Target populations:").grid(row=0, column=0, sticky='w', padx=5, pady=2)
  754. target_entry = tk.Entry(scrollable_frame, width=70)
  755. target_entry.grid(row=0, column=1, columnspan=2, padx=5, pady=2, sticky='we')
  756.  
  757. tk.Label(scrollable_frame, text="Left populations:").grid(row=1, column=0, sticky='w', padx=5, pady=2)
  758. left_var = tk.StringVar()
  759. left_entry = tk.Entry(scrollable_frame, width=70, textvariable=left_var)
  760. left_entry.grid(row=1, column=1, columnspan=2, padx=5, pady=2, sticky='we')
  761.  
  762. def open_left_editor():
  763. top = tk.Toplevel(root)
  764. top.title("Edit Left Populations - Large View")
  765. top.geometry("800x400")
  766. top.resizable(True, True)
  767.  
  768. # Keep the window on top permanently
  769. top.attributes("-topmost", True)
  770.  
  771. def keep_on_top():
  772. top.attributes("-topmost", True) # Reapply the attribute
  773. top.after(1000, keep_on_top) # Repeat every second
  774.  
  775. keep_on_top() # Start the loop
  776.  
  777. text = tk.Text(top, wrap=tk.WORD, font=("Consolas", 12))
  778. text.pack(expand=True, fill="both")
  779. text.insert("1.0", left_var.get())
  780.  
  781. syncing_from_popup = False
  782. syncing_from_main = False
  783.  
  784. def sync_popup_to_entry(event=None):
  785. nonlocal syncing_from_popup
  786. syncing_from_popup = True
  787. left_var.set(text.get("1.0", "end-1c"))
  788. syncing_from_popup = False
  789.  
  790. def sync_entry_to_popup(*args):
  791. nonlocal syncing_from_main
  792. if not syncing_from_popup:
  793. syncing_from_main = True
  794. value = left_var.get()
  795. if value != text.get("1.0", "end-1c"):
  796. text.delete("1.0", tk.END)
  797. text.insert("1.0", value)
  798. syncing_from_main = False
  799.  
  800. text.bind("<KeyRelease>", sync_popup_to_entry)
  801. trace_id = left_var.trace_add("write", sync_entry_to_popup)
  802.  
  803. def on_close():
  804. left_var.trace_remove("write", trace_id)
  805. top.destroy()
  806.  
  807. tk.Button(top, text="Close", command=on_close).pack(pady=5)
  808.  
  809.  
  810. tk.Button(scrollable_frame, text="šŸ”•", width=3, command=open_left_editor).grid(row=1, column=3, padx=(0, 5))
  811.  
  812. tk.Label(scrollable_frame, text="Right populations:").grid(row=2, column=0, sticky='w', padx=5, pady=2)
  813. right_var = tk.StringVar() # <-- Corrected to right_var
  814. right_entry = tk.Entry(scrollable_frame, width=70, textvariable=right_var) # <-- Corrected to right_entry
  815. right_entry.grid(row=2, column=1, columnspan=2, padx=5, pady=2, sticky='we') # <-- Corrected to right_entry
  816.  
  817. def open_right_editor():
  818. top = tk.Toplevel(root)
  819. top.title("Edit Right Populations - Large View")
  820. top.geometry("800x400")
  821. top.resizable(True, True)
  822.  
  823. # Keep the window on top permanently
  824. top.attributes("-topmost", True)
  825.  
  826. def keep_on_top():
  827. top.attributes("-topmost", True) # Reapply the attribute
  828. top.after(1000, keep_on_top) # Repeat every second
  829.  
  830. keep_on_top() # Start the loop
  831.  
  832. text = tk.Text(top, wrap=tk.WORD, font=("Consolas", 12))
  833. text.pack(expand=True, fill="both")
  834. text.insert("1.0", right_var.get())
  835.  
  836. syncing_from_popup = False
  837. syncing_from_main = False
  838.  
  839. def sync_popup_to_entry(event=None):
  840. nonlocal syncing_from_popup
  841. syncing_from_popup = True
  842. right_var.set(text.get("1.0", "end-1c"))
  843. syncing_from_popup = False
  844.  
  845. def sync_entry_to_popup(*args):
  846. nonlocal syncing_from_main
  847. if not syncing_from_popup:
  848. syncing_from_main = True
  849. value = right_var.get()
  850. if value != text.get("1.0", "end-1c"):
  851. text.delete("1.0", tk.END)
  852. text.insert("1.0", value)
  853. syncing_from_main = False
  854.  
  855. text.bind("<KeyRelease>", sync_popup_to_entry)
  856. trace_id = right_var.trace_add("write", sync_entry_to_popup)
  857.  
  858. def on_close():
  859. right_var.trace_remove("write", trace_id)
  860. top.destroy()
  861.  
  862. tk.Button(top, text="Close", command=on_close).pack(pady=5)
  863.  
  864. tk.Button(scrollable_frame, text="šŸ”•", width=3, command=open_right_editor).grid(row=2, column=3, padx=(0, 5))
  865.  
  866. tk.Label(scrollable_frame, text="Dataset prefix (path):").grid(row=3, column=0, sticky='w', padx=5, pady=2)
  867. prefix_entry_var = tk.StringVar()
  868. prefix_entry = tk.Entry(scrollable_frame, width=70, textvariable=prefix_entry_var)
  869. prefix_entry.grid(row=3, column=1, padx=5, pady=2, sticky='we')
  870.  
  871. def browse_folder():
  872. folder_selected = filedialog.askdirectory()
  873. if folder_selected:
  874. prefix_entry_var.set(folder_selected)
  875.  
  876. tk.Button(scrollable_frame, text="Browse...", command=browse_folder).grid(row=3, column=2, padx=5, pady=2)
  877.  
  878. # R Installation Folder
  879. if platform.system() == "Windows":
  880. # R Installation Folder (Windows only)
  881. tk.Label(scrollable_frame, text="R Installation Folder:").grid(row=4, column=0, sticky='w', padx=5, pady=2)
  882. r_folder_entry = tk.Entry(scrollable_frame, width=70)
  883. r_folder_entry.grid(row=4, column=1, padx=5, pady=2, sticky='we')
  884.  
  885. def browse_r_folder():
  886. folder_selected = filedialog.askdirectory()
  887. if folder_selected:
  888. r_folder_entry.delete(0, tk.END)
  889. r_folder_entry.insert(0, folder_selected)
  890.  
  891. tk.Button(scrollable_frame, text="Browse...", command=browse_r_folder).grid(row=4, column=2, padx=5, pady=2)
  892. else:
  893. # Hide these widgets on Linux
  894. r_folder_entry = None # Placeholder to avoid reference errors
  895.  
  896. # --- .ind FILE EDITOR ---
  897. ind_frame = tk.LabelFrame(scrollable_frame, text=".ind File Editor", padx=5, pady=5)
  898. ind_frame.grid(row=5, column=0, columnspan=3, sticky='we', padx=5, pady=10)
  899.  
  900. # Editor controls
  901. editor_controls = tk.Frame(ind_frame)
  902. editor_controls.pack(fill=tk.X, pady=(0, 5))
  903.  
  904. left_controls = tk.Frame(editor_controls)
  905. left_controls.pack(side=tk.LEFT)
  906.  
  907. right_controls = tk.Frame(editor_controls)
  908. right_controls.pack(side=tk.RIGHT)
  909.  
  910. tk.Button(right_controls, text="Undo Add", command=undo_population_addition).pack(side=tk.LEFT, padx=2)
  911. tk.Button(right_controls, text="Redo Add", command=redo_population_addition).pack(side=tk.LEFT, padx=2)
  912.  
  913. def undo_ind_changes():
  914. try: ind_text.edit_undo()
  915. except: pass
  916.  
  917. def redo_ind_changes():
  918. try: ind_text.edit_redo()
  919. except: pass
  920.  
  921. def save_ind_file():
  922. folder_path = prefix_entry_var.get().strip()
  923. if not folder_path:
  924. messagebox.showerror("Error", "Dataset folder is empty. Cannot save .ind file.")
  925. return
  926.  
  927. parent_folder = os.path.dirname(folder_path)
  928. last_folder_name = os.path.basename(folder_path)
  929. ind_file_name = f"{last_folder_name}.ind"
  930. ind_path = os.path.join(parent_folder, ind_file_name)
  931.  
  932. try:
  933. content = ind_text.get('1.0', tk.END)
  934. with open(ind_path, 'w') as f:
  935. f.write(content)
  936. status_label.config(text=f".ind file saved: {ind_file_name}")
  937. except Exception as e:
  938. messagebox.showerror("Error", f"Failed to save .ind file: {str(e)}")
  939.  
  940. tk.Button(editor_controls, text="Undo", command=undo_ind_changes).pack(side=tk.LEFT, padx=2)
  941. tk.Button(editor_controls, text="Redo", command=redo_ind_changes).pack(side=tk.LEFT, padx=2)
  942. tk.Button(editor_controls, text="Save", command=save_ind_file).pack(side=tk.LEFT, padx=2)
  943.  
  944. # Search functionality
  945. search_frame = tk.Frame(ind_frame)
  946. search_frame.pack(fill=tk.X, pady=(0, 5))
  947.  
  948. tk.Label(search_frame, text="Search:").pack(side=tk.LEFT, padx=(0, 5))
  949. search_var = tk.StringVar()
  950. search_entry = tk.Entry(search_frame, textvariable=search_var, width=40)
  951. search_entry.pack(side=tk.LEFT, fill=tk.X, expand=True)
  952.  
  953. search_button_frame = tk.Frame(search_frame)
  954. search_button_frame.pack(side=tk.LEFT, padx=(5, 0))
  955.  
  956. tk.Button(search_button_frame, text="ā–²", command=lambda: jump_to_prev_match(), width=2).pack(side=tk.LEFT)
  957. tk.Button(search_button_frame, text="ā–¼", command=lambda: jump_to_next_match(), width=2).pack(side=tk.LEFT)
  958.  
  959. # Text widget with scrollbars
  960. text_frame = tk.Frame(ind_frame)
  961. text_frame.pack(fill=tk.BOTH, expand=True)
  962.  
  963. ind_text = tk.Text(text_frame, wrap=tk.NONE, width=80, height=15, undo=True, autoseparators=True, maxundo=-1)
  964. ind_text.pack(side=tk.LEFT, fill=tk.BOTH, expand=True)
  965.  
  966. y_scroll = tk.Scrollbar(text_frame, command=ind_text.yview)
  967. y_scroll.pack(side=tk.RIGHT, fill=tk.Y)
  968. ind_text.config(yscrollcommand=y_scroll.set)
  969.  
  970. # Population selection buttons
  971. button_frame = tk.Frame(ind_frame)
  972. button_frame.pack(fill=tk.X, pady=(5, 0))
  973.  
  974. def get_selected_populations():
  975. try:
  976. sel_start = ind_text.index(tk.SEL_FIRST)
  977. sel_end = ind_text.index(tk.SEL_LAST)
  978. selected_text = ind_text.get(sel_start, sel_end)
  979. selected_lines = selected_text.split('\n')
  980. except:
  981. current_line = ind_text.index(tk.INSERT).split('.')[0]
  982. selected_lines = [ind_text.get(f"{current_line}.0", f"{current_line}.end")]
  983.  
  984. pops = set()
  985. for line in selected_lines:
  986. parts = line.strip().split()
  987. if len(parts) >= 3:
  988. pops.add(parts[2])
  989. return sorted(pops)
  990.  
  991. tk.Button(button_frame, text="Add to Target", command=add_to_target).pack(side=tk.LEFT, padx=5, expand=True)
  992. tk.Button(button_frame, text="Add to Left", command=add_to_left).pack(side=tk.LEFT, padx=5, expand=True)
  993. tk.Button(button_frame, text="Add to Right", command=add_to_right).pack(side=tk.LEFT, padx=5, expand=True)
  994. tk.Button(button_frame, text="Add to Fixed References", command=add_to_fixed_right).pack(side=tk.LEFT, padx=5, expand=True)
  995. tk.Button(button_frame, text="Add to Fixed Sources", command=add_to_fixed_left).pack(side=tk.LEFT, padx=5, expand=True)
  996. tk.Button(button_frame, text="Add to Rotational Pool", command=add_to_rotation_pool).pack(side=tk.LEFT, padx=5, expand=True)
  997.  
  998. def load_ind_file():
  999. folder_path = prefix_entry_var.get().strip()
  1000. if not folder_path:
  1001. status_label.config(text="Dataset folder is empty. No .ind file loaded.")
  1002. return
  1003.  
  1004. parent_folder = os.path.dirname(folder_path)
  1005. last_folder_name = os.path.basename(folder_path)
  1006. ind_file_name = f"{last_folder_name}.ind"
  1007. ind_path = os.path.join(parent_folder, ind_file_name)
  1008.  
  1009. if not os.path.isfile(ind_path):
  1010. status_label.config(text=f"No .ind file named '{ind_file_name}' found in parent folder.")
  1011. return
  1012.  
  1013. try:
  1014. with open(ind_path, 'r') as f:
  1015. content = f.read()
  1016. ind_text.delete('1.0', tk.END)
  1017. ind_text.insert(tk.END, content)
  1018. status_label.config(text=f".ind file loaded: {ind_file_name}")
  1019. except Exception as e:
  1020. status_label.config(text=f"Error loading .ind file: {str(e)}")
  1021.  
  1022. def on_prefix_change(*args):
  1023. load_ind_file()
  1024.  
  1025. prefix_entry_var.trace_add('write', on_prefix_change)
  1026.  
  1027. # Search functions
  1028. def search_ind_file(*args):
  1029. search_term = search_var.get()
  1030. if not search_term:
  1031. ind_text.tag_remove('highlight', '1.0', tk.END)
  1032. ind_text.tag_remove('current_match', '1.0', tk.END)
  1033. return
  1034.  
  1035. ind_text.tag_remove('highlight', '1.0', tk.END)
  1036. ind_text.tag_remove('current_match', '1.0', tk.END)
  1037.  
  1038. start_pos = '1.0'
  1039. count_var = tk.IntVar()
  1040. matches = []
  1041.  
  1042. while True:
  1043. pos = ind_text.search(search_term, start_pos, stopindex=tk.END, count=count_var, nocase=1)
  1044. if not pos: break
  1045. end_pos = f"{pos}+{count_var.get()}c"
  1046. matches.append((pos, end_pos))
  1047. ind_text.tag_add('highlight', pos, end_pos)
  1048. start_pos = end_pos
  1049.  
  1050. ind_text.tag_config('highlight', background='yellow', foreground='black')
  1051. ind_text.tag_config('current_match', background='orange', foreground='black')
  1052.  
  1053. if hasattr(ind_text, 'search_matches'):
  1054. del ind_text.search_matches
  1055. ind_text.search_matches = matches
  1056. ind_text.current_match = -1
  1057.  
  1058. if matches:
  1059. jump_to_match(0)
  1060.  
  1061. def jump_to_match(index):
  1062. if not hasattr(ind_text, 'search_matches') or not ind_text.search_matches:
  1063. return
  1064.  
  1065. matches = ind_text.search_matches
  1066. if 0 <= ind_text.current_match < len(matches):
  1067. prev_pos, prev_end = matches[ind_text.current_match]
  1068. ind_text.tag_add('highlight', prev_pos, prev_end)
  1069.  
  1070. ind_text.current_match = index % len(matches)
  1071. pos, end_pos = matches[ind_text.current_match]
  1072.  
  1073. ind_text.tag_add('current_match', pos, end_pos)
  1074. ind_text.mark_set(tk.INSERT, pos)
  1075. ind_text.see(pos)
  1076.  
  1077. def jump_to_next_match(event=None):
  1078. if hasattr(ind_text, 'search_matches') and ind_text.search_matches:
  1079. jump_to_match(ind_text.current_match + 1)
  1080.  
  1081. def jump_to_prev_match(event=None):
  1082. if hasattr(ind_text, 'search_matches') and ind_text.search_matches:
  1083. jump_to_match(ind_text.current_match - 1)
  1084.  
  1085. search_var.trace_add('write', search_ind_file)
  1086. search_entry.bind("<Return>", jump_to_next_match)
  1087. search_entry.bind("<Down>", jump_to_next_match)
  1088. search_entry.bind("<Up>", jump_to_prev_match)
  1089.  
  1090. fixed_left_var = tk.StringVar()
  1091. fixed_right_var = tk.StringVar()
  1092. rotation_pool_var = tk.StringVar()
  1093.  
  1094. # Add this near the other search functions (around line 1000)
  1095. def setup_output_search():
  1096. # Create search frame for output console
  1097. output_search_frame = tk.Frame(scrollable_frame)
  1098. output_search_frame.grid(row=9, column=0, columnspan=3, sticky='we', padx=5, pady=2)
  1099.  
  1100. tk.Label(output_search_frame, text="Output Search:").pack(side=tk.LEFT, padx=(0, 5))
  1101. output_search_var = tk.StringVar()
  1102. output_search_entry = tk.Entry(output_search_frame, textvariable=output_search_var, width=40)
  1103. output_search_entry.pack(side=tk.LEFT, fill=tk.X, expand=True)
  1104.  
  1105. output_search_button_frame = tk.Frame(output_search_frame)
  1106. output_search_button_frame.pack(side=tk.LEFT, padx=(5, 0))
  1107.  
  1108. tk.Button(output_search_button_frame, text="ā–²", command=lambda: jump_to_prev_output_match(), width=2).pack(side=tk.LEFT)
  1109. tk.Button(output_search_button_frame, text="ā–¼", command=lambda: jump_to_next_output_match(), width=2).pack(side=tk.LEFT)
  1110.  
  1111. def search_output(*args):
  1112. search_term = output_search_var.get()
  1113. if not search_term:
  1114. output_text.tag_remove('output_highlight', '1.0', tk.END)
  1115. output_text.tag_remove('output_current_match', '1.0', tk.END)
  1116. return
  1117.  
  1118. output_text.tag_remove('output_highlight', '1.0', tk.END)
  1119. output_text.tag_remove('output_current_match', '1.0', tk.END)
  1120.  
  1121. start_pos = '1.0'
  1122. count_var = tk.IntVar()
  1123. matches = []
  1124.  
  1125. while True:
  1126. pos = output_text.search(search_term, start_pos, stopindex=tk.END, count=count_var, nocase=1)
  1127. if not pos: break
  1128. end_pos = f"{pos}+{count_var.get()}c"
  1129. matches.append((pos, end_pos))
  1130. output_text.tag_add('output_highlight', pos, end_pos)
  1131. start_pos = end_pos
  1132.  
  1133. output_text.tag_config('output_highlight', background='yellow', foreground='black')
  1134. output_text.tag_config('output_current_match', background='orange', foreground='black')
  1135.  
  1136. if hasattr(output_text, 'output_search_matches'):
  1137. del output_text.output_search_matches
  1138. output_text.output_search_matches = matches
  1139. output_text.output_current_match = -1
  1140.  
  1141. if matches:
  1142. jump_to_output_match(0)
  1143.  
  1144. def jump_to_output_match(index):
  1145. if not hasattr(output_text, 'output_search_matches') or not output_text.output_search_matches:
  1146. return
  1147.  
  1148. matches = output_text.output_search_matches
  1149. if 0 <= output_text.output_current_match < len(matches):
  1150. prev_pos, prev_end = matches[output_text.output_current_match]
  1151. output_text.tag_add('output_highlight', prev_pos, prev_end)
  1152.  
  1153. output_text.output_current_match = index % len(matches)
  1154. pos, end_pos = matches[output_text.output_current_match]
  1155.  
  1156. output_text.tag_add('output_current_match', pos, end_pos)
  1157. output_text.mark_set(tk.INSERT, pos)
  1158. output_text.see(pos)
  1159.  
  1160. def jump_to_next_output_match(event=None):
  1161. if hasattr(output_text, 'output_search_matches') and output_text.output_search_matches:
  1162. jump_to_output_match(output_text.output_current_match + 1)
  1163.  
  1164. def jump_to_prev_output_match(event=None):
  1165. if hasattr(output_text, 'output_search_matches') and output_text.output_search_matches:
  1166. jump_to_output_match(output_text.output_current_match - 1)
  1167.  
  1168. output_search_var.trace_add('write', search_output)
  1169. output_search_entry.bind("<Return>", jump_to_next_output_match)
  1170. output_search_entry.bind("<Down>", jump_to_next_output_match)
  1171. output_search_entry.bind("<Up>", jump_to_prev_output_match)
  1172.  
  1173. # --- ROTATION PARAMETERS ---
  1174. rotation_frame = tk.LabelFrame(scrollable_frame, text="qpAdm Rotation Parameters", padx=5, pady=5)
  1175. rotation_frame.grid(row=6, column=0, columnspan=3, sticky='we', padx=5, pady=10)
  1176.  
  1177. tk.Label(rotation_frame, text="Fixed References (Right):").grid(row=0, column=0, sticky='w', padx=5)
  1178. fixed_right_entry = tk.Entry(rotation_frame, width=150, textvariable=fixed_right_var)
  1179. fixed_right_entry.grid(row=0, column=1, padx=5)
  1180.  
  1181. tk.Label(rotation_frame, text="Fixed Sources (Left - Optional):").grid(row=1, column=0, sticky='w', padx=5)
  1182. fixed_left_entry = tk.Entry(rotation_frame, width=150, textvariable=fixed_left_var)
  1183. fixed_left_entry.grid(row=1, column=1, padx=5)
  1184.  
  1185. tk.Label(rotation_frame, text="Rotational Pool:").grid(row=2, column=0, sticky='w', padx=5)
  1186. rotation_pool_entry = tk.Entry(rotation_frame, width=150, textvariable=rotation_pool_var)
  1187. rotation_pool_entry.grid(row=2, column=1, padx=5)
  1188.  
  1189. # Editor functions for each field
  1190. def open_fixed_left_editor():
  1191. create_synced_editor(
  1192. title="Edit Fixed Left Populations",
  1193. textvariable=fixed_left_var,
  1194. entry_widget=fixed_left_entry
  1195. )
  1196.  
  1197. def open_fixed_right_editor():
  1198. create_synced_editor(
  1199. title="Edit Fixed Right Populations",
  1200. textvariable=fixed_right_var,
  1201. entry_widget=fixed_right_entry
  1202. )
  1203.  
  1204. def open_rotation_pool_editor():
  1205. create_synced_editor(
  1206. title="Edit Rotation Pool Populations",
  1207. textvariable=rotation_pool_var,
  1208. entry_widget=rotation_pool_entry
  1209. )
  1210.  
  1211. # Generic editor creation function
  1212. def create_synced_editor(title, textvariable, entry_widget):
  1213. top = tk.Toplevel(root)
  1214. top.title(title)
  1215. top.geometry("800x400")
  1216. top.resizable(True, True)
  1217.  
  1218. # Keep window on top
  1219. def keep_on_top():
  1220. top.attributes("-topmost", True)
  1221. top.after(1000, keep_on_top)
  1222. keep_on_top()
  1223.  
  1224. text = tk.Text(top, wrap=tk.WORD, font=("Consolas", 12))
  1225. text.pack(expand=True, fill="both")
  1226. text.insert("1.0", textvariable.get())
  1227.  
  1228. # Synchronization variables
  1229. syncing_from_popup = False
  1230. syncing_from_main = False
  1231.  
  1232. # Pop-up → Main Entry sync
  1233. def sync_popup_to_entry(event=None):
  1234. nonlocal syncing_from_popup
  1235. syncing_from_popup = True
  1236. textvariable.set(text.get("1.0", "end-1c"))
  1237. syncing_from_popup = False
  1238.  
  1239. # Main Entry → Pop-up sync
  1240. def sync_entry_to_popup(*args):
  1241. nonlocal syncing_from_main
  1242. if not syncing_from_popup:
  1243. syncing_from_main = True
  1244. current_value = textvariable.get()
  1245. if current_value != text.get("1.0", "end-1c"):
  1246. text.delete("1.0", tk.END)
  1247. text.insert("1.0", current_value)
  1248. syncing_from_main = False
  1249.  
  1250. # Set up event bindings
  1251. text.bind("<KeyRelease>", sync_popup_to_entry)
  1252. trace_id = textvariable.trace_add("write", sync_entry_to_popup)
  1253.  
  1254. # Close handler
  1255. def on_close():
  1256. textvariable.trace_remove("write", trace_id)
  1257. top.destroy()
  1258.  
  1259. # Close button
  1260. tk.Button(top, text="Close", command=on_close).pack(pady=5)
  1261. top.protocol("WM_DELETE_WINDOW", on_close)
  1262.  
  1263.  
  1264. # Add buttons next to the rotation parameters entries
  1265. def create_editor_button(parent, row, column, command):
  1266. btn = tk.Button(parent, text="šŸ”•", width=3, command=command)
  1267. btn.grid(row=row, column=column, padx=(0, 5))
  1268. return btn
  1269.  
  1270. # Fixed Left Editor
  1271. create_editor_button(rotation_frame, 1, 2, open_fixed_left_editor)
  1272. # Fixed Right Editor
  1273. create_editor_button(rotation_frame, 0, 2, open_fixed_right_editor)
  1274. # Rotation Pool Editor
  1275. create_editor_button(rotation_frame, 2, 2, open_rotation_pool_editor)
  1276.  
  1277.  
  1278. tk.Label(rotation_frame, text="Model Size (min-max):").grid(row=3, column=0, sticky='w', padx=5)
  1279. model_min_entry = tk.Entry(rotation_frame, width=5)
  1280. model_min_entry.grid(row=3, column=1, sticky='w', padx=5)
  1281. model_max_entry = tk.Entry(rotation_frame, width=5)
  1282. model_max_entry.grid(row=3, column=1, sticky='e', padx=5)
  1283.  
  1284. # Rotation mode entry
  1285. tk.Label(rotation_frame, text="Rotation Mode (D=Default, L=Left, R=Right):").grid(row=4, column=0, sticky='w', padx=5)
  1286. rotation_mode_entry = tk.Entry(rotation_frame, width=5)
  1287. rotation_mode_entry.insert(0, "D") # Default to D
  1288. rotation_mode_entry.grid(row=4, column=1, sticky='w', padx=5)
  1289.  
  1290. # Add validation to only allow 1 character
  1291. def validate_rotation_mode_input(new_text):
  1292. if len(new_text) > 1:
  1293. return False
  1294. return True
  1295.  
  1296. rotation_mode_entry.config(validate="key")
  1297. rotation_mode_entry.config(validatecommand=(rotation_mode_entry.register(validate_rotation_mode_input), '%P'))
  1298.  
  1299. # Add this function to save output to text file
  1300. def save_output_to_file():
  1301. content = output_text.get("1.0", tk.END)
  1302. if not content.strip():
  1303. messagebox.showerror("Error", "No output to save")
  1304. return
  1305.  
  1306. file_path = filedialog.asksaveasfilename(
  1307. defaultextension=".log",
  1308. filetypes=[("Text Files", "*.log"), ("All Files", "*.*")]
  1309. )
  1310.  
  1311. if file_path:
  1312. try:
  1313. # Add encoding='utf-8' here
  1314. with open(file_path, 'w', encoding='utf-8') as f:
  1315. f.write(content)
  1316. status_label.config(text=f"Output saved to {file_path}")
  1317. except Exception as e:
  1318. messagebox.showerror("Error", f"Failed to save file: {str(e)}")
  1319.  
  1320. def clear_output():
  1321. answer = messagebox.askyesno("Confirm Clear", "Are you sure you want to clear the output?")
  1322. if answer:
  1323. output_text.delete('1.0', tk.END)
  1324.  
  1325.  
  1326. # --- RUN BUTTONS ---
  1327. run_button = tk.Button(scrollable_frame, text="Run qpAdm", command=run_qpadm, bg="lightblue")
  1328. run_button.grid(row=7, column=0, columnspan=2, pady=10, sticky='we')
  1329.  
  1330. edit_run_button = tk.Button(scrollable_frame, text="Edit and Run R Code", command=edit_and_run_r_code, bg="lightyellow")
  1331. edit_run_button.grid(row=7, column=2, pady=10, sticky='we')
  1332.  
  1333. rotation_run_button = tk.Button(
  1334. scrollable_frame,
  1335. text="Run Rotation Analysis",
  1336. command=run_rotation,
  1337. bg="#90EE90"
  1338. )
  1339. rotation_run_button.grid(row=8, column=0, columnspan=3, pady=10, sticky='we')
  1340.  
  1341. # Create a frame for the buttons
  1342. button_frame = tk.Frame(scrollable_frame)
  1343. button_frame.grid(row=11, column=2, sticky='e', padx=10, pady=(0, 10)) # Modified row from 11 to 10
  1344.  
  1345. # Clear button
  1346. clear_button = tk.Button(button_frame, text="Clear", command=clear_output, bg="lightgray", width=10, height=1)
  1347. clear_button.pack(side=tk.LEFT, padx=5)
  1348.  
  1349.  
  1350. #Button
  1351. save_output_button = tk.Button(button_frame, text="šŸ’¾ Save", command=save_output_to_file, bg="lightgray", width=10, height=1)
  1352. save_output_button.pack(side=tk.LEFT)
  1353.  
  1354. # --- OUTPUT CONSOLE ---
  1355. output_text = scrolledtext.ScrolledText(scrollable_frame, width=150, height=30)
  1356. output_text.grid(row=10, column=0, columnspan=3, padx=10, pady=5, sticky='nsew')
  1357.  
  1358. scrollable_frame.grid_rowconfigure(7, weight=1)
  1359.  
  1360. # Status label
  1361. status_label = tk.Label(scrollable_frame, text="Ready.", anchor='w')
  1362. status_label.grid(row=11, column=0, columnspan=2, sticky='w', padx=10, pady=(0, 10)) # Modified line
  1363.  
  1364. # Column configuration
  1365. scrollable_frame.grid_columnconfigure(1, weight=1)
  1366. scrollable_frame.grid_columnconfigure(2, weight=0)
  1367.  
  1368. # --- Repeating Vertical Text Label on the Right ---
  1369. word = 'pepsimanfire'
  1370. repeat_count = 5 # You can increase this to make it go further down
  1371. vertical_text = '\n'.join(list(word) * repeat_count)
  1372.  
  1373. vertical_label = tk.Label(scrollable_frame, text=vertical_text, font=("Helvetica", 12, "bold"), fg="gray")
  1374. vertical_label.grid(row=0, column=3, rowspan=999, sticky='ns', padx=(60, 5), pady=10)
  1375.  
  1376. # Call this function after creating the output_text widget
  1377. setup_output_search()
  1378.  
  1379. root.mainloop()
Tags: admixtools
Advertisement
Add Comment
Please, Sign In to add comment
Advertisement