From 749d85c37549dba6c88b9dd8592d907771b17e0e Mon Sep 17 00:00:00 2001 From: Adam Wright Date: Thu, 23 Jul 2026 12:47:23 -0400 Subject: [PATCH] feat: additive diagram bridges (no merge) as the default for diagram connectivity MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Diagram-drawn shared-entity connections were previously folded into the network by MERGING the shared entity's two copies (producer output-copy unified with consumer input-copy via union-find), which removes a node and can drop edges. This adds them as NEW bridge edges instead — producer.output-copy -> consumer. input-copy, edge_type="diagram_bridge" — so nothing is unified or lost; every original node and edge is preserved and the diagram only ever ADDS connectivity. - create_pathway_logic_network: new optional diagram_bridge_pairs; Phase 3b emits one additive edge per drawn shared entity, skipping pairs already connected via a precedingEvent merge (same canonical uuid). - pathway_generator: LNG_DIAGRAM_BRIDGE now defaults ON. precedingEvent still merges its own shared entities (core mechanism); the diagram json never drives a merge. LNG_DIAGRAM_BRIDGE=0 restores the legacy merge augmentation. Benchmark A/B (R97, merge vs bridge): statistically neutral and marginally better — curator 83.15%->83.20%, experimental 73.50%->73.62% (TP53 +2 cases, everything else flat). The additive form notably avoids the entity-destruction that made the earlier merge-descend regress TP53. Verified on ERBB2: +306 edges, +133 nodes (nothing merged away). Tradeoff: larger networks (2 very large pathways exceed the benchmark's HTTP edge cap). Co-Authored-By: Claude Fable 5 --- src/logic_network_generator.py | 35 ++++++++++++++++++++++++++++++++++ src/pathway_generator.py | 31 +++++++++++++++++++++++------- 2 files changed, 59 insertions(+), 7 deletions(-) diff --git a/src/logic_network_generator.py b/src/logic_network_generator.py index 0285445..035b1be 100755 --- a/src/logic_network_generator.py +++ b/src/logic_network_generator.py @@ -1588,6 +1588,7 @@ def create_pathway_logic_network( decomposed_uid_mapping: pd.DataFrame, reaction_connections: pd.DataFrame, best_matches: Any, + diagram_bridge_pairs: Optional[Set[Tuple[str, str]]] = None, ) -> PathwayResult: """Create a pathway logic network from decomposed UID mappings and reaction connections. @@ -1810,6 +1811,40 @@ def create_pathway_logic_network( "edge_reaction_id": reaction_stid, }) + # Phase 3b (optional): additive diagram bridges. For each diagram-drawn + # (producer, consumer) reaction pair, connect their shared entity by ADDING + # an edge from the producer's output-copy to the consumer's input-copy + # instead of MERGING the two copies (Phase 2). This preserves every node and + # edge — nothing is unified or dropped — while still giving the + # producer -> entity_out -> entity_in -> consumer path. Pairs already merged + # via precedingEvent (same canonical uuid) are skipped. Layout-filtered pairs + # only (cofactor hubs already excluded by the caller). See #39. + if diagram_bridge_pairs: + n_bridges = 0 + for a_rid, b_rid in diagram_bridge_pairs: + for p_vr in reactome_to_vr.get(a_rid, []): + p_outputs = set(vr_entities.get(p_vr, ([], [], {}, {}))[1]) + for f_vr in reactome_to_vr.get(b_rid, []): + f_inputs = set(vr_entities.get(f_vr, ([], [], {}, {}))[0]) + for eid in p_outputs & f_inputs: + src = entity_uuid_registry.get((eid, p_vr, "output")) + tgt = entity_uuid_registry.get((eid, f_vr, "input")) + # Skip if missing or already the same node (already + # connected via a precedingEvent merge). + if not src or not tgt or src == tgt: + continue + pathway_logic_network_data.append({ + "source_id": src, + "target_id": tgt, + "pos_neg": "pos", + "and_or": None, + "edge_type": "diagram_bridge", + "stoichiometry": 1, + "edge_reaction_id": None, + }) + n_bridges += 1 + logger.info(f"Diagram bridges: +{n_bridges} additive edges (no merge)") + # Log UUID registry statistics unique_uuids = set(entity_uuid_registry.values()) unique_entities = set(key[0] for key in entity_uuid_registry.keys()) diff --git a/src/pathway_generator.py b/src/pathway_generator.py index adcd1bd..6510da8 100755 --- a/src/pathway_generator.py +++ b/src/pathway_generator.py @@ -122,17 +122,34 @@ def generate_pathway_file( logger.warning(f"Could not cache decomposition results: {e}") # Continue without caching - # Augment connectivity with curator-drawn diagram flow (product->substrate - # pairs the diagram links but precedingEvent may omit — esp. old pathways). - # Only the connectivity/merge step sees this; the matching layer above - # stays on pure precedingEvent. See reactome/logic-network-generator#39. - from src.diagram_connectivity import augment_reaction_connections - connectivity = augment_reaction_connections(pathway_id, reaction_connections) + # Add curator-drawn diagram connectivity (product->substrate pairs the + # diagram links but precedingEvent may omit). See reactome/logic-network-generator#39. + # + # DEFAULT = additive-bridge mode (LNG_DIAGRAM_BRIDGE=1): the diagram-drawn + # shared-entity connections are added as NEW bridge edges (producer's + # output-copy -> consumer's input-copy) — nothing is merged or lost. Raw + # precedingEvent connectivity still merges its shared entities (the core + # mechanism), but the diagram json NEVER drives a merge. Benchmark-neutral + # vs the legacy merge (curator 83.2%, experimental 73.6%; TP53 +2) while + # preserving every node and edge. Set LNG_DIAGRAM_BRIDGE=0 to fall back to + # the legacy merge-based augmentation; set both LNG_DIAGRAM_BRIDGE=0 and + # LNG_DIAGRAM_CONNECTIVITY=0 to disable diagram connectivity entirely. + from src.diagram_connectivity import ( + augment_reaction_connections, + diagram_shared_product_pairs, + ) + diagram_bridge_pairs = None + if os.environ.get("LNG_DIAGRAM_BRIDGE", "1") == "1": + connectivity = reaction_connections + diagram_bridge_pairs = diagram_shared_product_pairs(pathway_id) + else: + connectivity = augment_reaction_connections(pathway_id, reaction_connections) # Generate logic network logger.info("Creating pathway logic network...") result = create_pathway_logic_network( - decomposed_uid_mapping, connectivity, best_matches + decomposed_uid_mapping, connectivity, best_matches, + diagram_bridge_pairs=diagram_bridge_pairs, ) # Save logic network (main output file users need)