causalphylo 0.1.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,79 @@
1
+ # Python-generated files
2
+ __pycache__/
3
+ *.py[oc]
4
+ build/
5
+ dist/
6
+ wheels/
7
+ *.egg-info
8
+
9
+ # Virtual environments
10
+ .venv
11
+
12
+ # Environment files (not .env.example)
13
+ .env
14
+ .env.local
15
+ .env.production
16
+
17
+ # IDE
18
+ .idea/
19
+ .vscode/
20
+ *.swp
21
+ *.swo
22
+ *~
23
+
24
+ # OS
25
+ .DS_Store
26
+ Thumbs.db
27
+
28
+ # Test / coverage
29
+ .pytest_cache/
30
+ htmlcov/
31
+ .coverage
32
+ .coverage.*
33
+ coverage.xml
34
+
35
+ # Generated outputs
36
+ reports/
37
+ results/
38
+
39
+ # Molecular data caches
40
+ modules/dockbot/cache/
41
+ *.pdb
42
+ *.pdbqt
43
+ *.sdf
44
+ *.mol2
45
+
46
+ # Jupyter
47
+ .ipynb_checkpoints/
48
+
49
+ # Model weights (too large for git)
50
+ *.pt
51
+ *.pth
52
+ *.h5
53
+ *.pkl
54
+ *.ckpt
55
+
56
+ # Shipped model asset — must be committed and packaged (see pyproject artifacts)
57
+ !src/neorx/genmol/assets/*.pt
58
+
59
+ # ChEMBL SQLite database (28 GB — too large for git)
60
+ chembl_*.db
61
+
62
+ # Local backups and trained-model checkpoints -- large and machine-specific,
63
+ # never meant to be committed (a `git add -A` would otherwise pull in tens
64
+ # of MB of these).
65
+ .backup-scripts/
66
+ checkpoints/
67
+
68
+ # Experiment run records are deliverables, not scratch -- they are the
69
+ # evidence behind every reported number, IF they are cited. See
70
+ # docs/run-manifest.toml. Default to NOT tracking a run directory (so a
71
+ # routine `git add -A` cannot sweep in an ad-hoc or smoke run); a record
72
+ # that actually backs a claim is added deliberately with
73
+ # `git add -f runs/<run-id>`.
74
+ !runs/
75
+ runs/*
76
+ !runs/.gitkeep
77
+
78
+ # MkDocs build output
79
+ site/
@@ -0,0 +1,49 @@
1
+ # Changelog
2
+
3
+ All notable changes to CausalPhylo will be documented in this file.
4
+
5
+ The format is based on [Keep a Changelog](https://keepachangelog.com/),
6
+ and this project adheres to [Semantic Versioning](https://semver.org/).
7
+
8
+ ## [Unreleased]
9
+
10
+ ## [0.1.0] — 2026-10-17
11
+
12
+ First release as a standalone package.
13
+
14
+ `graph_semantics` and `backdoor` were written inside
15
+ [NeoRx](https://github.com/Neoforge-Labs/neorx) and extracted here with their
16
+ commit history. They were the part of NeoRx's causal code that depended on
17
+ nothing but NetworkX, while NeoRx itself imported admissibility back out of
18
+ them — a cycle one distribution can hold and two cannot. So the boundary was
19
+ drawn where the dependency graph allows a real one.
20
+
21
+ ### Added
22
+
23
+ - `causalphylo.graph_semantics` — `is_causal_admissible`, `causal_subgraph`,
24
+ `cyclic_components`, `acyclic_core`, and the `CAUSAL_EDGE_TYPES` /
25
+ `GENETIC_EVIDENCE_CLASSES` vocabularies. Which edges may carry a causal
26
+ claim, and a DAG carved out of a graph with feedback loops that names what
27
+ it removed.
28
+ - `causalphylo.backdoor` — `find_adjustment_set`, `satisfies_backdoor`,
29
+ `mutilated_graph`, `confounding_sensitivity`, and the `Identification` /
30
+ `IdentificationReason` verdict types. Pearl's backdoor criterion, reporting
31
+ *why* identification failed as a closed enum rather than as prose, and
32
+ carrying `n_near_miss_confounders` so a trivial verdict cannot be mistaken
33
+ for an absence of confounding.
34
+ - Documentation at [causalphylo.neoforgelabs.tech](https://causalphylo.neoforgelabs.tech/).
35
+ Every `>>>` example in it runs as part of the test suite.
36
+
37
+ ### Changed from the NeoRx original
38
+
39
+ Behaviour is unchanged. Three things were cleaned up rather than carried:
40
+
41
+ - `IdentificationReason` is a `StrEnum` instead of inheriting `str` and `Enum`
42
+ separately. Member values and identity comparisons are identical; every
43
+ consumer read `.value` or compared with `is`, which was checked before the
44
+ change and is covered by NeoRx's own suite.
45
+ - A test asserting that `Identification` is immutable caught a blind
46
+ `Exception`, which a typo would also have satisfied. It names
47
+ `FrozenInstanceError` now.
48
+ - An unused import of `confounding_sensitivity` is gone. Both of its tests
49
+ reach it through `find_adjustment_set`; it still has no direct unit test.
@@ -0,0 +1,31 @@
1
+ CausalPhylo Source Available License
2
+
3
+ Copyright (c) 2026 Kelyn Paul Njeri, NeoForge Labs
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to use,
7
+ copy, modify, and distribute the Software for NON-COMMERCIAL PURPOSES ONLY,
8
+ subject to the following conditions:
9
+
10
+ 1. NON-COMMERCIAL USE PERMITTED. You may use, copy, modify, merge, and
11
+ distribute the Software and derivative works thereof for personal,
12
+ academic, educational, and research purposes at no charge.
13
+
14
+ 2. COMMERCIAL USE PROHIBITED WITHOUT LICENCE. You may NOT use the Software,
15
+ or any derivative works thereof, for commercial purposes — including but
16
+ not limited to: selling the Software or services built on it, incorporating
17
+ it into commercial products, or using it to generate revenue — without
18
+ obtaining a separate commercial licence from the copyright holder.
19
+
20
+ To obtain a commercial licence, contact: kelyn@neoforgelabs.tech
21
+
22
+ 3. ATTRIBUTION. The above copyright notice and this permission notice shall
23
+ be included in all copies or substantial portions of the Software.
24
+
25
+ 4. NO WARRANTY. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY
26
+ KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
27
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN
28
+ NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,
29
+ DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
30
+ OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE
31
+ USE OR OTHER DEALINGS IN THE SOFTWARE.
@@ -0,0 +1,160 @@
1
+ Metadata-Version: 2.5
2
+ Name: causalphylo
3
+ Version: 0.1.0
4
+ Summary: Pearl's backdoor criterion over knowledge graphs — separates edges that may carry a causal claim from those that only assert co-occurrence
5
+ Project-URL: Homepage, https://github.com/Neoforge-Labs/causalphylo
6
+ Project-URL: Documentation, https://causalphylo.neoforgelabs.tech/
7
+ Project-URL: Repository, https://github.com/Neoforge-Labs/causalphylo
8
+ Project-URL: Issues, https://github.com/Neoforge-Labs/causalphylo/issues
9
+ Author: Kelyn Paul Njeri
10
+ License-Expression: LicenseRef-CausalPhylo-NonCommercial
11
+ License-File: LICENSE
12
+ Keywords: backdoor-criterion,causal-inference,do-calculus,identification,knowledge-graph,networkx,pearl
13
+ Classifier: Development Status :: 4 - Beta
14
+ Classifier: Intended Audience :: Science/Research
15
+ Classifier: Operating System :: OS Independent
16
+ Classifier: Programming Language :: Python :: 3
17
+ Classifier: Programming Language :: Python :: 3.12
18
+ Classifier: Programming Language :: Python :: 3.13
19
+ Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
20
+ Classifier: Topic :: Scientific/Engineering :: Bio-Informatics
21
+ Classifier: Typing :: Typed
22
+ Requires-Python: >=3.12
23
+ Requires-Dist: networkx>=3.3
24
+ Description-Content-Type: text/markdown
25
+
26
+ # CausalPhylo
27
+
28
+ > Identification over knowledge graphs, where not every edge has earned its arrow.
29
+
30
+ [![Python 3.12+](https://img.shields.io/badge/python-3.12%2B-blue.svg)](https://python.org)
31
+ [![License](https://img.shields.io/badge/license-Non--Commercial-orange.svg)](LICENSE)
32
+ [![Docs](https://img.shields.io/badge/docs-causalphylo.neoforgelabs.tech-teal.svg)](https://causalphylo.neoforgelabs.tech/)
33
+
34
+ **CausalPhylo** implements Pearl's backdoor criterion over
35
+ [NetworkX](https://networkx.org) directed graphs, together with the
36
+ admissibility rules that decide which edges may carry a causal claim in the
37
+ first place. It was extracted from [NeoRx](https://github.com/Neoforge-Labs/neorx),
38
+ where it is the engine behind causal drug-target identification.
39
+
40
+ It has one runtime dependency: NetworkX.
41
+
42
+ **Full documentation: [causalphylo.neoforgelabs.tech](https://causalphylo.neoforgelabs.tech/)**
43
+
44
+ ## The problem it solves
45
+
46
+ A graph assembled from public sources mixes two kinds of edge.
47
+
48
+ Some assert a **mechanism with a direction** — a curated regulatory
49
+ interaction where someone has established that A acts on B. Others assert only
50
+ that two things **occur together**: a literature co-mention, membership of a
51
+ shared pathway, an undirected protein interaction whose arrow exists only
52
+ because one protein landed in the first column of an API response.
53
+
54
+ Running do-calculus over the second kind assumes the very thing a causal
55
+ method is supposed to establish. So CausalPhylo partitions the edges first,
56
+ and identification runs only on the causal-admissible subgraph. Associational
57
+ edges are not discarded — they stop being arrows.
58
+
59
+ Gene → disease edges are admitted only on genetic evidence. A germline variant
60
+ is randomised at conception, so a genetic association carries a
61
+ natural-experiment warrant that a co-mention does not — the Mendelian
62
+ randomisation argument ([Davey Smith & Ebrahim, 2003](https://doi.org/10.1093/ije/dyg070),
63
+ *Int J Epidemiol* 32:1–22). That warrant is what earns the arrow.
64
+
65
+ ## Install
66
+
67
+ ```bash
68
+ uv pip install causalphylo
69
+ ```
70
+
71
+ ## Use
72
+
73
+ ```python
74
+ import networkx as nx
75
+ from causalphylo import find_adjustment_set
76
+
77
+ # The textbook confounding shape: U is a common cause of X and D.
78
+ G = nx.DiGraph()
79
+ G.add_edge("U", "X", edge_type="activates", evidence_class="regulatory")
80
+ G.add_edge("U", "D", edge_type="associated_with", evidence_class="genetic_association")
81
+ G.add_edge("X", "D", edge_type="associated_with", evidence_class="genetic_association")
82
+
83
+ verdict = find_adjustment_set(G, "X", "D")
84
+
85
+ verdict.identifiable # True
86
+ verdict.adjustment_set # ('U',)
87
+ verdict.reason.value # 'identifiable_by_adjustment'
88
+ ```
89
+
90
+ `find_adjustment_set` takes the **full** graph. Restriction to the admissible
91
+ subgraph and to its acyclic core happens inside, so no caller can forget it.
92
+
93
+ Edges carry two attributes that the admissibility rules read: `edge_type` and
94
+ `evidence_class`. Nothing else about your graph is assumed — CausalPhylo has no
95
+ domain model of its own.
96
+
97
+ ## It tells you *why* it failed
98
+
99
+ A method that reports only pass/fail gives you a pile of prose to read. These
100
+ reasons are a closed enum, so a run's failures aggregate into a distribution:
101
+
102
+ | `reason` | Meaning |
103
+ |---|---|
104
+ | `identifiable_by_adjustment` | A valid backdoor adjustment set was found |
105
+ | `identifiable_trivially` | No confounder appears in the graph (see the caveat below) |
106
+ | `no_causal_path` | No admissible directed path from treatment to outcome |
107
+ | `cyclic_component` | Treatment or outcome sits in a feedback loop; d-separation is undefined there |
108
+ | `no_valid_adjustment_set` | Confounders exist, but no admissible subset blocks every backdoor path |
109
+ | `treatment_absent` / `outcome_absent` | Node is not in the admissible subgraph |
110
+
111
+ **The trivial case is not a clean bill of health.** A knowledge graph is
112
+ open-world: a missing edge is missing knowledge, not evidence of no
113
+ confounding. So a trivial verdict carries `n_near_miss_confounders` — how many
114
+ nodes regulate the treatment and have *some* association with the outcome that
115
+ fell short of admissibility. A target with many near misses is fragile. This is
116
+ the same move as an E-value in observational epidemiology: not "there is no
117
+ confounding", but "here is how much it would take to break this".
118
+
119
+ `search_truncated` reports when the subset search hit its bound
120
+ (`MAX_ADJUSTMENT_SET_SIZE`, `MAX_CANDIDATE_POOL`) rather than exhausting the
121
+ space. It is never hidden.
122
+
123
+ ## API
124
+
125
+ **`causalphylo.graph_semantics`** — which edges may carry a causal claim
126
+
127
+ | | |
128
+ |---|---|
129
+ | `is_causal_admissible(attrs)` | Whether an edge with these attributes may carry a causal claim |
130
+ | `causal_subgraph(G)` | The subgraph of admissible edges, attributes preserved |
131
+ | `cyclic_components(G)` | Strongly connected components with cycles, self-loops included |
132
+ | `acyclic_core(G)` | A DAG carved out of a cyclic graph, plus the nodes removed |
133
+ | `CAUSAL_EDGE_TYPES`, `GENETIC_EVIDENCE_CLASSES` | The admissibility vocabularies |
134
+
135
+ **`causalphylo.backdoor`** — identification on the admissible subgraph
136
+
137
+ | | |
138
+ |---|---|
139
+ | `find_adjustment_set(G, treatment, outcome)` | Search for a minimal valid adjustment set |
140
+ | `satisfies_backdoor(G, treatment, outcome, adjustment)` | Whether `adjustment` (a frozenset) satisfies the criterion |
141
+ | `mutilated_graph(G, treatment)` | G with edges *into* the treatment removed — the interventional graph G<sub>X&#772;</sub> |
142
+ | `confounding_sensitivity(dag, full_graph, treatment, outcome)` | Near-miss confounder count behind a trivial verdict |
143
+
144
+ Cyclic nodes are not silently repaired. `acyclic_core` removes every node in a
145
+ feedback loop and names them in `excluded`, because `nx.is_d_separator` requires
146
+ a DAG of the whole graph — not merely of the neighbourhood under test.
147
+
148
+ ## Development
149
+
150
+ ```bash
151
+ uv venv && uv pip install -e . --group dev
152
+ .venv/bin/python -m pytest
153
+ ```
154
+
155
+ ## Licence
156
+
157
+ CausalPhylo Source Available License — non-commercial use permitted, commercial
158
+ use requires a separate licence. See [LICENSE](LICENSE).
159
+
160
+ Copyright (c) 2026 Kelyn Paul Njeri, NeoForge Labs.
@@ -0,0 +1,135 @@
1
+ # CausalPhylo
2
+
3
+ > Identification over knowledge graphs, where not every edge has earned its arrow.
4
+
5
+ [![Python 3.12+](https://img.shields.io/badge/python-3.12%2B-blue.svg)](https://python.org)
6
+ [![License](https://img.shields.io/badge/license-Non--Commercial-orange.svg)](LICENSE)
7
+ [![Docs](https://img.shields.io/badge/docs-causalphylo.neoforgelabs.tech-teal.svg)](https://causalphylo.neoforgelabs.tech/)
8
+
9
+ **CausalPhylo** implements Pearl's backdoor criterion over
10
+ [NetworkX](https://networkx.org) directed graphs, together with the
11
+ admissibility rules that decide which edges may carry a causal claim in the
12
+ first place. It was extracted from [NeoRx](https://github.com/Neoforge-Labs/neorx),
13
+ where it is the engine behind causal drug-target identification.
14
+
15
+ It has one runtime dependency: NetworkX.
16
+
17
+ **Full documentation: [causalphylo.neoforgelabs.tech](https://causalphylo.neoforgelabs.tech/)**
18
+
19
+ ## The problem it solves
20
+
21
+ A graph assembled from public sources mixes two kinds of edge.
22
+
23
+ Some assert a **mechanism with a direction** — a curated regulatory
24
+ interaction where someone has established that A acts on B. Others assert only
25
+ that two things **occur together**: a literature co-mention, membership of a
26
+ shared pathway, an undirected protein interaction whose arrow exists only
27
+ because one protein landed in the first column of an API response.
28
+
29
+ Running do-calculus over the second kind assumes the very thing a causal
30
+ method is supposed to establish. So CausalPhylo partitions the edges first,
31
+ and identification runs only on the causal-admissible subgraph. Associational
32
+ edges are not discarded — they stop being arrows.
33
+
34
+ Gene → disease edges are admitted only on genetic evidence. A germline variant
35
+ is randomised at conception, so a genetic association carries a
36
+ natural-experiment warrant that a co-mention does not — the Mendelian
37
+ randomisation argument ([Davey Smith & Ebrahim, 2003](https://doi.org/10.1093/ije/dyg070),
38
+ *Int J Epidemiol* 32:1–22). That warrant is what earns the arrow.
39
+
40
+ ## Install
41
+
42
+ ```bash
43
+ uv pip install causalphylo
44
+ ```
45
+
46
+ ## Use
47
+
48
+ ```python
49
+ import networkx as nx
50
+ from causalphylo import find_adjustment_set
51
+
52
+ # The textbook confounding shape: U is a common cause of X and D.
53
+ G = nx.DiGraph()
54
+ G.add_edge("U", "X", edge_type="activates", evidence_class="regulatory")
55
+ G.add_edge("U", "D", edge_type="associated_with", evidence_class="genetic_association")
56
+ G.add_edge("X", "D", edge_type="associated_with", evidence_class="genetic_association")
57
+
58
+ verdict = find_adjustment_set(G, "X", "D")
59
+
60
+ verdict.identifiable # True
61
+ verdict.adjustment_set # ('U',)
62
+ verdict.reason.value # 'identifiable_by_adjustment'
63
+ ```
64
+
65
+ `find_adjustment_set` takes the **full** graph. Restriction to the admissible
66
+ subgraph and to its acyclic core happens inside, so no caller can forget it.
67
+
68
+ Edges carry two attributes that the admissibility rules read: `edge_type` and
69
+ `evidence_class`. Nothing else about your graph is assumed — CausalPhylo has no
70
+ domain model of its own.
71
+
72
+ ## It tells you *why* it failed
73
+
74
+ A method that reports only pass/fail gives you a pile of prose to read. These
75
+ reasons are a closed enum, so a run's failures aggregate into a distribution:
76
+
77
+ | `reason` | Meaning |
78
+ |---|---|
79
+ | `identifiable_by_adjustment` | A valid backdoor adjustment set was found |
80
+ | `identifiable_trivially` | No confounder appears in the graph (see the caveat below) |
81
+ | `no_causal_path` | No admissible directed path from treatment to outcome |
82
+ | `cyclic_component` | Treatment or outcome sits in a feedback loop; d-separation is undefined there |
83
+ | `no_valid_adjustment_set` | Confounders exist, but no admissible subset blocks every backdoor path |
84
+ | `treatment_absent` / `outcome_absent` | Node is not in the admissible subgraph |
85
+
86
+ **The trivial case is not a clean bill of health.** A knowledge graph is
87
+ open-world: a missing edge is missing knowledge, not evidence of no
88
+ confounding. So a trivial verdict carries `n_near_miss_confounders` — how many
89
+ nodes regulate the treatment and have *some* association with the outcome that
90
+ fell short of admissibility. A target with many near misses is fragile. This is
91
+ the same move as an E-value in observational epidemiology: not "there is no
92
+ confounding", but "here is how much it would take to break this".
93
+
94
+ `search_truncated` reports when the subset search hit its bound
95
+ (`MAX_ADJUSTMENT_SET_SIZE`, `MAX_CANDIDATE_POOL`) rather than exhausting the
96
+ space. It is never hidden.
97
+
98
+ ## API
99
+
100
+ **`causalphylo.graph_semantics`** — which edges may carry a causal claim
101
+
102
+ | | |
103
+ |---|---|
104
+ | `is_causal_admissible(attrs)` | Whether an edge with these attributes may carry a causal claim |
105
+ | `causal_subgraph(G)` | The subgraph of admissible edges, attributes preserved |
106
+ | `cyclic_components(G)` | Strongly connected components with cycles, self-loops included |
107
+ | `acyclic_core(G)` | A DAG carved out of a cyclic graph, plus the nodes removed |
108
+ | `CAUSAL_EDGE_TYPES`, `GENETIC_EVIDENCE_CLASSES` | The admissibility vocabularies |
109
+
110
+ **`causalphylo.backdoor`** — identification on the admissible subgraph
111
+
112
+ | | |
113
+ |---|---|
114
+ | `find_adjustment_set(G, treatment, outcome)` | Search for a minimal valid adjustment set |
115
+ | `satisfies_backdoor(G, treatment, outcome, adjustment)` | Whether `adjustment` (a frozenset) satisfies the criterion |
116
+ | `mutilated_graph(G, treatment)` | G with edges *into* the treatment removed — the interventional graph G<sub>X&#772;</sub> |
117
+ | `confounding_sensitivity(dag, full_graph, treatment, outcome)` | Near-miss confounder count behind a trivial verdict |
118
+
119
+ Cyclic nodes are not silently repaired. `acyclic_core` removes every node in a
120
+ feedback loop and names them in `excluded`, because `nx.is_d_separator` requires
121
+ a DAG of the whole graph — not merely of the neighbourhood under test.
122
+
123
+ ## Development
124
+
125
+ ```bash
126
+ uv venv && uv pip install -e . --group dev
127
+ .venv/bin/python -m pytest
128
+ ```
129
+
130
+ ## Licence
131
+
132
+ CausalPhylo Source Available License — non-commercial use permitted, commercial
133
+ use requires a separate licence. See [LICENSE](LICENSE).
134
+
135
+ Copyright (c) 2026 Kelyn Paul Njeri, NeoForge Labs.
@@ -0,0 +1 @@
1
+ causalphylo.neoforgelabs.tech
@@ -0,0 +1,132 @@
1
+ # Which edges earn an arrow
2
+
3
+ An arrow in a causal diagram is a claim: intervene on the tail and the head
4
+ responds. Most edges in a knowledge graph were never asserted with that
5
+ strength. CausalPhylo's first job is to say which ones were.
6
+
7
+ ## The rule
8
+
9
+ An edge is causal-admissible when either:
10
+
11
+ 1. its `edge_type` names a **directed mechanism** — someone has asserted that
12
+ the source acts on the target; or
13
+ 2. it is a gene-to-disease **association backed by genetic evidence**.
14
+
15
+ ```python
16
+ >>> from causalphylo import CAUSAL_EDGE_TYPES, GENETIC_EVIDENCE_CLASSES
17
+ >>> sorted(CAUSAL_EDGE_TYPES)
18
+ ['activates', 'causes', 'downregulates', 'inhibits', 'phosphorylates',
19
+ 'regulates', 'upregulates']
20
+ >>> sorted(GENETIC_EVIDENCE_CLASSES)
21
+ ['genetic_association', 'somatic_mutation']
22
+
23
+ ```
24
+
25
+ Everything else is associational.
26
+
27
+ ```python
28
+ from causalphylo import is_causal_admissible
29
+
30
+ is_causal_admissible({"edge_type": "activates", "evidence_class": "regulatory"})
31
+ is_causal_admissible({"edge_type": "associated_with", "evidence_class": "genetic_association"})
32
+ is_causal_admissible({"edge_type": "associated_with", "evidence_class": "literature"})
33
+ is_causal_admissible({"edge_type": "interacts_with", "evidence_class": "regulatory"})
34
+ is_causal_admissible({"edge_type": "participates_in", "evidence_class": ""})
35
+ ```
36
+
37
+ | Edge | Admissible |
38
+ |---|---|
39
+ | `activates` / `regulatory` | **True** |
40
+ | `associated_with` / `genetic_association` | **True** |
41
+ | `associated_with` / `literature` | False |
42
+ | `interacts_with` / `regulatory` (STRING) | False |
43
+ | `participates_in` (pathway membership) | False |
44
+
45
+ ## Why genetic evidence is the exception
46
+
47
+ A germline variant is randomised at conception. A genetic association
48
+ therefore carries a natural-experiment warrant that a co-mention does not —
49
+ this is the Mendelian randomisation argument
50
+ ([Davey Smith & Ebrahim, 2003](https://doi.org/10.1093/ije/dyg070),
51
+ *Int J Epidemiol* 32:1–22). That warrant is what earns the arrow.
52
+
53
+ A literature co-mention earns nothing. Two genes appear in one abstract for
54
+ reasons that include a shared assay, a shared author, and a shared review.
55
+
56
+ ## Why an undirected interaction earns nothing
57
+
58
+ !!! note "STRING interactions are not arrows"
59
+ A protein–protein interaction is symmetric. When it reaches you as a
60
+ directed edge, the direction is an artefact of which protein the API
61
+ returned first. Admitting it would let the ordering of a response
62
+ determine a causal conclusion.
63
+
64
+ ## Pruning the graph
65
+
66
+ `causal_subgraph` keeps the admissible edges with their attributes intact, and
67
+ drops nodes left with no admissible edge — a node participating in no causal
68
+ relation can be neither treatment, outcome, nor confounder.
69
+
70
+ ```python
71
+ import networkx as nx
72
+ from causalphylo import causal_subgraph
73
+
74
+ G = nx.DiGraph()
75
+ G.add_edge("U", "X", edge_type="activates", evidence_class="regulatory")
76
+ G.add_edge("X", "D", edge_type="associated_with", evidence_class="genetic_association")
77
+ G.add_edge("P", "D", edge_type="associated_with", evidence_class="literature")
78
+ G.add_edge("X", "pathway:1", edge_type="participates_in", evidence_class="")
79
+
80
+ sub = causal_subgraph(G)
81
+ ```
82
+
83
+ ```python
84
+ sorted(G.nodes()) # ['D', 'P', 'U', 'X', 'pathway:1']
85
+ sorted(sub.nodes()) # ['D', 'U', 'X']
86
+ sorted(sub.edges()) # [('U', 'X'), ('X', 'D')]
87
+ ```
88
+
89
+ `P` and `pathway:1` are gone. The association that linked `P` to `D` still
90
+ exists in your own graph and can still contribute to evidence scoring — it is
91
+ simply not an arrow here.
92
+
93
+ ## Feedback loops
94
+
95
+ `nx.is_d_separator` requires a DAG of the **whole** graph, not merely of the
96
+ neighbourhood under test. Biological regulatory networks are full of feedback,
97
+ so cyclic nodes have to come out — and `acyclic_core` names the ones it
98
+ removed rather than silently repairing them.
99
+
100
+ ```python
101
+ from causalphylo import acyclic_core
102
+
103
+ G = nx.DiGraph()
104
+ G.add_edge("a", "b") # a and b form a 2-cycle
105
+ G.add_edge("b", "a")
106
+ G.add_edge("b", "c")
107
+ G.add_edge("c", "d")
108
+
109
+ core = acyclic_core(G)
110
+ ```
111
+
112
+ ```python
113
+ sorted(core.excluded) # ['a', 'b']
114
+ sorted(core.dag.nodes()) # ['c', 'd']
115
+ nx.is_directed_acyclic_graph(core.dag) # True
116
+ ```
117
+
118
+ Self-loops count as cycles. A node that regulates itself is excluded on the
119
+ same grounds.
120
+
121
+ Identification involving an excluded node reports
122
+ `cyclic_component` rather than guessing an acyclic orientation. Choosing a
123
+ direction to break the loop would be inventing the mechanism the method is
124
+ supposed to test.
125
+
126
+ ## Adapting the vocabulary
127
+
128
+ The edge types and evidence classes above are frozensets of plain strings.
129
+ If your graph speaks a different vocabulary, map it to these names when you
130
+ build the graph. CausalPhylo deliberately has no configuration layer: the
131
+ admissibility rule is an argument about evidence, and burying it in a settings
132
+ file would make it look like a preference.
@@ -0,0 +1,54 @@
1
+ # API reference
2
+
3
+ Everything below is re-exported from the top level, so
4
+ `from causalphylo import find_adjustment_set` and
5
+ `from causalphylo.backdoor import find_adjustment_set` are equivalent.
6
+
7
+ ## causalphylo.graph_semantics
8
+
9
+ Which edges may carry a causal claim. See
10
+ [Which edges earn an arrow](admissibility.md).
11
+
12
+ ::: causalphylo.graph_semantics
13
+ options:
14
+ show_root_heading: false
15
+ members:
16
+ - is_causal_admissible
17
+ - causal_subgraph
18
+ - cyclic_components
19
+ - acyclic_core
20
+ - AcyclicCore
21
+
22
+ ### Vocabularies
23
+
24
+ `CAUSAL_EDGE_TYPES`
25
+ : Frozenset of `edge_type` values that assert a directed mechanism.
26
+
27
+ `GENETIC_EVIDENCE_CLASSES`
28
+ : Frozenset of `evidence_class` values that license a gene-to-disease arrow.
29
+
30
+ ## causalphylo.backdoor
31
+
32
+ Identification on the admissible subgraph. See
33
+ [Identification](identification.md).
34
+
35
+ ::: causalphylo.backdoor
36
+ options:
37
+ show_root_heading: false
38
+ members:
39
+ - find_adjustment_set
40
+ - Identification
41
+ - IdentificationReason
42
+ - satisfies_backdoor
43
+ - mutilated_graph
44
+ - confounding_sensitivity
45
+
46
+ ### Search bounds
47
+
48
+ `MAX_ADJUSTMENT_SET_SIZE`
49
+ : Largest adjustment set the search will consider. Default `3`.
50
+
51
+ `MAX_CANDIDATE_POOL`
52
+ : Largest candidate pool the search will draw from. Default `20`.
53
+
54
+ Reaching either bound sets `Identification.search_truncated`.