From 59737f504ce00a18846e4ac909f211c57e4ef53a Mon Sep 17 00:00:00 2001 From: Matteo Arnaudi Date: Wed, 24 Jun 2026 14:50:53 +0200 Subject: [PATCH] implement a pipeline to conevrt for protein of interet information about reactions contained in Reactome datatbase in a mavisp accessible format --- README.md | 2 + tools/reactome_to_mavisp/.DS_Store | Bin 0 -> 10244 bytes tools/reactome_to_mavisp/README.md | 477 +++++++++++++ tools/reactome_to_mavisp/example/readme.txt | 1 + tools/reactome_to_mavisp/example/run.sh | 2 + .../example/uniprot_list.txt | 2 + .../reactome_pipeline/__init__.py | 0 .../__pycache__/__init__.cpython-38.pyc | Bin 0 -> 154 bytes .../__pycache__/__init__.cpython-39.pyc | Bin 0 -> 177 bytes .../data_processing.cpython-38.pyc | Bin 0 -> 8270 bytes .../data_processing.cpython-39.pyc | Bin 0 -> 8285 bytes .../__pycache__/graph_utils.cpython-38.pyc | Bin 0 -> 2901 bytes .../__pycache__/graph_utils.cpython-39.pyc | Bin 0 -> 2914 bytes .../reactome_analysis.cpython-38.pyc | Bin 0 -> 16020 bytes .../reactome_analysis.cpython-39.pyc | Bin 0 -> 15462 bytes .../__pycache__/uniprot_utils.cpython-38.pyc | Bin 0 -> 2915 bytes .../__pycache__/uniprot_utils.cpython-39.pyc | Bin 0 -> 2932 bytes .../__pycache__/workflow.cpython-38.pyc | Bin 0 -> 15088 bytes .../__pycache__/workflow.cpython-39.pyc | Bin 0 -> 15079 bytes .../reactome_pipeline/data_processing.py | 341 +++++++++ .../reactome_pipeline/graph_utils.py | 95 +++ .../reactome_pipeline/reactome_analysis.py | 659 +++++++++++++++++ .../reactome_pipeline/uniprot_utils.py | 101 +++ .../reactome_pipeline/workflow.py | 660 ++++++++++++++++++ .../reactome_post_process.py | 356 ++++++++++ .../reactome_to_mavisp/reactome_to_mavisp.py | 212 ++++++ 26 files changed, 2908 insertions(+) create mode 100644 tools/reactome_to_mavisp/.DS_Store create mode 100644 tools/reactome_to_mavisp/README.md create mode 100644 tools/reactome_to_mavisp/example/readme.txt create mode 100644 tools/reactome_to_mavisp/example/run.sh create mode 100644 tools/reactome_to_mavisp/example/uniprot_list.txt create mode 100644 tools/reactome_to_mavisp/reactome_pipeline/__init__.py create mode 100644 tools/reactome_to_mavisp/reactome_pipeline/__pycache__/__init__.cpython-38.pyc create mode 100644 tools/reactome_to_mavisp/reactome_pipeline/__pycache__/__init__.cpython-39.pyc create mode 100644 tools/reactome_to_mavisp/reactome_pipeline/__pycache__/data_processing.cpython-38.pyc create mode 100644 tools/reactome_to_mavisp/reactome_pipeline/__pycache__/data_processing.cpython-39.pyc create mode 100644 tools/reactome_to_mavisp/reactome_pipeline/__pycache__/graph_utils.cpython-38.pyc create mode 100644 tools/reactome_to_mavisp/reactome_pipeline/__pycache__/graph_utils.cpython-39.pyc create mode 100644 tools/reactome_to_mavisp/reactome_pipeline/__pycache__/reactome_analysis.cpython-38.pyc create mode 100644 tools/reactome_to_mavisp/reactome_pipeline/__pycache__/reactome_analysis.cpython-39.pyc create mode 100644 tools/reactome_to_mavisp/reactome_pipeline/__pycache__/uniprot_utils.cpython-38.pyc create mode 100644 tools/reactome_to_mavisp/reactome_pipeline/__pycache__/uniprot_utils.cpython-39.pyc create mode 100644 tools/reactome_to_mavisp/reactome_pipeline/__pycache__/workflow.cpython-38.pyc create mode 100644 tools/reactome_to_mavisp/reactome_pipeline/__pycache__/workflow.cpython-39.pyc create mode 100644 tools/reactome_to_mavisp/reactome_pipeline/data_processing.py create mode 100644 tools/reactome_to_mavisp/reactome_pipeline/graph_utils.py create mode 100644 tools/reactome_to_mavisp/reactome_pipeline/reactome_analysis.py create mode 100644 tools/reactome_to_mavisp/reactome_pipeline/uniprot_utils.py create mode 100644 tools/reactome_to_mavisp/reactome_pipeline/workflow.py create mode 100644 tools/reactome_to_mavisp/reactome_post_process.py create mode 100644 tools/reactome_to_mavisp/reactome_to_mavisp.py diff --git a/README.md b/README.md index c80926a..5cc5ad0 100644 --- a/README.md +++ b/README.md @@ -29,6 +29,8 @@ This repository provides accessory tools associated with papers and activities o - **ThermoMPNN_trainingdata_overlap**: Contains a python script to find matching proteins in a MAVISp dataset and the training datasets of ThermoMPNN (see paper: https://doi.org/10.1073/pnas.2314853121) with the purpose of defining a benchmarking dataset for the ThermoMPNN tool integrated into MAVISp. The directory also includes scripts for data cleanup steps that do **not** have to be rerun before using the comparison script. - **enzymes_annotation**: This Python script retrieves enzyme annotations starting from a UniProt accession (UniProt AC) by querying the M-CSA (https://www.ebi.ac.uk/thornton-srv/m-csa/) database and, when needed, the UniProt REST API. Its main purpose is to obtain enzyme classification (EC number), catalytic site residues, and information about whether the annotation is manually curated or predicted by homology. Depending on database availability and selected flags, the script can return manually curated catalytic residues from M-CSA, predicted catalytic residues based on homologous alignments, or, if no catalytic site information is available, only the EC classification retrieved from UniProt. The script uses REST API requests to collect data, processes results into structured tables using pandas, and outputs the results as CSV files. - **mavisp_isoform_support**: A collection of tools and utilities that provide the necessary support to run **MAVISp** on **non-canonical isoforms**. The module helps extend standard MAVISp analyses beyond canonical UniProt entries by enabling identifier mapping, isoform-aware annotation, and preparation of the required input data and workflows for isoform-specific variant interpretation. +- **reactome_to_mavisp.py**: This pipeline runs an automated Reactome analysis for one or more UniProt accessions. For each target protein, the workflow retrieves human Reactome pathways, identifies reactions that contain the target, parses BioPAX-level reaction/protein/complex annotations, and writes cleaned CSV outputs in MAVISp supported format for downstream analysis. + ## Citations diff --git a/tools/reactome_to_mavisp/.DS_Store b/tools/reactome_to_mavisp/.DS_Store new file mode 100644 index 0000000000000000000000000000000000000000..313e3bf710befa2226d124fc343340bbb27d1b44 GIT binary patch literal 10244 zcmeHMTWl0n82XeHv>}A+6%pKS!X+AJ2E@7?96s6 zO_RoW3!3<#qWEe|GN~=4;wBGkXYag63Tv1jl0QHO9I#S(Ka+CiSw|HS z0ucfc0ucfc0uciL1qA3fn?3RpA)_)vAVMHQU={&(KE&x|G7-udA^Fik1y2D;7E?8U z=$rBY?h_AXB9t>ia#!k8WDgj+Vwhr}aHsY-Gbfn{<&2QR9ZNsBp8ssK7aR8)N(GBacnJ*%s*i7NY@Du-Pi7OUqPAgYZ}NzfcD>_bAs8M8P&#K}FV0_; zGuG5M>ZLQ+S8?j+0V{R< ziFU!zqKZOfepQG}MO~urA5@2j>`~9{vyWLxZMjdOxrfxAtTjO)IXLP$xwPviwX#ve z8O=FHV6{39Gxmo9YcgP8Bfggn+PPs;({jGmYmuEjVReu-xt6n@x-)_8x&D4D>yxZa zG>WiNPA{!kTXXMyjazoKwD0cfxm3DrxmH%Lsv{KRZeTxQTiJoUZ3g4RhCgb#rtOXm zPT0Pka;$#WNLyuCuPB#RuBt56WBQsx(_9x>tCfdxDf?*74xZv;R8{NhL6au9&okL5 zrgm0VcuQ@yt{-46?OBUTQ>pY{~xM(xC>L;UVaOVK5;NFTrbY0^Wwx@IIV}kKuE; z3|HW5_y)d*U*K2x6RyEu2&iB=R^vLX!CI`x?f4+>z;@h?kK$hJ!T}t?gLnuJ;~1vV z!weq9WB4>agU{k~_%gnMC-F^u3(w%YcoyHo^Y{r~#82@v{1Sh}pYUh=4X@(w5=e@) zMyiskrMR?Cs*@U|kY!Tg*dLi)4gZkXalNoGq6=zvU`Wo*!YJ1Hh`Re<4WlbdMc~}C zqN1vH^ZAA-;C$x$KZlEqQhIE)y7pEfHb8-3P0B1DwGv?+ zS8HSn1Z|N}*Q*<33IT1gQ0rp#G6kI`aaC=IH7OKYT9HueVp|khL!oR}cPJEOTCq@9 zt1XJ60as$Jv38jPMY{w3o`N%Q4lck&;_tWc9sB@)5NBmvhLu=Fyk)FikN0B(HsNM$ z#)q*56WB@&?!tZ8jXl_l2Z+IkF^L8~iAT^u7xOqtES|z?;_&nM0v^X#@Krp4uZL`Y z2Tut$&a!jQjqJn^7RJswKY%xO5@`_v5dskc5dskccZ@)h=poMT|2uE}|NlE4C+Z+V zAVT23ivSjP^mVk+{0e#1+^Mp=_Bg$d)5{)aHzOo>p@NyPdVU>G3cHTq7}kYp8*fj9 fbu&V8ck<)zB*kt1KmN~v= 3.8 +``` + +### Python packages + +Required packages: + +```text +reactome2py +pybiopax +pandas +numpy +networkx +requests +``` + +The workflow also uses standard-library modules such as `argparse`, `os`, `shutil`, `re`, `time`, `copy`, `contextlib`, `io`, `collections`, `pathlib`, and `typing`. + +Example installation: + +```bash +pip install reactome2py pybiopax pandas numpy networkx requests +``` + +--- + +## Description + +### Main files + +| File | Role | +|---|---| +| `reactome_to_mavisp.py` | Main command-line entry point. Parses arguments, checks whether a UniProt accession is mapped in Reactome, runs the workflow, and writes `entries_not_in_reactome.csv` when needed. | +| `reactome_pipeline/workflow.py` | Contains the `ReactomeScript` class and orchestrates the full Reactome workflow for one UniProt accession. | +| `reactome_pipeline/reactome_analysis.py` | Contains Reactome/BioPAX helper functions, safe retry logic for Reactome calls, BioPAX parsing, disease-link extraction, and target-reaction checks. | +| `reactome_pipeline/data_processing.py` | Flattens nested reaction/pathway/protein annotations into a cleaned `pandas.DataFrame`, reorders columns, removes duplicates, and propagates complex information. | +| `reactome_pipeline/graph_utils.py` | Builds and processes directed reaction graphs used for optional pathway-ordering analysis. | +| `reactome_pipeline/uniprot_utils.py` | Contains UniProt helper functions for gene-to-accession conversion, accession-to-protein-name retrieval, and parsing protein names. | +| `reactome_post_process.py` | Optional post-processing script that merges individual `result.csv` files into summary CSV tables. | +| `reactome_pipeline/__init__.py` | Marks `reactome_pipeline/` as a Python package. It can remain empty. | + +The scripts are organized as follow: + +```text +project/ +├── reactome_to_mavisp.py +├── reactome_post_process.py +├── reactome_pipeline/ +│ ├── __init__.py +│ ├── workflow.py +│ ├── reactome_analysis.py +│ ├── data_processing.py +│ ├── graph_utils.py +│ └── uniprot_utils.py +└── README.md +``` + +Python cache files such as `__pycache__/` or `*.pyc` are generated automatically and should not be edited or tracked manually. + +For each UniProt accession, the workflow performs the following steps. + +### 1. Initial Reactome mapping check + +Before running the full workflow, `reac_classified.py` first checks whether the input UniProt accession is mapped to at least one human Reactome pathway. + +This check is performed using: + +```python +rc.content.mapping( + id=uniprot_ac, + resource="UniProt", + species="9606", + by="pathways" +) +``` + +This initial step is used only to decide whether the accession should be processed further. + +If Reactome returns no mapped human pathways, the accession is not analysed and is written to: + +```text +entries_not_in_reactome.csv +``` + +with the status: + +```text +not_found_in_reactome +``` + +This prevents the workflow from spending time on accessions for which Reactome has no pathway-level annotation. + +--- + +### 2. Retrieve Reactome pathways + +For accessions that pass the initial mapping check, the workflow retrieves all human Reactome pathways associated with the UniProt accession using: + +```python +rc.content.mapping( + id=uniprot_ac, + resource="UniProt", + species="9606", + by="pathways" +) +``` + +Each retrieved pathway is later expanded into its full Reactome hierarchy using ancestor information. + +This allows the final output to report not only the specific low-level pathway containing a reaction, but also the broader pathway context in which that reaction occurs. + +--- + +### 3. Optional pathway ordering + +Unless `--skip_pathway_order` is used, the workflow attempts to infer the order of reactions within each retrieved pathway. + +For each pathway, the workflow downloads the corresponding BioPAX model and extracts next/previous reaction relationships. These relationships are used to build a directed graph where: + +- nodes represent reactions; +- edges represent next/previous relationships between reactions; +- inferred reaction paths are written to `ordered_paths.csv`. + +The pathway-ordering step is optional because it can be slow for proteins associated with many Reactome pathways. This is due to the need for multiple BioPAX downloads and graph operations. + +When `--skip_pathway_order` is used, the workflow skips this step and still produces the main `result.csv` output. + +--- + +### 4. Resolve target information + +Before filtering reactions, the workflow collects target-level information for the input UniProt accession. This is done in: + +```python +ReactomeScript.resolve_target_information() +``` + +The purpose of this step is to prepare the identifiers that will later be used to decide whether a candidate Reactome reaction actually contains the input protein. + +The workflow collects: + +- the readable name of the target protein, which will be written in `result.csv`; +- optional Reactome stable identifiers for the target protein; +- alternative Reactome forms of the same protein; +- Reactome complexes associated with the UniProt accession; +- the identifiers needed for target-reaction detection. + +The final `target_name` is selected using the following priority: + +1. protein name retrieved from UniProt; +2. Reactome display/name retrieved from `search_fireworks`, if that optional call works; +3. the original UniProt accession as fallback. + +The `search_fireworks` call is used only as an optional extra source of Reactome-specific protein identifiers. These identifiers can help detect reactions containing specific Reactome protein forms of the target. + +However, `search_fireworks` is not required for the workflow to continue. If Reactome returns a server error or no valid entries, the workflow continues using: + +- Reactome complexes associated with the UniProt accession; +- direct UniProt accession matching inside BioPAX reaction models. + +The target information dictionary contains: + +| Field | Meaning | +|---|---| +| `target_name` | Final target name written in `result.csv`. Preferentially retrieved from UniProt, otherwise from Reactome, otherwise set to the input UniProt accession. | +| `target_protein_stId` | Reactome stable IDs corresponding to the target protein, when available from `search_fireworks`. Used as an optional way to recognise reactions containing the target. | +| `target_protein_stId_other` | Alternative Reactome forms of the target protein, retrieved from Reactome when target stable IDs are available. | +| `all_target_protein_stId` | Combined list of direct and alternative Reactome target IDs. Used later during reaction filtering. | +| `target_protein_complexes_names` | Reactome complexes associated with the UniProt accession. Used later to identify reactions where the target appears as part of a complex. | + +--- + +### 5. Collect candidate reactions + +After retrieving the Reactome pathways, the workflow collects the reactions that may potentially involve the input protein. + +For each lowest-level pathway, the workflow retrieves the contained events and keeps only true Reactome reactions. + +Pathway containers are excluded to avoid analysing higher-level pathway objects as if they were individual biochemical reactions. + +At this stage, the reactions are still considered **candidate reactions**. This means that they belong to Reactome pathways associated with the input UniProt accession, but they have not yet been confirmed to directly contain the target protein. + +--- + +### 6. Identify target reactions + +The script takes: + +- the candidate reactions collected from Reactome pathways; +- the target information prepared by `resolve_target_information()`. + +For each candidate reaction, the workflow checks whether the reaction actually contains the input protein. + +A reaction is considered a **target reaction** if at least one of the following checks is true: + +- the reaction contains a Reactome complex associated with the target UniProt accession; +- the reaction contains a Reactome protein stable ID corresponding to the target protein or one of its alternative forms; +- the BioPAX model of the reaction directly contains the input UniProt accession. + +The third check is the most robust one because it searches BioPAX protein and entity-reference cross-references directly for the input UniProt accession. + +Only reactions that pass this filtering step are kept for detailed annotation extraction. + +--- + +### 7. Extract BioPAX annotations + +For each reaction confirmed to contain the target protein, the workflow downloads or reuses the corresponding BioPAX model and extracts detailed reaction-level annotations. + +The extracted information includes: + +- protein display name; +- UniProt accession; +- cellular location; +- sequence intervals; +- sequence sites; +- modification type; +- complex membership; +- stoichiometry; +- parent protein family or physical entity; +- pathway hierarchy; +- reaction name and Reactome stable ID; +- biochemical left/right participants; +- conversion direction; +- regulatory controllers; +- disease links. + +This step produces nested annotation dictionaries describing the target-containing reactions. + +--- + +### 8. Build and write output tables + +The nested annotation dictionaries are flattened into a `pandas.DataFrame`. + +The final table is then: + +- cleaned; +- deduplicated; +- column-ordered; +- filtered to remove protein-family rows; +- optionally reordered using pathway-ordering files; +- written to `result.csv`. + +Protein-family rows are removed so that the final output focuses on individual protein entries rather than broad family-level Reactome entities. + +The final `result.csv` therefore contains only Reactome reactions that passed the target-reaction filtering step and for which BioPAX-level annotations could be extracted. + + +### Optional post-processing + +After running the main workflow for multiple UniProt accessions, the optional script `reactome_post_process.py` can merge individual `result.csv` files. It concatenates the available result tables, harmonizes the target columns when needed, removes duplicate rows, and writes three post-processed output files: + + +**merged_reaction.csv** contains a compact reaction-level summary across all analysed UniProt accessions. It keeps the target accession, target name, pathway hierarchy, disease annotation, lowest-level pathway, reaction name, reaction ID, reaction participants, and reaction direction. This file is useful for quickly comparing which reactions are associated with each protein across the full Reactome output. + +**merged_highest_pathways.csv** contains a simplified pathway-level summary. It reports the highest-level Reactome pathways associated with each target protein, together with the corresponding Reactome pathway ID, UniProt accession, and target name. This file is useful for obtaining a non-redundant overview of the major biological areas covered by the analysed proteins. + +**disease_single_sequence_site.csv** contains a filtered subset of the concatenated results. It keeps only rows belonging to the highest-level Reactome pathway Disease and where the sequence-site annotation corresponds to a single numeric residue position. This output is useful for downstream inspection of disease-associated reactions involving specific modified or annotated residue sites + +--- + +## Input + +Input for reactome_to_mavisp.py script: + +| Argument | Description | +|---|---| +| `-u`, `--uniprot_ac` | UniProt accession to analyze. Default: `Q8N726`. Ignored when `--uniprot_file` is supplied. | +| `-uf`, `--uniprot_file` | Text file containing one UniProt accession per line. Blank lines and lines starting with `#` are ignored. | +| `-o`, `--output_dir` | Main output directory. Default: `reactome_outputs`. A subfolder is created for each accession. | +| `-s`, `--skip_pathway_order` | Skip pathway-order inference. This speeds up the analysis and avoids writing `pathways_order/` files. | + +Here an example of file with a list of uniprot ac + + +```text +P04637 +Q8N726 +Q9Y2X3 +``` +Input for reactome_post_process.py script: + +Arguments: + +| Argument | Description | +|---|---| +| `-i`, `--input_dir` | Main Reactome output directory containing UniProt-specific folders. | +| `-o`, `--output_dir` | Output directory for merged tables. Default: same as `--input_dir`. | +| `--result_filename` | Name of the result file inside each UniProt folder. Default: `result.csv`. | + +--- + +## Output + +By default, outputs are written under: + +```text +reactome_outputs/ +``` + +For a single accession such as `P04637`, the output structure is: + +```text +reactome_outputs/ +├── entries_not_in_reactome.csv # Only created when at least one accession fails +└── P04637/ + ├── result.csv + ├── skipped_reactions.csv # Only created when reactions are skipped + └── pathways_order/ # Only created when pathway ordering is enabled + └── / + ├── graph_edges.csv + ├── graph_nodes.csv + └── ordered_paths.csv +``` + + +### `result.csv` + +`result.csv` is the main output of the workflow. Each row corresponds to a protein entry in a Reactome reaction involving the target protein. + +Common columns include: + +| Column | Description | +|---|---| +| `target_uniprot_ac` | Input UniProt accession. | +| `target_name` | Final target name. Preferentially from UniProt, otherwise from Reactome, otherwise the UniProt accession. | +| `highest_pathway` | Highest-level Reactome pathway. | +| `highest_pathway_id` | Reactome stable ID of the highest-level pathway. | +| `pathway_1`, `pathway_2`, ... | Intermediate pathway hierarchy levels. | +| `pathway_1_id`, `pathway_2_id`, ... | Reactome stable IDs of intermediate pathway levels. | +| `lowest_pathway` | Lowest-level pathway containing the reaction. | +| `lowest_pathway_id` | Reactome stable ID of the lowest-level pathway. | +| `reaction_name` | Reactome reaction name. | +| `reaction_id` | Reactome stable reaction ID. | +| `protein` | Protein entry parsed from BioPAX. | +| `uniprot_ac` | UniProt accession associated with the parsed protein entry. | +| `cellular_location` | Cellular location of the protein entry. | +| `SequenceInterval` | Sequence interval annotation, when available. | +| `SequenceSite` | Sequence site annotation, when available. | +| `Modification_type` | Protein modification annotation, when available. | +| `is_a_protein_family` | Boolean flag indicating whether the BioPAX entry represents a protein family. Protein-family rows are removed from the final output. | +| `complex_of` | Complexes in which the protein participates. | +| `stoichiometry` | Stoichiometric coefficient of the protein within the corresponding complex. | +| `member_physical_entity_of` | Parent protein family or physical entity, when available. | +| `reaction_Left` | Left-side participants of the biochemical reaction. | +| `reaction_Right` | Right-side participants of the biochemical reaction. | +| `reaction_Conversion_Direction` | Biochemical reaction directionality. | +| `Controller_of_reaction_*` | Controllers, activators, or inhibitors associated with the reaction. | +| `disease_name` | Disease annotation/link when available. | +| `ordered` | Boolean flag indicating whether the reaction could be ordered using pathway-ordering files. | + +Additional columns can appear depending on the BioPAX content returned by Reactome. + + +### `skipped_reactions.csv` + +This file is written inside a UniProt-specific output folder when one or more reactions could not be processed. + +Typical reasons include: + +| Reason | Meaning | +|---|---| +| `query_id_returned_none_after_retries` | Reactome did not return usable metadata for a reaction after retries. | +| `biopax_unavailable_after_retries` | The BioPAX model for that reaction could not be downloaded after retries. | + +Common columns: + +| Column | Description | +|---|---| +| `uniprot_ac` | UniProt accession being analyzed. | +| `reaction_id` | Reactome stable reaction ID. | +| `pathway_id` | Reactome stable ID of the pathway associated with the reaction. | +| `pathway_name` | Pathway name associated with the reaction. | +| `reason` | Reason why the reaction was skipped. | + + +### `entries_not_in_reactome.csv` + +This file is written at the global output-directory level when at least one input accession does not produce a valid output. + +Possible statuses: + +| Status | Meaning | +|---|---| +| `not_found_in_reactome` | The UniProt accession had no mapped human Reactome pathways. | +| `no_valid_reactome_output` | Reactome contained mapped pathways for the accession, but no valid reactions remained after filtering. | + +Common columns: + +| Column | Description | +|---|---| +| `uniprot_ac` | UniProt accession. | +| `status` | Failure/status category. | +| `reason` | Explanation of why no final output was produced. | + + +### `pathways_order/` + +This directory is created only when pathway ordering is enabled. + +For each pathway, the workflow writes: + +| File | Description | +|---|---| +| `graph_edges.csv` | Directed edges between reaction nodes. | +| `graph_nodes.csv` | Reaction nodes with `is_start` and `is_end` flags. | +| `ordered_paths.csv` | Ordered reaction paths inferred from the directed graph. | + +When `--skip_pathway_order` is used, this directory is not created and reactions in `result.csv` are marked as unordered. + +### `post process analysis` + +The post-processing script writes: + +| File | Description | +|---|---| +| `merged_reaction.csv` | Deduplicated reaction-level summary across all analyzed UniProt accessions. | +| `merged_highest_pathways.csv` | Deduplicated table of highest-level pathways per target. | +| `disease_single_sequence_site.csv` | Subset of disease-pathway rows where the sequence-site annotation is a single numeric residue position. | + +--- + +## Run + +### Run one UniProt accession + +```bash +python reactome_to_mavisp.py -u P04637 +``` + +### Run one UniProt accession and skip pathway ordering + +```bash +python reactome_to_mavisp.py -u P04637 -s +``` +Skipping pathway ordering is faster because it avoids building pathway-level reaction graphs. + +### Run with input list + +```bash +python reactome_to_mavisp.py -uf uniprot_list.txt -o reactome_outputs +``` +### Run post process analysis + +```bash +python reactome_post_process.py -i reactome_outputs -o reactome_outputs/summary +``` + +--- + +## example + +```bash +# Single protein, faster run without pathway ordering +python reactome_to_mavisp.py -u P04637 -s + +# Multiple proteins +python reactome_to_mavisp.py -uf uniprot_list.txt -o reactome_outputs -s + +# Merge results +python reactome_to_mavisp.py -i reactome_post_process.py -o reactome_outputs/summary +``` + diff --git a/tools/reactome_to_mavisp/example/readme.txt b/tools/reactome_to_mavisp/example/readme.txt new file mode 100644 index 0000000..0a7b042 --- /dev/null +++ b/tools/reactome_to_mavisp/example/readme.txt @@ -0,0 +1 @@ +bash run.sh diff --git a/tools/reactome_to_mavisp/example/run.sh b/tools/reactome_to_mavisp/example/run.sh new file mode 100644 index 0000000..ea2fc4d --- /dev/null +++ b/tools/reactome_to_mavisp/example/run.sh @@ -0,0 +1,2 @@ +source /data/user/marnaudi/reactome/reactome2py/bin/activate +python ../reactome_to_mavisp.py -uf uniprot_list.txt -o reactome_outputs -s diff --git a/tools/reactome_to_mavisp/example/uniprot_list.txt b/tools/reactome_to_mavisp/example/uniprot_list.txt new file mode 100644 index 0000000..a40fe71 --- /dev/null +++ b/tools/reactome_to_mavisp/example/uniprot_list.txt @@ -0,0 +1,2 @@ +Q8N726 +P04637 diff --git a/tools/reactome_to_mavisp/reactome_pipeline/__init__.py b/tools/reactome_to_mavisp/reactome_pipeline/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tools/reactome_to_mavisp/reactome_pipeline/__pycache__/__init__.cpython-38.pyc b/tools/reactome_to_mavisp/reactome_pipeline/__pycache__/__init__.cpython-38.pyc new file mode 100644 index 0000000000000000000000000000000000000000..7d83f43f0bbd1e2407c106f3a126fe952c0fbed8 GIT binary patch literal 154 zcmWIL<>g`kg5VR@Ss?l`h(HF6K#l_t7qb9~6oz01O-8?!3`HPe1o6vWKP9mwQNOe} zwMaiVu_!OGG$m8NC^a#;BtJJ*za+J|1kQ^u$Sg?B$;?aDkB`sH%PfhH*DI*J#bJ}1 QpHiBWY6mj-GY~TX0L4-zWB>pF literal 0 HcmV?d00001 diff --git a/tools/reactome_to_mavisp/reactome_pipeline/__pycache__/__init__.cpython-39.pyc b/tools/reactome_to_mavisp/reactome_pipeline/__pycache__/__init__.cpython-39.pyc new file mode 100644 index 0000000000000000000000000000000000000000..9f668546b882e49937c6b4fe8a58053f3e53b4a4 GIT binary patch literal 177 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 DbN1m@ literal 0 HcmV?d00001 diff --git a/tools/reactome_to_mavisp/reactome_pipeline/__pycache__/data_processing.cpython-39.pyc b/tools/reactome_to_mavisp/reactome_pipeline/__pycache__/data_processing.cpython-39.pyc new file mode 100644 index 0000000000000000000000000000000000000000..a535a391c9f8b3c5792e6dbd992d12ad1285c7f6 GIT binary patch literal 8285 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 ZJ{ZMl^K^)76E2MaSjMm|sUsG$zW`i$<~#rZ literal 0 HcmV?d00001 diff --git a/tools/reactome_to_mavisp/reactome_pipeline/__pycache__/graph_utils.cpython-39.pyc b/tools/reactome_to_mavisp/reactome_pipeline/__pycache__/graph_utils.cpython-39.pyc new file mode 100644 index 0000000000000000000000000000000000000000..c48ff45d87cddd3d8859f1e8d8dbca1a62be3c91 GIT binary patch literal 2914 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 T1!mS3C?C4-OWqC+@YVkXlt5`< literal 0 HcmV?d00001 diff --git a/tools/reactome_to_mavisp/reactome_pipeline/__pycache__/reactome_analysis.cpython-39.pyc b/tools/reactome_to_mavisp/reactome_pipeline/__pycache__/reactome_analysis.cpython-39.pyc new file mode 100644 index 0000000000000000000000000000000000000000..02dc1edb87949c30be9402b5fcf393364613b3ee GIT binary patch literal 15462 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 Dn6b<; literal 0 HcmV?d00001 diff --git a/tools/reactome_to_mavisp/reactome_pipeline/__pycache__/uniprot_utils.cpython-38.pyc b/tools/reactome_to_mavisp/reactome_pipeline/__pycache__/uniprot_utils.cpython-38.pyc new file mode 100644 index 0000000000000000000000000000000000000000..56a1b76540242a3750a6d0f27c7e41e007f7be81 GIT binary patch literal 2915 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 literal 0 HcmV?d00001 diff --git a/tools/reactome_to_mavisp/reactome_pipeline/__pycache__/workflow.cpython-39.pyc b/tools/reactome_to_mavisp/reactome_pipeline/__pycache__/workflow.cpython-39.pyc new file mode 100644 index 0000000000000000000000000000000000000000..60a56b37635442d799436d31bcf867a3feed660b GIT binary patch literal 15079 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 Any: + if isinstance(value, (set, list)) and len(value) == 0: + return np.nan + return value + + @staticmethod + def process_data(data: List[Dict[str, Any]]) -> pd.DataFrame: + """Flatten nested pathway/reaction annotations into a tabular DataFrame. + + Each row represents one protein entry within one Reactome reaction and includes + pathway hierarchy, reaction metadata, protein features, complex membership, + stoichiometry, regulatory information, and disease annotations when available. + """ + rows: List[Dict[str, Any]] = [] + for entry in data: + pathways_keys = [int(k) for k in entry.keys() if k != 'reaction'] + pathways_keys.sort() + for reaction in entry['reaction']: + proteins = reaction.get('proteins', []) + for protein in proteins: + row: Dict[str, Any] = {} + row["protein"] = protein['display_name'] + row["uniprot_ac"] = protein['uniprot_ac'] + row["cellular_location"] = protein['cellular_location'] + prot_stId = protein['stId'] + + # Protein sequence features and residue modifications + + sequence_interval: List[str] = [] + sequence_site: List[str] = [] + modification_type: List[str] = [] + if 'feature' in protein: + features = protein['feature'] + for feature in features: + + if 'modification_type' in feature: + mod_type_match = re.search(r'"(.*?)"', str(feature['modification_type'])) + if mod_type_match: + modification_type.append(mod_type_match.group(1)) + elif mod_type_match is None: + modification_type.append("could be altered by a mutation event") + else: + raise ValueError("Unexpected modification_type annotation") + position = re.findall(r'\((\d+)\)', str(feature['feature_location'])) + position = list(map(str, position)) + + if "SequenceInterval" in str(feature['feature_location']): + sequence_interval.append("-".join(position)) + if "SequenceSite" in str(feature['feature_location']) and not "SequenceInterval" in str(feature['feature_location']): + sequence_site.append("-".join(position)) + if sequence_interval: + row["SequenceInterval"] = "_".join(sequence_interval) + if sequence_site: + row["SequenceSite"] = "_".join(sequence_site) + if modification_type: + row["Modification_type"] = "_".join(modification_type) + + # Protein family annotation + if not protein['member_physical_entity_of'] and protein['member_physical_entity']: + row['is_a_protein_family'] = True + else: + row['is_a_protein_family'] = False + + # Complex membership and stoichiometry + + # Get the complexes in which the protein participates + complex_of = protein['component_of'] + # Get all the complexes of the reaction + reaction_complexes = reaction.get('complex', {}) + # Annotate all the complexes in which the protein participates + complexes_per_protein: List[str] = [] + # Annotate the stoichiometry of the protein in the corresponding complex + stoichiometry: List[str] = [] + protein_complex_ids = { + prot_complex["stId"] + for prot_complex in complex_of + if prot_complex.get("stId") + } + + prot_reaction_complexes = [ + rcx for rcx in reaction_complexes + if protein_complex_ids.intersection(set(rcx.get("complexes_ids", []))) + ] + for prc in prot_reaction_complexes: + for entity in prc["complex_components"]: + entity_id = entity["stId"] + + if prot_stId == entity_id: + complexes_per_protein.append(str(prc["complex_name"])) + stoichiometry.append(str(entity["stoc_coefficient"])) + + else: + if entity_id not in ReactomeAnalysisFunctions.complex_entities_cache: + ReactomeAnalysisFunctions.complex_entities_cache[entity_id] = ( + ReactomeAnalysisFunctions.safe_reactome_call( + rc.content.entities_complex, + entity_id, + default=[] + ) if rc else [] + ) + + components_in_complex = ReactomeAnalysisFunctions.complex_entities_cache[entity_id] + + for component in components_in_complex: + if component and prot_stId == component.get("stId"): + if str(prc["complex_name"]) not in complexes_per_protein: + complexes_per_protein.append(str(prc["complex_name"])) + stoichiometry.append(str(entity["stoc_coefficient"])) + if complex_of: + row["complex_of"] = "_".join(complexes_per_protein) + row["stoichiometry"] = "_".join(stoichiometry) + + # Parent protein family, when available + + match = re.search(r"\((.*?)\)", str(protein['member_physical_entity_of'])) + if match: + result = match.group(1) + else: + result = None + + row['member_physical_entity_of'] = result + + # Pathway hierarchy annotation + + row['highest_pathway'] = entry[pathways_keys[0]].get('name') + row['highest_pathway_id'] = entry[pathways_keys[0]].get('id') + row['lowest_pathway'] = entry[pathways_keys[-1]].get('name') + row['lowest_pathway_id'] = entry[pathways_keys[-1]].get('id') + for key, index in zip(pathways_keys[1:-1], range(1, len(pathways_keys) - 1)): + row[f'pathway_{index}'] = entry[key].get('name') + row[f'pathway_{index}_id'] = entry[key].get('id') + + # Reaction identifiers + row['reaction_id'] = reaction['stID'] + row['reaction_name'] = reaction["Display_Name"] + + # Biochemical reaction metadata + biochemical = reaction.get('biochemical', {}) + if len(biochemical) > 1: + raise ValueError("Unexpected multiple biochemical entries") + for bio in biochemical: + for key, value in bio.items(): + if isinstance(value, list): + str_value: List[str] = [] + for val in value: + str_value.append(str(val)) + if str_value: + row[f'reaction_{key}'] = " ".join(str_value) + else: + row[f'reaction_{key}'] = [] + else: + row[f'reaction_{key}'] = value + + # Reaction regulation, such as activation or inhibition + + control_information = reaction.get('control_information', []) + + # Store the regulatory role of each controller, such as activation or inhibition. + + for reaction_control in control_information: + controllers: List[str] = [] + + for controller in reaction_control['controller']: + match = re.search(r"\((.*?)\)", str(controller)) + if match: + controllers.append(match.group(1)) + + control_type = reaction_control['control_type'] + row[f'Controller_of_reaction_{control_type}'] = "_".join(controllers) + + # Catalytic metadata, when available + catalytic = reaction.get('catalytic', {}) + for key, value in catalytic.items(): + if isinstance(value, list): + str_value: List[str] = [] + for val in value: + str_value.append(str(val)) + if str_value: + row[f'catalytic_{key}'] = " ".join(str_value) + else: + row[f'catalytic_{key}'] = [] + else: + row[f'catalytic_{key}'] = value + + # Disease annotation, when available + row["disease_name"] = reaction["disease"] + rows.append(row) + df = pd.DataFrame(rows) + df = df.applymap(DataProcessingFunctions.replace_empty_with_nan) + df.drop_duplicates(inplace=True) + return df + + @staticmethod + def get_column_type(col: str) -> str: + """Classify a DataFrame column into a semantic category.""" + + # Pathways: Columns that start with "highest_pathway", "pathway_", or "lowest_pathway" + if "pathway" in col: + if "highest_pathway" in col: + return "highest_pathway" + elif "lowest_pathway" in col: + return "lowest_pathway" + elif col.startswith("pathway_"): + return "pathway" + if col.startswith("disease_name"): + return "disease_name" + + # Reaction: Columns that start with "reaction" + if col.startswith("reaction"): + if "reaction_name" in col: + return "reaction_name" + elif "reaction_id" in col: + return "reaction_id" + elif "reaction_Left" in col: + return "reaction_left" + elif "reaction_Right" in col: + return "reaction_right" + elif "reaction_Conversion_Direction" in col: + return "reaction_conversion_direction" + elif "reaction_controller" in col: + return "reaction_controller" + return "reaction" + # Catalytic: Columns that start with "catalytic" + + if col.startswith("catalytic"): + return "catalytic" + + # Complex or other physical entities: Columns that start with "Complex", "SmallMolecule", or "Protein" + if col.startswith("Complex") or col.startswith("SmallMolecule") or col.startswith("Protein"): + return "Physical_entity" + return "Physical_entity" # Default category for uncategorized columns + + @staticmethod + def column_sort_key(col: str) -> Tuple[int, str]: + """Return a key for sorting columns.""" + column_type = DataProcessingFunctions.get_column_type(col) + # Assign priority for each column type + type_priority = { + "highest_pathway": 0, + "disease_name": 1, + "pathway": 2, + "lowest_pathway": 3, + "reaction_name": 4, + "reaction_id": 5, + "reaction_right": 7, + "reaction_left": 6, + "reaction_conversion_direction": 8, + "reaction_controller": 9, + "reaction": 10, + "catalytic": 11, + "complex": 12, + "Physical_entity": 13, + } + # First sort by category type, then alphabetically within each category + return (type_priority.get(column_type, 99), col) + + @staticmethod + def reorder_dataframe_from_ordered_paths( + df: pd.DataFrame, + lowest_pathway_id: str, + ordered_paths_df: pd.DataFrame + ) -> pd.DataFrame: + + df_filtered = df[df["lowest_pathway_id"] == lowest_pathway_id].copy() + + if df_filtered.empty or ordered_paths_df.empty: + return df_filtered + + reaction_set = set(df_filtered["reaction_name"].dropna().tolist()) + + best_path_id = None + best_overlap = set() + + for path_id, path_df in ordered_paths_df.groupby("path_id"): + path_reactions = set(path_df["reaction"].dropna().tolist()) + overlap = reaction_set.intersection(path_reactions) + + if len(overlap) > len(best_overlap): + best_overlap = overlap + best_path_id = path_id + + if best_path_id is None or not best_overlap: + df_filtered["ordered"] = False + return df_filtered + + best_path_df = ordered_paths_df[ + ordered_paths_df["path_id"] == best_path_id + ].sort_values("step") + + ordered_reactions = [ + r for r in best_path_df["reaction"].tolist() + if r in reaction_set + ] + + missing_reactions = [ + r for r in reaction_set + if r not in ordered_reactions + ] + + final_order = ordered_reactions + missing_reactions + + df_filtered["ordered"] = df_filtered["reaction_name"].isin(ordered_reactions) + + df_filtered["reaction_name"] = pd.Categorical( + df_filtered["reaction_name"], + categories=final_order, + ordered=True + ) + + df_filtered = df_filtered.sort_values("reaction_name") + + return df_filtered + + @staticmethod + def fill_complex_info(group: pd.DataFrame) -> pd.DataFrame: + """Fill missing complex and stoichiometry values in grouped DataFrame.""" + for index, row in group.iterrows(): + if pd.isna(row["complex_of"]): + matching_row = group[group["protein"] == row["member_physical_entity_of"]] + if not matching_row.empty: + group.at[index, "complex_of"] = matching_row.iloc[0]["complex_of"] + group.at[index, "stoichiometry"] = matching_row.iloc[0]["stoichiometry"] + return group \ No newline at end of file diff --git a/tools/reactome_to_mavisp/reactome_pipeline/graph_utils.py b/tools/reactome_to_mavisp/reactome_pipeline/graph_utils.py new file mode 100644 index 0000000..d937c0f --- /dev/null +++ b/tools/reactome_to_mavisp/reactome_pipeline/graph_utils.py @@ -0,0 +1,95 @@ + +from typing import Any, Dict, Iterable, List, Set, Tuple + +import networkx as nx + +class PathwayGraphFunctions: + """Utility functions for pathway graph construction and analysis.""" + + @staticmethod + def make_pathway_graph( + reactions_lists: Iterable[Tuple[str, str, str]], + pathway_id: str + ) -> Tuple[nx.DiGraph, List[str], List[str], List[Dict[str, Any]], List[Dict[str, Any]]]: + + G = nx.DiGraph() + starting_nodes: List[str] = [] + ending_nodes: List[str] = [] + edge_rows: List[Dict[str, Any]] = [] + + for current, next_reaction, previous in reactions_lists: + + # Skip invalid nodes to avoid adding empty nodes to the graph. + if not current: + print(f"[WARNING] Skipping invalid current: {current}, next={next_reaction}, previous={previous}") + continue + + if next_reaction: + G.add_edge(current, next_reaction) + edge_rows.append({ + "pathway_id": pathway_id, + "source": current, + "target": next_reaction + }) + + if previous: + G.add_edge(previous, current) + edge_rows.append({ + "pathway_id": pathway_id, + "source": previous, + "target": current + }) + + if not previous: + starting_nodes.append(current) + + if not next_reaction: + ending_nodes.append(current) + + # Remove duplicated edges while preserving row structure. + edge_rows = list({(e["pathway_id"], e["source"], e["target"]): e for e in edge_rows}.values()) + + node_rows = [] + for node in G.nodes: + node_rows.append({ + "pathway_id": pathway_id, + "node": node, + "is_start": node in starting_nodes, + "is_end": node in ending_nodes + }) + + return G, starting_nodes, ending_nodes, edge_rows, node_rows + + @staticmethod + def remove_duplicates_order(lists: Iterable[List[Any]]) -> List[List[Any]]: + """Remove duplicate lists while preserving order.""" + seen: Set[Tuple[Any, ...]] = set() + result: List[List[Any]] = [] + for sub in lists: + t = tuple(sub) + if t not in seen: + seen.add(t) + result.append(list(sub)) + return result + + @staticmethod + def is_subsequence(sub: List[Any], main: List[Any]) -> bool: + """Return True if ``sub`` is a subsequence of ``main``.""" + it = iter(main) + return all(x in it for x in sub) + + @staticmethod + def find_non_subsequences(lists: Iterable[List[Any]]) -> List[List[Any]]: + """Return those lists that are not subsequences of any other list.""" + lists = list(lists) + non_subsequences: List[List[Any]] = [] + for i, sub in enumerate(lists): + is_sub = False + for j, main in enumerate(lists): + if i != j and PathwayGraphFunctions.is_subsequence(sub, main): + is_sub = True + break + if not is_sub: + non_subsequences.append(sub) + return non_subsequences + diff --git a/tools/reactome_to_mavisp/reactome_pipeline/reactome_analysis.py b/tools/reactome_to_mavisp/reactome_pipeline/reactome_analysis.py new file mode 100644 index 0000000..17427af --- /dev/null +++ b/tools/reactome_to_mavisp/reactome_pipeline/reactome_analysis.py @@ -0,0 +1,659 @@ +import re +import copy +import contextlib +import io +import time +from collections import defaultdict +from typing import Any, Dict, List, Tuple + +import requests + +try: + import reactome2py as rc + from reactome2py import analysis, content, utils +except ImportError: + rc = None # type: ignore + +try: + import pybiopax +except ImportError: + pybiopax = None # type: ignore + +from reactome_pipeline.uniprot_utils import UniprotFunctions + + +class ReactomeAnalysisFunctions: + """Functions for extracting information from BioPAX objects.""" + + biopax_cache: Dict[str, Any] = {} + reactome_call_cache: Dict[Any, Any] = {} + complex_entities_cache: Dict[str, Any] = {} + disease_link_cache: Dict[str, Any] = {} + reaction_target_cache: Dict[Tuple[str, str], bool] = {} + reaction_information_cache: Dict[Tuple[str, Tuple[str, ...]], List[Dict[str, Any]]] = {} + + @staticmethod + def safe_model_from_reactome_cached(st_id: str, retries: int = 5, sleep: int = 3) -> Any: + import time + import requests + + if st_id in ReactomeAnalysisFunctions.biopax_cache: + return ReactomeAnalysisFunctions.biopax_cache[st_id] + + reactome_id = st_id.split("-")[2] + + for attempt in range(1, retries + 1): + try: + model = pybiopax.api.model_from_reactome(reactome_id) + ReactomeAnalysisFunctions.biopax_cache[st_id] = model + return model + + except requests.exceptions.RequestException as e: + print( + f"[WARNING] BioPAX download failed for {st_id} " + f"({attempt}/{retries}): {e}" + ) + + if attempt < retries: + time.sleep(sleep * attempt) + + print(f"[ERROR] Skipping {st_id}: BioPAX model unavailable") + ReactomeAnalysisFunctions.biopax_cache[st_id] = None + return None + + @staticmethod + def safe_reactome_call(func, *args, retries: int = 8, sleep: int = 5, default=None, **kwargs): + import time + import requests + import io + import contextlib + import copy + + func_key = f"{getattr(func, '__module__', '')}.{getattr(func, '__name__', str(func))}" + + cache_key = ( + func_key, + repr(args), + repr(sorted(kwargs.items())) + ) + + if cache_key in ReactomeAnalysisFunctions.reactome_call_cache: + return copy.deepcopy(ReactomeAnalysisFunctions.reactome_call_cache[cache_key]) + + for attempt in range(1, retries + 1): + try: + stdout_buffer = io.StringIO() + + with contextlib.redirect_stdout(stdout_buffer): + result = func(*args, **kwargs) + + printed_output = stdout_buffer.getvalue() + + if printed_output: + print(printed_output, end="") + + if "Status code returned a value of 404" in printed_output or "Not Found" in printed_output: + print(f"[INFO] Reactome returned 404 for {func_key}, args={args}, kwargs={kwargs}. Skipping.") + + ReactomeAnalysisFunctions.reactome_call_cache[cache_key] = copy.deepcopy(default) + return default + + if "Status code returned a value of 429" in printed_output or "Too Many Requests" in printed_output: + wait_time = max(60, sleep * attempt * 10) + + print( + f"[WARNING] Reactome rate limit reached for {func_key}, " + f"args={args}, kwargs={kwargs} ({attempt}/{retries}). " + f"Waiting {wait_time} seconds before retrying." + ) + + if attempt < retries: + time.sleep(wait_time) + continue + + print( + f"[WARNING] Reactome rate limit persisted after {retries} attempts. " + "Returning default." + ) + return default + + + if ( + "Status code returned a value of 500" in printed_output + or "Internal Server Error" in printed_output + or "Status code returned a value of 521" in printed_output + ): + print( + f"[WARNING] Reactome server error for {func_key}, args={args}, kwargs={kwargs} " + f"({attempt}/{retries}). Retrying." + ) + + if attempt < retries: + time.sleep(sleep * attempt) + continue + + print(f"[WARNING] Reactome server error persisted after {retries} attempts. Returning default.") + return default + + if result is None: + print( + f"[INFO] Empty response for {func_key}, args={args}, kwargs={kwargs}. " + f"No status code detected. Skipping." + ) + return default + + time.sleep(0.5) + ReactomeAnalysisFunctions.reactome_call_cache[cache_key] = copy.deepcopy(result) + return result + + except ( + requests.exceptions.ConnectionError, + requests.exceptions.Timeout, + requests.exceptions.ReadTimeout, + ) as e: + wait_time = max(30, sleep * attempt * 5) + print( + f"[WARNING] Connection problem for {func_key}, " + f"args={args}, kwargs={kwargs} ({attempt}/{retries}): {e}. " + f"Waiting {wait_time} seconds before retrying." + ) + + if attempt < retries: + time.sleep(wait_time) + continue + + return default + + except Exception as e: + error_msg = str(e) + + if "404" in error_msg or "Not Found" in error_msg: + print(f"[INFO] Reactome returned 404 for {func_key}, args={args}, kwargs={kwargs}. Skipping.") + + ReactomeAnalysisFunctions.reactome_call_cache[cache_key] = copy.deepcopy(default) + return default + + if "429" in error_msg or "Too Many Requests" in error_msg: + wait_time = max(60, sleep * attempt * 10) + + print( + f"[WARNING] Reactome rate limit reached for {func_key}, " + f"args={args}, kwargs={kwargs} ({attempt}/{retries}). " + f"Waiting {wait_time} seconds before retrying." + ) + + if attempt < retries: + time.sleep(wait_time) + continue + + print( + f"[WARNING] Reactome rate limit persisted after {retries} attempts. " + "Returning default." + ) + return default + + if "500" in error_msg or "Internal Server Error" in error_msg or "521" in error_msg: + print( + f"[WARNING] Reactome server error for {func_key}, args={args}, kwargs={kwargs} " + f"({attempt}/{retries}). Retrying." + ) + + if attempt < retries: + time.sleep(sleep * attempt) + continue + + print(f"[WARNING] Reactome server error persisted after {retries} attempts. Returning default.") + return default + + print(f"[WARNING] Reactome call failed for {func_key}, args={args}, kwargs={kwargs} ({attempt}/{retries}): {e}") + + if attempt < retries: + time.sleep(sleep * attempt) + continue + + return default + + return default + + @staticmethod + def check_attributes(pathway_step_obj: Any) -> None: + for attr in dir(pathway_step_obj): + if not attr.startswith("_"): + try: + getattr(pathway_step_obj, attr) + except Exception: + pass + @staticmethod + def top_level_pathways(pathways_list: List[Dict[str, Any]]) -> List[Dict[int, Dict[str, str]]]: + pathways_dic: List[Dict[int, Dict[str, str]]] = [] + for pathway in pathways_list: + pathway_code = pathway['stId'] + top_level = ReactomeAnalysisFunctions.safe_reactome_call( + rc.content.event_ancestors, + id=pathway_code, + default=[] + ) + dic: Dict[int, Dict[str, str]] = {} + for level in top_level: + for index, item in enumerate(level[::-1]): + dic[index] = {'name': item['displayName'], 'id': item['stId']} + pathways_dic.append(dic) + unique_dicts: List[Dict[int, Dict[str, str]]] = [] + seen: Set[frozenset] = set() + for item in pathways_dic: + frozen_item = frozenset((k, frozenset(v.items())) for k, v in item.items()) + if frozen_item not in seen: + seen.add(frozen_item) + unique_dicts.append(item) + return unique_dicts + + @staticmethod + + def collect_disease_link(stId: str) -> str: + if stId in ReactomeAnalysisFunctions.disease_link_cache: + return ReactomeAnalysisFunctions.disease_link_cache[stId] + + links: List[str] = [] + + disease = ReactomeAnalysisFunctions.safe_reactome_call( + rc.content.query_id, + id=stId, + enhanced=True, + default=None + ) + + if not disease or not disease.get("isInDisease"): + ReactomeAnalysisFunctions.disease_link_cache[stId] = "" + return "" + + for x in disease.get("crossReference", []): + if x.get("databaseName") == "Mondo" and x.get("url"): + links.append(x.get("url")) + + result = "_".join(links) + + ReactomeAnalysisFunctions.disease_link_cache[stId] = result + return result + + @staticmethod + def get_pathway_order(biopax_obj: Any) -> Dict[str, List[Dict[str, List[str]]]]: + step_processes: Dict[str, List[Dict[str, List[str]]]] = {} + for pathway_step_obj in biopax_obj.get_objects_by_type(pybiopax.biopax.BiochemicalReaction): + Step_Process_Of = pathway_step_obj.step_process_of + single_step_processes: List[Dict[str, List[str]]] = [] + if Step_Process_Of: + for process in list(Step_Process_Of): + single_step_process: Dict[str, List[str]] = {} + next_step = process.next_step + next_step_of = process.next_step_of + if next_step: + next_steps: List[str] = [] + for step in list(next_step): + next_steps.extend(i.name for i in step.step_process) + single_step_process['next_step'] = next_steps + if next_step_of: + next_steps_of: List[str] = [] + for step in list(next_step_of): + next_steps_of.extend(i.name for i in step.step_process) + single_step_process['previous'] = next_steps_of + single_step_processes.append(single_step_process) + reaction_name = ( + getattr(pathway_step_obj, "display_name", None) + or " ".join(getattr(pathway_step_obj, "name", []) or []) + ) + + if not reaction_name: + print("[WARNING] Reaction without display_name/name") + continue + + step_processes[reaction_name] = single_step_processes + return step_processes + + @staticmethod + def biopax_model_contains_uniprot(biopax_model: Any, uniprot_ac: str) -> bool: + for protein in biopax_model.get_objects_by_type(pybiopax.biopax.Protein): + + for xref in getattr(protein, "xref", []): + if str(getattr(xref, "id", "")) == uniprot_ac: + return True + + entity_reference = getattr(protein, "entity_reference", None) + if entity_reference is not None: + for xref in getattr(entity_reference, "xref", []): + if str(getattr(xref, "id", "")) == uniprot_ac: + return True + + return False + + @staticmethod + def build_reaction_information( + reaction: Dict[str, Any], + reaction_biopax_model: Any, + pathways_to_not_analyze: List[str] + ) -> List[Dict[str, Any]]: + reaction_stID = reaction["stId"] + reaction_name = reaction["displayName"] + + biochemical_reactions = list( + reaction_biopax_model.get_objects_by_type(pybiopax.biopax.BiochemicalReaction) + ) + + reaction_components: List[Any] = [ + biochemical_reaction.name + for biochemical_reaction in biochemical_reactions + ] + + should_analyze = True + + if len(reaction_components) > 1: + pathways_component: List[str] = [] + + for biochemical_reaction in biochemical_reactions: + for path_name in list(getattr(biochemical_reaction, "pathway_component_of", [])): + pathways_component.append(path_name.display_name) + + if any( + any(pathway_to_skip in pathway_component for pathway_to_skip in pathways_to_not_analyze) + for pathway_component in pathways_component + ): + should_analyze = False + + if not should_analyze: + print( + f"[INFO] Skipping {reaction_stID}: contains pathway components marked as not to analyze." + ) + return [] + + biochemical_information = ReactomeAnalysisFunctions.get_biochemical( + reaction_biopax_model + ) + complexes_information = ReactomeAnalysisFunctions.get_complex( + reaction_biopax_model + ) + control_information = ReactomeAnalysisFunctions.get_control_information( + reaction_biopax_model + ) + pathway_information = ReactomeAnalysisFunctions.get_pathway( + reaction_biopax_model + ) + proteins_information = ReactomeAnalysisFunctions.get_protein( + reaction_biopax_model + ) + disease = ReactomeAnalysisFunctions.collect_disease_link( + reaction_stID + ) + + if len(reaction_components) > 1: + display_names = reaction_components + else: + display_names = [reaction_name] + + reaction_information_list: List[Dict[str, Any]] = [] + + for reaction_name_component in display_names: + if isinstance(reaction_name_component, list): + display_name = " ".join(map(str, reaction_name_component)) + else: + display_name = str(reaction_name_component) + + reaction_information: Dict[str, Any] = { + "stID": reaction_stID, + "Display_Name": display_name, + "biochemical": biochemical_information, + "control_information": control_information, + #"catalytic": catalysis_information, + "complex": complexes_information, + "pathway": pathway_information, + "proteins": proteins_information, + "disease": disease + } + + reaction_information_list.append(reaction_information) + + return reaction_information_list + + @staticmethod + def get_complex(biopax_obj: Any) -> List[Dict[str, Any]]: + reaction_complexes: List[Dict[str, Any]] = [] + for pathway_step_obj in biopax_obj.get_objects_by_type(pybiopax.biopax.Complex): + complexes: Dict[str, Any] = {} + Name = pathway_step_obj.name + complex_name = "/".join(Name) + Component_stoichiometry = pathway_step_obj.component_stoichiometry + components: List[Dict[str, Any]] = [] + complex_ids: List[str] = [] + for reac_id in pathway_step_obj.xref: + if reac_id.id.startswith("R-HSA"): + complex_ids.append(reac_id.id) + complex_ids = list(set(complex_ids)) + for stoc in Component_stoichiometry: + st_id = None + stoc_coefficient = stoc.stoichiometric_coefficient + physical_entity = stoc.physical_entity + + for x in getattr(stoc.physical_entity, "xref", []): + xref_id = str(getattr(x, "id", "")) + + if ( + xref_id.startswith("R-HSA-") + or xref_id.startswith("R-ALL-") + or xref_id.startswith("R-COV-") + or xref_id.startswith("R-HIV-") + or xref_id.startswith("R-FLU-") + or xref_id.startswith("R-ECO-") + or xref_id.startswith("R-MTU-") + ): + st_id = xref_id + break + + components.append({ + "physical_entity": physical_entity, + "stoc_coefficient": stoc_coefficient, + "stId": st_id + }) + complexes["complex_name"] = complex_name + complexes["complexes_ids"] = complex_ids + complexes["complex_components"] = components + Participant = pathway_step_obj.participant_of + complexes['Participant'] = [i.display_name for i in Participant] + Evidence = pathway_step_obj.evidence + complexes['evidence'] = Evidence + reaction_complexes.append(complexes) + return reaction_complexes + + @staticmethod + def get_protein(biopax_obj: Any) -> List[Dict[str, Any]]: + proteins: List[Dict[str, Any]] = [] + for pathway_step_obj in biopax_obj.get_objects_by_type(pybiopax.biopax.Protein): + protein_features: Dict[str, Any] = {} + cellular_location = pathway_step_obj.cellular_location + entity_reference = pathway_step_obj.entity_reference + component_of_info = [] + + for comp in pathway_step_obj.component_of: + comp_id = None + + for ref in getattr(comp, "xref", []): + ref_id = getattr(ref, "id", None) + if ref_id and ref_id.startswith("R-HSA"): + comp_id = ref_id + break # Use the first Reactome identifier found for this component. + + component_of_info.append({ + "name": getattr(comp, "display_name", str(comp)), + "stId": comp_id + }) + controller_of = pathway_step_obj.controller_of + display_name = pathway_step_obj.display_name + evidence = pathway_step_obj.evidence + feature = pathway_step_obj.feature + get_plain_names = pathway_step_obj.get_plain_names + list_types = pathway_step_obj.list_types + member_physical_entity = pathway_step_obj.member_physical_entity + member_physical_entity_of = pathway_step_obj.member_physical_entity_of + names = pathway_step_obj.name + refs = pathway_step_obj.xref + stId = None + for ref in refs: + if ref.id.startswith("R-HSA"): + stId = ref.id + uniprot_acs: List[str] = [] + if '-' not in display_name: + if entity_reference: + uniprot_acs.append(entity_reference.name[0].split(" ")[0].split(":")[1]) + else: + splitted_names = display_name.split("-") + for splitted_name in splitted_names: + if splitted_name not in ["p", "S", "T", "Y"]: + uniprot_ac = UniprotFunctions.uniprot_gene_to_uniprot_ac(splitted_name) + if uniprot_ac and uniprot_ac not in uniprot_acs: + uniprot_acs.append(uniprot_ac) + if len(uniprot_acs) == 0: + uniprot_acs = [] + if entity_reference: + uniprot_acs.append(entity_reference.name[0].split(" ")[0].split(":")[1]) + not_feature = pathway_step_obj.not_feature + participant_of = pathway_step_obj.participant_of + standard_name = pathway_step_obj.standard_name + uid = pathway_step_obj.uid + uid_index = re.search(r'\d+$', uid).group() + if feature: + features: List[Dict[str, Any]] = [] + for i in feature: + features_dic: Dict[str, Any] = {} + features_dic["feature_location"] = i.feature_location + features_dic["feature_location_type"] = i.feature_location_type + features_dic["feature_of"] = i.feature_of + features_dic["uid"] = i.uid + features.append(features_dic) + if "modification_type" in dir(i): + features_dic["modification_type"] = i.modification_type + protein_features["feature"] = features + protein_features["cellular_location"] = cellular_location + protein_features["component_of"] = component_of_info + protein_features["controller_of"] = controller_of + protein_features["member_physical_entity"] = member_physical_entity + protein_features["member_physical_entity_of"] = member_physical_entity_of + protein_features["display_name"] = display_name + protein_features["available_names"] = "_".join(names) + protein_features["stId"] = stId + protein_features["uniprot_ac"] = "_".join(uniprot_acs) + proteins.append(protein_features) + return proteins + + @staticmethod + def get_control_information(biopax_obj: Any) -> List[Dict[str, Any]]: + reaction_controllers: List[Dict[str, Any]] = [] + for pathway_step_obj in biopax_obj.get_objects_by_type(pybiopax.biopax.Control): + reaction_controller: Dict[str, Any] = {} + control_type = pathway_step_obj.control_type + controlled = pathway_step_obj.controlled.display_name + controlled_of = pathway_step_obj.controlled_of + controller = pathway_step_obj.controller + controller_stid = [] + for controller_obj in pathway_step_obj.controller: + for x in controller_obj.xref: + if str(x.id).startswith("R-HSA-"): + controller_stid.append(x.id) + display_name = pathway_step_obj.display_name + get_plain_names = pathway_step_obj.get_plain_names() + interaction_type = pathway_step_obj.interaction_type + participant = pathway_step_obj.participant + participant_of = pathway_step_obj.participant_of + pathway_component_of = pathway_step_obj.pathway_component_of + standard_name = pathway_step_obj.standard_name + evidence = pathway_step_obj.evidence + step_process_of = pathway_step_obj.step_process_of + reaction_controller['control_type'] = control_type + reaction_controller['controlled'] = controlled + reaction_controller['controlled_of'] = controlled_of + reaction_controller['controller'] = controller + reaction_controller['controller_stid'] = controller_stid + reaction_controller['participant'] = participant + reaction_controller['pathway_component_of'] = pathway_component_of + reaction_controller['evidence'] = evidence + reaction_controller['control_type'] = control_type + reaction_controller['interaction_type'] = interaction_type + reaction_controllers.append(reaction_controller) + return reaction_controllers + + @staticmethod + def get_catalysis(biopax_obj: Any) -> Dict[str, List[Any]]: + all_catalysis: Dict[str, List[Any]] = defaultdict(list) + + for pathway_step_obj in biopax_obj.get_objects_by_type(pybiopax.biopax.Catalysis): + catalysis_direction = pathway_step_obj.catalysis_direction + cofactor = pathway_step_obj.cofactor + control_type = pathway_step_obj.control_type + controlled = pathway_step_obj.controlled.display_name + controlled_of = pathway_step_obj.controlled_of + controller = pathway_step_obj.controller + display_name = pathway_step_obj.display_name + interaction_type = pathway_step_obj.interaction_type + participant = pathway_step_obj.participant + pathway_component_of = pathway_step_obj.pathway_component_of + standard_name = pathway_step_obj.standard_name + evidence = pathway_step_obj.evidence + + all_catalysis["display_name"].append(display_name) + all_catalysis["standard_name"].append(standard_name) + all_catalysis["catalysis_direction"].append(catalysis_direction) + all_catalysis["cofactor"].append(cofactor) + all_catalysis["control_type"].append(control_type) + all_catalysis["controlled"].append(controlled) + all_catalysis["controlled_of"].append(controlled_of) + all_catalysis["controller"].append(controller) + all_catalysis["participant"].append(participant) + all_catalysis["pathway_component_of"].append(pathway_component_of) + all_catalysis["evidence"].append(evidence) + all_catalysis["interaction_type"].append(interaction_type) + + return dict(all_catalysis) + + @staticmethod + def get_pathway(biopax_obj: Any) -> None: + # This function in the original script prints debug information and + # returns nothing. It has been retained for completeness but does not + # contribute to the data collection. + for pathway_step_obj in biopax_obj.get_objects_by_type(pybiopax.biopax.Pathway): + dir(pathway_step_obj) + return None + + @staticmethod + def get_biochemical(biopax_obj: Any) -> List[Dict[str, Any]]: + all_reactions_parameters: List[Dict[str, Any]] = [] + for pathway_step_obj in biopax_obj.get_objects_by_type(pybiopax.biopax.BiochemicalReaction): + reaction_parameters: Dict[str, Any] = {} + Name = pathway_step_obj.name + reaction_name = " ".join(Name) + Display_Name = pathway_step_obj.display_name + Left = pathway_step_obj.left + Right = pathway_step_obj.right + Participant = pathway_step_obj.participant + Evidence = pathway_step_obj.evidence + Standard_Name = pathway_step_obj.standard_name + Conversion_Direction = pathway_step_obj.conversion_direction + Delta_G = pathway_step_obj.delta_g + Delta_H = pathway_step_obj.delta_h + Delta_S = pathway_step_obj.delta_s + K_eq = pathway_step_obj.k_e_q + Pathway_Component_Of = pathway_step_obj.pathway_component_of + pathways_component: List[str] = [] + if Pathway_Component_Of: + for path_name in list(Pathway_Component_Of): + pathways_component.append(path_name.display_name) + reaction_parameters['Left'] = Left + reaction_parameters['Right'] = Right + reaction_parameters['Participant'] = Participant + reaction_parameters['Pathway_Component_Of'] = pathways_component + reaction_parameters['Pathways_component'] = pathways_component + reaction_parameters['Evidence'] = Evidence + reaction_parameters['Standard_Name'] = Standard_Name + reaction_parameters['Conversion_Direction'] = Conversion_Direction + reaction_parameters['K_eq'] = K_eq + reaction_parameters['Delta_G'] = Delta_G + reaction_parameters['Delta_H'] = Delta_H + reaction_parameters['Delta_S'] = Delta_S + all_reactions_parameters.append(reaction_parameters) + return all_reactions_parameters + diff --git a/tools/reactome_to_mavisp/reactome_pipeline/uniprot_utils.py b/tools/reactome_to_mavisp/reactome_pipeline/uniprot_utils.py new file mode 100644 index 0000000..e31ee38 --- /dev/null +++ b/tools/reactome_to_mavisp/reactome_pipeline/uniprot_utils.py @@ -0,0 +1,101 @@ +import re +from typing import List, Optional + +import pandas as pd +import requests + +class UniprotFunctions: + """Helper functions for UniProt lookups.""" + + @staticmethod + def uniprot_gene_to_uniprot_ac(gene_name: str) -> Optional[str]: + gene_name = re.sub(r"\(.*", "", str(gene_name)).strip() + + params = { + "query": f"gene:{gene_name}", + "fields": "accession,gene_names,protein_name,organism_name", + "format": "json" + } + + try: + response = requests.get( + "https://rest.uniprot.org/uniprotkb/search", + params=params, + timeout=20 + ) + + if response.status_code != 200: + return None + + data = response.json() + + for entry in data.get("results", []): + if ( + entry.get("organism", {}).get("scientificName") == "Homo sapiens" + and entry.get("entryType") == "UniProtKB reviewed (Swiss-Prot)" + ): + return entry.get("primaryAccession") + + return None + + except requests.exceptions.RequestException: + return None + + @staticmethod + def uniprot_id_to_uniprot_ac(uniprot_id: str) -> Optional[str]: + uniprot_url = f"https://rest.uniprot.org/uniprotkb/{uniprot_id}.json" + uniprot_response = requests.get(uniprot_url) + if uniprot_response.status_code == 200: + gene_information = uniprot_response.json() + if gene_information['organism']['scientificName'] == 'Homo sapiens' and gene_information['entryType'] == "UniProtKB reviewed (Swiss-Prot)": + return gene_information['primaryAccession'] + return None + else: + return None + @staticmethod + def uniprot_ac_to_protein_name(uniprot_ac: str) -> Optional[str]: + uniprot_url = f"https://rest.uniprot.org/uniprotkb/{uniprot_ac}.json" + + try: + response = requests.get(uniprot_url, timeout=20) + + if response.status_code != 200: + return None + + data = response.json() + + protein_description = data.get("proteinDescription", {}) + + recommended_name = protein_description.get("recommendedName", {}) + full_name = recommended_name.get("fullName", {}) + + protein_name = full_name.get("value") + + if protein_name: + return protein_name + + submission_names = protein_description.get("submissionNames", []) + if submission_names: + return submission_names[0].get("fullName", {}).get("value") + + return None + + except requests.exceptions.RequestException: + return None + + @staticmethod + def process_gene_names(gene_names: str) -> str: + if pd.isna(gene_names) or not isinstance(gene_names, str): + return "None_uniprot_ac" + genes = gene_names.split('_') + accessions: List[str] = [] + for gene in genes: + single_proteins = gene.split("-") + for protein in single_proteins: + try: + accession = UniprotFunctions.uniprot_gene_to_uniprot_ac(protein.strip().upper()) + if accession and accession not in accessions: + accessions.append(str(accession)) + except Exception: + pass + return "_".join(accessions) \ No newline at end of file diff --git a/tools/reactome_to_mavisp/reactome_pipeline/workflow.py b/tools/reactome_to_mavisp/reactome_pipeline/workflow.py new file mode 100644 index 0000000..275e6fb --- /dev/null +++ b/tools/reactome_to_mavisp/reactome_pipeline/workflow.py @@ -0,0 +1,660 @@ +import os +from collections import defaultdict +from typing import Any, Dict, List, Set, Tuple + + +import pandas as pd +import networkx as nx + +try: + import reactome2py as rc + from reactome2py import analysis, content, utils +except ImportError: + rc = None # type: ignore + +try: + import pybiopax +except ImportError: + pybiopax = None # type: ignore + +from reactome_pipeline.graph_utils import PathwayGraphFunctions +from reactome_pipeline.data_processing import DataProcessingFunctions +from reactome_pipeline.reactome_analysis import ReactomeAnalysisFunctions +from reactome_pipeline.uniprot_utils import UniprotFunctions + +class ReactomeScript: + """Encapsulate the original script's workflow in an object oriented form.""" + + def __init__( + self, + uniprot_ac: str, + skip_pathway_order: bool = False, + output_dir: str = "." + ) -> None: + self.uniprot_ac = uniprot_ac + self.skip_pathway_order = skip_pathway_order + self.output_dir = output_dir + os.makedirs(self.output_dir, exist_ok=True) + + def output_path(self, *parts: str) -> str: + return os.path.join(self.output_dir, *parts) + + def retrieve_reactome_pathways(self) -> List[Dict[str, Any]]: + return ReactomeAnalysisFunctions.safe_reactome_call( + rc.content.mapping, + id=self.uniprot_ac, + resource="UniProt", + species="9606", + by="pathways", + default=[] + ) + def resolve_target_information(self) -> Dict[str, Any]: + """ + Retrieve target-related information. + + search_fireworks is used only as an optional extra check. + If it fails, the workflow continues using: + 1. UniProt-derived protein name. + 2. Reactome complexes associated with the UniProt accession. + 3. Direct UniProt accession matching inside BioPAX reaction models. + """ + target_protein_stId: List[str] = [] + target_protein_stId_other: List[str] = [] + + # Primary target name source: UniProt. + uniprot_target_name = UniprotFunctions.uniprot_ac_to_protein_name( + self.uniprot_ac + ) + + # Optional target name source: Reactome search_fireworks. + reactome_target_name = None + + target_protein_info = ReactomeAnalysisFunctions.safe_reactome_call( + rc.content.search_fireworks, + query=self.uniprot_ac, + default={"entries": []} + ) + + entries = target_protein_info.get("entries", []) if target_protein_info else [] + + if not entries: + print( + f"[WARNING] search_fireworks failed or returned no entries for " + f"{self.uniprot_ac}. Continuing without Reactome protein-form IDs." + ) + + for entry in entries: + if entry.get("stId"): + target_protein_stId.append(entry["stId"]) + + if reactome_target_name is None: + reactome_target_name = ( + entry.get("displayName") + or entry.get("name") + ) + + # Final target name priority: + # 1. UniProt protein name + # 2. Reactome display/name + # 3. UniProt accession fallback + target_name = ( + uniprot_target_name + or reactome_target_name + or self.uniprot_ac + ) + + # Optional extra check: alternative Reactome forms of the target protein. + for prot_id in target_protein_stId: + other_forms = ReactomeAnalysisFunctions.safe_reactome_call( + rc.content.entity_other_form, + id=prot_id, + default=[] + ) + + if other_forms: + for form in other_forms: + if form.get("stId"): + target_protein_stId_other.append(form["stId"]) + + all_target_protein_stId = list( + set(target_protein_stId + target_protein_stId_other) + ) + + # Main useful information: complexes associated with the UniProt accession. + target_protein_complexes_names: List[str] = [] + + complexes_list = ReactomeAnalysisFunctions.safe_reactome_call( + rc.content.entities_complexes, + id=self.uniprot_ac, + resource="UniProt", + default=[] + ) + + for complex_entry in complexes_list: + if complex_entry.get("stId"): + target_protein_complexes_names.append(complex_entry["stId"]) + + return { + "target_name": target_name, + "target_protein_stId": target_protein_stId, + "target_protein_stId_other": target_protein_stId_other, + "all_target_protein_stId": all_target_protein_stId, + "target_protein_complexes_names": target_protein_complexes_names, + } + def write_pathway_ordering(self, reactome_pathways: List[Dict[str, Any]]) -> None: + """ + Build and write reaction ordering information for each Reactome pathway. + """ + reactions_order: Dict[str, Dict[str, List[Dict[str, List[str]]]]] = {} + + for i in reactome_pathways: + biopax_model = ReactomeAnalysisFunctions.safe_model_from_reactome_cached(i['stId']) + + if biopax_model is None: + print(f"[WARNING] Skipping pathway order for {i['stId']}: BioPAX unavailable.") + continue + + reaction_order = ReactomeAnalysisFunctions.get_pathway_order(biopax_model) + reactions_order[i['stId']] = reaction_order + + for hrsa, reac in reactions_order.items(): + reactions_lists: List[Tuple[str, str, str]] = [] + + for key, value_list in reac.items(): + for value in value_list: + next_steps = value.get('next_step', []) + previous_steps = value.get('previous', []) + + if next_steps and previous_steps: + for next_step in next_steps: + for next_item in next_step: + for previous_step in previous_steps: + for prev_item in previous_step: + reactions_lists.append((key, next_item, prev_item)) + + if not next_steps and previous_steps: + for previous_step in previous_steps: + for prev_item in previous_step: + reactions_lists.append((key, "", prev_item)) + + if next_steps and not previous_steps: + for next_step in next_steps: + for next_item in next_step: + reactions_lists.append((key, next_item, "")) + + G, starting_nodes, ending_nodes, edge_rows, node_rows = PathwayGraphFunctions.make_pathway_graph( + reactions_lists, + hrsa + ) + + pathway_output_dir = self.output_path("pathways_order", hrsa) + os.makedirs(pathway_output_dir, exist_ok=True) + + pd.DataFrame(edge_rows).to_csv( + os.path.join(pathway_output_dir, "graph_edges.csv"), + index=False + ) + + pd.DataFrame(node_rows).to_csv( + os.path.join(pathway_output_dir, "graph_nodes.csv"), + index=False + ) + + simple_paths: List[List[str]] = [] + + for start_node in starting_nodes: + for end_node in ending_nodes: + if start_node != end_node and start_node in G.nodes and end_node in G.nodes: + paths = list(nx.all_simple_paths(G, source=start_node, target=end_node)) + simple_paths.extend(paths) + + paths_to_process = PathwayGraphFunctions.remove_duplicates_order(simple_paths) + filtered_paths = PathwayGraphFunctions.find_non_subsequences(paths_to_process) + + if filtered_paths: + ordered_paths_rows = [] + + for path_id, path in enumerate(filtered_paths): + for step, reaction in enumerate(path): + ordered_paths_rows.append({ + "pathway_id": hrsa, + "path_id": path_id, + "step": step, + "reaction": reaction + }) + + pd.DataFrame(ordered_paths_rows).to_csv( + os.path.join(pathway_output_dir, "ordered_paths.csv"), + index=False + ) + + def collect_candidate_reactions( + self, + reactome_pathways: List[Dict[str, Any]], + skipped_reactions: List[Dict[str, str]] + ) -> Tuple[List[Dict[int, Any]], List[str]]: + """ + Retrieve candidate reactions from the lowest-level Reactome pathways. + + Returns: + pathways_with_reactions: + Pathway hierarchy dictionaries with an added 'reactions' field. + pathways_to_not_analyze: + Lowest-level pathway names used to avoid treating pathways as reactions. + """ + hierarchy_to_low_level = ReactomeAnalysisFunctions.top_level_pathways(reactome_pathways) + + # ------------------------------ Filter steps ----------------------------- # + + # Lowest-level pathway names are excluded later to avoid treating pathway + # containers as actual reactions. + + pathways_to_not_analyze: List[str] = [] + for dic in hierarchy_to_low_level: + last_pathway = list(dic.keys())[-1] + pathways_to_not_analyze.append(dic[last_pathway]['name']) + + # Retrieve and keep only true reaction events from each lowest-level pathway. + + pathways_with_reactions: List[Dict[int, Any]] = [] + for dic in hierarchy_to_low_level: + last_pathway = list(dic.keys())[-1] + std_id = dic[last_pathway]['id'] + reactions = ReactomeAnalysisFunctions.safe_reactome_call( + rc.content.pathway_contained_event, + id=std_id, + default=[] + ) + filtered_reactions: List[Dict[str, Any]] = [] + for reaction in reactions: + + if not isinstance(reaction, int) and reaction['displayName'] not in pathways_to_not_analyze: + filtered_reactions.append(reaction) + elif isinstance(reaction, int): + rhsa = "R-HSA-" + str(reaction) + reaction = ReactomeAnalysisFunctions.safe_reactome_call( + rc.content.query_id, + id=rhsa, + enhanced=False, + default=None + ) + if reaction is None: + print(f"[WARNING] Skipping reaction {rhsa}: Reactome returned no usable data.") + skipped_reactions.append({ + "uniprot_ac": self.uniprot_ac, + "reaction_id": rhsa, + "pathway_id": std_id, + "pathway_name": dic[last_pathway].get("name", ""), + "reason": "query_id_returned_none_after_retries" + }) + + continue + if reaction.get('displayName') not in pathways_to_not_analyze and reaction.get('className') == "Reaction": + filtered_reactions.append(reaction) + dic['reactions'] = filtered_reactions + pathways_with_reactions.append(dic) + return pathways_with_reactions, pathways_to_not_analyze + + def is_target_reaction( + self, + reaction: Dict[str, Any], + target_info: Dict[str, Any] + ) -> bool: + """ + Return True if the reaction contains the target protein, one of its + Reactome protein forms, or one of its associated complexes. + """ + rsa = reaction["stId"] + + reaction_cache_key = (self.uniprot_ac, rsa) + + if reaction_cache_key in ReactomeAnalysisFunctions.reaction_target_cache: + return ReactomeAnalysisFunctions.reaction_target_cache[ + reaction_cache_key + ] + + is_target_reaction = False + + # Retrieve physical entities participating in the reaction. + components = ReactomeAnalysisFunctions.safe_reactome_call( + rc.content.participants_physical_entities, + rsa, + default=[] + ) + + components_name: List[str] = [] + for component in components: + if component and component.get("stId"): + components_name.append(component["stId"]) + + # Detect reactions containing any complex associated with the target protein. + if any( + target in component_id + for component_id in components_name + for target in target_info["target_protein_complexes_names"] + ): + is_target_reaction = True + + # Detect reactions containing any Reactome form of the target protein. + reaction_proteins = ReactomeAnalysisFunctions.safe_model_from_reactome_cached(rsa) + + if reaction_proteins is not None: + protein_stid: List[str] = [] + + for protein in reaction_proteins.get_objects_by_type(pybiopax.biopax.Protein): + for protein_ref in getattr(protein, "xref", []): + protein_ref_id = getattr(protein_ref, "id", "") + + if protein_ref_id.startswith("R-HSA"): + protein_stid.append(protein_ref_id) + + if any( + target in protein_id + for protein_id in protein_stid + for target in target_info["all_target_protein_stId"] + ): + is_target_reaction = True + + if ReactomeAnalysisFunctions.biopax_model_contains_uniprot( + reaction_proteins, + self.uniprot_ac + ): + is_target_reaction = True + + ReactomeAnalysisFunctions.reaction_target_cache[ + reaction_cache_key + ] = is_target_reaction + + return is_target_reaction + + + + def extract_target_reaction_information( + self, + pathways_with_reactions: List[Dict[int, Any]], + pathways_to_not_analyze: List[str], + target_info: Dict[str, Any], + skipped_reactions: List[Dict[str, str]] + ) -> List[Dict[Any, Any]]: + """ + Filter candidate reactions for the target protein and extract BioPAX-level + reaction information. + """ + pathways_with_reactions_information: List[Dict[Any, Any]] = [] + for pathway in pathways_with_reactions: + pathways_with_reaction_information: Dict[Any, Any] = {} + pathways_keys = [key for key in pathway.keys() if key != 'reactions'] + reactions_to_analyze: List[Dict[str, Any]] = [] + unique_reactions = { + reaction["stId"]: reaction + for reaction in pathway["reactions"] + if reaction and reaction.get("stId") + } + + reactions = list(unique_reactions.values()) + + for reaction in reactions: + if self.is_target_reaction( + reaction, + target_info + ): + reactions_to_analyze.append(reaction) + + # Extract BioPAX-level annotations for target reactions. + + reactions_information: List[Dict[str, Any]] = [] + + for reaction in reactions_to_analyze: + reaction_stID = reaction["stId"] + + reaction_info_cache_key = ( + reaction_stID, + tuple(sorted(pathways_to_not_analyze)) + ) + + if reaction_info_cache_key in ReactomeAnalysisFunctions.reaction_information_cache: + reactions_information.extend( + ReactomeAnalysisFunctions.reaction_information_cache[reaction_info_cache_key] + ) + continue + + reaction_biopax_model = ReactomeAnalysisFunctions.safe_model_from_reactome_cached( + reaction_stID + ) + + if reaction_biopax_model is None: + print(f"[WARNING] Skipping reaction {reaction_stID}: BioPAX unavailable.") + skipped_reactions.append({ + "uniprot_ac": self.uniprot_ac, + "reaction_id": reaction_stID, + "pathway_id": pathway[pathways_keys[-1]].get("id", ""), + "pathway_name": pathway[pathways_keys[-1]].get("name", ""), + "reason": "biopax_unavailable_after_retries" + }) + ReactomeAnalysisFunctions.reaction_information_cache[reaction_info_cache_key] = [] + continue + + reaction_information_list = ReactomeAnalysisFunctions.build_reaction_information( + reaction, + reaction_biopax_model, + pathways_to_not_analyze + ) + + ReactomeAnalysisFunctions.reaction_information_cache[reaction_info_cache_key] = reaction_information_list + reactions_information.extend(reaction_information_list) + for key in pathways_keys: + pathways_with_reaction_information[key] = pathway[key] + pathways_with_reaction_information["reaction"] = reactions_information + pathways_with_reactions_information.append(pathways_with_reaction_information) + return pathways_with_reactions_information + + def remove_duplicate_pathway_reactions( + self, + result_df: pd.DataFrame + ) -> pd.DataFrame: + """ + Remove duplicated reaction rows caused by overlapping Reactome pathway + hierarchies. + + When the same reaction appears in multiple nested pathways, the row + associated with the shorter, less specific pathway path is removed. + """ + grouped = result_df.groupby("reaction_name") + paths_to_remove: List[List[str]] = [] + for reaction in grouped.groups.keys(): + reaction_df = grouped.get_group(reaction) + # list of R-HSA id of the lowest-level pathway + lowest_pathway_id = list(set(reaction_df["lowest_pathway_id"].to_list())) + # list of R-HSA id of reaction + reaction_id = list(set(reaction_df["reaction_id"].to_list())) + # find the duplicated reactions + reaction_dict: Dict[str, Set[str]] = defaultdict(set) + if len(lowest_pathway_id) != len(reaction_id): + # create a dictionary with the reaction name and the R-HSA id of + # the lowest pathway that contains the same reaction + for q in zip(reaction_df["lowest_pathway_id"].to_list(), reaction_df["reaction_id"].to_list()): + reaction_dict[q[1]].add(q[0]) + reaction_with_multiple_pathways = {k: list(v) for k, v in reaction_dict.items() if len(v) > 1} + + # for each double reaction and R-HSA id of the lowest pathway to compare: + # extract the df and build a string with all the R-HSA ids of the pathways from that reaction, + # put the two strings to be compared in a list + + for reaction_id_val, pathways in reaction_with_multiple_pathways.items(): + paths_to_check: List[str] = [] + for pathway in pathways: + df_for_comparison = result_df[(result_df["reaction_name"] == reaction) & (result_df["reaction_id"] == reaction_id_val) & (result_df["lowest_pathway_id"] == pathway)] + df_for_comparison = df_for_comparison.astype(str) + pathway_cols = [col for col in result_df.columns if "pathway" in col and "id" in col] + pathway_cols.append("reaction_id") + for _, row in df_for_comparison.iterrows(): + combined_pathway = "_".join(row[pathway_cols].dropna()) + paths_to_check.append(combined_pathway) + paths_to_remove.append(list(set(paths_to_check))) + + # Identify the shorter duplicated pathway path and mark its + # lowest_pathway_id/reaction_id pair for removal. + + lines_to_remove: List[Tuple[str, str]] = [] + for paths in paths_to_remove: + compare_pathways = [path.split("_") for path in paths] + processed_pathways: List[List[str]] = [] + for pathway in compare_pathways: + pathway_no_nan: List[str] = [] + for reaction_elem in pathway: + if reaction_elem != "nan": + pathway_no_nan.append(reaction_elem) + processed_pathways.append(pathway_no_nan) + to_remove = min(processed_pathways, key=len) + lines_to_remove.append((to_remove[-2], to_remove[-1])) + + result_df_filtered = result_df[~result_df.apply(lambda row: (row["lowest_pathway_id"], row["reaction_id"]) in set(lines_to_remove), axis=1)] + + return result_df_filtered + + def order_reactions_by_pathway_files( + self, + result_df_filtered: pd.DataFrame + ) -> pd.DataFrame: + """ + Reorder reactions using pathway ordering files when available. + + If no ordering file exists for a pathway, rows are kept in their original + order and marked as unordered. + """ + ordered_result_df_filtered = pd.DataFrame() + + for stid in set(result_df_filtered["lowest_pathway_id"].dropna().tolist()): + + ordered_paths_file = self.output_path( + "pathways_order", + stid, + "ordered_paths.csv" + ) + + if os.path.exists(ordered_paths_file): + ordered_paths_df = pd.read_csv(ordered_paths_file) + + df_sorted = DataProcessingFunctions.reorder_dataframe_from_ordered_paths( + result_df_filtered, + stid, + ordered_paths_df + ) + + else: + df_sorted = result_df_filtered[ + result_df_filtered["lowest_pathway_id"] == stid + ].copy() + df_sorted["ordered"] = False + + ordered_result_df_filtered = pd.concat( + [ordered_result_df_filtered, df_sorted], + ignore_index=True + ) + return ordered_result_df_filtered + + + def build_and_write_result( + self, + pathways_with_reactions_information: List[Dict[Any, Any]], + target_info: Dict[str, Any], + skipped_reactions: List[Dict[str, str]] + ) -> bool: + """ + Build the final result DataFrame, clean duplicated pathway rows, + order reactions when pathway ordering is available, and write outputs. + """ + if skipped_reactions: + pd.DataFrame(skipped_reactions).drop_duplicates().to_csv( + self.output_path("skipped_reactions.csv"), + index=False + ) + result_df = DataProcessingFunctions.process_data(pathways_with_reactions_information) + if result_df.empty: + print(f"[INFO] No Reactome reactions found for {self.uniprot_ac}.") + return False + + sorted_columns = sorted(result_df.columns, key=DataProcessingFunctions.column_sort_key) + result_df = result_df[sorted_columns] + + # Propagate complex and stoichiometry annotations within each reaction group. + if "complex_of" in result_df.columns: + result_df = result_df.groupby("reaction_name", group_keys=False).apply(DataProcessingFunctions.fill_complex_info) + + # Remove rows representing protein families rather than individual proteins. + result_df = result_df.loc[result_df['is_a_protein_family'] == False] + if result_df.empty: + print( + f"[INFO] No Reactome reactions left for {self.uniprot_ac} " + "after removing protein families." + ) + return False + + result_df_filtered = self.remove_duplicate_pathway_reactions(result_df) + + ordered_result_df_filtered = self.order_reactions_by_pathway_files( + result_df_filtered + ) + + if ordered_result_df_filtered.empty: + print(f"[INFO] Final Reactome output is empty for {self.uniprot_ac}.") + return False + + ordered_result_df_filtered.insert( + 0, + "target_uniprot_ac", + self.uniprot_ac + ) + + ordered_result_df_filtered.insert( + 1, + "target_name", + target_info["target_name"] + ) + + ordered_result_df_filtered.to_csv( + self.output_path("result.csv"), + sep=",", + index=False + ) + + return True + + def run(self) -> bool: + """Execute the full Reactome analysis workflow.""" + if rc is None or pybiopax is None: + raise ImportError( + "reactome2py and pybiopax must be installed to run this script" + ) + + skipped_reactions: List[Dict[str, str]] = [] + + reactome_pathways = self.retrieve_reactome_pathways() + + if not self.skip_pathway_order: + self.write_pathway_ordering(reactome_pathways) + + target_info = self.resolve_target_information() + + pathways_with_reactions, pathways_to_not_analyze = ( + self.collect_candidate_reactions( + reactome_pathways, + skipped_reactions + ) + ) + + pathways_with_reactions_information = ( + self.extract_target_reaction_information( + pathways_with_reactions, + pathways_to_not_analyze, + target_info, + skipped_reactions + ) + ) + + return self.build_and_write_result( + pathways_with_reactions_information, + target_info, + skipped_reactions + ) + \ No newline at end of file diff --git a/tools/reactome_to_mavisp/reactome_post_process.py b/tools/reactome_to_mavisp/reactome_post_process.py new file mode 100644 index 0000000..daaadc2 --- /dev/null +++ b/tools/reactome_to_mavisp/reactome_post_process.py @@ -0,0 +1,356 @@ +#!/usr/bin/env python3 + +from __future__ import annotations + +import argparse +import re +from pathlib import Path +from typing import Dict, List, Optional + +import pandas as pd + + +MERGED_REACTION_BASE_COLUMNS = [ + "target_uniprot_ac", + "target_name", + "highest_pathway", + "highest_pathway_id", + "disease_name", +] + +MERGED_REACTION_TAIL_COLUMNS = [ + "lowest_pathway", + "lowest_pathway_id", + "reaction_name", + "reaction_id", + "reaction_Left", + "reaction_Right", + "reaction_Conversion_Direction", +] + +HIGHEST_PATHWAY_COLUMNS = [ + "highest_pathway", + "highest_pathway_id", + "target_uniprot_ac", + "target_name", +] + + +def parse_arguments() -> argparse.Namespace: + parser = argparse.ArgumentParser( + description=( + "Post-process Reactome result.csv files generated inside " + "UniProt-specific folders. The script creates: " + "merged_reaction.csv, merged_highest_pathways.csv, and " + "disease_single_sequence_site.csv." + ) + ) + + parser.add_argument( + "-i", + "--input_dir", + required=True, + type=str, + help="Main Reactome output directory containing UniProt-specific folders." + ) + + parser.add_argument( + "-o", + "--output_dir", + required=False, + default=None, + type=str, + help=( + "Directory where merged output files will be written. " + "Default: same as --input_dir." + ) + ) + + parser.add_argument( + "--result_filename", + required=False, + default="result.csv", + type=str, + help="Name of the result file inside each UniProt folder. Default: result.csv" + ) + + return parser.parse_args() + + +def find_result_files(input_dir: Path, result_filename: str) -> List[Path]: + """ + Find result files directly inside first-level UniProt subfolders. + + Example: + input_dir/P04637/result.csv + input_dir/Q8N726/result.csv + """ + return sorted(input_dir.glob(f"*/{result_filename}")) + + +def infer_target_uniprot_ac(result_file: Path) -> str: + """Infer the target UniProt accession from the parent folder name.""" + return result_file.parent.name + + +def harmonize_target_columns(df: pd.DataFrame, result_file: Path) -> pd.DataFrame: + """ + Ensure target_uniprot_ac and target_name are present. + + Priority for target_uniprot_ac: + 1. existing target_uniprot_ac column + 2. existing uniprot_ac column + 3. parent folder name + + Priority for target_name: + 1. existing target_name column + 2. existing protein column + 3. empty string + """ + df = df.copy() + + if "target_uniprot_ac" not in df.columns: + if "uniprot_ac" in df.columns: + df["target_uniprot_ac"] = df["uniprot_ac"] + else: + df["target_uniprot_ac"] = infer_target_uniprot_ac(result_file) + + if "target_name" not in df.columns: + if "protein" in df.columns: + df["target_name"] = df["protein"] + else: + df["target_name"] = "" + + return df + + +def load_result_files(result_files: List[Path]) -> pd.DataFrame: + dfs: List[pd.DataFrame] = [] + + for result_file in result_files: + try: + df = pd.read_csv(result_file) + + if df.empty: + print(f"[INFO] Skipping empty file: {result_file}") + continue + + df = harmonize_target_columns(df, result_file) + df["source_result_file"] = str(result_file) + df["source_folder"] = result_file.parent.name + dfs.append(df) + + except Exception as error: + print(f"[WARNING] Could not read {result_file}: {error}") + + if not dfs: + return pd.DataFrame() + + return pd.concat(dfs, ignore_index=True) + + +def get_pathway_columns(df: pd.DataFrame) -> List[str]: + """ + Return pathway_1/pathway_1_id ... pathway_n/pathway_n_id in numeric order. + """ + pathway_numbers = set() + + for column in df.columns: + match = re.fullmatch(r"pathway_(\d+)(_id)?", str(column)) + if match: + pathway_numbers.add(int(match.group(1))) + + pathway_columns: List[str] = [] + + for number in sorted(pathway_numbers): + pathway_col = f"pathway_{number}" + pathway_id_col = f"pathway_{number}_id" + + if pathway_col in df.columns: + pathway_columns.append(pathway_col) + + if pathway_id_col in df.columns: + pathway_columns.append(pathway_id_col) + + return pathway_columns + + +def select_existing_columns(df: pd.DataFrame, columns: List[str]) -> pd.DataFrame: + """ + Select requested columns, creating missing columns as empty strings. + + This makes the script robust if one result.csv lacks a specific column. + """ + df = df.copy() + + for column in columns: + if column not in df.columns: + df[column] = "" + + return df[columns] + + +def find_sequence_site_column(df: pd.DataFrame) -> Optional[str]: + """ + Find sequence_site column independently of case/style. + + It accepts: + sequence_site + SequenceSite + """ + candidates: Dict[str, str] = { + str(col).lower(): str(col) + for col in df.columns + } + + for key in ["sequence_site", "sequencesite"]: + if key in candidates: + return candidates[key] + + return None + + +def is_single_numeric_sequence_site(value: object) -> bool: + """ + True only for a single integer-like residue position. + + Accepted: + 330 + "330" + + Rejected: + "330_331" + "330-331" + "330;331" + empty / NaN + """ + if pd.isna(value): + return False + + value_str = str(value).strip() + + return bool(re.fullmatch(r"\d+", value_str)) + + +def write_outputs(concatenated_df: pd.DataFrame, output_dir: Path) -> None: + output_dir.mkdir(parents=True, exist_ok=True) + + pathway_columns = get_pathway_columns(concatenated_df) + + merged_reaction_columns = ( + MERGED_REACTION_BASE_COLUMNS + + pathway_columns + + MERGED_REACTION_TAIL_COLUMNS + ) + + merged_reaction_df = select_existing_columns( + concatenated_df, + merged_reaction_columns + ).drop_duplicates() + + merged_reaction_file = output_dir / "merged_reaction.csv" + + merged_reaction_df.to_csv( + merged_reaction_file, + index=False + ) + + merged_highest_pathways_df = select_existing_columns( + concatenated_df, + HIGHEST_PATHWAY_COLUMNS + ).drop_duplicates() + + merged_highest_pathways_file = output_dir / "merged_highest_pathways.csv" + + merged_highest_pathways_df.to_csv( + merged_highest_pathways_file, + index=False + ) + + sequence_site_column = find_sequence_site_column(concatenated_df) + + if sequence_site_column is None: + print( + "[WARNING] No sequence_site/SequenceSite column found. " + "Writing empty disease_single_sequence_site.csv." + ) + disease_single_sequence_site_df = concatenated_df.iloc[0:0].copy() + + else: + disease_single_sequence_site_df = concatenated_df[ + ( + concatenated_df["highest_pathway"] + .astype(str) + .str.strip() + .eq("Disease") + ) + & concatenated_df[sequence_site_column].apply( + is_single_numeric_sequence_site + ) + ].copy().drop_duplicates() + + disease_single_sequence_site_file = output_dir / "disease_single_sequence_site.csv" + + disease_single_sequence_site_df.to_csv( + disease_single_sequence_site_file, + index=False + ) + + print(f"[INFO] Output written: {merged_reaction_file}") + print(f"[INFO] Rows: {len(merged_reaction_df)}") + + print(f"[INFO] Output written: {merged_highest_pathways_file}") + print(f"[INFO] Rows: {len(merged_highest_pathways_df)}") + + print(f"[INFO] Output written: {disease_single_sequence_site_file}") + print(f"[INFO] Rows: {len(disease_single_sequence_site_df)}") + + +def main() -> None: + args = parse_arguments() + + input_dir = Path(args.input_dir).resolve() + + if args.output_dir: + output_dir = Path(args.output_dir).resolve() + else: + output_dir = input_dir + + if not input_dir.exists(): + raise FileNotFoundError(f"Input directory does not exist: {input_dir}") + + if not input_dir.is_dir(): + raise NotADirectoryError(f"Input path is not a directory: {input_dir}") + + result_files = find_result_files( + input_dir=input_dir, + result_filename=args.result_filename + ) + + if not result_files: + print(f"[WARNING] No {args.result_filename} files found inside {input_dir}") + return + + concatenated_df = load_result_files(result_files) + + if concatenated_df.empty: + print("[WARNING] No valid result files could be loaded.") + return + + if "highest_pathway" not in concatenated_df.columns: + raise ValueError("Missing required column: highest_pathway") + + write_outputs( + concatenated_df=concatenated_df, + output_dir=output_dir + ) + + print(f"[INFO] Found result files: {len(result_files)}") + print( + "[INFO] Concatenated rows before filtering/deduplication: " + f"{len(concatenated_df)}" + ) + + +if __name__ == "__main__": + main() \ No newline at end of file diff --git a/tools/reactome_to_mavisp/reactome_to_mavisp.py b/tools/reactome_to_mavisp/reactome_to_mavisp.py new file mode 100644 index 0000000..1037aa4 --- /dev/null +++ b/tools/reactome_to_mavisp/reactome_to_mavisp.py @@ -0,0 +1,212 @@ +""" +Reactome analysis workflow for UniProt accessions. + +Given one UniProt accession, or a file containing multiple UniProt accessions, +the script queries Reactome to retrieve associated pathways, identifies +candidate reactions involving the target protein, extracts BioPAX-level +protein/complex/reaction annotations, and writes cleaned CSV outputs. + +Main outputs +------------ +For each UniProt accession, the script creates a dedicated output folder +containing: + +- result.csv + Final cleaned table of Reactome reactions involving the target protein. + +- skipped_reactions.csv + Reactions that could not be processed because Reactome or BioPAX returned + no usable data after retries. + +- pathways_order/ + Optional pathway-ordering files generated when pathway ordering is not + skipped. + +At the global output level, the script can also write: + +- entries_not_in_reactome.csv + UniProt accessions that were not found in Reactome or did not produce a + valid final output. + +Usage +----- +Run the workflow for one UniProt accession: + + python reac_classified.py -u P04637 + +Run the workflow for multiple UniProt accessions: + + python reac_classified.py -uf uniprot_list.txt + +Skip pathway ordering: + + python reac_classified.py -u P04637 -s +""" +from __future__ import annotations + +import os +import argparse +import shutil +from typing import Dict, List + +import pandas as pd + +try: + import reactome2py as rc + from reactome2py import analysis, content, utils +except ImportError: + rc = None # type: ignore + +from reactome_pipeline.reactome_analysis import ReactomeAnalysisFunctions +from reactome_pipeline.workflow import ReactomeScript + +def parse_arguments() -> argparse.Namespace: + """Parse command line arguments.""" + parser = argparse.ArgumentParser( + description=( + 'This script retrieves pathways associated with a UniProt accession from Reactome, ' + 'orders the reactions, collects protein level data and writes several CSV outputs. ' + 'It is a class‑based refactoring of the original reac.py script.' + ) + ) + parser.add_argument( + '-uf', '--uniprot_file', + dest='uniprot_file', + required=False, + type=str, + help='Text file containing one UniProt accession per line.' + ) + + parser.add_argument( + '-o', '--output_dir', + dest='output_dir', + default='reactome_outputs', + type=str, + help='Main output directory.' + ) + parser.add_argument( + '-u', '--uniprot_ac', dest='uniprot_ac', default='Q8N726', type=str, + help='UniProt accession of the protein of interest (default: Q8N726).' + ) + parser.add_argument( + '-s', '--skip_pathway_order', dest='skip_pathway_order', action='store_true', + help='Skip the pathway ordering phase (analogous to supplying --pdb_file in the original script).' + ) + return parser.parse_args() + +def read_uniprot_accessions(uniprot_file: str) -> List[str]: + accessions: List[str] = [] + + with open(uniprot_file) as handle: + for line in handle: + accession = line.strip() + + if not accession: + continue + + if accession.startswith("#"): + continue + + accessions.append(accession) + + return accessions + +def uniprot_has_reactome_entry(uniprot_ac: str) -> bool: + """ + Return True if Reactome contains at least one mapped pathway for the UniProt accession. + """ + if rc is None: + raise ImportError("reactome2py is required to query Reactome.") + + reactome_pathways = ReactomeAnalysisFunctions.safe_reactome_call( + rc.content.mapping, + id=uniprot_ac, + resource="UniProt", + species="9606", + by="pathways", + default=[] + ) + + return bool(reactome_pathways) + + +def write_missing_reactome_entries( + output_dir: str, + missing_entries: List[Dict[str, str]] +) -> None: + """ + Write UniProt accessions not producing a valid Reactome output. + """ + if not missing_entries: + return + + os.makedirs(output_dir, exist_ok=True) + + output_file = os.path.join(output_dir, "entries_not_in_reactome.csv") + + new_df = pd.DataFrame(missing_entries) + + if os.path.exists(output_file): + old_df = pd.read_csv(output_file) + final_df = pd.concat([old_df, new_df], ignore_index=True) + final_df = final_df.drop_duplicates(subset=["uniprot_ac", "status"]) + else: + final_df = new_df.drop_duplicates(subset=["uniprot_ac", "status"]) + + final_df.to_csv(output_file, index=False) + +def main() -> None: + args = parse_arguments() + + if args.uniprot_file: + uniprot_accessions = read_uniprot_accessions(args.uniprot_file) + else: + uniprot_accessions = [args.uniprot_ac] + + missing_reactome_entries: List[Dict[str, str]] = [] + + for uniprot_ac in uniprot_accessions: + print(f"[INFO] Running Reactome analysis for {uniprot_ac}") + + protein_output_dir = os.path.join(args.output_dir, uniprot_ac) + + # First check: is this UniProt accession known by Reactome? + if not uniprot_has_reactome_entry(uniprot_ac): + print(f"[INFO] {uniprot_ac} is not present in Reactome.") + + missing_reactome_entries.append({ + "uniprot_ac": uniprot_ac, + "status": "not_found_in_reactome", + "reason": "Reactome search_fireworks returned no entries" + }) + + continue + + workflow = ReactomeScript( + uniprot_ac=uniprot_ac, + skip_pathway_order=args.skip_pathway_order, + output_dir=protein_output_dir + ) + + has_valid_output = workflow.run() + + if not has_valid_output: + print(f"[INFO] Removing empty output folder for {uniprot_ac}") + + if os.path.isdir(protein_output_dir): + shutil.rmtree(protein_output_dir) + + missing_reactome_entries.append({ + "uniprot_ac": uniprot_ac, + "status": "no_valid_reactome_output", + "reason": "Reactome entry found, but no valid reactions remained after filtering" + }) + + write_missing_reactome_entries( + output_dir=args.output_dir, + missing_entries=missing_reactome_entries + ) + + +if __name__ == '__main__': + main() \ No newline at end of file