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))