From 4a0bc39d58c5b848a698bf19bc5cd146ccbc1d11 Mon Sep 17 00:00:00 2001 From: Adam Wright Date: Wed, 11 Nov 2026 23:08:23 +0000 Subject: [PATCH] Find an interaction partner by its primary identifier, not another's alias InteractorsBuilder.getOrCreate looked a partner up in the container's IdentifiersMap, which also holds every interactor under its gene names and secondary identifiers. A partner whose identifier was another protein's alias was merged into that protein and never created, in an order that depended on hash iteration (#13). InteractorNodes now creates each interactor once per primary identifier, keyed as IdentifiersMap keys it (trimmed, upper-cased), and still records aliases in the map so analyses can be submitted with them. Tested; the alias test fails with the old lookup. Fixes #13 Co-Authored-By: Claude Opus 5.5 --- .../core/importer/InteractorNodes.java | 58 +++++++++++++++++++ .../core/importer/InteractorsBuilder.java | 28 ++------- .../core/importer/InteractorNodesTest.java | 56 ++++++++++++++++++ 3 files changed, 120 insertions(+), 22 deletions(-) create mode 100644 src/main/java/org/reactome/server/analysis/core/importer/InteractorNodes.java create mode 100644 src/test/java/org/reactome/server/analysis/core/importer/InteractorNodesTest.java diff --git a/src/main/java/org/reactome/server/analysis/core/importer/InteractorNodes.java b/src/main/java/org/reactome/server/analysis/core/importer/InteractorNodes.java new file mode 100644 index 00000000..b23458c9 --- /dev/null +++ b/src/main/java/org/reactome/server/analysis/core/importer/InteractorNodes.java @@ -0,0 +1,58 @@ +package org.reactome.server.analysis.core.importer; + +import org.reactome.server.analysis.core.model.IdentifiersMap; +import org.reactome.server.analysis.core.model.InteractorNode; +import org.reactome.server.analysis.core.model.resource.Resource; + +import java.util.HashMap; +import java.util.Map; + +/** + * The interactor nodes of the interactors container: created once for each + * primary identifier, and found in the container's map by that identifier and + * by every alias recorded for them (gene names, secondary identifiers), so an + * analysis can be submitted with any of them. + * + * An interaction partner is found by its primary identifier alone. Looking it + * up in the container's map, aliases included, found another protein whenever + * the partner's identifier was one of that protein's aliases, and the partner + * was never created: which of the two survived depended on the order the + * interactions were read in. Keys are trimmed and upper-cased, as + * IdentifiersMap keys them, so a primary identifier finds what it found before. + */ +class InteractorNodes { + + private final IdentifiersMap interactorsMap; + + private final Map> byPrimaryIdentifier = new HashMap<>(); + + private int created = 0; + + InteractorNodes(IdentifiersMap interactorsMap) { + this.interactorsMap = interactorsMap; + } + + /** The interactor for this primary identifier in this resource, created if there is none yet. */ + InteractorNode getOrCreate(Resource resource, String identifier) { + Map nodes = byPrimaryIdentifier.computeIfAbsent(resource, r -> new HashMap<>()); + String key = identifier.trim().toUpperCase(); + InteractorNode node = nodes.get(key); + if (node == null) { + node = new InteractorNode(identifier); + nodes.put(key, node); + interactorsMap.add(identifier, resource, node); + created++; + } + return node; + } + + /** Lets an analysis find this interactor by an alias as well; never used to find a partner. */ + void addAlias(String alias, Resource resource, InteractorNode node) { + interactorsMap.add(alias, resource, node); + } + + /** How many interactors have been created. */ + int created() { + return created; + } +} diff --git a/src/main/java/org/reactome/server/analysis/core/importer/InteractorsBuilder.java b/src/main/java/org/reactome/server/analysis/core/importer/InteractorsBuilder.java index 17ad17cc..8fb7bb83 100644 --- a/src/main/java/org/reactome/server/analysis/core/importer/InteractorsBuilder.java +++ b/src/main/java/org/reactome/server/analysis/core/importer/InteractorsBuilder.java @@ -33,8 +33,8 @@ public class InteractorsBuilder { //Will contain the RADIX-TREE with the map (identifiers -> [InteractorNode]) private IdentifiersMap interactorsMap = new IdentifiersMap<>(); - //Keeps track of the number of interactors that have been included - private int n = 0; + //Creates each interactor once, by its primary identifier, and records its aliases + private InteractorNodes interactorNodes = new InteractorNodes(interactorsMap); public void build(Set speciesNodes, EntitiesContainer entities, InteractionsService interactionsService) { if (Main.VERBOSE) System.out.print("Starting creation of the interactors container..."); @@ -108,7 +108,7 @@ public void build(Set speciesNodes, EntitiesContainer entities, Int Resource resource = ResourceFactory.getResource(interactsWith.getDatabaseName()); String variantIdentifier = interactor.fetchSingleValue("getVariantIdentifier"); String identifier = variantIdentifier != null ? variantIdentifier : interactor.getIdentifier(); - InteractorNode interactorNode = getOrCreate(resource, identifier); + InteractorNode interactorNode = interactorNodes.getOrCreate(resource, identifier); for (MapSet prs : compressedResult.getElements(target)) { for (Long pathwayId : prs.keySet()) { interactorNode.addInteractsWith(pathwayId, target); @@ -120,12 +120,12 @@ public void build(Set speciesNodes, EntitiesContainer entities, Int ReferenceSequence rs = (ReferenceSequence) interactor; if (rs.getGeneName() != null) { for (String alias : rs.getGeneName()) { - interactorsMap.add(alias, resource, interactorNode); + interactorNodes.addAlias(alias, resource, interactorNode); } } if (rs.getSecondaryIdentifier() != null){ for (String si : rs.getSecondaryIdentifier()) { - if(!si.contains(" ")) interactorsMap.add(si, resource, interactorNode); + if(!si.contains(" ")) interactorNodes.addAlias(si, resource, interactorNode); } } } @@ -133,26 +133,10 @@ public void build(Set speciesNodes, EntitiesContainer entities, Int } } if (Main.VERBOSE) - System.out.println("\rInteractors container successfully created >> " + n + " interactors have been added to Reactome."); + System.out.println("\rInteractors container successfully created >> " + interactorNodes.created() + " interactors have been added to Reactome."); } public IdentifiersMap getInteractorsMap() { return interactorsMap; } - - private InteractorNode getOrCreate(Resource resource, String identifier) { - MapSet map = interactorsMap.get(identifier); - Set interactors = map.getElements(resource); - if (interactors == null || interactors.isEmpty()) { - InteractorNode interactorNode = new InteractorNode(identifier); - interactorsMap.add(identifier, resource, interactorNode); - n++; - return interactorNode; - } else { - //Using IdentifiersMap causes this "oddity" here, but is a minor inconvenient - if (interactors.size() > 1) - logger.error("Duplicate interactors found for " + identifier + " [" + resource.getName() + "]"); - return interactors.iterator().next(); - } - } } diff --git a/src/test/java/org/reactome/server/analysis/core/importer/InteractorNodesTest.java b/src/test/java/org/reactome/server/analysis/core/importer/InteractorNodesTest.java new file mode 100644 index 00000000..7fdf4b68 --- /dev/null +++ b/src/test/java/org/reactome/server/analysis/core/importer/InteractorNodesTest.java @@ -0,0 +1,56 @@ +package org.reactome.server.analysis.core.importer; + +import org.junit.Test; +import org.reactome.server.analysis.core.model.IdentifiersMap; +import org.reactome.server.analysis.core.model.InteractorNode; +import org.reactome.server.analysis.core.model.resource.Resource; +import org.reactome.server.analysis.core.model.resource.ResourceFactory; + +import java.util.Set; + +import static org.junit.Assert.*; + +public class InteractorNodesTest { + + private final Resource uniprot = ResourceFactory.getResource("UniProt"); + + @Test + public void aPartnerWhoseIdentifierIsAnotherProteinsAliasIsItsOwnInteractor() { + // Release 98: Q6NSH3 is a secondary identifier of P0DMU9, and an + // interaction partner of its own in the Neurofascin interactions. + IdentifiersMap map = new IdentifiersMap<>(); + InteractorNodes nodes = new InteractorNodes(map); + InteractorNode p0dmu9 = nodes.getOrCreate(uniprot, "P0DMU9"); + nodes.addAlias("Q6NSH3", uniprot, p0dmu9); + + InteractorNode q6nsh3 = nodes.getOrCreate(uniprot, "Q6NSH3"); + + assertNotSame(p0dmu9, q6nsh3); + assertEquals(2, nodes.created()); + // An analysis submitting Q6NSH3 finds both: the protein, and the one it + // is an alias of. + Set found = map.get("Q6NSH3").getElements(uniprot); + assertTrue(found.contains(p0dmu9)); + assertTrue(found.contains(q6nsh3)); + } + + @Test + public void aPrimaryIdentifierFindsItsInteractorWhateverTheCase() { + IdentifiersMap map = new IdentifiersMap<>(); + InteractorNodes nodes = new InteractorNodes(map); + InteractorNode first = nodes.getOrCreate(uniprot, "P12345"); + + assertSame(first, nodes.getOrCreate(uniprot, "p12345")); + assertSame(first, nodes.getOrCreate(uniprot, " P12345 ")); + assertEquals(1, nodes.created()); + } + + @Test + public void theSameIdentifierInAnotherResourceIsAnotherInteractor() { + InteractorNodes nodes = new InteractorNodes(new IdentifiersMap<>()); + Resource ensembl = ResourceFactory.getResource("ENSEMBL"); + + assertNotSame(nodes.getOrCreate(uniprot, "X1"), nodes.getOrCreate(ensembl, "X1")); + assertEquals(2, nodes.created()); + } +}