Mercurial > repos > bimib > cobraxy
comparison COBRAxy/marea.py @ 456:a6e45049c1b9 draft
Uploaded
| author | francesco_lapi |
|---|---|
| date | Fri, 12 Sep 2025 17:28:45 +0000 |
| parents | f62b8625f6f1 |
| children | df90f40a156c |
comparison
equal
deleted
inserted
replaced
| 455:4e2bc80764b6 | 456:a6e45049c1b9 |
|---|---|
| 1 """ | |
| 2 MAREA: Enrichment and map styling for RAS/RPS data. | |
| 3 | |
| 4 This module compares groups of samples using RAS (Reaction Activity Scores) and/or | |
| 5 RPS (Reaction Propensity Scores), computes statistics (p-values, z-scores, fold change), | |
| 6 and applies visual styling to an SVG metabolic map (with optional PDF/PNG export). | |
| 7 """ | |
| 1 from __future__ import division | 8 from __future__ import division |
| 2 import csv | 9 import csv |
| 3 from enum import Enum | 10 from enum import Enum |
| 4 import re | 11 import re |
| 5 import sys | 12 import sys |
| 24 ERRORS = [] | 31 ERRORS = [] |
| 25 ########################## argparse ########################################## | 32 ########################## argparse ########################################## |
| 26 ARGS :argparse.Namespace | 33 ARGS :argparse.Namespace |
| 27 def process_args(args:List[str] = None) -> argparse.Namespace: | 34 def process_args(args:List[str] = None) -> argparse.Namespace: |
| 28 """ | 35 """ |
| 29 Interfaces the script of a module with its frontend, making the user's choices for various parameters available as values in code. | 36 Parse command-line arguments exposed by the Galaxy frontend for this module. |
| 30 | 37 |
| 31 Args: | 38 Args: |
| 32 args : Always obtained (in file) from sys.argv | 39 args: Optional list of arguments, defaults to sys.argv when None. |
| 33 | 40 |
| 34 Returns: | 41 Returns: |
| 35 Namespace : An object containing the parsed arguments | 42 Namespace: Parsed arguments. |
| 36 """ | 43 """ |
| 37 parser = argparse.ArgumentParser( | 44 parser = argparse.ArgumentParser( |
| 38 usage = "%(prog)s [options]", | 45 usage = "%(prog)s [options]", |
| 39 description = "process some value's genes to create a comparison's map.") | 46 description = "process some value's genes to create a comparison's map.") |
| 40 | 47 |
| 263 tmp.append('stroke-width:' + width) | 270 tmp.append('stroke-width:' + width) |
| 264 if not flag_dash: | 271 if not flag_dash: |
| 265 tmp.append('stroke-dasharray:' + dash) | 272 tmp.append('stroke-dasharray:' + dash) |
| 266 return ';'.join(tmp) | 273 return ';'.join(tmp) |
| 267 | 274 |
| 268 # TODO: remove, there's applyRPS whatever | |
| 269 # The type of d values is collapsed, losing precision, because the dict containst lists instead of tuples, please fix! | |
| 270 def fix_map(d :Dict[str, List[Union[float, FoldChange]]], core_map :ET.ElementTree, threshold_P_V :float, threshold_F_C :float, max_z_score :float) -> ET.ElementTree: | 275 def fix_map(d :Dict[str, List[Union[float, FoldChange]]], core_map :ET.ElementTree, threshold_P_V :float, threshold_F_C :float, max_z_score :float) -> ET.ElementTree: |
| 271 """ | 276 """ |
| 272 Edits the selected SVG map based on the p-value and fold change data (d) and some significance thresholds also passed as inputs. | 277 Edits the selected SVG map based on the p-value and fold change data (d) and some significance thresholds also passed as inputs. |
| 273 | 278 |
| 274 Args: | 279 Args: |
| 341 """ | 346 """ |
| 342 return utils.Result.Ok( | 347 return utils.Result.Ok( |
| 343 f"//*[@id=\"{reactionId}\"]").map( | 348 f"//*[@id=\"{reactionId}\"]").map( |
| 344 lambda xPath : metabMap.xpath(xPath)[0]).mapErr( | 349 lambda xPath : metabMap.xpath(xPath)[0]).mapErr( |
| 345 lambda _ : utils.Result.ResultErr(f"No elements with ID \"{reactionId}\" found in map")) | 350 lambda _ : utils.Result.ResultErr(f"No elements with ID \"{reactionId}\" found in map")) |
| 346 # ^^^ we shamelessly ignore the contents of the IndexError, it offers nothing to the user. | |
| 347 | 351 |
| 348 def styleMapElement(element :ET.Element, styleStr :str) -> None: | 352 def styleMapElement(element :ET.Element, styleStr :str) -> None: |
| 353 """Append/override stroke-related styles on a given SVG element.""" | |
| 349 currentStyles :str = element.get("style", "") | 354 currentStyles :str = element.get("style", "") |
| 350 if re.search(r";stroke:[^;]+;stroke-width:[^;]+;stroke-dasharray:[^;]+$", currentStyles): | 355 if re.search(r";stroke:[^;]+;stroke-width:[^;]+;stroke-dasharray:[^;]+$", currentStyles): |
| 351 currentStyles = ';'.join(currentStyles.split(';')[:-3]) # TODO: why the last 3? Are we sure? | 356 currentStyles = ';'.join(currentStyles.split(';')[:-3]) |
| 352 | |
| 353 #TODO: this is attempting to solve the styling override problem, not sure it does tho | |
| 354 | 357 |
| 355 element.set("style", currentStyles + styleStr) | 358 element.set("style", currentStyles + styleStr) |
| 356 | 359 |
| 357 # TODO: maybe remove vvv | |
| 358 class ReactionDirection(Enum): | 360 class ReactionDirection(Enum): |
| 359 Unknown = "" | 361 Unknown = "" |
| 360 Direct = "_F" | 362 Direct = "_F" |
| 361 Inverse = "_B" | 363 Inverse = "_B" |
| 362 | 364 |
| 370 @classmethod | 372 @classmethod |
| 371 def fromReactionId(cls, reactionId :str) -> "ReactionDirection": | 373 def fromReactionId(cls, reactionId :str) -> "ReactionDirection": |
| 372 return ReactionDirection.fromDir(reactionId[-2:]) | 374 return ReactionDirection.fromDir(reactionId[-2:]) |
| 373 | 375 |
| 374 def getArrowBodyElementId(reactionId :str) -> str: | 376 def getArrowBodyElementId(reactionId :str) -> str: |
| 377 """Return the SVG element id for a reaction arrow body, normalizing direction tags.""" | |
| 375 if reactionId.endswith("_RV"): reactionId = reactionId[:-3] #TODO: standardize _RV | 378 if reactionId.endswith("_RV"): reactionId = reactionId[:-3] #TODO: standardize _RV |
| 376 elif ReactionDirection.fromReactionId(reactionId) is not ReactionDirection.Unknown: reactionId = reactionId[:-2] | 379 elif ReactionDirection.fromReactionId(reactionId) is not ReactionDirection.Unknown: reactionId = reactionId[:-2] |
| 377 return f"R_{reactionId}" | 380 return f"R_{reactionId}" |
| 378 | 381 |
| 379 def getArrowHeadElementId(reactionId :str) -> Tuple[str, str]: | 382 def getArrowHeadElementId(reactionId :str) -> Tuple[str, str]: |
| 399 Invalid = "#BEBEBE" # gray, fold-change under treshold or not significant p-value | 402 Invalid = "#BEBEBE" # gray, fold-change under treshold or not significant p-value |
| 400 Transparent = "#ffffff00" # transparent, to make some arrow segments disappear | 403 Transparent = "#ffffff00" # transparent, to make some arrow segments disappear |
| 401 UpRegulated = "#ecac68" # orange, up-regulated reaction | 404 UpRegulated = "#ecac68" # orange, up-regulated reaction |
| 402 DownRegulated = "#6495ed" # lightblue, down-regulated reaction | 405 DownRegulated = "#6495ed" # lightblue, down-regulated reaction |
| 403 | 406 |
| 404 UpRegulatedInv = "#FF0000" | 407 UpRegulatedInv = "#FF0000" # bright red for reversible with conflicting directions |
| 405 # ^^^ bright red, up-regulated net value for a reversible reaction with | 408 |
| 406 # conflicting enrichment in the two directions. | 409 DownRegulatedInv = "#0000FF" # bright blue for reversible with conflicting directions |
| 407 | |
| 408 DownRegulatedInv = "#0000FF" | |
| 409 # ^^^ bright blue, down-regulated net value for a reversible reaction with | |
| 410 # conflicting enrichment in the two directions. | |
| 411 | 410 |
| 412 @classmethod | 411 @classmethod |
| 413 def fromFoldChangeSign(cls, foldChange :float, *, useAltColor = False) -> "ArrowColor": | 412 def fromFoldChangeSign(cls, foldChange :float, *, useAltColor = False) -> "ArrowColor": |
| 414 colors = (cls.DownRegulated, cls.DownRegulatedInv) if foldChange < 0 else (cls.UpRegulated, cls.UpRegulatedInv) | 413 colors = (cls.DownRegulated, cls.DownRegulatedInv) if foldChange < 0 else (cls.UpRegulated, cls.UpRegulatedInv) |
| 415 return colors[useAltColor] | 414 return colors[useAltColor] |
| 442 def applyTo(self, reactionId :str, metabMap :ET.ElementTree, styleStr :str) -> None: | 441 def applyTo(self, reactionId :str, metabMap :ET.ElementTree, styleStr :str) -> None: |
| 443 if getElementById(reactionId, metabMap).map(lambda el : styleMapElement(el, styleStr)).isErr: | 442 if getElementById(reactionId, metabMap).map(lambda el : styleMapElement(el, styleStr)).isErr: |
| 444 ERRORS.append(reactionId) | 443 ERRORS.append(reactionId) |
| 445 | 444 |
| 446 def styleReactionElements(self, metabMap :ET.ElementTree, reactionId :str, *, mindReactionDir = True) -> None: | 445 def styleReactionElements(self, metabMap :ET.ElementTree, reactionId :str, *, mindReactionDir = True) -> None: |
| 447 # If We're dealing with RAS data or in general don't care about the direction of the reaction we only style the arrow body | 446 # If direction is irrelevant (e.g., RAS), style only the arrow body |
| 448 if not mindReactionDir: | 447 if not mindReactionDir: |
| 449 return self.applyTo(getArrowBodyElementId(reactionId), metabMap, self.toStyleStr()) | 448 return self.applyTo(getArrowBodyElementId(reactionId), metabMap, self.toStyleStr()) |
| 450 | 449 |
| 451 # Now we style the arrow head(s): | 450 # Now we style the arrow head(s): |
| 452 idOpt1, idOpt2 = getArrowHeadElementId(reactionId) | 451 idOpt1, idOpt2 = getArrowHeadElementId(reactionId) |
| 453 self.applyTo(idOpt1, metabMap, self.toStyleStr(downSizedForTips = True)) | 452 self.applyTo(idOpt1, metabMap, self.toStyleStr(downSizedForTips = True)) |
| 454 if idOpt2: self.applyTo(idOpt2, metabMap, self.toStyleStr(downSizedForTips = True)) | 453 if idOpt2: self.applyTo(idOpt2, metabMap, self.toStyleStr(downSizedForTips = True)) |
| 455 | 454 |
| 456 # TODO: this seems to be unused, remove | |
| 457 def getMapReactionId(self, reactionId :str, mindReactionDir :bool) -> str: | |
| 458 """ | |
| 459 Computes the reaction ID as encoded in the map for a given reaction ID from the dataset. | |
| 460 | |
| 461 Args: | |
| 462 reactionId: the reaction ID, as encoded in the dataset. | |
| 463 mindReactionDir: if True forward (F_) and backward (B_) directions will be encoded in the result. | |
| 464 | |
| 465 Returns: | |
| 466 str : the ID of an arrow's body or tips in the map. | |
| 467 """ | |
| 468 # we assume the reactionIds also don't encode reaction dir if they don't mind it when styling the map. | |
| 469 if not mindReactionDir: return "R_" + reactionId | |
| 470 | |
| 471 #TODO: this is clearly something we need to make consistent in RPS | |
| 472 return (reactionId[:-3:-1] + reactionId[:-2]) if reactionId[:-2] in ["_F", "_B"] else f"F_{reactionId}" # "Pyr_F" --> "F_Pyr" | |
| 473 | |
| 474 def toStyleStr(self, *, downSizedForTips = False) -> str: | 455 def toStyleStr(self, *, downSizedForTips = False) -> str: |
| 475 """ | 456 """ |
| 476 Collapses the styles of this Arrow into a str, ready to be applied as part of the "style" property on an svg element. | 457 Collapses the styles of this Arrow into a str, ready to be applied as part of the "style" property on an svg element. |
| 477 | 458 |
| 478 Returns: | 459 Returns: |
| 480 """ | 461 """ |
| 481 width = self.w | 462 width = self.w |
| 482 if downSizedForTips: width *= 0.8 | 463 if downSizedForTips: width *= 0.8 |
| 483 return f";stroke:{self.col};stroke-width:{width};stroke-dasharray:{'5,5' if self.dash else 'none'}" | 464 return f";stroke:{self.col};stroke-width:{width};stroke-dasharray:{'5,5' if self.dash else 'none'}" |
| 484 | 465 |
| 485 # vvv These constants could be inside the class itself a static properties, but python | 466 # Default arrows used for different significance states |
| 486 # was built by brainless organisms so here we are! | |
| 487 INVALID_ARROW = Arrow(Arrow.MIN_W, ArrowColor.Invalid) | 467 INVALID_ARROW = Arrow(Arrow.MIN_W, ArrowColor.Invalid) |
| 488 INSIGNIFICANT_ARROW = Arrow(Arrow.MIN_W, ArrowColor.Invalid, isDashed = True) | 468 INSIGNIFICANT_ARROW = Arrow(Arrow.MIN_W, ArrowColor.Invalid, isDashed = True) |
| 489 TRANSPARENT_ARROW = Arrow(Arrow.MIN_W, ArrowColor.Transparent) # Who cares how big it is if it's transparent | 469 TRANSPARENT_ARROW = Arrow(Arrow.MIN_W, ArrowColor.Transparent) # Who cares how big it is if it's transparent |
| 490 | 470 |
| 491 # TODO: A more general version of this can be used for RAS as well, we don't need "fix map" or whatever | |
| 492 def applyRpsEnrichmentToMap(rpsEnrichmentRes :Dict[str, Union[Tuple[float, FoldChange], Tuple[float, FoldChange, float, float]]], metabMap :ET.ElementTree, maxNumericZScore :float) -> None: | 471 def applyRpsEnrichmentToMap(rpsEnrichmentRes :Dict[str, Union[Tuple[float, FoldChange], Tuple[float, FoldChange, float, float]]], metabMap :ET.ElementTree, maxNumericZScore :float) -> None: |
| 493 """ | 472 """ |
| 494 Applies RPS enrichment results to the provided metabolic map. | 473 Applies RPS enrichment results to the provided metabolic map. |
| 495 | 474 |
| 496 Args: | 475 Args: |
| 579 sample_ids.append(pat_id) | 558 sample_ids.append(pat_id) |
| 580 classes.iloc[j, 1] = None # TODO: problems? | 559 classes.iloc[j, 1] = None # TODO: problems? |
| 581 | 560 |
| 582 if l: | 561 if l: |
| 583 class_pat[classe] = { | 562 class_pat[classe] = { |
| 584 "values": list(map(list, zip(*l))), # trasposta | 563 "values": list(map(list, zip(*l))), # transpose |
| 585 "samples": sample_ids | 564 "samples": sample_ids |
| 586 } | 565 } |
| 587 continue | 566 continue |
| 588 | 567 |
| 589 utils.logWarning( | 568 utils.logWarning( |
| 590 f"Warning: no sample found in class \"{classe}\", the class has been disregarded", ARGS.out_log) | 569 f"Warning: no sample found in class \"{classe}\", the class has been disregarded", ARGS.out_log) |
| 591 | 570 |
| 592 return class_pat | 571 return class_pat |
| 593 | 572 |
| 594 ############################ conversion ############################################## | 573 ############################ conversion ############################################## |
| 595 #conversion from svg to png | 574 # Conversion from SVG to PNG |
| 596 def svg_to_png_with_background(svg_path :utils.FilePath, png_path :utils.FilePath, dpi :int = 72, scale :int = 1, size :Optional[float] = None) -> None: | 575 def svg_to_png_with_background(svg_path :utils.FilePath, png_path :utils.FilePath, dpi :int = 72, scale :int = 1, size :Optional[float] = None) -> None: |
| 597 """ | 576 """ |
| 598 Internal utility to convert an SVG to PNG (forced opaque) to aid in PDF conversion. | 577 Internal utility to convert an SVG to PNG (forced opaque) to aid in PDF conversion. |
| 599 | 578 |
| 600 Args: | 579 Args: |
| 658 ext : _description_ | 637 ext : _description_ |
| 659 | 638 |
| 660 Returns: | 639 Returns: |
| 661 utils.FilePath : _description_ | 640 utils.FilePath : _description_ |
| 662 """ | 641 """ |
| 663 # This function returns a util data structure but is extremely specific to this module. | |
| 664 # RAS also uses collections as output and as such might benefit from a method like this, but I'd wait | |
| 665 # TODO: until a third tool with multiple outputs appears before porting this to utils. | |
| 666 return utils.FilePath( | 642 return utils.FilePath( |
| 667 f"{dataset1Name}_vs_{dataset2Name}" + (f" ({details})" if details else ""), | 643 f"{dataset1Name}_vs_{dataset2Name}" + (f" ({details})" if details else ""), |
| 668 # ^^^ yes this string is built every time even if the form is the same for the same 2 datasets in | |
| 669 # all output files: I don't care, this was never the performance bottleneck of the tool and | |
| 670 # there is no other net gain in saving and re-using the built string. | |
| 671 ext, | 644 ext, |
| 672 prefix = ARGS.output_path) | 645 prefix = ARGS.output_path) |
| 673 | 646 |
| 674 FIELD_NOT_AVAILABLE = '/' | 647 FIELD_NOT_AVAILABLE = '/' |
| 675 def writeToCsv(rows: List[list], fieldNames :List[str], outPath :utils.FilePath) -> None: | 648 def writeToCsv(rows: List[list], fieldNames :List[str], outPath :utils.FilePath) -> None: |
| 681 for row in rows: | 654 for row in rows: |
| 682 sizeMismatch = fieldsAmt - len(row) | 655 sizeMismatch = fieldsAmt - len(row) |
| 683 if sizeMismatch > 0: row.extend([FIELD_NOT_AVAILABLE] * sizeMismatch) | 656 if sizeMismatch > 0: row.extend([FIELD_NOT_AVAILABLE] * sizeMismatch) |
| 684 writer.writerow({ field : data for field, data in zip(fieldNames, row) }) | 657 writer.writerow({ field : data for field, data in zip(fieldNames, row) }) |
| 685 | 658 |
| 686 OldEnrichedScores = Dict[str, List[Union[float, FoldChange]]] #TODO: try to use Tuple whenever possible | 659 OldEnrichedScores = Dict[str, List[Union[float, FoldChange]]] |
| 687 def temp_thingsInCommon(tmp :OldEnrichedScores, core_map :ET.ElementTree, max_z_score :float, dataset1Name :str, dataset2Name = "rest", ras_enrichment = True) -> None: | 660 def temp_thingsInCommon(tmp :OldEnrichedScores, core_map :ET.ElementTree, max_z_score :float, dataset1Name :str, dataset2Name = "rest", ras_enrichment = True) -> None: |
| 688 # this function compiles the things always in common between comparison modes after enrichment. | |
| 689 # TODO: organize, name better. | |
| 690 suffix = "RAS" if ras_enrichment else "RPS" | 661 suffix = "RAS" if ras_enrichment else "RPS" |
| 691 writeToCsv( | 662 writeToCsv( |
| 692 [ [reactId] + values for reactId, values in tmp.items() ], | 663 [ [reactId] + values for reactId, values in tmp.items() ], |
| 693 ["ids", "P_Value", "fold change", "z-score", "average_1", "average_2"], | 664 ["ids", "P_Value", "fold change", "z-score", "average_1", "average_2"], |
| 694 buildOutputPath(dataset1Name, dataset2Name, details = f"Tabular Result ({suffix})", ext = utils.FileFormat.TSV)) | 665 buildOutputPath(dataset1Name, dataset2Name, details = f"Tabular Result ({suffix})", ext = utils.FileFormat.TSV)) |
| 807 | 778 |
| 808 | 779 |
| 809 # TODO: the net RPS computation should be done in the RPS module | 780 # TODO: the net RPS computation should be done in the RPS module |
| 810 def compareDatasetPair(dataset1Data :List[List[float]], dataset2Data :List[List[float]], ids :List[str]) -> Tuple[Dict[str, List[Union[float, FoldChange]]], float, Dict[str, Tuple[np.ndarray, np.ndarray]]]: | 781 def compareDatasetPair(dataset1Data :List[List[float]], dataset2Data :List[List[float]], ids :List[str]) -> Tuple[Dict[str, List[Union[float, FoldChange]]], float, Dict[str, Tuple[np.ndarray, np.ndarray]]]: |
| 811 | 782 |
| 812 #TODO: the following code still suffers from "dumbvarnames-osis" | |
| 813 netRPS :Dict[str, Tuple[np.ndarray, np.ndarray]] = {} | 783 netRPS :Dict[str, Tuple[np.ndarray, np.ndarray]] = {} |
| 814 comparisonResult :Dict[str, List[Union[float, FoldChange]]] = {} | 784 comparisonResult :Dict[str, List[Union[float, FoldChange]]] = {} |
| 815 count = 0 | 785 count = 0 |
| 816 max_z_score = 0 | 786 max_z_score = 0 |
| 817 | 787 |
| 818 for l1, l2 in zip(dataset1Data, dataset2Data): | 788 for l1, l2 in zip(dataset1Data, dataset2Data): |
| 819 reactId = ids[count] | 789 reactId = ids[count] |
| 820 count += 1 | 790 count += 1 |
| 821 if not reactId: continue # we skip ids that have already been processed | 791 if not reactId: continue |
| 822 | 792 |
| 823 try: #TODO: identify the source of these errors and minimize code in the try block | 793 try: #TODO: identify the source of these errors and minimize code in the try block |
| 824 reactDir = ReactionDirection.fromReactionId(reactId) | 794 reactDir = ReactionDirection.fromReactionId(reactId) |
| 825 # Net score is computed only for reversible reactions when user wants it on arrow tips or when RAS datasets aren't used | 795 # Net score is computed only for reversible reactions when user wants it on arrow tips or when RAS datasets aren't used |
| 826 if (ARGS.net or not ARGS.using_RAS) and reactDir is not ReactionDirection.Unknown: | 796 if (ARGS.net or not ARGS.using_RAS) and reactDir is not ReactionDirection.Unknown: |
| 945 print(f'PDF file {pdfPath.filePath} successfully generated.') | 915 print(f'PDF file {pdfPath.filePath} successfully generated.') |
| 946 | 916 |
| 947 except Exception as e: | 917 except Exception as e: |
| 948 raise utils.DataErr(pdfPath.show(), f'Error generating PDF file: {e}') | 918 raise utils.DataErr(pdfPath.show(), f'Error generating PDF file: {e}') |
| 949 | 919 |
| 950 if not ARGS.generate_svg: # This argument is useless, who cares if the user wants the svg or not | 920 if not ARGS.generate_svg: |
| 951 os.remove(svgFilePath.show()) | 921 os.remove(svgFilePath.show()) |
| 952 | 922 |
| 953 ClassPat = Dict[str, List[List[float]]] | 923 ClassPat = Dict[str, List[List[float]]] |
| 954 def getClassesAndIdsFromDatasets(datasetsPaths :List[str], datasetPath :str, classPath :str, names :List[str]) -> Tuple[List[str], ClassPat, Dict[str, List[str]]]: | 924 def getClassesAndIdsFromDatasets(datasetsPaths :List[str], datasetPath :str, classPath :str, names :List[str]) -> Tuple[List[str], ClassPat, Dict[str, List[str]]]: |
| 955 # TODO: I suggest creating dicts with ids as keys instead of keeping class_pat and ids separate, | |
| 956 # for the sake of everyone's sanity. | |
| 957 columnNames :Dict[str, List[str]] = {} # { datasetName : [ columnName, ... ], ... } | 925 columnNames :Dict[str, List[str]] = {} # { datasetName : [ columnName, ... ], ... } |
| 958 class_pat :ClassPat = {} | 926 class_pat :ClassPat = {} |
| 959 if ARGS.option == 'datasets': | 927 if ARGS.option == 'datasets': |
| 960 num = 1 | 928 num = 1 |
| 961 for path, name in zip(datasetsPaths, names): | 929 for path, name in zip(datasetsPaths, names): |
| 981 for clas, values_and_samples_id in class_pat_with_samples_id.items(): | 949 for clas, values_and_samples_id in class_pat_with_samples_id.items(): |
| 982 class_pat[clas] = values_and_samples_id["values"] | 950 class_pat[clas] = values_and_samples_id["values"] |
| 983 columnNames[clas] = ["Reactions", *values_and_samples_id["samples"]] | 951 columnNames[clas] = ["Reactions", *values_and_samples_id["samples"]] |
| 984 | 952 |
| 985 return ids, class_pat, columnNames | 953 return ids, class_pat, columnNames |
| 986 #^^^ TODO: this could be a match statement over an enum, make it happen future marea dev with python 3.12! (it's why I kept the ifs) | 954 |
| 987 | |
| 988 #TODO: create these damn args as FilePath objects | |
| 989 def getDatasetValues(datasetPath :str, datasetName :str) -> Tuple[ClassPat, List[str]]: | 955 def getDatasetValues(datasetPath :str, datasetName :str) -> Tuple[ClassPat, List[str]]: |
| 990 """ | 956 """ |
| 991 Opens the dataset at the given path and extracts the values (expected nullable numerics) and the IDs. | 957 Opens the dataset at the given path and extracts the values (expected nullable numerics) and the IDs. |
| 992 | 958 |
| 993 Args: | 959 Args: |
| 1023 | 989 |
| 1024 core_map: ET.ElementTree = ARGS.choice_map.getMap( | 990 core_map: ET.ElementTree = ARGS.choice_map.getMap( |
| 1025 ARGS.tool_dir, | 991 ARGS.tool_dir, |
| 1026 utils.FilePath.fromStrPath(ARGS.custom_map) if ARGS.custom_map else None) | 992 utils.FilePath.fromStrPath(ARGS.custom_map) if ARGS.custom_map else None) |
| 1027 | 993 |
| 1028 # TODO: in the future keep the indices WITH the data and fix the code below. | |
| 1029 | |
| 1030 # Prepare enrichment results containers | 994 # Prepare enrichment results containers |
| 1031 ras_results = [] | 995 ras_results = [] |
| 1032 rps_results = [] | 996 rps_results = [] |
| 1033 | 997 |
| 1034 # Compute RAS enrichment if requested | 998 # Compute RAS enrichment if requested |
| 1035 if ARGS.using_RAS: # vvv columnNames only matter with RPS data | 999 if ARGS.using_RAS: |
| 1036 ids_ras, class_pat_ras, _ = getClassesAndIdsFromDatasets( | 1000 ids_ras, class_pat_ras, _ = getClassesAndIdsFromDatasets( |
| 1037 ARGS.input_datas, ARGS.input_data, ARGS.input_class, ARGS.names) | 1001 ARGS.input_datas, ARGS.input_data, ARGS.input_class, ARGS.names) |
| 1038 ras_results, _ = computeEnrichment(class_pat_ras, ids_ras, fromRAS=True) | 1002 ras_results, _ = computeEnrichment(class_pat_ras, ids_ras, fromRAS=True) |
| 1039 # ^^^ netRPS only matter with RPS data | 1003 |
| 1040 | 1004 |
| 1041 # Compute RPS enrichment if requested | 1005 # Compute RPS enrichment if requested |
| 1042 if ARGS.using_RPS: | 1006 if ARGS.using_RPS: |
| 1043 ids_rps, class_pat_rps, columnNames = getClassesAndIdsFromDatasets( | 1007 ids_rps, class_pat_rps, columnNames = getClassesAndIdsFromDatasets( |
| 1044 ARGS.input_datas_rps, ARGS.input_data_rps, ARGS.input_class_rps, ARGS.names_rps) | 1008 ARGS.input_datas_rps, ARGS.input_data_rps, ARGS.input_class_rps, ARGS.names_rps) |
| 1063 temp_thingsInCommon(tmp_ras, map_copy, max_z_ras, i, j, ras_enrichment=True) | 1027 temp_thingsInCommon(tmp_ras, map_copy, max_z_ras, i, j, ras_enrichment=True) |
| 1064 | 1028 |
| 1065 # Apply RPS styling to arrow heads | 1029 # Apply RPS styling to arrow heads |
| 1066 if res.get('rps'): | 1030 if res.get('rps'): |
| 1067 tmp_rps, max_z_rps = res['rps'] | 1031 tmp_rps, max_z_rps = res['rps'] |
| 1068 # applyRpsEnrichmentToMap styles only heads unless only RPS are used | 1032 |
| 1069 temp_thingsInCommon(tmp_rps, map_copy, max_z_rps, i, j, ras_enrichment=False) | 1033 temp_thingsInCommon(tmp_rps, map_copy, max_z_rps, i, j, ras_enrichment=False) |
| 1070 | 1034 |
| 1071 # Output both SVG and PDF/PNG as configured | 1035 # Output both SVG and PDF/PNG as configured |
| 1072 createOutputMaps(i, j, map_copy) | 1036 createOutputMaps(i, j, map_copy) |
| 1073 | 1037 |
| 1074 # Add net RPS output file | 1038 # Add net RPS output file |
| 1075 if ARGS.net or not ARGS.using_RAS: | 1039 if ARGS.net or not ARGS.using_RAS: |
| 1076 for datasetName, rows in netRPS.items(): | 1040 for datasetName, rows in netRPS.items(): |
| 1077 writeToCsv( | 1041 writeToCsv( |
| 1078 [[reactId, *netValues] for reactId, netValues in rows.items()], | 1042 [[reactId, *netValues] for reactId, netValues in rows.items()], |
| 1079 # vvv In weird comparison modes the dataset names are not recorded properly.. | |
| 1080 columnNames.get(datasetName, ["Reactions"]), | 1043 columnNames.get(datasetName, ["Reactions"]), |
| 1081 utils.FilePath( | 1044 utils.FilePath( |
| 1082 "Net_RPS_" + datasetName, | 1045 "Net_RPS_" + datasetName, |
| 1083 ext = utils.FileFormat.CSV, | 1046 ext = utils.FileFormat.CSV, |
| 1084 prefix = ARGS.output_path)) | 1047 prefix = ARGS.output_path)) |
