Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions PIPELINE_SETTINGS.md
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,7 @@ what NOT to touch. Overriding is for A/B benchmarking only.
| `LNG_RELEASE_POOLS` | `1` | a protein bound, modified and released by a complex, whose released copy carries on (beta-catenin and the destruction complex), as a free/bound pool whose released copy reads the free state; writes `released_twin` and `recycle` edges (deltasignal specs/086) |
| `LNG_UB_LIGASE_UNIT` | `1` | a ubiquitination does not deplete its own ligase: a leaf of the catalyst's curated `activeUnit`, or of a complex released unchanged, that leaves the reaction unmodified is not the substrate (CKS1B under SCF(Skp2); deltasignal specs/089) |
| `LNG_BALANCE_CHOICES` | `1` | drop a reaction copy whose chosen set members do not balance (its outputs gain, or its inputs lose, proteins) when better-balanced copies exist with the same outputs and with the same input choice: MDM2 alone no longer makes MDM2:MDM4, p-STAT6 no longer makes the STAT3 dimer (deltasignal specs/101) |
| `LNG_SUMMATION_REGULATIONS` | `1` | a curation supplement: three regulations Reactome states in a reaction's summation but not in its event graph, as structural rules in pathways holding the source reaction (RB1:E2F inhibits E2F1/2/3-stimulated transcription; AKT's p-T-p21/p27 inhibits Cip/Kip binding to cyclin:CDK; the p-Y-Cip/Kip CDK4/6 complex activates CDK4/6 catalysis). Each regulator is one pool of its variants. `0` builds Release 97 alone (deltasignal specs/102) |
| `LNG_SPECIALIZATION_BRIDGE` | `add` | join a generic complex of sets and the specific member combination Reactome curates under another id, ordered by `precedingEvent` (`specialization` OR edges; `replace` \| `off`; deltasignal specs/067) |
| `LNG_SPECIALIZATION_CLOSED` | `twin` | a specialization in-edge does not open a carrier loop (`off`; deltasignal specs/068) |
| `LNG_CATALYST_BUNDLE` | `0` | legacy; subsumed by `LNG_COMPLEX_AS_NODE` |
Expand Down
1 change: 1 addition & 0 deletions src/env_flags.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
"LNG_BOUNDARY_EXPANSION": True,
"LNG_BOUNDARY_HIERARCHY": True,
"LNG_BALANCE_CHOICES": True, # default since deltasignal specs/101 (held-out +30, exp -4; MDM4 KO inverts)
"LNG_SUMMATION_REGULATIONS": True, # default since deltasignal specs/102 (a curation supplement: exp +6, tuning +173, held-out 0)
"LNG_BIND_STOICH": True, # default since deltasignal specs/048 (homodimer binding; 0 predictions moved)
"LNG_CAP_POOLS": True, # default since deltasignal specs/045 (RAF experimental +14, held-out 0)
"LNG_CATALYST_BUNDLE": False,
Expand Down
12 changes: 12 additions & 0 deletions src/logic_network_generator.py
Original file line number Diff line number Diff line change
Expand Up @@ -2915,6 +2915,18 @@ def create_pathway_logic_network(
catalyst_map = get_catalysts_for_reaction(reaction_id_map, graph)
negative_regulator_map = get_negative_regulators_for_reaction(reaction_id_map, graph)
positive_regulator_map = get_positive_regulators_for_reaction(reaction_id_map, graph)
if env_flag("LNG_SUMMATION_REGULATIONS"):
from src.summation_regulations import summation_regulations
regs = summation_regulations(graph, list(reaction_id_map["reactome_id"].dropna().astype(str).unique()))
extra: Dict[str, List[dict]] = {"pos": [], "neg": []}
for _, row in reaction_id_map.iterrows():
for sign, ids in regs.get(str(row["reactome_id"]), {}).items():
for e in ids:
extra[sign].append({"reaction_id": row["reactome_id"], "entity_id": e,
"edge_type": "regulator", "uuid": str(uuid.uuid4()),
"reaction_uuid": row["uid"]})
positive_regulator_map = pd.concat([positive_regulator_map, pd.DataFrame(extra["pos"], columns=_CAT_REG_COLUMNS)], ignore_index=True)
negative_regulator_map = pd.concat([negative_regulator_map, pd.DataFrame(extra["neg"], columns=_CAT_REG_COLUMNS)], ignore_index=True)

# Print regulator statistics
_print_regulator_statistics(positive_regulator_map, negative_regulator_map, catalyst_map)
Expand Down
1 change: 1 addition & 0 deletions src/pathway_generator.py
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@
"LNG_POOL_SHARED",
"LNG_BIND_STOICH",
"LNG_BALANCE_CHOICES",
"LNG_SUMMATION_REGULATIONS",
"LNG_PATHOGEN_PROTEIN",
"LNG_VARIANT_NODES",
"LNG_DIAGRAM_SET_MEMBER",
Expand Down
72 changes: 72 additions & 0 deletions src/summation_regulations.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
"""LNG_SUMMATION_REGULATIONS (deltasignal specs/102): regulations a reaction's summation states.

Three Release 97 reactions in the cell cycle make a product no reaction uses, while
their summations say what it does (specs/101 T1, T3). Each rule below writes that
product as a regulator of the reactions the summation names, chosen by structure,
and only inside a pathway that holds both the source reaction and the target.

- E1: RB1:E2F (R-HSA-9018017's product) inhibits every reaction positively
regulated by an E2F1/2/3 entity. "RB1 binding inhibits transcriptional
activity of E2F1/2/3:TFDP1/2 complexes."
- E2: AKT's p-T-CDKN1A/B (R-HSA-198613) inhibits every binding of CDKN1A/B to a
CDK2/4/6 complex. "retention in the cytoplasm, segregating these CDK
inhibitors from cyclin-CDK complexes."
- E3: CDK4/6:CCND:p-Y-Cip/Kip (R-HSA-8942607) activates every reaction catalysed
by CDK4/6 with no other active unit. "increased catalytic activity of CDK4 or
CDK6."

This is a curation supplement to the release, not the release. On by default since
specs/102 (Adam, 2026-10-24); `LNG_SUMMATION_REGULATIONS=0` builds the release alone.
"""
from collections import Counter
from typing import Dict, List, Sequence

STATS: Counter = Counter()

_HOLDS = "-[:hasComponent|hasMember|hasCandidate*0..]->(:EntityWithAccessionedSequence)-[:referenceEntity]->"

# (id, source reaction, regulator, sign, Cypher returning target stIds among $rids)
RULES = [
("E1", "R-HSA-9018017", "R-HSA-68644", "neg",
"UNWIND $rids AS rid MATCH (r:ReactionLikeEvent {stId: rid})"
"-[:regulatedBy]->(:PositiveRegulation)-[:regulator]->(reg) "
f"MATCH (reg){_HOLDS}(g:ReferenceGeneProduct) WHERE g.geneName[0] IN ['E2F1', 'E2F2', 'E2F3'] "
"RETURN DISTINCT r.stId AS target"),
("E2", "R-HSA-198613", "R-HSA-198605", "neg",
"UNWIND $rids AS rid MATCH (r:ReactionLikeEvent {stId: rid})-[:input]->(a) "
f"MATCH (a){_HOLDS}(ga:ReferenceGeneProduct) WHERE ga.geneName[0] IN ['CDKN1A', 'CDKN1B'] "
"MATCH (r)-[:input]->(b) WHERE b <> a "
f"MATCH (b){_HOLDS}(gb:ReferenceGeneProduct) WHERE gb.geneName[0] IN ['CDK2', 'CDK4', 'CDK6'] "
"RETURN DISTINCT r.stId AS target"),
("E3", "R-HSA-8942607", "R-HSA-8942622", "pos",
"UNWIND $rids AS rid MATCH (r:ReactionLikeEvent {stId: rid})"
"-[:catalystActivity]->(ca)-[:physicalEntity]->(c) "
f"MATCH (c){_HOLDS}(g:ReferenceGeneProduct) WHERE g.geneName[0] IN ['CDK4', 'CDK6'] "
"OPTIONAL MATCH (ca)-[:activeUnit]->(u) "
"OPTIONAL MATCH (u)" + _HOLDS + "(gu:ReferenceGeneProduct) WHERE gu.geneName[0] IN ['CDK4', 'CDK6'] "
"WITH r, u, gu WHERE u IS NULL OR gu IS NOT NULL "
"RETURN DISTINCT r.stId AS target"),
]


def summation_regulations(graph, reaction_ids: Sequence[str]) -> Dict[str, Dict[str, List[str]]]:
"""target reaction stId -> {"pos": [regulator stIds], "neg": [...]} for the
pathway whose reactions are `reaction_ids`."""
STATS.clear()
rids = sorted(set(str(r) for r in reaction_ids))
held = set(rids)
out: Dict[str, Dict[str, List[str]]] = {}
for rule, source, regulator, sign, cypher in RULES:
if source not in held:
continue
for row in graph.run(cypher, rids=rids).data():
t = row["target"]
if t == source:
continue
lst = out.setdefault(t, {"pos": [], "neg": []})[sign]
if regulator not in lst:
lst.append(regulator)
STATS[rule] += 1
for d in out.values():
d["pos"].sort(); d["neg"].sort()
return out
14 changes: 14 additions & 0 deletions src/variant_emission.py
Original file line number Diff line number Diff line change
Expand Up @@ -355,6 +355,14 @@ def build_variant_reactions(graph, reaction_ids: Sequence[str]
STATS.clear()
cap = vk._max_variants()
parts_by_rx = fetch_participants(graph, reaction_ids)
# specs/102: a summation regulator is read as ONE pool of its variants, added
# after the copies are chosen, so it never multiplies them or pushes a
# reaction past the cap (as a curated participant it took the RBL1/RBL2
# phosphorylations over the cap and broke their recycle pools).
supplement: Dict[str, Dict[str, List[str]]] = {}
if env_flag("LNG_SUMMATION_REGULATIONS"):
from src.summation_regulations import summation_regulations
supplement = summation_regulations(graph, reaction_ids)
rows: List[dict] = []
vr_entities: Dict[str, tuple] = {}
cat_rows: List[dict] = []; neg_rows: List[dict] = []; pos_rows: List[dict] = []
Expand Down Expand Up @@ -398,6 +406,12 @@ def name(e, s=sig, pooled=pooled, output=False):
"edge_type": "catalyst" if target is cat_rows else "regulator",
"uuid": str(uuid.uuid4()), "reaction_uuid": uid,
"participant": e})
for sign, target in (("neg", neg_rows), ("pos", pos_rows)):
for e in supplement.get(rx, {}).get(sign, []):
target.append({"reaction_id": rx, "entity_id": e + POOL_SUFFIX,
"edge_type": "regulator", "uuid": str(uuid.uuid4()),
"reaction_uuid": uid, "participant": e})
STATS["summation_regulator_rows"] += 1
rid_map = pd.DataFrame(rows, columns=["uid", "reactome_id", "input_hash", "output_hash"])
mk = lambda r: pd.DataFrame(r, columns=_CAT_REG_COLUMNS) # noqa: E731
return rid_map, vr_entities, mk(cat_rows), mk(neg_rows), mk(pos_rows)
74 changes: 74 additions & 0 deletions tests/test_summation_regulations.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
"""deltasignal specs/102: regulations a reaction's summation states
(LNG_SUMMATION_REGULATIONS)."""
import pytest

import src.summation_regulations as sr


class _FakeGraph:
def __init__(self, targets):
self.targets = targets

def run(self, cypher, rids):
rule = next(r for r in sr.RULES if r[4] == cypher)[0]
rows = [{"target": t} for t in self.targets.get(rule, []) if t in rids]

class _R:
def data(self):
return rows
return _R()


def test_a_rule_applies_only_where_its_source_reaction_is():
g = _FakeGraph({"E1": ["R-HSA-TK1"], "E2": ["R-HSA-BIND"]})
# Mitotic G1 holds RB1 binding E2F (E1's source) but not AKT's p27 phosphorylation (E2's)
regs = sr.summation_regulations(g, ["R-HSA-9018017", "R-HSA-TK1", "R-HSA-BIND"])
assert regs == {"R-HSA-TK1": {"pos": [], "neg": ["R-HSA-68644"]}}
assert sr.STATS == {"E1": 1}


def test_signs_and_the_source_is_never_its_own_target():
g = _FakeGraph({"E2": ["R-HSA-198613", "R-HSA-BIND"], "E3": ["R-HSA-69227"]})
regs = sr.summation_regulations(g, ["R-HSA-198613", "R-HSA-8942607", "R-HSA-BIND", "R-HSA-69227"])
assert regs == {"R-HSA-BIND": {"pos": [], "neg": ["R-HSA-198605"]},
"R-HSA-69227": {"pos": ["R-HSA-8942622"], "neg": []}}


def test_flag_on_by_default(monkeypatch):
from src.env_flags import env_flag
monkeypatch.delenv("LNG_SUMMATION_REGULATIONS", raising=False)
assert env_flag("LNG_SUMMATION_REGULATIONS") is True


@pytest.mark.database
def test_release_targets_in_mitotic_g1():
"""The registered targets (specs/102 spec.md), read from the database."""
from src.neo4j_connector import get_graph
graph = get_graph()
rids = [r["s"] for r in graph.run(
"MATCH (:Pathway {stId: 'R-HSA-453279'})-[:hasEvent*]->(r:ReactionLikeEvent) "
"RETURN DISTINCT r.stId AS s").data()]
regs = sr.summation_regulations(graph, rids)
e1 = [t for t, d in regs.items() if "R-HSA-68644" in d["neg"]]
assert len(e1) == 15
assert sorted(t for t, d in regs.items() if "R-HSA-198605" in d["neg"]) == ["R-HSA-69562", "R-HSA-8941915"]
assert sorted(t for t, d in regs.items() if "R-HSA-8942622" in d["pos"]) == [
"R-HSA-1226094", "R-HSA-1226095", "R-HSA-69227"]


def test_a_summation_regulator_is_one_pool_and_never_multiplies_copies(monkeypatch):
"""specs/102: as a curated participant the regulator's set choices multiplied the
copies and took the RBL1/RBL2 phosphorylations past the cap."""
import src.variant_emission as ve
from tests.test_variant_emission import _mdm_world
p = _mdm_world(monkeypatch)
monkeypatch.setattr(ve, "fetch_participants", lambda g, ids: {"RX": p})
monkeypatch.setattr(sr, "summation_regulations", lambda g, ids: {"RX": {"pos": [], "neg": ["REG"]}})
monkeypatch.setenv("LNG_BALANCE_CHOICES", "0")
monkeypatch.setenv("LNG_SUMMATION_REGULATIONS", "0")
rid0, *_ = ve.build_variant_reactions(None, ["RX"])
monkeypatch.setenv("LNG_SUMMATION_REGULATIONS", "1")
rid1, _, _, neg, pos = ve.build_variant_reactions(None, ["RX"])
assert len(rid1) == len(rid0) == 4
assert sorted(neg["entity_id"]) == ["REG::pool"] * 4 and len(pos) == 0
assert set(neg["reaction_uuid"]) == set(rid1["uid"])
Loading