OSCR

Neuronal RNAi and oxygen-sensing circuit shape germline resilience to heat stress.

Code ↔ Paper

4 matches between paragraphs of the paper and lines of its authors' code, computed by the harvester (lexical-v1). Click a colored paragraph or line to see its counterpart.

The 4 matches · 1 of them tie a paragraph to a whole file, not to given lines: a weak match, whose lines are not tinted
  1. [1] § STAR★Methods › Method details › Bioinformatics ↔ rnalysis/gui/gui.py, lines 4304–4348 · score 0.72 · ShortStack, KEGG pathways, Gene Ontology, RNAlysis, enrichment
  2. [2] § STAR★Methods › Method details › Bioinformatics ↔ rnalysis/enrichment.py, lines 1082–1218 · score 0.61 · KEGG pathways, RNAlysis, FDR, Fisher, enrichment, GO
  3. [3] § STAR★Methods › Method details › RNA-seq ↔ rnalysis/fastq.py, lines 2006–2141 · score 0.60 · cDNA, mRNA, Illumina, libraries, sequence
  4. [4] § STAR★Methods › Method details › Locomotion assays ↔ rnalysis/data_files/report_templates/vis-network.min.js, the whole file · a weak match · score 0.52 · stamp, wt, edge, oC, intervals, min

Paper

Loaded from Europe PMC by your browser, not stored by OSCR: doi.org · Europe PMC

The paper is loaded when this pane is shown.

The authors' code

Python · 4,591 lines · 195 KB · MIT · 1 match

  1. import builtins
  2. import copy
  3. import functools
  4. import hashlib
  5. import importlib
  6. import itertools
  7. import os
  8. import platform
  9. import sys
  10. import time
  11. import typing
  12. import warnings
  13. from collections import OrderedDict
  14. from pathlib import Path
  15. from queue import Queue
  16. from typing import Callable, List, Tuple, Type, Union
  17. import matplotlib
  18. import matplotlib.pyplot as plt
  19. import numpy as np
  20. import polars as pl
  21. import yaml
  22. from PyQt6 import QtCore, QtGui, QtWidgets
  23. from rnalysis import __version__, enrichment, fastq, filtering
  24. from rnalysis.exceptions import InternalError, InvalidValueError
  25. from rnalysis.gui import gui_graphics, gui_quickstart, gui_style, gui_widgets, gui_windows
  26. from rnalysis.utils import clustering, generic, io, parsing, settings, validation
  27. FILTER_OBJ_TYPES = {
  28. 'Count matrix': filtering.CountFilter,
  29. 'Differential expression': filtering.DESeqFilter,
  30. 'Fold change': filtering.FoldChangeFilter,
  31. 'Other table': filtering.Filter,
  32. }
  33. FILTER_OBJ_TYPES_INV = {val.__name__: key for key, val in FILTER_OBJ_TYPES.items()}
  34. INIT_EXCLUDED_PARAMS = {'self', 'fname', 'suppress_warnings'}
  35. JOB_COUNTER = gui_widgets.JobCounter()
  36. ORIG_TQDM = filtering.tqdm
  37. ORIG_PARALLEL = generic.ProgressParallel
  38. def check_run_success(result: gui_widgets.WorkerOutput):
  39. if result.raised_exception:
  40. raise result.raised_exception
  41. class BarPlotWindow(gui_windows.FuncExternalWindow):
  42. EXCLUDED_PARAMS = set()
  43. __slots__ = {}
  44. def __init__(self, parent=None):
  45. func = enrichment.enrichment_bar_plot
  46. help_link = f'https://guyteichman.github.io/RNAlysis/build/rnalysis.enrichment.{func.__name__}.html'
  47. super().__init__('Enrichment bar-plot', func, help_link, self.EXCLUDED_PARAMS, threaded=False, parent=parent)
  48. self.init_ui()
  49. self.setWindowTitle('Create enrichment bar-plot')
  50. class OntologyGraphWindow(gui_windows.FuncExternalWindow):
  51. EXCLUDED_PARAMS = set()
  52. __slots__ = {}
  53. def __init__(self, parent=None):
  54. func = enrichment.gene_ontology_graph
  55. help_link = f'https://guyteichman.github.io/RNAlysis/build/rnalysis.fastq.{func.__name__}.html'
  56. super().__init__('Gene Ontology graph', func, help_link, self.EXCLUDED_PARAMS, threaded=False, parent=parent)
  57. self.init_ui()
  58. self.setWindowTitle('Plot Gene Ontology Graph')
  59. class PathwayGraphWindow(gui_windows.FuncExternalWindow):
  60. EXCLUDED_PARAMS = set()
  61. __slots__ = {}
  62. def __init__(self, parent=None):
  63. func = enrichment.kegg_pathway_graph
  64. help_link = f'https://guyteichman.github.io/RNAlysis/build/rnalysis.fastq.{func.__name__}.html'
  65. super().__init__('KEGG Pathway graph', func, help_link, self.EXCLUDED_PARAMS, threaded=False, parent=parent)
  66. self.init_ui()
  67. self.setWindowTitle('Plot KEGG Pathway Graph')
  68. class FeatureCountsSingleWindow(gui_windows.FuncExternalWindow):
  69. EXCLUDED_PARAMS = set()
  70. __slots__ = {}
  71. def __init__(self, parent=None):
  72. func = fastq.featurecounts_single_end
  73. help_link = f'https://guyteichman.github.io/RNAlysis/build/rnalysis.fastq.{func.__name__}.html'
  74. super().__init__(func.readable_name, func, help_link, self.EXCLUDED_PARAMS, parent=parent)
  75. self.init_ui()
  76. self.setWindowTitle('featureCounts single-end counting setup')
  77. class SamToFastqSingleWindow(gui_windows.FuncExternalWindow):
  78. EXCLUDED_PARAMS = set()
  79. __slots__ = {}
  80. def __init__(self, parent=None):
  81. func = fastq.sam_to_fastq_single
  82. help_link = f'https://guyteichman.github.io/RNAlysis/build/rnalysis.fastq.{func.__name__}.html'
  83. super().__init__(func.readable_name, func, help_link, self.EXCLUDED_PARAMS, parent=parent)
  84. self.init_ui()
  85. self.setWindowTitle('Convert SAM/BAM to FASTQ (single-end)')
  86. class SamToFastqPairedWindow(gui_windows.FuncExternalWindow):
  87. EXCLUDED_PARAMS = set()
  88. __slots__ = {}
  89. def __init__(self, parent=None):
  90. func = fastq.sam_to_fastq_paired
  91. help_link = f'https://guyteichman.github.io/RNAlysis/build/rnalysis.fastq.{func.__name__}.html'
  92. super().__init__(func.readable_name, func, help_link, self.EXCLUDED_PARAMS, parent=parent)
  93. self.init_ui()
  94. self.setWindowTitle('Convert SAM/BAM to FASTQ (paired-end)')
  95. class FastqToSamSingleWindow(gui_windows.FuncExternalWindow):
  96. EXCLUDED_PARAMS = {'self', 'return_new_filenames', 'legacy_args'}
  97. __slots__ = {}
  98. def __init__(self, parent=None):
  99. func = fastq.fastq_to_sam_single
  100. help_link = f'https://guyteichman.github.io/RNAlysis/build/rnalysis.fastq.{func.__name__}.html'
  101. super().__init__(func.readable_name, func, help_link, self.EXCLUDED_PARAMS, parent=parent)
  102. self.init_ui()
  103. self.setWindowTitle('Convert FASTQ to SAM/BAM (single-end)')
  104. class FastqToSamPairedWindow(gui_windows.PairedFuncExternalWindow):
  105. __slots__ = {}
  106. def __init__(self, parent=None):
  107. func = fastq.fastq_to_sam_paired
  108. help_link = f'https://guyteichman.github.io/RNAlysis/build/rnalysis.fastq.{func.__name__}.html'
  109. super().__init__(func.readable_name, func, help_link, self.EXCLUDED_PARAMS, parent=parent)
  110. self.init_ui()
  111. self.setWindowTitle('Convert FASTQ to SAM/BAM (paired-end)')
  112. class ConvertSamFormatWindow(gui_windows.FuncExternalWindow):
  113. EXCLUDED_PARAMS = set()
  114. __slots__ = {}
  115. def __init__(self, parent=None):
  116. func = fastq.convert_sam_format
  117. help_link = f'https://guyteichman.github.io/RNAlysis/build/rnalysis.fastq.{func.__name__}.html'
  118. super().__init__(func.readable_name, func, help_link, self.EXCLUDED_PARAMS, parent=parent)
  119. self.init_ui()
  120. self.setWindowTitle('Convert SAM/BAM format')
  121. class BamIndexWindow(gui_windows.FuncExternalWindow):
  122. EXCLUDED_PARAMS = set()
  123. __slots__ = {}
  124. def __init__(self, parent=None):
  125. func = fastq.create_bam_index
  126. help_link = f'https://guyteichman.github.io/RNAlysis/build/rnalysis.fastq.{func.__name__}.html'
  127. super().__init__('Create BAM index', func, help_link, self.EXCLUDED_PARAMS, parent=parent)
  128. self.init_ui()
  129. self.setWindowTitle('Create BAM index')
  130. class ValidateSamWindow(gui_windows.FuncExternalWindow):
  131. EXCLUDED_PARAMS = set()
  132. __slots__ = {}
  133. def __init__(self, parent=None):
  134. func = fastq.validate_sam
  135. help_link = f'https://guyteichman.github.io/RNAlysis/build/rnalysis.fastq.{func.__name__}.html'
  136. super().__init__('Validate SAM/BAM file', func, help_link, self.EXCLUDED_PARAMS, parent=parent)
  137. self.init_ui()
  138. self.setWindowTitle('Validate SAM/BAM file')
  139. class SortSamWindow(gui_windows.FuncExternalWindow):
  140. EXCLUDED_PARAMS = set()
  141. __slots__ = {}
  142. def __init__(self, parent=None):
  143. func = fastq.sort_sam
  144. help_link = f'https://guyteichman.github.io/RNAlysis/build/rnalysis.fastq.{func.__name__}.html'
  145. super().__init__('Sort SAM/BAM file', func, help_link, self.EXCLUDED_PARAMS, parent=parent)
  146. self.init_ui()
  147. self.setWindowTitle('Sort SAM/BAM file')
  148. class FindDuplicatesWindow(gui_windows.FuncExternalWindow):
  149. EXCLUDED_PARAMS = set()
  150. __slots__ = {}
  151. def __init__(self, parent=None):
  152. func = fastq.find_duplicates
  153. help_link = f'https://guyteichman.github.io/RNAlysis/build/rnalysis.fastq.{func.__name__}.html'
  154. super().__init__('Find duplicate reads', func, help_link, self.EXCLUDED_PARAMS, parent=parent)
  155. self.init_ui()
  156. self.setWindowTitle('Find duplicate reads')
  157. class FeatureCountsPairedWindow(gui_windows.FuncExternalWindow):
  158. EXCLUDED_PARAMS = set()
  159. __slots__ = {}
  160. def __init__(self, parent=None):
  161. func = fastq.featurecounts_paired_end
  162. help_link = f'https://guyteichman.github.io/RNAlysis/build/rnalysis.fastq.{func.__name__}.html'
  163. super().__init__(func.readable_name, func, help_link, self.EXCLUDED_PARAMS, parent=parent)
  164. self.init_ui()
  165. self.setWindowTitle('featureCounts paired-end counting setup')
  166. class Bowtie2IndexWindow(gui_windows.FuncExternalWindow):
  167. EXCLUDED_PARAMS = set()
  168. __slots__ = {}
  169. def __init__(self, parent=None):
  170. func = fastq.bowtie2_create_index
  171. help_link = f'https://guyteichman.github.io/RNAlysis/build/rnalysis.fastq.{func.__name__}.html'
  172. super().__init__(func.readable_name, func, help_link, self.EXCLUDED_PARAMS, parent=parent)
  173. self.init_ui()
  174. self.setWindowTitle('Bowtie2 - build genome index')
  175. class Bowtie2SingleWindow(gui_windows.FuncExternalWindow):
  176. EXCLUDED_PARAMS = set()
  177. __slots__ = {}
  178. def __init__(self, parent=None):
  179. func = fastq.bowtie2_align_single_end
  180. help_link = f'https://guyteichman.github.io/RNAlysis/build/rnalysis.fastq.{func.__name__}.html'
  181. super().__init__(func.readable_name, func, help_link, self.EXCLUDED_PARAMS, parent=parent)
  182. self.init_ui()
  183. self.setWindowTitle('Bowtie2 single-end alignment setup')
  184. class Bowtie2PairedWindow(gui_windows.PairedFuncExternalWindow):
  185. __slots__ = {}
  186. def __init__(self, parent=None):
  187. func = fastq.bowtie2_align_paired_end
  188. help_link = f'https://guyteichman.github.io/RNAlysis/build/rnalysis.fastq.{func.__name__}.html'
  189. super().__init__(func.readable_name, func, help_link, self.EXCLUDED_PARAMS, parent)
  190. self.init_ui()
  191. self.setWindowTitle('Bowtie2 paired-end alignment setup')
  192. class ShortStackWindow(gui_windows.FuncExternalWindow):
  193. EXCLUDED_PARAMS = set()
  194. __slots__ = {}
  195. def __init__(self, parent=None):
  196. func = fastq.shortstack_align_smallrna
  197. help_link = f'https://guyteichman.github.io/RNAlysis/build/rnalysis.fastq.{func.__name__}.html'
  198. super().__init__(func.readable_name, func, help_link, self.EXCLUDED_PARAMS, parent=parent)
  199. self.init_ui()
  200. self.setWindowTitle('ShortStack small RNA alignment setup')
  201. class KallistoIndexWindow(gui_windows.FuncExternalWindow):
  202. EXCLUDED_PARAMS = set()
  203. __slots__ = {}
  204. def __init__(self, parent=None):
  205. func = fastq.kallisto_create_index
  206. help_link = f'https://guyteichman.github.io/RNAlysis/build/rnalysis.fastq.{func.__name__}.html'
  207. super().__init__(func.readable_name, func, help_link, self.EXCLUDED_PARAMS, parent=parent)
  208. self.init_ui()
  209. self.setWindowTitle('Kallisto - build transcriptome index')
  210. class KallistoSingleWindow(gui_windows.FuncExternalWindow):
  211. EXCLUDED_PARAMS = {'legacy_args'}
  212. __slots__ = {}
  213. def __init__(self, parent=None):
  214. func = fastq.kallisto_quantify_single_end
  215. help_link = f'https://guyteichman.github.io/RNAlysis/build/rnalysis.fastq.{func.__name__}.html'
  216. super().__init__(func.readable_name, func, help_link, self.EXCLUDED_PARAMS, parent=parent)
  217. self.init_ui()
  218. self.setWindowTitle('Kallisto single-end quantification setup')
  219. class KallistoPairedWindow(gui_windows.PairedFuncExternalWindow):
  220. EXCLUDED_PARAMS = gui_windows.PairedFuncExternalWindow.EXCLUDED_PARAMS | {'legacy_args'}
  221. __slots__ = {}
  222. def __init__(self, parent=None):
  223. func = fastq.kallisto_quantify_paired_end
  224. help_link = f'https://guyteichman.github.io/RNAlysis/build/rnalysis.fastq.{func.__name__}.html'
  225. super().__init__(func.readable_name, func, help_link, self.EXCLUDED_PARAMS, parent)
  226. self.init_ui()
  227. self.setWindowTitle('Kallisto paired-end quantification setup')
  228. class CutAdaptSingleWindow(gui_windows.FuncExternalWindow):
  229. EXCLUDED_PARAMS = set()
  230. __slots__ = {}
  231. def __init__(self, parent=None):
  232. func = fastq.trim_adapters_single_end
  233. help_link = f'https://guyteichman.github.io/RNAlysis/build/rnalysis.fastq.{func.__name__}.html'
  234. super().__init__(func.readable_name, func, help_link, self.EXCLUDED_PARAMS, parent=parent)
  235. self.init_ui()
  236. self.setWindowTitle('CutAdapt single-end adapter trimming setup')
  237. class CutAdaptPairedWindow(gui_windows.PairedFuncExternalWindow):
  238. EXCLUDED_PARAMS = gui_windows.PairedFuncExternalWindow.EXCLUDED_PARAMS.copy()
  239. EXCLUDED_PARAMS.add('return_new_filenames')
  240. __slots__ = {}
  241. def __init__(self, parent=None):
  242. func = fastq.trim_adapters_paired_end
  243. help_link = f'https://guyteichman.github.io/RNAlysis/build/rnalysis.fastq.{func.__name__}.html'
  244. super().__init__(func.readable_name, func, help_link, self.EXCLUDED_PARAMS, parent)
  245. self.init_ui()
  246. self.setWindowTitle('CutAdapt paired-end adapter trimming setup')
  247. class DiffExpWindow(gui_windows.FuncExternalWindow):
  248. EXCLUDED_PARAMS = {
  249. 'self',
  250. 'comparisons',
  251. 'covariates',
  252. 'lrt_factors',
  253. 'model_factors',
  254. 'return_design_matrix',
  255. 'return_code',
  256. 'return_log',
  257. }
  258. IGNORED_WIDGETS = gui_windows.FuncExternalWindow.IGNORED_WIDGETS | {'load_design'}
  259. __slots__ = {
  260. 'comparisons': 'list of comparisons to make',
  261. 'design_mat': 'design matrix',
  262. 'comparisons_group': 'widget group for choosing comparisons',
  263. 'comparisons_grid': 'layout for choosing comparisons',
  264. 'comparisons_widgets': 'widgets for choosing comparisons',
  265. 'simplified': 'show simplified view',
  266. }
  267. def __init__(self, func: Callable, name: str, parent=None, simplified: bool = False):
  268. self.name = name
  269. help_link = f'https://guyteichman.github.io/RNAlysis/build/rnalysis.filtering.CountFilter.{func.__name__}.html'
  270. super().__init__(name, func, help_link, self.EXCLUDED_PARAMS, parent=parent)
  271. self.simplified = simplified
  272. self.comparisons = []
  273. self.covariates = []
  274. self.design_mat = None
  275. self.righthand_widget = QtWidgets.QWidget(self)
  276. self.righthand_layout = QtWidgets.QVBoxLayout(self.righthand_widget)
  277. cmp_title = '2. Choose pairwise comparisons' if self.simplified else '2. Choose pairwise comparisons (optional)'
  278. self.comparisons_group = QtWidgets.QGroupBox(cmp_title)
  279. self.comparisons_grid = QtWidgets.QGridLayout(self.comparisons_group)
  280. self.comparisons_widgets = {}
  281. self.covariates_group = QtWidgets.QGroupBox('3. Choose covariates to test (optional)')
  282. self.covariates_grid = QtWidgets.QGridLayout(self.covariates_group)
  283. self.covariates_widgets = {}
  284. self.lrt_group = QtWidgets.QGroupBox('4. Choose factors for Likelihood Ratio tests (optional)')
  285. self.lrt_grid = QtWidgets.QGridLayout(self.lrt_group)
  286. self.lrt_widgets = {}
  287. self.init_ui()
  288. def init_ui(self):
  289. self.setWindowTitle(f'{self.name} differential expression setup')
  290. self.scroll_layout.addWidget(self.righthand_widget, 0, 1)
  291. self.righthand_layout.addWidget(self.comparisons_group)
  292. super().init_ui()
  293. if self.simplified:
  294. _, _, width, height = self.scroll.geometry().getRect()
  295. self.resize(1200, height)
  296. else:
  297. self.righthand_layout.addWidget(self.covariates_group)
  298. self.righthand_layout.addWidget(self.lrt_group)
  299. self.resize(1200, 800)
  300. def init_param_ui(self):
  301. super().init_param_ui()
  302. self.param_widgets['load_design'] = QtWidgets.QPushButton('Load design matrix')
  303. self.param_widgets['load_design'].setEnabled(False)
  304. self.param_widgets['load_design'].clicked.connect(self.init_righthand_uis)
  305. self.param_grid.addWidget(self.param_widgets['load_design'], self.param_grid.rowCount(), 0, 1, 2)
  306. self.param_widgets['design_matrix'].textChanged.connect(self._change_accept_button_state)
  307. def _change_accept_button_state(self, is_legal: bool):
  308. self.param_widgets['load_design'].setEnabled(is_legal)
  309. def init_righthand_uis(self):
  310. self.init_design_mat()
  311. self.init_comparisons_ui()
  312. if not self.simplified:
  313. self.init_covariates_ui()
  314. self.init_lrt_ui()
  315. def init_design_mat(self):
  316. design_mat = io.load_table(self.param_widgets['design_matrix'].text())
  317. for factor in design_mat.columns:
  318. if parsing.slugify(factor) != factor:
  319. raise InvalidValueError(
  320. f"Invalid factor name '{factor}': contains invalid characters."
  321. f" \nSuggested alternative name: '{parsing.slugify(factor)}'. "
  322. )
  323. self.design_mat = design_mat
  324. def init_model_ui(self):
  325. raise NotImplementedError
  326. def init_comparisons_ui(self):
  327. if 'picker' in self.comparisons_widgets:
  328. self.comparisons_grid.removeWidget(self.comparisons_widgets['picker'])
  329. self.comparisons_widgets['picker'].deleteLater()
  330. self.comparisons_widgets['picker'] = gui_widgets.ComparisonPickerGroup(self.design_mat, self)
  331. self.comparisons_grid.addWidget(self.comparisons_widgets['picker'], 0, 0)
  332. def init_covariates_ui(self):
  333. if 'picker' in self.covariates_widgets:
  334. self.covariates_grid.removeWidget(self.covariates_widgets['picker'])
  335. self.covariates_widgets['picker'].deleteLater()
  336. self.covariates_widgets['picker'] = gui_widgets.CovariatePickerGroup(self.design_mat, self)
  337. self.covariates_grid.addWidget(self.covariates_widgets['picker'], 0, 0)
  338. def init_lrt_ui(self):
  339. if 'picker' in self.lrt_widgets:
  340. self.lrt_grid.removeWidget(self.lrt_widgets['picker'])
  341. self.lrt_widgets['picker'].deleteLater()
  342. self.lrt_widgets['picker'] = gui_widgets.LRTPickerGroup(self.design_mat, self)
  343. self.lrt_grid.addWidget(self.lrt_widgets['picker'], 0, 0)
  344. def get_analysis_kwargs(self):
  345. kwargs = super().get_analysis_kwargs()
  346. kwargs['return_design_matrix'] = True
  347. kwargs['return_code'] = True
  348. kwargs['return_log'] = True
  349. kwargs['comparisons'] = self.comparisons_widgets['picker'].get_comparison_values()
  350. if not self.simplified:
  351. kwargs['covariates'] = self.covariates_widgets['picker'].get_comparison_values()
  352. kwargs['lrt_factors'] = self.lrt_widgets['picker'].get_comparison_values()
  353. return kwargs
  354. def import_parameters(self):
  355. params = super().import_parameters()
  356. self.init_righthand_uis()
  357. kwargs = params['kwargs']
  358. comparisons = kwargs.get('comparisons', [])
  359. covariates = kwargs.get('covariates', [])
  360. lrt_factors = kwargs.get('lrt_factors', [])
  361. if 'picker' in self.comparisons_widgets:
  362. self.comparisons_widgets['picker'].set_comparison_values(comparisons)
  363. if self.simplified:
  364. # a parameter file exported from the full window is a user-supplied artifact, so an
  365. # unsupported entry in it is bad input - not an RNAlysis bug
  366. if not (covariates is None or len(covariates) == 0):
  367. raise InvalidValueError(
  368. 'This parameter file contains covariates, which the simplified window '
  369. 'does not support. Open the full version of this window to use it, '
  370. 'or remove the covariates from the file.'
  371. )
  372. if not (lrt_factors is None or len(lrt_factors) == 0):
  373. raise InvalidValueError(
  374. 'This parameter file contains Likelihood Ratio Test factors, which the '
  375. 'simplified window does not support. Open the full version of this window '
  376. 'to use it, or remove those factors from the file.'
  377. )
  378. else:
  379. if 'picker' in self.covariates_widgets:
  380. self.covariates_widgets['picker'].set_comparison_values(covariates)
  381. if 'picker' in self.lrt_widgets:
  382. self.lrt_widgets['picker'].set_comparison_values(lrt_factors)
  383. return params
  384. class DESeqWindow(DiffExpWindow):
  385. def __init__(self, parent=None, simplified: bool = False):
  386. func = filtering.CountFilter.differential_expression_deseq2
  387. name = 'DESeq2'
  388. super().__init__(func, name, parent, simplified)
  389. class LimmaWindow(DiffExpWindow):
  390. def __init__(self, parent=None, simplified: bool = False):
  391. func = filtering.CountFilter.differential_expression_limma_voom
  392. name = 'Limma-Voom'
  393. super().__init__(func, name, parent, simplified)
  394. class SimpleDESeqWindow(DiffExpWindow):
  395. def __init__(self, parent=None):
  396. func = filtering.CountFilter.differential_expression_deseq2_simplified
  397. name = 'DESeq2'
  398. super().__init__(func, name, parent, simplified=True)
  399. class SimpleLimmaWindow(DiffExpWindow):
  400. def __init__(self, parent=None):
  401. func = filtering.CountFilter.differential_expression_limma_voom_simplified
  402. name = 'Limma-Voom'
  403. super().__init__(func, name, parent, simplified=True)
  404. class ClicomWindow(gui_windows.FuncExternalWindow):
  405. EXCLUDED_PARAMS = {'self', 'parameter_dicts', 'gui_mode'}
  406. ADDITIONAL_EXCLUDED_PARAMS = {
  407. 'power_transform',
  408. 'plot_style',
  409. 'split_plots',
  410. 'return_probabilities',
  411. 'gui_mode',
  412. 'parallel_backend',
  413. }
  414. def __init__(self, funcs: dict, filter_obj: filtering.Filter, parent=None):
  415. func = filtering.CountFilter.split_clicom
  416. help_link = f'https://guyteichman.github.io/RNAlysis/build/rnalysis.filtering.CountFilter.{func.__name__}.html'
  417. super().__init__('CLICOM', func, help_link, self.EXCLUDED_PARAMS, parent=parent)
  418. self.parameter_dicts: List[dict] = []
  419. self.funcs = funcs
  420. self.setups_counter = {key: 0 for key in self.funcs.keys()}
  421. self.filter_obj = filter_obj
  422. self.stack = FuncTypeStack(
  423. self.funcs, self.filter_obj, self, additional_excluded_params=self.ADDITIONAL_EXCLUDED_PARAMS
  424. )
  425. self.setups_group = QtWidgets.QGroupBox('2. Choose clustering setups for CLICOM')
  426. self.setups_grid = QtWidgets.QGridLayout(self.setups_group)
  427. self.setups_widgets = {}
  428. self.init_ui()
  429. def connect_widget(self, widget: QtWidgets.QWidget):
  430. super().connect_widget(widget)
  431. if isinstance(widget, (gui_widgets.TableColumnPicker, gui_widgets.TableColumnPicker)):
  432. widget.add_columns(self.filter_obj.columns)
  433. def migrate_legacy_parameters(self, kwargs: dict) -> dict:
  434. # 'power_transform' used to be a boolean (or a pair of them); it is now a menu of named transforms.
  435. # Parameter files exported by older versions still carry the booleans, so translate them to the names
  436. # they now stand for -- the API accepts both, but the drop-down can only display the names.
  437. kwargs = super().migrate_legacy_parameters(kwargs)
  438. if 'power_transform' in kwargs:
  439. transforms = [
  440. generic.parse_power_transform(value) for value in parsing.data_to_list(kwargs['power_transform'])
  441. ]
  442. kwargs['power_transform'] = transforms[0] if len(transforms) == 1 else transforms
  443. return kwargs
  444. def init_ui(self):
  445. super().init_ui()
  446. self.setWindowTitle('CLICOM clustering setup')
  447. self.setMinimumWidth(1500)
  448. self.scroll_layout.addWidget(self.setups_group, 0, 1)
  449. self.init_setups_ui()
  450. def init_setups_ui(self):
  451. self.setups_grid.addWidget(self.stack, 1, 0)
  452. self.setups_widgets['list'] = gui_widgets.MultiChoiceListWithDelete(list(), parent=self.setups_group)
  453. self.setups_widgets['list'].itemDeleted.connect(self.remove_clustering_setup)
  454. self.setups_grid.addWidget(QtWidgets.QLabel('<b>Added setups</b>'), 0, 1, QtCore.Qt.AlignmentFlag.AlignCenter)
  455. self.setups_grid.addWidget(self.setups_widgets['list'], 1, 1, 2, 1)
  456. self.setups_widgets['add_button'] = QtWidgets.QPushButton('Add setup')
  457. self.setups_widgets['add_button'].clicked.connect(self.add_clustering_setup)
  458. self.stack.funcSelected.connect(self.setups_widgets['add_button'].setEnabled)
  459. self.setups_grid.addWidget(self.setups_widgets['add_button'], 2, 0)
  460. self.setups_widgets['add_button'].setDisabled(True)
  461. self.setups_grid.setRowStretch(1, 1)
  462. @QtCore.pyqtSlot(int)
  463. def remove_clustering_setup(self, ind: int):
  464. self.parameter_dicts.pop(ind)
  465. def add_clustering_setup(self):
  466. func_name = self.stack.get_function_name()
  467. func_params = self.stack.get_function_params()
  468. func_params['method'] = func_name.lstrip('split_').lower()
  469. self.parameter_dicts.append(func_params)
  470. self.setups_counter[func_name] += 1
  471. self.setups_widgets['list'].add_item(f'{func_params["method"]}_{self.setups_counter[func_name]}')
  472. def get_analysis_args(self):
  473. return self.parameter_dicts
  474. class EnrichmentWindow(gui_widgets.MinMaxDialog):
  475. EXCLUDED_PARAMS = {
  476. 'self',
  477. 'save_csv',
  478. 'fname',
  479. 'return_fig',
  480. 'biotype',
  481. 'background_genes',
  482. 'statistical_test',
  483. 'parametric_test',
  484. 'biotype_ref_path',
  485. 'gui_mode',
  486. }
  487. ANALYSIS_TYPES = {
  488. 'Gene Ontology (GO)': 'go',
  489. 'Kyoto Encyclopedia of Genes and Genomes (KEGG)': 'kegg',
  490. 'Categorical attributes': 'user_defined',
  491. 'Non-categorical attributes': 'non_categorical',
  492. }
  493. ANALYSIS_TYPES_BUTTONS = (
  494. ('External datasets:', ('Gene Ontology (GO)', 'Kyoto Encyclopedia of Genes and Genomes (KEGG)')),
  495. ('Custom dataset:', ('Categorical attributes', 'Non-categorical attributes')),
  496. )
  497. ANALYSIS_FUNCS = {
  498. ('go', False): enrichment.FeatureSet.go_enrichment,
  499. ('go', True): enrichment.RankedSet.single_set_go_enrichment,
  500. ('kegg', False): enrichment.FeatureSet.kegg_enrichment,
  501. ('kegg', True): enrichment.RankedSet.single_set_kegg_enrichment,
  502. ('user_defined', False): enrichment.FeatureSet.user_defined_enrichment,
  503. ('user_defined', True): enrichment.RankedSet.single_set_enrichment,
  504. ('non_categorical', False): enrichment.FeatureSet.non_categorical_enrichment,
  505. }
  506. STATISTICAL_TESTS = {
  507. 'Randomization test': 'randomization',
  508. "Fisher's Exact test": 'fisher',
  509. 'Hypergeometric test': 'hypergeometric',
  510. 'Single-set enrichment (XL-mHG test)': 'single_set',
  511. }
  512. ORDINAL_STATISTICAL_TESTS = {'One-sample T-test (parametric)': True, 'Sign test (non-parametric)': False}
  513. STATISTICAL_TEST_ARGS = {
  514. 'randomization': {'alpha', 'randomization_reps', 'random_seed'},
  515. 'fisher': {'alpha'},
  516. 'hypergeometric': {'alpha'},
  517. 'single_set': {'alpha', 'min_positive_genes', 'lowest_cutoff'},
  518. 'non_categorical': {'alpha'},
  519. None: {},
  520. True: {'alpha'},
  521. False: {'alpha'},
  522. }
  523. PLOT_ARGS = {
  524. 'user_defined': {'plot_horizontal', 'plot_style', 'show_expected'},
  525. 'go': {'plot_horizontal', 'plot_style', 'show_expected', 'plot_ontology_graph', 'ontology_graph_format'},
  526. 'kegg': {'plot_horizontal', 'plot_style', 'show_expected', 'plot_pathway_graphs', 'pathway_graphs_format'},
  527. 'non_categorical': {'plot_log_scale', 'plot_style', 'n_bins'},
  528. }
  529. enrichmentStarted = QtCore.pyqtSignal(object, object)
  530. geneSetsRequested = QtCore.pyqtSignal(object)
  531. functionApplied = QtCore.pyqtSignal(gui_widgets.WorkerOutput)
  532. def __init__(self, parent=None):
  533. super().__init__(parent)
  534. self.parameters_signature = {}
  535. self.stats_signature = {}
  536. self.plot_signature = {}
  537. self.scroll = QtWidgets.QScrollArea()
  538. self.widgets = {}
  539. self.list_group = QtWidgets.QGroupBox('Enrichment analysis', self)
  540. self.list_grid = QtWidgets.QGridLayout(self.list_group)
  541. self.parameter_group = QtWidgets.QGroupBox('Additional parameters', self)
  542. self.parameter_grid = QtWidgets.QGridLayout(self.parameter_group)
  543. self.parameter_widgets = {}
  544. self.stats_group = QtWidgets.QGroupBox('Statistical test', self)
  545. self.stats_grid = QtWidgets.QGridLayout(self.stats_group)
  546. self.stats_widgets = {}
  547. self.plot_group = QtWidgets.QGroupBox('Configure plot', self)
  548. self.plot_grid = QtWidgets.QGridLayout(self.plot_group)
  549. self.plot_widgets = {}
  550. self.scroll_widget = QtWidgets.QWidget(self.scroll)
  551. self.main_layout = QtWidgets.QVBoxLayout(self)
  552. self.scroll_layout = QtWidgets.QVBoxLayout(self.scroll_widget)
  553. self.init_basic_ui()
  554. def init_basic_ui(self):
  555. self.setWindowTitle('Enrichment Analysis')
  556. self.main_layout.addWidget(self.scroll)
  557. self.parameter_group.setVisible(False)
  558. self.plot_group.setVisible(False)
  559. self.stats_group.setVisible(False)
  560. self.scroll.setHorizontalScrollBarPolicy(QtCore.Qt.ScrollBarPolicy.ScrollBarAlwaysOff)
  561. self.scroll.setWidgetResizable(True)
  562. self.scroll.setWidget(self.scroll_widget)
  563. self.scroll_layout.setSizeConstraint(QtWidgets.QLayout.SizeConstraint.SetMinAndMaxSize)
  564. self.scroll_layout.addWidget(self.list_group)
  565. self.scroll_layout.addWidget(self.stats_group)
  566. self.scroll_layout.addWidget(self.parameter_group)
  567. self.scroll_layout.addWidget(self.plot_group)
  568. self.widgets['run_button'] = QtWidgets.QPushButton('Run')
  569. self.widgets['run_button'].clicked.connect(self.run_analysis)
  570. gui_widgets.mark_primary(self.widgets['run_button'])
  571. self.widgets['run_button'].setVisible(False)
  572. self.scroll_layout.addWidget(self.widgets['run_button'])
  573. self.scroll_layout.addStretch(1)
  574. for lst in ['enrichment_list', 'bg_list']:
  575. self.widgets[lst] = gui_widgets.GeneSetComboBox(self)
  576. self.widgets[lst].boxOpened.connect(functools.partial(self.geneSetsRequested.emit, self.widgets[lst]))
  577. self.widgets[lst].currentTextChanged.connect(self._verify_inputs)
  578. self.widgets['dataset_radiobox'] = gui_widgets.RadioButtonBox(
  579. 'Choose enrichment dataset', self.ANALYSIS_TYPES_BUTTONS
  580. )
  581. self.widgets['dataset_radiobox'].buttonClicked.connect(self.update_uis)
  582. self.list_grid.addWidget(self.widgets['dataset_radiobox'], 0, 0, 6, 1)
  583. self.list_grid.addWidget(self.widgets['enrichment_list'], 1, 1)
  584. self.list_grid.addWidget(self.widgets['bg_list'], 3, 1)
  585. self.list_grid.addWidget(QtWidgets.QLabel('<b>Choose enrichment set:</b>', self), 0, 1)
  586. self.list_grid.addWidget(QtWidgets.QLabel('<b>Choose background set:</b>', self), 2, 1)
  587. self.scroll.setMinimumWidth(self.scroll_widget.sizeHint().width() + 150)
  588. def _set_background_select_mode(self, selectable: bool = True):
  589. if selectable:
  590. self.widgets['bg_list'].setDisabled(False)
  591. else:
  592. self.widgets['bg_list'].setDisabled(True)
  593. def _get_statistical_test_name(self):
  594. try:
  595. button = self.stats_widgets['stats_radiobox'].checkedButton()
  596. except KeyError:
  597. button = None
  598. if button is None:
  599. return None
  600. return button.text()
  601. def _get_statistical_test(self):
  602. stat_test = self._get_statistical_test_name()
  603. if stat_test is None:
  604. return None
  605. try:
  606. if self.is_categorical():
  607. return self.STATISTICAL_TESTS[stat_test]
  608. return self.ORDINAL_STATISTICAL_TESTS[stat_test]
  609. except KeyError:
  610. return None
  611. def _update_single_set(self):
  612. if self.is_single_set():
  613. self._set_background_select_mode(False)
  614. else:
  615. self._set_background_select_mode(True)
  616. def _update_signatures(self, update_params: bool = True, update_stats: bool = True, update_plot: bool = True):
  617. if update_params:
  618. self.parameters_signature = {}
  619. if update_stats:
  620. self.stats_signature = {}
  621. if update_plot:
  622. self.plot_signature = {}
  623. analysis_type = self.get_current_analysis_type()
  624. chosen_func = self.get_current_func()
  625. signature = generic.get_method_signature(chosen_func)
  626. func_desc, param_desc = io.get_method_docstring(chosen_func)
  627. for name, param in signature.items():
  628. this_desc = param_desc.get(name, '')
  629. if name in self.EXCLUDED_PARAMS:
  630. continue
  631. elif update_plot and name in self.PLOT_ARGS[analysis_type]:
  632. self.plot_signature[name] = (param, this_desc)
  633. elif update_stats and name in set.union(*self.STATISTICAL_TEST_ARGS.values()):
  634. self.stats_signature[name] = (param, this_desc)
  635. elif update_params:
  636. self.parameters_signature[name] = (param, this_desc)
  637. def update_uis(self):
  638. self._update_signatures()
  639. self.update_parameters_ui()
  640. self.update_stats_ui()
  641. self.update_plot_ui()
  642. self.widgets['run_button'].setVisible(True)
  643. self.widgets['run_button'].setDisabled(True)
  644. self._verify_inputs()
  645. if 'help_link' in self.widgets:
  646. self.scroll_layout.removeWidget(self.widgets['help_link'])
  647. self.widgets['help_link'].deleteLater()
  648. chosen_func_name = self.get_current_func().__name__
  649. obj_type = enrichment.RankedSet if self.is_single_set() else enrichment.FeatureSet
  650. help_address = (
  651. f'https://guyteichman.github.io/RNAlysis/build/rnalysis.enrichment.'
  652. f'{obj_type.__name__}.{chosen_func_name}.html'
  653. )
  654. self.widgets['help_link'] = QtWidgets.QLabel(
  655. f'<a href="{help_address}">Open documentation for function '
  656. f'<b>{obj_type.__name__}.{chosen_func_name}</b></a>'
  657. )
  658. self.widgets['help_link'].setOpenExternalLinks(True)
  659. self.scroll_layout.insertWidget(4, self.widgets['help_link'])
  660. _, _, width, height = self.scroll.geometry().getRect()
  661. self.resize(width, 750)
  662. @QtCore.pyqtSlot()
  663. def _verify_inputs(self):
  664. is_legal = True
  665. if not self.widgets['enrichment_list'].is_legal():
  666. is_legal = False
  667. if self.widgets['bg_list'].isEnabled() and (not self.widgets['bg_list'].is_legal()):
  668. is_legal = False
  669. if self._get_statistical_test() is None:
  670. is_legal = False
  671. self.widgets['run_button'].setEnabled(is_legal)
  672. def get_current_analysis_type(self):
  673. button = self.widgets['dataset_radiobox'].checkedButton()
  674. if button is None:
  675. return None
  676. return self.ANALYSIS_TYPES[self.widgets['dataset_radiobox'].checkedButton().text()]
  677. def get_current_func(self):
  678. single_set = self.is_single_set()
  679. name = self.get_current_analysis_type()
  680. func = self.ANALYSIS_FUNCS[(name, single_set)]
  681. return func
  682. def update_plot_ui(self):
  683. self.plot_group.setVisible(True)
  684. # delete previous widgets
  685. self.plot_widgets = {}
  686. gui_widgets.clear_layout(self.plot_grid)
  687. i = 0
  688. func = self.get_current_func()
  689. for name, (param, desc) in self.plot_signature.items():
  690. self.plot_widgets[name] = gui_widgets.param_to_widget(param, name)
  691. label = QtWidgets.QLabel(f'{generic.get_param_readable_name(name, func)}:', self.plot_widgets[name])
  692. label.setToolTip(desc)
  693. help_button = gui_widgets.HelpButton()
  694. self.plot_grid.addWidget(label, i, 0)
  695. self.plot_grid.addWidget(self.plot_widgets[name], i, 1)
  696. self.plot_grid.addWidget(help_button, i, 2)
  697. help_button.set_param_help(name, desc)
  698. i += 1
  699. def update_parameters_ui(self):
  700. self.parameter_group.setVisible(True)
  701. # delete previous widgets
  702. self.parameter_widgets = {}
  703. gui_widgets.clear_layout(self.parameter_grid)
  704. i = 0
  705. func = self.get_current_func()
  706. for name, (param, desc) in self.parameters_signature.items():
  707. self.parameter_widgets[name] = gui_widgets.param_to_widget(param, name)
  708. label = QtWidgets.QLabel(f'{generic.get_param_readable_name(name, func)}:', self.parameter_widgets[name])
  709. label.setToolTip(desc)
  710. help_button = gui_widgets.HelpButton()
  711. self.parameter_grid.addWidget(help_button, i, 2)
  712. self.parameter_grid.addWidget(label, i, 0)
  713. self.parameter_grid.addWidget(self.parameter_widgets[name], i, 1)
  714. help_button.set_param_help(name, desc)
  715. i += 1
  716. def update_stats_ui(self):
  717. self._update_signatures(False, True, False)
  718. self.stats_group.setVisible(True)
  719. prev_test = self._get_statistical_test()
  720. prev_test_name = self._get_statistical_test_name()
  721. self._update_single_set()
  722. # delete previous widgets
  723. self.stats_widgets = {}
  724. gui_widgets.clear_layout(self.stats_grid)
  725. radio_options = parsing.data_to_list(
  726. self.STATISTICAL_TESTS.keys() if self.is_categorical() else self.ORDINAL_STATISTICAL_TESTS.keys()
  727. )
  728. self.stats_widgets['stats_radiobox'] = gui_widgets.RadioButtonBox('Choose statistical test:', radio_options)
  729. self.stats_widgets['stats_radiobox'].selectionChanged.connect(self._verify_inputs)
  730. self.stats_widgets['stats_radiobox'].buttonClicked.connect(self._verify_inputs)
  731. if prev_test_name is not None:
  732. self.stats_widgets['stats_radiobox'].set_selection(prev_test_name)
  733. self.stats_widgets['stats_radiobox'].buttonClicked.connect(self.update_stats_ui)
  734. self.stats_grid.addWidget(self.stats_widgets['stats_radiobox'], 0, 0, 3, 2)
  735. i = 0
  736. func = self.get_current_func()
  737. for name, (param, desc) in self.stats_signature.items():
  738. if name in self.STATISTICAL_TEST_ARGS[prev_test]:
  739. self.stats_widgets[name] = gui_widgets.param_to_widget(param, name)
  740. label = QtWidgets.QLabel(f'{generic.get_param_readable_name(name, func)}:', self.stats_widgets[name])
  741. label.setToolTip(desc)
  742. help_button = gui_widgets.HelpButton()
  743. self.stats_grid.addWidget(help_button, i, 4)
  744. self.stats_grid.addWidget(label, i, 2)
  745. self.stats_grid.addWidget(self.stats_widgets[name], i, 3)
  746. help_button.set_param_help(name, desc)
  747. i += 1
  748. def is_single_set(self):
  749. stat_test = self._get_statistical_test()
  750. return stat_test == 'single_set'
  751. def is_categorical(self):
  752. analysis_type = self.get_current_analysis_type()
  753. return analysis_type != 'non_categorical'
  754. def get_analysis_params(self):
  755. kwargs = dict()
  756. pred_ids = []
  757. stat_test = self._get_statistical_test()
  758. if not self.is_single_set():
  759. if self.is_categorical():
  760. kwargs['statistical_test'] = stat_test
  761. else:
  762. kwargs['parametric_test'] = stat_test
  763. gene_set = self.widgets['enrichment_list'].value()
  764. pred_ids.append(self.widgets['enrichment_list'].current_id())
  765. gene_set_name = self.widgets['enrichment_list'].currentText()
  766. for param_name, widget in itertools.chain(
  767. self.parameter_widgets.items(), self.plot_widgets.items(), self.stats_widgets.items()
  768. ):
  769. if param_name in {'help_link', 'dataset_radiobox', 'stats_radiobox'}:
  770. continue
  771. val = gui_widgets.get_val_from_widget(widget)
  772. kwargs[param_name] = val
  773. if not self.is_single_set():
  774. bg_set = self.widgets['bg_list'].value()
  775. pred_ids.append(self.widgets['bg_list'].current_id())
  776. else:
  777. bg_set = None
  778. return gene_set, bg_set, gene_set_name.rstrip('*'), kwargs, pred_ids
  779. @QtCore.pyqtSlot()
  780. def run_analysis(self):
  781. func = self.get_current_func()
  782. gene_set, bg_set, set_name, kwargs, pred_ids = self.get_analysis_params()
  783. kwargs['gui_mode'] = True
  784. print('Enrichment analysis started')
  785. self.showMinimized()
  786. try:
  787. is_single_set = self.is_single_set()
  788. if is_single_set:
  789. bg_set_obj = None
  790. else:
  791. bg_set_obj = enrichment.FeatureSet(bg_set, 'background_set')
  792. feature_set_obj = (
  793. enrichment.RankedSet(gene_set, set_name) if is_single_set else enrichment.FeatureSet(gene_set, set_name)
  794. )
  795. if is_single_set:
  796. partial = functools.partial(func, feature_set_obj, **kwargs)
  797. else:
  798. partial = functools.partial(func, feature_set_obj, background_genes=bg_set_obj, **kwargs)
  799. worker = gui_widgets.Worker(partial, JOB_COUNTER.get_id(), pred_ids, set_name)
  800. worker.finished.connect(self.functionApplied.emit)
  801. self.enrichmentStarted.emit(worker, self.showNormal)
  802. except Exception as e:
  803. self.showNormal()
  804. raise e
  805. class SetOperationWindow(gui_widgets.MinMaxDialog):
  806. SET_OPERATIONS = {
  807. 'Union': 'union',
  808. 'Majority-Vote Intersection': 'majority_vote_intersection',
  809. 'Intersection': 'intersection',
  810. 'Difference': 'difference',
  811. 'Symmetric Difference': 'symmetric_difference',
  812. 'Other': 'other',
  813. }
  814. EXCLUDED_PARAMS = {'self', 'other', 'others', 'return_type', 'legacy_args'}
  815. geneSetReturned = QtCore.pyqtSignal(set, str, list, dict)
  816. primarySetUsed = QtCore.pyqtSignal(str)
  817. primarySetChangedDifference = QtCore.pyqtSignal(str)
  818. primarySetChangedIntersection = QtCore.pyqtSignal()
  819. def __init__(self, available_objects: dict, parent=None):
  820. super().__init__(parent)
  821. self.available_objects = available_objects
  822. self.widgets = {}
  823. self.list_group = QtWidgets.QGroupBox('Choose gene sets', self)
  824. self.list_grid = QtWidgets.QGridLayout(self.list_group)
  825. self.parameter_group = QtWidgets.QGroupBox('Additional parameters', self)
  826. self.parameter_grid = QtWidgets.QGridLayout(self.parameter_group)
  827. self.parameter_widgets = {}
  828. self.operations_group = QtWidgets.QGroupBox('Set operation')
  829. self.operations_grid = QtWidgets.QGridLayout(self.operations_group)
  830. self.layout = QtWidgets.QHBoxLayout(self)
  831. self.init_ui()
  832. def create_canvas(self):
  833. set_names = [item.text() for item in self.widgets['set_list'].get_sorted_selection()]
  834. sets = [self.available_objects[name][0].obj() for name in set_names]
  835. ind = 0
  836. while ind < len(set_names):
  837. if sets[ind] is None or self.available_objects[set_names[ind]][0].is_empty():
  838. set_names.pop(ind)
  839. sets.pop(ind)
  840. else:
  841. ind += 1
  842. if len(set_names) < 2:
  843. canvas = gui_graphics.EmptyCanvas('Please select 2 or more gene sets to continue', self)
  844. else:
  845. items = {}
  846. for s, s_name in zip(sets, set_names):
  847. if validation.isinstanceinh(s, filtering.Filter):
  848. s_set = s.index_set
  849. elif validation.isinstanceinh(s, enrichment.FeatureSet):
  850. s_set = s.gene_set
  851. elif isinstance(s, set):
  852. s_set = s
  853. else:
  854. raise TypeError(type(s))
  855. items[s_name] = s_set
  856. if 2 <= len(set_names) <= 3:
  857. canvas = gui_graphics.VennInteractiveCanvas(items, self)
  858. else:
  859. canvas = gui_graphics.UpSetInteractiveCanvas(items, self)
  860. if 'canvas' in self.widgets:
  861. # detach the old canvas/toolbar from the layout and reparent them to None *before*
  862. # scheduling deletion, so no queued paint/draw event can fire against a widget whose
  863. # C++ object is being torn down (a source of flaky native crashes on teardown).
  864. self.operations_grid.removeWidget(self.widgets['canvas'])
  865. self.operations_grid.removeWidget(self.widgets['toolbar'])
  866. self.widgets['canvas'].setParent(None)
  867. self.widgets['toolbar'].setParent(None)
  868. self.widgets['canvas'].deleteLater()
  869. self.widgets['toolbar'].deleteLater()
  870. self.widgets['canvas'] = canvas
  871. self.widgets['toolbar'] = gui_graphics.CleanPlotToolBar(self.widgets['canvas'], self)
  872. self.operations_grid.addWidget(self.widgets['canvas'], 1, 2, 3, 3)
  873. self.operations_grid.addWidget(self.widgets['toolbar'], 0, 2, 1, 3)
  874. for col in range(2, 5):
  875. self.operations_grid.setColumnStretch(col, 1)
  876. for row in range(1, 4):
  877. self.operations_grid.setRowStretch(row, 1)
  878. if not isinstance(canvas, gui_graphics.EmptyCanvas):
  879. self._connect_canvas(canvas)
  880. def _connect_canvas(self, canvas: gui_graphics.BaseInteractiveCanvas):
  881. canvas.manualChoice.connect(self._set_op_other)
  882. self.widgets['radio_button_box'].radio_buttons['Union'].clicked.connect(canvas.union)
  883. self.widgets['radio_button_box'].radio_buttons['Intersection'].clicked.connect(canvas.intersection)
  884. self.primarySetChangedDifference.connect(canvas.difference)
  885. self.primarySetChangedIntersection.connect(canvas.intersection)
  886. if isinstance(canvas, gui_graphics.VennInteractiveCanvas):
  887. self.widgets['radio_button_box'].radio_buttons['Symmetric Difference'].clicked.connect(
  888. canvas.symmetric_difference
  889. )
  890. this_button = self.widgets['radio_button_box'].checkedButton()
  891. if this_button is not None:
  892. self.widgets['radio_button_box'].set_selection(this_button.text())
  893. def init_ui(self):
  894. self.setWindowTitle('Set Operations')
  895. self.setGeometry(600, 50, 1050, 800)
  896. self.setLayout(self.layout)
  897. self.widgets['splitter'] = QtWidgets.QSplitter(QtCore.Qt.Orientation.Horizontal)
  898. self.layout.addWidget(self.widgets['splitter'])
  899. self.widgets['splitter'].addWidget(self.list_group)
  900. self.widgets['splitter'].addWidget(self.operations_group)
  901. self.widgets['splitter'].setSizes([int(self.width() * 0.15), int(self.width() * 0.85)])
  902. self.parameter_group.setVisible(False)
  903. self.init_sets_ui()
  904. self.init_operations_ui()
  905. def init_sets_ui(self):
  906. self.widgets['set_list'] = gui_widgets.MultiChoiceListWithReorder(
  907. self.available_objects, [val[1] for val in self.available_objects.values()], self
  908. )
  909. for func in [
  910. self.create_canvas,
  911. self._check_legal_operations,
  912. self._validate_input,
  913. self._toggle_choose_primary_set,
  914. ]:
  915. self.widgets['set_list'].itemSelectionChanged.connect(func)
  916. self.widgets['set_list'].itemOrderChanged.connect(func)
  917. self.list_grid.addWidget(self.widgets['set_list'], 0, 0)
  918. def _toggle_choose_primary_set(self):
  919. if self.get_current_func_name() in ['difference', 'intersection']:
  920. self.widgets['choose_primary_set'].setVisible(True)
  921. self.widgets['choose_primary_set_label'].setVisible(True)
  922. self.widgets['choose_primary_set'].clear()
  923. self.widgets['choose_primary_set'].addItems(
  924. [item.text() for item in self.widgets['set_list'].get_sorted_selection()]
  925. )
  926. self.widgets['canvas'].clear_selection()
  927. else:
  928. self.widgets['choose_primary_set'].setVisible(False)
  929. self.widgets['choose_primary_set_label'].setVisible(False)
  930. def init_operations_ui(self):
  931. self.widgets['set_op_box'] = QtWidgets.QWidget(self)
  932. self.widgets['set_op_box_layout'] = QtWidgets.QVBoxLayout(self.widgets['set_op_box'])
  933. self.operations_grid.addWidget((self.widgets['set_op_box']), 1, 0, 3, 1)
  934. self.widgets['radio_button_box'] = gui_widgets.RadioButtonBox(
  935. 'Choose set operation', self.SET_OPERATIONS.keys()
  936. )
  937. for func in [self.update_paremeter_ui, self._validate_input, self._toggle_choose_primary_set]:
  938. self.widgets['radio_button_box'].buttonClicked.connect(func)
  939. self.widgets['radio_button_box'].selectionChanged.connect(func)
  940. self.widgets['radio_button_box'].radio_buttons['Majority-Vote Intersection'].clicked.connect(
  941. self._majority_vote_intersection
  942. )
  943. self.widgets['set_op_box_layout'].addWidget(self.widgets['radio_button_box'], stretch=1)
  944. self.widgets['choose_primary_set'] = gui_widgets.MandatoryComboBox('Choose primary set...', self)
  945. self.widgets['choose_primary_set'].currentTextChanged.connect(self._primary_set_changed)
  946. self.widgets['choose_primary_set_label'] = QtWidgets.QLabel('Primary set for operation:')
  947. self.widgets['set_op_box_layout'].addWidget(self.widgets['choose_primary_set_label'])
  948. self.widgets['set_op_box_layout'].addWidget(self.widgets['choose_primary_set'])
  949. self.widgets['set_op_box_layout'].addWidget(self.parameter_group)
  950. self.widgets['set_op_box_layout'].addStretch(1)
  951. self._toggle_choose_primary_set()
  952. self.create_canvas()
  953. self.widgets['apply_button'] = QtWidgets.QPushButton('Apply')
  954. self.widgets['apply_button'].clicked.connect(self.apply_set_op)
  955. gui_widgets.mark_primary(self.widgets['apply_button'])
  956. self.widgets['apply_button'].setEnabled(False)
  957. self.operations_grid.addWidget(self.widgets['apply_button'], 4, 0, 1, 6)
  958. def _majority_vote_intersection(self):
  959. if not isinstance(self.widgets['canvas'], gui_graphics.EmptyCanvas):
  960. if 'majority_threshold' in self.parameter_widgets:
  961. threshold = gui_widgets.get_val_from_widget(self.parameter_widgets['majority_threshold'])
  962. else:
  963. threshold = 0.5
  964. self.widgets['canvas'].majority_vote_intersection(threshold)
  965. @QtCore.pyqtSlot(str)
  966. def _primary_set_changed(self, set_name: str):
  967. func_name = self.get_current_func_name()
  968. if func_name == 'difference':
  969. self.primarySetChangedDifference.emit(set_name)
  970. elif func_name == 'intersection':
  971. self.primarySetChangedIntersection.emit()
  972. self._validate_input()
  973. def update_paremeter_ui(self):
  974. # delete previous widgets
  975. try:
  976. self.operations_grid.removeWidget(self.parameter_widgets['help_link'])
  977. self.parameter_widgets['help_link'].setParent(None)
  978. self.parameter_widgets['help_link'].deleteLater()
  979. except KeyError:
  980. pass
  981. self.parameter_widgets = {}
  982. gui_widgets.clear_layout(self.parameter_grid)
  983. chosen_func_name = self.get_current_func_name()
  984. signature = generic.get_method_signature(chosen_func_name, filtering.Filter)
  985. func = getattr(filtering.Filter, chosen_func_name, None)
  986. i = 0
  987. for name, param in signature.items():
  988. if name in self.EXCLUDED_PARAMS:
  989. continue
  990. self.parameter_widgets[name] = gui_widgets.param_to_widget(param, name)
  991. self.parameter_grid.addWidget(
  992. QtWidgets.QLabel(f'{generic.get_param_readable_name(name, func)}:', self.parameter_widgets[name]), i, 0
  993. )
  994. self.parameter_grid.addWidget(self.parameter_widgets[name], i, 1)
  995. if chosen_func_name == 'majority_vote_intersection':
  996. self.parameter_widgets[name].valueChanged.connect(self._majority_vote_intersection)
  997. self._majority_vote_intersection()
  998. i += 1
  999. self.parameter_group.setVisible(i > 0)
  1000. if chosen_func_name != 'other':
  1001. help_address = (
  1002. f'https://guyteichman.github.io/RNAlysis/build/rnalysis.filtering.'
  1003. f'{filtering.Filter.__name__}.{chosen_func_name}.html'
  1004. )
  1005. self.parameter_widgets['help_link'] = QtWidgets.QLabel(
  1006. f'<a href="{help_address}">Open documentation for function '
  1007. f'<b>{filtering.Filter.__name__}.{chosen_func_name}</b></a>',
  1008. self,
  1009. )
  1010. self.parameter_widgets['help_link'].setOpenExternalLinks(True)
  1011. self.operations_grid.addWidget(self.parameter_widgets['help_link'], 5, 0, 1, 6)
  1012. def get_current_func_name(self):
  1013. button = self.widgets['radio_button_box'].checkedButton()
  1014. if button is None:
  1015. return None
  1016. return self.SET_OPERATIONS[button.text()]
  1017. def _check_legal_operations(self):
  1018. n_items = len(self.widgets['set_list'].get_sorted_selection())
  1019. if self.get_current_func_name() == 'symmetric_difference' and n_items > 2:
  1020. self.widgets['radio_button_box'].set_selection('Other')
  1021. sym_diff_button = self.widgets['radio_button_box'].radio_buttons['Symmetric Difference']
  1022. sym_diff_button.setEnabled(n_items <= 2)
  1023. def _validate_input(self):
  1024. is_legal = True
  1025. if isinstance(self.widgets['canvas'], gui_graphics.EmptyCanvas):
  1026. is_legal = False
  1027. if self.get_current_func_name() is None:
  1028. is_legal = False
  1029. if self.widgets['choose_primary_set'].isVisible() and not self.widgets['choose_primary_set'].is_legal():
  1030. is_legal = False
  1031. self.widgets['apply_button'].setEnabled(is_legal)
  1032. def _set_op_other(self):
  1033. self.widgets['radio_button_box'].set_selection('Other')
  1034. def _get_set_names(self):
  1035. return [item.text() for item in self.widgets['set_list'].get_sorted_selection()]
  1036. def _get_function_params(self):
  1037. set_names = self._get_set_names()
  1038. if self.get_current_func_name() in ['intersection', 'difference']:
  1039. primary_set_name = self.widgets['choose_primary_set'].currentText()
  1040. self.primarySetUsed.emit(primary_set_name)
  1041. else:
  1042. primary_set_name = set_names[0]
  1043. kwargs = {}
  1044. for param_name, widget in self.parameter_widgets.items():
  1045. if param_name in {'apply_button', 'help_link'}:
  1046. continue
  1047. val = gui_widgets.get_val_from_widget(widget)
  1048. kwargs[param_name] = val
  1049. return set_names, primary_set_name, kwargs
  1050. @QtCore.pyqtSlot()
  1051. def apply_set_op(self):
  1052. func_name = self.get_current_func_name()
  1053. ancestor_ids = [self.available_objects[name][0].tab_id for name in self._get_set_names()]
  1054. if func_name == 'other':
  1055. output_set = self.widgets['canvas'].get_custom_selection()
  1056. output_name = 'Other set operation output'
  1057. kwargs = {}
  1058. else:
  1059. set_names, primary_set_name, kwargs = self._get_function_params()
  1060. first_obj = self.available_objects[primary_set_name][0].obj()
  1061. if isinstance(first_obj, set):
  1062. first_obj = filtering.Filter.from_dataframe(pl.DataFrame([list(first_obj)]), 'placeholder')
  1063. other_objs = []
  1064. for name in set_names:
  1065. if name != primary_set_name:
  1066. other_objs.append(self.available_objects[name][0].obj())
  1067. output_name = f'{func_name} output'
  1068. if kwargs.get('inplace', False):
  1069. command = SetOpInplacCommand(
  1070. self.available_objects[primary_set_name][0],
  1071. func_name,
  1072. other_objs,
  1073. kwargs,
  1074. f'Apply "{func_name}"',
  1075. ancestor_ids,
  1076. )
  1077. self.available_objects[primary_set_name][0].undo_stack.push(command)
  1078. output_set = None
  1079. else:
  1080. output_set = getattr(first_obj, func_name)(*other_objs, **kwargs)
  1081. if output_set is not None:
  1082. self.geneSetReturned.emit(output_set, output_name, ancestor_ids, kwargs)
  1083. self.close()
  1084. class SetVisualizationWindow(gui_widgets.MinMaxDialog):
  1085. VISUALIZATION_FUNCS = {'Venn Diagram': 'venn_diagram', 'UpSet Plot': 'upset_plot'}
  1086. EXCLUDED_PARAMS = {'objs', 'attr_ref_table_path', 'fig', 'legacy_args'}
  1087. figureGenerated = QtCore.pyqtSignal(plt.Figure, str, list, dict)
  1088. def __init__(self, available_objects: dict, parent=None):
  1089. super().__init__(parent)
  1090. self.available_objects = available_objects
  1091. self.layout = QtWidgets.QHBoxLayout(self)
  1092. self.widgets = {}
  1093. self.list_group = QtWidgets.QGroupBox('Choose gene sets', self)
  1094. self.list_grid = QtWidgets.QGridLayout(self.list_group)
  1095. self.visualization_group = QtWidgets.QGroupBox('Gene set visualization')
  1096. self.visualization_grid = QtWidgets.QGridLayout(self.visualization_group)
  1097. self.parameter_widgets = {}
  1098. self.parameter_group = QtWidgets.QGroupBox('Additional parameters')
  1099. self.parameter_grid = QtWidgets.QGridLayout(self.parameter_group)
  1100. self.init_ui()
  1101. def init_ui(self):
  1102. self.setWindowTitle('Gene Set Visualization')
  1103. self.setGeometry(600, 50, 1050, 800)
  1104. self.setLayout(self.layout)
  1105. self.widgets['splitter'] = QtWidgets.QSplitter(QtCore.Qt.Orientation.Horizontal)
  1106. self.layout.addWidget(self.widgets['splitter'])
  1107. self.widgets['splitter'].addWidget(self.list_group)
  1108. self.widgets['splitter'].addWidget(self.visualization_group)
  1109. self.widgets['splitter'].setSizes([int(self.width() * 0.1), int(self.width() * 0.9)])
  1110. self.parameter_group.setVisible(False)
  1111. self.init_list_ui()
  1112. self.init_visualization_ui()
  1113. def init_list_ui(self):
  1114. self.widgets['set_list'] = gui_widgets.MultiChoiceListWithReorder(
  1115. self.available_objects, [val[1] for val in self.available_objects.values()], self
  1116. )
  1117. for func in [self._check_legal_operations, self._validate_input, self.create_canvas]:
  1118. self.widgets['set_list'].itemSelectionChanged.connect(func)
  1119. self.widgets['set_list'].itemOrderChanged.connect(func)
  1120. self.list_grid.addWidget(self.widgets['set_list'], 0, 0)
  1121. def init_visualization_ui(self):
  1122. self.widgets['radio_button_box'] = gui_widgets.RadioButtonBox(
  1123. 'Choose visualization type:', self.VISUALIZATION_FUNCS, parent=self
  1124. )
  1125. for func in [self.update_parameter_ui, self._validate_input, self.create_canvas]:
  1126. self.widgets['radio_button_box'].buttonClicked.connect(func)
  1127. self.widgets['radio_button_box'].selectionChanged.connect(func)
  1128. self.visualization_grid.addWidget(self.widgets['radio_button_box'], 0, 0, 2, 1)
  1129. self.visualization_grid.addWidget(self.parameter_group, 2, 0, 2, 1)
  1130. self.visualization_grid.setRowStretch(self.visualization_grid.count(), 1)
  1131. self.widgets['generate_button'] = QtWidgets.QPushButton('Generate graph')
  1132. self.widgets['generate_button'].clicked.connect(self.generate_graph)
  1133. gui_widgets.mark_primary(self.widgets['generate_button'])
  1134. self.widgets['generate_button'].setEnabled(False)
  1135. self.visualization_grid.addWidget(self.widgets['generate_button'], 4, 0, 1, 5)
  1136. self.create_canvas()
  1137. def create_canvas(self):
  1138. set_names = [item.text() for item in self.widgets['set_list'].get_sorted_selection()]
  1139. sets = [self.available_objects[name][0].obj() for name in set_names]
  1140. ind = 0
  1141. while ind < len(set_names):
  1142. if sets[ind] is None or self.available_objects[set_names[ind]][0].is_empty():
  1143. set_names.pop(ind)
  1144. sets.pop(ind)
  1145. else:
  1146. ind += 1
  1147. func_name = self.get_current_func_name()
  1148. if len(set_names) < 2:
  1149. canvas = gui_graphics.EmptyCanvas('Please select 2 or more gene sets to continue', self)
  1150. elif func_name is None:
  1151. canvas = gui_graphics.EmptyCanvas('Please choose a visualization function to continue', self)
  1152. else:
  1153. objs_to_plot, kwargs, _ = self._get_function_params()
  1154. try:
  1155. canvas = gui_graphics.BasePreviewCanvas(
  1156. getattr(enrichment, func_name), self, objs=objs_to_plot, **kwargs
  1157. )
  1158. except Exception:
  1159. canvas = gui_graphics.EmptyCanvas('Invalid input; please change one or more of your parameters')
  1160. if 'canvas' in self.widgets:
  1161. # detach the old canvas from the layout and reparent it to None *before* scheduling
  1162. # deletion, so no queued paint/draw event can fire against a widget whose C++ object is
  1163. # being torn down (a source of flaky native crashes on teardown).
  1164. self.visualization_grid.removeWidget(self.widgets['canvas'])
  1165. self.widgets['canvas'].setParent(None)
  1166. self.widgets['canvas'].deleteLater()
  1167. self.widgets['canvas'] = canvas
  1168. self.visualization_grid.addWidget(self.widgets['canvas'], 0, 2, 4, 3)
  1169. for col in range(2, self.visualization_grid.columnCount()):
  1170. self.visualization_grid.setColumnStretch(col, 2)
  1171. for row in range(0, 4):
  1172. self.visualization_grid.setRowStretch(row, 1)
  1173. def _validate_input(self):
  1174. is_legal = True
  1175. if self.get_current_func_name() is None:
  1176. is_legal = False
  1177. if len(self.widgets['set_list'].get_sorted_selection()) < 2:
  1178. is_legal = False
  1179. self.widgets['generate_button'].setEnabled(is_legal)
  1180. def _check_legal_operations(self):
  1181. n_items = len(self.widgets['set_list'].get_sorted_selection())
  1182. if self.get_current_func_name() == 'venn_diagram' and n_items > 3:
  1183. self.widgets['radio_button_box'].set_selection('UpSet Plot')
  1184. venn_button = self.widgets['radio_button_box'].radio_buttons['Venn Diagram']
  1185. venn_button.setEnabled(n_items <= 3)
  1186. def update_parameter_ui(self):
  1187. # delete previous widgets
  1188. try:
  1189. self.visualization_grid.removeWidget(self.parameter_widgets['help_link'])
  1190. self.parameter_widgets['help_link'].setParent(None)
  1191. self.parameter_widgets['help_link'].deleteLater()
  1192. except KeyError:
  1193. pass
  1194. self.parameter_widgets = {}
  1195. gui_widgets.clear_layout(self.parameter_grid)
  1196. chosen_func_name = self.get_current_func_name()
  1197. signature = generic.get_method_signature(chosen_func_name, enrichment)
  1198. func = getattr(enrichment, chosen_func_name, None)
  1199. i = 0
  1200. for name, param in signature.items():
  1201. if name in self.EXCLUDED_PARAMS:
  1202. continue
  1203. self.parameter_widgets[name] = gui_widgets.param_to_widget(
  1204. param, name, actions_to_connect=self.create_canvas
  1205. )
  1206. self.parameter_grid.addWidget(
  1207. QtWidgets.QLabel(f'{generic.get_param_readable_name(name, func)}:', self.parameter_widgets[name]), i, 0
  1208. )
  1209. self.parameter_grid.addWidget(self.parameter_widgets[name], i, 1)
  1210. i += 1
  1211. help_address = f'https://guyteichman.github.io/RNAlysis/build/rnalysis.enrichment.{chosen_func_name}.html'
  1212. self.parameter_widgets['help_link'] = QtWidgets.QLabel(
  1213. f'<a href="{help_address}">Open documentation for function <b>enrichment.{chosen_func_name}</b></a>', self
  1214. )
  1215. self.parameter_widgets['help_link'].setOpenExternalLinks(True)
  1216. self.visualization_grid.addWidget(self.parameter_widgets['help_link'], 5, 0, 1, 4)
  1217. self.parameter_group.setVisible(i > 0)
  1218. def get_current_func_name(self):
  1219. button = self.widgets['radio_button_box'].checkedButton()
  1220. if button is None:
  1221. return None
  1222. return self.VISUALIZATION_FUNCS[button.text()]
  1223. def _get_function_params(self):
  1224. set_names = [item.text() for item in self.widgets['set_list'].get_sorted_selection()]
  1225. objs_to_plot = {
  1226. name: self.available_objects[name][0].obj()
  1227. for name in set_names
  1228. if not self.available_objects[name][0].is_empty()
  1229. }
  1230. ancestor_ids = [
  1231. self.available_objects[name][0].tab_id
  1232. for name in set_names
  1233. if not self.available_objects[name][0].is_empty()
  1234. ]
  1235. kwargs = {}
  1236. for param_name, widget in self.parameter_widgets.items():
  1237. if param_name in {'apply_button', 'help_link'}:
  1238. continue
  1239. val = gui_widgets.get_val_from_widget(widget)
  1240. kwargs[param_name] = val
  1241. return objs_to_plot, kwargs, ancestor_ids
  1242. @QtCore.pyqtSlot()
  1243. def generate_graph(self):
  1244. func_name = self.get_current_func_name()
  1245. objs_to_plot, kwargs, ancestor_ids = self._get_function_params()
  1246. fig = getattr(enrichment, func_name)(objs_to_plot, **kwargs)
  1247. self.figureGenerated.emit(fig, func_name, ancestor_ids, kwargs)
  1248. class TabPage(QtWidgets.QWidget):
  1249. functionApplied = QtCore.pyqtSignal(gui_widgets.WorkerOutput)
  1250. itemSpawned = QtCore.pyqtSignal(str, int, int, object)
  1251. filterObjectCreated = QtCore.pyqtSignal(object, int)
  1252. featureSetCreated = QtCore.pyqtSignal(object, int)
  1253. startedJob = QtCore.pyqtSignal(object, object, object)
  1254. tabNameChange = QtCore.pyqtSignal(str, bool)
  1255. tabSaved = QtCore.pyqtSignal()
  1256. changeIcon = QtCore.pyqtSignal(str)
  1257. geneSetsRequested = QtCore.pyqtSignal(object)
  1258. tabLoaded = QtCore.pyqtSignal(int, str, object)
  1259. tabReverted = QtCore.pyqtSignal(int)
  1260. EXCLUDED_FUNCS = set()
  1261. SUMMARY_FUNCS = set()
  1262. CLUSTERING_FUNCS = ()
  1263. GENERAL_FUNCS = ()
  1264. THREADED_FUNCS = set()
  1265. def __init__(self, parent=None, undo_stack: QtGui.QUndoStack = None, tab_id: int = None):
  1266. super().__init__(parent)
  1267. self.tab_id = tab_id
  1268. self.undo_stack = undo_stack
  1269. self.sup_layout = QtWidgets.QVBoxLayout(self)
  1270. self.splitter = QtWidgets.QSplitter(QtCore.Qt.Orientation.Vertical)
  1271. self.sup_layout.addWidget(self.splitter)
  1272. # initiate the splitter layout for the tab
  1273. self.scroll = QtWidgets.QScrollArea()
  1274. self.scroll.setVerticalScrollBarPolicy(QtCore.Qt.ScrollBarPolicy.ScrollBarAlwaysOn)
  1275. self.scroll.setHorizontalScrollBarPolicy(QtCore.Qt.ScrollBarPolicy.ScrollBarAlwaysOff)
  1276. self.scroll.setWidgetResizable(True)
  1277. self.splitter.addWidget(self.scroll)
  1278. self.container = QtWidgets.QWidget()
  1279. self.layout = QtWidgets.QVBoxLayout(self.container)
  1280. self.scroll.setWidget(self.container)
  1281. self.name = None
  1282. self.creation_time = time.time()
  1283. self.object_views = []
  1284. # initiate function stack
  1285. self.stack = QtWidgets.QStackedWidget(self)
  1286. self.stack.setVisible(False)
  1287. self.button_box = QtWidgets.QButtonGroup(self)
  1288. self.stack_buttons = []
  1289. self.stack_widgets = {}
  1290. # initiate standard tab groups and their widget containers
  1291. self.overview_group = QtWidgets.QGroupBox('Data overview')
  1292. self.overview_grid = QtWidgets.QGridLayout(self.overview_group)
  1293. self.overview_widgets = {}
  1294. self.function_group = QtWidgets.QGroupBox('Apply functions')
  1295. self.function_grid = QtWidgets.QGridLayout(self.function_group)
  1296. self.function_widgets = {}
  1297. self.layout.addWidget(self.function_group)
  1298. self.function_group.setVisible(False)
  1299. self.stdout_group = QtWidgets.QGroupBox('Log')
  1300. self.stdout_grid = QtWidgets.QGridLayout(self.stdout_group)
  1301. self.stdout_widgets = {}
  1302. # initiate apply button
  1303. self.apply_button = QtWidgets.QPushButton('Apply')
  1304. self.apply_button.clicked.connect(self.apply_function)
  1305. gui_widgets.mark_primary(self.apply_button)
  1306. self.layout.addWidget(self.apply_button)
  1307. self.apply_button.setVisible(False)
  1308. self.splitter.addWidget(self.stdout_group)
  1309. self.splitter.setStretchFactor(0, 1)
  1310. self.init_stdout_ui()
  1311. def obj(self):
  1312. raise NotImplementedError
  1313. def obj_name(self):
  1314. raise NotImplementedError
  1315. def update_obj(self, obj):
  1316. raise NotImplementedError
  1317. def update_tab(self, is_unsaved: bool = True):
  1318. raise NotImplementedError
  1319. def obj_type(self):
  1320. raise NotImplementedError
  1321. def obj_properties(self) -> dict:
  1322. return {}
  1323. def is_empty(self):
  1324. return True
  1325. def init_overview_ui(self):
  1326. raise NotImplementedError
  1327. def init_function_ui(self):
  1328. self.function_group.setVisible(True)
  1329. sorted_actions = self.get_all_actions()
  1330. self.stack.setVisible(True)
  1331. self.stack.addWidget(QtWidgets.QWidget(self)) # start the stack empty
  1332. i = 0
  1333. for i, action_type in enumerate(sorted_actions):
  1334. bttn = QtWidgets.QPushButton(action_type)
  1335. bttn.setCheckable(True)
  1336. bttn.setStyleSheet("""QPushButton::checked {background-color : purple;
  1337. color: white;
  1338. border: 1px solid #ba32ba;
  1339. border-radius: 4px;}""")
  1340. bttn.setEnabled(len(sorted_actions[action_type]) > 0) # disable the button if there are no relevant actions
  1341. self.stack_widgets[action_type] = FuncTypeStack(sorted_actions[action_type], self.obj())
  1342. self.stack_widgets[action_type].funcSelected.connect(self.apply_button.setVisible)
  1343. self.stack_widgets[action_type].funcSelected.connect(self._check_for_special_functions)
  1344. self.stack_widgets[action_type].geneSetsRequested.connect(self.geneSetsRequested)
  1345. self.stack.addWidget(self.stack_widgets[action_type])
  1346. bttn.clicked.connect(functools.partial(self.stack.setCurrentIndex, i + 1))
  1347. self.button_box.addButton(bttn)
  1348. self.stack_buttons.append(bttn)
  1349. self.function_grid.addWidget(bttn, 0, i)
  1350. self.stack.currentChanged.connect(self._update_stack_status)
  1351. self.function_grid.addWidget(self.stack, 1, 0, 1, i + 1)
  1352. def _check_for_special_functions(self):
  1353. pass
  1354. def _update_stack_status(self, ind: int):
  1355. self.stack.widget(ind).check_selection_status()
  1356. def get_all_actions(self):
  1357. if self.obj() is None:
  1358. raise InternalError('No object was loaded!')
  1359. all_methods = dir(self.obj())
  1360. public_methods = [
  1361. mthd
  1362. for mthd in all_methods
  1363. if (not mthd.startswith('_'))
  1364. and (callable(getattr(type(self.obj()), mthd)))
  1365. and (mthd not in self.EXCLUDED_FUNCS)
  1366. ]
  1367. sorted_methods = {'Filter': [], 'Normalize': [], 'Summarize': [], 'Visualize': [], 'Cluster': [], 'General': []}
  1368. for method in public_methods:
  1369. if method in self.SUMMARY_FUNCS:
  1370. sorted_methods['Summarize'].append(method)
  1371. elif method in self.CLUSTERING_FUNCS:
  1372. sorted_methods['Cluster'].append(method)
  1373. elif method in self.GENERAL_FUNCS:
  1374. sorted_methods['General'].append(method)
  1375. elif 'normalize' in method:
  1376. sorted_methods['Normalize'].append(method)
  1377. elif 'filter' in method or 'split' in method:
  1378. sorted_methods['Filter'].append(method)
  1379. else:
  1380. sorted_methods['Visualize'].append(method)
  1381. return sorted_methods
  1382. def apply_function(self):
  1383. this_stack: FuncTypeStack = self.stack.currentWidget()
  1384. func_name = this_stack.get_function_name()
  1385. func_params = this_stack.get_function_params()
  1386. predecessors = this_stack.get_function_predecessors()
  1387. if func_params.get('inplace', False):
  1388. if func_name in self.THREADED_FUNCS:
  1389. command = InplaceCachedCommand(
  1390. self,
  1391. func_name,
  1392. args=[],
  1393. kwargs=func_params,
  1394. description=f'Apply "{func_name}"',
  1395. predecessors=predecessors,
  1396. )
  1397. else:
  1398. command = InplaceCommand(
  1399. self,
  1400. func_name,
  1401. args=[],
  1402. kwargs=func_params,
  1403. description=f'Apply "{func_name}"',
  1404. predecessors=predecessors,
  1405. )
  1406. self.undo_stack.push(command)
  1407. else:
  1408. self._apply_function_from_params(func_name, args=[], kwargs=func_params, predecessors=predecessors)
  1409. def _apply_function_from_params(
  1410. self, func_name, args: list, kwargs: dict, finish_slot=None, job_id: int = None, predecessors: list = None
  1411. ):
  1412. partial = functools.partial(getattr(self.obj(), func_name), *args, **kwargs)
  1413. source_name = generic.get_method_readable_name(partial.func)
  1414. job_id = JOB_COUNTER.get_id() if job_id is None else job_id
  1415. predecessors = predecessors if isinstance(predecessors, list) else []
  1416. worker = gui_widgets.Worker(partial, job_id, predecessors + [self.tab_id], source_name)
  1417. if func_name in self.THREADED_FUNCS:
  1418. self.startedJob.emit(self, worker, [self.functionApplied.emit, finish_slot])
  1419. return
  1420. prev_name = self.get_tab_name()
  1421. worker.finished.connect(check_run_success)
  1422. worker.finished.connect(self.functionApplied.emit)
  1423. result = worker.run()
  1424. if kwargs.get('inplace', False):
  1425. self.tab_id = JOB_COUNTER.get_id()
  1426. self.itemSpawned.emit(f"'{source_name}'\noutput", self.tab_id, job_id, self.obj())
  1427. self.update_tab(prev_name != self.obj_name())
  1428. self.process_outputs(result, job_id, source_name)
  1429. def process_outputs(self, outputs, job_id: int, source_name: str = ''):
  1430. if isinstance(outputs, (list, tuple)) and len(outputs) == 0:
  1431. return
  1432. elif validation.isinstanceinh(outputs, (filtering.Filter, fastq.filtering.Filter)):
  1433. new_id = JOB_COUNTER.get_id()
  1434. self.itemSpawned.emit(f"'{source_name}'\noutput", new_id, job_id, outputs)
  1435. self.filterObjectCreated.emit(outputs, new_id)
  1436. elif validation.isinstanceinh(outputs, enrichment.FeatureSet):
  1437. new_id = JOB_COUNTER.get_id()
  1438. self.itemSpawned.emit(f"'{source_name}'\noutput", new_id, job_id, outputs)
  1439. self.featureSetCreated.emit(outputs, new_id)
  1440. elif isinstance(outputs, (pl.DataFrame, pl.Series)):
  1441. new_id = JOB_COUNTER.get_id()
  1442. self.itemSpawned.emit(f"'{source_name}'\noutput", new_id, job_id, outputs)
  1443. self.object_views.append(gui_windows.DataFrameView(outputs, source_name, self))
  1444. self.object_views[-1].show()
  1445. elif isinstance(outputs, np.ndarray):
  1446. df = pl.DataFrame(outputs)
  1447. self.process_outputs(df, job_id, source_name)
  1448. elif isinstance(outputs, plt.Figure):
  1449. new_id = JOB_COUNTER.get_id()
  1450. self.itemSpawned.emit(f"'{source_name}'\ngraph", new_id, job_id, outputs)
  1451. elif isinstance(outputs, (tuple, list)):
  1452. if validation.isinstanceiter_inh(
  1453. outputs, (filtering.Filter, fastq.filtering.Filter, enrichment.FeatureSet)
  1454. ):
  1455. dialog = MultiKeepWindow(outputs, job_id, self)
  1456. dialog.accepted.connect(functools.partial(self._multi_keep_window_accepted, dialog, source_name))
  1457. dialog.exec()
  1458. else:
  1459. for output in outputs:
  1460. self.process_outputs(output, job_id, source_name)
  1461. elif isinstance(outputs, dict):
  1462. tab_name = self.get_tab_name()
  1463. for this_src_name, output in outputs.items():
  1464. self.process_outputs(output, job_id, f'{this_src_name} {tab_name}')
  1465. elif isinstance(outputs, Path):
  1466. new_id = JOB_COUNTER.get_id()
  1467. self.itemSpawned.emit(f"'{source_name}'\noutput", new_id, job_id, outputs)
  1468. def _multi_keep_window_accepted(self, dialog: 'MultiKeepWindow', source_name: str):
  1469. kept_outputs = dialog.result()
  1470. job_id = dialog.job_id
  1471. for i, output in enumerate(kept_outputs):
  1472. self.process_outputs(output, job_id, source_name)
  1473. def init_stdout_ui(self):
  1474. self.stdout_widgets['text_edit_stdout'] = gui_widgets.StdOutTextEdit(self)
  1475. self.stdout_widgets['text_edit_stdout'].setStyleSheet("""QTextEdit {background: #dddddd;}""")
  1476. self.stdout_grid.addWidget(self.stdout_widgets['text_edit_stdout'], 0, 0, 3, 4)
  1477. def get_console(self):
  1478. return self.stdout_widgets['text_edit_stdout']
  1479. @QtCore.pyqtSlot()
  1480. @generic.readable_name('Rename tab')
  1481. def rename(self, new_name: str = None):
  1482. if new_name is None:
  1483. new_name = self.overview_widgets['table_name'].text()
  1484. prev_name = self.get_tab_name()
  1485. command = RenameCommand(prev_name, new_name, self, f'Rename "{prev_name}" to "{new_name}"')
  1486. self.undo_stack.push(command)
  1487. def _rename(self, new_name: str = None, job_id: int = None):
  1488. prev_id = self.tab_id
  1489. self.tab_id = JOB_COUNTER.get_id() if job_id is not None else prev_id
  1490. self.tabNameChange.emit(new_name, True)
  1491. self.overview_widgets['table_name_label'].setText(f"Table name: '<b>{new_name}</b>'")
  1492. self.overview_widgets['table_name'].setText('')
  1493. self.name = new_name.rstrip('*')
  1494. if job_id is not None:
  1495. worker_output = gui_widgets.WorkerOutput(
  1496. self.obj(), functools.partial(self.rename, new_name=new_name), job_id, [prev_id]
  1497. )
  1498. self.functionApplied.emit(worker_output)
  1499. self.itemSpawned.emit(self.name, self.tab_id, job_id, self.obj())
  1500. def cache(self):
  1501. raise NotImplementedError
  1502. def get_tab_name(self):
  1503. return self.name.rstrip('*')
  1504. # custom method to write anything printed out to console/terminal to my QTextEdit widget via append function.
  1505. def output_terminal_written(self, text):
  1506. self.stdout_widgets['stdout'].append(text)
  1507. class SetTabPage(TabPage):
  1508. EXCLUDED_FUNCS = {'union', 'intersection', 'majority_vote_intersection', 'difference', 'symmetric_difference'}
  1509. SUMMARY_FUNCS = {'biotypes_from_ref_table', 'biotypes_from_gtf'}
  1510. GENERAL_FUNCS = {
  1511. 'translate_gene_ids',
  1512. 'map_orthologs_phylomedb',
  1513. 'map_orthologs_orthoinspector',
  1514. 'map_orthologs_ensembl',
  1515. 'map_orthologs_panther',
  1516. 'find_paralogs_panther',
  1517. 'find_paralogs_ensembl',
  1518. }
  1519. THREADED_FUNCS = {
  1520. 'translate_gene_ids',
  1521. 'filter_by_kegg_annotations',
  1522. 'filter_by_go_annotations',
  1523. 'map_orthologs_phylomedb',
  1524. 'map_orthologs_orthoinspector',
  1525. 'map_orthologs_ensembl',
  1526. 'map_orthologs_panther',
  1527. 'find_paralogs_panther',
  1528. 'find_paralogs_ensembl',
  1529. }
  1530. def __init__(
  1531. self,
  1532. set_name: str,
  1533. gene_set: typing.Union[set, enrichment.FeatureSet] = None,
  1534. parent=None,
  1535. undo_stack: QtGui.QUndoStack = None,
  1536. tab_id: int = None,
  1537. ):
  1538. super().__init__(parent, undo_stack, tab_id)
  1539. if gene_set is None:
  1540. gene_set = enrichment.FeatureSet(set(), set_name)
  1541. elif isinstance(gene_set, set):
  1542. gene_set = enrichment.FeatureSet(gene_set, set_name)
  1543. self.gene_set = gene_set
  1544. self.name = set_name
  1545. self.init_overview_ui()
  1546. self.init_function_ui()
  1547. def obj(self):
  1548. return self.gene_set
  1549. def obj_name(self):
  1550. return self.gene_set.set_name
  1551. def update_obj(self, obj: set):
  1552. self.update_gene_set(obj)
  1553. def obj_type(self):
  1554. return type(self.gene_set)
  1555. def get_index_string(self):
  1556. return '\n'.join(self.obj())
  1557. def start_from_gene_set(self, tab_id: int, gene_set: set):
  1558. self.tab_id = tab_id
  1559. self.update_obj(gene_set)
  1560. def init_overview_ui(self):
  1561. this_row = 0
  1562. self.layout.insertWidget(0, self.overview_group)
  1563. self.overview_widgets['table_name_label'] = QtWidgets.QLabel(f"Gene set name: '<b>{self.obj().set_name}</b>'")
  1564. self.overview_widgets['table_name_label'].setWordWrap(True)
  1565. self.overview_widgets['preview'] = gui_windows.ReactiveListWidget()
  1566. self.update_set_preview()
  1567. self.overview_grid.addWidget(self.overview_widgets['table_name_label'], this_row, 0, 1, 4)
  1568. this_row += 1
  1569. self.overview_widgets['table_name'] = QtWidgets.QLineEdit()
  1570. self.overview_widgets['rename_label'] = QtWidgets.QLabel('Rename your gene set (optional):')
  1571. self.overview_widgets['rename_button'] = QtWidgets.QPushButton('Rename')
  1572. self.overview_widgets['rename_button'].clicked.connect(self.rename)
  1573. self.overview_grid.addWidget(self.overview_widgets['rename_label'], this_row, 0)
  1574. self.overview_grid.addWidget(self.overview_widgets['table_name'], this_row, 1, 1, 2)
  1575. self.overview_grid.addWidget(self.overview_widgets['rename_button'], this_row, 3)
  1576. this_row += 1
  1577. self.overview_grid.addWidget(self.overview_widgets['preview'], this_row, 0, 1, 4)
  1578. this_row += 1
  1579. self.overview_widgets['shape'] = QtWidgets.QLabel()
  1580. self.overview_grid.addWidget(self.overview_widgets['shape'], this_row, 0, 1, 2)
  1581. self.update_set_shape()
  1582. self.overview_widgets['view_button'] = QtWidgets.QPushButton('View full gene set')
  1583. self.overview_widgets['view_button'].clicked.connect(self.view_full_gene_set)
  1584. self.overview_grid.addWidget(self.overview_widgets['view_button'], this_row, 2, 2, 1)
  1585. self.overview_widgets['save_button'] = QtWidgets.QPushButton('Save gene set')
  1586. self.overview_widgets['save_button'].clicked.connect(self.save_file)
  1587. self.overview_grid.addWidget(self.overview_widgets['save_button'], this_row, 3, 2, 1)
  1588. this_row += 1
  1589. self.overview_grid.addWidget(QtWidgets.QLabel(''), this_row, 0, 1, 1)
  1590. this_row += 1
  1591. def view_full_gene_set(self):
  1592. set_window = gui_windows.GeneSetView(self.gene_set.gene_set, self.get_tab_name(), self)
  1593. self.overview_widgets['full_table_view'] = set_window
  1594. set_window.show()
  1595. def save_file(self):
  1596. default_name = parsing.slugify(self.get_tab_name().rstrip('*')) + '.txt'
  1597. filename, _ = QtWidgets.QFileDialog.getSaveFileName(
  1598. self, 'Save gene set', str(Path.home().joinpath(default_name)), 'Text document (*.txt);;All Files (*)'
  1599. )
  1600. if filename:
  1601. self.gene_set.save_txt(filename)
  1602. print(f'Successfully saved at {io.get_datetime()} under {filename}')
  1603. self.tabSaved.emit()
  1604. @QtCore.pyqtSlot()
  1605. def _rename(self, new_name: str = None, job_id: int = None):
  1606. self.gene_set.change_set_name(new_name)
  1607. super()._rename(new_name, job_id)
  1608. def cache(self) -> str:
  1609. base_str = str(time.time_ns()) + self.get_tab_name() + str(len(self.gene_set))
  1610. hex_hash = hashlib.sha1(base_str.encode('utf-8')).hexdigest()
  1611. filename = f'{hex_hash}.txt'
  1612. io.cache_gui_file(self.gene_set.gene_set, filename)
  1613. return filename
  1614. def is_empty(self):
  1615. return self.gene_set is None or len(self.gene_set) == 0
  1616. def update_set_shape(self):
  1617. if self.gene_set is not None:
  1618. shape = len(self.gene_set)
  1619. self.overview_widgets['shape'].setText(f'This gene set contains {shape} features')
  1620. def update_set_preview(self):
  1621. if self.gene_set is not None:
  1622. self.overview_widgets['preview'].addItems([str(item) for item in self.gene_set])
  1623. if self.overview_widgets['preview'].count() > 0:
  1624. item_height = self.overview_widgets['preview'].sizeHintForRow(0)
  1625. self.overview_widgets['preview'].setFixedHeight((item_height + 8) * 4)
  1626. def update_gene_set(self, gene_set: set, set_name: str = None):
  1627. if isinstance(gene_set, enrichment.FeatureSet):
  1628. self.gene_set = gene_set
  1629. else:
  1630. set_name = self.gene_set.set_name if set_name is None else set_name
  1631. self.gene_set = enrichment.FeatureSet(gene_set, set_name)
  1632. self.update_tab()
  1633. def update_tab(self, is_unsaved: bool = True):
  1634. self.update_set_shape()
  1635. self.update_set_preview()
  1636. self.changeIcon.emit('set')
  1637. def get_all_actions(self):
  1638. sorted_methods = super().get_all_actions()
  1639. for discarded_category in ('Normalize', 'Cluster', 'Visualize'):
  1640. sorted_methods[discarded_category] = []
  1641. return sorted_methods
  1642. class FuncTypeStack(QtWidgets.QWidget):
  1643. EXCLUDED_PARAMS = {'self', 'backend', 'gui_mode', 'legacy_args', 'function_kwargs'}
  1644. NO_FUNC_CHOSEN_TEXT = 'Choose a function...'
  1645. funcSelected = QtCore.pyqtSignal(bool)
  1646. geneSetsRequested = QtCore.pyqtSignal(object)
  1647. __slots__ = {
  1648. 'parameter_widgets': 'widgets for function parameters',
  1649. 'layout': 'layout',
  1650. 'parameter_grid': 'layout for function parameters',
  1651. 'func_combo': 'combo box for choosing functions',
  1652. 'func_help_button': 'help button for function combo box',
  1653. 'func_combo_layout': 'layout for function combo box',
  1654. 'func': 'dict of functions',
  1655. 'filter_obj': 'filtering.Filter object to apply functions to',
  1656. 'excluded_params': 'parameters to be excluded from functions',
  1657. 'pipeline_mode': 'indicating if in the function selector is in Pipeline mode',
  1658. }
  1659. def __init__(
  1660. self,
  1661. funcs: typing.Iterable,
  1662. filter_obj: filtering.Filter,
  1663. parent=None,
  1664. additional_excluded_params: set = None,
  1665. pipeline_mode: bool = False,
  1666. ):
  1667. super().__init__(parent)
  1668. self.parameter_widgets = {}
  1669. self.layout = QtWidgets.QVBoxLayout(self)
  1670. self.parameter_grid = QtWidgets.QGridLayout()
  1671. self.func_combo = QtWidgets.QComboBox(self)
  1672. self.func_help_button = gui_widgets.HelpButton(self)
  1673. self.func_combo_layout = QtWidgets.QHBoxLayout()
  1674. self.funcs = {}
  1675. for func in funcs:
  1676. self.funcs[generic.get_method_readable_name(func, filter_obj)] = func
  1677. self.filter_obj = filter_obj
  1678. self.excluded_params = self.EXCLUDED_PARAMS.copy()
  1679. if additional_excluded_params is not None:
  1680. self.excluded_params.update(additional_excluded_params)
  1681. self.pipeline_mode = pipeline_mode
  1682. self.init_ui()
  1683. def init_ui(self):
  1684. self.layout.addLayout(self.func_combo_layout)
  1685. self.layout.addLayout(self.parameter_grid)
  1686. self.func_combo_layout.addWidget(self.func_combo)
  1687. self.func_combo_layout.addWidget(self.func_help_button)
  1688. self._set_empty_tooltip()
  1689. self.layout.addStretch(1)
  1690. self.func_combo.addItem(self.NO_FUNC_CHOSEN_TEXT)
  1691. self.func_combo.addItems(sorted(self.funcs.keys()))
  1692. self.func_combo.currentTextChanged.connect(self.update_parameter_ui)
  1693. def _set_empty_tooltip(self):
  1694. txt = 'Choose a function from this list to read its description. '
  1695. self.func_combo.setToolTip(txt)
  1696. self.func_help_button.set_desc_help(txt)
  1697. def deselect(self):
  1698. self.func_combo.setCurrentIndex(0)
  1699. def check_selection_status(self):
  1700. self.funcSelected.emit(self.get_function_name() != self.NO_FUNC_CHOSEN_TEXT)
  1701. def update_parameter_ui(self):
  1702. # delete previous widgets
  1703. gui_widgets.clear_layout(self.parameter_grid)
  1704. self.parameter_widgets = {}
  1705. chosen_func_name = self.get_function_name()
  1706. if chosen_func_name == self.NO_FUNC_CHOSEN_TEXT:
  1707. self._set_empty_tooltip()
  1708. self.funcSelected.emit(False)
  1709. return
  1710. signature = generic.get_method_signature(chosen_func_name, self.filter_obj)
  1711. desc, param_desc = io.get_method_docstring(chosen_func_name, self.filter_obj)
  1712. func = getattr(self.filter_obj, chosen_func_name, None)
  1713. self.func_combo.setToolTip(desc)
  1714. self.func_help_button.set_param_help(self.get_function_readable_name(), desc)
  1715. i = 1
  1716. for name, param in signature.items():
  1717. if name in self.excluded_params:
  1718. continue
  1719. self.parameter_widgets[name] = gui_widgets.param_to_widget(param, name, pipeline_mode=self.pipeline_mode)
  1720. self.connect_widget(self.parameter_widgets[name])
  1721. label = QtWidgets.QLabel(f'{generic.get_param_readable_name(name, func)}:', self.parameter_widgets[name])
  1722. if name in param_desc:
  1723. label.setToolTip(param_desc[name])
  1724. help_button = gui_widgets.HelpButton()
  1725. self.parameter_grid.addWidget(help_button, i, 2)
  1726. help_button.set_param_help(name, param_desc[name])
  1727. self.parameter_grid.addWidget(label, i, 0)
  1728. self.parameter_grid.addWidget(self.parameter_widgets[name], i, 1)
  1729. i += 1
  1730. help_address = (
  1731. f'https://guyteichman.github.io/RNAlysis/build/rnalysis.filtering.'
  1732. f'{type(self.filter_obj).__name__}.{chosen_func_name}.html'
  1733. )
  1734. self.parameter_widgets['help_link'] = QtWidgets.QLabel(
  1735. text=f'<a href="{help_address}">Open documentation for function '
  1736. f'<b>{type(self.filter_obj).__name__}.{chosen_func_name}</b></a>'
  1737. )
  1738. self.parameter_widgets['help_link'].setOpenExternalLinks(True)
  1739. self.parameter_grid.addWidget(self.parameter_widgets['help_link'], i + 1, 0, 1, 2)
  1740. self.parameter_grid.setColumnStretch(1, 1)
  1741. self.funcSelected.emit(True)
  1742. def connect_widget(self, widget: QtWidgets.QWidget):
  1743. if self.pipeline_mode:
  1744. return
  1745. if isinstance(widget, (gui_widgets.ComboBoxOrOtherWidget, gui_widgets.OptionalWidget)):
  1746. self.connect_widget(widget.other)
  1747. elif isinstance(widget, (gui_widgets.TableColumnPicker, gui_widgets.TableColumnPicker)):
  1748. widget.add_columns(self.filter_obj.columns)
  1749. elif isinstance(widget, gui_widgets.GeneSetComboBox):
  1750. widget.boxOpened.connect(functools.partial(self.geneSetsRequested.emit, widget))
  1751. def get_function_params(self):
  1752. func_params = {}
  1753. for param_name, widget in self.parameter_widgets.items():
  1754. if param_name in {'function_combo', 'help_link'}:
  1755. continue
  1756. val = gui_widgets.get_val_from_widget(widget)
  1757. func_params[param_name] = val
  1758. return func_params
  1759. def get_function_predecessors(self):
  1760. predecessors = []
  1761. for param_name, widget in self.parameter_widgets.items():
  1762. if isinstance(widget, (gui_widgets.OptionalWidget, gui_widgets.ComboBoxOrOtherWidget)):
  1763. if widget.other.isEnabled():
  1764. widget = widget.other
  1765. if isinstance(widget, gui_widgets.GeneSetComboBox):
  1766. predecessors.append(widget.current_id())
  1767. return predecessors
  1768. def get_function_readable_name(self):
  1769. return self.func_combo.currentText()
  1770. def get_function_name(self):
  1771. readable_name = self.get_function_readable_name()
  1772. if readable_name == self.NO_FUNC_CHOSEN_TEXT:
  1773. return self.NO_FUNC_CHOSEN_TEXT
  1774. name = self.funcs[readable_name]
  1775. return name
  1776. class FilterTabPage(TabPage):
  1777. EXCLUDED_FUNCS = {
  1778. 'union',
  1779. 'intersection',
  1780. 'majority_vote_intersection',
  1781. 'difference',
  1782. 'symmetric_difference',
  1783. 'from_folder',
  1784. 'from_folder_htseqcount',
  1785. 'save_txt',
  1786. 'save_csv',
  1787. 'save_table',
  1788. 'save_parquet',
  1789. 'from_dataframe',
  1790. 'print_features',
  1791. }
  1792. CLUSTERING_FUNCS = {
  1793. 'split_kmeans': 'K-Means',
  1794. 'split_kmedoids': 'K-Medoids',
  1795. 'split_hierarchical': 'Hierarchical (Agglomerative)',
  1796. 'split_hdbscan': 'HDBSCAN',
  1797. 'split_clicom': 'CLICOM (Ensemble)',
  1798. }
  1799. SUMMARY_FUNCS = {'describe', 'head', 'tail', 'biotypes_from_ref_table', 'biotypes_from_gtf', 'print_features'}
  1800. GENERAL_FUNCS = {
  1801. 'concatenate',
  1802. 'sort',
  1803. 'sort_by_principal_component',
  1804. 'transform',
  1805. 'translate_gene_ids',
  1806. 'annotate_from_gtf',
  1807. 'differential_expression_deseq2',
  1808. 'differential_expression_deseq2_simplified',
  1809. 'fold_change',
  1810. 'average_replicate_samples',
  1811. 'drop_columns',
  1812. 'differential_expression_limma_voom',
  1813. 'differential_expression_limma_voom_simplified',
  1814. 'map_orthologs_phylomedb',
  1815. 'map_orthologs_orthoinspector',
  1816. 'map_orthologs_ensembl',
  1817. 'map_orthologs_panther',
  1818. 'find_paralogs_panther',
  1819. 'find_paralogs_ensembl',
  1820. }
  1821. THREADED_FUNCS = {
  1822. 'translate_gene_ids',
  1823. 'differential_expression_deseq2',
  1824. 'differential_expression_deseq2_simplified',
  1825. 'filter_by_kegg_annotations',
  1826. 'filter_by_go_annotations',
  1827. 'differential_expression_limma_voom',
  1828. 'differential_expression_limma_voom_simplified',
  1829. 'map_orthologs_phylomedb',
  1830. 'map_orthologs_orthoinspector',
  1831. 'map_orthologs_ensembl',
  1832. 'map_orthologs_panther',
  1833. 'find_paralogs_panther',
  1834. 'find_paralogs_ensembl',
  1835. }
  1836. startedClustering = QtCore.pyqtSignal(object, object, object)
  1837. widthChanged = QtCore.pyqtSignal()
  1838. def __init__(self, parent=None, undo_stack: QtGui.QUndoStack = None, tab_id: int = None):
  1839. super().__init__(parent, undo_stack, tab_id)
  1840. self.filter_obj = None
  1841. self._last_autodetected_path = None
  1842. self.basic_group = QtWidgets.QGroupBox('Load a data table')
  1843. self.basic_grid = QtWidgets.QGridLayout(self.basic_group)
  1844. self.basic_widgets = {}
  1845. self.basic_param_container = QtWidgets.QWidget(self)
  1846. self.basic_param_widgets = {}
  1847. self.basic_param_grid = QtWidgets.QGridLayout(self.basic_param_container)
  1848. self.clicom_window = None
  1849. self.deseq_window = None
  1850. self.limma_window = None
  1851. self.widthChanged.connect(self.update_table_preview_width)
  1852. self.init_basic_ui()
  1853. def obj(self):
  1854. return self.filter_obj
  1855. def obj_name(self):
  1856. name = self.filter_obj.fname.name
  1857. for suffix in ['.csv', '.tsv', '.txt', '.parquet']:
  1858. if self.filter_obj.fname.suffix == suffix:
  1859. name = self.filter_obj.fname.stem
  1860. break
  1861. return name
  1862. def update_obj(self, obj: filtering.Filter):
  1863. self.filter_obj = obj
  1864. self.update_tab()
  1865. def obj_type(self):
  1866. return type(self.filter_obj)
  1867. def obj_properties(self) -> dict:
  1868. if self.obj_type() == filtering.CountFilter:
  1869. return dict(is_normalized=self.filter_obj.is_normalized)
  1870. elif self.obj_type() == filtering.DESeqFilter:
  1871. return dict(
  1872. log2fc_col=self.filter_obj.log2fc_col,
  1873. padj_col=self.filter_obj.padj_col,
  1874. pval_col=self.filter_obj.pval_col,
  1875. )
  1876. elif self.obj_type() == filtering.FoldChangeFilter:
  1877. return dict(numerator_name=self.filter_obj.numerator, denominator_name=self.filter_obj.denominator)
  1878. else:
  1879. return {}
  1880. @QtCore.pyqtSlot()
  1881. def _rename(self, new_name: str = None, job_id: int = None):
  1882. self.filter_obj.fname = Path(
  1883. os.path.join(str(self.filter_obj.fname.parent), f'{new_name}{self.filter_obj.fname.suffix}')
  1884. )
  1885. super()._rename(new_name, job_id)
  1886. def cache(self):
  1887. base_str = str(time.time_ns()) + str(self.filter_obj.fname) + str(len(self.filter_obj.shape))
  1888. hex_hash = hashlib.sha1(base_str.encode('utf-8')).hexdigest()
  1889. filename = f'{hex_hash}.parquet'
  1890. io.cache_gui_file(self.filter_obj.df, filename)
  1891. return filename
  1892. def is_empty(self):
  1893. return self.filter_obj is None
  1894. def get_table_type(self):
  1895. return FILTER_OBJ_TYPES_INV[type(self.filter_obj).__name__]
  1896. def update_table_name_label(self):
  1897. self.overview_widgets['table_name_label'].setText(f"Table name: '<b>{self.get_tab_name()}</b>'")
  1898. self.overview_widgets['table_name_label'].setWordWrap(True)
  1899. def init_overview_ui(self):
  1900. this_row = 0
  1901. self.layout.insertWidget(1, self.overview_group)
  1902. self.overview_widgets['table_type_label'] = QtWidgets.QLabel(f'Table type: {self.get_table_type()}')
  1903. self.overview_widgets['table_name_label'] = QtWidgets.QLabel()
  1904. self.overview_widgets['table_name_label'].setWordWrap(True)
  1905. self.overview_widgets['preview'] = gui_windows.ReactiveTableView()
  1906. self.overview_widgets['preview'].setVerticalScrollBarPolicy(QtCore.Qt.ScrollBarPolicy.ScrollBarAlwaysOff)
  1907. self.overview_widgets['preview'].setHorizontalScrollBarPolicy(QtCore.Qt.ScrollBarPolicy.ScrollBarAlwaysOff)
  1908. self.overview_grid.addWidget(self.overview_widgets['table_name_label'], this_row, 0, 1, 4)
  1909. this_row += 1
  1910. self.overview_widgets['table_name'] = QtWidgets.QLineEdit()
  1911. self.overview_widgets['rename_label'] = QtWidgets.QLabel('Rename your table (optional):')
  1912. self.overview_widgets['rename_button'] = QtWidgets.QPushButton('Rename')
  1913. self.overview_widgets['rename_button'].clicked.connect(self.rename)
  1914. self.overview_grid.addWidget(self.overview_widgets['rename_label'], this_row, 0)
  1915. self.overview_grid.addWidget(self.overview_widgets['table_name'], this_row, 1, 1, 2)
  1916. self.overview_grid.addWidget(self.overview_widgets['rename_button'], this_row, 3)
  1917. this_row += 1
  1918. self.overview_grid.addWidget(self.overview_widgets['preview'], this_row, 0, 1, 4)
  1919. this_row += 1
  1920. self.overview_widgets['save_button'] = QtWidgets.QPushButton('Save table')
  1921. self.overview_widgets['save_button'].clicked.connect(self.save_file)
  1922. self.overview_grid.addWidget(self.overview_widgets['save_button'], this_row, 3, 2, 1)
  1923. self.overview_widgets['shape'] = QtWidgets.QLabel()
  1924. self.overview_grid.addWidget(self.overview_widgets['shape'], this_row, 0, 1, 2)
  1925. self.overview_widgets['view_button'] = QtWidgets.QPushButton('View full table')
  1926. self.overview_widgets['view_button'].clicked.connect(self.view_full_dataframe)
  1927. self.overview_grid.addWidget(self.overview_widgets['view_button'], this_row, 2, 2, 1)
  1928. this_row += 1
  1929. self.overview_grid.addWidget(self.overview_widgets['table_type_label'], this_row, 0, 1, 1)
  1930. this_row += 1
  1931. self.update_tab(False)
  1932. def update_filter_obj_shape(self):
  1933. if self.is_empty():
  1934. return
  1935. shape = self.filter_obj.shape
  1936. if len(shape) == 1:
  1937. shape = (shape[0], 1)
  1938. self.overview_widgets['shape'].setText(f'This table contains {shape[0]} rows, {shape[1]} columns')
  1939. def _change_start_button_state(self, is_legal: bool):
  1940. self.basic_widgets['start_button'].setEnabled(is_legal)
  1941. def _autodetect_table_type(self, is_legal: bool):
  1942. # When a valid file is chosen, pre-select the most likely table type as a convenience default.
  1943. # This is fully overridable: the user can still pick any type from the combo afterwards.
  1944. if not is_legal:
  1945. return
  1946. file_path = self.basic_widgets['file_path'].text()
  1947. if file_path == self._last_autodetected_path:
  1948. return
  1949. self._last_autodetected_path = file_path
  1950. detected = filtering.infer_table_type(file_path)
  1951. if detected in FILTER_OBJ_TYPES:
  1952. self.basic_widgets['table_type_combo'].setCurrentText(detected)
  1953. def update_basic_ui(self):
  1954. # clear previous layout
  1955. gui_widgets.clear_layout(self.basic_param_grid)
  1956. self.basic_param_widgets = {}
  1957. func_name = '__init__'
  1958. filter_obj_type = FILTER_OBJ_TYPES[self.basic_widgets['table_type_combo'].currentText()]
  1959. signature = generic.get_method_signature(func_name, filter_obj_type)
  1960. desc, param_desc = io.get_method_docstring(func_name, filter_obj_type)
  1961. func = getattr(filter_obj_type, func_name, None)
  1962. self.basic_widgets['table_type_combo'].setToolTip(desc)
  1963. i = 1
  1964. for name, param in signature.items():
  1965. if name in INIT_EXCLUDED_PARAMS:
  1966. continue
  1967. self.basic_param_widgets[name] = gui_widgets.param_to_widget(param, name)
  1968. label = QtWidgets.QLabel(f'{generic.get_param_readable_name(name, func)}:', self.basic_param_widgets[name])
  1969. if name in param_desc:
  1970. label.setToolTip(param_desc[name])
  1971. help_button = gui_widgets.HelpButton()
  1972. self.basic_param_grid.addWidget(help_button, i, 2)
  1973. help_button.set_param_help(name, param_desc[name])
  1974. self.basic_param_grid.addWidget(label, i, 0)
  1975. self.basic_param_grid.addWidget(self.basic_param_widgets[name], i, 1)
  1976. i += 1
  1977. def init_basic_ui(self):
  1978. self.layout.insertWidget(0, self.basic_group)
  1979. self.basic_widgets['table_type_combo'] = QtWidgets.QComboBox()
  1980. self.basic_widgets['table_type_combo'].addItems(FILTER_OBJ_TYPES.keys())
  1981. self.basic_widgets['table_type_combo'].currentIndexChanged.connect(self.update_basic_ui)
  1982. self.basic_widgets['table_type_combo'].setCurrentText('Other table')
  1983. self.basic_widgets['start_button'] = QtWidgets.QPushButton('Load')
  1984. self.basic_widgets['start_button'].clicked.connect(self.start)
  1985. self.basic_widgets['start_button'].setEnabled(False)
  1986. gui_widgets.mark_primary(self.basic_widgets['start_button'])
  1987. self.basic_widgets['file_path'] = gui_widgets.PathLineEdit(
  1988. button_text='Choose table', file_types='Data table (*.csv;*.tsv;*.txt;*.parquet);;All Files (*)'
  1989. )
  1990. self.basic_widgets['file_path'].textChanged.connect(self._change_start_button_state)
  1991. self.basic_widgets['file_path'].textChanged.connect(self._autodetect_table_type)
  1992. self.basic_widgets['table_name'] = QtWidgets.QLineEdit()
  1993. self.basic_widgets['file_label'] = QtWidgets.QLabel('Choose a file:')
  1994. self.basic_widgets['type_label'] = QtWidgets.QLabel('Choose table type:')
  1995. self.basic_widgets['name_label'] = QtWidgets.QLabel('Name your table (optional):')
  1996. self.basic_grid.addWidget(self.basic_widgets['file_label'], 0, 0, 1, 2)
  1997. self.basic_grid.addWidget(self.basic_widgets['type_label'], 0, 2)
  1998. self.basic_grid.addWidget(self.basic_widgets['name_label'], 0, 3)
  1999. self.basic_grid.addWidget(self.basic_widgets['file_path'], 1, 0, 1, 2)
  2000. self.basic_grid.addWidget(self.basic_widgets['table_type_combo'], 1, 2)
  2001. self.basic_grid.addWidget(self.basic_widgets['table_name'], 1, 3)
  2002. self.basic_grid.addWidget(self.basic_param_container, 2, 0, 1, 4)
  2003. self.basic_grid.addWidget(self.basic_widgets['start_button'], 3, 0, 1, 4)
  2004. self.basic_grid.addWidget(QtWidgets.QWidget(self), 4, 0, 1, 4)
  2005. self.basic_grid.addWidget(QtWidgets.QWidget(self), 0, 4, 4, 1)
  2006. self.basic_grid.setRowStretch(4, 1)
  2007. # give the extra horizontal space to the file path field (column 1) instead of the
  2008. # trailing padding column, so the field actually widens with the window
  2009. self.basic_grid.setColumnStretch(1, 1)
  2010. self.basic_grid.setColumnStretch(4, 1)
  2011. def _check_for_special_functions(self, is_selected: bool = True):
  2012. if not is_selected:
  2013. return
  2014. this_stack: FuncTypeStack = self.stack.currentWidget()
  2015. func_name = this_stack.get_function_name()
  2016. if func_name == 'split_clicom':
  2017. other_clustering_funcs = self.CLUSTERING_FUNCS.copy()
  2018. other_clustering_funcs.pop('split_clicom')
  2019. this_stack.deselect()
  2020. self.clicom_window = ClicomWindow(other_clustering_funcs, self.filter_obj, self)
  2021. self.clicom_window.paramsAccepted.connect(functools.partial(self._apply_function_from_params, func_name))
  2022. self.clicom_window.show()
  2023. elif func_name.startswith('differential_expression_deseq2'):
  2024. this_stack.deselect()
  2025. window_type = SimpleDESeqWindow if func_name.endswith('simplified') else DESeqWindow
  2026. self.deseq_window = window_type(self)
  2027. self.deseq_window.paramsAccepted.connect(functools.partial(self._apply_function_from_params, func_name))
  2028. self.deseq_window.show()
  2029. elif func_name.startswith('differential_expression_limma_voom'):
  2030. this_stack.deselect()
  2031. window_type = SimpleLimmaWindow if func_name.endswith('simplified') else LimmaWindow
  2032. self.limma_window = window_type(self)
  2033. self.limma_window.paramsAccepted.connect(functools.partial(self._apply_function_from_params, func_name))
  2034. self.limma_window.show()
  2035. def view_full_dataframe(self):
  2036. df_window = gui_windows.DataFrameView(self.filter_obj.df, self.name, self)
  2037. self.overview_widgets['full_table_view'] = df_window
  2038. df_window.show()
  2039. def update_table_preview(self):
  2040. if self.is_empty():
  2041. return
  2042. model = gui_windows.DataFramePreviewModel(self.filter_obj.df)
  2043. self.overview_widgets['preview'].setModel(model)
  2044. self.update_table_preview_width()
  2045. def update_table_preview_width(self):
  2046. if self.is_empty():
  2047. return
  2048. self.overview_widgets['preview'].resizeColumnsToContents()
  2049. self.overview_widgets['preview'].resizeRowsToContents()
  2050. new_width = min(
  2051. self.overview_widgets['preview'].horizontalHeader().length()
  2052. + self.overview_widgets['preview'].verticalHeader().width(),
  2053. self.width() - 100,
  2054. )
  2055. self.overview_widgets['preview'].setFixedWidth(new_width)
  2056. self.overview_widgets['preview'].setFixedHeight(
  2057. self.overview_widgets['preview'].verticalHeader().length()
  2058. + self.overview_widgets['preview'].horizontalHeader().height()
  2059. )
  2060. def resizeEvent(self, a0: QtGui.QResizeEvent) -> None:
  2061. if a0.size().width() != a0.oldSize().width():
  2062. self.widthChanged.emit()
  2063. def update_tab(self, is_unsaved: bool = True):
  2064. if self.is_empty():
  2065. return
  2066. self.update_filter_obj_shape()
  2067. self.update_table_preview()
  2068. self.name = str(self.obj_name())
  2069. self.tabNameChange.emit(self.name, is_unsaved)
  2070. self.update_table_name_label()
  2071. def _apply_function_from_params(
  2072. self, func_name, args: list, kwargs: dict, finish_slot=None, job_id: int = None, predecessors: list = None
  2073. ):
  2074. # since clustering functions can be computationally intensive, start them in another thread.
  2075. # furthermode, make sure they use the 'multiprocessing' backend instead of the 'loky' backend -
  2076. # otherwise this could cause issues in Pyinstaller-frozen versions of RNAlysis
  2077. if func_name in self.CLUSTERING_FUNCS:
  2078. kwargs['gui_mode'] = True
  2079. partial = functools.partial(getattr(self.filter_obj, func_name), *args, **kwargs)
  2080. predecessors = predecessors if isinstance(predecessors, list) else []
  2081. worker = gui_widgets.Worker(partial, JOB_COUNTER.get_id(), predecessors + [self.tab_id], func_name)
  2082. worker.finished.connect(self.functionApplied.emit)
  2083. self.startedClustering.emit(self, worker, finish_slot)
  2084. return
  2085. return super()._apply_function_from_params(func_name, args, kwargs, finish_slot, job_id, predecessors)
  2086. def apply_pipeline(self, pipeline: filtering.Pipeline, pipeline_name: str, pipeline_id: int, inplace: bool):
  2087. predecessors = [pipeline_id]
  2088. if inplace:
  2089. command = PipelineInplaceCommand(
  2090. self, pipeline, pipeline_name, f'Apply Pipeline "{pipeline_name}"', predecessors
  2091. )
  2092. self.undo_stack.push(command)
  2093. else:
  2094. self._apply_pipeline(pipeline, pipeline_name, False, JOB_COUNTER.get_id(), predecessors)
  2095. def _apply_pipeline(
  2096. self, pipeline, pipeline_name: str, inplace: bool, job_id: int = None, predecessors: list = None
  2097. ):
  2098. job_name = f'Pipeline {pipeline_name} on {self.get_tab_name()}'
  2099. partial = functools.partial(pipeline.apply_to, self.filter_obj, inplace)
  2100. job_id = JOB_COUNTER.get_id() if job_id is None else job_id
  2101. predecessors = predecessors if isinstance(predecessors, list) else []
  2102. worker = gui_widgets.Worker(partial, job_id, predecessors + [self.tab_id], f"Pipeline '{pipeline_name}'")
  2103. worker.finished.connect(check_run_success)
  2104. worker.finished.connect(self.functionApplied.emit)
  2105. prev_name = self.get_tab_name()
  2106. result = worker.run()
  2107. self.update_tab(prev_name != self.filter_obj.fname.name)
  2108. self.process_outputs(result, job_id, job_name)
  2109. def get_index_string(self):
  2110. if self.is_empty():
  2111. return ''
  2112. return self.filter_obj.index_string
  2113. def save_file(self):
  2114. if self.filter_obj is None:
  2115. warnings.warn('Cannot save an empty tab!')
  2116. return
  2117. default_name = parsing.slugify(str(self.filter_obj.fname.stem).rstrip('*')) + '.csv'
  2118. filename, _ = QtWidgets.QFileDialog.getSaveFileName(
  2119. self,
  2120. 'Save filtering result',
  2121. str(Path.home().joinpath(default_name)),
  2122. 'Comma-Separated Values (*.csv);;Tab-Separated Values (*.tsv);;Parquet file (*.parquet);;All Files (*)',
  2123. )
  2124. if filename:
  2125. suffix = Path(filename).suffix
  2126. self.filter_obj.save_table(suffix, filename)
  2127. print(f'Successfully saved at {io.get_datetime()} under {filename}')
  2128. self.tabSaved.emit()
  2129. def start(self):
  2130. filter_obj_type = FILTER_OBJ_TYPES[self.basic_widgets['table_type_combo'].currentText()]
  2131. file_path = self.basic_widgets['file_path'].text()
  2132. kwargs = {}
  2133. for name, widget in self.basic_param_widgets.items():
  2134. kwargs[name] = gui_widgets.get_val_from_widget(widget)
  2135. self.filter_obj = filter_obj_type(file_path, **kwargs)
  2136. table_name_user_input = self.basic_widgets['table_name'].text()
  2137. if table_name_user_input != '':
  2138. new_name = table_name_user_input
  2139. self.filter_obj._update(fname=Path(new_name).with_suffix('.csv'))
  2140. else:
  2141. new_name = self.filter_obj.fname.stem
  2142. self.tab_id = JOB_COUNTER.get_id()
  2143. print(self.filter_obj)
  2144. self.tabNameChange.emit(new_name, False)
  2145. self.init_overview_ui()
  2146. self.init_function_ui()
  2147. gui_widgets.clear_layout(self.basic_grid)
  2148. self.layout.removeWidget(self.basic_group)
  2149. self.basic_group.deleteLater()
  2150. self.changeIcon.emit(type(self.filter_obj).__name__)
  2151. self.tabLoaded.emit(self.tab_id, self.name, self.obj())
  2152. def start_from_filter_obj(self, filter_obj: filtering.Filter, tab_id: int, name: str = None):
  2153. self.tab_id = tab_id
  2154. self.filter_obj = filter_obj
  2155. self.basic_group.setVisible(False)
  2156. self.init_overview_ui()
  2157. self.init_function_ui()
  2158. if name is not None:
  2159. self._rename(name)
  2160. print(self.filter_obj)
  2161. self.changeIcon.emit(type(self.filter_obj).__name__)
  2162. class CreatePipelineWindow(gui_widgets.MinMaxDialog, FilterTabPage):
  2163. pipelineSaved = QtCore.pyqtSignal(str, generic.GenericPipeline)
  2164. pipelineExported = QtCore.pyqtSignal(str, generic.GenericPipeline)
  2165. widthChanged = QtCore.pyqtSignal()
  2166. geneSetsRequested = QtCore.pyqtSignal()
  2167. PIPELINE_TYPES = {name: filtering.Pipeline for name in FILTER_OBJ_TYPES.keys()}
  2168. PIPELINE_TYPES.update(
  2169. {'Sequence files (paired-end)': fastq.PairedEndPipeline, 'Sequence files (single-end)': fastq.SingleEndPipeline}
  2170. )
  2171. INV_PIPELINE_TYPES = {val.__name__: key for key, val in PIPELINE_TYPES.items()}
  2172. __slots__ = {
  2173. 'pipeline': 'Pipeline object',
  2174. 'is_unsaved': 'indicates whether the Pipeline was saved since changes were last made',
  2175. }
  2176. def __init__(self, parent=None):
  2177. super().__init__(parent=parent)
  2178. self.setLayout(self.layout)
  2179. self.setWindowTitle('Create new Pipeline')
  2180. self.setGeometry(500, 200, 900, 800)
  2181. self.pipeline = None
  2182. self.is_unsaved = False
  2183. @classmethod
  2184. def start_from_pipeline(cls, pipeline: generic.GenericPipeline, pipeline_name: str, parent=None):
  2185. window = cls(parent)
  2186. window.basic_widgets['pipeline_name'].setText(pipeline_name)
  2187. window.pipeline = pipeline
  2188. pipeline_type = cls.INV_PIPELINE_TYPES[type(pipeline).__name__]
  2189. if isinstance(pipeline, filtering.Pipeline):
  2190. window.filter_obj = pipeline.filter_type.__new__(pipeline.filter_type)
  2191. pipeline_type = FILTER_OBJ_TYPES_INV[pipeline.filter_type.__name__]
  2192. window.basic_widgets['table_type_combo'].setCurrentText(pipeline_type)
  2193. window.init_overview_ui()
  2194. window.init_function_ui()
  2195. window.basic_group.setVisible(False)
  2196. window.is_unsaved = False
  2197. return window
  2198. def init_basic_ui(self):
  2199. self.layout.insertWidget(0, self.basic_group)
  2200. self.basic_group.setTitle('Choose data table type for Pipeline')
  2201. self.basic_widgets['table_type_combo'] = QtWidgets.QComboBox()
  2202. self.basic_widgets['table_type_combo'].addItems(self.PIPELINE_TYPES.keys())
  2203. self.basic_widgets['table_type_combo'].setCurrentText('Other table')
  2204. self.basic_widgets['pipeline_name'] = QtWidgets.QLineEdit()
  2205. self.basic_widgets['pipeline_name'].setText('New Pipeline')
  2206. self.basic_widgets['name_label'] = QtWidgets.QLabel('Name your Pipeline:')
  2207. self.basic_widgets['start_button'] = QtWidgets.QPushButton('Create Pipeline')
  2208. self.basic_widgets['start_button'].clicked.connect(self.start)
  2209. gui_widgets.mark_primary(self.basic_widgets['start_button'])
  2210. # overrides the base 'Apply' button (TabPage.__init__) with this window's own
  2211. # main commit action; the two are never visible at the same time (this one takes
  2212. # over once basic_group - and its "Create Pipeline" button - is hidden), so it gets
  2213. # the same primary styling
  2214. self.apply_button = QtWidgets.QPushButton('Add to Pipeline')
  2215. self.apply_button.clicked.connect(self.apply_function)
  2216. gui_widgets.mark_primary(self.apply_button)
  2217. self.layout.insertWidget(2, self.apply_button)
  2218. self.apply_button.setVisible(False)
  2219. self.basic_widgets['type_label'] = QtWidgets.QLabel('Choose Pipeline type:')
  2220. self.basic_grid.addWidget(self.basic_widgets['pipeline_name'], 1, 1)
  2221. self.basic_grid.addWidget(self.basic_widgets['name_label'], 0, 0, 1, 2)
  2222. self.basic_grid.addWidget(self.basic_widgets['type_label'], 0, 2)
  2223. self.basic_grid.addWidget(self.basic_widgets['table_type_combo'], 1, 2)
  2224. self.basic_grid.addWidget(self.basic_widgets['start_button'], 1, 3)
  2225. self.basic_grid.addWidget(QtWidgets.QWidget(), 2, 0)
  2226. self.basic_grid.addWidget(QtWidgets.QWidget(), 0, 4)
  2227. self.basic_grid.setRowStretch(2, 1)
  2228. self.basic_grid.setColumnStretch(4, 1)
  2229. def get_all_fastq_actions(self):
  2230. func_type = 'single' if isinstance(self.pipeline, fastq.SingleEndPipeline) else 'paired'
  2231. all_funcs = dir(fastq)
  2232. public_funcs = []
  2233. for func in all_funcs:
  2234. func_obj = getattr(fastq, func)
  2235. if func.startswith('_'):
  2236. continue
  2237. if not hasattr(func_obj, 'func_type'):
  2238. continue
  2239. if getattr(func_obj, 'func_type') not in [func_type, 'both']:
  2240. continue
  2241. public_funcs.append(func)
  2242. return public_funcs
  2243. def init_function_ui(self):
  2244. if isinstance(self.pipeline, (fastq.SingleEndPipeline, fastq.PairedEndPipeline)):
  2245. self.function_group.setVisible(True)
  2246. self.funcs = {}
  2247. actions = self.get_all_fastq_actions()
  2248. for func in actions:
  2249. self.funcs[generic.get_method_readable_name(func, fastq)] = func
  2250. self.stack_widgets['main'] = FuncTypeStack(
  2251. actions,
  2252. fastq,
  2253. self,
  2254. {'input_folder', 'fastq_folder', 'output_folder', 'r1_files', 'r2_files', 'return_new_filenames'},
  2255. True,
  2256. )
  2257. self.stack_widgets['main'].funcSelected.connect(self.apply_button.setVisible)
  2258. self.stack.addWidget(self.stack_widgets['main'])
  2259. self.function_grid.addWidget(self.stack, 1, 0, 1, 1)
  2260. else:
  2261. super().init_function_ui()
  2262. sorted_actions = self.get_all_actions()
  2263. for action_type in sorted_actions:
  2264. self.stack_widgets[action_type].pipeline_mode = True
  2265. self.stack_widgets[action_type].excluded_params.add('inplace')
  2266. self.function_group.setTitle('Add functions to Pipeline')
  2267. def apply_function(self):
  2268. this_stack: FuncTypeStack = self.stack.currentWidget()
  2269. func_name = this_stack.get_function_name()
  2270. func_params = this_stack.get_function_params()
  2271. self.pipeline.add_function(func_name, **func_params)
  2272. self.update_pipeline_preview()
  2273. self.is_unsaved = True
  2274. def _apply_function_from_params(self, func_name, args: list, kwargs: dict, finish_slot=None, job_id: int = None):
  2275. raise NotImplementedError
  2276. def update_table_preview_width(self):
  2277. return
  2278. def update_pipeline_preview(self):
  2279. self.overview_widgets['preview'].setPlainText(str(self.pipeline))
  2280. def update_table_preview(self):
  2281. raise NotImplementedError
  2282. def update_filter_obj_shape(self):
  2283. raise NotImplementedError
  2284. def set_file_path_bg_color(self):
  2285. raise NotImplementedError
  2286. def start(self):
  2287. table_type = self.basic_widgets['table_type_combo'].currentText()
  2288. pipeline_type = self.PIPELINE_TYPES[table_type]
  2289. if pipeline_type == filtering.Pipeline:
  2290. filt_obj_type = FILTER_OBJ_TYPES[self.basic_widgets['table_type_combo'].currentText()]
  2291. self.filter_obj = filt_obj_type.__new__(filt_obj_type)
  2292. self.pipeline = filtering.Pipeline(filt_obj_type)
  2293. else:
  2294. self.pipeline = pipeline_type()
  2295. self.init_overview_ui()
  2296. self.init_function_ui()
  2297. self.basic_group.setVisible(False)
  2298. self.is_unsaved = True
  2299. def init_overview_ui(self):
  2300. self.overview_group.setTitle('Pipeline preview')
  2301. self.layout.insertWidget(1, self.overview_group)
  2302. self.overview_widgets['preview'] = QtWidgets.QPlainTextEdit()
  2303. self.overview_widgets['preview'].setReadOnly(True)
  2304. self.update_pipeline_preview()
  2305. self.overview_grid.addWidget(
  2306. QtWidgets.QLabel(f"Pipeline name: '<b>{self._get_pipeline_name()}</b>'"), 0, 0, 1, 6
  2307. )
  2308. self.overview_grid.addWidget(self.overview_widgets['preview'], 2, 0, 2, 6)
  2309. self.overview_grid.addWidget(
  2310. QtWidgets.QLabel(f'Pipeline type: {self.basic_widgets["table_type_combo"].currentText()}'), 5, 0, 1, 3
  2311. )
  2312. self.overview_widgets['remove_button'] = QtWidgets.QPushButton('Remove last function')
  2313. self.overview_widgets['remove_button'].clicked.connect(self.remove_last_function)
  2314. self.overview_grid.addWidget(self.overview_widgets['remove_button'], 5, 3)
  2315. self.overview_widgets['export_button'] = QtWidgets.QPushButton('Export Pipeline')
  2316. self.overview_widgets['export_button'].clicked.connect(self.export_pipeline)
  2317. self.overview_grid.addWidget(self.overview_widgets['export_button'], 5, 4)
  2318. self.overview_widgets['save_button'] = QtWidgets.QPushButton('Save Pipeline')
  2319. self.overview_widgets['save_button'].clicked.connect(self.save_file)
  2320. self.overview_grid.addWidget(self.overview_widgets['save_button'], 5, 5)
  2321. def remove_last_function(self):
  2322. try:
  2323. self.pipeline.remove_last_function()
  2324. self.update_pipeline_preview()
  2325. except InvalidValueError:
  2326. err = QtWidgets.QMessageBox(self)
  2327. err.setWindowTitle('Pipeline is already empty!')
  2328. err.setText('Cannot remove functions from the Pipeline - it is already empty!')
  2329. err.setIcon(err.Icon.Warning)
  2330. err.exec()
  2331. def _get_pipeline_name(self):
  2332. return self.basic_widgets['pipeline_name'].text()
  2333. def export_pipeline(self):
  2334. self.pipelineExported.emit(self._get_pipeline_name(), self.pipeline)
  2335. def save_file(self):
  2336. try:
  2337. self.pipelineSaved.emit(self._get_pipeline_name(), self.pipeline)
  2338. self.is_unsaved = False
  2339. except Exception as e:
  2340. print('Failed to save Pipeline')
  2341. raise e
  2342. def closeEvent(self, event): # pragma: no cover
  2343. if self.is_unsaved:
  2344. quit_msg = (
  2345. 'Are you sure you want to close this window without saving your Pipeline?\n'
  2346. 'All unsaved progress will be lost'
  2347. )
  2348. reply = QtWidgets.QMessageBox.question(
  2349. self,
  2350. "Close 'Create Pipeline' window?",
  2351. quit_msg,
  2352. QtWidgets.QMessageBox.StandardButton.No,
  2353. QtWidgets.QMessageBox.StandardButton.Yes,
  2354. )
  2355. if reply == QtWidgets.QMessageBox.StandardButton.Yes:
  2356. event.accept()
  2357. else:
  2358. event.ignore()
  2359. else:
  2360. event.accept()
  2361. class MultiKeepWindow(gui_widgets.MinMaxDialog):
  2362. __slots__ = {
  2363. 'objs': 'objects to keep or discard',
  2364. 'job_id': 'job ID',
  2365. 'files': 'filenames of the objects to keep',
  2366. 'button_box': 'button box for selecting objects to keep',
  2367. 'labels': 'labels for the objects',
  2368. 'keep_marks': 'check boxes for the objects',
  2369. 'names': 'potential new names for the objects',
  2370. 'select_all': 'select all checkbox',
  2371. 'scroll': 'scroll area widget',
  2372. 'scroll_layout': 'layout for scroll area',
  2373. 'scroll_widget': 'widget containing scroll area',
  2374. 'main_layout': 'main layout for the window',
  2375. }
  2376. def __init__(self, objs: List[Union[filtering.Filter, enrichment.FeatureSet]], job_id: int, parent=None):
  2377. super().__init__(parent)
  2378. self.job_id = job_id
  2379. # make sure there are no two objects with the same name
  2380. self.objs = {}
  2381. for obj in objs:
  2382. key = (
  2383. str(obj.fname.stem)
  2384. if validation.isinstanceinh(obj, (filtering.Filter, fastq.filtering.Filter))
  2385. else obj.set_name
  2386. )
  2387. if key in self.objs:
  2388. i = 1
  2389. key += '_{i}'
  2390. while key.format(i=i) in self.objs:
  2391. i += 1
  2392. key = key.format(i=i)
  2393. self.objs[key] = obj
  2394. self.files = list(self.objs.keys())
  2395. self.button_box = QtWidgets.QDialogButtonBox(
  2396. QtWidgets.QDialogButtonBox.StandardButton.Ok | QtWidgets.QDialogButtonBox.StandardButton.Cancel
  2397. )
  2398. self.labels = dict()
  2399. self.keep_marks = dict()
  2400. self.names = dict()
  2401. self.select_all = QtWidgets.QCheckBox('Select all')
  2402. self.scroll = QtWidgets.QScrollArea()
  2403. self.scroll_layout = QtWidgets.QGridLayout(self.scroll)
  2404. self.scroll_widget = QtWidgets.QWidget(self.scroll)
  2405. self.main_layout = QtWidgets.QVBoxLayout(self)
  2406. self.init_ui()
  2407. def init_ui(self):
  2408. self.setLayout(self.main_layout)
  2409. self.main_layout.addWidget(self.scroll)
  2410. self.scroll.setWidgetResizable(True)
  2411. self.scroll.setWidget(self.scroll_widget)
  2412. self.scroll_widget.setLayout(self.scroll_layout)
  2413. self.scroll_layout.setSizeConstraint(QtWidgets.QLayout.SizeConstraint.SetMinAndMaxSize)
  2414. self.select_all.clicked.connect(self.change_all)
  2415. self.button_box.accepted.connect(self.accept)
  2416. self.button_box.rejected.connect(self.reject)
  2417. self.setWindowTitle('Choose tables to keep')
  2418. self.scroll_layout.addWidget(
  2419. QtWidgets.QLabel(
  2420. 'Please choose which tables to keep (mandatory) out of the tables generated by the last function call, '
  2421. 'and rename them (optional):\n\n'
  2422. ),
  2423. 0,
  2424. 0,
  2425. 1,
  2426. 3,
  2427. )
  2428. self.scroll_layout.addWidget(QtWidgets.QLabel('<b>Tables:</b>'), 1, 0)
  2429. self.scroll_layout.addWidget(QtWidgets.QLabel('<b>Choose tables to keep</b>'), 1, 1)
  2430. self.scroll_layout.addWidget(QtWidgets.QLabel('<b>Rename tables (optional):</b>'), 1, 2)
  2431. self.scroll_layout.addWidget(self.select_all, 2, 1)
  2432. for i, file in enumerate(self.files, start=3):
  2433. self.labels[file] = QtWidgets.QLabel(file)
  2434. self.keep_marks[file] = QtWidgets.QCheckBox('Keep table?')
  2435. self.names[file] = QtWidgets.QLineEdit()
  2436. self.scroll_layout.addWidget(self.labels[file], i, 0)
  2437. self.scroll_layout.addWidget(self.keep_marks[file], i, 1)
  2438. self.scroll_layout.addWidget(self.names[file], i, 2)
  2439. self.main_layout.addWidget(self.button_box)
  2440. self.scroll.setMinimumWidth(self.scroll_widget.sizeHint().width() + 150)
  2441. def change_all(self):
  2442. for widget in self.keep_marks.values():
  2443. widget.setChecked(self.select_all.isChecked())
  2444. def result(self) -> list:
  2445. keep_tables = {file: widget.isChecked() for file, widget in self.keep_marks.items()}
  2446. new_names = {file: gui_widgets.get_val_from_widget(widget) for file, widget in self.names.items()}
  2447. out = []
  2448. for file in keep_tables:
  2449. if keep_tables[file]:
  2450. obj = self.objs[file]
  2451. if new_names[file] != '':
  2452. if validation.isinstanceinh(obj, (filtering.Filter, fastq.filtering.Filter)):
  2453. obj.fname = Path(f'{new_names[file]}.csv')
  2454. else:
  2455. obj.set_name = new_names[file]
  2456. out.append(obj)
  2457. return out
  2458. class MultiOpenWindow(QtWidgets.QDialog):
  2459. __slots__ = {
  2460. 'files': 'filenames of the objects to open',
  2461. 'button_box': 'button box for selecting objects to open',
  2462. 'paths': 'paths for the objects to open',
  2463. 'table_types': 'table types for the objects',
  2464. 'names': 'potential new names for the objects',
  2465. 'kwargs': 'kwargs for the objects to open',
  2466. 'kwargs_widgets': 'widgets representing kwargs for the objects',
  2467. 'scroll': 'scroll area widget',
  2468. 'scroll_layout': 'layout for scroll area',
  2469. 'scroll_widget': 'widget containing scroll area',
  2470. 'main_layout': 'main layout for the window',
  2471. }
  2472. def __init__(self, files: List[str], parent=None):
  2473. super().__init__(parent)
  2474. self.files = files
  2475. self.button_box = QtWidgets.QDialogButtonBox(
  2476. QtWidgets.QDialogButtonBox.StandardButton.Ok | QtWidgets.QDialogButtonBox.StandardButton.Cancel
  2477. )
  2478. self.all_types_combo = QtWidgets.QComboBox(self)
  2479. self.paths = dict()
  2480. self.table_types = dict()
  2481. self.names = dict()
  2482. self.kwargs = dict()
  2483. self.kwargs_widgets = dict()
  2484. self.scroll = QtWidgets.QScrollArea()
  2485. self.scroll_layout = QtWidgets.QGridLayout(self.scroll)
  2486. self.scroll_widget = QtWidgets.QWidget(self.scroll)
  2487. self.main_layout = QtWidgets.QVBoxLayout(self)
  2488. self.init_ui()
  2489. def init_ui(self):
  2490. self.setLayout(self.main_layout)
  2491. self.main_layout.addWidget(self.scroll)
  2492. self.scroll.setWidgetResizable(True)
  2493. self.scroll.setWidget(self.scroll_widget)
  2494. self.scroll_widget.setLayout(self.scroll_layout)
  2495. self.scroll_layout.setSizeConstraint(QtWidgets.QLayout.SizeConstraint.SetMinAndMaxSize)
  2496. self.button_box.accepted.connect(self.accept)
  2497. self.button_box.rejected.connect(self.reject)
  2498. self.setWindowTitle('Choose table types and names')
  2499. self.scroll_layout.addWidget(
  2500. QtWidgets.QLabel(
  2501. 'Please choose a table type (mandatory) and table name (optional) for each loaded table\n\n'
  2502. ),
  2503. 0,
  2504. 0,
  2505. 1,
  2506. 3,
  2507. )
  2508. self.scroll_layout.addWidget(QtWidgets.QLabel('Table paths:'), 1, 0)
  2509. self.scroll_layout.addWidget(QtWidgets.QLabel('Table types:'), 1, 1)
  2510. self.scroll_layout.addWidget(QtWidgets.QLabel('Table names (optional):'), 1, 2)
  2511. self.scroll_layout.addWidget(QtWidgets.QLabel('Additional parameters:'), 1, 3)
  2512. self.scroll_layout.addWidget(self.all_types_combo, 2, 1)
  2513. self.all_types_combo.addItems(list(FILTER_OBJ_TYPES.keys()))
  2514. self.all_types_combo.setCurrentText('Other table')
  2515. self.all_types_combo.currentTextChanged.connect(self.change_all_table_types)
  2516. for i, file in enumerate(self.files, 3):
  2517. self.paths[file] = gui_widgets.PathLineEdit(file, parent=self)
  2518. self.table_types[file] = QtWidgets.QComboBox(self)
  2519. self.table_types[file].addItems(list(FILTER_OBJ_TYPES.keys()))
  2520. self.names[file] = QtWidgets.QLineEdit(self)
  2521. kwargs_widget = QtWidgets.QWidget(self)
  2522. self.kwargs[file] = QtWidgets.QGridLayout(kwargs_widget)
  2523. self.table_types[file].currentTextChanged.connect(functools.partial(self.update_args_ui, file))
  2524. self.table_types[file].setCurrentText('Other table')
  2525. self.scroll_layout.addWidget(self.paths[file], i, 0)
  2526. self.scroll_layout.addWidget(self.table_types[file], i, 1)
  2527. self.scroll_layout.addWidget(self.names[file], i, 2)
  2528. self.scroll_layout.addWidget(kwargs_widget, i, 3)
  2529. self.main_layout.addWidget(self.button_box)
  2530. self.scroll.setMinimumWidth(self.scroll_widget.sizeHint().width() + 150)
  2531. @QtCore.pyqtSlot(str)
  2532. def change_all_table_types(self, new_type: str):
  2533. for file in self.files:
  2534. self.table_types[file].setCurrentText(new_type)
  2535. @QtCore.pyqtSlot(str)
  2536. def update_args_ui(self, file: str):
  2537. # clear previous layout
  2538. gui_widgets.clear_layout(self.kwargs[file])
  2539. self.kwargs_widgets[file] = {}
  2540. func_name = '__init__'
  2541. filter_obj_type = FILTER_OBJ_TYPES[self.table_types[file].currentText()]
  2542. signature = generic.get_method_signature(func_name, filter_obj_type)
  2543. desc, param_desc = io.get_method_docstring(func_name, filter_obj_type)
  2544. func = getattr(filter_obj_type, func_name, None)
  2545. self.table_types[file].setToolTip(desc)
  2546. i = 1
  2547. for name, param in signature.items():
  2548. if name in INIT_EXCLUDED_PARAMS:
  2549. continue
  2550. self.kwargs_widgets[file][name] = gui_widgets.param_to_widget(param, name)
  2551. label = QtWidgets.QLabel(f'{generic.get_param_readable_name(name, func)}:', self.kwargs_widgets[file][name])
  2552. if name in param_desc:
  2553. label.setToolTip(param_desc[name])
  2554. help_button = gui_widgets.HelpButton()
  2555. self.kwargs[file].addWidget(help_button, i, 2)
  2556. help_button.set_param_help(name, param_desc[name])
  2557. self.kwargs[file].addWidget(label, i, 0)
  2558. self.kwargs[file].addWidget(self.kwargs_widgets[file][name], i, 1)
  2559. i += 1
  2560. def result(self):
  2561. paths = {file: gui_widgets.get_val_from_widget(widget) for file, widget in self.paths.items()}
  2562. types = {file: gui_widgets.get_val_from_widget(widget) for file, widget in self.table_types.items()}
  2563. names = {file: gui_widgets.get_val_from_widget(widget) for file, widget in self.names.items()}
  2564. kwargs = {
  2565. file: {key: gui_widgets.get_val_from_widget(val) for key, val in widget_dict.items()}
  2566. for file, widget_dict in self.kwargs_widgets.items()
  2567. }
  2568. return paths, types, names, kwargs
  2569. class ReactiveTabWidget(QtWidgets.QTabWidget):
  2570. tabRightClicked = QtCore.pyqtSignal(int)
  2571. newTabFromSet = QtCore.pyqtSignal(set, int, str)
  2572. newTabFromFilter = QtCore.pyqtSignal(filtering.Filter, int, str)
  2573. tabClosed = QtCore.pyqtSignal(int)
  2574. def __init__(self, parent=None):
  2575. super().__init__(parent)
  2576. self.tabBar().setDocumentMode(True)
  2577. self.tabBar().setMovable(True)
  2578. self.setTabsClosable(True)
  2579. self.setElideMode(QtCore.Qt.TextElideMode.ElideMiddle)
  2580. def mousePressEvent(self, event: QtGui.QMouseEvent):
  2581. if event.button() == QtCore.Qt.MouseButton.LeftButton:
  2582. super().mousePressEvent(event)
  2583. elif event.button() == QtCore.Qt.MouseButton.RightButton:
  2584. point = event.pos()
  2585. if point.isNull():
  2586. return
  2587. ind = self.tabBar().tabAt(point)
  2588. if ind == -1:
  2589. return
  2590. self.tabRightClicked.emit(ind)
  2591. def new_tab_from_item(self, item: Union[filtering.Filter, set], name: str, tab_id: int):
  2592. if isinstance(item, set):
  2593. self.newTabFromSet.emit(item, tab_id, name)
  2594. elif validation.isinstanceinh(item, filtering.Filter):
  2595. self.newTabFromFilter.emit(item, tab_id, name)
  2596. else:
  2597. raise TypeError(type(item))
  2598. def removeTab(self, index):
  2599. h = self.cornerWidget().height()
  2600. tab_id = self.widget(index).tab_id
  2601. super().removeTab(index)
  2602. self.update()
  2603. if self.count() == 0:
  2604. self.cornerWidget().setMinimumHeight(h)
  2605. self.setMinimumHeight(h)
  2606. self.tabClosed.emit(tab_id)
  2607. def addTab(self, widget: TabPage, a1: str) -> int:
  2608. return super().addTab(widget, a1)
  2609. def currentWidget(self) -> Union[FilterTabPage, SetTabPage]:
  2610. return super().currentWidget()
  2611. def widget(self, index: int) -> Union[FilterTabPage, SetTabPage]:
  2612. return super().widget(index)
  2613. class RenameCommand(QtGui.QUndoCommand):
  2614. __slots__ = {
  2615. 'prev_name': 'previous name of the tab',
  2616. 'new_name': 'new name of the tab',
  2617. 'prev_id': 'previous ID',
  2618. 'job_id': 'job ID of the rename command',
  2619. 'tab': 'tab widget',
  2620. }
  2621. def __init__(self, prev_name: str, new_name: str, tab: TabPage, description: str):
  2622. super().__init__(description)
  2623. self.prev_name = prev_name
  2624. self.new_name = new_name
  2625. self.prev_id = tab.tab_id
  2626. self.job_id = JOB_COUNTER.get_id()
  2627. self.tab = tab
  2628. def undo(self):
  2629. self.tab.tabReverted.emit(self.tab.tab_id)
  2630. self.tab._rename(self.prev_name)
  2631. self.tab.tab_id = self.prev_id
  2632. def redo(self):
  2633. self.job_id = JOB_COUNTER.get_id() if self.job_id is None else self.job_id
  2634. self.tab._rename(self.new_name, self.job_id)
  2635. class CloseTabCommand(QtGui.QUndoCommand):
  2636. __slots__ = {
  2637. 'tab_container': 'ReactiveTabWidget containing the tabs',
  2638. 'tab_index': 'index of the tab to be closed',
  2639. 'tab_icon': 'icon of the tab',
  2640. 'tab_name': 'name of the tab',
  2641. 'obj_type': "object type of the tab's underlying object",
  2642. 'filename': "filename to cache the tab's underlying object under",
  2643. 'kwargs': 'kwargs for the underlying object',
  2644. }
  2645. def __init__(self, tab_container: ReactiveTabWidget, tab_index: int, description: str):
  2646. super().__init__(description)
  2647. self.tab_container = tab_container
  2648. self.tab_index = tab_index
  2649. self.tab_id = self.tab_container.widget(self.tab_index).tab_id
  2650. self.tab_icon = tab_container.tabIcon(self.tab_index)
  2651. self.tab_name = tab_container.tabText(self.tab_index).rstrip('*')
  2652. self.obj_type = self.tab_container.widget(self.tab_index).obj_type()
  2653. self.filename = self.tab_container.widget(self.tab_index).cache()
  2654. self.kwargs = self.tab_container.widget(self.tab_index).obj_properties()
  2655. def undo(self):
  2656. item = io.load_cached_gui_file(self.filename)
  2657. if isinstance(item, pl.DataFrame):
  2658. item = self.obj_type.from_dataframe(item, self.tab_name, **self.kwargs)
  2659. self.tab_container.new_tab_from_item(item, self.tab_name, self.tab_id)
  2660. self.tab_container.setTabIcon(self.tab_container.currentIndex(), self.tab_icon)
  2661. def redo(self):
  2662. self.tab_container.removeTab(self.tab_index)
  2663. class InplaceCommand(QtGui.QUndoCommand):
  2664. __slots__ = {
  2665. 'tab': 'tab widget',
  2666. 'prev_job_id': 'previous job ID',
  2667. 'new_job_id': 'new job ID',
  2668. 'func_name': 'name of the function to apply/undo',
  2669. 'args': 'function args',
  2670. 'kwargs': 'function kwargs',
  2671. 'obj_copy': 'copy of the original object',
  2672. 'predecessors': 'ancestor job IDs for the action',
  2673. }
  2674. def __init__(
  2675. self, tab: TabPage, func_name: str, args: list, kwargs: dict, description: str, predecessors: List[int]
  2676. ):
  2677. super().__init__(description)
  2678. self.tab = tab
  2679. self.prev_job_id = tab.tab_id
  2680. self.new_job_id = None
  2681. self.func_name = func_name
  2682. self.args = args
  2683. self.kwargs = kwargs
  2684. self.obj_copy = copy.copy(self.tab.obj())
  2685. self.predecessors = predecessors if isinstance(predecessors, list) else []
  2686. def undo(self):
  2687. obj = self.tab.obj()
  2688. del obj
  2689. self.tab.update_obj(copy.copy(self.obj_copy))
  2690. self.tab.update_tab(is_unsaved=True)
  2691. self.tab.tabReverted.emit(self.tab.tab_id)
  2692. self.tab.tab_id = self.prev_job_id
  2693. def redo(self):
  2694. self.new_job_id = JOB_COUNTER.get_id() if self.new_job_id is None else self.new_job_id
  2695. self.tab._apply_function_from_params(
  2696. self.func_name,
  2697. self.args,
  2698. self.kwargs,
  2699. job_id=self.new_job_id,
  2700. predecessors=self.predecessors,
  2701. finish_slot=functools.partial(self.tab.update_tab, is_unsaved=True),
  2702. )
  2703. class InplaceCachedCommand(InplaceCommand):
  2704. __slots__ = {
  2705. 'first_pass': 'indicates whether the command was already applied once',
  2706. 'processed_obj': 'object after application of the function',
  2707. 'new_spawn_id': 'tab id of the tab after running redo()',
  2708. }
  2709. def __init__(self, tab: TabPage, func_name: str, args: list, kwargs: dict, description: str, predecessors: list):
  2710. super().__init__(tab, func_name, args, kwargs, description, predecessors)
  2711. self.first_pass = True
  2712. self.processed_obj = None
  2713. self.new_spawn_id = None
  2714. def redo(self):
  2715. if self.first_pass:
  2716. self.first_pass = False
  2717. super().redo()
  2718. else:
  2719. if self.new_spawn_id is None:
  2720. raise InternalError
  2721. source_name = generic.get_method_readable_name(getattr(self.tab.obj(), self.func_name))
  2722. self.new_job_id = JOB_COUNTER.get_id() if self.new_job_id is None else self.new_job_id
  2723. self.tab.update_obj(copy.copy(self.processed_obj))
  2724. self.tab.update_tab(is_unsaved=True)
  2725. self.tab.tab_id = self.new_spawn_id
  2726. mock_partial = functools.partial(lambda *args, **kwargs: None, self.args, self.kwargs)
  2727. worker_output = gui_widgets.WorkerOutput(
  2728. self.tab.obj(), mock_partial, self.new_job_id, self.predecessors + [self.prev_job_id]
  2729. )
  2730. self.tab.functionApplied.emit(worker_output)
  2731. self.tab.itemSpawned.emit(
  2732. f"'{source_name}'\noutput", self.new_spawn_id, self.new_job_id, self.processed_obj
  2733. )
  2734. def undo(self):
  2735. processed_obj = self.tab.obj()
  2736. self.processed_obj = copy.copy(processed_obj)
  2737. self.tab.update_obj(copy.copy(self.obj_copy))
  2738. self.tab.update_tab(is_unsaved=True)
  2739. self.new_spawn_id = self.tab.tab_id
  2740. self.tab.tabReverted.emit(self.tab.tab_id)
  2741. self.tab.tab_id = self.prev_job_id
  2742. class SetOpInplacCommand(InplaceCommand):
  2743. def redo(self):
  2744. source_name = generic.get_method_readable_name(getattr(self.tab.obj(), self.func_name))
  2745. self.new_job_id = JOB_COUNTER.get_id() if self.new_job_id is None else self.new_job_id
  2746. is_filter_obj = validation.isinstanceinh(self.tab.obj(), filtering.Filter)
  2747. first_obj = self.tab.obj()
  2748. if not is_filter_obj:
  2749. first_obj = filtering.Filter.from_dataframe(pl.DataFrame(index=first_obj), 'placeholder')
  2750. partial = functools.partial(getattr(first_obj, self.func_name), *self.args, **self.kwargs)
  2751. worker = gui_widgets.Worker(partial, self.new_job_id, self.predecessors + [self.prev_job_id])
  2752. worker.finished.connect(check_run_success)
  2753. worker.finished.connect(self.tab.functionApplied.emit)
  2754. worker.run()
  2755. if not is_filter_obj:
  2756. self.tab.update_obj(first_obj.index_set)
  2757. self.tab.update_tab()
  2758. new_spawn_id = JOB_COUNTER.get_id()
  2759. self.tab.tab_id = new_spawn_id
  2760. self.tab.itemSpawned.emit(f"'{source_name}'\noutput", new_spawn_id, self.new_job_id, self.tab.obj())
  2761. class PipelineInplaceCommand(QtGui.QUndoCommand):
  2762. __slots__ = {
  2763. 'tab': 'tab object',
  2764. 'pipeline': 'Pipeline to apply',
  2765. 'pipeline_name': 'Pipeline name',
  2766. 'obj_copy': 'copy of the original Filter object of the tab',
  2767. 'prev_job_id': 'previous job ID',
  2768. 'new_job_id': 'new job ID',
  2769. 'predecessors': 'ancestor job IDs for the action',
  2770. }
  2771. def __init__(
  2772. self,
  2773. tab: FilterTabPage,
  2774. pipeline: filtering.Pipeline,
  2775. pipeline_name: str,
  2776. description: str,
  2777. predecessors: List[int],
  2778. ):
  2779. super().__init__(description)
  2780. self.tab = tab
  2781. self.prev_job_id = tab.tab_id
  2782. self.new_job_id = None
  2783. self.pipeline = pipeline
  2784. self.pipeline_name = pipeline_name
  2785. self.predecessors = predecessors if isinstance(predecessors, list) else []
  2786. self.obj_copy = copy.copy(self.tab.filter_obj)
  2787. def undo(self):
  2788. del self.tab.filter_obj
  2789. self.tab.filter_obj = copy.copy(self.obj_copy)
  2790. self.tab.update_tab(is_unsaved=True)
  2791. self.tab.tabReverted.emit(self.tab.tab_id)
  2792. self.tab.tab_id = self.prev_job_id
  2793. def redo(self):
  2794. self.new_job_id = JOB_COUNTER.get_id() if self.new_job_id is None else self.new_job_id
  2795. self.tab._apply_pipeline(self.pipeline, self.pipeline_name, True, self.new_job_id, self.predecessors)
  2796. class MainWindow(QtWidgets.QMainWindow):
  2797. USER_GUIDE_URL = 'https://guyteichman.github.io/RNAlysis/build/user_guide_gui.html'
  2798. TUTORIAL_URL = 'https://guyteichman.github.io/RNAlysis/build/tutorial.html'
  2799. FAQ_URL = 'https://guyteichman.github.io/RNAlysis/build/faq.html'
  2800. BUGS_URL = (
  2801. 'https://github.com/GuyTeichman/RNAlysis/issues/new?assignees=&labels=bug+report&projects='
  2802. '&template=bug_report.yaml&title=Bug+Report%3A+'
  2803. )
  2804. FEATURE_URL = (
  2805. 'https://github.com/GuyTeichman/RNAlysis/issues/new?assignees=&labels=feature+request&projects='
  2806. '&template=feature_request.yaml&title=Feature+Request%3A+'
  2807. )
  2808. QUESTION_URL = 'https://github.com/GuyTeichman/RNAlysis/discussions'
  2809. jobQueued = QtCore.pyqtSignal()
  2810. def __init__(self, gather_stdout: bool = True):
  2811. super().__init__()
  2812. # own the GUI cache-write queue for this window's lifetime so table caching streams off the UI
  2813. # thread; io falls back to synchronous writes whenever no window has registered a queue
  2814. self.gui_cache_writer = io.GuiCacheWriteQueue()
  2815. io.set_active_gui_cache_writer(self.gui_cache_writer)
  2816. self._generate_report = False
  2817. self.report = None
  2818. self.tabs = ReactiveTabWidget(self)
  2819. self.closed_tabs_stack = QtGui.QUndoStack(self)
  2820. self.undo_group = QtGui.QUndoGroup(self)
  2821. self.tabs.currentChanged.connect(self._change_undo_stack)
  2822. self.undo_view = QtWidgets.QUndoView(self.undo_group)
  2823. self.command_history_dock = QtWidgets.QDockWidget('Command history', self)
  2824. self.add_tab_button = QtWidgets.QToolButton()
  2825. self.add_tab_button.setToolTip('Add New Tab')
  2826. self.add_tab_button.clicked.connect(functools.partial(self.add_new_tab, name=None))
  2827. self.add_tab_button.setText('+')
  2828. self.status_bar = gui_windows.StatusBar(self)
  2829. self.error_window = None
  2830. self.menu_bar = QtWidgets.QMenuBar(self)
  2831. self.tab_contextmenu = None
  2832. self.pipelines: typing.OrderedDict[str, (generic.GenericPipeline, int)] = OrderedDict()
  2833. self.pipeline_window = None
  2834. self.whatsnew_window = gui_windows.WhatsNewWindow(self)
  2835. self.about_window = gui_windows.AboutWindow(self)
  2836. self.settings_window = gui_windows.SettingsWindow(self)
  2837. self.settings_window.styleSheetUpdated.connect(self.update_style_sheet)
  2838. self.set_op_window = None
  2839. self.set_visualization_window = None
  2840. self.quickstart_window = gui_quickstart.QuickStartWizard(self)
  2841. self.cite_window = gui_windows.HowToCiteWindow(self)
  2842. self.enrichment_window = None
  2843. self.enrichment_results = []
  2844. self.task_queue_window = gui_windows.TaskQueueWindow(self)
  2845. self.external_windows = {}
  2846. self.init_ui()
  2847. self.add_new_tab()
  2848. self.init_actions()
  2849. self.init_menus()
  2850. if gather_stdout:
  2851. self.queue_stdout = Queue()
  2852. # create console text read thread + receiver object
  2853. self.thread_stdout_queue_listener = QtCore.QThread()
  2854. self.stdout_receiver = gui_widgets.ThreadStdOutStreamTextQueueReceiver(self.queue_stdout)
  2855. sys.stdout = gui_widgets.WriteStream(self.queue_stdout)
  2856. # attach console text receiver to console text thread
  2857. self.stdout_receiver.moveToThread(self.thread_stdout_queue_listener)
  2858. # connect receiver object to widget for text update
  2859. self.stdout_receiver.queue_stdout_element_received_signal.connect(self.append_text_to_current_console)
  2860. # attach to start / stop methods
  2861. self.thread_stdout_queue_listener.started.connect(self.stdout_receiver.run)
  2862. self.thread_stdout_queue_listener.start()
  2863. else:
  2864. self.queue_stdout = None
  2865. self.stdout_receiver = None
  2866. self.thread_stdout_queue_listener = None
  2867. # init thread execution attributes
  2868. self.current_worker = None
  2869. self.job_thread = QtCore.QThread()
  2870. self.job_queue = Queue()
  2871. self.job_timer = QtCore.QTimer(self)
  2872. self.job_timer.timeout.connect(self.run_threaded_workers)
  2873. self.job_timer.start(3500)
  2874. self.jobQueued.connect(self.run_threaded_workers)
  2875. def show_tutorial(self):
  2876. if settings.get_show_tutorial_settings():
  2877. self.quickstart_window.show()
  2878. def whats_new(self):
  2879. self.whatsnew_window.exec()
  2880. def _reset_reporting(self):
  2881. JOB_COUNTER.set_total(0)
  2882. if not self._generate_report:
  2883. self.report = None
  2884. else:
  2885. from rnalysis.gui import gui_report
  2886. self.report = gui_report.ReportGenerator()
  2887. @QtCore.pyqtSlot(bool)
  2888. def _toggle_reporting(self, state: bool):
  2889. if state:
  2890. print('Turning on report generation...')
  2891. try:
  2892. from rnalysis.gui import gui_report
  2893. self._generate_report = True
  2894. self.report = gui_report.ReportGenerator()
  2895. cleared = self.clear_session(not (self.tabs.count() == 1 and self.tabs.currentWidget().is_empty()))
  2896. if not cleared:
  2897. raise InternalError
  2898. self.toggle_report_action.setChecked(True)
  2899. print('Report generation turned on. ')
  2900. except ImportError:
  2901. warnings.warn("The RNAlysis 'reports' module is not installed. Please install it and try again. ")
  2902. self._toggle_reporting(False)
  2903. except InternalError:
  2904. warnings.warn(
  2905. 'You must clear the current session before turning report generation on. '
  2906. 'Please clear your current session and try again. '
  2907. )
  2908. self._toggle_reporting(False)
  2909. else:
  2910. self._generate_report = False
  2911. self.report = None
  2912. self.toggle_report_action.setChecked(False)
  2913. print('Report generation turned off. ')
  2914. def prompt_auto_report_gen(self):
  2915. preset = settings.get_report_gen_settings()
  2916. if preset is None:
  2917. message_box = gui_windows.ReportGenerationMessageBox(self)
  2918. choice, dont_ask_again = message_box.exec()
  2919. if dont_ask_again:
  2920. new_setting = choice
  2921. else:
  2922. new_setting = None
  2923. settings.set_report_gen_settings(new_setting)
  2924. else:
  2925. choice = preset
  2926. self._toggle_reporting(choice)
  2927. def init_ui(self):
  2928. self.setWindowTitle(f'RNAlysis {__version__}')
  2929. self.setGeometry(350, 50, 900, 750)
  2930. self.update_style_sheet()
  2931. self.tabs.tabRightClicked.connect(self.init_tab_contextmenu)
  2932. self.tabs.tabCloseRequested.connect(self.close_tab)
  2933. self.tabs.newTabFromSet.connect(self.new_tab_from_gene_set)
  2934. self.tabs.newTabFromFilter.connect(self.new_tab_from_filter_obj)
  2935. self.tabs.tabClosed.connect(self.remove_tab_from_report)
  2936. self.command_history_dock.setWidget(self.undo_view)
  2937. self.command_history_dock.setFloating(False)
  2938. self.addDockWidget(QtCore.Qt.DockWidgetArea.RightDockWidgetArea, self.command_history_dock)
  2939. self.setStatusBar(self.status_bar)
  2940. self.task_queue_window.cancelRequested.connect(self.cancel_job)
  2941. self.tabs.setCornerWidget(self.add_tab_button, QtCore.Qt.Corner.TopRightCorner)
  2942. self.setCentralWidget(self.tabs)
  2943. @QtCore.pyqtSlot()
  2944. def clear_history(self, confirm_action: bool = True):
  2945. if confirm_action:
  2946. clear_msg = """Are you sure you want to clear all command history?
  2947. This cannot be undone!"""
  2948. reply = QtWidgets.QMessageBox.question(
  2949. self,
  2950. 'Clear history',
  2951. clear_msg,
  2952. QtWidgets.QMessageBox.StandardButton.No,
  2953. QtWidgets.QMessageBox.StandardButton.Yes,
  2954. )
  2955. else:
  2956. reply = QtWidgets.QMessageBox.StandardButton.Yes
  2957. if reply == QtWidgets.QMessageBox.StandardButton.Yes:
  2958. for stack in self.undo_group.stacks():
  2959. stack.clear()
  2960. self.closed_tabs_stack.clear()
  2961. @QtCore.pyqtSlot(int)
  2962. def init_tab_contextmenu(self, ind: int):
  2963. self.tab_contextmenu = QtWidgets.QMenu(self)
  2964. new_to_right_action = QtGui.QAction('New tab to the right')
  2965. new_to_right_action.triggered.connect(
  2966. functools.partial(self.add_new_tab_at, index=ind + 1, name=None, is_set=False)
  2967. )
  2968. self.tab_contextmenu.addAction(new_to_right_action)
  2969. self.tab_contextmenu.addSeparator()
  2970. color_menu = self.tab_contextmenu.addMenu('Change tab &color')
  2971. actions = []
  2972. for color in gui_graphics.COLOR_ICONS:
  2973. this_action = QtGui.QAction(color.capitalize())
  2974. this_action.setIcon(gui_graphics.get_icon(color))
  2975. this_action.triggered.connect(functools.partial(self.set_tab_icon, ind, icon_name=color))
  2976. actions.append(this_action)
  2977. color_menu.addAction(this_action)
  2978. reset_action = QtGui.QAction('Reset color')
  2979. reset_action.triggered.connect(functools.partial(self.set_tab_icon, ind, icon_name=None))
  2980. color_menu.addAction(reset_action)
  2981. self.tab_contextmenu.addSeparator()
  2982. # sort_menu = self.tab_contextmenu.addMenu("Sort tabs")
  2983. sort_by_name = QtGui.QAction('Sort by tab &name')
  2984. sort_by_name.triggered.connect(self.sort_tabs_by_name)
  2985. sort_by_time = QtGui.QAction('Sort by creation &time')
  2986. sort_by_time.triggered.connect(self.sort_tabs_by_creation_time)
  2987. sort_by_type = QtGui.QAction('Sort by tab type')
  2988. sort_by_type.triggered.connect(self.sort_tabs_by_type)
  2989. sort_by_size = QtGui.QAction('Sort by number of features')
  2990. sort_by_size.triggered.connect(self.sort_tabs_by_n_features)
  2991. reverse = QtGui.QAction('Reverse tab order')
  2992. reverse.triggered.connect(self.sort_reverse)
  2993. self.tab_contextmenu.addActions([sort_by_name, sort_by_time, sort_by_type, sort_by_size, reverse])
  2994. self.tab_contextmenu.addSeparator()
  2995. close_this_action = QtGui.QAction('Close')
  2996. close_this_action.triggered.connect(functools.partial(self.close_tab, ind))
  2997. close_others_action = QtGui.QAction('Close other tabs')
  2998. close_others_action.triggered.connect(functools.partial(self.close_other_tabs, ind))
  2999. close_right_action = QtGui.QAction('Close tabs to the right')
  3000. close_right_action.triggered.connect(functools.partial(self.close_tabs_to_the_right, ind))
  3001. close_left_action = QtGui.QAction('Close tabs to the left')
  3002. close_left_action.triggered.connect(functools.partial(self.close_tabs_to_the_left, ind))
  3003. self.tab_contextmenu.addActions([close_this_action, close_others_action, close_right_action, close_left_action])
  3004. self.tab_contextmenu.exec(QtGui.QCursor.pos())
  3005. def update_style_sheet(self):
  3006. self.setStyleSheet(gui_style.get_stylesheet())
  3007. @QtCore.pyqtSlot(int)
  3008. def _change_undo_stack(self, ind: int):
  3009. if self.tabs.count() == 0:
  3010. self.add_new_tab()
  3011. self.undo_group.setActiveStack(self.undo_group.stacks()[0])
  3012. else:
  3013. stack = self.tabs.widget(ind).undo_stack
  3014. self.undo_group.setActiveStack(stack)
  3015. def sort_reverse(self):
  3016. prev_order = {i: self.tabs.widget(i) for i in range(self.tabs.count())}
  3017. self._sort_by_map(prev_order, reversed(list(prev_order.keys())))
  3018. def sort_tabs_by_name(self):
  3019. tab_names = {}
  3020. for i in range(self.tabs.count()):
  3021. key = [self.tabs.tabText(i), 0]
  3022. while tuple(key) in tab_names:
  3023. key[1] += 1
  3024. tab_names[tuple(key)] = self.tabs.widget(i)
  3025. sorted_names = sorted(tab_names.keys())
  3026. self._sort_by_map(tab_names, sorted_names)
  3027. def sort_tabs_by_creation_time(self):
  3028. widgets = [self.tabs.widget(i) for i in range(self.tabs.count())]
  3029. tab_times = {}
  3030. for widget in widgets:
  3031. key = [widget.creation_time, 0]
  3032. while tuple(key) in tab_times:
  3033. key[1] += 1
  3034. tab_times[tuple(key)] = widget
  3035. sorted_times = sorted(tab_times.keys())
  3036. self._sort_by_map(tab_times, sorted_times)
  3037. def sort_tabs_by_n_features(self):
  3038. widgets = [self.tabs.widget(i) for i in range(self.tabs.count())]
  3039. tab_count = {}
  3040. for widget in widgets:
  3041. if isinstance(widget, FilterTabPage):
  3042. if widget.filter_obj is None:
  3043. key = [0, 0]
  3044. else:
  3045. key = [widget.filter_obj.shape[0], 0]
  3046. elif isinstance(widget, SetTabPage):
  3047. key = [len(widget.gene_set), 0]
  3048. else:
  3049. raise TypeError(type(widget))
  3050. while tuple(key) in tab_count:
  3051. key[1] += 1
  3052. tab_count[tuple(key)] = widget
  3053. self._sort_by_map(tab_count, sorted(tab_count.keys(), reverse=True))
  3054. def sort_tabs_by_type(self):
  3055. widgets = [self.tabs.widget(i) for i in range(self.tabs.count())]
  3056. type_to_order = {
  3057. filtering.CountFilter: 1,
  3058. filtering.DESeqFilter: 2,
  3059. filtering.FoldChangeFilter: 3,
  3060. filtering.Filter: 4,
  3061. enrichment.FeatureSet: 5,
  3062. type(None): 6,
  3063. }
  3064. tab_types = {}
  3065. for widget in widgets:
  3066. if isinstance(widget, FilterTabPage):
  3067. key = [type_to_order[type(widget.filter_obj)], 0]
  3068. elif isinstance(widget, SetTabPage):
  3069. key = [type_to_order[type(widget.gene_set)], 0]
  3070. else:
  3071. raise TypeError(type(widget))
  3072. while tuple(key) in tab_types:
  3073. key[1] += 1
  3074. tab_types[tuple(key)] = widget
  3075. self._sort_by_map(tab_types, sorted(tab_types.keys()))
  3076. def _sort_by_map(self, key_map: dict, sorted_keys: typing.Iterable):
  3077. for to_ind, name in enumerate(sorted_keys):
  3078. widget = key_map[name]
  3079. from_ind = self.tabs.indexOf(widget)
  3080. self.tabs.tabBar().moveTab(from_ind, to_ind)
  3081. def add_new_tab_at(self, index: int, name: str = None, is_set: bool = False):
  3082. self.add_new_tab(name, is_set)
  3083. self.tabs.tabBar().moveTab(self.tabs.currentIndex(), index)
  3084. def add_new_tab(self, name: str = None, is_set: bool = False):
  3085. new_undo_stack = QtGui.QUndoStack()
  3086. self.undo_group.addStack(new_undo_stack)
  3087. if name is None:
  3088. name = 'New Table'
  3089. else:
  3090. print(name)
  3091. if is_set:
  3092. tab = SetTabPage(name, parent=self.tabs, undo_stack=new_undo_stack)
  3093. else:
  3094. tab = FilterTabPage(self.tabs, undo_stack=new_undo_stack)
  3095. tab.startedClustering.connect(self.start_clustering)
  3096. tab.startedJob.connect(self.start_generic_job)
  3097. tab.filterObjectCreated.connect(self.new_tab_from_filter_obj)
  3098. tab.featureSetCreated.connect(self.new_tab_from_gene_set)
  3099. tab.changeIcon.connect(self.set_current_tab_icon)
  3100. tab.tabNameChange.connect(self.rename_tab)
  3101. tab.geneSetsRequested.connect(self.update_gene_sets_widget)
  3102. tab.functionApplied.connect(self.update_report_from_worker)
  3103. tab.itemSpawned.connect(self.update_report_spawn)
  3104. tab.tabLoaded.connect(self.add_loaded_item_to_report)
  3105. tab.tabReverted.connect(self.remove_tab_from_report)
  3106. self.tabs.addTab(tab, name)
  3107. self.tabs.setCurrentIndex(self.tabs.count() - 1)
  3108. @QtCore.pyqtSlot(object)
  3109. def update_gene_sets_widget(self, widget: gui_widgets.GeneSetComboBox):
  3110. widget.update_gene_sets(self.get_available_objects())
  3111. @QtCore.pyqtSlot(str, bool)
  3112. def rename_tab(self, new_name: str, is_unsaved: bool):
  3113. if is_unsaved:
  3114. new_name += '*'
  3115. else:
  3116. new_name = new_name.rstrip('*')
  3117. self.tabs.setTabText(self.tabs.currentIndex(), new_name)
  3118. self.tabs.setTabToolTip(self.tabs.currentIndex(), new_name)
  3119. @QtCore.pyqtSlot()
  3120. def remove_tab_asterisk(self):
  3121. current_name = self.tabs.tabText(self.tabs.currentIndex())
  3122. self.tabs.setTabText(self.tab.currentIndex(), current_name.rstrip('*'))
  3123. def new_table_from_folder(self):
  3124. folder_name = QtWidgets.QFileDialog.getExistingDirectory(self, 'Choose directory')
  3125. if folder_name:
  3126. filter_obj = filtering.CountFilter.from_folder(folder_name)
  3127. if self.tabs.currentWidget().is_empty():
  3128. self.tabs.removeTab(self.tabs.currentIndex())
  3129. self.new_tab_from_filter_obj(filter_obj, JOB_COUNTER.get_id())
  3130. def new_table_from_folder_htseqcount(self):
  3131. folder_name = QtWidgets.QFileDialog.getExistingDirectory(self, 'Choose directory')
  3132. if folder_name:
  3133. normalize_answer = QtWidgets.QMessageBox.question(
  3134. self,
  3135. 'Normalize values?',
  3136. 'Do you want to normalize your count table to reads-per-million (RPM)?',
  3137. QtWidgets.QMessageBox.StandardButton.Yes | QtWidgets.QMessageBox.StandardButton.No,
  3138. )
  3139. to_normalize = normalize_answer == QtWidgets.QMessageBox.StandardButton.Yes
  3140. filter_obj = filtering.CountFilter.from_folder_htseqcount(folder_name, norm_to_rpm=to_normalize)
  3141. if self.tabs.currentWidget().is_empty():
  3142. self.tabs.removeTab(self.tabs.currentIndex())
  3143. self.new_tab_from_filter_obj(filter_obj, JOB_COUNTER.get_id())
  3144. def load_multiple_files(self):
  3145. filenames, _ = QtWidgets.QFileDialog.getOpenFileNames(self, 'Choose files')
  3146. if len(filenames) > 0:
  3147. window = MultiOpenWindow(filenames, self)
  3148. accepted = window.exec()
  3149. if accepted:
  3150. paths, types, names, kwargs = window.result()
  3151. tabs_to_close = None
  3152. if self.tabs.currentWidget().is_empty():
  3153. tabs_to_close = self.tabs.currentIndex()
  3154. for filename in filenames:
  3155. path = paths[filename]
  3156. table_type = FILTER_OBJ_TYPES[types[filename]]
  3157. name = names[filename]
  3158. filter_obj = table_type(path, **kwargs[filename])
  3159. if name == '':
  3160. self.new_tab_from_filter_obj(filter_obj, JOB_COUNTER.get_id())
  3161. else:
  3162. self.new_tab_from_filter_obj(filter_obj, JOB_COUNTER.get_id(), name)
  3163. QtWidgets.QApplication.processEvents()
  3164. if tabs_to_close is not None:
  3165. self.tabs.removeTab(tabs_to_close)
  3166. @QtCore.pyqtSlot(filtering.Filter, int, str)
  3167. @QtCore.pyqtSlot(filtering.Filter, int)
  3168. def new_tab_from_filter_obj(self, filter_obj: filtering.Filter, tab_id: int, name: str = None):
  3169. self.add_new_tab(filter_obj.fname.name if name is None else name)
  3170. self.tabs.currentWidget().start_from_filter_obj(filter_obj, tab_id)
  3171. if self._generate_report:
  3172. self.add_loaded_item_to_report(tab_id, self.tabs.currentWidget().name, self.tabs.currentWidget().obj())
  3173. @QtCore.pyqtSlot(str)
  3174. def set_current_tab_icon(self, icon_name: str = None):
  3175. self.set_tab_icon(self.tabs.currentIndex(), icon_name)
  3176. def set_tab_icon(self, ind: int, icon_name: str = None):
  3177. if icon_name is None:
  3178. if isinstance(self.tabs.currentWidget(), SetTabPage):
  3179. obj_type_str = 'set'
  3180. else:
  3181. obj_type = type(self.tabs.currentWidget().filter_obj)
  3182. obj_type_str = 'blank' if obj_type is type(None) else obj_type.__name__
  3183. icon = gui_graphics.get_icon(obj_type_str)
  3184. else:
  3185. icon = gui_graphics.get_icon(icon_name)
  3186. self.tabs.setTabIcon(ind, icon)
  3187. @QtCore.pyqtSlot(set, int, str)
  3188. @QtCore.pyqtSlot(set, int)
  3189. def new_tab_from_gene_set(self, gene_set: enrichment.FeatureSet, tab_id: int, gene_set_name: str = None):
  3190. if gene_set_name is None and validation.isinstanceinh(gene_set, enrichment.FeatureSet):
  3191. gene_set_name = gene_set.set_name
  3192. self.add_new_tab(gene_set_name, is_set=True)
  3193. self.tabs.currentWidget().start_from_gene_set(tab_id, gene_set)
  3194. if self._generate_report:
  3195. self.add_loaded_item_to_report(tab_id, self.tabs.currentWidget().name, self.tabs.currentWidget().obj())
  3196. def add_loaded_item_to_report(
  3197. self, item_id: int, item_name: str, obj: Union[filtering.Filter, enrichment.FeatureSet, generic.GenericPipeline]
  3198. ):
  3199. if not self._generate_report:
  3200. return
  3201. if item_id in self.report.nodes:
  3202. if self.report.nodes[item_id].is_active:
  3203. return
  3204. self.report.add_node('reactivate node', item_id, [0], '', '', '')
  3205. return
  3206. obj_type = self._get_spawn_type(obj)
  3207. prefix = f'{item_id}_{item_name}' if obj_type in ('Other output', 'Pipeline') else str(item_id)
  3208. filename = self._cache_spawn(obj, prefix)
  3209. desc = self._format_report_desc(obj, filename, obj_type)
  3210. label = f"Loaded {obj_type}\n'{item_name}'"
  3211. self.report.add_node(label, item_id, [0], desc, obj_type, filename)
  3212. def remove_tab_from_report(self, tab_id: int):
  3213. if self._generate_report:
  3214. self.report.trim_node(tab_id)
  3215. def update_report(
  3216. self,
  3217. name: str,
  3218. job_id: int,
  3219. predecessor_ids: List[int],
  3220. description: str,
  3221. node_type: str = 'Other table',
  3222. filename: Union[str, Path] = None,
  3223. ):
  3224. self.report.add_node(name, job_id, predecessor_ids, description, node_type, filename)
  3225. @QtCore.pyqtSlot(gui_widgets.WorkerOutput)
  3226. def update_report_from_worker(self, worker_output: gui_widgets.WorkerOutput):
  3227. if not self._generate_report:
  3228. return
  3229. if worker_output.raised_exception:
  3230. return
  3231. partial = worker_output.partial
  3232. kwargs = partial.keywords
  3233. name = generic.get_method_readable_name(partial.func)
  3234. desc = f'{partial.func.__name__}({parsing.format_dict_for_display(kwargs)})'.replace('\n', '<br>')
  3235. self.update_report(name, worker_output.job_id, worker_output.predecessor_ids, desc, 'Function')
  3236. def update_report_spawn(
  3237. self,
  3238. name: str,
  3239. spawn_id: int,
  3240. predecessor_id: int,
  3241. spawn: Union[filtering.Filter, enrichment.FeatureSet, pl.DataFrame, plt.Figure, generic.GenericPipeline],
  3242. ):
  3243. if not self._generate_report:
  3244. return
  3245. if spawn is None:
  3246. return
  3247. if not self.is_valid_spawn(spawn):
  3248. raise InternalError(f"Invalid spawn type '{type(spawn)}'!")
  3249. spawn_type = self._get_spawn_type(spawn)
  3250. prefix = f'{spawn_id}_{name}' if spawn_type in ('Other output', 'Pipeline') else str(spawn_id)
  3251. filename = self._cache_spawn(spawn, prefix)
  3252. desc = self._format_report_desc(spawn, filename, spawn_type)
  3253. self.update_report(name, spawn_id, [predecessor_id], desc, spawn_type, filename)
  3254. @staticmethod
  3255. def is_valid_spawn(spawn: object):
  3256. if (
  3257. validation.isinstanceinh(spawn, filtering.Filter)
  3258. or validation.isinstanceinh(spawn, enrichment.FeatureSet)
  3259. or isinstance(spawn, (pl.Series, pl.DataFrame, plt.Figure, Path))
  3260. or validation.isinstanceinh(spawn, generic.GenericPipeline)
  3261. ):
  3262. return True
  3263. return False
  3264. @staticmethod
  3265. def _get_spawn_type(spawn: Union[filtering.Filter, enrichment.FeatureSet, pl.DataFrame, plt.Figure]):
  3266. if validation.isinstanceinh(spawn, filtering.Filter):
  3267. spawn_type = FILTER_OBJ_TYPES_INV.get(type(spawn).__name__, 'Other table')
  3268. elif validation.isinstanceinh(spawn, enrichment.FeatureSet):
  3269. spawn_type = 'Gene set'
  3270. elif isinstance(spawn, (pl.Series, pl.DataFrame, plt.Figure, Path)):
  3271. spawn_type = 'Other output'
  3272. elif validation.isinstanceinh(spawn, generic.GenericPipeline):
  3273. spawn_type = 'Pipeline'
  3274. else:
  3275. raise TypeError(f"Invalid spawn type '{type(spawn)}' for spawn '{spawn}'!")
  3276. return spawn_type
  3277. @staticmethod
  3278. def _cache_spawn(spawn: Union[filtering.Filter, enrichment.FeatureSet, pl.DataFrame], suffix: str):
  3279. obj = spawn
  3280. if validation.isinstanceinh(spawn, filtering.Filter):
  3281. obj = spawn.df
  3282. filename = parsing.slugify(f'{suffix}_{spawn.fname.stem}') + '.parquet'
  3283. elif validation.isinstanceinh(spawn, enrichment.FeatureSet):
  3284. filename = parsing.slugify(f'{suffix}_{spawn.set_name}') + '.txt'
  3285. elif isinstance(spawn, (pl.Series, pl.DataFrame)):
  3286. filename = parsing.slugify(suffix) + '.parquet'
  3287. elif isinstance(spawn, plt.Figure):
  3288. filename = parsing.slugify(suffix) + '.svg'
  3289. elif validation.isinstanceinh(spawn, generic.GenericPipeline):
  3290. filename = parsing.slugify(suffix) + '.yaml'
  3291. obj = spawn.export_pipeline(None)
  3292. elif isinstance(spawn, Path):
  3293. filename = parsing.slugify(suffix) + spawn.suffix
  3294. else:
  3295. raise TypeError(f"Invalid spawn type '{type(spawn)}' for spawn '{spawn}'!")
  3296. io.cache_gui_file(obj, filename)
  3297. return filename
  3298. @staticmethod
  3299. def _format_report_desc(
  3300. obj: Union[filtering.Filter, enrichment.FeatureSet, pl.DataFrame], filename: str, obj_type: str
  3301. ):
  3302. href = Path('data').joinpath(filename).as_posix()
  3303. if validation.isinstanceinh(obj, filtering.Filter):
  3304. html = parsing.df_to_html(obj.df)
  3305. shape = obj.shape if len(obj.shape) >= 2 else (obj.shape[0], 1)
  3306. desc = f'{obj_type}:<br>"{obj.fname.stem}"<br>{html}{shape[0]} rows, {shape[1]} columns'
  3307. elif validation.isinstanceinh(obj, enrichment.FeatureSet):
  3308. items = []
  3309. for i, item in enumerate(obj.gene_set):
  3310. if i == 5:
  3311. break
  3312. items.append(item)
  3313. items.append('...')
  3314. desc = (
  3315. f'{obj_type}:<br>"{obj.set_name}"<br>{parsing.items_to_html_table(items)}{len(obj.gene_set)} features'
  3316. )
  3317. elif isinstance(obj, (pl.DataFrame, pl.Series)):
  3318. desc = parsing.df_to_html(obj)
  3319. elif isinstance(obj, plt.Figure):
  3320. desc = f'<img src="data/{filename}" alt="Figure" height="400">'
  3321. elif validation.isinstanceinh(obj, generic.GenericPipeline):
  3322. desc = str(obj).replace('\n', '<br>')
  3323. elif isinstance(obj, Path):
  3324. data = io.load_cached_gui_file(obj)
  3325. desc = f'<pre><code>{data}</code></pre>'.replace('\n', '<br>')
  3326. else:
  3327. raise TypeError(f"Invalid object type '{type(obj)}' of object '{obj}'.")
  3328. desc += f'<br><a href="{href}" target="_blank" rel="noopener noreferrer">Open file</a>'
  3329. return desc
  3330. def delete_pipeline(self):
  3331. if len(self.pipelines) == 0:
  3332. warnings.warn('No Pipelines to delete')
  3333. return
  3334. pipeline_name, status = QtWidgets.QInputDialog.getItem(
  3335. self, 'Delete Pipeline', 'Choose Pipeline to delete:', self.pipelines.keys()
  3336. )
  3337. if status:
  3338. reply = QtWidgets.QMessageBox.question(
  3339. self,
  3340. 'Delete Pipeline?',
  3341. 'Are you sure you want to delete this Pipeline? This action cannot be undone!',
  3342. QtWidgets.QMessageBox.StandardButton.No | QtWidgets.QMessageBox.StandardButton.Yes,
  3343. )
  3344. if reply == QtWidgets.QMessageBox.StandardButton.Yes:
  3345. self.pipelines.pop(pipeline_name)
  3346. print(f"Pipeline '{pipeline_name}' deleted successfully")
  3347. def export_pipeline(self):
  3348. if len(self.pipelines) == 0:
  3349. warnings.warn('No Pipelines to export')
  3350. return
  3351. pipeline_name, status = QtWidgets.QInputDialog.getItem(
  3352. self, 'Export Pipeline', 'Choose Pipeline to export:', self.pipelines.keys()
  3353. )
  3354. if status:
  3355. pipeline = self.pipelines[pipeline_name][0]
  3356. self._export_pipeline_from_obj(pipeline_name, pipeline)
  3357. def _export_pipeline_from_obj(self, pipeline_name: str, pipeline: filtering.Pipeline):
  3358. default_name = parsing.slugify(pipeline_name) + '.yaml'
  3359. filename, _ = QtWidgets.QFileDialog.getSaveFileName(
  3360. self, 'Save Pipeline', str(Path.home().joinpath(default_name)), 'YAML file (*.yaml)'
  3361. )
  3362. if filename:
  3363. pipeline.export_pipeline(filename)
  3364. print(f'Successfully saved at {io.get_datetime()} under {filename}')
  3365. def _import_pipeline_from_str(self, pipeline_name: str, content: str):
  3366. d = yaml.safe_load(content)
  3367. if d.get('filter_type') is not None:
  3368. pipeline = filtering.Pipeline.import_pipeline(content)
  3369. elif d['metadata'].get('pipeline_type') == 'single':
  3370. pipeline = fastq.SingleEndPipeline.import_pipeline(content)
  3371. elif d['metadata'].get('pipeline_type') == 'paired':
  3372. pipeline = fastq.PairedEndPipeline.import_pipeline(content)
  3373. else:
  3374. raise TypeError(f"Pipeline file '{content}' is invalid.")
  3375. pipeline_id = JOB_COUNTER.get_id()
  3376. self.pipelines[pipeline_name] = (pipeline, pipeline_id)
  3377. if self._generate_report:
  3378. self.add_loaded_item_to_report(pipeline_id, pipeline_name, pipeline)
  3379. def import_pipeline(self):
  3380. filename, _ = QtWidgets.QFileDialog.getOpenFileName(
  3381. self, 'Choose a Pipeline file', str(Path.home()), 'YAML file (*.yaml)'
  3382. )
  3383. if filename:
  3384. pipeline_name = str(Path(filename).stem)
  3385. with open(filename) as f:
  3386. content = f.read()
  3387. self._import_pipeline_from_str(pipeline_name, content)
  3388. def import_multiple_gene_sets(self):
  3389. filenames, _ = QtWidgets.QFileDialog.getOpenFileNames(self, 'Choose files')
  3390. tabs_to_close = None
  3391. if len(filenames) > 0 and self.tabs.currentWidget().is_empty():
  3392. tabs_to_close = self.tabs.currentIndex()
  3393. for filename in filenames:
  3394. gene_set = self._filename_to_gene_set(filename)
  3395. self.new_tab_from_gene_set(gene_set, JOB_COUNTER.get_id(), Path(filename).stem)
  3396. if tabs_to_close is not None:
  3397. self.tabs.removeTab(tabs_to_close)
  3398. def import_gene_set(self):
  3399. filename, _ = QtWidgets.QFileDialog.getOpenFileName(
  3400. self,
  3401. 'Choose a file',
  3402. filter='Text Document (*.txt);;Comma-Separated Values (*.csv);;Tab-Separated Values (*.tsv);;All Files (*)',
  3403. )
  3404. if filename:
  3405. tabs_to_close = None
  3406. if self.tabs.currentWidget().is_empty():
  3407. tabs_to_close = self.tabs.currentIndex()
  3408. gene_set = self._filename_to_gene_set(filename)
  3409. self.new_tab_from_gene_set(gene_set, JOB_COUNTER.get_id(), Path(filename).stem)
  3410. if tabs_to_close is not None:
  3411. self.tabs.removeTab(tabs_to_close)
  3412. @staticmethod
  3413. def _filename_to_gene_set(filename: str):
  3414. if filename.endswith(('.csv', '.tsv')):
  3415. separator = '\t' if filename.endswith('.tsv') else ','
  3416. first_col = pl.scan_csv(filename, separator=separator).select(pl.first()).collect().to_series()
  3417. if first_col.dtype == pl.String:
  3418. # trim stray leading/trailing whitespace so imported identifiers match how gene IDs
  3419. # are read from reference and count tables (io.load_table), then null out and drop
  3420. # cells that were empty/whitespace-only so they don't import as an empty "gene"
  3421. first_col = first_col.str.strip_chars().replace('', None).drop_nulls()
  3422. gene_set = parsing.data_to_set(first_col)
  3423. else:
  3424. with open(filename) as f:
  3425. gene_set = {line.strip() for line in f.readlines()}
  3426. return gene_set
  3427. def close_current_tab(self):
  3428. ind = self.tabs.currentIndex()
  3429. self.close_tab(ind)
  3430. def close_other_tabs(self, index: int):
  3431. self.close_tabs_to_the_right(index)
  3432. self.close_tabs_to_the_left(index)
  3433. def close_tabs_to_the_right(self, index: int):
  3434. while self.tabs.count() > index + 1:
  3435. self.close_tab(self.tabs.count() - 1)
  3436. def close_tabs_to_the_left(self, index: int):
  3437. start_count = self.tabs.count()
  3438. while self.tabs.count() > start_count - index:
  3439. self.close_tab(0)
  3440. def close_tab(self, index: int):
  3441. if self.tabs.widget(index).is_empty():
  3442. self.tabs.removeTab(index)
  3443. else:
  3444. self.undo_group.removeStack(self.tabs.widget(index).undo_stack)
  3445. command = CloseTabCommand(self.tabs, index, '"' + self.tabs.tabText(index).rstrip('*') + '"')
  3446. self.closed_tabs_stack.push(command)
  3447. def copy_gene_set(self):
  3448. gene_set = self.tabs.currentWidget().get_index_string()
  3449. QtWidgets.QApplication.clipboard().setText(gene_set)
  3450. def excepthook(self, exc_type, exc_value, exc_tb): # pragma: no cover
  3451. sys.__excepthook__(exc_type, exc_value, exc_tb)
  3452. self.error_window = gui_windows.ErrorMessage(exc_type, exc_value, exc_tb, self)
  3453. self.error_window.exec()
  3454. def _get_current_console(self): # pragma: no cover
  3455. if self.pipeline_window is not None and self.pipeline_window.isVisible():
  3456. current_console = self.pipeline_window.get_console()
  3457. else:
  3458. current_console = self.tabs.currentWidget().get_console()
  3459. return current_console
  3460. @QtCore.pyqtSlot(str)
  3461. def append_text_to_current_console(self, text: str): # pragma: no cover
  3462. current_console = self._get_current_console()
  3463. current_console.append_text(text)
  3464. def add_pipeline(self):
  3465. self.pipeline_window = CreatePipelineWindow(self)
  3466. self.pipeline_window.pipelineSaved.connect(self.save_pipeline)
  3467. self.pipeline_window.pipelineExported.connect(self._export_pipeline_from_obj)
  3468. self.pipeline_window.exec()
  3469. # self.pipeline_window = None
  3470. @QtCore.pyqtSlot(str)
  3471. def edit_pipeline(self, pipeline_name: str):
  3472. if pipeline_name not in self.pipelines:
  3473. raise InternalError(f"Pipeline {pipeline_name} doesn't exist!")
  3474. pipeline = self.pipelines[pipeline_name][0]
  3475. self.pipeline_window = CreatePipelineWindow.start_from_pipeline(pipeline, pipeline_name, self)
  3476. self.pipeline_window.pipelineSaved.connect(self.save_pipeline)
  3477. self.pipeline_window.pipelineExported.connect(self._export_pipeline_from_obj)
  3478. self.pipeline_window.exec()
  3479. self.pipeline_window = None
  3480. @QtCore.pyqtSlot(str, generic.GenericPipeline)
  3481. def save_pipeline(self, pipeline_name: str, pipeline: filtering.Pipeline):
  3482. if pipeline_name in self.pipelines:
  3483. is_new = False
  3484. response = QtWidgets.QMessageBox.question(
  3485. self,
  3486. 'Overwrite Pipeline?',
  3487. 'A Pipeline with this name already exists. Are you sure you want to overwrite it?',
  3488. defaultButton=QtWidgets.QMessageBox.StandardButton.No,
  3489. )
  3490. else:
  3491. is_new = True
  3492. response = QtWidgets.QMessageBox.StandardButton.Yes
  3493. if response == QtWidgets.QMessageBox.StandardButton.Yes:
  3494. new_pipeline_id = JOB_COUNTER.get_id()
  3495. if self._generate_report:
  3496. if is_new:
  3497. self.add_loaded_item_to_report(new_pipeline_id, pipeline_name, pipeline)
  3498. else:
  3499. self.update_report_spawn(pipeline_name, new_pipeline_id, self.pipelines[pipeline_name][1], pipeline)
  3500. self.pipelines[pipeline_name] = (pipeline, new_pipeline_id)
  3501. print(f"Successfully saved Pipeline '{pipeline_name}'")
  3502. def settings(self):
  3503. self.settings_window.exec()
  3504. def create_action(self, name, triggered_func, checkable=False, checked=False, enabled=True, shortcut=None):
  3505. action = QtGui.QAction(name, self)
  3506. action.triggered.connect(triggered_func)
  3507. action.setCheckable(checkable)
  3508. action.setChecked(checked)
  3509. action.setEnabled(enabled)
  3510. if shortcut:
  3511. action.setShortcut(QtGui.QKeySequence(shortcut))
  3512. return action
  3513. def init_actions(self):
  3514. # Table Actions
  3515. self.new_table_action = self.create_action(
  3516. '&New table', functools.partial(self.add_new_tab, name=None), shortcut='Ctrl+N'
  3517. )
  3518. self.new_table_from_folder_action = self.create_action('New table from &folder', self.new_table_from_folder)
  3519. self.new_table_from_folder_htseq_action = self.create_action(
  3520. 'New table from folder (HTSeq-count output)', self.new_table_from_folder_htseqcount
  3521. )
  3522. self.new_multiple_action = self.create_action(
  3523. '&Multiple new tables', self.load_multiple_files, shortcut='Ctrl+Shift+N'
  3524. )
  3525. # File Actions
  3526. self.save_action = self.create_action('&Save...', self.save_file, shortcut='Ctrl+S')
  3527. self.load_session_action = self.create_action('&Load session...', self.load_session)
  3528. self.save_session_action = self.create_action('Sa&ve session...', self.save_session)
  3529. self.clear_session_action = self.create_action('Clea&r session...', self.clear_session)
  3530. # Settings and Updates
  3531. self.clear_cache_action = self.create_action('&Clear cache...', self.clear_cache)
  3532. self.settings_action = self.create_action('&Settings...', self.settings)
  3533. self.exit_action = self.create_action('&Exit', self.close)
  3534. self.check_update_action = self.create_action('Check for &updates...', self.check_for_updates)
  3535. # Report Actions
  3536. self.toggle_report_action = self.create_action('&Enable report generation', self._toggle_reporting, True, False)
  3537. self.generate_report_action = self.create_action('&Create session report', self.generate_report)
  3538. # Undo and Redo Actions
  3539. self.undo_action = self.undo_group.createUndoAction(self, 'Ctrl+Z')
  3540. self.redo_action = self.undo_group.createRedoAction(self, 'Ctrl+Shift+Z')
  3541. self.restore_tab_action = self.create_action(
  3542. 'Restore tab', self.closed_tabs_stack.undo, shortcut='Ctrl+Shift+T'
  3543. )
  3544. self.close_current_action = self.create_action('&Close current tab', self.close_current_tab, shortcut='Ctrl+W')
  3545. # View and History Actions
  3546. self.close_figs_action = self.create_action('Close all &Figures', functools.partial(plt.close, 'all'))
  3547. self.task_queue_action = self.create_action('Task &queue', self.task_queue_window.show)
  3548. self.status_bar.taskQueueRequested.connect(self.task_queue_action.trigger)
  3549. self.show_history_action = self.create_action(
  3550. 'Command &History', self.toggle_history_window, checkable=True, checked=True
  3551. )
  3552. self.clear_history_action = self.create_action('Clea&r command history', self.clear_history)
  3553. # Gene Set Actions
  3554. self.copy_action = self.create_action('&Copy Gene Set', self.copy_gene_set, shortcut='Ctrl+C')
  3555. self.import_set_action = self.create_action(
  3556. '&Import Gene Set...', self.import_gene_set, shortcut='Ctrl+Shift+I'
  3557. )
  3558. self.import_multiple_sets_action = self.create_action(
  3559. 'Import &Multiple Gene Sets...', self.import_multiple_gene_sets, shortcut='Ctrl+Shift+M'
  3560. )
  3561. self.export_set_action = self.create_action(
  3562. '&Export Gene Set...', self.export_gene_set, shortcut='Ctrl+Shift+E'
  3563. )
  3564. self.set_op_action = self.create_action('Set &Operations...', self.choose_set_op, shortcut='Ctrl+Shift+O')
  3565. self.enrichment_action = self.create_action(
  3566. 'Enrichment &Analysis...', self.open_enrichment_analysis, shortcut='Ctrl+Shift+A'
  3567. )
  3568. self.set_vis_action = self.create_action(
  3569. '&Visualize Gene Sets...', self.visualize_gene_sets, shortcut='Ctrl+Shift+V'
  3570. )
  3571. # External Tool Actions
  3572. self.cutadapt_single_action = self.create_action(
  3573. '&Single-end adapter trimming...', functools.partial(self.start_external_window, CutAdaptSingleWindow)
  3574. )
  3575. self.cutadapt_paired_action = self.create_action(
  3576. '&Paired-end adapter trimming...', functools.partial(self.start_external_window, CutAdaptPairedWindow)
  3577. )
  3578. self.kallisto_index_action = self.create_action(
  3579. 'kallisto build &index...', functools.partial(self.start_external_window, KallistoIndexWindow)
  3580. )
  3581. self.kallisto_single_action = self.create_action(
  3582. '&kallisto Single-end RNA-seq quantification...',
  3583. functools.partial(self.start_external_window, KallistoSingleWindow),
  3584. )
  3585. self.kallisto_paired_action = self.create_action(
  3586. 'kallisto &Paired-end RNA-seq quantification...',
  3587. functools.partial(self.start_external_window, KallistoPairedWindow),
  3588. )
  3589. self.bowtie2_index_action = self.create_action(
  3590. 'Bowtie2 build &index...', functools.partial(self.start_external_window, Bowtie2IndexWindow)
  3591. )
  3592. self.bowtie2_single_action = self.create_action(
  3593. 'Bowtie2 &Single-end alignment...', functools.partial(self.start_external_window, Bowtie2SingleWindow)
  3594. )
  3595. self.bowtie2_paired_action = self.create_action(
  3596. 'Bowtie2 &Paired-end alignment...', functools.partial(self.start_external_window, Bowtie2PairedWindow)
  3597. )
  3598. self.featurecounts_single_action = self.create_action(
  3599. '&featureCounts Single-end counting...',
  3600. functools.partial(self.start_external_window, FeatureCountsSingleWindow),
  3601. )
  3602. self.featurecounts_paired_action = self.create_action(
  3603. 'featureCounts &Paired-end counting...',
  3604. functools.partial(self.start_external_window, FeatureCountsPairedWindow),
  3605. )
  3606. self.sam2fastq_single_action = self.create_action(
  3607. 'Convert SAM/BAM to FASTQ (single-end)...',
  3608. functools.partial(self.start_external_window, SamToFastqSingleWindow),
  3609. )
  3610. self.sam2fastq_paired_action = self.create_action(
  3611. 'Convert SAM/BAM to FASTQ (paired-end)...',
  3612. functools.partial(self.start_external_window, SamToFastqPairedWindow),
  3613. )
  3614. self.fastq2sam_single_action = self.create_action(
  3615. 'Convert FASTQ to SAM (single-end)...',
  3616. functools.partial(self.start_external_window, FastqToSamSingleWindow),
  3617. )
  3618. self.fastq2sam_paired_action = self.create_action(
  3619. 'Convert FASTQ to SAM (paired-end)...',
  3620. functools.partial(self.start_external_window, FastqToSamPairedWindow),
  3621. )
  3622. self.convert_sam_action = self.create_action(
  3623. 'Convert SAM/BAM to BAM/SAM...', functools.partial(self.start_external_window, ConvertSamFormatWindow)
  3624. )
  3625. self.bam_index_action = self.create_action(
  3626. 'Create BAM index...', functools.partial(self.start_external_window, BamIndexWindow)
  3627. )
  3628. self.sort_sam_action = self.create_action(
  3629. 'Sort SAM/BAM...', functools.partial(self.start_external_window, SortSamWindow)
  3630. )
  3631. self.validate_sam_action = self.create_action(
  3632. 'Validate SAM/BAM...', functools.partial(self.start_external_window, ValidateSamWindow)
  3633. )
  3634. self.find_duplicates_action = self.create_action(
  3635. 'Find PCR/optical &duplicates...', functools.partial(self.start_external_window, FindDuplicatesWindow)
  3636. )
  3637. # Conditional Action for Platform-Specific Use
  3638. action_name = (
  3639. 'ShortStack small &RNA alignment...'
  3640. if platform.system() != 'Windows'
  3641. else 'ShortStack small &RNA alignment (not available on Windows)'
  3642. )
  3643. self.shortstack_action = self.create_action(
  3644. action_name,
  3645. functools.partial(self.start_external_window, ShortStackWindow),
  3646. enabled=platform.system() != 'Windows',
  3647. )
  3648. # Visualization Actions
  3649. self.bar_plot_action = self.create_action(
  3650. 'Create enrichment &bar-plot...', functools.partial(self.start_external_window, BarPlotWindow)
  3651. )
  3652. self.ontology_graph_action = self.create_action(
  3653. 'Visualize &Gene Ontology...', functools.partial(self.start_external_window, OntologyGraphWindow)
  3654. )
  3655. self.pathway_graph_action = self.create_action(
  3656. 'Visualize &KEGG Pathway...', functools.partial(self.start_external_window, PathwayGraphWindow)
  3657. )
  3658. # Help Actions
  3659. self.quick_start_action = self.create_action('&Quick-start guide', self.quickstart_window.show)
  3660. self.user_guide_action = self.create_action(
  3661. '&User Guide', functools.partial(self.open_link, self.USER_GUIDE_URL)
  3662. )
  3663. self.tutorial_action = self.create_action('&Tutorial', functools.partial(self.open_link, self.TUTORIAL_URL))
  3664. self.faq_action = self.create_action(
  3665. '&Frequently Asked Questions', functools.partial(self.open_link, self.FAQ_URL)
  3666. )
  3667. self.bug_report_action = self.create_action(
  3668. 'Submit an &issue', functools.partial(self.open_link, self.BUGS_URL)
  3669. )
  3670. self.request_feature_action = self.create_action(
  3671. '&Request a feature', functools.partial(self.open_link, self.FEATURE_URL)
  3672. )
  3673. self.ask_question_action = self.create_action(
  3674. 'Ask a &question', functools.partial(self.open_link, self.QUESTION_URL)
  3675. )
  3676. self.check_update_action = self.create_action('Check for &updates...', self.check_for_updates)
  3677. self.whatsnew_action = self.create_action("&What's new in RNAlysis", self.whats_new)
  3678. self.about_action = self.create_action('&About', self.about)
  3679. self.cite_action = self.create_action('How to &cite RNAlysis', self.cite)
  3680. # Pipeline Actions
  3681. self.new_pipeline_action = self.create_action('&New Pipeline...', self.add_pipeline, shortcut='Ctrl+Alt+N')
  3682. self.import_pipeline_action = self.create_action(
  3683. '&Import Pipeline...', self.import_pipeline, shortcut='Ctrl+Alt+I'
  3684. )
  3685. self.export_pipeline_action = self.create_action(
  3686. '&Export Pipeline...', self.export_pipeline, shortcut='Ctrl+Alt+E'
  3687. )
  3688. self.delete_pipeline_action = self.create_action(
  3689. '&Delete Pipeline...', self.delete_pipeline, shortcut='Ctrl+Alt+D'
  3690. )
  3691. @QtCore.pyqtSlot()
  3692. def clear_cache(self):
  3693. reply = QtWidgets.QMessageBox.question(
  3694. self,
  3695. 'Clear cache?',
  3696. 'Are you sure you want to clear the <i>RNAlysis</i> cache? This cannot be undone!',
  3697. defaultButton=QtWidgets.QMessageBox.StandardButton.No,
  3698. )
  3699. if reply == QtWidgets.QMessageBox.StandardButton.Yes:
  3700. io.clear_gui_cache()
  3701. io.clear_cache()
  3702. @QtCore.pyqtSlot()
  3703. def check_for_updates(self, confirm_updated: bool = True): # pragma: no cover
  3704. if io.is_rnalysis_outdated():
  3705. # frozen releases of RNAlysis cannot update using pip. new version must be downloaded manually
  3706. if getattr(sys, 'frozen', False) and hasattr(sys, '_MEIPASS'):
  3707. reply = QtWidgets.QMessageBox.question(
  3708. self,
  3709. 'A new version is available',
  3710. 'A new version of <i>RNAlysis</i> is available! Do you wish to download it?',
  3711. )
  3712. if reply == QtWidgets.QMessageBox.StandardButton.Yes:
  3713. url = QtCore.QUrl('https://github.com/GuyTeichman/RNAlysis/releases/latest')
  3714. if not QtGui.QDesktopServices.openUrl(url):
  3715. QtWidgets.QMessageBox.warning(self, 'Connection failed', 'Could not download new version')
  3716. return
  3717. reply = QtWidgets.QMessageBox.question(
  3718. self,
  3719. 'A new version is available',
  3720. 'A new version of <i>RNAlysis</i> is available! Do you wish to update?',
  3721. )
  3722. if reply == QtWidgets.QMessageBox.StandardButton.Yes:
  3723. io.update_rnalysis()
  3724. QtCore.QCoreApplication.quit()
  3725. self.deleteLater()
  3726. QtCore.QProcess.startDetached(
  3727. Path(sys.executable).parent.joinpath('Scripts', 'rnalysis-gui').as_posix(), sys.argv
  3728. )
  3729. else:
  3730. if confirm_updated:
  3731. _ = QtWidgets.QMessageBox.information(
  3732. self,
  3733. 'You are using the latest version of RNAlysis',
  3734. f'Your version of <i>RNAlysis</i> ({__version__}) is up to date!',
  3735. )
  3736. @QtCore.pyqtSlot(bool)
  3737. def toggle_history_window(self, state: bool):
  3738. if state:
  3739. self.command_history_dock.show()
  3740. else:
  3741. self.command_history_dock.close()
  3742. def start_external_window(self, window_type: Type):
  3743. self.external_windows[window_type] = window_type(self)
  3744. func_name = self.external_windows[window_type].func_name
  3745. func = self.external_windows[window_type].func
  3746. self.external_windows[window_type].paramsAccepted.connect(
  3747. functools.partial(self.start_generic_job_from_params, func_name, func)
  3748. )
  3749. self.external_windows[window_type].geneSetsRequested.connect(self.update_gene_sets_widget)
  3750. self.external_windows[window_type].show()
  3751. def save_file(self):
  3752. self.tabs.currentWidget().save_file()
  3753. def export_gene_set(self):
  3754. this_tab = self.tabs.currentWidget()
  3755. if isinstance(this_tab, FilterTabPage):
  3756. filter_obj = this_tab.obj()
  3757. gene_set = filter_obj.index_set if filter_obj is not None else None
  3758. else:
  3759. gene_set = this_tab.obj()
  3760. if gene_set is None:
  3761. warnings.warn('Cannot export an empty gene set')
  3762. return
  3763. default_name = parsing.slugify(self.tabs.tabText(self.tabs.currentIndex()).rstrip('*')) + '.txt'
  3764. filename, _ = QtWidgets.QFileDialog.getSaveFileName(
  3765. self, 'Save gene set', str(Path.home().joinpath(default_name)), 'Text document (*.txt);;All Files (*)'
  3766. )
  3767. if filename:
  3768. io.save_gene_set(gene_set, filename)
  3769. print(f'Successfully saved at {io.get_datetime()} under {filename}')
  3770. def open_link(self, link: str):
  3771. url = QtCore.QUrl(link)
  3772. if not QtGui.QDesktopServices.openUrl(url):
  3773. QtGui.QMessageBox.warning(self, 'Connection failed', 'Could not open link. Please try again later. ')
  3774. def get_gene_set_by_ind(self, ind: int):
  3775. gene_set = (
  3776. self.tabs.widget(ind).filter_obj
  3777. if isinstance(self.tabs.currentWidget(), FilterTabPage)
  3778. else self.tabs.widget(ind).gene_set.gene_set
  3779. )
  3780. return gene_set
  3781. def get_available_objects(self):
  3782. tab_names = self.get_tab_names()
  3783. checked = {}
  3784. available_objects_unique = {}
  3785. for i, name in enumerate(tab_names):
  3786. if name in checked:
  3787. key = f'{name}_{checked[name]}'
  3788. else:
  3789. checked[name] = 1
  3790. key = name
  3791. available_objects_unique[key] = (self.tabs.widget(i), self.tabs.tabIcon(i))
  3792. checked[name] += 1
  3793. return available_objects_unique
  3794. def get_gene_set_by_name(self, name: str):
  3795. ind = self.get_tab_names().index(name)
  3796. return self.get_gene_set_by_ind(ind)
  3797. def choose_set_op(self):
  3798. available_objs = self.get_available_objects()
  3799. self.set_op_window = SetOperationWindow(available_objs, self)
  3800. self.set_op_window.primarySetUsed.connect(self.choose_tab_by_name)
  3801. self.set_op_window.geneSetReturned.connect(self.resolve_set_op)
  3802. self.set_op_window.show()
  3803. @QtCore.pyqtSlot(set, str, list, dict)
  3804. def resolve_set_op(self, output_set: set, output_name: str, ancestor_ids: list, kwargs: dict):
  3805. set_op_id = JOB_COUNTER.get_id()
  3806. new_tab_id = JOB_COUNTER.get_id()
  3807. if self._generate_report:
  3808. self.update_report(
  3809. output_name.replace(' output', ''),
  3810. set_op_id,
  3811. ancestor_ids,
  3812. parsing.format_dict_for_display(kwargs),
  3813. 'Function',
  3814. )
  3815. self.update_report_spawn(
  3816. 'Set operation output', new_tab_id, set_op_id, enrichment.FeatureSet(output_set, output_name)
  3817. )
  3818. self.new_tab_from_gene_set(output_set, new_tab_id, output_name)
  3819. def visualize_gene_sets(self):
  3820. available_objs = self.get_available_objects()
  3821. self.set_visualization_window = SetVisualizationWindow(available_objs, self)
  3822. self.set_visualization_window.show()
  3823. if self._generate_report:
  3824. self.set_visualization_window.figureGenerated.connect(self.resolve_set_vis)
  3825. @QtCore.pyqtSlot(plt.Figure, str, list, dict)
  3826. def resolve_set_vis(self, output_fig: plt.Figure, output_name: str, ancestor_ids: list, kwargs: dict):
  3827. func_id = JOB_COUNTER.get_id()
  3828. fig_id = JOB_COUNTER.get_id()
  3829. if self._generate_report:
  3830. self.update_report(output_name, func_id, ancestor_ids, parsing.format_dict_for_display(kwargs), 'Function')
  3831. self.update_report_spawn('Set visualization output', fig_id, func_id, output_fig)
  3832. @QtCore.pyqtSlot(str)
  3833. def choose_tab_by_name(self, set_name: str):
  3834. available_objs = self.get_available_objects()
  3835. for i, name in enumerate(available_objs.keys()):
  3836. if name == set_name:
  3837. self.tabs.setCurrentIndex(i)
  3838. return
  3839. def display_enrichment_results(self, result: pl.DataFrame, gene_set_name: str):
  3840. df_window = gui_windows.DataFrameView(result, f'Enrichment results for set "{gene_set_name}"', self)
  3841. self.enrichment_results.append(df_window)
  3842. df_window.show()
  3843. def open_enrichment_analysis(self):
  3844. self.enrichment_window = EnrichmentWindow(self)
  3845. self.enrichment_window.geneSetsRequested.connect(self.update_gene_sets_widget)
  3846. self.enrichment_window.enrichmentStarted.connect(self.start_enrichment)
  3847. if self._generate_report:
  3848. self.enrichment_window.functionApplied.connect(self.update_report_from_worker)
  3849. self.enrichment_window.show()
  3850. def get_tab_names(self) -> List[str]:
  3851. return [self.tabs.tabText(i).rstrip('*') for i in range(self.tabs.count())]
  3852. def generate_report(self):
  3853. if not self._generate_report:
  3854. QtWidgets.QMessageBox.warning(
  3855. self,
  3856. 'Report generation was not enabled!',
  3857. 'Cannot generate a report since report generation was not enabled. '
  3858. 'You can enable it from the Settings menu.',
  3859. )
  3860. return
  3861. dialog = self.report.generate_report_dialog(self)
  3862. def accept(args, kwargs, _):
  3863. outdir = kwargs.get('output_folder')
  3864. if outdir:
  3865. outdir = Path(outdir).joinpath('RNAlysis_report')
  3866. if not outdir.exists():
  3867. outdir.mkdir()
  3868. kwargs['output_folder'] = outdir
  3869. self.report.generate_report(*args, **kwargs)
  3870. self._save_session_to(outdir.joinpath('data', self.report.ROOT_FNAME))
  3871. dialog.paramsAccepted.connect(accept)
  3872. dialog.exec()
  3873. def init_menus(self):
  3874. self.setMenuBar(self.menu_bar)
  3875. file_menu = self.menu_bar.addMenu('&File')
  3876. self.new_menu = file_menu.addMenu('&New...')
  3877. self.new_menu.addActions(
  3878. [
  3879. self.new_table_action,
  3880. self.new_multiple_action,
  3881. self.new_table_from_folder_action,
  3882. self.new_table_from_folder_htseq_action,
  3883. ]
  3884. )
  3885. file_menu.addActions(
  3886. [
  3887. self.save_action,
  3888. self.load_session_action,
  3889. self.save_session_action,
  3890. self.clear_session_action,
  3891. self.clear_cache_action,
  3892. self.toggle_report_action,

gui.py at commit f1349da, under MIT · at the source

Overview

Authors: Chee Kiang Ewe1, Hanna Achache2, Hanna Schön3, Leonid Kontorovich1, Guy Teichman1, Shir Weiss1, Anna Mogilevskaya2, Myriam Valenski2, Sarit Anava1, Rutwik Bardapurkar1, Hila Gingold1, Rachel Posner1, Olga Antonova1, Mario de Bono3, Yonatan B. Tzur2, Oded Rechavi1
  1. School of Neurobiology, Biochemistry and Biophysics, Wise Faculty of Life Sciences and Sagol School of Neuroscience, Tel Aviv University; Chaim Levanon Street 55, Tel Aviv Yafo 6997801, Israel
  2. The Alexander Silberman Institute of Life Sciences, The Hebrew University of Jerusalem, Givat Ram, Jerusalem 9190401, Israel
  3. Institute of Science and Technology Austria (ISTA), Am Campus 1, Klosterneuburg 3400, Austria
Journal: Current biology : CB, volume 36, issue 14, pages 3566-3579.e5
Dates: received 7 October 2025; accepted 8 June 2026; published online 6 July 2026; in print 20 July 2026
Type: Research article · Language: English
License: CC BY
Identifiers: DOI 10.1016/j.cub.2026.06.016 · PMID 42409014 · PMCID PMC13399994 · OpenAlex W7167468578
Open access: hybrid, a free copy (OpenAlex)
Status: code verified
Categories: C. elegans (organism)
Methods: Statistics, Evoked potentials
Keywords: inter-tissue communication, reproduction, laboratory domestication, developmental plasticity
MeSH: Caenorhabditis elegans*, Germ Cells*, Heat-Shock Response*, Oxygen*, RNA Interference*, Thermotolerance*, Animals, Neurons (* major topic)
Topic: RNA regulation and disease (Molecular Biology, Biochemistry, Genetics and Molecular Biology), according to OpenAlex
Funding: NIH Office of Research Infrastructure Programs (P40 OD010440); Morris Kahn Foundation; EMBO (ALTF 6-2022); Eric and Wendy Schmidt Fund for Strategic Innovation Polymath Award (0140001000); European Research Council (335624); Israel Science Foundation (979/21); US-Israel Binational Science Foundation (2023036)
Citations: not cited yet (Europe PMC); 115 references in the paper
Research resources: RRID:AB_2307443, Cy™3 AffiniPure® Donkey Anti-Mouse IgG RRID:AB_2315777, Mouse anti-MSP RRID:AB_760348

Abstract

Thermal pollution, whether local or driven by global warming, threatens biodiversity in part through its detrimental effects on reproduction. Non-coding small RNAs (sRNAs) are crucial for maintaining germline developmental robustness under heat stress. Remarkably, we uncovered that neuronal sRNAs regulate germ cells’ thermotolerance, affecting both spermatogenic and oogenic germlines in a cell-non-autonomous manner. Furthermore, we demonstrate that, in RNAi mutants, an oxygen-sensing neural circuit, modulated by neuropeptide signaling, antagonizes germline maintenance, likely reflecting the nematode’s innate association of reduced oxygen levels with food availability and reproductive permissive environments. Finally, we provide evidence that laboratory-domesticated alleles of oxygen-response genes encoding neuropeptide receptor NPR-1 and hexacoordinated globin GLB-5 compromise germline thermotolerance. Hence, our findings raise the possibility that sensory perception, independent of direct environmental change, modulates germline integrity, highlighting a novel mechanism by which neural circuits integrate environmental information to safeguard reproductive fitness in fluctuating environments.

Reproduced under the paper's license (CC BY), from the paper cited above.

Repositories

Its files are read in the Code ↔ Paper reader above, with 4 matches between paragraphs and lines of code.

GuyTeichman/RNAlysis

License: MIT
State: the link answers, verified on 27 September 2026
Evidence: files inventoried
Commit: f1349da74239ee769918a07e20dded9af26ea1e2, 15 September 2026
Languages: Python (82), R (33), JavaScript (18)
Size: 2,535 files, 133 scripts
Software Heritage: archived
Found in: the text, “Key resources table”
Holds: README, license file, environment (requirements.txt, requirements_dev.txt, requirements_extra.txt, requirements_pyinstaller.txt, setup.py), tests, continuous integration, documentation
Not found: CITATION.cff
Tools: NumPy (18 files), Matplotlib (16 files), DESeq2 (9 files), limma (8 files), Subread (featureCounts) (7 files), SciPy (5 files), scikit-learn (3 files), statsmodels (2 files), NetworkX (1 file), Numba (1 file), pandas (1 file)
Availability: 1 check, the latest on 27 September 2026: the link answers
  • 27 September 2026: the link answers
135 files

wormtracker/zentracker

License: none: the authors keep all their rights
State: the link answers, verified on 27 September 2026
Evidence: files inventoried
Commit: 79eac14dfa0ddb48c019b60587861273d75ba823, 5 November 2015
Languages: MATLAB (3)
Size: 11 files, 3 scripts
Software Heritage: archived
Found in: the text, “Locomotion assays”
Holds: README
Not found: license file, CITATION.cff, environment file, tests, continuous integration, documentation
Availability: 1 check, the latest on 27 September 2026: the link answers
  • 27 September 2026: the link answers
4 files

The paper's code and data availability statement is in the Data section.

Tracing map

Proposed by the machine: these links were found in the paper and verified at the source, without human review. The map will receive a Zenodo DOI once one of the paper's authors has validated it with their ORCID.

What the map holds:

  • 2 repositories of the authors' code, each at its verified commit, with its license and how the link was found in the paper;
  • 136 scripts, each with its path and the digest of its content;
  • 4 matches between paragraphs of the paper and lines of the code (method lexical-v1);
  • neither the text of the paper nor the code itself.

Its JSON (tracing-map.json) is deposited on Zenodo with its DOI once the map is validated.

Data

No dataset and no data link were found in the paper.

Data and code availability

• All NGS data are available through GEO under accession number GSE331410. • This paper does not report original code. • Any additional information required to reanalyze the data reported in this paper is available from the lead contact upon request.

Reproduced under the paper's license (CC BY), from the paper cited above.

Versions

The history of this record: each version stored by the harvester or made by a correction of its authors or of the maintainers of its code, and what changed in its facts. The texts of the paper (its abstract, its availability statements) are not part of it; versions that changed only those are not listed.

Version 2, 28 September 2026

  • Publisher: n/a → Elsevier BV

Version 1, 27 September 2026: the first record

Recorded: type, language, journal, volume, issue, pages, dates, 16 authors, 4 keywords, 8 MeSH terms, 7 funders, 115 references, 3 RRIDs.

Cite

This paper

Ewe, C. K., Achache, H., Schön, H., Kontorovich, L., Teichman, G., Weiss, S., Mogilevskaya, A., Valenski, M., Anava, S., Bardapurkar, R., Gingold, H., Posner, R., Antonova, O., de Bono, M., Tzur, Y. B., & Rechavi, O. (2026). Neuronal RNAi and oxygen-sensing circuit shape germline resilience to heat stress. Current biology : CB, 36(14), 3566-3579.e5. https://doi.org/10.1016/j.cub.2026.06.016

BibTeX

@article{ewe2026neuronal,
author = {Ewe, Chee Kiang and Achache, Hanna and Schön, Hanna and Kontorovich, Leonid and Teichman, Guy and Weiss, Shir and Mogilevskaya, Anna and Valenski, Myriam and Anava, Sarit and Bardapurkar, Rutwik and Gingold, Hila and Posner, Rachel and Antonova, Olga and de Bono, Mario and Tzur, Yonatan B. and Rechavi, Oded},
title = {{Neuronal RNAi and oxygen-sensing circuit shape germline resilience to heat stress}},
journal = {Current biology : CB},
year = {2026},
month = jul,
volume = {36},
number = {14},
pages = {3566--3579.e5},
publisher = {Elsevier BV},
issn = {0960-9822},
doi = {10.1016/j.cub.2026.06.016},
url = {https://doi.org/10.1016/j.cub.2026.06.016},
pmid = {42409014},
pmcid = {PMC13399994}
}

RIS

TY - JOUR
AU - Ewe, Chee Kiang
AU - Achache, Hanna
AU - Schön, Hanna
AU - Kontorovich, Leonid
AU - Teichman, Guy
AU - Weiss, Shir
AU - Mogilevskaya, Anna
AU - Valenski, Myriam
AU - Anava, Sarit
AU - Bardapurkar, Rutwik
AU - Gingold, Hila
AU - Posner, Rachel
AU - Antonova, Olga
AU - de Bono, Mario
AU - Tzur, Yonatan B.
AU - Rechavi, Oded
TI - Neuronal RNAi and oxygen-sensing circuit shape germline resilience to heat stress
T2 - Current biology : CB
J2 - Curr Biol
PY - 2026
DA - 2026/07/06
VL - 36
IS - 14
SP - 3566
EP - 3579.e5
SN - 0960-9822
PB - Elsevier BV
DO - 10.1016/j.cub.2026.06.016
UR - https://doi.org/10.1016/j.cub.2026.06.016
LA - en
ER -

CSL-JSON

{
"id": "10.1016/j.cub.2026.06.016",
"type": "article-journal",
"title": "Neuronal RNAi and oxygen-sensing circuit shape germline resilience to heat stress",
"container-title": "Current biology : CB",
"author": [
{
"family": "Ewe",
"given": "Chee Kiang"
},
{
"family": "Achache",
"given": "Hanna"
},
{
"family": "Schön",
"given": "Hanna"
},
{
"family": "Kontorovich",
"given": "Leonid"
},
{
"family": "Teichman",
"given": "Guy"
},
{
"family": "Weiss",
"given": "Shir"
},
{
"family": "Mogilevskaya",
"given": "Anna"
},
{
"family": "Valenski",
"given": "Myriam"
},
{
"family": "Anava",
"given": "Sarit"
},
{
"family": "Bardapurkar",
"given": "Rutwik"
},
{
"family": "Gingold",
"given": "Hila"
},
{
"family": "Posner",
"given": "Rachel"
},
{
"family": "Antonova",
"given": "Olga"
},
{
"family": "de Bono",
"given": "Mario"
},
{
"family": "Tzur",
"given": "Yonatan B."
},
{
"family": "Rechavi",
"given": "Oded"
}
],
"container-title-short": "Curr Biol",
"volume": "36",
"issue": "14",
"page": "3566-3579.e5",
"DOI": "10.1016/j.cub.2026.06.016",
"PMID": "42409014",
"PMCID": "PMC13399994",
"ISSN": "0960-9822",
"publisher": "Elsevier BV",
"URL": "https://doi.org/10.1016/j.cub.2026.06.016",
"language": "en",
"issued": {
"date-parts": [
[
2026,
7,
6
]
]
}
}

The tracing map gets a citation of its own once an author has validated it and it has a DOI.

Similar papers

The papers with a page that share the most with this one: the tools found in their code, their categories, datasets, cited references and authors, the rarest counting most.

[1] doi:10.1038/s42003-026-10957-8 [code]
Brain defence by the extracellular matrix protein Cochlin.
Journal: Communications biology
In common: Subread (featureCounts), limma, DESeq2, 9 other tools
[2] doi:10.1038/s41586-026-10348-3 [code]
An enteric neuron ionotropic receptor regulates salt stress resistance.
Journal: Nature
In common: Numba, statsmodels, scikit-learn, 4 other tools, C. elegans, 4 references
[3] doi:10.1093/bioinformatics/btag592 [code]
Network-based stratification of allele-specific expression reveals patient subgroups in Huntington's disease.
Journal: Bioinformatics (Oxford, England)
In common: Subread (featureCounts), limma, DESeq2, 6 other tools, 2 references
[4] doi:10.1038/s41586-026-10629-x [code]
Whole-genome duplication shaped cell-type evolution in the vertebrate brain.
Journal: Nature
In common: DESeq2, Numba, NetworkX, 6 other tools, 2 references
[5] doi:10.1016/j.xcrm.2026.102766 [code]
A longitudinal single-cell and spatial multiomic atlas of pediatric high-grade glioma.
Journal: Cell reports. Medicine
In common: limma, DESeq2, Numba, 6 other tools, 1 reference
[6] doi:10.1038/s41593-026-02232-0 [code]
Entorhinal cortex represents task-relevant remote locations independently of CA1.
Journal: Nature neuroscience
In common: Numba, NetworkX, Image Processing Toolbox, 7 other tools
[7] doi:10.1126/sciadv.aed2952 [code]
Activation of transposable elements is linked to a region- and cell type-specific interferon response in Parkinson's disease.
Journal: Science advances
In common: Subread (featureCounts), limma, DESeq2, 4 other tools, 2 references
[8] doi:10.1016/j.isci.2026.117375 [code]
Motor priming is associated with widespread recruitment into neural ensembles and more rapid ensemble transitions.
Journal: iScience
In common: NetworkX, Image Processing Toolbox, statsmodels, 6 other tools, 1 reference
[9] doi:10.1038/s41467-026-76675-1 [code]
Long-read proteogenomic atlas of human neuronal differentiation reveals isoform diversity informing neurodevelopmental risk mechanisms.
Journal: Nature communications
In common: limma, DESeq2, NetworkX, 5 other tools, 1 reference
[10] doi:10.1038/s41467-026-72598-z [code]
Functional impact of genetic background on variable expressivity in neurodevelopmental disorders.
Journal: Nature communications
In common: DESeq2, NetworkX, statsmodels, 4 other tools, 2 references

Contribute

The authors of this paper can claim it, correct its record and validate its tracing map, and the maintainers of its code (its owner, or a public member of its organization) correct what it says of their repository; anyone signed in can ask for its removal. Every request goes to OSCR's own machine, which answers it; your account page follows them.

Sign in with ORCID to claim this paper as one of its authors, correct its record or validate its tracing map: when the paper's metadata lists your ORCID iD, you are recognized at once. Maintainers of its code: sign in with GitHub, then claim the repository on your account page.

Request its removal

To ask OSCR to remove this record, the copies of its authors' scripts or its tracing map, use the removal request page: signed in, you say who you are, what to remove and why, then review and confirm the request. Published rules decide every request (how).

Discussion, reproductions, activity

Discussion: questions and error reports about this paper and its code, from signed-in readers and its authors. It opens with sign-in.

Reproductions: reports from readers who ran the authors' code: what they reproduced, with which environment, commit and data. It opens with sign-in.

Activity: what happens around this paper: new versions of its record, its map's validation, discussions and reproductions. It opens with sign-in.